wxPython: Grid Tips and Tricks

Last month I mentioned that we would go on a journey to learn some tips and tricks for wxPython’s Grid widget. Well, the coding is done and I thought it was time to actually do this thing. In the following article you will learn how to:

  • Create a right-click pop-up menu in a cell
  • How to Get the Col/Row on Right-click
  • Put tooltips on the Row and Column labels and on the cells
  • How to use your keyboards arrow keys to move out of a cell that’s being edited
  • Hide Row/Column labels/headers
  • Show a pop-up when clicking a row label
  • Change row/column labels

Well, what are you waiting for? Click the “more” link and start reading!

How to Create a Pop-Up Menu in a Grid Cell

wxGrid with a popup menu

This is a super easy trick to accomplish. Let’s take a look at some code so you can see just how simple it is!

import wx
import wx.grid as  gridlib

class MyForm(wx.Frame):
 
    def __init__(self):
        wx.Frame.__init__(self, None, wx.ID_ANY, 
                          "Grid with Popup Menu")
 
        # Add a panel so it looks the correct on all platforms
        panel = wx.Panel(self, wx.ID_ANY)
        self.grid = gridlib.Grid(panel)
        self.grid.CreateGrid(25,8)
        self.grid.Bind(gridlib.EVT_GRID_CELL_RIGHT_CLICK,
                       self.showPopupMenu)
        
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.grid, 1, wx.EXPAND, 5)
        panel.SetSizer(sizer)
        
    #----------------------------------------------------------------------
    def showPopupMenu(self, event):
        """
        Create and display a popup menu on right-click event
        """
        if not hasattr(self, "popupID1"):
            self.popupID1 = wx.NewId()
            self.popupID2 = wx.NewId()
            self.popupID3 = wx.NewId()
            # make a menu
        
        menu = wx.Menu()
        # Show how to put an icon in the menu
        item = wx.MenuItem(menu, self.popupID1,"One")
        menu.AppendItem(item)
        menu.Append(self.popupID2, "Two")
        menu.Append(self.popupID3, "Three")
        
        # Popup the menu.  If an item is selected then its handler
        # will be called before PopupMenu returns.
        self.PopupMenu(menu)
        menu.Destroy()

# Run the program
if __name__ == "__main__":
    app = wx.PySimpleApp()
    frame = MyForm().Show()
    app.MainLoop()

There are only two things in here that we really care about: the event binding and the event handler. The binding is simple: gridlib.EVT_GRID_CELL_RIGHT_CLICK. Note that you need to use the grid’s event and not the general EVT_RIGHT_CLICK. The event handler comes from the wxPython demo. In it, you just build the menu on the fly and then display it by calling the PopupMenu method. The menu will be destroyed when you click outside the menu or when you make a choice and said choice’s method call(s) has finished.

How to Get the Col/Row on Right-click

Sometimes you want to know which cell you are right-clicking on specifically. Why? Well if you want to do a context menu, then you’ll need to know which cell you clicked on. The code above doesn’t really tell you that. It just creates a little pop-up menu. Thus you can use the following snippet and combine it with the previous one to create a context menu.

import wx
import wx.grid as gridlib
 
########################################################################
class MyForm(wx.Frame):
    """"""
 
    #----------------------------------------------------------------------
    def __init__(self):
        """Constructor"""
        wx.Frame.__init__(self, parent=None, title="Getting the Row/Col")
        panel = wx.Panel(self)
 
        myGrid = gridlib.Grid(panel)
        myGrid.CreateGrid(12, 8)
        self.myGrid = myGrid
        self.myGrid.GetGridWindow().Bind(wx.EVT_RIGHT_DOWN, self.onRightClick)
 
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(myGrid, 1, wx.EXPAND)
        panel.SetSizer(sizer)
        
    #----------------------------------------------------------------------
    def onRightClick(self, event):
        """"""
        x, y = self.myGrid.CalcUnscrolledPosition(event.GetX(),
                                                  event.GetY())
        row, col = self.myGrid.XYToCell(x, y)
        print row, col
        
