Thursday, April 28, 2016

Make Your Own GUI with Python

Suppose you have written a Python script that carries out a simulation based on a physical model and creates a nice plot of the results. Now you wish to explore the model and run the calculation on many different sets of input parameters. What is the best way to proceed?

You could write a script that runs the same calculation on a list of inputs that you select ahead of time:

inputs = [(x0, y0, z0), (x1, y1, z1), (x2, y2, z2)]
for I in inputs:
    # Insert simulation code here.

You could place your script inside a function and call the function repeatedly from the IPython command line with different inputs:

def run_simulation(x, y, z):
    # Insert simulation code here.

Both methods work, but both neither is ideal. If you use a script with an input list, you have to select the inputs ahead of time. If you find an interesting region of parameter space, you have to modify the script and run it again. If you embed your simulation within a function, you have to do a lot of typing at the command line.

A simple graphical user interface (GUI) that runs the script would be a convenient way to explore the model. You could enter your inputs in the appropriate boxes — or even set them with slider bars — and run the simulation; change one, and run it again. Such an interface would also make it much easier for someone else to run your code.

This post will describe how to create a simple graphical user interface (GUI) for your own functions and scripts using the Tkinter module. The goal is not to provide an introduction to GUI development or to create a beautiful user interface. Instead, this post will focus on building a minimal GUI wrapper for a working Python program. You can learn how to add the bells and whistles later.

Anatomy of a GUI

Take a look at the screenshot of the GUI below. This post will describe how to construct it from scratch.

A simple GUI.
A simple GUI.

A GUI is an interactive window that controls a program or series of programs. It allows the user to provide input to and receive feedback from those programs. Before getting into the details of creating this particular GUI, let’s examine a few of the key ideas behind GUI programming.

A GUI is built from widgets.

A widget is, in essence, anything you can put inside a GUI. These are the fundamental units from which a GUI is built:

  • Text Boxes: These are inactive widgets that simply display a text message.

  • Entry Boxes: These are interactive text boxes. The user can enter text or numbers. The data in an entry box can be extracted and used within programs.

  • Buttons: These are interactive regions of the GUI. When the user clicks on a button, it usually causes something to happen. The button can display explanatory text.

Widgets are packed into frames.

A frame is an abstract region of the GUI occupied by a widget or collection of widgets. The GUI program will arrange frames within the GUI window and try to adjust the size of the frames to fill the available space.

We will pack widgets into frames by creating a widget, then specifying which frame to put it in. We can provide additional information about where it is to be placed, like “top”, “bottom”, “left”, or “right”.

We can also pack frames within frames. For instance, in the GUI above, the third line of the window contains a text box, an entry box, a button, and another text box that displays the result of the calculation. All of these widgets were packed into a single frame, then this frame was packed within the application window frame.

Events trigger actions.

When we interact with a GUI, we expect something to happen. An event is something that happens within the GUI: you press a key or click on a button. GUI programming links events like these to specific actions. The great thing about building your own GUIs is that you get to choose both the events and the actions.

  • A program runs when you click on a <Run> button.

  • A variable is updated when you click the mouse in a certain region of the window.

  • A calculation is carried out after you press <Enter> in a text box.

  • The window closes when you press the <Escape> key.

Events are bound to widgets.

When creating a GUI, we bind events to specific widgets. For instance, if you create a <Press Here> button, you get to assign the event “click mouse on <Press Here> button” to anything you like:

  • do nothing
  • display a message
  • make noise
  • evaluate a numerical calculation
  • run a Monte Carlo simulation
  • create a plot
  • exit the program

The same event can have different effects with different widgets: pressing <Escape> in an entry box may clear the entry, but pressing <Escape> outside of the entry box may close the window.

A Word on GUI Design

