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 \
  15 '\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\
  16 \x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\
  17 \x00\x00<IDAT8\x8dcddbf\xa0\x040Q\xa4{h\x18\xf0\xff\xdf\xdf\xffd\x1b\x00\xd3\
  18 \x8c\xcf\x10\x9c\x06\xa0k\xc2e\x08m\xc2\x00\x97m\xd8\xc41\x0c \x14h\xe8\xf2\
  19 \x8c\xa3)q\x10\x18\x00\x00R\xd8#\xec\xb2\xcd\xc1Y\x00\x00\x00\x00IEND\xaeB`\
  20 \x82'
  21 
  22 def getSmallUpArrowBitmap():
  23     return BitmapFromImage(getSmallUpArrowImage())
  24 
  25 def getSmallUpArrowImage():
  26     stream = cStringIO.StringIO(getSmallUpArrowData())
  27     return ImageFromStream(stream)
  28 
  29 def getSmallDnArrowData():
  30     return \
  31 "\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\
  32 \x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\
  33 \x00\x00HIDAT8\x8dcddbf\xa0\x040Q\xa4{\xd4\x00\x06\x06\x06\x06\x06\x16t\x81\
  34 \xff\xff\xfe\xfe'\xa4\x89\x91\x89\x99\x11\xa7\x0b\x90%\ti\xc6j\x00>C\xb0\x89\
  35 \xd3.\x10\xd1m\xc3\xe5*\xbc.\x80i\xc2\x17.\x8c\xa3y\x81\x01\x00\xa1\x0e\x04e\
  36 ?\x84B\xef\x00\x00\x00\x00IEND\xaeB`\x82"
  37 
  38 def getSmallDnArrowBitmap():
  39     return BitmapFromImage(getSmallDnArrowImage())
  40 
  41 def getSmallDnArrowImage():
  42     stream = cStringIO.StringIO(getSmallDnArrowData())
  43     return ImageFromStream(stream)
  44 
  45 
  46 #----------------------------------------------------------------------
  47 class myListCtrl(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin):
  48     def __init__(self, parent, ID=-1, pos=wx.DefaultPosition,
  49                  size=wx.DefaultSize, style=0):
  50         wx.ListCtrl.__init__(self, parent, ID, pos, size, style)
  51         listmix.ListCtrlAutoWidthMixin.__init__(self)
  52 
  53 class TextCtrlAutoComplete (wx.TextCtrl, listmix.ColumnSorterMixin ):
  54     def __init__ ( self, parent, colNames=None, choices = None,
  55                   multiChoices=None, showHead=True, dropDownClick=True,
  56                   colFetch=-1, colSearch=0, hideOnNoMatch=True,
  57                   selectCallback=None, entryCallback=None, matchFunction=None,
  58                   **therest) :
  59         '''
  60         Constructor works just like wx.TextCtrl except you can pass in a
  61         list of choices.  You can also change the choice list at any time
  62         by calling setChoices.
  63         '''
  64         if 'style' in therest:
  65             therest['style']=wx.TE_PROCESS_ENTER | therest['style']
  66         else:
  67             therest['style']=wx.TE_PROCESS_ENTER
  68         wx.TextCtrl.__init__(self, parent, **therest )
  69         #Some variables
  70         self._dropDownClick = dropDownClick
  71         self._colNames = colNames
  72         self._multiChoices = multiChoices
  73         self._showHead = showHead
  74         self._choices = choices
  75         self._lastinsertionpoint = 0
  76         self._hideOnNoMatch = hideOnNoMatch
  77         self._selectCallback = selectCallback
  78         self._entryCallback = entryCallback
  79         self._matchFunction = matchFunction
  80         self._screenheight = wx.SystemSettings.GetMetric( wx.SYS_SCREEN_Y )
  81         #sort variable needed by listmix
  82         self.itemDataMap = dict()
  83         #Load and sort data
  84         if not (self._multiChoices or self._choices):
  85             raise ValueError, "Pass me at least one of multiChoices OR choices"
  86         #widgets
  87         self.dropdown = wx.PopupWindow( self )
  88         #Control the style
  89         flags = wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_SORT_ASCENDING
  90         if not (showHead and multiChoices) :
  91             flags = flags | wx.LC_NO_HEADER
  92         #Create the list and bind the events
  93         self.dropdownlistbox = myListCtrl( self.dropdown, style=flags,
  94                                  pos=wx.Point( 0, 0) )
  95         #initialize the parent
  96         if multiChoices: ln = len(multiChoices)
  97         else: ln = 1
  98         #else: ln = len(choices)
  99         listmix.ColumnSorterMixin.__init__(self, ln)
 100         #load the data
 101         if multiChoices: self.SetMultipleChoices (multiChoices, colSearch=colSearch, colFetch=colFetch)
 102         else: self.SetChoices ( choices )
 103         gp = self
 104         while gp != None :
 105             gp.Bind ( wx.EVT_MOVE , self.onControlChanged, gp )
 106             gp.Bind ( wx.EVT_SIZE , self.onControlChanged, gp )
 107             gp = gp.GetParent()
 108         self.Bind( wx.EVT_KILL_FOCUS, self.onControlChanged, self )
 109         self.Bind( wx.EVT_TEXT , self.onEnteredText, self )
 110         self.Bind( wx.EVT_KEY_DOWN , self.onKeyDown, self )
 111         #If need drop down on left click
 112         if dropDownClick:
 113             self.Bind ( wx.EVT_LEFT_DOWN , self.onClickToggleDown, self )
 114             self.Bind ( wx.EVT_LEFT_UP , self.onClickToggleUp, self )
 115         self.dropdown.Bind( wx.EVT_LISTBOX , self.onListItemSelected, self.dropdownlistbox )
 116         self.dropdownlistbox.Bind(wx.EVT_LEFT_DOWN, self.onListClick)
 117         self.dropdownlistbox.Bind(wx.EVT_LEFT_DCLICK, self.onListDClick)
 118         self.dropdownlistbox.Bind(wx.EVT_LIST_COL_CLICK, self.onListColClick)
 119         self.il = wx.ImageList(16, 16)
 120         self.sm_dn = self.il.Add(getSmallDnArrowBitmap())
 121         self.sm_up = self.il.Add(getSmallUpArrowBitmap())
 122         self.dropdownlistbox.SetImageList(self.il, wx.IMAGE_LIST_SMALL)
 123         self._ascending = True
 124 
 125     #-- methods called from mixin class
 126     def GetSortImages(self):
 127         return (self.sm_dn, self.sm_up)
 128 
 129     def GetListCtrl(self):
 130         return self.dropdownlistbox
 131     # -- event methods
 132 
 133     def onListClick(self, evt):
 134         toSel, flag = self.dropdownlistbox.HitTest( evt.GetPosition() )
 135         #no values on poition, return
 136         if toSel == -1: return
 137         self.dropdownlistbox.Select(toSel)
 138 
 139     def onListDClick(self, evt):
 140         self._setValueFromSelected()
 141 
 142     def onListColClick(self, evt):
 143         col = evt.GetColumn()
 144         #reverse the sort
 145         if col == self._colSearch:
 146             self._ascending = not self._ascending
 147         self.SortListItems( evt.GetColumn(), ascending=self._ascending )
 148         self._colSearch = evt.GetColumn()
 149         evt.Skip()
 150 
 151     def onEnteredText(self, event):
 152         text = event.GetString()
 153         if self._entryCallback:
 154             self._entryCallback()
 155         if not text:
 156             # control is empty; hide dropdown if shown:
 157             if self.dropdown.IsShown():
 158                 self._showDropDown(False)
 159             event.Skip()
 160             return
 161         found = False
 162         if self._multiChoices:
 163             #load the sorted data into the listbox
 164             dd = self.dropdownlistbox
 165             choices = [dd.GetItem(x, self._colSearch).GetText()
 166                 for x in xrange(dd.GetItemCount())]
 167         else:
 168             choices = self._choices
 169         for numCh, choice in enumerate(choices):
 170             if self._matchFunction and self._matchFunction(text, choice):
 171                 found = True
 172             elif choice.lower().startswith(text.lower()) :
 173                 found = True
 174             if found:
 175                 self._showDropDown(True)
 176                 item = self.dropdownlistbox.GetItem(numCh)
 177                 toSel = item.GetId()
 178                 self.dropdownlistbox.Select(toSel)
 179                 break
 180         if not found:
 181             self.dropdownlistbox.Select(self.dropdownlistbox.GetFirstSelected(), False)
 182             if self._hideOnNoMatch:
 183                 self._showDropDown(False)
 184         self._listItemVisible()
 185         event.Skip ()
 186 
 187     def onKeyDown ( self, event ) :
 188         """ Do some work when the user press on the keys:
 189             up and down: move the cursor
 190             left and right: move the search
 191         """
 192         skip = True
 193         sel = self.dropdownlistbox.GetFirstSelected()
 194         visible = self.dropdown.IsShown()
 195         KC = event.GetKeyCode()
 196         if KC == wx.WXK_DOWN :
 197             if sel < self.dropdownlistbox.GetItemCount () - 1:
 198                 self.dropdownlistbox.Select ( sel+1 )
 199                 self._listItemVisible()
 200             self._showDropDown ()
 201             skip = False
 202         elif KC == wx.WXK_UP :
 203             if sel > 0 :
 204                 self.dropdownlistbox.Select ( sel - 1 )
 205                 self._listItemVisible()
 206             self._showDropDown ()
 207             skip = False
 208         elif KC == wx.WXK_LEFT :
 209             if not self._multiChoices: return
 210             if self._colSearch > 0:
 211                 self._colSearch -=1
 212             self._showDropDown ()
 213         elif KC == wx.WXK_RIGHT:
 214             if not self._multiChoices: return
 215             if self._colSearch < self.dropdownlistbox.GetColumnCount() -1:
 216                 self._colSearch += 1
 217             self._showDropDown()
 218         if visible :
 219             if event.GetKeyCode() == wx.WXK_RETURN :
 220                 self._setValueFromSelected()
 221                 skip = False
 222             if event.GetKeyCode() == wx.WXK_ESCAPE :
 223                 self._showDropDown( False )
 224                 skip = False
 225         if skip :
 226             event.Skip()
 227 
 228     def onListItemSelected (self, event):
 229         self._setValueFromSelected()
 230         event.Skip()
 231 
 232     def onClickToggleDown(self, event):
 233         self._lastinsertionpoint = self.GetInsertionPoint()
 234         event.Skip ()
 235 
 236     def onClickToggleUp ( self, event ) :
 237         if self.GetInsertionPoint() == self._lastinsertionpoint :
 238             self._showDropDown ( not self.dropdown.IsShown() )
 239         event.Skip ()
 240 
 241     def onControlChanged(self, event):
 242         if self.IsShown():
 243             self._showDropDown( False )
 244         event.Skip()
 245 
 246     # -- Interfaces methods
 247     def SetMultipleChoices(self, choices, colSearch=0, colFetch=-1):
 248         ''' Set multi-column choice
 249         '''
 250         self._multiChoices = choices
 251         self._choices = None
 252         if not isinstance(self._multiChoices, list):
 253             self._multiChoices = list(self._multiChoices)
 254         flags = wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_SORT_ASCENDING
 255         if not self._showHead:
 256             flags |= wx.LC_NO_HEADER
 257         self.dropdownlistbox.SetWindowStyleFlag(flags)
 258         #prevent errors on "old" systems
 259         if sys.version.startswith("2.3"):
 260             self._multiChoices.sort(lambda x, y: cmp(x[0].lower(), y[0].lower()))
 261         else:
 262             self._multiChoices.sort(key=lambda x: locale.strxfrm(x[0]).lower() )
 263         self._updateDataList(self._multiChoices)
 264         if len(choices)<2 or len(choices[0])<2:
 265             raise ValueError, "You have to pass me a multi-dimension list with at least two entries" 
 266             # with only one entry, the dropdown artifacts
 267         for numCol, rowValues in enumerate(choices[0]):
 268             if self._colNames: colName = self._colNames[numCol]
 269             else: colName = "Select %i" % numCol
 270             self.dropdownlistbox.InsertColumn(numCol, colName)
 271         for numRow, valRow in enumerate(choices):
 272             for numCol, colVal in enumerate(valRow):
 273                 if numCol == 0:
 274                     index = self.dropdownlistbox.InsertImageStringItem(sys.maxint, colVal, -1)
 275                 self.dropdownlistbox.SetStringItem(index, numCol, colVal)
 276                 self.dropdownlistbox.SetItemData(index, numRow)
 277         self._setListSize()
 278         self._colSearch = colSearch
 279         self._colFetch = colFetch
 280 
 281     def SetChoices(self, choices):
 282         '''
 283         Sets the choices available in the popup wx.ListBox.
 284         The items will be sorted case insensitively.
 285         '''
 286         self._choices = choices
 287         self._multiChoices = None
 288         flags = wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_SORT_ASCENDING | wx.LC_NO_HEADER
 289         self.dropdownlistbox.SetWindowStyleFlag(flags)
 290         if not isinstance(choices, list):
 291             self._choices = list(choices)
 292         #prevent errors on "old" systems
 293         if sys.version.startswith("2.3"):
 294             self._choices.sort(lambda x, y: cmp(x.lower(), y.lower()))
 295         else:
 296             self._choices.sort(key=lambda x: locale.strxfrm(x).lower())
 297         self._updateDataList(self._choices)
 298         self.dropdownlistbox.InsertColumn(0, "")
 299         for num, colVal in enumerate(self._choices):
 300             index = self.dropdownlistbox.InsertImageStringItem(sys.maxint, colVal, -1)
 301             self.dropdownlistbox.SetStringItem(index, 0, colVal)
 302             self.dropdownlistbox.SetItemData(index, num)
 303         self._setListSize()
 304         # there is only one choice for both search and fetch if setting a single column:
 305         self._colSearch = 0
 306         self._colFetch = -1
 307 
 308     def GetChoices(self):
 309         return self._choices or self._multiChoices
 310 
 311     def SetSelectCallback(self, cb=None):
 312         self._selectCallback = cb
 313 
 314     def SetEntryCallback(self, cb=None):
 315         self._entryCallback = cb
 316 
 317     def SetMatchFunction(self, mf=None):
 318         self._matchFunction = mf
 319 
 320     #-- Internal methods
 321     def _setValueFromSelected( self ) :
 322          '''
 323          Sets the wx.TextCtrl value from the selected wx.ListCtrl item.
 324          Will do nothing if no item is selected in the wx.ListCtrl.
 325          '''
 326          sel = self.dropdownlistbox.GetFirstSelected()
 327          if sel > -1:
 328             if self._colFetch != -1: col = self._colFetch
 329             else: col = self._colSearch
 330             itemtext = self.dropdownlistbox.GetItem(sel, col).GetText()
 331             if self._selectCallback:
 332                 dd = self.dropdownlistbox
 333                 values = [dd.GetItem(sel, x).GetText()
 334                     for x in xrange(dd.GetColumnCount())]
 335                 self._selectCallback( values )
 336             self.SetValue (itemtext)
 337             self.SetInsertionPointEnd ()
 338             self.SetSelection ( -1, -1 )
 339             self._showDropDown ( False )
 340 
 341     def _showDropDown ( self, show = True ) :
 342         '''
 343         Either display the drop down list (show = True) or hide it (show = False).
 344         '''
 345         if show :
 346             size = self.dropdown.GetSize()
 347             width, height = self . GetSizeTuple()
 348             x, y = self . ClientToScreenXY ( 0, height )
 349             if size.GetWidth() != width :
 350                 size.SetWidth(width)
 351                 self.dropdown.SetSize(size)
 352                 self.dropdownlistbox.SetSize(self.dropdown.GetClientSize())
 353             if y + size.GetHeight() < self._screenheight :
 354                 self.dropdown . SetPosition ( wx.Point(x, y) )
 355             else:
 356                 self.dropdown . SetPosition ( wx.Point(x, y - height - size.GetHeight()) )
 357         self.dropdown.Show ( show )
 358 
 359     def _listItemVisible( self ) :
 360         '''
 361         Moves the selected item to the top of the list ensuring it is always visible.
 362         '''
 363         toSel =  self.dropdownlistbox.GetFirstSelected ()
 364         if toSel == -1: return
 365         self.dropdownlistbox.EnsureVisible( toSel )
 366 
 367     def _updateDataList(self, choices):
 368         #delete, if need, all the previous data
 369         if self.dropdownlistbox.GetColumnCount() != 0:
 370             self.dropdownlistbox.DeleteAllColumns()
 371             self.dropdownlistbox.DeleteAllItems()
 372         #and update the dict
 373         if choices:
 374             for numVal, data in enumerate(choices):
 375                 self.itemDataMap[numVal] = data
 376         else:
 377             numVal = 0
 378         self.SetColumnCount(numVal)
 379 
 380     def _setListSize(self):
 381         if self._multiChoices:
 382             choices = self._multiChoices
 383         else:
 384             choices = self._choices
 385         longest = 0
 386         for choice in choices :
 387             longest = max(len(choice), longest)
 388         longest += 3
 389         itemcount = min( len( choices ) , 7 ) + 2
 390         charheight = self.dropdownlistbox.GetCharHeight()
 391         charwidth = self.dropdownlistbox.GetCharWidth()
 392         self.popupsize = wx.Size( charwidth*longest, charheight*itemcount )
 393         self.dropdownlistbox.SetSize ( self.popupsize )
 394         self.dropdown.SetClientSize( self.popupsize )
 395 
 396 class test:
 397     def __init__(self):
 398         args = {}
 399         if True:
 400             args["colNames"] = ("col1", "col2")
 401             args["multiChoices"] = [ ("Zoey","WOW"), ("Alpha", "wxPython"),
 402                                     ("Ceda","Is"), ("Beta", "fantastic"),
 403                                     ("zoebob", "!!")]
 404             args["colFetch"] = 1
 405         else:
 406             args["choices"] = ["123", "cs", "cds", "Bob","Marley","Alpha"]
 407         args["selectCallback"] = self.selectCallback
 408         self.dynamic_choices = [
 409                         'aardvark', 'abandon', 'acorn', 'acute', 'adore',
 410                         'aegis', 'ascertain', 'asteroid',
 411                         'beautiful', 'bold', 'classic',
 412                         'daring', 'dazzling', 'debonair', 'definitive',
 413                         'effective', 'elegant',
 414                         'http://python.org', 'http://www.google.com',
 415                         'fabulous', 'fantastic', 'friendly', 'forgiving', 'feature',
 416                         'sage', 'scarlet', 'scenic', 'seaside', 'showpiece', 'spiffy',
 417                         'www.wxPython.org', 'www.osafoundation.org'
 418                         ]
 419         app = wx.PySimpleApp()
 420         frm = wx.Frame(None,-1,"Test",style=wx.TAB_TRAVERSAL|wx.DEFAULT_FRAME_STYLE)
 421         panel = wx.Panel(frm)
 422         sizer = wx.BoxSizer(wx.VERTICAL)
 423         self._ctrl = TextCtrlAutoComplete(panel, **args)
 424         but = wx.Button(panel,label="Set other multi-choice")
 425         but.Bind(wx.EVT_BUTTON, self.onBtMultiChoice)
 426         but2 = wx.Button(panel,label="Set other one-colum choice")
 427         but2.Bind(wx.EVT_BUTTON, self.onBtChangeChoice)
 428         but3 = wx.Button(panel,label="Set the starting choices")
 429         but3.Bind(wx.EVT_BUTTON, self.onBtStartChoices)
 430         but4 = wx.Button(panel,label="Enable dynamic choices")
 431         but4.Bind(wx.EVT_BUTTON, self.onBtDynamicChoices)
 432         sizer.Add(but, 0, wx.ADJUST_MINSIZE, 0)
 433         sizer.Add(but2, 0, wx.ADJUST_MINSIZE, 0)
 434         sizer.Add(but3, 0, wx.ADJUST_MINSIZE, 0)
 435         sizer.Add(but4, 0, wx.ADJUST_MINSIZE, 0)
 436         sizer.Add(self._ctrl, 0, wx.EXPAND|wx.ADJUST_MINSIZE, 0)
 437         panel.SetAutoLayout(True)
 438         panel.SetSizer(sizer)
 439         sizer.Fit(panel)
 440         sizer.SetSizeHints(panel)
 441         panel.Layout()
 442         app.SetTopWindow(frm)
 443         frm.Show()
 444         but.SetFocus()
 445         app.MainLoop()
 446 
 447     def onBtChangeChoice(self, event):
 448         #change the choices
 449         self._ctrl.SetChoices(["123", "cs", "cds", "Bob","Marley","Alpha"])
 450         self._ctrl.SetEntryCallback(None)
 451         self._ctrl.SetMatchFunction(None)
 452 
 453     def onBtMultiChoice(self, event):
 454         #change the choices
 455         self._ctrl.SetMultipleChoices( [ ("Test","Hello"), ("Other word","World"),
 456                                         ("Yes!","it work?") ], colFetch = 1 )
 457         self._ctrl.SetEntryCallback(None)
 458         self._ctrl.SetMatchFunction(None)
 459 
 460     def onBtStartChoices(self, event):
 461         #change the choices
 462         self._ctrl.SetMultipleChoices( [ ("Zoey","WOW"), ("Alpha", "wxPython"),
 463                                     ("Ceda","Is"), ("Beta", "fantastic"),
 464                                     ("zoebob", "!!")], colFetch = 1 )
 465         self._ctrl.SetEntryCallback(None)
 466         self._ctrl.SetMatchFunction(None)
 467 
 468     def onBtDynamicChoices(self, event):
 469         '''
 470         Demonstrate dynamic adjustment of the auto-complete list, based on what's
 471         been typed so far:
 472         '''
 473         self._ctrl.SetChoices(self.dynamic_choices)
 474         self._ctrl.SetEntryCallback(self.setDynamicChoices)
 475         self._ctrl.SetMatchFunction(self.match)
 476 
 477     def match(self, text, choice):
 478         '''
 479         Demonstrate "smart" matching feature, by ignoring http:// and www. when doing
 480         matches.
 481         '''
 482         t = text.lower()
 483         c = choice.lower()
 484         if c.startswith(t): return True
 485         if c.startswith(r'http://'): c = c[7:]
 486         if c.startswith(t): return True
 487         if c.startswith('www.'): c = c[4:]
 488         return c.startswith(t)
 489 
 490     def setDynamicChoices(self):
 491         ctrl = self._ctrl
 492         text = ctrl.GetValue().lower()
 493         current_choices = ctrl.GetChoices()
 494         choices = [choice for choice in self.dynamic_choices if self.match(text, choice)]
 495         if choices != current_choices:
 496             ctrl.SetChoices(choices)
 497 
 498     def selectCallback(self, values):
 499         """ Simply function that receive the row values when the
 500             user select an item
 501         """
 502         print "Select Callback called...:",  values
 503 
 504 if __name__ == "__main__":
 505     test()

Comment by Franz Steinhaeusler

If I replace the init line of the textctrl,

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

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:

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.

TextCtrlAutoComplete (last edited 2011-04-01 07:41:43 by c-98-210-210-193)

NOTE: To edit pages in this wiki you must be a member of the TrustedEditorsGroup.