Introduction
The TextCtrlAutoComplete control is a control used where rapid data entry is needed. It is akin to the ComboBox except that typing in the TextCtrl opens the drop down list and selects the first item that starts with what you are typing. This control has been implemented to mimic as much as possible Firefox's <input type="text"> boxes. Single clicking in it toggles the drop down menu (with choices that you define). Hitting enter when an item is selected (or clicking the item) selects the item. Escape hides the drop down. Hitting down arrow moves the selection down one item, and conversely with up.
(Note: this code does not currently run on Mac, since wxMAC does not yet support PopupWindow.)
(Will Sadkin 8/05/2008: This may no longer be true-- see below.)
(Daniel Levine 7/10/2010: This is still not supported on Macs as of wxPython 2.8.10.1)
Michele Petrazzo modified the code for use wxListCtrl instead of wxListBox, so now TextCtrlAutoComplete can handle more than one column, has column sorting, can fetch the data on the column you want and at the end, can call a "callback" function when an item are select.
Will Sadkin modified the code to allow dynamic generation of the select list based on what's typed, added an argument to hide the dropdown if no match, added an option to set a match function, and enhanced the demo to show how you can now do dynamic list updates based on what is typed.
For easier downloading, the code is also available via this attachment: TextCtrlAutoComplete.py
Code
1 '''
2 wxPython Custom Widget Collection 20060207
3 Written By: Edward Flick (eddy -=at=- cdf-imaging -=dot=- com)
4 Michele Petrazzo (michele -=dot=- petrazzo -=at=- unipex -=dot=- it)
5 Will Sadkin (wsadkin-=at=- nameconnector -=dot=- com)
6 Copyright 2006 (c) CDF Inc. ( http://www.cdf-imaging.com )
7 Contributed to the wxPython project under the wxPython project's license.
8 '''
9 import locale, wx, sys, cStringIO
10 import wx.lib.mixins.listctrl as listmix
11 from wx import ImageFromStream, BitmapFromImage
12 #----------------------------------------------------------------------
13 def getSmallUpArrowData():
14 return \
'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\
15 \x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\
16 \x00\x00<IDAT8\x8dcddbf\xa0\x040Q\xa4{h\x18\xf0\xff\xdf\xdf\xffd\x1b\x00\xd3\
17 \x8c\xcf\x10\x9c\x06\xa0k\xc2e\x08m\xc2\x00\x97m\xd8\xc41\x0c \x14h\xe8\xf2\
18 \x8c\xa3)q\x10\x18\x00\x00R\xd8#\xec\xb2\xcd\xc1Y\x00\x00\x00\x00IEND\xaeB`\
19 \x82'
20
21 def getSmallUpArrowBitmap():
22 return BitmapFromImage(getSmallUpArrowImage())
23
24 def getSmallUpArrowImage():
25 stream = cStringIO.StringIO(getSmallUpArrowData())
26 return ImageFromStream(stream)
27
28 def getSmallDnArrowData():
29 return \
"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\
30 \x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\
31 \x00\x00HIDAT8\x8dcddbf\xa0\x040Q\xa4{\xd4\x00\x06\x06\x06\x06\x06\x16t\x81\
32 \xff\xff\xfe\xfe'\xa4\x89\x91\x89\x99\x11\xa7\x0b\x90%\ti\xc6j\x00>C\xb0\x89\
33 \xd3.\x10\xd1m\xc3\xe5*\xbc.\x80i\xc2\x17.\x8c\xa3y\x81\x01\x00\xa1\x0e\x04e\
34 ?\x84B\xef\x00\x00\x00\x00IEND\xaeB`\x82"
35
36 def getSmallDnArrowBitmap():
37 return BitmapFromImage(getSmallDnArrowImage())
38
39 def getSmallDnArrowImage():
40 stream = cStringIO.StringIO(getSmallDnArrowData())
41 return ImageFromStream(stream)
42
43
44 #----------------------------------------------------------------------
45 class myListCtrl(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin):
46 def __init__(self, parent, ID=-1, pos=wx.DefaultPosition,
47 size=wx.DefaultSize, style=0):
48 wx.ListCtrl.__init__(self, parent, ID, pos, size, style)
49 listmix.ListCtrlAutoWidthMixin.__init__(self)
50
51 class TextCtrlAutoComplete (wx.TextCtrl, listmix.ColumnSorterMixin ):
52 def __init__ ( self, parent, colNames=None, choices = None,
53 multiChoices=None, showHead=True, dropDownClick=True,
54 colFetch=-1, colSearch=0, hideOnNoMatch=True,
55 selectCallback=None, entryCallback=None, matchFunction=None,
56 **therest) :
57 '''
58 Constructor works just like wx.TextCtrl except you can pass in a
59 list of choices. You can also change the choice list at any time
60 by calling setChoices.
61 '''
62 if 'style' in therest:
63 therest['style']=wx.TE_PROCESS_ENTER | therest['style']
64 else:
65 therest['style']=wx.TE_PROCESS_ENTER
66 wx.TextCtrl.__init__(self, parent, **therest )
67 #Some variables
68 self._dropDownClick = dropDownClick
69 self._colNames = colNames
70 self._multiChoices = multiChoices
71 self._showHead = showHead
72 self._choices = choices
73 self._lastinsertionpoint = 0
74 self._hideOnNoMatch = hideOnNoMatch
75 self._selectCallback = selectCallback
76 self._entryCallback = entryCallback
77 self._matchFunction = matchFunction
78 self._screenheight = wx.SystemSettings.GetMetric( wx.SYS_SCREEN_Y )
79 #sort variable needed by listmix
80 self.itemDataMap = dict()
81 #Load and sort data
82 if not (self._multiChoices or self._choices):
83 raise ValueError, "Pass me at least one of multiChoices OR choices"
84 #widgets
85 self.dropdown = wx.PopupWindow( self )
86 #Control the style
87 flags = wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_SORT_ASCENDING
88 if not (showHead and multiChoices) :
89 flags = flags | wx.LC_NO_HEADER
90 #Create the list and bind the events
91 self.dropdownlistbox = myListCtrl( self.dropdown, style=flags,
92 pos=wx.Point( 0, 0) )
93 #initialize the parent
94 if multiChoices: ln = len(multiChoices)
95 else: ln = 1
96 #else: ln = len(choices)
97 listmix.ColumnSorterMixin.__init__(self, ln)
98 #load the data
99 if multiChoices: self.SetMultipleChoices (multiChoices, colSearch=colSearch, colFetch=colFetch)
100 else: self.SetChoices ( choices )
101 gp = self
102 while gp != None :
103 gp.Bind ( wx.EVT_MOVE , self.onControlChanged, gp )
104 gp.Bind ( wx.EVT_SIZE , self.onControlChanged, gp )
105 gp = gp.GetParent()
106 self.Bind( wx.EVT_KILL_FOCUS, self.onControlChanged, self )
107 self.Bind( wx.EVT_TEXT , self.onEnteredText, self )
108 self.Bind( wx.EVT_KEY_DOWN , self.onKeyDown, self )
109 #If need drop down on left click
110 if dropDownClick:
111 self.Bind ( wx.EVT_LEFT_DOWN , self.onClickToggleDown, self )
112 self.Bind ( wx.EVT_LEFT_UP , self.onClickToggleUp, self )
113 self.dropdown.Bind( wx.EVT_LISTBOX , self.onListItemSelected, self.dropdownlistbox )
114 self.dropdownlistbox.Bind(wx.EVT_LEFT_DOWN, self.onListClick)
115 self.dropdownlistbox.Bind(wx.EVT_LEFT_DCLICK, self.onListDClick)
116 self.dropdownlistbox.Bind(wx.EVT_LIST_COL_CLICK, self.onListColClick)
117 self.il = wx.ImageList(16, 16)
118 self.sm_dn = self.il.Add(getSmallDnArrowBitmap())
119 self.sm_up = self.il.Add(getSmallUpArrowBitmap())
120 self.dropdownlistbox.SetImageList(self.il, wx.IMAGE_LIST_SMALL)
121 self._ascending = True
122
123 #-- methods called from mixin class
124 def GetSortImages(self):
125 return (self.sm_dn, self.sm_up)
126
127 def GetListCtrl(self):
128 return self.dropdownlistbox
129 # -- event methods
130
131 def onListClick(self, evt):
132 toSel, flag = self.dropdownlistbox.HitTest( evt.GetPosition() )
133 #no values on poition, return
134 if toSel == -1: return
135 self.dropdownlistbox.Select(toSel)
136
137 def onListDClick(self, evt):
138 self._setValueFromSelected()
139
140 def onListColClick(self, evt):
141 col = evt.GetColumn()
142 #reverse the sort
143 if col == self._colSearch:
144 self._ascending = not self._ascending
145 self.SortListItems( evt.GetColumn(), ascending=self._ascending )
146 self._colSearch = evt.GetColumn()
147 evt.Skip()
148
149 def onEnteredText(self, event):
150 text = event.GetString()
151 if self._entryCallback:
152 self._entryCallback()
153 if not text:
154 # control is empty; hide dropdown if shown:
155 if self.dropdown.IsShown():
156 self._showDropDown(False)
157 event.Skip()
158 return
159 found = False
160 if self._multiChoices:
161 #load the sorted data into the listbox
162 dd = self.dropdownlistbox
163 choices = [dd.GetItem(x, self._colSearch).GetText()
164 for x in xrange(dd.GetItemCount())]
165 else:
166 choices = self._choices
167 for numCh, choice in enumerate(choices):
168 if self._matchFunction and self._matchFunction(text, choice):
169 found = True
170 elif choice.lower().startswith(text.lower()) :
171 found = True
172 if found:
173 self._showDropDown(True)
174 item = self.dropdownlistbox.GetItem(numCh)
175 toSel = item.GetId()
176 self.dropdownlistbox.Select(toSel)
177 break
178 if not found:
179 self.dropdownlistbox.Select(self.dropdownlistbox.GetFirstSelected(), False)
180 if self._hideOnNoMatch:
181 self._showDropDown(False)
182 self._listItemVisible()
183 event.Skip ()
184
185 def onKeyDown ( self, event ) :
186 """ Do some work when the user press on the keys:
187 up and down: move the cursor
188 left and right: move the search
189 """
190 skip = True
191 sel = self.dropdownlistbox.GetFirstSelected()
192 visible = self.dropdown.IsShown()
193 KC = event.GetKeyCode()
194 if KC == wx.WXK_DOWN :
195 if sel < self.dropdownlistbox.GetItemCount () - 1:
196 self.dropdownlistbox.Select ( sel+1 )
197 self._listItemVisible()
198 self._showDropDown ()
199 skip = False
200 elif KC == wx.WXK_UP :
201 if sel > 0 :
202 self.dropdownlistbox.Select ( sel - 1 )
203 self._listItemVisible()
204 self._showDropDown ()
205 skip = False
206 elif KC == wx.WXK_LEFT :
207 if not self._multiChoices: return
208 if self._colSearch > 0:
209 self._colSearch -=1
210 self._showDropDown ()
211 elif KC == wx.WXK_RIGHT:
212 if not self._multiChoices: return
213 if self._colSearch < self.dropdownlistbox.GetColumnCount() -1:
214 self._colSearch += 1
215 self._showDropDown()
216 if visible :
217 if event.GetKeyCode() == wx.WXK_RETURN :
218 self._setValueFromSelected()
219 skip = False
220 if event.GetKeyCode() == wx.WXK_ESCAPE :
221 self._showDropDown( False )
222 skip = False
223 if skip :
224 event.Skip()
225
226 def onListItemSelected (self, event):
227 self._setValueFromSelected()
228 event.Skip()
229
230 def onClickToggleDown(self, event):
231 self._lastinsertionpoint = self.GetInsertionPoint()
232 event.Skip ()
233
234 def onClickToggleUp ( self, event ) :
235 if self.GetInsertionPoint() == self._lastinsertionpoint :
236 self._showDropDown ( not self.dropdown.IsShown() )
237 event.Skip ()
238
239 def onControlChanged(self, event):
240 if self.IsShown():
241 self._showDropDown( False )
242 event.Skip()
243
244 # -- Interfaces methods
245 def SetMultipleChoices(self, choices, colSearch=0, colFetch=-1):
246 ''' Set multi-column choice
247 '''
248 self._multiChoices = choices
249 self._choices = None
250 if not isinstance(self._multiChoices, list):
251 self._multiChoices = list(self._multiChoices)
252 flags = wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_SORT_ASCENDING
253 if not self._showHead:
254 flags |= wx.LC_NO_HEADER
255 self.dropdownlistbox.SetWindowStyleFlag(flags)
256 #prevent errors on "old" systems
257 if sys.version.startswith("2.3"):
258 self._multiChoices.sort(lambda x, y: cmp(x[0].lower(), y[0].lower()))
259 else:
260 self._multiChoices.sort(key=lambda x: locale.strxfrm(x[0]).lower() )
261 self._updateDataList(self._multiChoices)
262 if len(choices)<2 or len(choices[0])<2:
263 raise ValueError, "You have to pass me a multi-dimension list with at least two entries"
264 # with only one entry, the dropdown artifacts
265 for numCol, rowValues in enumerate(choices[0]):
266 if self._colNames: colName = self._colNames[numCol]
267 else: colName = "Select %i" % numCol
268 self.dropdownlistbox.InsertColumn(numCol, colName)
269 for numRow, valRow in enumerate(choices):
270 for numCol, colVal in enumerate(valRow):
271 if numCol == 0:
272 index = self.dropdownlistbox.InsertImageStringItem(sys.maxint, colVal, -1)
273 self.dropdownlistbox.SetStringItem(index, numCol, colVal)
274 self.dropdownlistbox.SetItemData(index, numRow)
275 self._setListSize()
276 self._colSearch = colSearch
277 self._colFetch = colFetch
278
279 def SetChoices(self, choices):
280 '''
281 Sets the choices available in the popup wx.ListBox.
282 The items will be sorted case insensitively.
283 '''
284 self._choices = choices
285 self._multiChoices = None
286 flags = wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_SORT_ASCENDING | wx.LC_NO_HEADER
287 self.dropdownlistbox.SetWindowStyleFlag(flags)
288 if not isinstance(choices, list):
289 self._choices = list(choices)
290 #prevent errors on "old" systems
291 if sys.version.startswith("2.3"):
292 self._choices.sort(lambda x, y: cmp(x.lower(), y.lower()))
293 else:
294 self._choices.sort(key=lambda x: locale.strxfrm(x).lower())
295 self._updateDataList(self._choices)
296 self.dropdownlistbox.InsertColumn(0, "")
297 for num, colVal in enumerate(self._choices):
298 index = self.dropdownlistbox.InsertImageStringItem(sys.maxint, colVal, -1)
299 self.dropdownlistbox.SetStringItem(index, 0, colVal)
300 self.dropdownlistbox.SetItemData(index, num)
301 self._setListSize()
302 # there is only one choice for both search and fetch if setting a single column:
303 self._colSearch = 0
304 self._colFetch = -1
305
306 def GetChoices(self):
307 return self._choices or self._multiChoices
308
309 def SetSelectCallback(self, cb=None):
310 self._selectCallback = cb
311
312 def SetEntryCallback(self, cb=None):
313 self._entryCallback = cb
314
315 def SetMatchFunction(self, mf=None):
316 self._matchFunction = mf
317
318 #-- Internal methods
319 def _setValueFromSelected( self ) :
320 '''
321 Sets the wx.TextCtrl value from the selected wx.ListCtrl item.
322 Will do nothing if no item is selected in the wx.ListCtrl.
323 '''
324 sel = self.dropdownlistbox.GetFirstSelected()
325 if sel > -1:
326 if self._colFetch != -1: col = self._colFetch
327 else: col = self._colSearch
328 itemtext = self.dropdownlistbox.GetItem(sel, col).GetText()
329 if self._selectCallback:
330 dd = self.dropdownlistbox
331 values = [dd.GetItem(sel, x).GetText()
332 for x in xrange(dd.GetColumnCount())]
333 self._selectCallback( values )
334 self.SetValue (itemtext)
335 self.SetInsertionPointEnd ()
336 self.SetSelection ( -1, -1 )
337 self._showDropDown ( False )
338
339 def _showDropDown ( self, show = True ) :
340 '''
341 Either display the drop down list (show = True) or hide it (show = False).
342 '''
343 if show :
344 size = self.dropdown.GetSize()
345 width, height = self . GetSizeTuple()
346 x, y = self . ClientToScreenXY ( 0, height )
347 if size.GetWidth() != width :
348 size.SetWidth(width)
349 self.dropdown.SetSize(size)
350 self.dropdownlistbox.SetSize(self.dropdown.GetClientSize())
351 if y + size.GetHeight() < self._screenheight :
352 self.dropdown . SetPosition ( wx.Point(x, y) )
353 else:
354 self.dropdown . SetPosition ( wx.Point(x, y - height - size.GetHeight()) )
355 self.dropdown.Show ( show )
356
357 def _listItemVisible( self ) :
358 '''
359 Moves the selected item to the top of the list ensuring it is always visible.
360 '''
361 toSel = self.dropdownlistbox.GetFirstSelected ()
362 if toSel == -1: return
363 self.dropdownlistbox.EnsureVisible( toSel )
364
365 def _updateDataList(self, choices):
366 #delete, if need, all the previous data
367 if self.dropdownlistbox.GetColumnCount() != 0:
368 self.dropdownlistbox.DeleteAllColumns()
369 self.dropdownlistbox.DeleteAllItems()
370 #and update the dict
371 if choices:
372 for numVal, data in enumerate(choices):
373 self.itemDataMap[numVal] = data
374 else:
375 numVal = 0
376 self.SetColumnCount(numVal)
377
378 def _setListSize(self):
379 if self._multiChoices:
380 choices = self._multiChoices
381 else:
382 choices = self._choices
383 longest = 0
384 for choice in choices :
385 longest = max(len(choice), longest)
386 longest += 3
387 itemcount = min( len( choices ) , 7 ) + 2
388 charheight = self.dropdownlistbox.GetCharHeight()
389 charwidth = self.dropdownlistbox.GetCharWidth()
390 self.popupsize = wx.Size( charwidth*longest, charheight*itemcount )
391 self.dropdownlistbox.SetSize ( self.popupsize )
392 self.dropdown.SetClientSize( self.popupsize )
393
394 class test:
395 def __init__(self):
396 args = {}
397 if True:
398 args["colNames"] = ("col1", "col2")
399 args["multiChoices"] = [ ("Zoey","WOW"), ("Alpha", "wxPython"),
400 ("Ceda","Is"), ("Beta", "fantastic"),
401 ("zoebob", "!!")]
402 args["colFetch"] = 1
403 else:
404 args["choices"] = ["123", "cs", "cds", "Bob","Marley","Alpha"]
405 args["selectCallback"] = self.selectCallback
406 self.dynamic_choices = [
407 'aardvark', 'abandon', 'acorn', 'acute', 'adore',
408 'aegis', 'ascertain', 'asteroid',
409 'beautiful', 'bold', 'classic',
410 'daring', 'dazzling', 'debonair', 'definitive',
411 'effective', 'elegant',
412 'http://python.org', 'http://www.google.com',
413 'fabulous', 'fantastic', 'friendly', 'forgiving', 'feature',
414 'sage', 'scarlet', 'scenic', 'seaside', 'showpiece', 'spiffy',
415 'www.wxPython.org', 'www.osafoundation.org'
416 ]
417 app = wx.PySimpleApp()
418 frm = wx.Frame(None,-1,"Test",style=wx.TAB_TRAVERSAL|wx.DEFAULT_FRAME_STYLE)
419 panel = wx.Panel(frm)
420 sizer = wx.BoxSizer(wx.VERTICAL)
421 self._ctrl = TextCtrlAutoComplete(panel, **args)
422 but = wx.Button(panel,label="Set other multi-choice")
423 but.Bind(wx.EVT_BUTTON, self.onBtMultiChoice)
424 but2 = wx.Button(panel,label="Set other one-colum choice")
425 but2.Bind(wx.EVT_BUTTON, self.onBtChangeChoice)
426 but3 = wx.Button(panel,label="Set the starting choices")
427 but3.Bind(wx.EVT_BUTTON, self.onBtStartChoices)
428 but4 = wx.Button(panel,label="Enable dynamic choices")
429 but4.Bind(wx.EVT_BUTTON, self.onBtDynamicChoices)
430 sizer.Add(but, 0, wx.ADJUST_MINSIZE, 0)
431 sizer.Add(but2, 0, wx.ADJUST_MINSIZE, 0)
432 sizer.Add(but3, 0, wx.ADJUST_MINSIZE, 0)
433 sizer.Add(but4, 0, wx.ADJUST_MINSIZE, 0)
434 sizer.Add(self._ctrl, 0, wx.EXPAND|wx.ADJUST_MINSIZE, 0)
435 panel.SetAutoLayout(True)
436 panel.SetSizer(sizer)
437 sizer.Fit(panel)
438 sizer.SetSizeHints(panel)
439 panel.Layout()
440 app.SetTopWindow(frm)
441 frm.Show()
442 but.SetFocus()
443 app.MainLoop()
444
445 def onBtChangeChoice(self, event):
446 #change the choices
447 self._ctrl.SetChoices(["123", "cs", "cds", "Bob","Marley","Alpha"])
448 self._ctrl.SetEntryCallback(None)
449 self._ctrl.SetMatchFunction(None)
450
451 def onBtMultiChoice(self, event):
452 #change the choices
453 self._ctrl.SetMultipleChoices( [ ("Test","Hello"), ("Other word","World"),
454 ("Yes!","it work?") ], colFetch = 1 )
455 self._ctrl.SetEntryCallback(None)
456 self._ctrl.SetMatchFunction(None)
457
458 def onBtStartChoices(self, event):
459 #change the choices
460 self._ctrl.SetMultipleChoices( [ ("Zoey","WOW"), ("Alpha", "wxPython"),
461 ("Ceda","Is"), ("Beta", "fantastic"),
462 ("zoebob", "!!")], colFetch = 1 )
463 self._ctrl.SetEntryCallback(None)
464 self._ctrl.SetMatchFunction(None)
465
466 def onBtDynamicChoices(self, event):
467 '''
468 Demonstrate dynamic adjustment of the auto-complete list, based on what's
469 been typed so far:
470 '''
471 self._ctrl.SetChoices(self.dynamic_choices)
472 self._ctrl.SetEntryCallback(self.setDynamicChoices)
473 self._ctrl.SetMatchFunction(self.match)
474
475 def match(self, text, choice):
476 '''
477 Demonstrate "smart" matching feature, by ignoring http:// and www. when doing
478 matches.
479 '''
480 t = text.lower()
481 c = choice.lower()
482 if c.startswith(t): return True
483 if c.startswith(r'http://'): c = c[7:]
484 if c.startswith(t): return True
485 if c.startswith('www.'): c = c[4:]
486 return c.startswith(t)
487
488 def setDynamicChoices(self):
489 ctrl = self._ctrl
490 text = ctrl.GetValue().lower()
491 current_choices = ctrl.GetChoices()
492 choices = [choice for choice in self.dynamic_choices if self.match(text, choice)]
493 if choices != current_choices:
494 ctrl.SetChoices(choices)
495
496 def selectCallback(self, values):
497 """ Simply function that receive the row values when the
498 user select an item
499 """
500 print "Select Callback called...:", values
501
502 if __name__ == "__main__":
503 test()
Comment by Franz Steinhaeusler
If I replace the init line of the textctrl,
wx.TextCtrl.init( self , parent , style = wx.TE_PROCESS_ENTER, **therest )
when the Enter Key seems to be processed accordingly.
I see, your implementation to this is much better than mine.
Comment by Edward Flick
Thanks Franz I think we just about tied for the solution to that. I had just added the TE_PROCESS_ENTER dynamically in the init section.
Either way :-). So do you have any idea on how to fix the PopupWindow issue?
Comment by Franz Steinhaeusler
No, sorry. I will ask in the wxPython mailing list.
Michele Petrazzo posted a solution using a ListCtrl instead of a ListBox.
http://lists.wxwidgets.org/cgi-bin/ezmlm-cgi?11:mss:47543:200602:jjhfcaoagldbfbigklnf
Comment by Michele Petrazzo
New class uploaded
Comment by Michele Petrazzo
Add two interface methods: setMultipleChoices and setChoices that permit to modify the list after the initialization.
Comment by Edward Flick
Changed init to support single column list of choices (was broken). Also, changed the sort function to locale specific sort (as is used in the mixin). Commented out the mixin sort called at the beginning, since its unnecessary.
Comment by Michele Petrazzo
Change the sort methods for prevent the old (python 2.3) sort method that not accepting arguments
Comment by Will Sadkin
Fixed bug in design that could leave fetch column set to a non-existent column if you switched from a multi-choice to a single choice list, causing the control not to autocomplete.
Instead, .setChoices() now automatically sets colSearch and colFetch to 0 and -1, respectively, (as there's only one choice), and .setMultipleChoices() now takes colSearch and colFetch as arguments. (I also changed fetchCol to colFetch, to be consistent with colSearch and colNames.)
I also made the behavior more consistent with the Firefox address bar behavior, so that when the control is empty(ied), the dropdown is hidden, and added a constructor argument, hideOnNoMatch (by default True), so that when a user is typing, if nothing in the choice list matches, the dropdown is hidden as well.
(Finally, I fixed this wiki page to not put wiki links in where none were meant to be.)
Comment by Will Sadkin
Fixed another bug with case-sensitive sorting in choice list, but case-insensitive sorting in ListCtrl, causing incorrect selection from the list when auto-completing.
- Changed single-column display to not show an empty header in the dropdown, to better match address bar look and feel, and to make the control look right when the choice list is empty.
- Renamed existing callBack argument to selectCallback, added entryCallback, added matchFunction, and setters for each of these.
Changed the MixedCase format of the control's public methods to match rest of wx.Python.
Changed SetChoices and SetMultipleChoices to not auto-clear the text control when the list changes, so you can do dynamic updates of the list based on what is typed.
Added button in demo to demonstrate both dynamic list generation and "smart" matching via match function-- Click the Enable dynamic choices button, and then try typing 'as' or 'www', and see what happens!
Attached a working copy of the source for easier extension by others, (so they don't have to cut & paste from the above and then remove the trailing whitespaces in the bitmaps.)
Comment by Marc Hedlund
This class looks great -- thanks to the people who have worked on it. As a note so that others are aware, the class depends on PopupWindow, which is not currently (Jan 2008) implemented on Mac. If you need something cross-platform, you'll have to look elsewhere...
Comment by Will Sadkin
According to http://trac.wxwidgets.org/ticket/9377, as of 6/27/2008, wxWidgets was updated to support PopupWindow... I don't know how long it will take for wxPython to incorporate this change, but someone who has access to a Mac should check this out and update this page if this restriction no longer applies.)
Comment by Marcelo Fernandez
This widget is great! And it also works passing a cursor.fetchall() (from a SQLite DB) to the SetMultipleChoices() method.
I was looking at the custom widget TextCtrl and implementing it in a little application I'm developing. The thing is, the widget works very well overall, but I added a new method which I needed:
1 def GetSelectedItems(self):
2 ''' Returns the complete listbox item selected as a list '''
3 dd = self.dropdownlistbox
4 sel = dd.GetFirstSelected()
5 if sel > -1:
6 values = [dd.GetItem(sel, x).GetText()
7 for x in xrange(dd.GetColumnCount())]
8 return values
9 else:
10 return None
It borrows a lot of code from the internal "_setValueFromSelected()" method, but its intention is to return the selected listbox item as a list.
I needed it because in the "Save Form" button I wanted to know not only the TextCtrl value, but all the ListCtrl values selected; and I didn't want to set a callback to _selectCallback() (which adds more code).
IMHO, I think this approach to get all the ListCtrl values stored there, in any time, is cleaner.
Comment by Matthijs Sypkens Smit
I believe to have discovered a bug in the current code (29-7-2009). In lines 101-105 ('gp = self' to 'gp = gp.GetParent()') bindings are created to catch movement and resize events for parents. However, these are never removed when the control is terminated. The problem came to light when I put the control on a custom dialog. After the dialog had been destroyed I got PyDeadObjectErrors:
"wx._core.PyDeadObjectError: The C++ part of the TextCtrlAutoComplete object has been deleted, attribute access no longer allowed."
The following code fixed it, at least for my case. I inserted them between line 105 and 106, but there might be
1 def handler_close(evt):
2 # unbinding...
3 gp = self
4 while ( gp != None ) :
5 gp.Unbind( wx.EVT_MOVE , gp, -1, -1)
6 gp.Unbind( wx.EVT_SIZE , gp, -1, -1)
7 gp = gp.GetParent()
8
9 self.Bind(wx.EVT_WINDOW_DESTROY, handler_close)
Comment by Matthijs Sypkens Smit
This widget does not work perfectly when placed on a modal dialog. I suspect this is because the ListBox is on a separate popup window. Doubleclick on an item in the listbox or scrolling with the scrollbars does not work on a modal dialog. Scrolling with the keyboard does work.
Comment by James Hofmann
16 October 2009: Clarified the exact usage of multiChoices(the test was incorrect before).
Comment by Daniel Levine
10 July 2010: Checked with a friend who has wxPython on a Mac (wxPython 2.8.10.1) and got a non-implementation error. This still does not work for Macs.
