Web Development

A Quickstart to building GUI based applications in Python

Nick Gibson will show you how Python's emphasis on simplicity can get you up and running on your graphical application in no time.

When you're learning a new language, particularly a scripting language such as Python, you might be forced to stick to console based programs for some time before you've picked up enough to start writing graphical based programs. It's now been more than 25 years since the first commercial graphical user interface was released (for the curious, the Xerox STAR) and it seems a little archaic to still be using the console for applications.

Thankfully Python's emphasis on simplicity means that you can include a graphical user interface in your programs without needing to be a Python guru. To prove this, I'll run through the creation of a simple note taking program, using the standard GUI toolkit for Python: Tk. I'll be assuming some familiarity with Python though, so if you're a bit lacking you might want to read my previous articles on the subject (here and here).

Let's start with the basics, first you need to import the Tk interface into your programs namespace. Since we'll be referring to Tk widgets pretty consistently, we don't want to have to qualify them with a package all the time, so the best way to do this is:

from Tkinter import *

This differs from the traditional import statement in that it puts everything in the module into the default namespace of your program, so now rather than needing to refer to a textbox as Tkinter.Textbox you can just write Textbox.

Now we create the root window and set its title to something a bit more explanatory:

root = Tk()
root.title("Note Taker")

Creating the root window is as simple as creating an instance of the Tk class, which loads the graphical toolkit and gives us a blank window for which to put our widgets on. This is the first half of the basic procedure for starting a Tk program.:

root.mainloop()

The second (shown above) is to call the Tk mainloop, which processes events, such as keyboard or mouse input, allowing the user to interact with the window. In fact, that's all you really need for a GUI program. Run a python script with just those four lines and a window will pop up and just sit there, not really doing anything.

Placing Widgets in your Window


That's pretty boring though, what really makes an interface is its widgets: the buttons, forms and so on that the user can do things to. Let's create a few buttons, a text box and a list box:

button1 = Button(root, text="button1")
button2 = Button(root, text="button2")
button3 = Button(root, text="button3")

text = Entry(root)
listbox = Listbox(root)

It's that simple to create basic objects. The first parameter of each class initialisation is the surface the widget belongs to, in this case we've only got the root window available. The text attribute of a Button sets the label of a button. There are more types of widgets, and many more ways to customise them, but this is pretty much all we'll need for this program -- for a complete list, take a look at the official Tkinter documentation here. Now all we need to do is drop these widgets on our window using the pack method like so:

text.pack()
button1.pack()
button2.pack()
button3.pack()
listbox.pack()

The order in which you pack the widgets onto the window is the order in which they appear on the screen. Running this program gives you a window that will look something like this, depending upon the natural look of your operating system or window manager:




Interactions Between Widgets


Now this looks more interesting, but it still doesn't really do anything, you can click on the buttons or type into the text box, but there's not a lot of point. Let's change that. The way this works in Tk, and indeed in most GUI toolkits, is through callbacks -- functions that are called when a certain event occurs. Let's write a simple callback to provide feedback when button1 is pressed:

def Button1():
	listbox.insert(END, "button1 pressed")

All this function does is add the string "button1 pressed" to the end of the items in the list box. All we need then to make this function called when the button is pressed is to change the creation of button1 as follows:

button1 = Button(root, text="button1", command = Button1)

Do the same thing for button2 by creating a new callback function which inserts a different string into the listbox when the second button is pressed and then try out the program. Sure enough, clicking the buttons changes the contents of the list box. What if we were getting bored of those strings and wanted to add new things to the box? Hang on, don't we have a textbox lying around somewhere, can't we just type in the string we want each time and add that to the box? Well of course we can, that's the point of the program, write a callback for the third button like so:

def Button3():
	text_contents = text.get()
	listbox.insert(END, text_contents)
	text.delete(0,END)

