Building Controls

Creating new wxPython windows is a simple task. You override a few methods, maybe catch a few events and your class does what you want it to do. You can distribute this new "control", people can use it, life is good. What this document attempts to describe is how to go beyond the basics of a functional window into creating a reusable control.

It should be noted that this document is describing "best practice" as understood by the author, rather than current practice as exemplified by the wxWindows developers. Certain of the built in controls provide counterexamples to the advice here.

Controls normally have the following properties:

Most controls follow one or more of these patterns:

Creating a Control from a Window

For our working example, let's take a very simple control candidate, a simple floating point value editor control. This will be a simple subclassing of the wxTextControl to provide conversion into and from float values. Here is the "window level" class.

from wxPython.wx import *
import types
class FloatControl (wxTextCtrl):
        def __init__ (self, parent, value= 0.0):
                wxTextCtrl.__init__( self, parent,-1, self.toGUI(value))
        def get(self):
                return self.fromGUI( self.GetValue())
        def set(self, value):
                self.SetValue(self.toGUI(value))

        def toGUI( self, value ):
                if type(value) not in ( types.FloatType, types.IntType, types.LongType):
                        raise ValueError ('''FloatControl requires float values, passed %r'''%value)
                return str(value)
        def fromGUI( self, value ):
                return float( value )

As you can see, this is a fairly simple implementation. It allows for basic getting and setting of the float value, with the editing by the user. Within a particular application, you can use this control to provide editing of a particular float value and be quite happy with its operation.

Common API

Unfortunately, our FloatControl is not a particularly reusable control. It does not follow the common API conventions for wxPython controls (and particularly, value editing controls).

Initializers in wxPython controls, generally provide a fairly complete signature of this basic pattern:

        def __init__ (
                self, parent, id=-1, [value = 0.0,]
                pos = wxDefaultPosition, size = wxDefaultSize,
                [choices=[]],
                style = 0, validator= wxDefaultValidator, name = "float",
        ):

Where the [] brackets indicate optional elements in the control's argument list. The names of the "value" and "choices" arguments are subject to variation, as are (potentially) their position within the argument list. When building new controls for use by other developers, is generally a good idea to follow this pattern (including the order of arguments), so that those developers can guess at the appropriate position and names for the values to be passed.

Similarly, value editing controls generally have two methods, "GetValue" and "SetValue" which are used to get the current value of the control. As you can see above, we're actually using these methods to get the current value of the text control in our "get" and "set" methods. A user of the control, however, would expect that the GetValue method would return the current float value, and would not expect the ValueError that would be raised were they to use the SetValue method with a float value. To make the control match the value editing control pattern, we override the expected method names:

        def GetValue(self):
                return self.fromGUI( wxTextCtrl.GetValue(self))
        def SetValue(self, value):
                wxTextCtrl.SetValue(self, self.toGUI(value))

Event Generation

Event driven wxPython programming relies on controls to inform their parents of changes to their internal state. In our (simplified) example, the only change to our internal state in which we're interested is the entry of a new, valid float. In a real world example, we would also want to inform the parent of invalid floats.

Although the wxTextCtrl class does not provide for using the event to retrieve the new value, we will provide this functionality. I would generally suggest following this pattern to minimize the dependencies between parent and child classes (the parent need only deal with the event, it does not need to know about the particular implementation details of the child).

Our event generating code is standard boilerplate:

wxEVT_FLOAT_UPDATED = wxNewId()
def EVT_FLOAT(win, id, func):
        '''Used to trap events indicating that the current
        float has been changed.'''
        win.Connect(id, -1, wxEVT_FLOAT_UPDATED, func)
class FloatUpdatedEvent(wxPyCommandEvent):
        def __init__(self, id, value =0.0):
                wxPyCommandEvent.__init__(self, wxEVT_FLOAT_UPDATED, id)
                self.value = value
        def GetValue(self):
                '''Retrieve the value of the float control at the time this event was generated'''
                return self.value

You'll notice that I have not used the function name EVT_FLOAT_UPDATED for the binding function. This was a conscious choice based on the name of the EVT_TEXT binding function. Arguably you would want to include EVT_FLOAT_UPDATED = EVT_FLOAT to allow for a more predictable set of names when using this control.