It is easy to get distracted by all of the options available in GUI programming, by all of the features you could add, by all of the fine-tuning you can do to the appearance and interface. To avoid getting sidetracked, you should plan before you write any code:

  1. Decide which events are necessary. If your goal is to enter the value of two parameters, then run a simulation, stick to that. Anything else is unnecessary.

  2. Decide which widgets you need to carry out your task. For the example above, we might use two entry boxes and a <Run> button. Perhaps some explanatory text boxes would also be helpful. The 3D control knobs for adjusting parameter values can wait …

  3. Make a quick sketch of the layout. This will help you in packing your minimal set of widgets into the window.

Now you are ready to build a GUI!

The Tkinter Module

To actually construct a GUI, we need to choose a GUI programming library. Tkinter is the standard GUI library for Python, and it is included with almost every Python distribution. It has the benefits of being widely available and platform-independent. (I.e., you can create a GUI for Windows, Mac OS X, or Linux with the same Python code.) There are many other options, but this post will focus exclusively on Tkinter.

Tkinter provides an object-oriented framework for GUI programming. There are objects like Frame, Label, Entry, and Button that implement the widgets we need. We will build a GUI by creating a collection of these objects, then using their methods to adjust their properties and pack them together into a single window.

To gain access to the Tkinter module, we must import it. In my scripts I use the following lines:

try:
    # This will work in Python 2.7
    import Tkinter
except ImportError:
    # This will work in Python 3.5
    import tkinter as Tkinter

For some reason, Python 3 uses the lower-case tkinter while Python 2.7 uses the upper-case Tkinter. The lines above will work with either environment and allow us to access the module as Tkinter.

There is one more caveat for those who use the Anaconda distribution of Python. If you are going to use PyPlot and Tkinter in the same program, you need to instruct PyPlot to use a Tk-based back end for displaying plots:

import matplotlib
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt

If you do not set the back end before importing PyPlot, you may see a bunch of error messages instead of your GUI, even if you never call a PyPlot command.

Some Simple GUIs

The first few GUIs we build will be simple and not very useful, but they will illustrate the basic properties of any GUI.

Your First GUI

Together with the import lines above, the following two lines will create the simplest GUI possible: an empty window that does absolutely nothing.

# Create main window.
root = Tkinter.Tk()

# Activate the window.
root.mainloop()

The first command creates an object for the window that will contain the entire GUI. According to the docstrings of Tkinter.Tk, this object is a

Toplevel widget of Tk which represents mostly the main window
of an application. It has an associated Tcl interpreter.

This command should probably come near the top of your program. Later, we will pack all of our widgets and frames into this master widget.

The second command should come near the end of your program. It launches the window. Any code you type after this command will not have any effect until the window closes.

Try the import command and the two commands above to make sure you can create a Tkinter window on your system. You should see a blank application window with the title “tk”. The window you create may not appear in the foreground. You can close it by clicking on the close button, as you would any other application.

Adding a Widget

Now let’s add the simplest possible widget to our GUI — a text box — and pack it into the main window.

# Create main window.
root = Tkinter.Tk()

# Create a text box and pack it in.
greeting = Tkinter.Label(text="Hello, world!")
greeting.pack()

# Activate the window.
root.mainloop()

Note how the widget was created. I created a variable greeting and assigned it to a Tkinter.Label object. When I created the object, I used the keyword argument text="Hello, world!" to set the message. Then, I packed this newly created widget into the main window.

If you run this script, you will see the effect of packing a single widget. The resulting window is tiny — just large enough to contain the text message. The widgets are packed into the smallest amount of space that will contain them.

Binding an Event to a Keystroke

Next, we will bind an event to a keystroke within the main window. With the following script, you can now close the window by pressing the <Escape> key as long as the window is active.

# Create main window.
root = Tkinter.Tk()

# Create a text box and pack it in.
greeting = Tkinter.Label(text="Hello, world!")
greeting.pack()

# Define a function to close the window.
def quit(event=None):
    root.destroy()

# Cause pressing <Esc> to close the window.
root.bind('<Escape>', quit)

# Activate the window.
root.mainloop()

