CCPN Tkinter Graphical Objects

General Principles

This last part of the programming tutorial involves the creation of graphical user interfaces. Writing a Python macro script for Analysis may be sufficient for simple, linear tasks, but a bespoke graphical interface will allow things to get more complex.

Widget Classes

The graphical user interfaces that are demonstrated here are constructed using graphical objects, which will be referred to as widgets. A widget is a Python object that has a graphical representation. Examples of widgets include popup windows (separate moveable areas on your screen that contain other widgets), buttons that you click to perform operations and entry fields that you can type text into. The basic idea is that you create graphical widgets to enable a user to set various parameters, perform operations and display results. All of the graphical widgets that we will demonstrate for use with CCPN are based on the Tkinter library (which is the Python implementation of the Tcl/Tk system), however the CCPN widgets have a layer of Python that mostly separates you from some of the underying Tkinter complexities and awkwardness.

Geometry Management

An important concept when building graphical interfaces is to arrange the widgets in an appropriate way. With the Tkinter based graphics system that CCPN currently uses there are three possible ways of specifying the locations of different widgets. Of these CCPN mostly uses the grid geometry manager (the alternatives being place and pack). As the name suggests this system allows you to locate widgets inside their window (or other parent) by specifying the row and column of an internal grid. The grid is not of a fixed size and will adapt to the components it contains, although often you have to be aware of how a widget inside a grid cell expands; sometimes you want a widget to stick to the edges of its cell and sometimes not, and given a whole grid you might want some rows and columns to respond to resizing, but have others remain fixed.

Updates & Callbacks

Having pretty graphical widgets arranged on your screen is just the start. Eventually you want your widgets to actually do something. A widget may change its appearance (i.e. update) to reflect the state of your data or may respond to the actions of the user (or both). All of this is controlled by calling Python functions. Accordingly when building graphical interfaces you have to be mindful of what events should call the update functions to change your graphics and which functions are called (i.e. a callback) when the user selects or click on something.

Widget Construction

Initially this part of the tutorial will give a simple guide on the basic placement and construction of  widgets, without giving a proper scientific example; that will follow in the next section.

The first thing that is requires when using the CCPN widgets is to create a top-level object to which all of the other graphical elements belong. This is usually called the root and below we explicitly create a new one. However, it is also common to extend an existing graphical setup. In such circumstances the top-level object will already exist and you simply have to add your new components to this (or one of its children). We will create a new root by using a function (method) called directly from the Tkinter library, but this will be the only time that we will use the Tkinter object directly. All following construction will be with the CCPN widget library.

import Tkinter

root = Tkinter.Tk()

Or you could do:

from Tkinter import Tk

root = Tk()

This root object is the parent window to which everything else will belong and be embedded within. Note that it is possible to make new windows which are not inside the root, but we will come to that later. After you have issued this at the Python command-line you will hopefully see a new box/window appear on screen which you can resize and close. Note that if you are entering commands into a file and running the Python as a script you will have to add the following command to make the graphics persist; otherwise your program will complete its execution and remove the graphics, immediately after rendering them.

# If running from a script

root.mainloop()

Given our top-level box, we will place a text label inside. This means we import a CCPN widget class called a Label and then make an instance of this kind of object, specifying the container (root in this case) it goes in, the text it should carry and the location on the geometry grid.

from memops.gui.Label import Label

label = Label(root, text='This is a text example.', grid=(0,0))

Hopefully you will now see your new Label object appear as text within the existing window.

Observe how you can change the text within the existing labels:

label.set('Some totally different text')

Note that if you did not include the grid information initially then your object would not appear on screen; there would be no information on how to locate it. However, you could easily specify the location after the widget is created by using the grid method of the widget (part of its Tkinter specification), as follows:

label2 = Label(root, text='A second example.')

label2.grid(row=1, column=0)

Note that this second Label appears below the first because it is in row 1 rather than row 0.

Now put the labels side by side, with different grid parameters:

label2.grid(row=0, column=1)

Grab the corner of the root window and enlarge it. You will see that the Labels remain at the centre of the window. In essence the grid system has given the minimum space for the widgets and positioned it in the middle. We will now change this by giving the first column (number zero) priority:

root.grid_columnconfigure(0, weight=1)

You will see that this command separates the Labels. This is because the first column has expanded. Next we will move the first label to the right hand side of its column (as you can see the default is to the left). Note that the way of specifying directions in the Tkinter grid system is with letters representing the cardinal compass directions like 'N','S','SE','NW' etc.

label.grid(sticky='E')

Now try:

label.grid(sticky='NW')

This does not move the Label to the top-left as you might expect, only the left. This is because our row does not expand. So if you give the row weight it will move:

root.grid_rowconfigure(0, weight=1)

Frames

Frames are a simple widget that contain other widgets and they can be very useful in creating arrangements. They must exist inside a given window (like the Label above) but their have their own internal widget placement (grid in this case).

Close any existing windows you may have created and construct a new one as follows, noting that we are using a new type of widget called Entry, into which you can type text. The second column (1) is set to expand and we make sure the Entry widget expands to touch at either end (i.e. East and West).

import Tkinter

from memops.gui.Label import Label

from memops.gui.Entry import Entry

root = Tkinter.Tk()

root.grid_columnconfigure(1, weight=1)

label = Label(root, text='Label A', grid=(0,0))

entry = Entry(root, text='Type here', grid=(0,1), sticky='EW')

