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:
- Common API -- generally allow for all the options you associate with wxWindow classes, including standard method signatures. Event generation -- generate comprehensive change notices, often with their own associated event classes. Parent windows can use event mappings to accomplish most interactions with the control. Programmatic API -- allowing for direct manipulation and retrieval of control value, adding/removing items, and altering the functionality of the control. Use styles -- manipulation of general class behavior is controlled through the application of style bitmasks in the style argument to the class constructor. Allow for validators -- although not easy to do for custom-built controls, validators are part of the general interface of controls. Virtual methods/OnX methods -- provide customisation points for commonly subclassed behavior. Documentation -- as should go without saying, reusable composable controls are very seldom useful if they have no accompanying documentation.
Most controls follow one or more of these patterns:
Value editors -- include a "value" argument, GetValue and SetValue methods, and value-changed events (wxTextCtrl, wxCheckBox, wxCalendarCtrl, wxChoice, wxListBox, wxSpinCtrl, wxComboBox) Event generators -- largely unconcerned with value editing, generally provide a source for events through interactions with the user (wxButton, wxScrollBar, wxSlider, wxSpinButton) Visualisers -- largely unconcerned with user interactions, take a value (often continuously updating) and present that value visually (wxGauge, wxStaticBitmap, wxStaticText) List views -- provide customisation points for interacting with collections of objects/choices, providing the ability to associate arbitrary objects with internal structures (wxListCtrl, wxTreeCtrl, wxChoice, wxComboBox)
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:
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:
-- 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.)
-- Terrel Shumway