= How to create a list control (info) = Basic operation: . Associating python data with items Selecting and manipulating items in wx.ListCtrls (using SetItem* methods) Report-view List controls "why don't my items show up" (missing headers) {{{self.list.InsertColumn( 0, "Items", width=-1)}}} See Also: . [[wxListCtrl ToolTips]] . ObjectListView <<TableOfContents>> == Associating Python Data with Items (Phoenix) == When you need to associate data with an item: {{{#!python listctrl.SetItemData(item_by_index, integer_for_item) }}} Yep! You get to associate a ''WHOLE'' integer with the item! To associate more, make a num generator (or use wx.NewIdRef) and keep a dict binding id to whatever data you want to associate. So, for example: {{{#!python id = wx.NewIdRef() data_dict[id] = some_data_to_associate listctrl.InsertItem(index_to_place_at, string_name_of_item) listctrl.SetItemData(index_to_place_at, id) }}} So, now you can traverse: index --> associated data (id) --> some_data_to_associate == Python Data Mixin == I'm surprised I haven't seen one of these yet. {{{#!python class ListCtrlPyDataMixin(object): def __init__(self): import sys self.id = -sys.maxint self.map = {} self.Bind(wx.EVT_LIST_DELETE_ITEM, self.OnDeleteItem) self.Bind(wx.EVT_LIST_DELETE_ALL_ITEMS, self.OnDeleteAllItems) def SetPyData(self, item, data): self.map[self.id] = data self.SetItemData(item, self.id) self.id += 1 def SortPyItems(self, fn): from functools import wraps @wraps(fn) def wrapper(a, b): return fn(self.map[a], self.map[b]) self.SortItems(wrapper) def OnDeleteItem(self, event): try: del self.map[event.Data] except KeyError: pass event.Skip() def OnDeleteAllItems(self, event): self.map.clear() event.Skip() }}} Have fun. -- ChristopherMonsanto == Selecting Items Programmatically == To select an item programmatically in a wx.ListCtrl (note that this appears to be a bug ? Shouldn't need to use wx.LIST_STATE_SELECTED for stateMask argument, should be able to use a MASK argument instead ?) : . {{{self.SetItemState(ID, wx.LIST_STATE_SELECTED, wx.LIST_STATE_SELECTED)}}} It should be (I think) : . {{{self.SetItemState(ID, wx.LIST_STATE_SELECTED, wx.LIST_MASK_STATE)}}} ''Note : I just tried both versions, and the first version works in wxPython 2.5 ; the second version (using LIST_MASK_STATE) does not.'' I think only the first version is correct. The last parameter is a mask of all the states you want to modify, so it must consist only of wx.LIST_STATE_* combinations. The wx.LIST_MASK_STATE is only used in SetItem, where it tells which fields are valid. Seems the docs could be clearer though. == Retrieve Currently Selected Indices in List Control == A common task, there's no pre-built function to accomplish this, so here's a recipe: {{{#!python def _getSelectedIndices(self, state = wx.LIST_STATE_SELECTED): indices = [] lastFound = -1 while True: index = self.GetNextItem( lastFound, wxLIST_NEXT_ALL, state, ) if index == -1: break else: lastFound = index indices.append( index ) return indices }}} == wx.ListCtrl's Hello world (Phoenix) == It took half an hour for a newbie like me to get this tiny application running. Until I saw that you must call "InsertItem" :-) . {{attachment:img_sample_one.png}} {{{#!python # sample_one.py import wx # class MyFrame # class MyApp #--------------------------------------------------------------------------- data = [("World", "Python", "Welcome"), ("wxWidgets", "Data", "Items"), ("ListCtrl", "Report", "Border"), ("Index", "Column", "Insert"), ("Width", "Header", "Image")] #--------------------------------------------------------------------------- class MyFrame(wx.Frame): def __init__(self): wx.Frame.__init__(self, None, -1, "List control report", size=(380, 220)) id = wx.NewIdRef() self.list = wx.ListCtrl(self, id, style=wx.LC_REPORT| wx.SUNKEN_BORDER) # Add some columns self.list.InsertColumn(0, "Data #1") self.list.InsertColumn(1, "Data #2") self.list.InsertColumn(2, "Data #3") # Add the rows for item in data: index = self.list.InsertItem(self.list.GetItemCount(), item[0]) for col, text in enumerate(item[1:]): self.list.SetItem(index, col+1, text) # Set the width of the columns self.list.SetColumnWidth(0, 120) self.list.SetColumnWidth(1, 120) self.list.SetColumnWidth(2, 120) #--------------------------------------------------------------------------- class MyApp(wx.App): def OnInit(self): #------------ frame = MyFrame() self.SetTopWindow(frame) frame.Show(True) return True #--------------------------------------------------------------------------- def main(): app = MyApp(False) app.MainLoop() #--------------------------------------------------------------------------- if __name__ == "__main__" : main() }}} == Changing fonts in a ListCtrl (Phoenix) == Supposing you have a List Control, and you want to set some items to, say, bold. That's all - no changing anything else about the font, so the face remains the same as the other items. (Bear in mind the face will most likely be the default face as set by the system, and hence we don't know what it is.) Because I (Dave Cridland) am terminally stupid, I didn't figure out how to do this, and couldn't find any documentation. In case anyone else is trying to do the same thing, and can't figure it out either, here's what I did: {{attachment:img_sample_two.png}} {{{#!python # sample_two.py import wx # class MyFancyListCtrl # class MyFrame # class MyApp #--------------------------------------------------------------------------- class MyFancyListCtrl(wx.ListCtrl): def __init__(self, parent, id): wx.ListCtrl.__init__(self, parent, id, style=wx.LC_REPORT| wx.LC_VIRTUAL| wx.SUNKEN_BORDER) # Add some columns self.InsertColumn(0, "Data #1") self.InsertColumn(1, "Data #2") self.InsertColumn(2, "Data #3") # Set the width of the columns self.SetColumnWidth(0, 120) self.SetColumnWidth(1, 120) self.SetColumnWidth(2, 120) self.SetItemCount(10) # We setup our pretty attribute once, here. self._funky_attr = wx.ListItemAttr() # This now contains default attributes. # We can set the text colour to be different : self._funky_attr.SetTextColour(wx.BLUE) # But we can't do either of : # self._funky_attr.GetFont().SetWeight(wx.BOLD) # (Above fails silently - probably GetFont() returns a copy). # new_font = wx.Font() # (No default constructor in wxPython). # But we can extract the default font : new_font = self._funky_attr.GetFont() # Set the weight on it : new_font.SetWeight(wx.BOLD) # Then put it back. self._funky_attr.SetFont(new_font) # self._funky_attr is now ready to be returned by OnGetItemAttr() # (since this happens to be virtual). self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnItemSelected) self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.OnItemActivated) self.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.OnItemDeselected) #----------------------------------------------------------------------- def OnItemSelected(self, event): self.currentItem = event.Index print('OnItemSelected: "%s", "%s", "%s", "%s"\n' % (self.currentItem, self.GetItemText(self.currentItem), self.GetColumnText(self.currentItem, 1), self.GetColumnText(self.currentItem, 2))) def OnItemActivated(self, event): self.currentItem = event.Index print("OnItemActivated: %s\nTopItem: %s\n" % (self.GetItemText(self.currentItem), self.GetTopItem())) def OnItemDeselected(self, evt): print("OnItemDeselected: %s" % evt.Index) def GetColumnText(self, index, col): item = self.GetItem(index, col) return item.GetText() def OnGetItemText(self, item, col): return "Item %d, column %d" % (item, col) def OnGetItemAttr(self, item): if item % 2 == 1: return self._funky_attr else: return None #--------------------------------------------------------------------------- class MyFrame(wx.Frame): def __init__(self): wx.Frame.__init__(self, None, -1, "Fancy list control", size=(400, 240)) id = wx.NewIdRef() self.list = MyFancyListCtrl(self, id) #--------------------------------------------------------------------------- class MyApp(wx.App): def OnInit(self): #------------ frame = MyFrame() self.SetTopWindow(frame) frame.Show(True) return True #--------------------------------------------------------------------------- def main(): app = MyApp(False) app.MainLoop() #--------------------------------------------------------------------------- if __name__ == "__main__" : main() }}} Simply changing the font of a specific item : {{{#!python # Get the item at a specific index : item = self.myListControl.GetItem(index) # Get its font, change it, and put it back : font = item.GetFont() font.SetWeight(wx.FONTWEIGHT_BOLD) item.SetFont(font) # This does the trick: self.myListControl.SetItem(item) }}} == Unselecting an item programmatically (Phoenix) == It took me (Werner Bruhin) some searching, as always when one figures it out it is easy! {{{#!python # Selecting as shown in the demo self.SetItemState(item, wx.LIST_STATE_SELECTED, wx.LIST_STATE_SELECTED) # Unselecting self.SetItemState(item, 0, wx.LIST_STATE_SELECTED) }}} == Type Ahead Mixin == Windows (and presumably other OS do as well) provides Type Ahead functionality by default in list controls. But not if they are Virtual. I had a need to provide Type Ahead for some virtual controls in my app, so I knocked up this Mixin class for wxListCtrl objects. Happily it works with both Virtual and Normal list controls which means if you use it in your app then all your list controls can have the same Type Ahead behaviour. There are some things I've tried to make configurable; the type ahead timeout, case sensitivity to the search, special keycodes to avoid during typeahead, and I've provided methods that you could override to change the default behaviour that I've coded (it was almost on a whim as I didn't really like the behaviour that Windows was providing, it seemed, weird). To help understand what's going on I've tried to document the code to explain the bits I think are weird. One thing that is notable is that the default windows type ahead doesn't seem to recognise the space character, with some playing about with EVT_KEY_DOWN and EVT_CHAR events I've made my type ahead deal with spaces. Which is nice. Anyway, on with the code (please excuse the verbosity of my variable and method names :) ). {{{#!python from wxPython.wx import * class TypeAheadListCtrlMixin: """ A Mixin Class that does type ahead scrolling for list controls. Assumes that the wxListCtrl it is mixed into is sorted (it's using a binary search to find the correct item). I wrote this because on Windows there was no type ahead when you used a virtual list control, and then couldn't be bohered deciphering the default windows typeahead for non-virtual list controls. So I made it work for both virtual and non-virtual controls so that all my list controls would have the same functionality. Things you can change programatically: * expand or contract the list of keycodes that stop the type ahead * expand or contract the list of keycodes that are allowed to be used inside typeahead, but won't start it (e.g. space which normally acts as an Activation Key) * change the timeout time (init param, defaults to 500 milliseconds) * change the sensitivity of the search (init param, defaults to caseinsensitive) Things you can change in the class that you mix this into: * override the following methods: - currentlySelectedItemFoundByTypeAhead - currentlySelectedItemNotFoundByTypeAhead - newItemFoundByTypeAhead - nothingFoundByTypeAhead changing these changes the behaviour of the typeahead in various stages. See doc comments on methods. Written by Murray Steele (muz at h-lame dot com) """ # These Keycodes are the ones that if we detect them we will cancel the current # typeahead state. stopTypeAheadKeyCodes = [ WXK_BACK, WXK_TAB, WXK_RETURN, WXK_ESCAPE, WXK_DELETE, WXK_START, WXK_LBUTTON, WXK_RBUTTON, WXK_CANCEL, WXK_MBUTTON, WXK_CLEAR, WXK_PAUSE, WXK_CAPITAL, WXK_PRIOR, WXK_NEXT, WXK_END, WXK_HOME, WXK_LEFT, WXK_UP, WXK_RIGHT, WXK_DOWN, WXK_SELECT, WXK_PRINT, WXK_EXECUTE, WXK_SNAPSHOT, WXK_INSERT, WXK_HELP, WXK_F1, WXK_F2, WXK_F3, WXK_F4, WXK_F5, WXK_F6, WXK_F7, WXK_F8, WXK_F9, WXK_F10, WXK_F11, WXK_F12, WXK_F13, WXK_F14, WXK_F15, WXK_F16, WXK_F17, WXK_F18, WXK_F19, WXK_F20, WXK_F21, WXK_F22, WXK_F23, WXK_F24, WXK_NUMLOCK, WXK_SCROLL] # These are the keycodes that we have to catch in evt_key_down, not evt_char # By the time they get to evt_char then the OS has looked at them and gone, # hey, this keypress means do something (like pressing space acts as an ACTIVATE # key in a list control. catchInKeyDownIfDuringTypeAheadKeyCodes = [ WXK_SPACE ] # These are the keycodes that we will allow during typeahead, but won't allow to start # the type ahead process. dontStartTypeAheadKeyCodes = [ WXK_SHIFT, WXK_CONTROL, WXK_MENU #ALT Key, ALT Gr generates both WXK_CONTROL and WXK_MENU. ] dontStartTypeAheadKeyCodes.extend(catchInKeyDownIfDuringTypeAheadKeyCodes) def __init__(self, typeAheadTimeout = 500, casesensitive = False, columnToSearch = 0): # Do most work in the char handler instead of keydown. # This means we get the correct keycode for the key pressed as it should # appear on screen, rather than all uppercase or "default" us keyboard # punctuation. # However there are things that we need to catch in key_down to stop # them getting sent to the underlying windows control and generating # other events (notably I'm talking about the SPACE key which generates # an ACTIVATE event in these list controls). EVT_KEY_DOWN(self, self.OnTypeAheadKeyDown) EVT_CHAR(self, self.OnTypeAheadChar) timerId = wxNewId() self.typeAheadTimer = wxTimer(self,timerId) EVT_TIMER(self,timerId,self.OnTypeAheadTimer) self.clearTypeAhead() self.typeAheadTimeout = typeAheadTimeout self.columnToSearch = columnToSearch if not casesensitive: self._GetItemText = lambda idx: self.GetItem(idx,self.columnToSearch).GetText().lower() self._GetKeyCode = lambda keycode: chr(keycode).lower() else: self._GetItemText = lambda idx: self.GetItem(idx,self.columnToSearch).GetText() self._GetKeyCode = chr def OnTypeAheadKeyDown(self, event): keycode = event.GetKeyCode() if keycode in self.stopTypeAheadKeyCodes: self.clearTypeAhead() elif self.typeAhead == None: if keycode in self.dontStartTypeAheadKeyCodes: self.clearTypeAhead() else: if keycode in self.catchInKeyDownIfDuringTypeAheadKeyCodes: self.OnTypeAheadChar(event) return event.Skip() def OnTypeAheadChar(self, event): # stop the timer, to make sure that it doesn't fire in the middle of # doing this and screw up by None-ifying the typeAhead string. # TODO: Yes some kind of lock around a typeAheadState object # that contained typeAhead, lastTypeAheadFoundAnything and lastTypeAhead # would be better... self.typeAheadTimer.Stop() keycode = event.GetKeyCode() if keycode in self.stopTypeAheadKeyCodes: self.clearTypeAhead() event.Skip() return else: if self.typeAhead == None: if keycode in self.dontStartTypeAheadKeyCodes: self.clearTypeAhead() event.Skip() return else: self.typeAhead = self._GetKeyCode(keycode) else: self.typeAhead += self._GetKeyCode(keycode) self.doTypeAhead() # This timer is used to nullify the typeahead after a while self.typeAheadTimer.Start(self.typeAheadTimeout,wxTIMER_ONE_SHOT) def inTypeAhead(self): return self.typeAhead != None def currentlySelectedItemFoundByTypeAhead(self, idx): """This method is called when the typeahead string matches the text of the currently selected item. Put code here if you want to have something happen in this case. NOTE: Method only called if there was a currently selected item. idx refers to the index of the currently selected item.""" # we don't do anything as we've already selected the thing we want pass def currentlySelectedItemNotFoundByTypeAhead(self, idx): """This method is called when the typeahead string matches an item that isn't the currently selected one. Put code here if you want something to happen to the currently selected item. NOTE: use newItemFoundByTypeAhead for doing something to the newly matched item. NOTE: Method only called if there was a currently selected item. idx referes to the index of the currently selected item.""" # we deselect it. self.SetItemState(idx, 0, wxLIST_STATE_SELECTED) self.SetItemState(idx, 0, wxLIST_STATE_FOCUSED) def newItemFoundByTypeAhead(self, idx): """This is called when the typeahead string matches an item that isn't the currently selected one. Put code here if you want something to happen to the newly found item. NOTE: use currentlySelectedItemNotFoundByTypeAhead for doing something to the previously selected item. idx refers to the index of the newly matched item.""" # we select it and make sure it is focused self.SetItemState(idx, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED) self.SetItemState(idx, wxLIST_STATE_FOCUSED, wxLIST_STATE_FOCUSED) self.EnsureVisible(idx) def nothingFoundByTypeAhead(self, idx): """This method is called when the typeahead string doesn't match any items. Put code here if you want something to happen in this case. idx refers to the index of the currently selected item or -1 if nothing was selected.""" # don't do anything here, what could we do? pass def doTypeAhead(self): curselected = -1 if self.lastTypeAheadFoundSomething: curselected = self.lastTypeAhead else: curselected = self.GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED) min = 0 if curselected != -1: term_name = self._GetItemText(curselected) if term_name.startswith(self.typeAhead): self.currentlySelectedItemFoundByTypeAhead(curselected) self.lastTypeAheadFoundAnything = True self.lastTypeAhead = curselected return #We don't want this edgecase falling through new_idx = self.binary_search(self.typeAhead,min) if new_idx != -1: if new_idx != curselected and curselected != -1: self.currentlySelectedItemNotFoundByTypeAhead(curselected) self.newItemFoundByTypeAhead(new_idx) self.lastTypeAheadFoundAnything = True self.lastTypeAhead = new_idx else: self.nothingFoundByTypeAhead(curselected) self.lastTypeAheadFoundAnything = False self.lastTypeAhead = -1 # NOTE: Originally from ASPN. Augmented. def binary_search(self, t, min = 0): min = min; max = self.GetItemCount() - 1 while True: if max < min: return self.doEdgeCase(m, t) m = (min + max) / 2 cur_term = self._GetItemText(m) if cur_term < t: min = m + 1 elif cur_term > t: max = m - 1 else: return m def doEdgeCase(self, m, t): """ This method makes sure that if we don't find the typeahead as an actual string, then we will return the first item that starts with the typeahead string (if there is one)""" before = self._GetItemText(max(0,m-1)) this = self._GetItemText(m) after = self._GetItemText(min(self.GetItemCount()-1,m+1)) if this.startswith(t): return m elif before.startswith(t): return max(0,m-1) elif after.startswith(t): return min(self.GetItemCount()-1,m+1) else: return -1 def clearTypeAhead(self): self.typeAhead = None self.lastTypeAheadFoundSomething = False self.lastTypeAheadIdx = -1 def OnTypeAheadTimer(self, event): self.clearTypeAhead() }}} == Customizing ColumnSorterMixin to Sort Dates == This code augments wx.lib.mixins.listctrl.ColumnSorterMixin so that it can sort dates that match the date_re regular expression listed at the top of the code. You need to modify another section of the code to transform your date into the YYYYMMDD format so that the dates can be easily sorted numerically. Alternatively, you could use the datetime module and store and sort date objects. {{{#!python import wx import wx.lib.mixins.listctrl import locale import re # Change this to your settings date_re = re.compile("(\d{2})-(\d{2})-(\d{4})") #---------------------------------------------------------------------------- class CustColumnSorterMixin(wx.lib.mixins.listctrl.ColumnSorterMixin): def __init__(self, numColumns): wx.lib.mixins.listctrl.ColumnSorterMixin(self, numColumns) def GetColumnSorter(self): return self.CustColumnSorter def CustColumnSorter(self, key1, key2): col = self._col ascending = self._colSortFlag[col] item1 = self.itemDataMap[key1][col] item2 = self.itemDataMap[key2][col] alpha = date_re.match(item1) beta = date_re.match(item2) if alpha and beta: # Change these from your settings to YYYYMMDD item1 = alpha.group(3)+alpha.group(1)+alpha.group(2) item2 = beta.group(3)+ beta.group(1)+ beta.group(2) item1 = int(item1) item2 = int(item2) #--- Internationalization of string sorting with locale module if type(item1) == type('') or type(item2) == type(''): cmpVal = locale.strcoll(str(item1), str(item2)) else: cmpVal = cmp(item1, item2) #--- # If the items are equal then pick something else to make the sort value unique if cmpVal == 0: cmpVal = cmp(*self.GetSecondarySortValues(col, key1, key2)) if ascending: return cmpVal else: return -cmpVal }}} == Drag and Drop with lists - 1 == Here's how you can use drag-and-drop to reorder a simple list, or to move items from one list to another. As it stands, item data will be lost. Maybe in version 2! - JohnFouhy {{{#!python """ DnD demo with listctrl. """ import wx class DragList(wx.ListCtrl): def __init__(self, *arg, **kw): if 'style' in kw and (kw['style']&wx.LC_LIST or kw['style']&wx.LC_REPORT): kw['style'] |= wx.LC_SINGLE_SEL else: kw['style'] = wx.LC_SINGLE_SEL|wx.LC_LIST wx.ListCtrl.__init__(self, *arg, **kw) self.Bind(wx.EVT_LIST_BEGIN_DRAG, self._startDrag) dt = ListDrop(self._insert) self.SetDropTarget(dt) def _startDrag(self, e): """ Put together a data object for drag-and-drop _from_ this list. """ # Create the data object: Just use plain text. data = wx.PyTextDataObject() idx = e.GetIndex() text = self.GetItem(idx).GetText() data.SetText(text) # Create drop source and begin drag-and-drop. dropSource = wx.DropSource(self) dropSource.SetData(data) res = dropSource.DoDragDrop(flags=wx.Drag_DefaultMove) # If move, we want to remove the item from this list. if res == wx.DragMove: # It's possible we are dragging/dropping from this list to this list. In which case, the # index we are removing may have changed... # Find correct position. pos = self.FindItem(idx, text) self.DeleteItem(pos) def _insert(self, x, y, text): """ Insert text at given x, y coordinates --- used with drag-and-drop. """ # Clean text. import string text = filter(lambda x: x in (string.letters + string.digits + string.punctuation + ' '), text) # Find insertion point. index, flags = self.HitTest((x, y)) if index == wx.NOT_FOUND: if flags & wx.LIST_HITTEST_NOWHERE: index = self.GetItemCount() else: return # Get bounding rectangle for the item the user is dropping over. rect = self.GetItemRect(index) # If the user is dropping into the lower half of the rect, we want to insert _after_ this item. if y > rect.y + rect.height/2: index += 1 self.InsertStringItem(index, text) class ListDrop(wx.PyDropTarget): """ Drop target for simple lists. """ def __init__(self, setFn): """ Arguments: - setFn: Function to call on drop. """ wx.PyDropTarget.__init__(self) self.setFn = setFn # specify the type of data we will accept self.data = wx.PyTextDataObject() self.SetDataObject(self.data) # Called when OnDrop returns True. We need to get the data and # do something with it. def OnData(self, x, y, d): # copy the data from the drag source to our data object if self.GetData(): self.setFn(x, y, self.data.GetText()) # what is returned signals the source what to do # with the original data (move, copy, etc.) In this # case we just return the suggested value given to us. return d if __name__ == '__main__': items = ['Foo', 'Bar', 'Baz', 'Zif', 'Zaf', 'Zof'] class MyApp(wx.App): def OnInit(self): self.frame = wx.Frame(None, title='Main Frame') self.frame.Show(True) self.SetTopWindow(self.frame) return True app = MyApp(redirect=False) dl1 = DragList(app.frame) dl2 = DragList(app.frame) sizer = wx.BoxSizer() app.frame.SetSizer(sizer) sizer.Add(dl1, proportion=1, flag=wx.EXPAND) sizer.Add(dl2, proportion=1, flag=wx.EXPAND) for item in items: dl1.InsertStringItem(99, item) dl2.InsertStringItem(99, item) app.frame.Layout() app.MainLoop() }}} The content on this page came from ListAndTreeControls, which was split up into this page and TreeControls .