Updated May 31, 2003 | February 2003 | Fredrik Lundh
The Widget Construction Kit (WCK) is a simple programming interface that you can use to create new widgets in Python.
The WCK is currently available for the Tkinter library, but implementations for other environments are perfectly possible.
This effbot.org article introduces the library, and shows how to create a very simple widget. Future articles will teach you more about the art of widget making.
In this article:
- Installing the Library
- Testing the Library
- Writing Your First Widget
- Precomputing Graphics Objects
- Widget Options
Installing the Library #
For the examples in this article series, I will use the Tkinter 3000 implementation of the WCK.
Here’s how to install the Tkinter 3000 WCK on your computer:
-
Make sure you have a working installation of
Tkinter(dead link). -
Download the Tkinter 3000 source code. The most current version is available from the effbot.org downloads site. For this article series, I’m using the 1.0 release (but any 1.0 beta should work, unless otherwise noted).
-
Unpack it into a temporary build directory.
-
Build the library, in
the usual way(dead link).
If your system has wget, unzip, and a working C compiler, you can use the following commands to download, build and install the library:
$ wget http://effbot.org/downloads/tkinter3000-1.0-20031212.zip $ unzip tkinter3000-1.0-20031212.zip $ cd tkinter3000-1.0-20031212 $ python setup.py install
You can of course use other tools to download and unpack the library (e.g. your favourite browser, and unzipping tools like winzip). If you’re on Windows, you may find prebuilt versions at the effbot.org downloads site.
Testing the Library #
The Tkinter 3000 WCK ships with a number of demo scripts (in files named demoSomething.py. Before you start writing your own widget, it’s a good idea to try one or more of the demos, to make sure everything is properly installed.
Some examples:
$ python demoScrolledText.py $ python demoLissajou.py
Writing Your First Widget #
The Tkinter 3000 implementation of the WCK consists of a single public module, called WCK. This module provides a number of base classes and helper functions, including the Widget base class.
Note (May 2003): In releases before 1.0b4, the WCK code was located in a module named tk3. In newer releases, the code has been moved to a package, and should be accessed via the WCK module. Just replace tk3 with WCK in your programs (or use import WCK as tk3). The tk3 module is still available, but will most likely be removed in a future release.
(Other implementations should provide the same WCK components, but via a different module or namespace.)
The Widget base class implements a generic WCK widget. By default, the widget behaves pretty much like a Tkinter Frame widget; it has a configurable background color and a border style, but has no other behavior.
To create a more expressive widget, you have to create a subclass of the Widget base class, and implement necessary drawing and event handling methods. The framework will call methods in your subclass when the widget needs to be redrawn, when the widget is resized, when the widget is reconfigured, and so on.
For example, the ui_handle_repair method is called every time the widget needs to be redrawn:
from WCK import Widget class SomeWidget(Widget): def ui_handle_repair(self, draw, x0, y0, x1, y1): ...
This method is called when the widget is first displayed, and later on when the window manager decides that the widget contents have been “damaged” (for example, when the window becomes visible after having been covered by another window).
The method takes five arguments:
ui_handle_repair(draw, x0, y0, x1, y1)
The first argument, draw, provides an interface to the underlying graphics library. All WCK implementations allow you to draw text, lines, rectangles, and other 2D graphic elements via this interface. (Some implementations offer more functionality, including image support and 3D graphics. More on this in a later article.)
The rest of the arguments are coordinates (in pixel units) for the upper-left and lower-right coordinates of the widget’s drawing area.
Here’s a simple widget implementation:
from Tkinter import Tk from WCK import Widget class MyTextWidget(Widget): def ui_handle_repair(self, draw, x0, y0, x1, y1): font = self.ui_font("black", "times") draw.text((0, 0), "hello!", font) root = Tk() widget = MyTextWidget(root) widget.pack() root.mainloop()
In this example, the ui_handle_repair method calls the WCK’s ui_font method to obtain a font object for the “times” font, and passes the string “hello!” and the font object to the text method of the drawing object.
Running this example produces a window looking something like:
Starting with release 1.0b4, the Tkinter WCK supports Unicode strings. In earlier versions, you can pass in text as standard 8-bit strings, encoded in ASCII or UTF-8.
The Tkinter implementation also handles strings encoded in ISO-8859-1 (Latin-1).
Precomputing Graphics Objects #
In the first example, a font object is created for each call to ui_handle_repair. For a simple widget like this, creating new objects every time the widget needs to be redrawn is usually not much of a problem; the object factory is reasonably fast, and Python’s garbage collector will make sure we’re not leaking objects.
However, if you need to use lots of fonts in your widget, a more efficient approach would be to create all font objects at once, and reuse the same objects every time the widget is redrawn.
One way to do this is to add a custom constructor (an __init__ method), and create the font object when the widget itself is created. Adding a constructor also allows you to pass in the text and the font as arguments:
from Tkinter import Tk from WCK import Widget class MyTextWidget(Widget): def __init__(self, master, text="", font="times", color="black"): Widget.__init__(self, master) self.font = self.ui_font(color, font) self.text = text def ui_handle_repair(self, draw, x0, y0, x1, y1): draw.text((0, 0), self.text, self.font) root = Tk() widget = MyTextWidget(root, text="hello!") widget.pack() root.mainloop()
Note that the base class constructor must be called before calling any other WCK methods. If you don’t do this, ui_font will most likely raise an exception.
Widget Options #
The previous example has a possibly serious problem; once you’ve created an instance of the widget, there’s no way to change the text or the font. You could solve this by adding configuration methods (e.g. settext, setfont), but that can quickly become unwieldy. A simpler approach is to use the WCK’s Tkinter-style option database interface.
Widgets that use the option database behave like standard Tkinter widgets. You can pass in options when you create the widget, and you can use the config method to change the configuration later on, if you need. You can also use cget to fetch the current value of an option.
To make a your widget support a given option, all you have to do is to add a class attribute to your widget subclass. The class attribute should have a name of the form ui_option_name, where name is the name of the option, and the value should be the default value for the corresponding widget option.
class MyWidget(Widget): ui_option_name = "default"
When an instance of this widget is created, the framework creates an instance attribute with the same name as the class attribute. For example, if you call the widget constructor with a value for the name option, the value will be assigned to the ui_option_name instance attribute:
widget = MyWidget(root, name="a better name") # peek inside the widget print widget.ui_option_name
The ui_option attributes should be treated as “protected” attributes. Don’t access them from outside the widget implementation.
To get the current value for an option, use the standard Tkinter-style interface:
widget = MyWidget(root, name="a better name") # use the official tkinter-style api print widget.cget("name")
Inside the widget implementation, you should always access the option value via the self instance variable:
class MyWidget(Widget): def getname(self): return self.ui_option_name
It’s important that you always use Python’s standard mechanims for attribute access. The reason for this is that a WCK implementation may choose to save some space by falling back on the the class attribute for options that are not set, and also for options that are explicitly set to their default value. If you access the attributes via self, Python will automatically do the right thing in those cases.
:::
To allow the user to change the option value at any time, you need to make sure that your widget is always using the current value for each option.
One way to do this is to revert to the approach used in the first example; simply create new graphics objects every time you need them.
def ui_handle_repair(self, draw, x0, y0, x1, y1): font = self.ui_font( self.ui_option_color, self.ui_option_font ) draw.text((0, 0), self.ui_option_text, font)
A better approach is to track changes to the options, and recompute the graphics objects every time the configuration changes, instead of doing it every time the widget needs to be updated. The framework provides a special method for this purpose, called ui_handle_config. This method is called when the widget is first created, and every time the configuration is changed.
ui_handle_config()
Note that the ui_handle_config method does not take any arguments; if you need to check if a given option has changed, you may need to store the current value somewhere yourself, and compare it with the new value inside the config method.
The following example uses option attributes for the text, font, and color options, and a ui_handle_config method that is used to to update the font object when necessary:
from Tkinter import Tk from WCK import Widget class MyTextWidget(Widget): ui_option_text = "" ui_option_font = "times" ui_option_color = "black" def ui_handle_config(self): self.font = self.ui_font( self.ui_option_color, self.ui_option_font ) def ui_handle_repair(self, draw, x0, y0, x1, y1): draw.text((0, 0), self.ui_option_text, self.font) root = Tk() widget = MyTextWidget(root, text="hello!") widget.pack() widget.config(color="blue", background="white") root.mainloop()
The background option used in the config example is a standard option, inherited from the Widget base class. The base class provides the following standard options:
- background
-
The widget’s background color. By default, the background is drawn in the system’s standard background color.
- relief
- borderwidth
- highlightthickness
-
Controls the appearance of the widget border. (the default is no border and no focus highlight).
- cursor
-
What mouse cursor to show when the mouse pointer is moved over the widget. If not set, the widget will use the system’s default cursor (usually an arrow).