Page 1 of 5

[Windows] AutoComplete for StringGadgets

Posted: Wed Nov 22, 2006 1:39 am
by freak
I know there is a custom implementation by Xombie here: http://www.purebasic.fr/english/viewtopic.php?t=18310

This code however shows how to use the IAutoComplete interface provided by
windows to do this. It provides an in-place suggestion as well as a drop-down mode.
This requires Windows 2000 or Windows ME btw.

The strings need to be provided through an IEnumString interface.
I created an Includefile to create such an interface easily from an array of strings.
Note that this IEnumString implementation is independant of the rest, so if you need an IEnumString
for some other reason and happen to stumble upon this thread, you found your solution ;)

Example code:

Code: Select all

XIncludeFile "IEnumString.pb"

#ACO_NONE               = 0   ; No autocompletion.
#ACO_AUTOSUGGEST        = $1  ; Enable the autosuggest drop-down list.
#ACO_AUTOAPPEND	        = $2  ; Enable autoappend.
#ACO_SEARCH             = $4  ; Add a search item to the dropdown list of completed strings. If this is item is selected, you should launch a search engine to assist the user.
#ACO_FILTERPREFIXES   	= $8  ; Don't match common prefixes, such as "www.", "http://", and so on.
#ACO_USETAB	            = $10 ; Use the TAB key to select an item from the drop-down list. This flag is disabled by default.
#ACO_UPDOWNKEYDROPSLIST	= $20 ; Use the UP ARROW and DOWN ARROW keys to display the autosuggest drop-down list.
#ACO_RTLREADING	        = $40 ; If ACO_RTLREADING is set, the text is read in the opposite direction from the text in the parent window.


; -----------------------------------------------

#Window_0 = 0
#String_0 = 0

Dim Strings.s(17)
Strings(0)  = "Else"
Strings(1)  = "ElseIf"
Strings(2)  = "EnableDebugger"
Strings(3)  = "EnableExplicit"
Strings(4)  = "End"
Strings(5)  = "EndDataSection"
Strings(6)  = "EndEnumeration"
Strings(7)  = "EndIf"
Strings(8)  = "EndImport"
Strings(9)  = "EndInterface"
Strings(10) = "EndMacro"
Strings(11) = "EndProcedure"
Strings(12) = "EndSelect"
Strings(13) = "EndStructure"
Strings(14) = "EndStructureUnion"
Strings(15) = "EndWith"
Strings(16) = "Enumeration"

; Initialize the COM environment
CoInitialize_(0)

