A wxPython model PyGrid TableBase

The class wx.grid.Grid is the wxPython control for a spreadsheet-style layout of rows and columns. You're probably familiar with the basic concept, but figure 5.3 shows how the wxPython version looks.

The grid control has a lot of interesting features, including the ability to create custom renderers and editors on a cell-by-cell basis, as well as dragable rows and columns. Those features will be discussed in greater detail in chapter 13. In this chapter, we'll stick to the basics and show how to use a model to populate a grid. Listing 5.6 shows the simple non-model way of setting the cell values in a grid. In this case, the grid values are the lineup for the 1984 Chicago Cubs.

Wxpython Listings
Figure 5.3 A sample of the wxPython grid control

Listing 5.6 Populating a grid without models import wx import wx.grid class SimpleGrid(wx.grid.Grid)

def _init_(self, parent)

wx.grid.Grid._init_(

self.CreateGrid(9, 2) self.SetColLabelValue( self.SetColLabelValue( self.SetRowLabelValue( self.SetCellValue(0, 0 self.SetCellValue(0, 1 self.SetRowLabelValue( self.SetCellValue(1, 0 self.SetCellValue(1, 1 self.SetRowLabelValue( self.SetCellValue(2, 0 self.SetCellValue(2, 1 self.SetRowLabelValue( self.SetCellValue(3, 0 self.SetCellValue(3, 1 self.SetRowLabelValue( self.SetCellValue(4, 0 self.SetCellValue(4, 1 self.SetRowLabelValue( self.SetCellValue(5, 0 self.SetCellValue(5, 1 self.SetRowLabelValue( self.SetCellValue(6, 0 self.SetCellValue(6, 1 self.SetRowLabelValue( self.SetCellValue(7, 0 self.SetCellValue(7, 1 self.SetRowLabelValue( self.SetCellValue(8, 0 self.SetCellValue(8, 1

"First") "Last") "CF") "Bob") "Dernier")

"2B") "Ryne") "Sandberg")

"LF") "Gary") "Matthews")

"1B") "Leon") "Durham")

"RF") "Keith") "Moreland")

"C") "Jody") "Davis")

"SS") "Larry") "Bowa")

"P") "Rick") "Sutcliffe")

class TestFrame(wx.Frame):

wx.Frame._init_(self, parent, -1, "A Grid"

size=(275, 275)) grid = SimpleGrid(self)

app = wx.PySimpleApp() frame = TestFrame(None) frame.Show(True) app.MainLoop()

In listing 5.6, we have the class SimpleGrid, a subclass of the wxPython class wx.grid.Grid. As mentioned earlier, wx.grid.Grid has oodles of methods that we're going to discuss later. For now, we'll focus on the SetRowLabelValue(), Set-ColLabelValue(), and SetCellValue() methods which are actually setting the values displayed in the grid. As you can see by comparing figure 5.3 and listing 5.6, the SetCellValue() method takes a row index, a column index, and a value, while the other two methods take an index and a value. The row and column labels are not considered part of the grid for the purposes of assigning indexes to the cells.

This code directly assigns values to the grid using the setter methods. While this method has an admirable directness, it can become tedious and error-prone on larger grids. And even if we were to create utility methods to ease the burden, the code would still have the problem we saw in the refactoring section of this chapter. The data would be intertwined with the display in a way that would make future modifications to the code—such as adding a column or swapping the data out completely—difficult.

The answer is wx.grid.PyGridTableBase. As with other classes we've seen thus far, the Py prefix indicates that this is a Python-specific wrapper around a C + + class. Like the PyEvent class we saw in chapter 3, the PyGridTableBase class is implemented as a simple Python wrapper around a wxWidgets C++ class specifically for the purpose of allowing Python subclasses to be declared. A PyGridTableBase is a model class for a grid. That is, it contains methods that the grid object can use to draw itself, without having to know about the internal structure of that data.

Methods of PyGridTableBase

The wx.grid.PyGridTableBase has several methods, many of which you will not have to deal with. The class is abstract and cannot be instantiated directly. You will have to provide an implementation of five required methods every time you create a PyGridTableBase. Table 5.4 describes the methods.

Table 5.4 Required methods of wx.grid.PyGridTableBase

Method

Description

GetNumberRows()

Returns an integer indicating the number of rows in the grid.

GetNumberCols()

Returns an integer indicating the number of columns in the grid.

IsEmptyCell(row, col)

Returns True if the cell at index (row, col) is empty.

Table 5.4 Required methods of wx.grid.PyGridTableBase (continued)

Method

Description

GetValue(row, col)

Returns the value that should be displayed at the cell (row, col).

SetValue(row, col, value)

Sets the value associated with (row, col). If you want a read-only model, you still must include this method, but you can have it pass.

The table is attached to the grid by using the SetTable() method of the grid. After that property is set, the grid object will call the methods of the table to get the information it needs to draw the grid. The grid will no longer expect to have the values explicitly set with grid methods.

Using a PyGridTableBase

In general, there are two ways to use a PyGridTableBase. You can explicitly have your model class be a subclass of PyGridTableBase, or you can create a separate PyGridTableBase subclass that connects to your actual model class. The first option is easier and makes sense when your data is not very complex. The second option enforces a stronger separation between the Model and the View, which is preferable if your data is complex. The second option is also preferred if you have a pre-existing data class that you want to adapt into wxPython, because you can create a table without changing the existing code. We'll show an example of both options in this section.

Using a PyGridTableBase: application-specific subclass

Our first example will use an application-specific subclass of PyGridTableBase as our model. This works because our lineup example is relatively straightforward, so we can directly incorporate the data into a class derived from PyGridTableBase. We'll set up the actual data in a two-dimensional Python list, and set up the other methods to read from that list. Listing 5.7 shows the Cubs lineup generated from a Model class.

Listing 5.7 A table generated from a PyGridTableBase model import wx import wx.grid class LineupTable(wx.grid.PyGridTableBase):

data = (("CF", "Bob", "Dernier"), ("2B", "Ryne", "Sandberg"), ("LF", "Gary", "Matthews"), ("1B", "Leon", "Durham"), ("RF", "Keith", "Moreland"), ("3B", "Ron", "Cey"),

("C", "Jody", "Davis"), ("SS", "Larry", "Bowa"), ("P", "Rick", "Sutcliffe"))

wx.grid.PyGridTableBase._init_(self)

def GetNumberRows(self): return len(self.data)

def GetNumberCols(self):

def GetColLabelValue(self, col): return self.colLabels[col]

def GetRowLabelValue(self, row): return self.data[row][0]

def IsEmptyCell(self, row, col): return False def GetValue(self, row, col):

return self.data[row][col + 1]

def SetValue(self, row, col, value): pass class SimpleGrid(wx.grid.Grid):

| Table set here class TestFrame(wx.Frame):

wx.Frame._init_(self, parent, -1, "A Grid", size=(275, 275)) grid = SimpleGrid(self)

app = wx.PySimpleApp() frame = TestFrame(None) frame.Show(True) app.MainLoop()

In listing 5.7, we've defined all the required PyGridTableBase methods, plus the additional methods GetColLabelValue() and GetRowLabelValue(). Hopefully you will not be too surprised to learn that these methods allow the table to specify the column and row labels, respectively. As in the refactoring section, the effect of using the model class is to separate the data from the display. In this case, we've also moved the data into a more structured format, which could easily be separated to an external file or resource (a database would be particularly easy to add here).

Using a PyGridTableBase: a generic example

In fact, we're very close to having a generic table that can read any two-dimensional Python list. Listing 5.8 shows what the generic model would look like.

Listing 5.8 A generic table for two-dimensional lists import wx import wx.grid class GenericTable(wx.grid.PyGridTableBase):

def _init_(self, data, rowLabels=None, colLabels=None):

wx.grid.PyGridTableBase._init_(self)

self.data = data self.rowLabels = rowLabels self.colLabels = colLabels def GetNumberRows(self): return len(self.data)

def GetNumberCols(self):

return len(self.data[0])

def GetColLabelValue(self, col): if self.colLabels:

return self.colLabels[col]

def GetRowLabelValue(self, row): if self.rowLabels:

return self.rowLabels[row]

def IsEmptyCell(self, row, col): return False def GetValue(self, row, col): return self.data[row][col]

def SetValue(self, row, col, value) pass

The GenericTable class takes a two-dimensional list of data and an optional list of row and/or column headers. It's suitable to be imported into any wxPython program. With a slight change in the data format, we can now use the generic table to display the lineup, as in listing 5.9.

Listing 5.9 The lineup display using the generic table import wx import wx.grid import generictable data = (("Bob", "Dernier"), ("Ryne", "Sandberg"), ("Gary", "Matthews"), ("Leon", "Durham"), ("Keith", "Moreland"), ("Ron", "Cey"), ("Jody", "Davis"), ("Larry", "Bowa"), ("Rick", "Sutcliffe"))

rowLabels = ("CF", "2B", "LF", "1B", "RF", "3B", "C", "SS", "P")

class SimpleGrid(wx.grid.Grid):

tableBase = generictable.GenericTable(data, rowLabels, colLabels) self.SetTable(tableBase)

class TestFrame(wx.Frame):

wx.Frame._init_(self, parent, -1, "A Grid", size=(275, 275)) grid = SimpleGrid(self)

app = wx.PySimpleApp() frame = TestFrame(None) frame.Show(True) app.MainLoop()

Using a PyGridTableBase: a standalone Model class

At the risk of being repetitive, there is one more way to use the PyGridTableBase that is worth showing here. This is the second option alluded to earlier, where the data is kept in a separate model class which is accessed by the PyGridTableBase. Python's self-inspection capabilities are very useful here, allowing you to make a list of the attributes that are displayed in each column and then use the built-in function getattr() to retrieve the actual value. In this case, the model takes a list of elements. Structuring your program with separate model objects has one big advantage in wxPython. Under normal circumstances, you can only call SetTable() once for a grid—if you want to change the table, you need to create a new grid, and that can be annoying. However, if, as in the next example, your PyGridTableBase only stores references to instances of your real data class, then you can update the table to new data by just changing the underlying data object in the table.

Listing 5.10 shows the PyGridTableBase using a separate data class for the lineup entries we've been displaying—we'll spare you another listing of the frame and data creation itself, as it's quite similar to the previous ones.

Listing 5.10 The lineup display table using a custom data class import wx import wx.grid class LineupEntry:

self.pos = pos self.first = first self.last = last class LineupTable(wx.grid.PyGridTableBase):

colLabels = ("First", "Last") colAttrs = ("first", "last")

The column headers b The attribute names def init (self, entries): <1—i , ... ,. .

— .T" , . . , 1 Initializing wx.grid.PyGridTableBase._init_(self) the model self.entries = entries def GetNumberRows(self):

return len(self.entries)

def GetNumberCols(self): return 2

def GetColLabelValue(self, col):

return self.colLabels[col] <— Reading the value of the header def GetRowLabelValue(self, row):

return self.entries[row].pos Q Reading the row header def IsEmptyCell(self, row, col): return False def GetValue(self, row, col):

entry = self.entries[row] ^^ Re.ad.'ng the return getattr(entry, self.colAttrs[col]) " 1 attribute value def SetValue(self, row, col, value): pass

O This list contains the attributes that need to be referenced to display the values column by column.

© The model takes a list of entries where each entry is an instance of the Lineup-Entry class. (We're not doing any error checking or validation here).

d To get the row header, we look up the pos attribute of the entry in the proper row.

O The first step here is getting the correct entry based on the row. The attribute is taken from the list in line O, and then the getattr() built-in is used to reference the actual value. This mechanism is extensible even in the case where you don't know if the name refers to an attribute or a method by checking to see if <object>.<attribute> is callable(). If it is, then call it using normal Python function syntax, and return that value.

The grid class is an example where wxPython already has a valuable model component to help you structure your application. The next section will discuss how to create model components for other wxPython objects.

Was this article helpful?

+4 -1

Responses

  • venla
    How to set column size in wxGrid wxWidget in C?
    8 years ago
  • May Chubb-Baggins
    What is pygridtablebase?
    8 years ago

Post a comment