if __name__ == "__main__":
    app = wx.App(False)
    frame = MyForm().Show()
    app.MainLoop()

You are probably wondering why we’re binding to the generic wx.EVT_RIGHT_DOWN instead of the grid’s EVT_GRID_CELL_RIGHT_CLICK. The reason is that the grid’s right click event doesn’t give us X/Y coordinates. We need those coordinates to pass to the grid’s CalcUnscrolledPosition method. That in turn will give us the X/Y position relative to the grid to pass to another grid method, namely XYToCell which will return the row and col we right clicked on. This will give us the information we need should we want to create a context menu based on a specific cell.

UPDATE: Some might thing this is doing it the hard way and they may be right. Why? Because while the event object that is sent by EVT_GRID_CELL_RIGHT_CLICK doesn’t have the GetX/GetY methods, it DOES have GetRow and GetCol methods. So you could use it after all. If you want to translate from an arbitrary pixel point, the above method is the way to go. If you want to be a tad cleaner, then you may want to bind to EVT_GRID_CELL_RIGHT_CLICK instead.

Grids and Tooltips

Putting tooltips in grids can be somewhat difficult to figure out. I spent a lot of time on it before finally asking for help on the wxPython mailing list. I wanted to know how to put tooltips in a certain column and make them change as I went over the various cells. The first method suggested worked great until I had to scroll the grid. Fortunately, Robin Dunn (creator of wxPython) had the answer for that issue too. The following code will also show how to put tooltips on column and row labels.

import wx
import wx.grid as  gridlib

class MyForm(wx.Frame):
 
    #----------------------------------------------------------------------
    def __init__(self):
        wx.Frame.__init__(self, None, wx.ID_ANY, "Grid Tooltips")
 
        # Add a panel so it looks the correct on all platforms
        panel = wx.Panel(self, wx.ID_ANY)
        
        self.grid = gridlib.Grid(panel)
        self.grid.CreateGrid(25,8)
        
        # put a tooltip on the cells in a column
        self.grid.GetGridWindow().Bind(wx.EVT_MOTION, self.onMouseOver)
        # the following is equivalent to the above mouse binding
##        for child in self.grid.GetChildren():
##             if child.GetName() == 'grid window':
##                 child.Bind(wx.EVT_MOTION, self.onMouseOver)
        
        # put a tooltip on a column label
        self.grid.GetGridColLabelWindow().Bind(wx.EVT_MOTION, 
                                               self.onMouseOverColLabel)
        # put a tooltip on a row label
        self.grid.GetGridRowLabelWindow().Bind(wx.EVT_MOTION, 
                                               self.onMouseOverRowLabel)
        
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.grid, 1, wx.EXPAND, 5)
        panel.SetSizer(sizer)
        
    #----------------------------------------------------------------------
    def onMouseOver(self, event):
        """
        Displays a tooltip over any cell in a certain column
        """
        # Use CalcUnscrolledPosition() to get the mouse position within the 
        # entire grid including what's offscreen
        # This method was suggested by none other than Robin Dunn
        x, y = self.grid.CalcUnscrolledPosition(event.GetX(),event.GetY())
        coords = self.grid.XYToCell(x, y)
        col = coords[1]
        row = coords[0]
        
        # Note: This only sets the tooltip for the cells in the column
        if col == 1:
            msg = "This is Row %s, Column %s!" % (row, col)
            event.GetEventObject().SetToolTipString(msg)
        else:
            event.GetEventObject().SetToolTipString('')
            
    #----------------------------------------------------------------------
    def onMouseOverColLabel(self, event):
        """
        Displays a tooltip when mousing over certain column labels
        """
        x = event.GetX()
        y = event.GetY()
        col = self.grid.XToCol(x, y)
        
        if col == 0:
            self.grid.GetGridColLabelWindow().SetToolTipString('Column One')
        elif col == 1:
            self.grid.GetGridColLabelWindow().SetToolTipString('Column Two')
        else:
            self.grid.GetGridColLabelWindow().SetToolTipString('')
        event.Skip()
        
    #----------------------------------------------------------------------
    def onMouseOverRowLabel(self, event):
        """
        Displays a tooltip on a row label
        """
        x = event.GetX()
        y = event.GetY()
        row = self.grid.YToRow(y)
        print row
        if row == 0:
            self.grid.GetGridRowLabelWindow().SetToolTipString("Row One")
        elif row == 1:
            self.grid.GetGridRowLabelWindow().SetToolTipString('Row Two')
        else:
            self.grid.GetGridRowLabelWindow().SetToolTipString("")
        event.Skip()
        