If OpenWindow(#Window_0, 323, 219, 600, 300, "AutoComplete",  #PB_Window_SystemMenu | #PB_Window_TitleBar | #PB_Window_ScreenCentered )
  If CreateGadgetList(WindowID(#Window_0))
    StringGadget(#String_0, 40, 40, 320, 20, "")
    SetActiveGadget(#String_0)
    
    ; This creates the autocomplete object:
    ;
    If CoCreateInstance_(?CLSID_AutoComplete, 0, 1, ?IID_IAutoComplete2, @AutoComplete.IAutoComplete2) = #S_OK    
    
      ; Lets change the options for autocomplete a bit:
      ; See the constant definitions above for the available options.
      ; There is a dropdown or in-place suggestion mode.
      ;
      AutoComplete\SetOptions(#ACO_AUTOSUGGEST|#ACO_USETAB)      
    
      ; This creates the IEnumString interface from our string array:
      ;
      Enum.IEnumString = New_EnumString(Strings(), 18)
      
      ; This sets the autocomplet with our Enum object.
      ; If you want more info on the last 2 parameters, read here:
      ; http://msdn.microsoft.com/library/default.asp?url=/library/en-us/shellcc/platform/shell/reference/ifaces/iautocomplete/Init.asp
      ; Note that these must be unicode strings if present!
      ;
      ; This also works for Comboboxes, you just need to get the handle to the edit
      ; control that is contained inside with the GetComboBoxInfo_() function.     
      ;
      AutoComplete\Init(GadgetID(#String_0), Enum, 0, 0)              
      
      Repeat
      Until WaitWindowEvent() = #PB_Event_CloseWindow
      
      ; Release our own references to the objects to avoid a memory leak
      ;
      Enum\Release()        
      AutoComplete\Release()      
    EndIf
  EndIf
EndIf


CoUninitialize_()

DataSection


  CLSID_AutoComplete: ; {00BB2763-6A77-11D0-A535-00C04FD7D062}
    Data.l $00BB2763
    Data.w $6A77, $11D0
    Data.b $A5, $35, $00, $C0, $4F, $D7, $D0, $62

  IID_IAutoComplete: ; {00BB2762-6A77-11D0-A535-00C04FD7D062}
    Data.l $00BB2762
    Data.w $6A77, $11D0
    Data.b $A5, $35, $00, $C0, $4F, $D7, $D0, $62

  IID_IAutoComplete2: ; {EAC04BC0-3791-11D2-BB95-0060977B464C}
    Data.l $EAC04BC0
    Data.w $3791, $11D2
    Data.b $BB, $95, $00, $60, $97, $7B, $46, $4C

EndDataSection
Includefile for IEnumString:

Code: Select all

;
;  Generic Implementation of an IEnumString Interface.
;  New_EnumString(StringArray(), ItemCount) will create a new IEnumString instance
;  with a referencecount of 1. (this means you must call IEnumString\Release()
;  to release this reference once you do not need it anymore.)
;
;
;


; To allow for the ::Clone() method without dublicating the buffer,
; the string buffer is kept separate, with its own reference count.
Structure EnumStringBuffer
  RefCount.l
  Strings.l[0]
  ; following is the literal string data
EndStructure

; IEnumString data
Structure EnumString
 *Vtbl
  RefCount.l
  StringCount.l
  Enumerator.l
 *Buffer.EnumStringBuffer
EndStructure


Procedure IEnumString_QueryInterface(*THIS.EnumString, *IID.IID, *Object.LONG)
  If *Object = 0
    ProcedureReturn #E_INVALIDARG
  ElseIf CompareMemory(*IID, ?IID_IUnknown, SizeOf(IID)) Or  CompareMemory(*IID, ?IID_IEnumString, SizeOf(IID))
    *Object\l = *THIS
    *THIS\RefCount + 1
    ProcedureReturn #S_OK
  Else
    *Object\l = 0
    ProcedureReturn #E_NOINTERFACE
  EndIf
EndProcedure


Procedure IEnumString_AddRef(*THIS.EnumString)
  *THIS\RefCount + 1
  ProcedureReturn *THIS\RefCount
EndProcedure


Procedure IEnumString_Release(*THIS.EnumString)
  *THIS\RefCount - 1
  
  If *THIS\RefCount = 0
    *THIS\Buffer\RefCount - 1
    If *THIS\Buffer\RefCount = 0
      FreeMemory(*THIS\Buffer)
    EndIf
    
    FreeMemory(*THIS)
    ProcedureReturn 0
  Else
    ProcedureReturn *THIS\RefCount
  EndIf
EndProcedure


Procedure IEnumString_Next(*THIS.EnumString, celt, *rgelt.LONG, *pceltFetched.LONG)
  If *THIS\Enumerator + celt <= *THIS\StringCount
    count = celt
  Else
    count = *THIS\StringCount - *THIS\Enumerator
  EndIf
  
  For i = 0 To count-1
    *rgelt\l = *THIS\Buffer\Strings[*THIS\Enumerator + i]
    *rgelt + 4
  Next i
  
  *THIS\Enumerator + count
  
  If *pceltFetcted
    *pceltFetched\l = count
  EndIf
  
  If count = celt
    ProcedureReturn #S_OK
  Else
    ProcedureReturn #S_FALSE
  EndIf 
EndProcedure


Procedure IEnumString_Skip(*THIS.EnumString, celt)
  *THIS\Enumerator + celt
  If *THIS\Enumerator <= *THIS\StringCount
    ProcedureReturn #S_OK
  Else
    ProcedureReturn #S_FALSE
  EndIf
EndProcedure


Procedure IEnumString_Reset(*THIS.EnumString)
  *THIS\Enumerator = 0
  ProcedureReturn #S_OK
EndProcedure


Procedure IEnumString_Clone(*THIS.EnumString, *ppenum.LONG) 
  If *ppenum = 0
    ProcedureReturn #E_INVALIDARG
  Else    
    *Clone.EnumString = AllocateMemory(SizeOf(EnumString))
    If *Clone
      CopyMemory(*THIS, *Clone, SizeOf(EnumString))
      *Clone\RefCount = 1
      *Clone\Buffer\RefCount + 1
      *ppenum\l = *Clone
      ProcedureReturn #S_OK
    Else
      *ppenum\l = 0
      ProcedureReturn #E_OUTOFMEMORY
    EndIf  
  EndIf
EndProcedure


Procedure New_EnumString(StringArray$(1), StringCount)
  *THIS.EnumString = 0

  Size = 4 + StringCount * 4
  For i = 0 To StringCount-1
    Size + Len(StringArray$(i)) * 2 + 2
  Next i

  *StringBuffer.EnumStringBuffer = AllocateMemory(Size)
  If *StringBuffer
    *StringBuffer\RefCount = 1
    *Pointer = *StringBuffer + 4 + StringCount * 4
    
    For i = 0 To StringCount - 1      
      *StringBuffer\Strings[i] = *Pointer  
      PokeS(*Pointer, StringArray$(i), -1, #PB_Unicode)
      *Pointer + Len(StringArray$(i)) * 2 + 2
    Next i
    
    *THIS = AllocateMemory(SizeOf(EnumString))
    If *THIS
      *THIS\Vtbl        = ?IEnumStringVtbl
      *THIS\RefCount    = 1
      *THIS\StringCount = StringCount
      *THIS\Enumerator  = 0
      *THIS\Buffer      = *StringBuffer
    Else
      FreeMemory(*StringBuffer)
    EndIf
  EndIf    

  ProcedureReturn *THIS
EndProcedure


DataSection

  IEnumStringVtbl:
    Data.l @IEnumString_QueryInterface()
    Data.l @IEnumString_AddRef()
    Data.l @IEnumString_Release()
    Data.l @IEnumString_Next()
    Data.l @IEnumString_Skip()
    Data.l @IEnumString_Reset()
    Data.l @IEnumString_Clone()
    
  IID_IUnknown: ; {00000000-0000-0000-C000-000000000046}
    Data.l $00000000
    Data.w $0000, $0000
    Data.b $C0, $00, $00, $00, $00, $00, $00, $46
    
  IID_IEnumString: ; {00000101-0000-0000-C000-000000000046}
    Data.l $00000101
    Data.w $0000, $0000
    Data.b $C0, $00, $00, $00, $00, $00, $00, $46

EndDataSection
[edit]
Oh well, just found a similar example by Justin: http://www.purebasic.fr/english/viewtopic.php?t=15627
Mine could still be usefull though, as the my IEnumString implementation
is more complete, with all methods implemented and all specified scenarios handled.
Its also simpler to use imho.

Posted: Wed Nov 22, 2006 5:39 am
by kawasaki
Very Useful. Thanks :)

Posted: Mon Jul 09, 2007 12:37 am
by Karbon
Great stuff!

Question, though..

Code: Select all

CoCreateInstance_(?CLSID_AutoComplete, 0, 1, ?IID_IAutoComplete2, @AutoComplete.IAutoComplete2)
What do the 0 and 1 correspond to? Gadget IDs?

Can a single object operate on multiple controls (with the same list of items to be autocompleted)?

Posted: Mon Jul 09, 2007 4:08 am
by netmaestro
For those parameters:

The second parameter is either #Null (as in this case) if the object is not being created as part of an aggregate, else it's a pointer to the aggregate object's IUnknown interface.

The third parameter is the execution context in which the object is to be run, in this case #CLSCTX_INPROC_SERVER = $1, which is part of the CLSCTX enumeration.

For multiple gadgets having the autocomplete enabled at once, I believe you'd need to create and configure a separate instance for each gadget you're using it on, though you shouldn't have any problem using the same string list for all if you so desired, ie New_EnumString() would only need to be called once.

Posted: Mon Jul 09, 2007 12:10 pm
by freak
netmaestro explained the parameters well.

You need a separate AutoComplete and Enumerator object for each gadget that uses autocomplete.
(the enumerator has an internal state which would be messed up if it is used by two gadgets)

You can however clone the enumerator:

Code: Select all

Enum.IEnumString = New_EnumString(StringArray(), Count)

If Enum\Clone(@NewEnum.IEnumString) = #S_OK
  ; there are 2 objects now
EndIf
In my implementation, a cloned enumerator shares the stringbuffer with the original,
so you get two independant objects with very little extra memory usage.
(The stringbuffer is freed when the last enumerator that uses it is released)

Posted: Mon Jul 09, 2007 2:52 pm
by Karbon
Excellent -- thanks!!

Posted: Thu Jul 12, 2007 11:00 pm
by Karbon
I released a version of kBilling with this implemented and have received a number of crash reports on this line :

*StringBuffer.EnumStringBuffer = AllocateMemory(Size)

There have been too many reports for it to be an issue where someone is really running out of memory (the last person had 3 GB of memory).

I am calling this code multiple times, if that could cause any conflicts (though I'm not cloning).

I know that's not much to go on but would you have any ideas?

Posted: Thu Jul 12, 2007 11:10 pm
by Karbon
Oh, I also tried to compile kBilling as threadsafe but backed that out when this problem arose. The problem seems to persist after I did that, though.

Posted: Thu Jul 12, 2007 11:19 pm
by freak
Well, the only reason that comes to mind is if you call the function with a negative "StringCount"
value, in which case the 'Size' variabe could become negative.
Can you check this ?

Otherwise i do not really know what might cause a crash at this line. There is really
not much about AllocateMemory() itself that could cause a crash.

Posted: Thu Jul 12, 2007 11:27 pm
by Karbon
Yea, that's what I was thinking -- I added a negative check to see.. Thanks!

Posted: Tue Jul 17, 2007 5:45 am
by Karbon
Nope, that wasn't it. Dozens more reports with a crash at the

Code: Select all

*StringBuffer.EnumStringBuffer = AllocateMemory(Size) 
And
*THIS = AllocateMemory(SizeOf(EnumString)) 
..lines from :

Code: Select all

Procedure New_EnumString(StringArray$(1), StringCount)
  *THIS.EnumString = 0
  
  Size = 4 + StringCount * 4
  For i = 0 To StringCount-1
    Size + Len(StringArray$(i)) * 2 + 2
  Next i
  
  *StringBuffer.EnumStringBuffer = AllocateMemory(Size)
  If *StringBuffer
    *StringBuffer\RefCount = 1
    *Pointer = *StringBuffer + 4 + StringCount * 4
    
    For i = 0 To StringCount - 1     
      *StringBuffer\Strings[i] = *Pointer 
      PokeS(*Pointer, StringArray$(i), -1, #PB_Unicode)
      *Pointer + Len(StringArray$(i)) * 2 + 2
    Next i
    
    *THIS = AllocateMemory(SizeOf(EnumString))
    If *THIS
      *THIS\Vtbl        = ?IEnumStringVtbl
      *THIS\RefCount    = 1
      *THIS\StringCount = StringCount
      *THIS\Enumerator  = 0
      *THIS\Buffer      = *StringBuffer
    Else
      FreeMemory(*StringBuffer)
    EndIf
  EndIf   
  
  ProcedureReturn *THIS
EndProcedure
It's crashing before I have a chance to test the result. Sizes are all reporting positive.. Especially the SizeOf(EnumString) one -- that's a constant! The call itself is causing the crash I guess.

I've tried both a thread safe and non-thread safe but got the same results. I haven't tried a Unicode compile due to some userlib issues.

Posted: Tue Jul 17, 2007 4:42 pm
by freak
The only way AllocateMemory() can crash is if the Memory Heap got invalid.
This happens when the heap control structures are overwritten by a wrong memory operation.
The problem is that these kinds of errors are very hard to trace, as the problem
can be somewhere totally else than the line at which it crashes.

I just fixed a bug in PokeS() which might be related, so please test with this new Memory lib first:
http://freak.purearea.net/v4/Memory

If this does not help, here is a small procedure/macro to test for Heap invalidation:

Code: Select all

Procedure ValidatePBHeap(LineNumber)
  Protected Heap
  !mov eax, dword [_PB_MemoryBase]
  !mov [p.v_Heap], eax
  If HeapValidate_(Heap, 0, 0) = 0
    MessageRequester("Error", "PB Memory Heap invalid at Line: "+Str(LineNumber))
  EndIf  
EndProcedure

Macro _validate
  ValidatePBHeap(#PB_Compiler_Line)
EndMacro
Place the _validate macro before the AllocateMemory() lines to confirm if the heap
is indeed invalid. If so, try to place the _validate macro in various places to
narrow down the place that causes the invalidation.

Sorry for the inconvinience, but there is no easier way to track such an error.
If notning else helps, i could provide you with a special build of the debugger
that checks heap validation for every executed source line. (its quite show then though)

btw, which PB version do you compile this with exactly ?

Posted: Tue Jul 17, 2007 4:47 pm
by Karbon
Thanks for the explanation! And no worries, I'll work to figure out what's going on.

PureBasic v4.02 is what I compiled with. I'll get the heap validation macros in there and see if I can get some users that were having trouble to test. The frustrating thing for me is that I can't reproduce the problem on any computer here (I've tried 6 different machines!).

I downloaded that memory lib. It went from 7k to up over 34k.. Was that expected?

Posted: Tue Jul 17, 2007 5:01 pm
by freak
> I downloaded that memory lib. It went from 7k to up over 34k.. Was that expected?

I accidently build an uncompressed PB library.
It makes no difference for the created executable though. It only affects how the compiler loads the lib.

Posted: Tue Jul 17, 2007 5:59 pm
by Trond
The first time I tested this I got an invalid memory access on the last line. But it was only once.

Edit:
Ok, with this code: (the example with the include file pasted at the top) http://pastebin.ca/623482
I get an invalid memory access at the last (empty) line of the file after I do this:
1. Run it
2. Type "else"
3. Enlarge the dropdown list (drag with mouse)
4. Press arrow down
5. Press enter
6. Press Alt+F4