Machinery within the control which generates these events in response to user edits is also fairly standard:

        def __init__ (
                self, parent, id=-1, value = 0.0,
                pos = wxDefaultPosition, size = wxDefaultSize,
                style = 0, validator= wxDefaultValidator, name = "float",
        ):
                wxTextCtrl.__init__(
                        self, parent, id, self.toGUI(value),
                        pos, size, style, validator, name,
                )
                # following lets us set out our float update events
                EVT_TEXT( self, self.GetId(), self.OnText )
        def OnText( self, event ):
                '''Handle an event indicating that the text control's value
                has changed.  Because the incoming event does not provide for
                getting the value of the string, we just use our GetValue method.
                '''
                try:
                        self.GetEventHandler().ProcessEvent(
                                FloatUpdatedEvent(
                                        self.GetId(),
                                        self.GetValue()
                                )
                        )
                except ValueError:
                        # here is where you would add handling for
                        # values which are not valid floats
                        pass
                # let normal processing of the text continue
                event.Skip()

Programmatic API

Don't really see much need for a programmatic API for this simple a control.

Styles

Again, could use some suggestions here...

Validators

The float control should be able to use validators without any extra work (since it is a subclass of a text control). Here is a sample validator...

class FloatValidator( wxPyValidator ):
        def Clone (self):
                return self.__class__()
        def Validate(self, window):
                window =wxPyTypeCast(window, "wxTextCtrl")
                try:
                        value = float( window.GetValue())
                        return true
                except ValueError:
                        return false

Composite Controls

Composite controls are the most "involved" of the control types. In this pattern, multiple "sub controls" interact by sending events to a parent window which then dispatches those events (or their results) to the registered sub controls. It is possible to create entire classes of control configurations in this manner, allowing the user to choose which particular functionality and display are required for their particular application.

In this section, we will work with a considerably more complex example, a composite color editing control, which will provide (potentially multiple) "color cube" controls, RGB float editing controls, and visualisers for the current and original color values.

Reader Comments


The Validate() method in the example of the Validators section above is incorrect; the window passed to this method is the *parent* of the control in question, not the control that needs to be validated. If, for instance, your float control was placed on a wxDialog, the above code would be trying to cast the wxDialog as a wxTextCtrl, and then get the value, something that results in very nasty program behavior (ie. crashes.)

A more proper example would be:

   1         def Validate(self, window):        # (window is parent window for control in question)
   2                 ctrl =wxPyTypeCast(self.GetWindow(), "wxTextCtrl")
   3                 try:
   4                         value = float( ctrl.GetValue())
   5                         return True
   6                 except ValueError:
   7                         return False

Further, in order to be complete, the validator example really needs the methods TranferToWindow() and TransferFromWindow(), so a control using the validator can be properly used in a wxDialog. The reason for this is that wxDialog automatically calls wxPanel::InitDialog (wxPanel does not) which, in turn, calls the Transfer functions to allow each validator to move data in and out of the relevant dialog fields.

What the validator overview does not mention is that the base wxPyValidator class returns false for these methods, so you get annoying popups saying "Could not transfer data to window" if you don't override them. However, unless you're doing something complicated, it is usually sufficient simply to include:

   1         def TransferToWindow(self):
   2                 return true
   3         def TransferFromWindow(self):
   4                 return true

-- Will Sadkin, 10/11/2002

Model View Controller separation

If you are going to the trouble of building a reuseable control, please consider separating the data model from the view and controller. e.g. instead of

class MyCoolListControl(...):
    def __init__(self,parent,id,choices):
        ...
    def InsertItem(...):
    def AppendItem(...):

where choices is a static list, do something like this

class ListDataModel:
    """ a minimal model interface to be used by MyCoolListControl """
    def __len__(self):
        """ return the number of items in the list """
    def __getitem__(self,index):
        """ return the specified item """

    # consider notifications for observers (e.g. the list control)

    # allow the user to define whatever mutators are appropriate


class MyCoolListControl(...):
    def __init__(self,parent,id,model):
        ...
    def [GS]etModel(self,model):
        ...

This allows the user (programmer) to store the data the way he thinks is best, rather than having to copy the data into your structure whenever it changes. It also reduces the need for kludges like [GS]etItemData for maintaining association of the real data with its currently displayed string representation.

wxMVCTree (in wxPython/lib/mvctree.py) is a decent example. The fancy footwork for rendering (basically it reimplents the tree control in python) would not have been as necessary, if the underlying toolkit had implemented MVC to begin with. (MSWindows' tree control is nasty to work with natively.)

BuildingControls (last edited 2010-12-13 23:20:35 by 208)

NOTE: To edit pages in this wiki you must be a member of the TrustedEditorsGroup.