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
Here’s a very simple example. This widget displays the text “hello, world” in the upper left corner:
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:
ui_handle_clear is called to redraw the background
ui_handle_config is called when the widget is (re)configured
ui_handle_damage is called to indicate that some region needs to be redrawn. The widget should redraw itself in ui_handle_repair, not in this method.
ui_handle_destroy is called to release widget resources
ui_handle_focus is called to redraw the focus indicator
ui_handle_repair is called to redraw damaged regions
ui_handle_resize is called when the widget’s geometry is changed
ui_handle_select is called to handle cut buffers/selections
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.
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()
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 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:
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.
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.
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 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:
class HelloWorldFixedFont(HelloWorld): ui_option_font = NOT_INHERITED ui_option_foreground = FOREGROUND ui_option_background = FOREGROUND