How to build Pmw megawidgets

Introduction

This document briefly describes how to design and code Pmw megawidgets by inheriting from the Pmw base classes. It shows step by step how to build a simple example megawidget. This megawidget allows the user to select one of a range of numbers and it also indicates if the selected number is greater than a given threshold.

Choosing the components

The megawidget will be built using a tkinter.Scale widget to allow the user to select a number in a range, and a tkinter.Frame widget to act as an indicator, displaying red (say) if the selected number exceeds the threshold. It will look something like this:

Scale 2

The programmer using this megawidget will need access to the scale widget, since they will need to set the scale's range. Therefore the scale will be made a component of the megawidget. The programmer will probably not need access to the indicator frame, but, just in case the need arises to change the borderwidth or relief of the indicator, we will make it a component too. This illustrates a convention about components - for maximum configurability, make all sub-widgets components.

Choosing the options

Apart from the component options now available through the scale and indicator components, the megawidget will need a few options of its own. It will need a threshold option to set the threshold. It may also need options to set the colors of the indicator when the selected value is both above and below the threshold. Other options could be orient or indicatorpos to specify the relative position of components and margin, padx or pady to specify spacing between and around the components. For this example, we will define three options - threshold, colors and value. The colors option will be a 2-element sequence specifying two colors (below threshold, above threshold). The value option will be the initial value of the scale.

Coding the megawidget

The first things to do are to decide on a name for the new megawidget, decide which base class to inherit from and to begin to write the constructor. Most Pmw megawidgets are derived from either Pmw.MegaWidget, Pmw.MegaToplevel or Pmw.Dialog. In this case, since the widget is not to be contained within its own toplevel window, we will inherit from Pmw.MegaWidget. The constructors of megawidgets take one argument (the widget to use as the parent of the megawidget's hull, defaulting to the root window) and any number of keyword arguments.

class ThresholdScale(Pmw.MegaWidget):
    """ Megawidget containing a scale and an indicator.
    """
 
    def __init__(self, parent = None, **kw):

Next, we need to define the options supplied by this megawidget. Each option is specified by a 3-element sequence. The first element is the option's name. The second element is the default value. The third element is either a callback function, Pmw.INITOPT or None. In the first case, the function is called at the end of construction (during the call to self.inialiseoptions) and also whenever the option is set by a call to configure. Pmw.INITOPT indicates that the option is an initialisation option - it cannot be set by calling configure. None indicates that the option can be set by calling configure, but that there is no callback function.

The call to self.defineoptions also includes the keyword arguments passed in to the constructor. The value given to any option specified in the keywords will override the default value.

        # Define the megawidget options.
        optiondefs = (
            ('colors',    ('green', 'red'), None),
            ('threshold', 50,               None),
            ('value',     None,             Pmw.INITOPT),
        )
        self.defineoptions(kw, optiondefs)

After defining the options, the constructor of the base class should be called. The options need to be defined first so that a derived class can redefine the default value of an option defined in a base class. This is because the value specified by the derived class must be made available before the base class constructor is called. The keyword arguments should not be passed into the base class constructor since they have already been dealt with in the previous step.

        # Initialise base class (after defining options).
        Pmw.MegaWidget.__init__(self, parent)

Now we should create the components. The components are created as children (or grandchildren ...) of the megawidget's interior.

        # Create the components.
        interior = self.interior()

The first component to create is the indicator. The createcomponent method creates the sub-widget and registers the widget as a component of this megawidget. It takes five arguments plus any number of keyword arguments. The arguments are name, aliases, group, class and constructor arguments. See the Pmw.MegaArchetype reference manual) for full details.

        # Create the indicator component.
        self.indicator = self.createcomponent('indicator',
                (), None,
                tkinter.Frame, (interior,),
                        width = 16,
                        height = 16,
                        borderwidth = 2,
                        relief = 'raised')
        self.indicator.grid()

The scale component is created in a similar way. In this case, the initial value of the scale is also set to the value of the value initialisation option.

        # Create the scale component.
        self.scale = self.createcomponent('scale',
                (), None,
                tkinter.Scale, (interior,),
                        command = self._doCommand,
                        tickinterval = 20,
                        length = 200,
                        from_ = 100,
                        to = 0,
                        showvalue = 0)
        self.scale.grid()
 
        value = self['value']
        if value is not None:
            self.scale.set(value)

At the end of the constructor, the initialiseoptions method is called to check that all keyword arguments have been used (that is, the caller did not specify any unknown or misspelled options) and to call the option callback functions.

        # Check keywords and initialise options.
        self.initialiseoptions()

All other methods must now be defined. In this case, only one method is required - a method called whenever the scale changes and which sets the indicator color according to the threshold.

    def _doCommand(self, valueStr):
        if self.scale.get() > self['threshold']:
            color = self['colors'][1]
        else:
            color = self['colors'][0]
        self.indicator.configure(background = color)

To complete the megawidget, methods from other classes can be copied into this class. In this case, all tkinter.Scale methods not already defined by the megawidget are made available as methods of this class and are forwarded to the scale component. Note that the third argument to Pmw.forwardmethods is the name of the instance variable referring to the tkinter.Scale widget and not the name of the component. This function is called outside of and after the class definition.

Pmw.forwardmethods(ThresholdScale, tkinter.Scale, 'scale')

Important note: If a megawidget defines options using defineoptions(), then this method must be called in the megawidget constructor before the call to the base class constructor and a matching call to initialiseoptions() must made at the end of the constructor. For example:

    def __init__(self, parent = None, **kw):
	optionDefs = ...
	self.defineoptions(kw, optionDefs)
	BaseClass.__init__(self, parent)
	...
	self.initialiseoptions()

Creating instances of the megawidget

The code below creates two of our example megawidgets. The first is created with default values for all options. The second is created with new values for the options. It also redefines some of the options of the components.

# Create and pack two ThresholdScale megawidgets.
mega1 = ThresholdScale()
mega1.pack(side = 'left', padx = 10, pady = 10)

mega2 = ThresholdScale(
        colors = ('green', 'yellow'),
        threshold = 75,
        value = 80,
        indicator_width = 32,
        scale_width = 25)
mega2.pack(side = 'left', padx = 10, pady = 10)

Scale 1

The complete code

The complete code for this example can be seen here.

Exercises

These exercises build on the example presented so far.

  1. Change the call to create mega1 so that the scale widget displays the current value next to the slider. (You may need to look at the Tk scale manual page to find which option to the scale component to set.) You will be able to do this without modifying the ThresholdScale class code.
  2. Add a tkinter.Label component between the indicator and scale components. Modify the _doCommand method so that it displays the current value of the scale in this label.
  3. Modify the colors and threshold options so that they both accept a tuple. Now implement multiple thresholds, so that the indicator displays one of several colors, depending on the value of the scale.
  4. Add an orient initialisation option and lay out the components horizontally or vertically depending on its value.
  5. Read the description of the createlabel() method in the Pmw.MegaArchetype reference manual and add labelpos and labelmargin initialisation options which allow the creation of a label for the megawidget.

An example of how these changes can be made can be seen here.

Contributing your megawidgets to Pmw

If you have completed a megawidget that may be useful to others, you may like to consider contributing it to Pmw. See Contributions welcome for how to contribute.

Pmw coding conventions

As a final note, the Pmw code makes an attempt to follow these coding conventions.

Pmw 2.1 - 31 Dec 2020 - Home