Updated May 31, 2003 | February 2003 | Fredrik Lundh
The Widget Construction Kit (WCK) is a programming interface that you can use to create new widgets for Tkinter and other toolkits, in pure Python.
In the first article in this series, I showed how to create a very simple widget, how to draw the widget on the screen, and how to keep the widget updated when option values are changed.
In this article, we’ll look at widget geometry issues, and briefly discuss how to handle mouse and keyboard events.
In this article:
Widget Geometry #
The simple widget we used in the first article didn’t contain any code to set the widget’s size. That doesn’t stop the WCK from giving it a size, though; if you don’t say anything, the framework will set the size to 100x100 pixels.
To request another size, you can calculate the size in the ui_handle_config method, and return a tuple containing the requested width and height.
In the following example, I’ve added two options that can be used to set the widget size, in pixels. I’ve also added code to the ui_handle_config method:
from Tkinter import Tk from WCK import Widget class MyTextWidget(Widget): ui_option_text = "" ui_option_font = "times" ui_option_color = "black" ui_option_width = 100 ui_option_height = 100 def ui_handle_config(self): self.font = self.ui_font(self.ui_option_color, self.ui_option_font) return int(self.ui_option_width), int(self.ui_option_height) 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!", width=200) widget.pack() root.mainloop()
Note the use of int to make sure the options are integer objects. This allows the user to pass in option values as strings, just like when using Tkinter.
Also note that the framework will interpret the return value from ui_handle_config as a request, rather than an absolute requirement. The geometry manager will assign an actual size, basing its decision on the available space, the requested sizes of all other widgets managed by the same manager, and the manager configuration settings.
To figure out the current size of the widget from within the widget implementation, you can use the coordinates passed to the ui_handle_repair method.
Another approach is to implement the ui_handle_resize method. This method is called every time the widget size is changed, and can be used to recalculate parameters related to the widget’s actual geometry.
Here’s a more extensive example. The ui_handle_config method now calculates the actual size of the text (determined by a call to the font object’s measure method), and uses it to request a suitable size for the widget. The ui_handle_resize method uses the text size and the current widget size to calculate where to draw the text:
from Tkinter import Tk from WCK import Widget class MyTextWidget(Widget): ui_option_text = "" ui_option_font = "times" ui_option_color = "black" ui_option_width = 0 ui_option_height = 0 def ui_handle_config(self): self.font = self.ui_font( self.ui_option_color, self.ui_option_font ) # calculate text size width, height = self.font.measure(self.ui_option_text) self.textsize = width, height # calculate widget size width = max(width, int(self.ui_option_width)) height = max(height, int(self.ui_option_height)) return width, height def ui_handle_resize(self, width, height): # center the text w, h = self.textsize self.pos = (width - w) / 2, (height - h) / 2 def ui_handle_repair(self, draw, x0, y0, x1, y1): draw.text(self.pos, self.ui_option_text, self.font) root = Tk() widget = MyTextWidget(root, text="hello!") widget.pack(expand=1, fill="both") root.mainloop()
If you run this example and resize the window, the widget will be resized by the geometry manager (based on the expand and fill settings), but the text will stay centered.
Handling User Events #
Note (May 2003): This section has been modified to use only standard controllers (EventMixin, ButtonMixin). Information on how to create your own controllers will appear in a later article.
The WCK provides ui_handle methods for most events generated by the windowing system (e.g. redraw requests), but keyboard and mouse events are handled via separate widget controller classes.
For simple widgets, WCK provides a standard controller that maps incoming events to DOM/DHTML-style method calls. The standard controller supports the following events:
- onkey(event)
-
A key was pressed. The event object contains more information about the key (see below).
- onclick(event)
-
A mouse button was pressed and released. The event object contains more information about the mouse button (see below).
- onmousedown(event)
-
A mouse button was pressed.
- onmouseup(event)
-
A mouse button was released.
- onmouseover(event)
-
The mouse pointer was moved into the widget.
- onmousemove(event)
-
The mouse pointer was moved when inside the widget.
- onmouseout(event)
-
The mouse pointer was moved out of the widget.
To use the standard controller, modify your widget so it inherits both the EventMixin mix-in class and the Widget class. An example:
from WCK import EventMixin, Widget class MyWidget(EventMixin, Widget): def onclick(self, event): print "click!"
The event structure contains information about the event, including the following attributes:
- widget
-
The target widget. For keyboard events, this is the widget that has the keyboard focus. For mouse events, this is where the mouse pointer is.
- x, y
-
For mouse events, where in the widget the mouse pointer was when the event was generated (relative to the upper left corner of the widget).
- num
-
For mouse events, the button number.
- char
-
For keyboard events, the character(s) generated by the key press. For special keys (function keys, shift, etc), this attribute is set to an empty string. Use the keysym attribute instead.
- keysym
-
For keyboard events, the key symbol (the name of the key).
A Simple Button Widget
Here’s a somewhat larger example, which shows how to use a controller to provide behavior for a simple button-like widget.
from WCK import Widget, EventMixin class MyTextWidget(Widget): ... see above ... class MyButtonWidget(EventMixin, MyTextWidget): ui_option_relief = "raised" ui_option_borderwidth = 2 ui_option_command = None def onclick(self, event): if callable(self.ui_option_command): self.ui_option_command() root = Tk() def callback(): print "Click!" widget = MyButtonWidget(root, text=" click me ", command=callback) widget.pack() root.mainloop()
Note that the MyButtonWidget class reuses the text widget from above, but overrides the relief and borderwidth options to give it a more “button-like” appearance.
Adding Visual Feedback
A problem with the previous example is that it registers button clicks, but it doesn’t provide any kind of visual feedback to the user. No matter how much she clicks, the button just appears to sit there.
The following example uses additional controller methods to deal with this. When the mouse button is pressed, the onmousedown method changes the border style (the relief option) to indicate that the button is ‘armed’. When the button is released, the onmouseup method restores the border.
from WCK import Widget, EventMixin class MyTextWidget(Widget): ... see above ... class MyButtonWidget(EventMixin, MyTextWidget): ui_option_relief = "raised" ui_option_borderwidth = 2 ui_option_command = None def onmousedown(self, event): self.__relief = self.ui_option_relief self.config(relief="sunken") def onmouseup(self, event): self.config(relief=self.__relief) def onclick(self, event): if callable(self.ui_option_command): self.ui_option_command() root = Tk() def callback(): print "Click!" widget = MyButtonWidget(root, text=" click me ", command=callback) widget.pack() root.mainloop()
Native buttons tend to use a slightly more sophisticated controller; the button is still ‘armed’ when you press the mouse button over the widget, but you can ‘disarm’ the button by moving the pointer out of the widget before you release the mouse button.
To get this behavior, you can replace your controller with the standard button controller provided by the WCK. The controller is similar to the one used in the EventMixin class, but it calls different methods:
class MyButtonWidget(ButtonMixin, Widget): ... def ui_button_arm(self): self.__relief = self.ui_option_relief self.config(relief="sunken") def ui_button_disarm(self): self.config(relief=self.__relief) def invoke(self): if callable(self.ui_option_command): self.ui_option_command()
The standard button controller also provides ui_button_enter and ui_button_leave methods, which are called when the mouse pointer enters and leaves the button.