Tkinter Tricks: Using WCK-Style Controllers in Tkinter
Fredrik Lundh | November 2005 | Originally posted to online.effbot.org
In an earlier article, I described the Controller mechanism in the Widget Construction Kit (WCK).
The WCK uses different classes for code that draws a widget, and code that implements interactive behaviour. The main widget class is responsible for drawing the widget, but all interactive behaviour is provided by a separate controller class.
The tkController module provides a similar mechanism for plain Tkinter. Just like in the WCK, you subclass the Controller class, and manipulate the target widget via attributes on the event instance. Once you have a working controller, you can attach it to any compatible widget.
To see this in action, here’s a Tkinter version of the first example from the earlier article. Here, the MyController class responds to button clicks by printing a message to the console.
from tkController import Controller from Tkinter import Canvas class ClickController(Controller): def create(self, bind): bind("<Button-1>", self.click) def click(self, event): print "CLICK!", event.x, event.y # try it out canvas = Canvas() canvas.pack() controller = ClickController() controller.install(canvas) canvas.mainloop()
Since this controller don’t really depend on the widget itself, you can attach it to any Tkinter widget. To track mouse movements and clicks, a Frame widget works as well as a Canvas.
Here’s a more advanced example, based on the WCK LineController example. This controller adds a line item to the canvas when you press the mouse button over the canvas, updates it as long as you keep the button pressed, and redraws it as a 2-pixel wide red line when you’re done:
from Tkinter import Canvas from tkController import Controller class LineController(Controller): def create(self, bind): bind("<ButtonPress-1>", self.press) bind("<B1-Motion>", self.motion) bind("<ButtonRelease-1>", self.release) def press(self, event): self.anchor = event.x, event.y self.item = None def motion(self, event): xy = self.anchor + (event.x, event.y) if self.item is None: self.item = event.widget.create_line(xy) else: event.widget.coords(self.item, xy) def release(self, event): xy = self.anchor + (event.x, event.y) event.widget.coords(self.item, xy) event.widget.itemconfig(self.item, fill="red", width=2) print "LINE", xy # try it out canvas = Canvas(bg="white") canvas.pack() canvas_controller = LineController() canvas_controller.install(canvas) canvas.mainloop()
This controller uses the event.widget attribute to locate the current widget. This lets you use the same controller instance in multiple controllers:
canvas1 = Canvas(bg="white") canvas1.pack(side="left") canvas2 = Canvas(bg="white") canvas2.pack(side="left") canvas_controller = LineController() canvas_controller.install(canvas1) canvas_controller.install(canvas2) canvas.mainloop()
This controller uses Canvas-specific methods to do the drawing, which limits its use to Canvas widgets. While it may be overkill in this case, you can reduce the coupling somewhat by defining a more “abstract” interface, and move more of the implementation over to the widget itself.
Here’s a slightly refactored version of the LineController, where the controller uses rubberband_line and add_line methods to do the drawing, and a Canvas subclass is used to provide the actual implementation.
from Tkinter import Canvas from tkController import Controller class LineController(Controller): def create(self, bind): bind("<ButtonPress-1>", self.press) bind("<B1-Motion>", self.motion) bind("<ButtonRelease-1>", self.release) def press(self, event): self.anchor = event.x, event.y def motion(self, event): event.widget.rubberband_line(self.anchor, (event.x, event.y)) def release(self, event): event.widget.add_line(self.anchor, (event.x, event.y)) class MyCanvas(Canvas): item = None def rubberband_line(self, start, end): if self.item is None: self.item = self.create_line(start, end) self.coords(self.item, start + end) def add_line(self, start, end): self.delete(self.item); self.item = None self.create_line(start, end, fill="red", width=2) # try it out canvas = MyCanvas(bg="white") canvas.pack() canvas_controller = LineController() canvas_controller.install(canvas) canvas.mainloop()
With this design, you can use the same controller on different kind of widgets. The new methods might also be useful for other tools.
The earlier article also discussed the tracker model, which uses multiple controllers to implement more complex tool behaviour. I’ll get back to this in a later article.