Learning to cook

(G)-rated techniques for loving wx.ListCtrl

Learning to cook

1. What flavour of ObjectListView do I want to use?

There are three flavours of ObjectListView (four if you include GroupListView):

ObjectListView - Plain Vanilla

_images/icecream3.jpg

Stock standard, but still very nice

A normal ObjectListView is capable of anything a ListCtrl can do – only in a much easier to use package. Though it will handle a large number of rows, it is better suited for smaller lists, that is, about 1000 rows or less.

If in doubt, start with this flavour. You can always change it to one of the others later on.

VirtualObjectListView - Expresso

_images/coffee.jpg

Has a slight bitter taste, but makes you capable of anything

When you want to knock your users out with 10 million search results in a ListCtrl, a VirtualObjectListView is for you. It can handle any number of objects.

But it does so at a cost:

  • You have to implement your own sorting method.
  • You cannot use object level commands like SelectObject or RefreshObject since the list has no way of knowing where any given model object is in the list.

But for the cost of this “bitterness,” you really can do anything.

FastObjectListView - Red Bull

_images/redbull.jpg

Also makes you capable of anything, but without the bitterness

When you want speed, speed, and more speed, but you don’t want the bitterness of the VirtualObjectListView, a FastObjectListView is your weapon of choice.

It operates just like a normal ObjectListView – only much faster.

Did I mention the speed?

GroupListView

A GroupListView is slightly different type of beast. You would use this creature when you want your users to be able to group your model objects into different “partitions” depending on the column they last clicked.

See Using a GroupListView for more information.

2. How do I let the user edit the values shown in the list?

This really needs a page to itself: Cell Editing in an ObjectListView

3. How can I stop the user from making a column too narrow or too wide?

Columns have both minimumWidth and maximumWidth attributes. By default, these are -1, which means that no limit is enforced. But if they are set to some other value, the column will be limited to the given minimum or maximum width.

For example, this:

self.titleColumn.minimumWidth = 30

will stop the “Title” column from becoming less than 30 pixels in width. This is useful for preventing users from shrinking columns to 0 width and then not being about to find them again.

4. How can I stop the user from resizing a column?

There are some columns just don’t make sense to be resizable. A column that always shows a 16x16 status icon makes no sense to be resizable. To make a column be fixed width and unresizable by the user, you can create the column with a fixedWidth parameter:

self.statusColumn = ColumnDefn("", imageGetter=statusImageGetter, fixedWidth=16)

Or, if you decide after column creation that the column should be fixed width, you can call SetColumnFixedWidth:

self.olv1.SetColumnFixedWidth(0, 16) # the first column is fixed to 16 pixel wide

5. How can I make a column get bigger when the ObjectListView is made bigger?

On most columns, the column’s width is static, meaning that it doesn’t change by itself. But sometimes it would be useful if a column would resize itself to show more (or less) of itself when the user changed the size of the ListCtrl. For example, the rightmost column of a personnel list might display “Comments” about that person. When the window was made larger, it would be nice if that column automatically expanded to show more of the comments about that person. You can make this happen by setting the isSpaceFilling attribute to True on that column.

An ObjectListView can have more than one space filling column, and they generally share the available space equally between them (see the freeSpaceProportion attribute to change this).

You should be aware that as the ObjectListView becomes smaller, the space filling columns will become smaller too, until they eventually disappear (have zero width). The minimumWidth and maximumWidth attributes still work for space filling columns. So you can use the minimumWidth attribute to make sure that space filling columns don’t disappear.

6. How can I change the colours of a row?

You install a rowFormatter on the ObjectListView.

A rowFormatter is a callable that accepts two parameters: a wx.ListItem, and the model object for that wx.ListItem. The rowFormatter can change the formatting of the list item, or any of its other properties.

To show customers in red when they owe money, you could do this:

def rowFormatter(listItem, customer):
    if customer.amountOwed > 0:
        listItem.SetTextColour(wx.RED)

self.objectListView1.rowFormatter = rowFormatter

Due to the vaguarities of virtual lists, rowFormatter for a VirtualObjectListView operates slightly differently. Instead of being given a wx.ListItem, it is given a wx.ListItemAttr object. These object only support a limited subset of wx.ListItem method, specifically those members related to the background color, text colour and font. See wx.ListItemAttr for more details of what is supported.

7. How do I use checkboxes in my ObjectListView?

ObjectListView allows several ways of using check boxes. The simplest way is to call CreateCheckStateColumn:

self.objectListView1.CreateCheckStateColumn()

This creates a new column in the control that is solely for the purpose of showing a checkbox for each row. By default, this column is the first column (you can pass an integer to CreateCheckStateColumn to create the column at a different position). It results in something that looks like this:

_images/cookbook-checkbox1.png

If you don’t want to have a specific column just for the checkbox, you can use an existing column as the check box column. To do this, call InstallCheckStateColumn with the column defn you want to show the check boxes. Be aware that a column can only have one image, so that column will have the checkbox as its image, and will ignore anything you might have set up with the imageGetter attribute.

So, if we installed the “Title” column as a checkbox column:

self.objectListView1.InstallCheckStateColumn(self.titleColumn)

It would produce something different:

_images/cookbook-checkbox2.png

Each track now has the checkbox image instead of the track image.

