Autocompleting Combobox and Strings
Posted: Mon Dec 19, 2005 8:43 pm
Here is an autocompletion routine for both the string gadget and combobox gadget. There are some slight bloats in variables since my own internal version has a few more things to it that are specific to my project. Feel free to remove what you like from this in order to lighten it up. The post text stuff could probably be safely removed, for example.
Anyway, string gadgets use string arrays for their completion. So do something like this...
And then set the autocompleting for the string gadget by simply doing...
...that. You can have different string arrays tied to different string gadgets or the same string array tied to different string gadgets. Whatever you like. Setting autocomplete for a combobox is done with the same procedure by calling...
...that. It will use the combobox's internal strings to autocomplete.
You can remove autocompletion during the program by calling
Or remove all autocompletion (for example, at the end of your program) by calling
Let me know if y'all want some other functionality built in to handle some other behavior. Or modify yourself 
Here's my test form - saved as "Main.pb"
And here's the autocomplete routines. I have this saved as "Autocomplete.pb" for my example. Yes, I know, big shock that it's not called "xComplete.pb", right? 
There it is. Let me know if you spot any bugs or need something added to it. Have fun ^_^ Now I'm off to make more code examples that refuse to work for gnozal! 
Anyway, string gadgets use string arrays for their completion. So do something like this...
Code: Select all
Dim Array01.s(10)
Array01(0) = "Jack"
Array01(1) = "and"
Array01(2) = "Jill"
Array01(3) = "went"
Array01(4) = "up"
Array01(5) = "the"
Array01(6) = "hill"
Array01(7) = "to"
Array01(8) = "fetch"
Array01(9) = "a"
Array01(10) = "pail"
Code: Select all
ac_SetAutocomplete(#StringTest, @Array01())
Code: Select all
ac_SetAutocomplete(#ComboTest, 0)
You can remove autocompletion during the program by calling
Code: Select all
ac_RemoveAutocomplete(#comboTest)
Code: Select all
ac_DestroyAutocomplete()

Here's my test form - saved as "Main.pb"
Code: Select all
; By Xombie - 12/12/2005
;
Enumeration ; Window List
#WindowMain
EndEnumeration
Enumeration ; Menu List
#MenuMain
EndEnumeration
Enumeration ; Control List
#ButtonTest
#StringTest
#StringTest2
#ComboTest
EndEnumeration
;- Global Variables
Dim Array01.s(10)
Dim array02.s(20)
;- Includes
XIncludeFile "Autocomplete.pb"
;- Main Program
DoQuit.b
;
If OpenWindow(#WindowMain, 100, 300, 300, 200, #PB_Window_SystemMenu | #PB_Window_MinimizeGadget | #PB_Window_MaximizeGadget, "Test")
;
If CreateGadgetList(WindowID())
AdvancedGadgetEvents(#True)
StringGadget(#StringTest, 0, 0, 100, 20, "")
StringGadget(#StringTest2, 0, 21, 100, 20, "")
ComboBoxGadget(#ComboTest, GadgetX(#StringTest2), GadgetY(#StringTest2) + GadgetHeight(#StringTest2) + 1, 100, 200, #PB_ComboBox_Editable)
ButtonGadget(#ButtonTest, GadgetX(#ComboTest), GadgetY(#ComboTest) + GadgetHeight(#ComboTest) + 1, 100, 20, "Test")
EndIf
;
;{ Create autocomplete lists
;
;/ Create items for the combobox.
AddGadgetItem(#ComboTest, -1, "One")
AddGadgetItem(#ComboTest, -1, "Two")
AddGadgetItem(#ComboTest, -1, "Three")
AddGadgetItem(#ComboTest, -1, "Four")
AddGadgetItem(#ComboTest, -1, "Five")
AddGadgetItem(#ComboTest, -1, "Six")
AddGadgetItem(#ComboTest, -1, "Seven")
AddGadgetItem(#ComboTest, -1, "Eight")
AddGadgetItem(#ComboTest, -1, "Nine")
AddGadgetItem(#ComboTest, -1, "Ten")
AddGadgetItem(#ComboTest, -1, "The")
AddGadgetItem(#ComboTest, -1, "quick")
AddGadgetItem(#ComboTest, -1, "brown")
AddGadgetItem(#ComboTest, -1, "fox")
AddGadgetItem(#ComboTest, -1, "jumped")
AddGadgetItem(#ComboTest, -1, "over")
AddGadgetItem(#ComboTest, -1, "the")
AddGadgetItem(#ComboTest, -1, "two")
AddGadgetItem(#ComboTest, -1, "lazy")
AddGadgetItem(#ComboTest, -1, "dogs")
;/ Create items for the first string
Array01(0) = "Jack"
Array01(1) = "and"
Array01(2) = "Jill"
Array01(3) = "went"
Array01(4) = "up"
Array01(5) = "the"
Array01(6) = "hill"
Array01(7) = "to"
Array01(8) = "fetch"
Array01(9) = "a"
Array01(10) = "pail"
;/ Create items for the second string
array02(0) = "Jack"
array02(1) = "Sprat"
array02(2) = "could"
array02(3) = "eat"
array02(4) = "no"
array02(5) = "fat"
array02(6) = "and"
array02(7) = "his"
array02(8) = "wife"
array02(9) = "could"
array02(10) = "eat"
array02(11) = "no"
array02(12) = "lean"
array02(13) = "but"
array02(14) = "between"
array02(15) = "the"
array02(16) = "both"
array02(17) = "of"
array02(18) = "them"
array02(19) = "they"
array02(20) = "licked"
;}
;
ac_SetAutocomplete(#StringTest, @Array01())
;
ac_SetAutocomplete(#StringTest2, @array02())
;
ac_SetAutocomplete(#ComboTest, 0)
;
Repeat
;
EventID.l = WaitWindowEvent()
;
If EventID = #PB_Event_CloseWindow ; If the user has pressed on the close button
;
DoQuit = #True
;
ElseIf EventID = #PB_Event_Gadget
;
If EventGadgetID() = #ButtonTest
;
If EventType() = #PB_EventType_LeftClick
;
;
EndIf
;
EndIf
;
EndIf
;
Until DoQuit = #True
EndIf
ac_DestroyAutocomplete()
; Remove all autocompleting items.
End

Code: Select all
;
;- Coded by Xombie 12/19/2005
;
;- Enumeration
Enumeration ; Control Type Enumeration
#ac_String
#ac_Combo
EndEnumeration
;- Structures
Structure s_ACStringStructure
FakeString.s[0]
EndStructure
Structure s_AutoComplete
;
Gadget.l
Handle.l
Parent.l
CallBack.l
; The old callback handle.
ArrayAddress.l
; Only valid for edit controls, this will be the address of the first element in an array of strings.
EndStructure
;- Global Variables
NewList _ac_Main.s_AutoComplete()
;- Helper Functions
Procedure.b ac_GetGadgetType(Handle.l)
; Return the gadget type based on the classname used in CreateWindowExW_()
HoldString.s
; This will store the length of the wide character string, in characters.
lCount.l
; Used to store the number of character copied.
HoldString = Space(255)
; Allocate size for our string.
GetClassName_(Handle, @HoldString, 255)
; Call our function to retrieve the classname.
If HoldString = "Edit"
ProcedureReturn #ac_String
ElseIf HoldString = "ComboBox"
ProcedureReturn #ac_Combo
EndIf
;
EndProcedure
Procedure.b ac_GadgetExists(Gadget.l)
;
ResetList(_ac_Main())
While NextElement(_ac_Main())
If _ac_Main()\Gadget = Gadget : ProcedureReturn #True : EndIf
Wend
;
ProcedureReturn #False
;
EndProcedure
Procedure.b ac_GetOldCallback(Handle.l)
;
ResetList(_ac_Main())
While NextElement(_ac_Main())
If _ac_Main()\Handle = Handle : ProcedureReturn _ac_Main()\CallBack : EndIf
Wend
;
ProcedureReturn -1
;
EndProcedure
;- Callback Functions
Procedure.l ac_HandleEditEvents(HandleWindow.l, Message.l, wParam.l, lParam.l)
; Custom callback for edit controls.
lResult.l
;
CallBack.l
;
Gadget.l
;
ArrayAddress.l
;
iLoop.l
;
HoldTotal.l
;
HoldLength.l
;
HoldCombinedLength.l
;
HoldStart.l
;
HoldEnd.l
;
HoldSelection.l
;
HoldString.s
;
PreText.s
;
PostText.s
;
CombinedText.s
;
MatchText.s
;
*Position.s_ACStringStructure
; This will be a pointer to the strings in the array.
CallBack = ac_GetOldCallback(HandleWindow)
; Return the old callback procedure address.
If CallBack = -1 : ProcedureReturn : EndIf
; This should never happen as this callback is only set for autocomplete items.
ResetList(_ac_Main())
While NextElement(_ac_Main())
If _ac_Main()\Handle = HandleWindow : ArrayAddress = _ac_Main()\ArrayAddress : Gadget = _ac_Main()\Gadget : Break : EndIf
Wend
;
HoldTotal = PeekL(ArrayAddress - 8)
; Return the number of items in the array.
If HoldTotal = 0 : ProcedureReturn : EndIf
; No need to complete if 0 items.
*Position = ArrayAddress
; Point to the first element in the array.
If Message = #WM_CHAR
;
HoldString = GetGadgetText(Gadget)
; Store the current text.
HoldLength = Len(HoldString)
;
SendMessage_(HandleWindow, #EM_GETSEL, @HoldStart, @HoldEnd)
; Store the start and end selection values.
PreText = Mid(HoldString, 1, HoldStart)
; The text before the selection.
PostText = Mid(HoldString, HoldEnd + 1, HoldLength - HoldEnd)
; The text after the selection.
CombinedText = LCase(PreText + PostText + Chr(wParam))
;
HoldCombinedLength = Len(CombinedText)
;
For iLoop = 0 To HoldTotal - 1
; Loop through all items in the array.
MatchText = PeekS(*Position\FakeString[iLoop])
; Store the text at index iLoop. This little trick of getting the items in a array by addresses is thanks (again) to freak ( http://forums.purebasic.com/english/viewtopic.php?t=15366 )
If LCase(Left(MatchText, HoldCombinedLength)) = CombinedText
; Found a matching item in the combobox.
SetGadgetText(Gadget, MatchText)
;
SendMessage_(HandleWindow, #EM_SETSEL, HoldCombinedLength, -1)
;
wParam = 0
;
Break
; Exit the loop.
EndIf
;
Next iLoop
;
If wParam <> 0 : lResult = CallWindowProc_(CallBack, HandleWindow, Message, wParam, lParam) : EndIf
;
Else
;
lResult = CallWindowProc_(CallBack, HandleWindow, Message, wParam, lParam)
;
EndIf
;
ProcedureReturn lResult
;
EndProcedure
Procedure.l ac_HandleComboEvents(HandleWindow.l, Message.l, wParam.l, lParam.l)
; Custom callback for combobox controls.
lResult.l
;
CallBack.l
;
Gadget.l
;
Parent.l
;
iLoop.l
;
HoldTotal.l
;
HoldLength.l
;
HoldCombinedLength.l
;
HoldStart.l
;
HoldEnd.l
;
HoldSelection.l
;
HoldString.s
;
PreText.s
;
PostText.s
;
CombinedText.s
;
MatchText.s
;
CallBack = ac_GetOldCallback(HandleWindow)
; Return the old callback procedure address.
If CallBack = -1 : ProcedureReturn : EndIf
; This should never happen as this callback is only set for autocomplete items.
ResetList(_ac_Main())
While NextElement(_ac_Main())
If _ac_Main()\Handle = HandleWindow : Parent = _ac_Main()\Parent : Gadget = _ac_Main()\Gadget : Break : EndIf
Wend
; Retrieve the handle to the combobox and the gadget id.
If Message = #WM_CHAR
;
HoldTotal = SendMessage_(Parent, #CB_GETCOUNT, 0, 0)
; Return the number of items in the combobox.
If HoldTotal
; Only need to check for autocompletion if items exist in the combobox.
HoldString = GetGadgetText(Gadget)
; Store the current combobox text.
HoldLength = Len(HoldString)
;
SendMessage_(Parent, #CB_GETEDITSEL, @HoldStart, @HoldEnd)
; Store the start and end selection values.
PreText = Mid(HoldString, 1, HoldStart)
; The text before the selection.
PostText = Mid(HoldString, HoldEnd + 1, HoldLength - HoldEnd)
; The text after the selection.
CombinedText = LCase(PreText + PostText + Chr(wParam))
;
HoldCombinedLength = Len(CombinedText)
;
For iLoop = 0 To HoldTotal - 1
; Loop through all items in the combo box.
MatchText = GetGadgetItemText(Gadget, iLoop, 0)
; Store the text at index iLoop.
If LCase(Left(MatchText, HoldCombinedLength)) = CombinedText
; Found a matching item in the combobox.
SetGadgetText(Gadget, MatchText)
;
HoldSelection = HoldCombinedLength | -1 << 16
; Convert the start and end selection into an lParam. The start position is the combined length of the pre
; and post text. The end is set to -1 to select the rest of the text.
SendMessage_(Parent, #CB_SETEDITSEL, 0, HoldSelection)
;
wParam = 0
;
Break
; Exit the loop.
EndIf
;
Next iLoop
;
If wParam <> 0 : lResult = CallWindowProc_(CallBack, HandleWindow, Message, wParam, lParam) : EndIf
;
Else
;
lResult = CallWindowProc_(CallBack, HandleWindow, Message, wParam, lParam)
;
EndIf
;
Else
;
lResult = CallWindowProc_(CallBack, HandleWindow, Message, wParam, lParam)
;
EndIf
;
ProcedureReturn lResult
;
EndProcedure
;- Main Functions
Procedure ac_SetAutocomplete(Gadget.l, EditGadgetArrayAddress.l)
; EditGadgetArrayAddress is exactly that. The address to the first element of a plain string array. This will be used
; for the autocompletion list. Feel free to modify this to handle linked lists or whatever. It will be ignored for combobox.
Type.b
;
Handle.l
;
If ac_GadgetExists(Gadget) : ProcedureReturn : EndIf
; Check if the gadget already exists in the autocomplete list.
Handle = GadgetID(Gadget)
;
Type = ac_GetGadgetType(Handle)
;
If Type = #ac_String
; String type.
If EditGadgetArrayAddress = 0 : ProcedureReturn : EndIf
; An edit control must be associated with an array address.
AddElement(_ac_Main())
;
_ac_Main()\Gadget = Gadget
_ac_Main()\Handle = Handle
; The handle to the edit control.
_ac_Main()\Parent = 0
; An edit control is not like a combobox. Does not have a parent container.
_ac_Main()\CallBack = SetWindowLong_(Handle, #GWL_WNDPROC, @ac_HandleEditEvents())
; The callback will be set for the edit control, not the combobox.
_ac_Main()\ArrayAddress = EditGadgetArrayAddress
;
ElseIf Type = #ac_Combo
; Combobox type
AddElement(_ac_Main())
;
_ac_Main()\Gadget = Gadget
_ac_Main()\Handle = GetWindow_(Handle, #GW_CHILD)
; This is the handle to the edit control within the combobox. I think I got this little tidbit from fr34k but don't quote me on that.
_ac_Main()\Parent = Handle
; This will be the handle to the combobox control.
_ac_Main()\CallBack = SetWindowLong_(_ac_Main()\Handle, #GWL_WNDPROC, @ac_HandleComboEvents())
; The callback will be set for the edit control, not the combobox.
_ac_Main()\ArrayAddress = 0
;
Else
; Not a combo or string type. Currently, no other controls are supported.
ProcedureReturn
;
EndIf
;
EndProcedure
Procedure ac_RemoveAutocomplete(Gadget.l)
;
Handle.l
;
If ac_GadgetExists(Gadget) : ProcedureReturn : EndIf
; Check if the gadget already exists in the autocomplete list.
Handle = GadgetID(Gadget)
;
ResetList(_ac_Main())
While NextElement(_ac_Main())
If _ac_Main()\Gadget = Gadget : SetWindowLong_(Handle, #GWL_WNDPROC, _ac_Main()\CallBack) : Break : EndIf
Wend
;
EndProcedure
Procedure ac_DestroyAutocomplete()
; Remove all autocomplete items.
If CountList(_ac_Main()) = 0 : ProcedureReturn : EndIf
; No need to destroy if no items in the list.
ResetList(_ac_Main())
While NextElement(_ac_Main())
SetWindowLong_(Handle, #GWL_WNDPROC, _ac_Main()\CallBack)
Wend
;
EndProcedure