#----------------------------------------------------------------------
# Run the program
if __name__ == "__main__":
    app = wx.PySimpleApp()
    frame = MyForm().Show()
    app.MainLoop()

My original use case for putting tooltips on a certain column’s cells was that I had some users that wanted the data I had displayed converted by 10% to reflect a different way of calculating the same thing. I had more users that wanted it left alone. So I compromised by making tooltips that could do a calculation on the cell’s contents and display the other disputed value in the tooltip. Annoying, but potentially handy in the future. The first step in achieving this affect was to figure out the correct binding. Here it is:

self.grid.GetGridWindow().Bind(wx.EVT_MOTION, self.onMouseOver)

The event handler takes care of the next part of the equation via a three-step process. Let’s take a closer look:

def onMouseOver(self, event):
    """
    Displays a tooltip over any cell in a certain column
    """
    # Use CalcUnscrolledPosition() to get the mouse position within the 
    # entire grid including what's offscreen
    # This method was suggested by none other than Robin Dunn
    x, y = self.grid.CalcUnscrolledPosition(event.GetX(),event.GetY())
    coords = self.grid.XYToCell(x, y)
    col = coords[1]
    row = coords[0]
    
    # Note: This only sets the tooltip for the cells in the column
    if col == 1:
        msg = "This is Row %s, Column %s!" % (row, col)
        event.GetEventObject().SetToolTipString(msg)
    else:
        event.GetEventObject().SetToolTipString('')

The first step entails getting the cell’s row and column coordinates using CalcUnscrolledPosition(event.GetX(),event.GetY())/ This will return the x/y coordinates that we can use XYToCell to get access to the actual row/col. The third step would be to grab the cell’s contents using GetCellValue, but since I don’t care about that in this example, I just set the tooltip by using this: event.GetEventObject().SetToolTipString(msg). I thought it was pretty neat. Also note the alternative method for the original binding of the event that I commented out. That was an ugly hack, but it worked as long as I didn’t have a scrollbar (I think).

The next tooltip-related tricks are related in that they both deal with putting tooltips on labels; specifically, you’ll learn how to put tooltips on row and column labels. The binding and the handlers are essentially the same, you just need to specify row or column related method calls. For example, the column binding is GetGridColLabelWindow and the row binding is GetGridRowLabelWindow. The next step is to get the column or row that you’re mousing over, so we use XToCol for the column and YToRow for the row. Finally, we make a call to GetGridColLabelWindow().SetToolTipString to set the column’s label tooltip. The row is almost exactly the same.

Using Arrow Keys to Navigate Out of a Cell Edit

In Microsoft Excel, you can use the keyboard’s arrow keys to stop editing and more to another cell rather than pressing tab or the enter key. Since most user’s are used to this functionality, we should probably have it in our applications. Here’s one way to do it:

import wx
import wx.grid as  gridlib

