DatePickerEditor – A useful control for wxPython

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.

%d bloggers like this: