Note: I have updated the code in this post. There were some errors being kicked out by it, but I’ve fixed those now. Enjoy!
There hasn’t been a lot of posting going on due to marketing for my book as well as the fact that I’m deep into a large piece of software that I’ve been working on for quite some time. It combines many ideas from the filmmaking world that, right now, are sadly lacking.
There have been some side effects from the development process which I will share with you, gratis. One of them was my need to develop a custom GridCellEditor control for wxPython for use with grids. Now you can select dates from within a grid cell without having to refer back to another calendar.
For the moment, it outputs a formatted date in US format (MM/DD/YYYY for the uninformed), but it could easily be adapted to use another format with either a flag passed to the constructor or just recoded for other formats.
Output from the control is a string which is placed in the cell after successful edits are completed.
And here’s the code:
# Custom Grid Cell Editor that performs date picking import string import wx import wx.grid as Grid class DatePickerEditor(Grid.PyGridCellEditor): """ This GridCellEditor allows you to date pick from a calendar inside the cell of a grid. """ def __init__(self): # Constructor Grid.PyGridCellEditor.__init__(self) # end __init__ def Create(self, parent, id, evtHandler): """ Called to create the control, which must derive from wx.Control. """ self._picker = wx.DatePickerCtrl(parent, id, style=wx.DP_DROPDOWN) self.startingDate = None self.SetControl(self._picker) #if evtHandler: # self._picker.PushEventHandler(evtHandler) # end Create def SetSize(self, rect): """ Called to position/size the edit control within the cell rectangle. If you don't fill the cell (the rect) then be sure to override PaintBackground and do something meaningful there. """ self._picker.SetDimensions(rect.x, rect.y, rect.width+2, rect.height+2, wx.SIZE_ALLOW_MINUS_ONE) # end SetSize def Show(self, show, attr): """ Show or hide the edit control. You can use the attr (if not None) to set colours or fonts for the control. """ super(DatePickerEditor, self).Show(show, attr) # end Show def PaintBackground(self, rect, attr): """ Draws the part of the cell not occupied by the edit control. The base class version just fills it with background colour from the attribute. In this class the edit control fills the whole cell so don't do anything at all in order to reduce flicker. """ pass # end PaintBackground def BeginEdit(self, row, col, grid): """ Fetch the value from the table and prepare the edit control to begin editing. Set the focus to the edit control. *Must Override* """ self.startValue = str(grid.GetTable().GetValue(row, col)).strip() if not self.startValue == '': # Split the string up and then insert it in there tmpDate = wx.DateTime() tmpDate.ParseDate(self.startValue) self._picker.SetValue(tmpDate) self.startingDate = tmpDate self._picker.SetFocus() # end BeginEdit def EndEdit(self, row, col, grid): """ Complete the editing of the current cell. Returns True if the value has changed. If necessary, the control may be destroyed. *Must Override* """ changed = False val = self._picker.GetValue().GetDateOnly() if val.Format("%m/%d/%Y") != self.startValue: changed = True grid.SetCellValue(row, col, str(val.Format("%m/%d/%Y"))) # update the table self.startValue = val.Format("%m/%d/%Y") return changed # end EndEdit def Reset(self): """ Reset the value in the control back to its starting value. *Must Override* """ if self.startingDate is not None: self._picker.SetValue(self.startingDate) # end Reset def IsAcceptedKey(self, evt): """ Return True to allow the given key to start editing: the base class version only checks that the event has no modifiers. F2 is special and will always start the editor. """ # or do it ourselves return (not (evt.ControlDown() or evt.AltDown()) and evt.GetKeyCode() != wx.WXK_SHIFT) # end IsAcceptedKey def StartingKey(self, evt): """ If the editor is enabled by pressing keys on the grid, this will be called to let the editor do something about that first key if desired. """ key = evt.GetKeyCode() ch = None if key in [ wx.WXK_NUMPAD0, wx.WXK_NUMPAD1, wx.WXK_NUMPAD2, wx.WXK_NUMPAD3, wx.WXK_NUMPAD4, wx.WXK_NUMPAD5, wx.WXK_NUMPAD6, wx.WXK_NUMPAD7, wx.WXK_NUMPAD8, wx.WXK_NUMPAD9 ]: ch = ch = chr(ord('0') + key - wx.WXK_NUMPAD0) elif key < 256 and key >= 0 and chr(key) in string.printable: ch = chr(key) evt.Skip() # end StartingKey def StartingClick(self): """ If the editor is enabled by clicking on the cell, this method will be called to allow the editor to simulate the click on the control if needed. """ pass # end StartingClick def Destroy(self): """final cleanup""" self.base_Destroy() # end Destroy def Clone(self): """ Create a new object which is the copy of this one *Must Override* """ return DatePickerEditor() # end Clone |
To use this control, you’ll have to use it with a column’s attribute, rather than set it for individual cells.
Like so:
renderer = DatePickerEditor() attr = wx.grid.GridCellAttr() attr.SetEditor(renderer) self.grid.SetColAttr(6, attr) |
As you can see, it’s not too hard to make a custom control in wxPython to do what you want it to do. That’s only about 172 lines of code for the control in total.
And for those of you wondering if I’m giving away the farm by showing this snippet of code, I’ve run the numbers for source and it constitutes less than 1% of the total codebase for my product.
I still like to share solutions to problems that I’ve solved that maybe someone else out there may have encountered. And hopefully save them some time and banging of head against the wall.