HowTo: Work with Python and Tkinter to Generate Graphical User Interfaces

Version: 16.2

Table of Contents

Preface

Graphical User Interface (GUI) composition can be done via the Tkinter library that is installed with Python 2.2. Other methods are available, such as wxWidgets, which is used by the Grail.mwx module. This document briefly discusses GUI composition using Tkinter.

Assumptions

It is assumed that you are familiar with python and have a very basic understanding of dialog programming. You should have a very light understanding of concepts such as event handling and windows message loops. If you do not see Grayson's book, Python and Tkinter Programming [1].

You should have a working knowledge of Python. See the Python Tutorial [2] for a good introduction to programming with python.

Getting Started

Lets start with generating a simple dialog that displays hello world to the user.

The Tkinter GUI library comes bundled with the Python distributions, and as such does not require any further downloads.

The process of building a simple dialog that displays "Hello World" involves four basic steps,

  1. Initialize the Tkinter library
  2. Create our label
  3. Position our label
  4. Start the windows manager.

An example of some Python code that accomplishes the above steps, is as follows,

# helloworld.py
# 1
from Tkinter import *
root = Tk()
# 2
label = Label(root, text="Hello World!")
# 3
label.pack()
# 4
root.mainloop()

In step (1) we import the Tkinter library into our script, and call Tk() to initialize the Tkinter library. The root is our main application window, and serves as the parent to all our other widgets.

In step (2) we create a Label widget. The label widget receives two parameters. First it needs to know who its parent is--in this case the root--and second, it needs to know what text to display.

All widgets have a list of configuration keywords that they can take. For example, if we decided to also use the 'background' keyword, then we could have changed the background color for the label, like this,

label = Label(root, text="Hello World!", background="white")

In step (3) we perform the widget layout. In this case we have one simple widget that we request pack itself into the parent.

In addition to the pack option you can also grid and place widgets. Using the grid to layout widgets involves laying widgets out in a row by column system. Placing widgets involves laying out widgets with exact x and y coordinates. In practice you will often use either pack or grid.

Finally, in step (4), we tell the parent window--recall this is our main application window--that it should start receiving events from the user via the main message loop. This step displays the window to the user.

A Simple Window Framework

A simple window can be composed with a little Graphical User Interface background knowledge and a basic framework for simple windows.

Background

Consider composing a framework for writing Graphical User Interfaces (GUI). Any framework that uses a GUI can be divided into at least two distinct layers,

  1. Presentation Layer.

    This is the dialog proper, and has to do with presenting, acquiring and validating data from the user. In addition, it is responsible for cajoling the data into a format that the underlying Business Layer can use, and representing the results of the underlying layer's logic.

  2. Business Layer.

    This receives information (input) from the presentation layer processes, and returns information (output). The Business Layer is frequently, in our problem domain, the part that accesses data files or runs calculations based on input.

Given this two layer description, the basic sequence of events for our simple framework will be,

1.0 Create our Window

2.0 Post the Window to the user

3.0 Respond to APPLY Commands

3.1 Validate window options

3.2 Freeze window inputs

3.3 Cajole data into Business Layer format

3.4 Execute the Business layer

3.5 Display the results

3.6 Unfreeze window inputs and wait.

4.0 Responds to OK Commands

4.1 Same as APPLY except exit when return from Business Layer.

5.0 Respond to CANCEL commands

5.1 Exit

The Framework

The basic framework is shown in gui-boilerplate.py. To enhance the framework copy the file to your own script filename, and go through all the TODO sections.

Keep in mind that you do not need to store all the logic within one script. In fact it might be clearer to have the presentation stored within one script, and the processing (business) stored within another.

For example, if your window was in a script called mywindow.py and the name of the window class was MyWindow, then you could access it from myprocess.py via,

import mywindow

def process(options):
   
print "processing..."

if __name__=="__main__":
   
window = mywindow.MyWindow(process)
   
window.start()

This would initialize the MyWindow class in the mywindow.py script and start the dialog. When the user clicks OK or APPLY, the process function will be called from options gathered from the user.

An Example of the Framework

As an example, let us ask the user for a MineSight® Resource (MSR) filename. The path to the MSR will be used to generate a report on the MSR file.

This example is illustrated with msr_report.py script.

The first thing we will do is insert our widgets. We will need a label to display the path, and a BROWSE... button to allow the user to browse for a MSR file. To do this we go to the # TODO: insert your widgets and start coding.