Then set the command of button3 to be this function. When the button is clicked, the callback Button3 first grabs the contents of the textbox and inserts it into the bottom of the listbox, then clears the textbox. Load up the program and try this out.

If you play with this it may seem that after awhile the buttons stop working, that the listbox is full. In fact it's working fine, but we can only see the top group of items in the list, and you can use the in built support for mouse wheel scrolling to scroll down to the rest of the list. This is a little unwieldy, what we need here is a scrollbar, so we can choose where in the list we want to look. Adding a scrollbar is only a little more complicated than one of the other widgets, because we must hook it up to the list box, if we replace the creation of the listbox with the following:

scrollbar = Scrollbar(root, orient=VERTICAL)
listbox = Listbox(root, yscrollcommand=scrollbar.set)
scrollbar.configure(command=listbox.yview)

Then when we move the scrollbar around it will change the y-position (vertical position) of the listbox. Plus, now if we use the mouse wheel to scroll the listbox, then the position of the scrollbar will update.

If you've been following along, the code should look something like this:

#!/usr/bin/python

from Tkinter import *

root = Tk()
root.title("Note Taker")

def Button1():
	listbox.insert(END, "button1 pressed")

def Button2():
	listbox.insert(END, "button2 pressed")

def Button3():
	text_contents = text.get()
	listbox.insert(END, text_contents)
	text.delete(0,END)

button1 = Button(root, text="button1", command = Button1)
button2 = Button(root, text="button2", command = Button2)
button3 = Button(root, text="button3", command = Button3)

text = Entry(root)

scrollbar = Scrollbar(root, orient=VERTICAL)
listbox = Listbox(root, yscrollcommand=scrollbar.set)
scrollbar.configure(command=listbox.yview)

text.pack()
button1.pack()
button2.pack()
button3.pack()
listbox.pack()
scrollbar.pack()

root.mainloop()

Positioning widgets


We're starting to get somewhere; we can type our notes into the text box and then add them to a list. The program is a bit ugly though, everything's lined up vertically, and the textbox and listbox are too small to really use that well. We need a bit more control over where widgets are placed. There are a few ways to do this, but all we're really after is to divide the window into two sections, one for the text box and the buttons, and the other holding the listbox. For that we're going to use the Frame widget, which is like a container in which you can put other widgets -- just like the root window. So let's create a couple of frames:

textframe = Frame(root)
listframe = Frame(root)

Now so far we've been setting the owner of each widget to be the root window, let's change that to reflect our new design:

button1 = Button(textframe, text="button1", command = Button1)
button2 = Button(textframe, text="button2", command = Button2)
button3 = Button(textframe, text="button3", command = Button3)

text = Entry(textframe)

scrollbar = Scrollbar(listframe, orient=VERTICAL)
listbox = Listbox(listframe, yscrollcommand=scrollbar.set)
scrollbar.configure(command=listbox.yview)

That's not going to do a lot unless we tell Tk where in the frame to put the widgets, for this we're going to use the pack method and three keywords: side, fill and expand. You can think of side as being similar to alignment in a word processor, it tells the widgets which side of the frame they should generally try to be on, the TOP, BOTTOM, LEFT or RIGHT -- the default being to cluster around the centre. In this case, we'll want everything except the scrollbar to be LEFT aligned, with the scrollbar hugging the right side of the listbox.

The second option, fill, tells Tk which directions to fill out the widget if it grows, in the X direction (horizontal), the Y direction (vertical) or BOTH. Generally speaking, we're alright with buttons as they are, but it makes sense for the text and list boxes to use as much space as is available -- so set the textbox fill to X and the listbox to BOTH -- the scrollbar should resize with the listbox, so set its fill to Y. Finally the expand option tells the widget whether or not to expand into free space if it is possible -- add expand=1 to the textbox and listbox pack options. You should now have code looking like this:

text.pack(side=LEFT, fill=X, expand=1)
button1.pack(side=LEFT)
button2.pack(side=LEFT)
button3.pack(side=LEFT)
listbox.pack(side=LEFT,fill=BOTH, expand=1)
scrollbar.pack(side=RIGHT, fill=Y)

