The Widget Construction Kit

The Widget Construction Kit (WCK) is an extension API that allows you to implement all sorts of custom widgets, in pure Python.

The WCK is based on PythonWare’s uiToolkit‘s extension API, and is designed to let you run new widgets under other toolkits as well (including uiToolkit, of course)

Widgets vs. Megawidgets

There are several other Tkinter-based toolkits out there which allows the user to build custom widgets, including Tix and Pmw.

Tix allows you to create new “megawidgets” by combining existing widgets (e.g. a label and an entry widget), and make them behave as any other widget class on the Tk level.

Pmw does the same thing, on the Tkinter level.

In contrast, the Tkinter 3000 WCK allows you to implement new Tkinter widgets in Python, instead of writing them in C or C++.

The Tkinter 3000 approach has several advantages, including:

  • A WCK widget is a single widget (window). You don’t need to delegate option settings, method calls, or events to subwidgets.

  • The drawing API takes Python objects, not Tcl objects. You don’t have to wait for the Tkinter layer to convert your data for you - just draw it directly from the Python data structures.

  • Things like double buffering and lazy redrawing (though idletasks) are optional. If speed is more important than anything else, you can draw directly to the window.

  • The WCK uses ordinary Python classes, and inheritance and object composition work as usual. No need to learn another object model.

The Widget Construction Kit

Widget Interface

To implement a new Tkinter 3000 widget, all you have to do is to subclass the Widget class, and implement one or more methods from the Widget interface:

Here’s a very simple example. This widget displays the text “hello, world” in the upper left corner:

A very simple WCK widget
class HelloWorld(Widget):

    def ui_handle_repair(self, draw, x0, y0, x1, y1):
        font = self.ui_font("black", "times")
        draw.text((0, 0), "hello, world", font)

widget = HelloWorld(root)
widget.pack()

You can override the following Widget methods:

All methods have default implementations, but you probably want to override at least ui_handle_repair.

In the following example, the widget uses ui_handle_config to prepare the text and font attributes when the widget’s options are changed, and ui_handle_resize to handle changes to the widget’s size.

A slightly more advanced widget
class HelloWorld(Widget):

    ui_option_message = "hello, world"

    ui_option_font = FONT

    ui_option_foreground = FOREGROUND
    ui_option_background = BACKGROUND

    def ui_handle_config(self):
        self.text = str(self.ui_option_message)
        self.font = self.ui_font(
            self.ui_option_foreground,
            self.ui_option_font
        )
        return self.font.measure(self.text)

    def ui_handle_resize(self, width, height):
        w, h = self.font.measure(self.text)
        self.offset = (width - w) / 2, (height - h) / 2

    def ui_handle_repair(self, draw, x0, y0, x1, y1):
        draw.text(self.offset, self.text, self.font)

widget = HelloWorld(root, font="helvetica")
widget.pack()

Graphics Library

The ui_handle_repair method is called with a draw object, which are used to draw on the screen. This object implements the Draw interface:

  • line draws a line consisting of one or more line segments.

  • rectangle draws a rectangle.

  • polygon draws a polygon.

  • ellipse draws an ellipse.

  • text draws text.

  • textsize measures the size of a text fragment.

  • image draws an image.

Most methods in the Draw interface take graphics resource objects, such as brushes, fonts, etc. To create such objects, you should use the following methods in the Widget interface:

  • ui_pen creates a pen resource object.

  • ui_brush creates a brush resource object.

  • ui_font creates a font resource object.

  • ui_image creates an image resource object.

Resource objects are opaque, and can only be used in the widget for which they were allocated. The framework may cache resources and use them for several widgets, but you shouldn’t rely on that.

Controllers

Controllers are used to provide interactive behaviour for widgets. A controller receives mouse and keyboard events, and uses them to update the widget.

To implement a controller, subclass the Controller class, and bind events to actions in the create method.

Here’s a simple example:

A very simple controller
class ClickController(Controller):

    def create(self, bind):
        bind("<Button-1>", self.handle_click)

    def handle_click(self, event):
        widget = event.widget
        widget.invoke()

The create method is called to initialize the controller. It’s interface is a bit unusual: it’s called with a function that should be used to associate callbacks with events.

In this example, the create function adds a single event handler. When the user presses the left mouse button (button 1), the handle_click method is called with an event descriptor.

Controller instances are shared between widgets of the same type. To figure out what widget the event came from, the event handler must use the event structure’s widget attribute.

The widget implementation might look something like this:

class SimpleButton(Widget):

    ui_option_command = None

    ui_controller = ClickController

    ...

    def invoke(self):
        if callable(self.ui_option_command):
            self.ui_option_command()

To associate a controller with a widget class, set the ui_controller attribute in the widget class. When present, this attribute must point to a Controller subclass.

Option Management

Widget options are implemented using attributes of the form ui_option_name. For each option, just set the default values in the widget class, and the framework will take care of the rest.

Defining options
class HelloWorld(Widget):

    ui_option_font = FONT

    ui_option_foreground = FOREGROUND
    ui_option_background = FOREGROUND

The above code fragment defines three options, font, foreground, and background. The widget will also accept any standard option provided by the Widget base class (this includes borderwidth, cursor, and a few others)

To change options after the widget has been created, use the config method as usual. Do not modify the attributes directly; the widget framework will not detect such changes, and anything may happen.

Option inheritance

Option attributes are inherited, like all other Python attributes. But unlike attributes, you can hide an inherited option by assigning the NOT_INHERITED constant to it in the subclass:

Hiding options
class HelloWorldFixedFont(HelloWorld):

    ui_option_font = NOT_INHERITED

    ui_option_foreground = FOREGROUND
    ui_option_background = FOREGROUND
 

A Django site. rendered by a django application. hosted by webfaction.