Differences between revisions 28 and 29
Revision 28 as of 2002-03-01 12:59:29
Size: 11236
Editor: anonymous
Comment: missing edit-log entry for this revision
Revision 29 as of 2002-10-11 16:15:34
Size: 12942
Editor: anonymous
Comment: missing edit-log entry for this revision
Deletions are marked like this. Additions are marked like this.
Line 47: Line 47:
 
Line 163: Line 163:
----
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
wxText``Ctrl, and then get the value, something that results in very nasty program behavior (ie. crashes.)

A more proper example would be:
{{{
#!python
 def Validate(self, window): # (window is parent window for control in question)
  ctrl =wxPyTypeCast(self.GetWindow(), "wxTextCtrl")
  try:
   value = float( ctrl.GetValue())
   return true
  except ValueError:
   return false
}}}

Further, in order to be complete, the validator example really needs the methods '''Translate``To``Window()''' and
'''Translate``From``Window()''', so a control using the validator can be properly used in a wxDialog. The reason
for this is that wxDialog automatically calls wxPanel::Init``Dialog (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 wxPy``Validator 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:
{{{
#!python
        def TransferToWindow(self):
                return true
        def TransferFromWindow(self):
                return true
}}}


-- ''Will Sadkin, 10/11/2002''
Line 166: Line 202:
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  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
Line 188: Line 224:
    
Line 197: Line 233:
This allows the user (programmer) to store the data the way he thinks is best,  This allows the user (programmer) to store the data the way he thinks is best,
Line 202: Line 238:
wxPython/lib/mvctree.py is a decent example. The fancy footwork for rendering  wxPython/lib/mvctree.py is a decent example. The fancy footwork for rendering
Line 204: Line 240:
would not have been as necessary, if the underlying toolkit had implemented MVC  would not have been as necessary, if the underlying toolkit had implemented MVC

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, string
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 not type(value) in ( types.FloatType, types.IntType, types.LongType):
                        raise ValueError ('''FloatControl requires float values, passed %s'''%repr(value))
                return str(value)
        def fromGUI( self, value ):
                return string.atof( 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:

Toggle line numbers
   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 TranslateToWindow() and TranslateFromWindow(), 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:

Toggle line numbers
   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.

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

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.