Notice the procedure used here. First, I defined a function that carried out some action. In this case, it calls the destroy() method of the root window, closing the window. Next, I used the bind() method of the root window to associate this function with a particular event: “user presses the <Escape> key.” Note that you pass only the function name to the bind() method — no arguments and no parentheses. The following would have produced an error:

root.bind('<Escape>', quit() )

Some Tkinter methods pass an event object to the function they are given, and some do not. To accommodate both types of methods, I gave my function an optional argument with a default value.

If you want to bind an event to a mouse click instead of a keystroke, use '<Button-1>' as the “key”. Add the following lines to the script above, just before the root.mainloop() command:

def ring(event=None):
    root.bell()

root.bind('<Button-1>', ring)

Now you will here a beep any time you click the mouse inside the main window.

Binding an Event to a Widget

You can also bind events to specific widgets within the main window. Let’s add a button to close the window.

# Create main window.
root = Tkinter.Tk()

# Create a text box and pack it in.
greeting = Tkinter.Label(text="Hello, world!")
greeting.pack()

# Define a function to close the window.
def quit(event=None):
    root.destroy()

# Cause pressing <Esc> to close the window.
root.bind('<Escape>', quit)

# Create a button that will close the window.
button = Tkinter.Button(text="Exit", command=quit)
button.pack(side='bottom', fill='both')

# Activate the window.
root.mainloop()

In this example, I have created a button with some text and bound it to the quit() command that closes the window. Notice, also, that I have provided instructions for where the button is to be placed: side='bottom'. I also specified that the button should be expanded to fill all of the available space around it in the frame.

Entry Boxes, Variables, and Frames

Now, let’s look at the last few elements we will need to create a useful GUI.

An entry box allows you to pass information to programs called by the GUI. In order for the GUI to keep track of the variables it contains, we need to assign the data in an entry box to a Tkinter variable. Tkinter recognizes several types: IntVar for integers, DoubleVar for floats, StringVar for strings. I prefer to use strings to store exactly what the user types, then convert this to other types as needed.

If you provide an entry box, it is often useful to provide a text box that indicates what the user is entering. This can create a problem in packing: You want the text box and entry box to be side by side, but the program that arranges all of our widgets in the main window might not arrange things in an aesthetically pleasing manner. The solution is to pack the text box and entry box into a separate frame, then pack this frame into the application window.

The script logarithm.py below illustrates all of these ideas. Liberal comments explain all of the steps in the construction of this GUI, which computes the logarithm of a number entered by the user. You can evaluate the logarithm by pressing the “is” button or by pressing <Enter> in the entry box.

A Useful GUI

Now it is time to assemble everything into a useful application. The script ‘interference.py’ below creates a GUI wrapper that allows the user to set the amplitude and frequency of two waves. The function it calls adds the two waves together and displays the resulting interference pattern.

It uses one new construct: a grid for arranging the input text boxes and entry boxes. Instead of calling widget.pack() to place a widget, one calls widget.grid(I, J) to place the widget in cell (I,J) of a grid. The upper left corner of the grid is cell (0,0).

This script is not a sterling example of GUI programming. The functions that do the numerical calculation and create the graph should defined in a separate module so that they can be run with or without the GUI. The GUI wrapper should import the function that creates the plot from this module. However, ‘interference.py’ has the benefit of being self-contained: you can copy and paste it into your own editor and run it without further modification.

Once you understand the basics of constructing a GUI wrapper for a program, you can simplify the process by writing functions to help create the GUI! For instance, you could write a function that takes a list of variable names and automatically generates a grid of text and entry boxes.

And there are always embellishments. You could replace entry boxes with sliders for some variables. (Look up Tkinter.Scale.) You can add check boxes for Boolean variables. (Look up Tkinter.Checkbutton.) You can add menus and save dialogs and … well, you get the picture. Just don’t spend so much time building a fancy interface that you have none left to actually run the simulation!

Summary

This post covered a lot of ground quickly, but I hope it has provided enough information for you to create a GUI window to run your own scripts whenever this is a useful thing to do. (Many programs do not benefit from a GUI at all, and it is not useful to create a GUI for a program that does not yet run properly …)

