What will we cover?
In this topic we look at how a GUI program is assembled in a general sense, then how this is done using Python's native GUI toolkit, Tkinter. This will not be a full blown Tkinter reference nor even a complete tutorial. There is already a very good and detailed tutor linked from the Python web site (One small problem is that the documentation uses Python v2 which has different module names so you'll need to check the Python module list to get the correct names for your imports). This tutorial will instead try to lead you through the basics of GUI programming, introducing some of the basic GUI components and how to use them. We will also look at how Object Oriented programming can help organize a GUI application.
The first thing I want to say is that you won't learn anything new about programming here. Programming a GUI is exactly like any other kind of programming, you can use sequences, loops, branches and modules just as before. What is different is that in programming a GUI you usually use a Toolkit and must follow the pattern of program design laid down by the toolkit provider. Each new toolkit will have its own set of modules, classes and functions, known as its Application Programming Interface or API. It will also have a set of design rules and you as a programmer need to learn both the API and design rules. This is why most programmers try to standardize on only a few toolkits which are available across multiple languages - learning a new toolkit tends to be much harder than learning a new programming language!
Most windows programming languages come with a toolkit included (usually a thin veneer over the very primitive toolkit built into the windowing system itself). Visual Basic, Delphi and Visual C++/.NET are examples of this.
Java is different in that the language includes its own graphics toolkit (actually more than one!) which runs on any platform that Java runs on - which is almost any platform!
There are other toolkits that you can get separately which can be used on any OS (Unix, Mac, Windows...). These generally have adapters to allow them to be used from many different languages. Some of these are commercial but many are freeware. Examples are: GT/K, Qt, Tk, and wxWidgets.
They all have web sites and all support Windows, MacOS and Linux. For some examples try:
Qt and GT/k are what most Linux applications are written in and are both free for non commercial use (i.e. where you don't sell your programs for profit). Qt can provide a commercial license too if you want to use it for commercial purposes and GTk is licensed under the Gnu GPL which has its own special terms.
The standard Python graphics toolkit (i.e. it comes with the language) is Tkinter which is based on Tk, a fairly old multi-OS toolkit. This is the toolkit we will look at most closely. Versions of it are available for Tcl, Haskell, Ruby and Perl as well as Python.
The principles in Tk are slightly different to other toolkits so I will conclude with a very brief look at another popular GUI toolkit for Python (and C/C++) which is more conventional in its approach. But first, some general principles:
As we have already stated several times GUI applications are nearly always event driven in nature. If you don't remember what that means go back and look at the event driven programming topic.
I will assume that you are already familiar with GUIs as a user and will focus on how GUI programs work from a programmer's perspective. I will not be going into details of how to write large complex GUIs with multiple windows, MDI interfaces etc. I will stick to the basics of creating a single window application with some labels, buttons, text boxes and message boxes.
First things first, we need to check our vocabulary. GUI programming has its own set of programming terms. The most common terms are described in the table below:
Term | Description |
---|---|
Window | An area of the screen controlled by an application. Windows are usually rectangular but some GUI environments permit other shapes. Windows can contain other windows and frequently every single GUI control is treated as a window in its own right. |
Control | A control is a GUI object used for controlling the application. Controls have properties and usually generate events. Normally controls correspond to application level objects and the events are coupled to methods of the corresponding object such that when an event occurs the object executes one of its methods. The GUI environment provides a mechanism for binding events to methods. |
Widget |
A control, sometimes restricted to visible controls. Some
controls (such as timers) can be associated with a given
window but are not visible. Widgets are that subset of
controls which are visible and can be manipulated by the
user or programmer. The widgets that we shall cover are:
The ones we won't discuss in this topic but are used elsewhere in the tutor are:
|
Frame | A type of widget used to group other widgets together. Often a Frame is used to represent the complete window and further frames are embedded within it. |
Layout | Controls are laid out within a Frame according to a particular set of rules or guidelines. These rules form a Layout. The Layout may be specified in a number of ways, either using on-screen coordinates specified in pixels, using relative position to other components (left, top etc) or using a grid or table arrangement. A coordinate system is easy to understand but difficult to manage when a window is resized etc. Beginners are advised to use non-resizable windows if working with coordinate based layouts. |
Parent/Child | GUI applications tend to consist of a hierarchy of widgets/controls. The top level Frame comprising the application window will contain sub frames which in turn contain still more frames or controls. These controls can be visualized as a tree structure with each control having a single parent and a number of children. In fact it is normal for this structure to be stored explicitly by the widgets so that the programmer, or more commonly the GUI environment itself, can often perform some common action to a control and all of its children. For example, closing the topmost widget results in all of the child widgets being closed too. |
One very important principle to grasp in GUI programming is the idea of a containment hierarchy. That is, the widgets are contained in a tree like structure with a top level widget controlling the entire interface. It has various child widgets which in turn may have children of their own. Events arrive at a child widget and if it is unable to handle it it will pass the event to its parent and so on up to the top level. Similarly if a command is given to draw a widget it will send the command on down to its children, thus a draw command to the top level widget will redraw the entire application whereas one sent to a button will likely only redraw the button itself.
This concept of events percolating up the tree and commands being pushed down is fundamental to understanding how GUIs operate at the programmer level. It is also the reason that you always need to specify a widget's parent when creating it, so that it knows where it sits in the containment tree. We can draw the containment tree for a simple application that we will create later in this topic like this:
This illustrates the top level widget containing a single Frame which represents the outermost window border. This in turn contains two more Frames, the first of which contains a Text Entry widget and the second contains the two Buttons used to control the application. We will refer back to this diagram later in the topic when we come to build the GUI.
In this section we will use the Python interactive prompt to create some simple windows and widgets. Note that because IDLE is itself a Tkinter application you cannot always reliably run Tkinter applications within IDLE. You can of course create the files using IDLE as an editor and run them in the IDE as usual, but you may find that unexpected things happen (but you may be lucky!). If things are behaving strangely you should run the applicatoion from a OS command prompt before trying anything eldse, it may well just be a conflict between IDLE's internat Tkinter framework and your app's version. Only if that doesn't fix things should you start looking for bugs in your code. Pythonwin users can usually run Tkinter applications directly without any issues since PythonWin does not use Tkinter internally. However, even within Pythonwin there are certain unexpected behaviors with Tkinter applications. As a result I always use the raw Python prompt from an OS terminal window.
>>> from tkinter import *
This is the first requirement of any Tkinter program - import the names of the widgets. You should of course just import the module but it quickly gets tiring typing tkinter in front of every component name. However, you can use an alias, like so:
>>> import tkinter as tk
Meaning you only need to prefix everything with tk. However, since we are only experimenting here, it is more convenient to just import everything for now.
>>> top = Tk()
This creates the top level widget in our widget hierarchy. Notice the case used in Tk. The module is all lower case but the widget name is capitalised. All other widgets will be created as children of the top widget.
What happened at this point will depend on where you are typing the program. If, like me, you are using Python from an OS prompt you will have seen that a new blank window has appeared complete with an empty title bar save for a Tk logo as icon and the usual set of control buttons (iconify, maximize etc). If you are using an IDE you may or may not not see anything yet, it might only appear when we complete the GUI and start the main event loop running.
We will now add components to this window as we build an application. First, let's take a peek at what's on offer.
>>> dir(top) [....lots of stuff!...]
The dir() function shows us what names are known to the argument. You can use it on modules but in this case we are looking at the internals of the top object, an instance of the Tk class. These are the attributes of top, and there are a lot of them! Take a look, in particular, for the children and master attributes which are the links to the widget containment tree. Note also the attribute _tclCommands, this is because, as you might recall, Tkinter is built on a Tcl toolkit called Tk.
>>> F = Frame(top)
Create a Frame widget which will in turn contain the child controls/widgets that we use. Frame specifies top as its first (and in this case only) parameter thus signifying that F will be a child widget of top. We can easily check that:
>>> top.children {'!frame': <tkinter.Frame object .!frame>} >>> F.master <tkinter.Tk object .>
You can see that top.children is a dictionary mapping a slightly weird name ('!frame') to an object reference. (Don't worry about the strange looking name formats, that's just a Tcl/Tk thing carried through to Tkinter internally).
F.master is just a reference back to top. Feel free to experiment with the master and children attributes of our widgets as we go through this example, it will help you make the connection from the widgets to the containment tree we discussed earlier.
>>> F.pack()
Notice that the Tk window (if it's visible) has now shrunk to the size of the added Frame widget - which is currently empty so the window is now very small! (You should however still be able to resize it with your mouse and iconify/deiconify it etc.)
The pack() method invokes a Layout Manager known as the packer which is very easy to use for simple layouts but becomes a little clumsy as the layouts get more complex. We will stick with it for now because it is easy to use. The packer, by default, just stacks widgets one on top of the other. Note that widgets will not be visible in our application until we pack them (or use another Layout Manager method). We will talk a lot more about Layout Managers later on, after we complete this short program.
>>> lHello = Label(F, text="Hello world")
Here we create a new object, lHello, an instance of the Label class, with a parent widget F and a text attribute of "Hello world". Notice that because Tkinter object constructors tend to have many parameters (each with default values) it is usual to use the named parameter technique of passing arguments to Tkinter objects. Also notice that the object is not yet visible because we haven't packed it yet.
One final point to note is the use of a naming convention: I put a lowercasel, for Label, in front of a name, Hello, which reminds me of its purpose. Like most naming conventions this is a matter of personal choice, but I find it helps.
>>> lHello.pack()
Now we can see it. Hopefully your's looks quite a lot like this:
We can specify other properties of the Label such as the font and color using parameters to the object constructor too. We can also access the corresponding properties using the configure method of Tkinter widgets, like so:
>>> lHello.configure(text="Goodbye")
The message changed. That was easy, wasn't it? configure is an especially good technique if you need to change multiple properties at once because they can all be passed as arguments. However, if you only want to change a single property at a time, as we did above you can treat the object like a dictionary, thus:
>>> lHello['text'] = "Hello again"
which is shorter and arguably easier to understand.
Labels are pretty boring widgets, they can only display read-only text, albeit in various colors, fonts and sizes. (In fact they can be used to display simple graphics too but we won't bother with that here).
Before we look at another object type there is one more thing to do and that's to set the title of the window. We do that by using a method of the top level widget top:
>>> F.master.title("Hello")
We could have used top directly but, as we'll see later, access through the Frame's master property is a useful technique.
>>> bQuit = Button(F, text="Quit", command=F.quit)
Here we create a new widget - a button. The button has a label "Quit" and is associated with the command F.quit. Note that we pass the method name, we do not call the method by adding parentheses after it. This means we must pass a function object in Python terms, it can be a built-in method provided by Tkinter, as here, or any other function that we define. The function or method must take no arguments. The quit method, like the pack method, is defined in a base class and is inherited by all Tkinter widgets, but is usually called at the top window level of the application.
>>> bQuit.pack()
Once again the pack method makes the button visible. (Although, depending on your system setup, you may need to resize the window to see it!) If you try pressing it though, nothing happens. That's because we don't yet have an event loop running to capture the button-click event and process it.
>>> top.mainloop()
And finally we start the Tkinter event loop. Notice that the Python >>> prompt has now disappeared. That tells us that Tkinter now has control. If you press the Quit button the prompt will return, proving that our command option worked. Don't expect the window to close, the python interpreter is still running and we only quit the mainloop function, the various widgets will be destroyed when Python exits - which in real programs is usually immediately after the mainloop terminates!
Note that if running this from Pythonwin or IDLE you may not have seen anything until this point! And you may get a slightly different result, if so try typing the commands so far into a Python script and running them from an OS command prompt.
In fact, it's probably a good time to try that anyhow, after all, it's how most Tkinter programs will be run in practice. Use the principle commands from those we've discussed so far as shown (and use the preferred import style):
import tkinter as tk # set up the window itself top = tk.Tk() F = tk.Frame(top) F.pack() # add the widgets lHello = tk.Label(F, text="Hello") lHello.pack() bQuit = tk.Button(F, text="Quit", command=F.quit) bQuit.pack() # set the loop running top.mainloop()
The call to the top.mainloop method starts the Tkinter event loop generating events. In this case the only event that we catch will be the button press event which is connected to the F.quit method. F.quit in turn will terminate the application and this time the window will also close because Python has also exited. Try it, it should look like this:
Notice that I missed the line that changes the window title. Try adding that line in by yourself and check that it works as expected.
Note: from now on I'll provide examples as Python script files rather than as commands at the >>> prompt. In most cases I'll only be providing snippets of code so you will have to put in the calls to Tk() and the mainloop() yourself, use the previous program as a template.
In this section I want to look at how Tkinter positions widgets within a window. We already have seen Frame, Label and Button widgets and those are all we need for this section. In the previous example we used the pack method of the widget to locate it within its parent widget. Technically what we are doing is invoking Tk's packer Layout Manager. (Another name for Layout Manager is Geometry Manager.) The Layout Manager's job is to determine the best layout for the widgets based on hints that the programmer provides, plus constraints such as the size of the window as controlled by the user. Some Layout managers use exact locations within the window, specified in pixels normally, and this is very common in Microsoft Windows environments such as Visual Basic. Tkinter includes a placer Layout Manager which can do this too via a place method. I won't look at that in this tutor because usually one of the other, more intelligent, managers is a better choice, since they take the need to worry about what happens when a window is resized away from us, as programmers.
The simplest Layout Manager in Tkinter is the packer which we've been using. The packer, by default, just stacks widgets one on top of the other. (We can change that behaviour to stack the widgets left and right instead but thats still pretty limited.) That is very rarely what we want for normal widgets, but if we build our applications from Frames then stacking Frames on top of (or beside) each other is quite a reasonable approach. We can then put our other widgets into the Frames using either the packer or another Layout Manager within each Frame as appropriate. (Each Frame can have its own Layout Manager, but you cannot mix managers within a single frame.) You can see an example of this in action in the Case Study topic.
Even the simple packer provides a multitude of options, however. For example we can arrange our widgets vertically or horizontally and we can adjust the sizes and separation of the widgets as well as what side of the frame they align to. Here's a simple example of horizontal packing in action:
lHello = tk.Label(F, text="Hello") lHello.pack(side="left") bQuit = tk.Button(F, text="Quit", command=F.quit) bQuit.pack(side="left")
That will force the widgets to go to the left thus the first widget (the label) will appear at the extreme left hand side, followed by the next widget (the Button). If you modify the lines in the example above it will look like this:
If you change the "left" to "right" then the Label appears on the extreme right and the Button to the left of it, like so:
One thing you notice is that it doesn't look very nice because the widgets are squashed together. The packer also provides us with some parameters to deal with that. The easiest to use is Padding and is specified in terms of horizontal padding (padx), and vertical padding(pady). These values are specified in pixels. Let's try adding some horizontal padding to our example:
lHello.pack(side="left", padx=10) bQuit.pack(side='left', padx=10)
It should look like this:
If you try resizing the window width you'll see that the widgets retain their positions relative to one another but stay centered in the window. Why is that, if we packed them to the left? The answer is that we packed them into a Frame but the Frame was packed without a side, so it is positioned top, center - the packers default. If you want the widgets to stay at the correct side of the window you will need to pack the Frame to the appropriate side too:
F.pack(side='left')
Also note that the widgets stay centered if you resize the window vertically - again that's the packers default behavior.
I'll leave you to play with padx and pady for yourself to see the effect of different values and combinations etc. Between them, side and padx/pady allow quite a lot of flexibility in the positioning of widgets using the packer. There are several other options, each adding another subtle form of control, please check the Tkinter documentation for details.
There are a few other layout managers in Tkinter, known as the grid, and the placer. (In addition the Tix module, which augments Tkinter, provides a Form layout manager. We do not cover Tix here, and it officially has a deprecated status in the standard library.) To use the grid manager you use grid() instead of pack() and for the placer you call place() instead of pack(). Each has its own set of options and since I'll only cover the packer in this intro you'll need to look up the Tkinter tutorial and reference for the details. The main points to note are that
The Frame widget actually has a few useful properties that we can use. After all, it's very well having a logical frame around components but sometimes we want something we can see too. This is especially useful for grouped controls like radio buttons or check boxes. The Frame solves this problem by providing, in common with many other Tk widgets, a border known as the relief property. Relief can have any one of several values: sunken, raised, groove, ridge or flat. Let's use the sunken value on our simple dialog box. Simply change the Frame creation line to:
F = Frame(top, relief="sunken", border=1)
Note 1:You need to provide a border too. If you don't the Frame will be sunken but with an invisible border - you don't see any difference!
Note 2: that you don't put the border size in quotes. This is one of the confusing aspects of Tk programming is knowing when to use quotes around an option and when to leave them out. In general if it's a numeric value you can safely leave the quotes off. If it's a mixture of digits and letters or a string then you need the quotes. Likewise with which letter case to use. Unfortunately there is no easy solution, you just learn from experience - Python often gives a list of the valid options in it's error messages!
One other thing to notice is that the Frame doesn't fill the window. We can fix that with another packer option called, unsurprisingly, fill. When you pack the frame do it thusly:
F.pack(fill="x")
This fills horizontally, if you want the frame to fill the entire window just use fill='y' too. Because this is quite a common requirement there is a special fill option called both so you could type:
F.pack(fill="both")
The end result of running the script now looks like:
Let's now look at a text Entry widget. This is the familiar single line of text input box. It shares a lot of the methods of the more sophisticated, multi-line Text widget which we used in the event handling topic and will also use in the case study topic. Essentially we will simply use an Entry to capture what the user types and to clear that text on demand.
Going back to our "Hello World" program we'll add a text Entry widget inside a Frame of its own and then, in a second Frame, put a button that can clear the text that we type into the Entry. We will also add a second button to quit the application. This will demonstrate not only how to create and use the Entry widget but also how to define our own event handling functions and connect them to widgets.
import tkinter as tk # create the event handler to clear the text def evClear(): eHello.delete(0,tk.END) # create the top level window/frame top = tk.Tk() F = tk.Frame(top) F.pack(fill="both") # Now the frame with text entry fEntry = tk.Frame(F, border=1) eHello = tk.Entry(fEntry) fEntry.pack(side="top") eHello.pack(side="left") # Finally the frame with the buttons. # We'll sink this one for emphasis fButtons = tk.Frame(F, relief="sunken", border=1) bClear = tk.Button(fButtons, text="Clear Text", command=evClear) bClear.pack(side="left", padx=5, pady=2) bQuit = tk.Button(fButtons, text="Quit", command=F.quit) bQuit.pack(side="left", padx=5, pady=2) fButtons.pack(side="top", fill="x") # Now run the eventloop F.mainloop()
Note 1: We define an event handler just like any other function. Since we intend to assign it to the command event of a button we know that it must have no parameters. (Some event handlers, such as mouse events, will take parameters, you need to check the documentation to find out what is required for any given event.)
Note 2: We pass the name of the event handlers (evClear and F.quit), without parentheses, as the command argument to the buttons. Note also the use of a naming convention: evXXX to link the event handler with the corresponding XXX widget. So evClear is the event handler for the bClear widget.
Note 3: The event handler calls the delete method of the Entry widget. The indexing system used for the arguments is a tad complicated but, at this level, we can simply say it clears is a tad complicated but, at this level, we can simply say it clears the text from position 0 (the start) to tk.END (the last position). Notice that tk.END is a constant defined in tkinter. There are actually a whole bunch of these that you can use instead of the option strings "right", "left", "top" etc. if you prefer.
Running the program yields this:
And if you type something in the text entry box then hit the "Clear Text" button it removes it again.
Please note that we have now built the GUI that our containment diagram at the start of the topic depicted. The top widget has a Frame under it. That Frame, in turn, has 2 Frames under it, one of which has an Entry widget and the other two Buttons. That's exactly what the diagram shows.
Of course there is not much point in having an Entry widget unless we can get access to the text contained within it. We do this using the get method of the widget. I'll illustrate that by copying the text from the widget to a label just before clearing it so that we can always see the last text that the widget held. To do that we need to add a Label widget just under the Entry widget and extend the evClear event handler to copy the text. And just for fun we will colour the label text a light blue. The modified program is shown below with the modifications in bold:
import tkinter as tk # create the event handler to clear the text def evClear(): lHistory['text'] = eHello.get() eHello.delete(0,tk.END) # create the top level window/frame top = tk.Tk() F = tk.Frame(top) F.pack(fill="both") # Now the frame with text entry fEntry = tk.Frame(F, border=1) eHello = tk.Entry(fEntry) eHello.pack(side="left") lHistory = tk.Label(fEntry, foreground="steelblue") lHistory.pack(side="bottom", fill="x") fEntry.pack(side="top") # Finally the frame with the buttons. # We'll sink this one for emphasis fButtons = tk.Frame(F, relief="sunken", border=1) bClear = tk.Button(fButtons, text="Clear Text", command=evClear) bClear.pack(side="left", padx=5, pady=2) bQuit = tk.Button(fButtons, text="Quit", command=F.quit) bQuit.pack(side="left", padx=5, pady=2) fButtons.pack(side="top", fill="x") # Now run the eventloop F.mainloop()
Notice that while we have here assigned the text directly to the Label property but we could equally well have assigned it to a normal Python variable for use later in our program.
Up till now we have used the command property of buttons to associate Python functions with GUI events. Sometimes we want more explicit control, for example to catch a particular key combination. The way to do that is use the bind function to explicitly tie together (or bind) an event and a Python function.
We'll now define a hot key - let's say CTRL-c - to delete the text in the above example. To do that we need to bind the CTRL-c key combination to the same event handler as the Clear button. Unfortunately there's an unexpected snag. When we use the command option the function specified must take no arguments. When we use the bind function to do the same job the bound function must take one argument. Thus we need to create a new function with a single parameter which calls evClear. Add the following after the evClear definition:
def evHotKey(event): evClear()
And add the following line immediately after the definition of the eHello Entry widget:
eHello.bind("<Control-c>",evHotKey) # the key definition is case sensitive
Run the program again and you can now clear the text by either hitting the button or typing Ctrl-c. We could also use bind to capture things like mouse clicks or capturing or losing focus (that is, making the window active or inactive) or even the window becoming visible (or hidden). See the Tkinter documentation for more information on this. The hardest part is usually figuring out the format of the event description!
You can report short messages to your users using a MessageBox. This is very easy in Tk and is accomplished using the Tkinter messagebox module functions as shown:
from tkinter import messagebox messagebox.showinfo("Window Text", "A short message")There are also error, warning, Yes/No and OK/Cancel boxes available via different showXXX functions. They are distinguished by different icons and buttons. The latter two use askXXX instead of showXXX and return a value to indicate which button the user pressed, like so:
res = messagebox.askokcancel("Which?", "Ready to stop?") print res
Here are some of the Tkinter message boxes:
This is very like that alert or MsgBox dialogs we used in our JavaScript and VBSCript web programs earlier in the tutorial.
There are also standard dialog boxes that you can use to get filenames or directory names from the user that look just like the normal GUI "Open File" or "Save File" dialogs. I won't describe them here but you will find examples in the Tkinter reference pages under Standard Dialogs.
It's common when programming GUI's to wrap the entire application as a class. This begs the question, how do we fit the widgets of a Tkinter application into this class structure? There are two choices, we either decide to make the application itself as a subclass of a Tkinter Frame or have a member field store a reference to the top level window. The latter approach is the one most commonly used in other toolkits so that's the approach we'll use here. If you want to see the first approach in action go back and look at the example in the Event Driven Programming topic. (That example also illustrates the basic use of the incredibly versatile Tkinter Text widget as well as another example of using bind)
I will convert the example above, using an Entry field, a Clear button and a Quit button, to an OO structure. First we create an Application class and within the constructor assemble the visual parts of the GUI.
We assign the resultant Frame to self.mainWindow, thus allowing other methods of the class access to the top level Frame. Other widgets that we may need to access (such as the Entry field) are likewise assigned to member variables of the application. Using this technique the event handlers become methods of the application class and all have access to any other data members of the application (although in this case there are none) through the self reference. This provides seamless integration of the GUI with the underlying application objects:
import tkinter as tk # create the event handler to clear the text class ClearApp: def __init__(self, parent): # create the top level window/frame self.mainWindow = tk.Frame(parent) self.eHello = tk.Entry(self.mainWindow) self.eHello.insert(0,"Hello world") self.eHello.pack(fill="x", padx=5, pady=5) self.eHello.bind("<Control-c>", self.evHotKey) # Now the frame with the buttons. fButtons = tk.Frame(self.mainWindow, height=2) self.bClear = tk.Button(fButtons, text="Clear", width=10, height=1,command=self.evClear) self.bQuit = tk.Button(fButtons, text="Quit", width=10, height=1, command=self.mainWindow.quit) self.bClear.pack(side="left", padx=15, pady=1) self.bQuit.pack(side="right", padx=15, pady=1) fButtons.pack(side="top", pady=2, fill="x") self.mainWindow.pack() self.mainWindow.master.title("Clear") def evClear(self): self.eHello.delete(0,tk.END) def evHotKey(self, event): self.evClear() # Now create the app and run the eventloop top = tk.k() app = ClearApp(top) top.mainloop()
Here's the result:
The result looks remarkably like the previous incarnation although I have tweaked some of the configuration and pack options to look more similar to the wxPython example shown below.
Of course it's not just the main application that we can wrap up as an object. We could create a class based around a Frame containing a standard set of buttons and reuse that class in building dialog windows say. We could even create whole dialogs and use them across several projects. Or we can extend the capabilities of the standard widgets by subclassing them - maybe to create a button that changes colour depending on its state. (This is what has been done with the Tix module which is an extension to Tkinter which we mentioned earlier.)
Since version 3.1 Tkinter also includes some new features, known as themed widgets, and found in the tkinter.ttk module, which greatly improve the look of Tkinter so that it is virtually indistinguishable from the native OS widgets. I won't cover these here but you can read about them on the Tcl/Tk web site.
There are many other GUI toolkits available but one of the most popular is the WxPython toolkit which is, in turn, a wrapper for the C++ toolkit wxWidgets. WxPython is much more typical than Tkinter of GUI toolkits in general. It also provides more standard functionality than Tk "out of the box" - things like tooltips, status bars etc which have to be hand crafted in Tkinter. We'll use wxPython to recreate the simple "Hello World" Label and Button example above.
One major snag is that wxPython is not yet available for Python v3! It is intended to port it but the maintainer has not, at the time of writing, done so. That means you will need to treat the v2 code below as a reading exercise only (or install a copy of Python 2.7!)
I won't go through this in detail, if you do want to know more about how wxPython works you will need to download the package from the wxPython website.
In general terms the toolkit defines a framework which allows us to create windows and populate them with controls and to bind methods to those controls. It is fully object oriented so you should use methods rather than functions. The example looks like this:
import wx # --- Define a custom Frame, this will become the main window --- class HelloFrame(wx.Frame): def __init__(self, parent, id, title, pos, size): super().__init__(parent, id, title, pos, size) # we need a panel to get the right background panel = wx.Panel(self) # Now create the text and button widgets self.tHello = wx.TextCtrl(panel, -1, "Hello world", pos=(3,3), size=(185,22)) bClear = wx.Button(panel, -1, "Clear", pos=(15, 32)) self.Bind(wx.EVT_BUTTON, self.OnClear, bClear) bQuit = wx.Button(panel, -1, "Quit", pos=(100, 32)) self.Bind(wx.EVT_BUTTON, self.OnQuit, bQuit) # these are our event handlers def OnClear(self, event): self.tHello.Clear() def OnQuit(self, event): self.Destroy() # --- Define the Application Object --- # Note that all wxPython programs MUST define an # application class derived from wx.App class HelloApp(wx.App): def OnInit(self): frame = HelloFrame(None, -1, "Hello", (200,50), (200,90) ) frame.Show(True) self.SetTopWindow(frame) return True # create instance and start the event loop HelloApp().MainLoop()
And it looks like this:
Points to note are the use of a naming convention for the methods that get called by the framework - OnXXXX. Also note the EVT_XXX constants used to bind events to widgets - there is a whole family of these. wxPython has a vast array of widgets, far more than Tkinter, and with them you can build quite sophisticated GUIs. Unfortunately they tend to use a coordinate based placement scheme which becomes very tedious after a while. It is possible to use a scheme very similar to the Tkinter packer but its not so well documented.
Incidentally it might be of interest to note that this and the very similar Tkinter example above have both got about the same number of lines of executable code - Tkinter: 23, wxPython: 21.
In conclusion, if you just want a quick GUI front end to a text based tool then Tkinter should meet your needs with minimal effort. If you want to build full featured cross platform GUI applications look more closely at wxPython.
Other toolkits include MFC and .NET and of course there is the venerable curses which is a kind of text based GUI! Many of the lessons we've learned with Tkinter apply to all of these toolkits but each has its own characteristics and foibles. Pick one, get to know it and enjoy the wacky world of GUI design. Finally I should mention that many of the toolkits do have graphical GUI builder tools, for example Qt has Blackadder and GTK has Glade. wxPython has a free GUI builder, Boa Constructor, available although still only in Alpha release state. There is even a GUI builder for Tkinter called GUI Builder originally for building Tcl/Tk interfaces, but capable of generating code in multiple languages including Python. The caveat is that I've never really used any of these GUI builders in anger so I cannot recommend them, just indicate that they exist.
That's enough for now. This wasn't meant to be a Tkinter reference page, just enough to get you started. See the Tkinter section of the Python web pages for links to other Tkinter resources.
There are also several books on using Tcl/Tk and several Python books have chapters on Tkinter. I will however come back to Tkinter in the case study, where I illustrate one way of encapsulating a batch mode program in a GUI for improved usability.
Things to remember