Now all that's left is to pack the two frames into the root window, which is just the same as packing any other widget. Remember which ways the frames should expand:

textframe.pack(fill=X)
listframe.pack(fill=BOTH, expand=1)

Lastly, we should set the default size of the window to give a little more room for our widgets:

root.geometry("600x400")

And that's it, your window should now look like:



Keyboard and Mouse Events


We've got something that works, more or less -- but its interface is still a bit unwieldy. It would be nice if we could use the mouse and keyboard to make things a bit more intuitive. When you're running the Tk main loop, hitting a key on the keyboard or moving the mouse around generates events, which you can bind to callback functions in a similar way to when a button is pressed. Let's take a look at how we could make the text box respond to pressing Return rather than clicking the button. Now we've already got a function to handle that, the Button3 callback -- unfortunately we can't just use that since callbacks from events are different from buttons. We'll have to wrap it in another function:

def ReturnInsert(event):
	Button3()

Then we register this callback with the event we're looking for using the bind function:

text.bind("<Return>", ReturnInsert)

It's important here that we use the <Return> event code rather than the <Enter> event code, since the second triggers when the mouse enters the listbox. Now say we want to let the user use right click to remove items from the listbox, it's pretty much the same process. First we write a callback that accepts the event as input:

def DeleteCurrent(event):
	listbox.delete(ANCHOR)	

Then we bind the event to this callback:

listbox.bind("<Double-Button-3>", DeleteCurrent)

The right mouse button is called Button-3 in Tk (not to be confused with the name of our third form button callback), since the second mouse button refers to middle click. Finally, let's allow the user to copy a note back to the text box, in case they want to modify it. We don't have a function that allows this yet, so we'll have to write some new code in the callback:

def CopyToText(event):
	text.delete(0,END)
	current_note = listbox.get(ANCHOR)
	text.insert(0, current_note)

Then bind the event to the callback like before:

listbox.bind("<Double-Button-1>", CopyToText)

That's all there is to it. You're not just limited to these events though, for a guide to which events you can bind, check out the Tkinter library introduction.

Now seems like a good time to clean up our program. Button1 is a poor name for a function -- we've already been tripped up by similar names once, let's not do it again. We'll also take this opportunity to change some of the button effects, as it makes more sense for the Enter button to be closest to the text box for example, and Button1 isn't particularly useful, so let's get rid of it. No real changes to functionality, but the program now looks like this:

#!/usr/bin/python

from Tkinter import *

root = Tk()
root.geometry("600x400")
root.title("Note Taker")

def Enter():
	text_contents = text.get()
	listbox.insert(END, text_contents)
	text.delete(0,END)

def Remove():
	listbox.delete(ANCHOR)	

def Save():
	pass

def ReturnInsert(event):
	Enter()

def DeleteCurrent(event):
	Remove()

def CopyToText(event):
	text.delete(0, END)
	current_note = listbox.get(ANCHOR)
	text.insert(0, current_note)

textframe = Frame(root)
listframe = Frame(root)

enter_button = Button(textframe, text="Enter", command = Enter)
remove_button = Button(textframe, text="Remove", command = Remove)
save_button = Button(textframe, text="Save", command = Save)

text = Entry(textframe)

scrollbar = Scrollbar(listframe, orient=VERTICAL)
listbox = Listbox(listframe, yscrollcommand=scrollbar.set, selectmode=EXTENDED)
scrollbar.configure(command=listbox.yview)

text.bind("<Return>", ReturnInsert)
listbox.bind("<Double-Button-3>", DeleteCurrent)
listbox.bind("<Double-Button-1>", CopyToText)

text.pack(side=LEFT, fill=X, expand=1)
enter_button.pack(side=LEFT)
remove_button.pack(side=LEFT)
save_button.pack(side=LEFT)
listbox.pack(side=LEFT,fill=BOTH, expand=1)
scrollbar.pack(side=RIGHT, fill=Y)