Design a GUI before you start building it. Decide what it should do first. Then identify the widgets that will accomplish your goal and sketch the layout of the application before you write any GUI code. The Tkinter module available with most Python distributions provides a suite of tools for building GUIs in Python.

This post has only scratched the surface of the Tkinter module and has completely ignored other GUI programming libraries. A Web search for “Tkinter” or “GUI programming with Python” will reveal a wealth of resources for more advanced GUI programming.











Code Samples

The logarithm.py Module

# -----------------------------------------------------------------------------
# logarithm.py
# ----------------------------------------------------------------------------- 
"""
Create a GUI application to compute logarithms using the Tkinter module.
"""
try:
    # This will work in Python 2.7
    import Tkinter
except ImportError:
    # This will work in Python 3.5
    import tkinter as Tkinter

# -----------------------------------------------------------------------------
# Create main window.
# ----------------------------------------------------------------------------- 
root = Tkinter.Tk()

# Create two text boxes and pack them in.
greeting = Tkinter.Label(text="Hello, world!")
greeting.pack(side='top')

advertisement = Tkinter.Label(text="I am logarithm computing GUI.")
advertisement.pack(side='top')

# Define a function to close the window.
def quit(event=None):
    root.destroy()

# Cause pressing <Esc> to close the window.
root.bind('<Escape>', quit)

# Create a button that will close the window.
button = Tkinter.Button(text="Exit", command=quit)
button.pack(side='bottom', fill='both')

# -----------------------------------------------------------------------------
# Create a frame within the main window.
# ----------------------------------------------------------------------------- 
# The frame will contain the widgets needed to do a calculation.
# Each widget in "frame" is created with "frame" as its first argument.
frame = Tkinter.Frame(root)
frame.pack(side='top')

# Create a text box that explains the calculation.
invitation = Tkinter.Label(frame, text="The natural logarithm of")
invitation.pack(side='left')

# Define an input variable and add an entry box so the user can change its value.
x = Tkinter.StringVar()
x.set('2.71828')
x_entry = Tkinter.Entry(frame, width=8, textvariable=x)
x_entry.pack(side='left')

# Define an output variable and a function to compute its value.
y = Tkinter.StringVar()

def compute_y(event=None):
    from math import log
    # Get x and y from outside the function.
    global x, y

    # Get the string value of the x StringVar and convert it to a float.
    x_value = float(x.get())

    # Compute the floating point value of y.
    y_value = log(x_value)

    # Convert this to a formatted string, and store it in the y StringVar.
    y.set('%.6f' % y_value)

# Bind an event to the x_entry box: pressing <Enter> will calculate the
# logarithm of whatever number the user has typed.
x_entry.bind('<Return>', compute_y)

# Create a button to perform the calculation and pack it into the frame.
compute = Tkinter.Button(frame, text=' is ', command=compute_y)
compute.pack(side='left')

# Create a text box that displays the value of the y StringVar.
y_label = Tkinter.Label(frame, textvariable=y, width=8)
y_label.pack(side='left')

# -----------------------------------------------------------------------------
# Activate the window.
# ----------------------------------------------------------------------------- 
root.mainloop()

The interference.py Module

# -----------------------------------------------------------------------------
# interference.py
# ----------------------------------------------------------------------------- 
"""
Author:     Jesse M. Kinder
Created:    2016 Apr 15
Modified:   2016 Apr 15

Description
-----------
Build a GUI wrapper to explore the interference pattern of two waves.
"""
try:
    # This will work in Python 2.7
    import Tkinter
except ImportError:
    # This will work in Python 3.5
    import tkinter as Tkinter

# -----------------------------------------------------------------------------
# To use matplotlib, the author must use the TkAgg backend, or none of this will
# work and a long string of inexplicable error messages will ensue.
# ----------------------------------------------------------------------------- 
import matplotlib
matplotlib.use('TkAgg')
import numpy as np
import matplotlib.pyplot as plt

# Define a bold font:
BOLD = ('Courier', '24', 'bold')