The user can change checkboxes by clicking on the check box (obviously) or by selecting one or more rows and pressing Space. Pressing Space toggles the values of the selected rows. Actually, it toggles the top-most check box, and the sets all the other rows to have the same value.

Data-based Checkboxes

Both of these methods install checkboxes where the “checked-ness” of the rows are specific to that ObjectListView. So if the same model object was visible in two different lists, it could be checked in one but not in the other.

But sometimes, the “checked-ness” of a row is part of the model object itself. Consider a customer management system, where customers could be marked as active or not. This isActive property should be the same regardless of which listview was showing the customer.

To define that checkbox column where the data comes from the model object, you would give that column a checkStateGetter attribute:

isActiveColumn = ColumnDefn("Active?", fixedWidth=24, checkStateGetter="isActive")

Or instead of giving the checkbox its own column, you could combine the checkbox with the customers name:

nameColumn = ColumnDefn("Name", valueGetter="name", checkStateGetter="isActive")

Though this would stop the “Name” column from having its own image.

ObjectListViews support multiple check box columns.

8. How do I change the message that’s shown when the ObjectListView is empty?

When an ObjectListView is empty, it can display a “this list is empty” type message. You alter the text and its font using the following methods:

self.objectListView1.SetEmptyListMsg("This database has no rows")
self.objectListView1.SetEmptyListMsgFont(wx.FFont(24, wx.DEFAULT, face="Tekton"))

9. How can I show a list of dictionaries in the ObjectListView?

I have a list of dictionaries that hold the data I want to show. How can I show them in an ObjectListView?

In your ColumnDefn, set the valueGetter to the key of the data you want to display in that column. Everything should just work. As a bonus, your dictionary will be automatically updated when the user edits a cell value (if the ObjectListView is editable).

Example:

self.listOfDictionaries = [
    { "title":"Shiver", "artist": "Natalie Imbruglia", "album":"Counting Down the Days" },
    { "title":"Who's Gonna Ride Your Wild Horses", "artist": "U2",  "album":"Achtung Baby" },
    { "title":"So Cruel", "artist": "U2",  "album":"Achtung Baby" },
    { "title":"The Fly", "artist": "U2",  "album":"Achtung Baby" }
]
self.myOlv.SetColumns([
    ColumnDefn("Title", "left", -1, "title"),
    ColumnDefn("Artist", "left", -1, "artist"),
    ColumnDefn("Album", "center", -1, "album")
])
self.myOlv.SetObjects(self.listOfDictionaries)

10. How can I reference the column in a valueGetter function?

I’ve got a super-duper valueGetter function, but it needs to know which column it’s being used for. How can I do that?

Normally, valueGetter functions don’t know which column they are being used for. But there could be cases where you might want to know the column: for example, you might have a central getter function that decides that to do based on which column is being used.

So, imagine our super valueGetter looks like this:

def MySuperValueGetter(modelObject, columnDefn):
    # Do something clever here
    return value

There are (at least) three possible solutions:

a) Use functools.partial():

import functools

for column in self.olv1.columns:
    column.valueGetter = functools.partial(MySuperValueGetter, columnDefn=column)

This only works with Python 2.5 and later.

b) Use local functions and default parameters:

for column in self.olv1.columns:
    def myFunc(modelObject, col=column):
        return MySuperValueGetter(modelObject, col)
    column.valueGetter = myFunc

c) Subclass ColumnDefn and override GetValue():

class MyColumnDefn(ColumnDefn):

    def GetValue(self, modelObject):
        return MySuperValueGetter(modelObject, self)

11. How can I change the way group headers look?

Whoever decided on the colour scheme for group headers was an artistic incompetent. I want to use my own snazzy scheme. How do I do that?

The formatting of group headers is strictly limited. wx.ListCtrl’s do not support any form of owner drawing, so anything snazzy is currently impossible. Don’t even think about trying to do gradient fills or fancy text effects – it’s just not possible.

What you can do is:

  • change the colour of the text, via the groupTextColour variable.
  • change the colour of background of the entire row, via the groupBackgroundColour variable. You can’t change just the group header background. It is the whole row or nothing.
  • change the font of the header via the groupFont variable. Remember that row height is fixed, so if you make the font too big, the text will be truncated. The header row will not become bigger.

12. How can I only redraw the control every N seconds?

I’m writing a network monitor app. In some circumstances, the model objects can be updated 100 times or more each second. But if I try to update the ObjectListView that often, the application grinds to a halt. Is there is an easy way to make the ObjectListView not redraw so often?

Yes. You can use a BatchedUpdate adapter. This wraps an ObjectListView such that no matter how often you update it, it will redraw at most once every N seconds (you supply the value of N).

So in your network monitor app, you need to added a line like this some time after the ObjectListView is created and before it is used:

self.olv = ObjectListView.BatchedUpdate(self.olv, SECONDS_BETWEEN_UPDATES)

This wraps the original olv with an Adapter that intercepts some of the model updating commands (SetObjects() or AddObjects() or RefreshObjects()) so that the control only redraws once every SECONDS_BETWEEN_UPDATES seconds (SECONDS_BETWEEN_UPDATES is a constant defined by you somewhere else).

See Examples/BatchedUpdateExample.py for a demonstration and here for class docs.