Now we have a grid system with two columns. Next we expand the second row and add a Frame with a red background to the window such that it covers both columns by specifying gridSpan (one row, two columns):

from memops.gui.Frame import Frame

root.grid_rowconfigure(1, weight=1)

frame = Frame(root, grid=(1,0), bg='red', gridSpan=(1,2))

We could also have used the long form, noting that unless we specify that the frame sticks to all sides ('NSEW') we won't actually see the frame until something is placed inside, causing it to expand.

# Long, traditional Tkinter form

frame = Frame(root, bg='green')

frame.grid(row=1, column=0, columnspan=2, sticky='NSEW')

Now inside our frame we will place a Button widget that we can press, to execute a command. In this case the command is this a very simple function we write for demonstration purposes. Take special note that we don't use root for constructing the button, we use the frame we just made and so the grid location for the button (0,0) is a location relative to the inside of the frame.

from memops.gui.Button import Button

def clickFunc():

  print "Button was pressed"

button = Button(frame, text='Press', command=clickFunc, grid=(0,0))

Left-click on the button and see that it produces output at the Python command-line as specified by the clickFunc() call.

If we wanted a really big button that expands with the frame, we would need to make the button stick to the sides:

button.grid(sticky='NSEW')

And then expand the frame:

frame.expandGrid(0,0)

# or use the long, traditional form

frame.grid_columnconfigure(0, weight=1)

frame.grid_rowconfigure(0, weight=1)

Widget Variety

The next example is just to illustrate variety of different widgets that you can use. We will actually make some of these work properly in the next section. So first import the widget classes:

from memops.gui.CheckButton import CheckButton

from memops.gui.Frame import Frame

from memops.gui.LabelDivider import LabelDivider

from memops.gui.LabelFrame import LabelFrame

from memops.gui.LinkChart import LinkChart

from memops.gui.Menu import Menu

from memops.gui.PulldownList import PulldownList

from memops.gui.RadioButtons import RadioButtons

from memops.gui.ScrolledGraph import ScrolledGraph

from memops.gui.ScrolledMatrix import ScrolledMatrix

from memops.gui.Text import Text

Then make a window :

import Tkinter

root = Tkinter.Tk()

root.grid_columnconfigure(0, weight=1)

Define some functions to call:

def functionA(*value):

 print "Called function A", value

def functionB(*value):

 print "Called function B", value

And make some widget instances inside the root window.

First there will be a menu at the top of the window. Note that we do not put the menu into the grid system, instead it is passes as a special menu option to the root window:

menu = Menu(root)

menu.add_command(label='Func A', shortcut='A', command=functionA)

menu.add_command(label='Func B', shortcut='B', command=functionB)

root.config(menu=menu)

A large text entry area, five rows high:

blurb = """

If you want to see how to setup the widgets in more detail,

look at the example code at the bottom of the files in the

$CCPN_HOME/python/memops/gui/ directory."""

text = Text(root, grid=(1,0), height=5, text=blurb)

Radio buttons. Here clicking on the buttons that are created called the functions we defined earlier. Clicking the different RadioButtons changes the selection to 'one', 'two' or 'three'.

radioButtons = RadioButtons(root, ['one', 'two', 'three'],

                           select_callback=functionA, grid=(3,0))

A bordered frame frame that is labeled, with a pulldown list inside. Note that the pulldown list takes a list of text strings for the display, but (if specified) passes back an object from a separate list.

labelFrame = LabelFrame(root, text='This is a LabelFrame',

                       grid=(4,0), gridSpan=(1,2))

labelFrame.expandGrid(2,0)

texts = ['One','Two','Three']

objects = [1,2,3]

pulldown = PulldownList(labelFrame, callback=functionA,

                       texts=texts, objects=objects, grid=(0,0))

A labeled separator, to split the frame in two:

divider = LabelDivider(labelFrame, text='New Section', grid=(1,0))

A table, that gets filled in with some numbers and text (including Unicode for greek characters) using the update() function:

headingList = ['#','Name','Square','Greek']

table = ScrolledMatrix(labelFrame, headingList=headingList,

                      callback=functionB, grid=(2,0))

textMatrix = [[1,'One',1.00,u'\u03B1'],

             [2,'Two',4.00,u'\u03B2'],

             [3,'Three',9.00, u'\u03B3'],

             [4,'Four',16.00, u'\u03B4']]

objectList = [1,2,3,4]

table.update(objectList=objectList, textMatrix=textMatrix)

A graph, showing some maths:

import math

numbers = [float(x/10.0) for x in range(1,11)]

dataSets = []

dataSets.append([(x,x*x) for x in numbers])

dataSets.append([(x,math.exp(x)) for x in numbers])

# The third value of 0.6 in the tuple is for error bars.

dataSets.append([(x,1/x,0.6) for x in numbers])

dataNames = ['x^2','e^x','1/x']

colors = ['#A00000','#008000','#0000C0']

graph = ScrolledGraph(labelFrame,dataSets=dataSets, title='Demo Graph',

                     width=300, height=200, dataNames=dataNames,

                     symbolSize=5, xLabel='x', yLabel='f(x)',

                     dataColors=colors, graphType='line', grid=(3,0))

Further Examples

If you want to see how to setup the widgets in more detail, look at the example code at the bottom of the Python files in the $CCPN_HOME/python/memops/gui/ directory. Most of the files can be run as Python scripts from the shell command-line to demonstrate something useful - for example:

> python  $CCPN_HOME/python/memops/gui/ScrolledMatrix.py