# Create main application window.
root = Tkinter.Tk()

# Create a text box explaining the application.
greeting = Tkinter.Label(text="Create an Interference Pattern", font=BOLD)
greeting.pack(side='top')

# Create a frame for variable names and entry boxes for their values.
frame = Tkinter.Frame(root)
frame.pack(side='top')

# Variables for the calculation, and default values.
amplitudeA = Tkinter.StringVar()
amplitudeA.set('1.0')
frequencyA = Tkinter.StringVar()
frequencyA.set('1.0')

amplitudeB = Tkinter.StringVar()
amplitudeB.set('1.0')
frequencyB = Tkinter.StringVar()
frequencyB.set('1.0')

deltaPhi = Tkinter.StringVar()
deltaPhi.set('0.0')

# Create text boxes and entry boxes for the variables.
# Use grid geometry manager instead of packing the entries in.
row_counter = 0
aa_text = Tkinter.Label(frame, text='Amplitude of 1st wave:') 
aa_text.grid(row=row_counter, column=0)

aa_entry = Tkinter.Entry(frame, width=8, textvariable=amplitudeA)
aa_entry.grid(row=row_counter, column=1)

row_counter += 1
fa_text = Tkinter.Label(frame, text='Frequency of 1st wave:') 
fa_text.grid(row=row_counter, column=0)

fa_entry = Tkinter.Entry(frame, width=8, textvariable=frequencyA)
fa_entry.grid(row=row_counter, column=1)

row_counter += 1
ab_text = Tkinter.Label(frame, text='Amplitude of 2nd wave:') 
ab_text.grid(row=row_counter, column=0)

ab_entry = Tkinter.Entry(frame, width=8, textvariable=amplitudeB)
ab_entry.grid(row=row_counter, column=1)

row_counter += 1
fb_text = Tkinter.Label(frame, text='Frequency of 2nd wave:') 
fb_text.grid(row=row_counter, column=0)

fb_entry = Tkinter.Entry(frame, width=8, textvariable=frequencyB)
fb_entry.grid(row=row_counter, column=1)

row_counter += 1
dp_text = Tkinter.Label(frame, text='Phase Difference:') 
dp_text.grid(row=row_counter, column=0)

dp_entry = Tkinter.Entry(frame, width=8, textvariable=deltaPhi)
dp_entry.grid(row=row_counter, column=1)

# Define a function to create the desired plot.
def make_plot(event=None):
    # Get these variables from outside the function, and update them.
    global amplitudeA, frequencyA, amplitudeB, frequencyB, deltaPhi

    # Convert StringVar data to numerical data.
    aa = float(amplitudeA.get())
    fa = float(frequencyA.get())
    ab = float(amplitudeB.get())
    fb = float(frequencyB.get())
    phi = float(deltaPhi.get())

    # Define the range of the plot.
    t_min = -10
    t_max = 10
    dt = 0.01
    t = np.arange(t_min, t_max+dt, dt)

    # Create the two waves and find the combined intensity.
    waveA = aa * np.cos(fa * t)
    waveB = ab * np.cos(fb * t + phi)
    intensity = (waveA + waveB)**2

    # Create the plot.
    plt.figure()
    plt.plot(t, intensity, lw=3)
    plt.title('Interference Pattern')
    plt.xlabel('Time')
    plt.ylabel('Intensity')
    plt.show()


# Add a button to create the plot.
MakePlot = Tkinter.Button(root, command=make_plot, text="Create Plot")
MakePlot.pack(side='bottom', fill='both')

# Allow pressing <Return> to create plot.
root.bind('<Return>', make_plot)

# Allow pressing <Esc> to close the window.
root.bind('<Escape>', root.destroy)

# Activate the window.
root.mainloop()

1 comment:

  1. Thanks for sharing the information about the Python and keep updating us.This information is really useful to me.

    ReplyDelete

Thank you for your input!
To avoid duplication, spam, and inappropriate content, we will review comments before posting them.


— Jesse & Phil