textframe.pack(fill=X)
listframe.pack(fill=BOTH, expand=1)

root.mainloop()

Keeping data between sessions


Our program is usable as it is, but when we close the program, all our notes disappear -- we can fix this by saving the contents of the listbox to a file and loading it when the program is run. To do this we'll use the pickle module -- Python's version of serialisation or marshalling data types to files. First we need to import the module:

import pickle

Then say we've got a variable, notes, which contains the list of notes. To save it to a file we just need to open the file to write to, and then use the dump function. You might have noticed that the callback for the third button is empty, let's change that to save the current list of notes to a file:

def Save():
	f = file("notes.db", "wb")
	notes = listbox.get(0, END)
	pickle.dump(notes, f)

Now when the "Save" button is clicked, we've got a copy of our notes on disk. This wont help us much unless we've got some way of loading these notes, so let's fill up the listbox right before starting the main loop:

try:
	f = file("notes.db", "rb")
	notes = pickle.load(f)
	for item in notes:
		listbox.insert(END,item)
	f.close()
except:
	pass

We need to wrap the whole thing in a try clause in case the file open throws an exception, say, if the file "notes.db" doesn't yet exist. If an exception is thrown however, we don't really care, we just wont load any notes into the list.

That's it, you've now got a working note-taking program -- complete with keyboard and mouse input, and loading and saving of data, written in Python and Tk, in less than 70 lines of code. With that, you can see how simple it can be to knock up quick GUI based applications. This example is a little simple to be particularly useful as an application, but with a few improvements, such as a multi-line text field, or copying a note to the clipboard, automatically saving notes, etc, it could be a pretty handy way of keeping track of your to do list. I'll leave those as an exercise though. If you want to find out more about what you can do in Tk with Python, the best place to look is the library documentation here. If you've been following along, it should look something like this:



Here's the code for this example:

#!/usr/bin/python

from Tkinter import *
import pickle

root = Tk()
root.geometry("600x400")
root.title("Note Taker")

def Enter():
	text_contents = text.get()
	listbox.insert(END, text_contents)
	text.delete(0,END)

def Remove():
	listbox.delete(ANCHOR)	

def Save():
	f = file("notes.db", "wb")	
	notes = listbox.get(0, END)
	pickle.dump(notes, f)

def ReturnInsert(event):
	Enter()

def DeleteCurrent(event):
	Remove()

def CopyToText(event):
	text.delete(0, END)
	current_note = listbox.get(ANCHOR)
	text.insert(0, current_note)

textframe = Frame(root)
listframe = Frame(root)

enter_button = Button(textframe, text="Enter", command = Enter)
remove_button = Button(textframe, text="Remove", command = Remove)
save_button = Button(textframe, text="Save", command = Save)

text = Entry(textframe)

scrollbar = Scrollbar(listframe, orient=VERTICAL)
listbox = Listbox(listframe, yscrollcommand=scrollbar.set, selectmode=EXTENDED)
scrollbar.configure(command=listbox.yview)

text.bind("<Return>", ReturnInsert)
listbox.bind("<Double-Button-3>", DeleteCurrent)
listbox.bind("<Double-Button-1>", CopyToText)

text.pack(side=LEFT, fill=X, expand=1)
enter_button.pack(side=LEFT)
remove_button.pack(side=LEFT)
save_button.pack(side=LEFT)
listbox.pack(side=LEFT,fill=BOTH, expand=1)
scrollbar.pack(side=RIGHT, fill=Y)

textframe.pack(fill=X)
listframe.pack(fill=BOTH, expand=1)

try:
	f = file("notes.db", "rb")
	notes = pickle.load(f)
	for item in notes:
		listbox.insert(END,item)
	f.close()
except:
	pass

root.mainloop()
0 comments