class MyForm(wx.Frame):
    
    #----------------------------------------------------------------------
    def __init__(self):
        wx.Frame.__init__(self, None, wx.ID_ANY, "Navigating out of a cell")
 
        # Add a panel so it looks the correct on all platforms
        panel = wx.Panel(self, wx.ID_ANY)
        self.grid = gridlib.Grid(panel)
        self.grid.CreateGrid(25,8)
        
        self.grid.Bind(gridlib.EVT_GRID_EDITOR_CREATED, self.onCellEdit)
        
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.grid, 1, wx.EXPAND, 5)
        panel.SetSizer(sizer)
        
    #----------------------------------------------------------------------
    def onCellEdit(self, event):
        '''
        When cell is edited, get a handle on the editor widget
        and bind it to EVT_KEY_DOWN
        '''        
        editor = event.GetControl()        
        editor.Bind(wx.EVT_KEY_DOWN, self.onEditorKey)
        event.Skip()
    
    #----------------------------------------------------------------------
    def onEditorKey(self, event):
        '''
        Handler for the wx.grid's cell editor widget's keystrokes. Checks for specific
        keystrokes, such as arrow up or arrow down, and responds accordingly. Allows
        all other key strokes to pass through the handler.
        '''
        keycode = event.GetKeyCode() 
        if keycode == wx.WXK_UP:
            print 'you pressed the UP key!'
            self.grid.MoveCursorUp(False)
        elif keycode == wx.WXK_DOWN:
            print 'you pressed the down key!'
            self.grid.MoveCursorDown(False)
        elif keycode == wx.WXK_LEFT:
            print 'you pressed the left key!'
            self.grid.MoveCursorLeft(False)
        elif keycode == wx.WXK_RIGHT:
            print 'you pressed the right key'
            self.grid.MoveCursorRight(False)
        else:
            pass
        event.Skip()
        
#----------------------------------------------------------------------
# Run the program
if __name__ == "__main__":
    app = wx.PySimpleApp()
    frame = MyForm().Show()
    app.MainLoop()

Catching key events in a grid’s cell is a little tricky. First you need to bind a handle to the EVT_GRID_EDITOR_CREATED event like this: self.grid.Bind(gridlib.EVT_GRID_EDITOR_CREATED, self.onCellEdit). Then in the handler you need to get a handle on the cell’s editor which is created when you start editing the cell. To do that, you need to use event.GetControl(). Once you have that handle, you can bind wx.EVT_KEY_DOWN to it and catch key events. In the code above, we bind this event to the onEditorKey in which we check to see if the user has pressed one of the arrow keys. If the user did press one of them, then we use the MoveCursorXX method to move the selected cell in the specified direction. If the user did not press an arrow key, then we don’t do anything. Note that we need to call event.Skip() regardless. The reason is that we want the keys to continue to be processed. If you don’t call Skip(), then most of your typing will not do anything.

How to Hide Row and Column Labels

Hiding row or column labels is extremely simple. In fact, that information is supplied in the wxPython FAQ. Paul McNett, one of the creators of Dabo, contributed that information to the wiki. The following is what the wiki has:

grid.SetRowLabelSize(0)
grid.SetColLabelSize(0)

I decided to create a sample application that showed the example in context:

import wx
import wx.grid as  gridlib

class MyForm(wx.Frame):
 
    def __init__(self):
        wx.Frame.__init__(self, None, wx.ID_ANY, "Changing Row/Col Labels")
 
        # Add a panel so it looks the correct on all platforms
        panel = wx.Panel(self, wx.ID_ANY)
        grid = gridlib.Grid(panel)
        grid.CreateGrid(25,8)
        
        # http://bit.ly/aXbeNF - link to FAQ
        grid.SetRowLabelSize(0) # hide the rows
        grid.SetColLabelSize(0) # hide the columns
        
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(grid, 1, wx.EXPAND, 5)
        panel.SetSizer(sizer)

# Run the program
if __name__ == "__main__":
    app = wx.PySimpleApp()
    frame = MyForm().Show()
    app.MainLoop()

Display Something (Dialog/Frame/Whatever) When the User Clicks on a Row Label

I created a timesheet application at work a while back that used the Grid widget and I got a request to allow the user’s to navigate from one part of the program to the other using the Grid’s row labels. You see, I had an overview screen which showed weekly totals and the user’s wanted to be able to just from that screen to the worksheet screen in case they needed to be able to edit their day’s hour totals. Anyway, the following example shows how to make the row labels sensitive to clicks:

import wx
import wx.grid as  gridlib

