Dynamic list updating with a GridCellChoiceEditor

A common question is how to dynamically update the list in a GridCellChoiceEditor. The problem comes from not having direct access to the underlying ComboBox widget in the grid editor. Fortunately the grid editor throws an event that allows us to get to the underlying ComboBox.

In addition to dynamic updates to the choice list, having access to the underlying ComboBox allows us to use application data and the choice index.

The following example shows a method of doing this.

   1 #-----------------------------------------------------------------------------
   2 # Name:        GridCombo.py
   3 # Purpose:     Dynamic list updating with a wx.grid.GridCellChoiceEditor
   4 #
   5 # Author:      Thomas M Wetherbee
   6 #
   7 # Created:     2009/04/27
   8 # RCS-ID:      $Id: GridCombo.py $
   9 # Copyright:   (c) 2009
  10 # Licence:     Distributed under the terms of the GNU General Public License
  11 #-----------------------------------------------------------------------------
  12 #!/usr/bin/env python
  13 
  14 
  15 '''
  16 Dynamic list updating with a wx.grid.GridCellChoiceEditor.
  17 
  18 This example shows how to dynamically update the choices in a 
  19 GridCellChoiceEditor. This simple example creates a two column
  20 grid where the top row in each column is a wx.grid.GridCellChoiceEditor.
  21 The choices listed in the editor are created on the fly, and may change
  22 with each selection. Text entered into the GridCellChoiceEditor cell 
  23 is appended as an additional choice.
  24 
  25 In addition to appending new choices, this example also shows how to get
  26 the selection index and client data from the choice.
  27 
  28 Cell editor interactions are printed for every step.
  29 
  30 This example is deliberately simple, lacking sizers and other useful but 
  31 confusing niceties.
  32 
  33 Theory:
  34     
  35 The GridCellChoiceEditor uses an underlying ComboBox to do the editing.
  36 This underlying ComboBox is created when the cell editor is created. Normally
  37 the ComboBox is completely hidden, but in this example we retrieve a reference 
  38 to the ComboBox and use it to load choices and retrieve index and client data.
  39 
  40 The example starts with a GridCellChoiceEditor attached to the two top cells of
  41 the grid. When the GridCellChoiceEditor is invoked for the first time, two 
  42 choice items are added to the choice list along with their associated user
  43 data. The items are ('spam', 42) and ('eggs', 69), where spam is the text to
  44 display and 42 is the associated client data. In this example 'spam' has an
  45 index of 0 while eggs, being the second item of the list, has an index of 1.
  46 
  47 Note that the index and user data are not required. The demonstrated method
  48 works fine without either, but sometimes it is useful to know the index of a
  49 selection, especially when the user is allowed to create choices. For example,
  50 we might have the list ['spam', 'eggs', 'spam', 'spam'] where the three spam
  51 items are different objects. In this case simply returning the item value
  52 'spam' is ambiguous. We need to know the index, or perhaps some associated
  53 client data.
  54 
  55 In our example, when the user enters a new choice, the choice is appended to
  56 the end of the choice list. A unique integer number is created for each new
  57 choice, in succession, with the first number being 100. This number is used
  58 for client data.
  59 
  60 In this example we bind directly to the ComboBox events, rather than getting
  61 the events through the frame. This is done to keep the grid from eating the
  62 events. The difference in binding can be seen in the two binding methods:
  63     
  64     self.Bind(wx.EVT_BUTTON, self.OnButton, self.button)
  65     self.button.Bind(wx.EVT_BUTTON, self.OnButton)
  66     
  67 The latter method binds directly to the widget, where the first method
  68 receives the event up the chain through the parent.
  69 
  70 Note that this example does not save the new choice list: it persists only
  71 for the life of the program. In a real application, you will probably want
  72 to save this list and reload it the next time the program runs.
  73 '''
  74 
  75 import wx
  76 import wx.grid
  77 
  78 ##modules ={}
  79 
  80 class Frame1(wx.Frame):
  81     def __init__(self, parent):
  82         wx.Frame.__init__(self, id=-1, name='', parent=None,
  83               pos=wx.Point(100, 100), size=wx.Size(480, 250),
  84               style=wx.DEFAULT_FRAME_STYLE, title='Spam & Eggs')
  85         self.SetClientSize(wx.Size(400, 250))
  86 
  87         self.scrolledWindow1 = wx.ScrolledWindow(id=-1,
  88               name='scrolledWindow1', parent=self, pos=wx.Point(0, 0),
  89               size=wx.Size(400, 250), style=wx.HSCROLL | wx.VSCROLL)
  90 
  91         self.grid1 = wx.grid.Grid(id=-1, name='grid1',
  92               parent=self.scrolledWindow1, pos=wx.Point(0, 0),
  93               size=wx.Size(400, 250), style=0)
  94         
  95         self.grid1.CreateGrid(4, 2)
  96 
  97         #Create the GridCellChoiceEditor with a blank list. Items will
  98         #be added later at runtime. "allowOthers" allows the user to
  99         #create new selection items on the fly.
 100         tChoiceEditor = wx.grid.GridCellChoiceEditor([], allowOthers=True)
 101 
 102         #Assign the cell editors for the top row (row 0). Note that on a
 103         #larger grid you would loop through the cells or set a default.
 104         self.grid1.SetCellEditor(0, 0, tChoiceEditor)
 105         self.grid1.SetCellEditor(0, 1, tChoiceEditor)
 106         
 107         #Create a starter list to seed the choices. In this list the item
 108         #format is (item, ClientData), where item is the string to display
 109         #in the drop list, and ClientData is a behind-the-scenes piece of
 110         #data to associate with this item. A seed list is optional.
 111         #If this were a real application, you would probably load this list
 112         #from a file.
 113         self.grid1.list = [('spam', 42), ('eggs', 69)]
 114         
 115         #Show the first item of the list in each ChoiceEditor cell. The
 116         #displayed text is optional. You could leave these cells blank, or
 117         #display 'Select...' or something of that nature.
 118         self.grid1.SetCellValue(0, 0, self.grid1.list[0][0])
 119         self.grid1.SetCellValue(0, 1, self.grid1.list[0][0])
 120         
 121         #The counter below will be used to automatically generate a new
 122         #piece of unique client data for each new item. This isn't very
 123         #useful, but it does let us demonstrate client data. Typically
 124         #you would use something meaningful for client data, such as a key
 125         #or id number.
 126         self.grid1.counter = 100
 127         
 128         #The following two objects store the client data and item index
 129         #from a choice selection. Client data and selection index are not
 130         #directly exposed to the grid object. We will get this information by
 131         #directly accessing the underlying ComboBox object created by the
 132         #GridCellChoiceEditor. 
 133         self.grid1.data = None
 134         self.grid1.index = None
 135 
 136 
 137         self.grid1.Bind(wx.grid.EVT_GRID_CELL_CHANGE,
 138               self.OnGrid1GridCellChange)
 139               
 140         self.grid1.Bind(wx.grid.EVT_GRID_EDITOR_CREATED,
 141               self.OnGrid1GridEditorCreated)
 142 
 143         self.grid1.Bind(wx.grid.EVT_GRID_EDITOR_HIDDEN,
 144               self.OnGrid1GridEditorHidden)
 145 
 146 
 147     #This method fires when a grid cell changes. We are simply showing
 148     #what has changed and any associated index and client data. Typically
 149     #this method is where you would put your real code for processing grid
 150     #cell changes.
 151     def OnGrid1GridCellChange(self, event):
 152         Row = event.GetRow()
 153         Col = event.GetCol()
 154         
 155         #All cells have a value, regardless of the editor.
 156         print 'Changed cell: (%u, %u)' % (Row, Col)
 157         print 'value: %s' % self.grid1.GetCellValue(Row, Col)
 158         
 159         #Row 0 means a GridCellChoiceEditor, so we should have associated
 160         #an index and client data.
 161         if Row == 0:
 162             print 'index: %u' % self.grid1.index
 163             print 'data: %s' % self.grid1.data
 164         
 165         print ''            #blank line to make it pretty.
 166         event.Skip()
 167  
 168     
 169     #This method fires when the underlying GridCellChoiceEditor ComboBox
 170     #is done with a selection.
 171     def OnGrid1ComboBox(self, event):
 172         #Save the index and client data for later use.
 173         self.grid1.index = self.comboBox.GetSelection()
 174         self.grid1.data = self.comboBox.GetClientData(self.grid1.index)
 175         
 176         print 'ComboBoxChanged: %s' % self.comboBox.GetValue()
 177         print 'ComboBox index: %u' % self.grid1.index 
 178         print 'ComboBox data: %u\n' % self.grid1.data
 179         event.Skip()
 180 
 181 
 182     #This method fires when any text editing is done inside the text portion
 183     #of the ComboBox. This method will fire once for each new character, so
 184     #the print statements will show the character by character changes.
 185     def OnGrid1ComboBoxText(self, event):
 186         #The index for text changes is always -1. This is how we can tell
 187         #that new text has been entered, as opposed to a simple selection
 188         #from the drop list. Note that the index will be set for each character,
 189         #but it will be -1 every time, so the final result of text changes is
 190         #always an index of -1. The value is whatever text that has been 
 191         #entered. At this point there is no client data. We will have to add
 192         #that later, once all of the text has been entered.
 193         self.grid1.index = self.comboBox.GetSelection()
 194         
 195         print 'ComboBoxText: %s' % self.comboBox.GetValue()
 196         print 'ComboBox index: %u\n' % self.grid1.index
 197         event.Skip()
 198 
 199 
 200     #This method fires after editing is finished for any cell. At this point
 201     #we know that any added text is complete, if there is any.
 202     def OnGrid1GridEditorHidden(self, event):
 203         Row = event.GetRow()
 204         Col = event.GetCol()
 205         
 206         #If the following conditions are true, it means that new text has 
 207         #been entered in a GridCellChoiceEditor cell, in which case we want
 208         #to append the new item to our selection list.
 209         if Row == 0 and self.grid1.index == -1:
 210             #Get the new text from the grid cell
 211             item = self.comboBox.GetValue()
 212             
 213             #The new item will be appended to the list, so its new index will
 214             #be the same as the current length of the list (origin zero).
 215             self.grid1.index = self.comboBox.GetCount()
 216             
 217             #Generate some unique client data. Remember this counter example
 218             #is silly, but it makes for a reasonable demonstration. Client
 219             #data is optional. If you can use it, this is where you attach
 220             #your real client data.
 221             self.grid1.data = self.grid1.counter
 222             
 223             #Append the new item to the selection list. Remember that this list
 224             #is used by all cells with the same editor, so updating the list
 225             #here updates it for every cell using this editor.
 226             self.comboBox.Append(item, self.grid1.data)
 227             
 228             #Update the silly client data counter
 229             self.grid1.counter = self.grid1.counter + 1
 230         
 231         print 'OnGrid1EditorHidden: (%u, %u)\n' % (Row, Col)
 232 
 233         event.Skip()
 234 
 235     #This method fires when a cell editor is created. It appears that this
 236     #happens only on the first edit using that editor.
 237     def OnGrid1GridEditorCreated(self, event):
 238         Row = event.GetRow()
 239         Col = event.GetCol()
 240         
 241         print 'OnGrid1EditorCreated: (%u, %u)\n' % (Row, Col)
 242         
 243         #In this example, all cells in row 0 are GridCellChoiceEditors,
 244         #so we need to setup the selection list and bindings. We can't
 245         #do this in advance, because the ComboBox control is created with
 246         #the editor.
 247         if Row == 0:
 248             #Get a reference to the underlying ComboBox control.
 249             self.comboBox = event.GetControl()
 250             
 251             #Bind the ComboBox events.
 252             self.comboBox.Bind(wx.EVT_COMBOBOX, self.OnGrid1ComboBox)
 253             self.comboBox.Bind(wx.EVT_TEXT, self.OnGrid1ComboBoxText)
 254             
 255             #Load the initial choice list.
 256             for (item, data) in self.grid1.list:
 257                 self.comboBox.Append(item, data)
 258         
 259         event.Skip()
 260         
 261 
 262 if __name__ == '__main__':
 263     app = wx.PySimpleApp()
 264     frame = Frame1(None)
 265     frame.Show(True)
 266     app.MainLoop()

ThomasWetherbee

GridCellChoiceEditor (last edited 2010-06-28 14:59:10 by p57A4D445)

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