self.browse_frame = Frame(self.root) # above button frame
self.browse_frame.pack(anchor='nw', side='top')
self.label_path = Label(self.browse_frame, width=30)
self.btn_browse = Button(self.browse_frame, text="BROWSE...",
        
command=self.on_browse) # we need to define command!
self.label_path.grid(row=0, column=0)
self.btn_browse.grid(row=0, column=1)

We create another frame, that we anchor in the top north-west (nw) corner of the dialog, because the OK, CANCEL and APPLY are in the bottom south-east (se) corner. Then we grid our widgets using a table coordinates onto our browse frame.

At this point, in terms of parent and child widgets and frames we have the following configuration,

+ self.root (pack)
  +- self.browse_frame (pack)
     - self.label_path (grid)
     - self.btn_browse (grid)
  + self.buttonframe (pack)
     - self.btn_ok (pack)
     - self.btn_apply (pack)
     - self.btn_cancel (pack)

In the above snippet of code we use the self.on_browse function, we now need to define what the on_browse function will do.

def on_browse(self, event=None):
   
types = [("Geometry MSR", "*.msr"), ("All Files", "*.*")]
   
path = tkFileDialog.askopenfilename(parent=self.root,
           
defaultextension=".msr", filetypes=types,
           
title="Open MSR File")
   
self.label_path.configure(text=path)

This function will be executed each time the user clicks the BROWSE... button, and each time we will update the label with the path that the user selected.

The next step is to specify the freeze and unfreeze of widgets during processing. We will add the following lines to disable and enable the btn_browse widget,

self.btn_browse.configure(state=DISABLED)

and,

self.btn_browse.configure(state=ACTIVE)

Next, we validate the data in the validate_input otherwise it should display an error.

def validate_input(self):
    
# TODO: validate input for your widgets. Return 1 if everything
    
# is valid; return 0 otherwise. Also post error messages from
    
# here, like this,
    
#
    
#  tkMessageBox.showerror("your path is invalid!")
    
#
    
isvalid = 0
    
path = self.label_path.cget("text")
    
if not os.path.isfile(path):
        
tkMessageBox.showerror(self.root,
                
"Invalid path: %s" % (path))
        
isvalid = 0
    
elif not self._is_geometry_msr(path):
        
tkMessageBox.showerror(self.root,
                
"Path not pointing to Geometry MSR: %s" % (path))
        
isvalid = 0
    
else:
        
isvalid = 1 # it must be valid by now.
    
return isvalid

def _is_geometry_msr(self, path):
    
try:
        
msr = geometry.Geometry(path)
    
except geometry.GeometryError, e:
        
return 0 # invalid
    
return 1 # valid

The validate_input returns 1 if everything is valid, otherwise it displays an error dialog using the tkMessageDialog module's showerror function. Notice that we defined an _is_geometry_msr test function that verifies that the path is pointing to a valid MSR.

Afterward we define what happens when we call the window's command function,

def call_business_layer(self):
   
# TODO: Compose your options and call the call back command if
   
# there is one.
   
options = {"path":self.label_path.cget("text")} # valid path
   
if self.command:
       
output = self.command(self, options)
       
tkMessage.showinfo(self.root, output)

Here we store the path into a dictionary with "path" as a key. The command (in our case a function called report), has an agreement with the window to receive a dictionary with a "path" key that points to a valid MSR file. The callback then generates the report based on the input arguments and returns the output back to the presentation layer. The presentation layer then displays the output via tkMessageBox's showinfo function.

All that is left to do now is define our command function and start the window. We will define the command function as follows,

def report_command(window, options):
   
path = options["path"]
   
msr = geometry.Geometry(path)
   
output = "generate report!"
   
return report

To start the window we pass the the command function to the Window object and call its start function,

if __name__=="__main__":
   
win = MsrReportWindow(report_command)
   
win.start()

And we have completed our very simple example.

References

[1](1, 2) Grayson, John E. Python and Tkinter Programming. Greenwich: Manning Publications, 2000.
[2]Python Tutorial. 30 May 2003. 30 March 2005. <http://www.python.org/doc/2.2.3/tut/tut.html>
[3]Tkinter Reference: a GUI for Python. 28 December 2004. 25 February 2005. <http://infohost.nmt.edu/tcc/help/pubs/tkinter/index.html>