class MyForm(wx.Frame):
    
    #----------------------------------------------------------------------
    def __init__(self):
        wx.Frame.__init__(self, None, wx.ID_ANY,
                          "Show Dialog on Row Label Click")
 
        # Add a panel so it looks the correct on all platforms
        panel = wx.Panel(self, wx.ID_ANY)
        self.grid = gridlib.Grid(panel)
        self.grid.CreateGrid(25,8)
        
        self.grid.Bind(gridlib.EVT_GRID_LABEL_LEFT_CLICK, self.onRowClick)
        
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.grid, 1, wx.EXPAND, 5)
        panel.SetSizer(sizer)
        
    #----------------------------------------------------------------------
    def onRowClick(self, event):
        """
        Displays a message dialog to the user which displays which row
        label the user clicked on
        """
        # Note that rows are zero-based
        row = event.GetRow()
        dlg = wx.MessageDialog(None, "You clicked on Row Label %s" % row,
                               "Notice",
                               wx.OK|wx.ICON_INFORMATION)
        dlg.ShowModal()
        dlg.Destroy()
        
# Run the program
if __name__ == "__main__":
    app = wx.PySimpleApp()
    frame = MyForm().Show()
    app.MainLoop()

In the code above, all we needed to do was bind to the grid’s EVT_GRID_LABEL_LEFT_CLICK event and then create or show a dialog in the event handler. Simple and easy to follow, I hope.

How to Change Column/Row Labels

Changing a grid’s row or column labels is very easy; but don’t take my word for it. Look at the code instead!

import wx
import wx.grid as  gridlib

class MyForm(wx.Frame):
 
    def __init__(self):
        wx.Frame.__init__(self, None, wx.ID_ANY, "Hiding Rows and Columns")
 
        # Add a panel so it looks the correct on all platforms
        panel = wx.Panel(self, wx.ID_ANY)
        grid = gridlib.Grid(panel)
        grid.CreateGrid(25,8)
        
        # change a couple column labels
        grid.SetColLabelValue(0, "Name")
        grid.SetColLabelValue(1, "Phone")
        
        # change the row labels
        for row in range(25):
            rowNum = row + 1
            grid.SetRowLabelValue(row, "Row %s" % rowNum)
        
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(grid, 1, wx.EXPAND, 5)
        panel.SetSizer(sizer)

# Run the program
if __name__ == "__main__":
    app = wx.PySimpleApp()
    frame = MyForm().Show()
    app.MainLoop()

As you can see, all you need to do to change a column’s label is call SetColLabelValue. The grid also has a similar method for the row: SetRowLabelValue. Just pass in which row you want to change and the string you want it to display and you should be set.

Wrapping Up

Well, that was a lot of code. I hope you’ve learned a lot of cool tricks that you can use in your current and future projects. Let me know what you think in the comments!

Downloads

Further Reading

5 thoughts on “wxPython: Grid Tips and Tricks”

  1. You seem to know your way around the wxGrid! If you (or some of your readers) would want to exercise those wxMuscles, any and all help with PESSheet would be appreciated! It is a free “Python Extendable Spread Sheet”. One tab with the spreadsheet, and one tab with function declarations (in python). Some of the wx-code is a mess, so it is quite easy to find stuff to improve..

    Welcome: http://github.com/offe/pessheet

  2. You seem to know your way around the wxGrid! If you (or some of your readers) would want to exercise those wxMuscles, any and all help with PESSheet would be appreciated! It is a free “Python Extendable Spread Sheet”. One tab with the spreadsheet, and one tab with function declarations (in python). Some of the wx-code is a mess, so it is quite easy to find stuff to improve..

    Welcome: http://github.com/offe/pessheet

  3. If you want to maximize your code’s visibility among wxPython developers, then you should write an announcement for it on the wxPython mailing list! I may take a look as well. Thanks for your readership!

    – Mike

  4. If you want to maximize your code’s visibility among wxPython developers, then you should write an announcement for it on the wxPython mailing list! I may take a look as well. Thanks for your readership!

    – Mike

  5. wxPython: Grid Tips and Tricks thanks to your program it’s some kind a learning to anybody.I will ty this …. thanks alot G-d bless

Comments are closed.