Heavy widgetry
This lesson will discuss the joys of using Tk to develop
graphical user interfaces (GUI) in Perl. Tk was originally
developed for the scripting language Tcl, but a perl port has been around
for many years now, and comes with the ActiveState Windows port of perl.
Using Tk is relatively easy, the canonical script being
simply:
perl -MTk -e "$mw = MainWindow->new;
$mw->Button( -text => 'Hello World', -command => sub{exit} )->pack; MainLoop"
However, I have found it very difficult to find documentation on the
more esoteric aspects of Tk, and the memory-leaks that await
the unwary. We will briefly look at the anatomy of a Tk
script in the first part of this lesson, then throw ourselves into
something meatier, and how to create composite widgets of your very
own.
Before you go leaping into writing a GUI, it is very important to think about what your end product should do. Unlike a normal script, there is usually no clear linear thread of control in a GUI. In a normal script, perl essentially executes a list of commands: open file, read file, munge file, write file, exit. A GUI, on the other hand, will likely have a menubar with commands like Open, Close, Munge, Write and Exit. What should happen if the user selects 'Munge' before 'Open'? Should Munge be disabled until a file has been Opened? How will you keep track of whether your program has opened a valid file? These design decisions should be largely made before you go anywhere near a text editor.
Using Tk generally requires you to consider three
things:
- The arty ergonomic bit: i.e. the interface itself. This is generally the easy bit, but can be very time consuming. I would recommend ignoring this bit until you have fleshed out the rest, otherwise you will spend a great deal of time gilding turds before you realise that the beautiful options menu you have just written is a terrible way to implement option choices, and what you really wanted was a window with some checkboxes.
- The linear code bit: i.e. the subroutines that will be invoked when you click a button, or request a file save, or press Ctrl-N.
- The housekeeping: i.e. the bits that tie the two together.
This includes the event-loop (more below), and e.g. disabling
Munge until the file-scoped lexical variable
$filehas had something Opened and read into it. This is the bit that can get rather complex. It may be worth thinking of the various modes in which your GUI can operate e.g. 'not got a file' (when we start up) and 'got a file'. We can use a file-scoped lexical to hold this data, allowing subroutinesopenandcloseto toggle it, and enable/disable callbacks tomungedepending on its value.
Enough waffle. How about a concrete example? First, the basic anatomy
of a perl Tk script:
#!/usr/bin/perl
use strict;
use warnings;
use Tk;
# create window
my $mw = MainWindow->new( -title => "Basic Tk script" );
# create menubar
my $menu = $mw->Menu();
my $file_menu = $menu->cascade
( -label => 'File' );
$file_menu->command
( -label => 'Exit', -command => sub {exit} );
my $help_menu = $menu->cascade
( -label => 'Help' );
$help_menu->command
( -label => 'Help', -command => sub { print "you'll be lucky" } );
$mw->configure( -menu => $menu );
# create widgets
my $button = $mw->Button
( -text => "Press me", -command => \&press );
my $label = $mw->Label
( -text => "foo", -borderwidth => 2, -relief => "groove" );
# geometry management
$label->pack
( -padx => 10, -pady => 10, -ipadx => 10, -ipady => 10 );
$button->pack
( -pady => 10 );
# key bindings
$mw->bind
( "<Alt-F4>" => sub { exit } );
# event loop
MainLoop();
# create callbacks
sub press
{
my $label_message = $label->cget( '-text' );
print "Pressed while label was in state $label_message\n";
$label->configure
( -text => $label_message eq "foo" ? "bar" : "foo" );
}
This script demonstrates the use of the eight main things you'll find
in a Tk script: main windows, menubars, widgets, geometry
management, bindings, callbacks, configures and event loops.
- The first thing to understand is the creation of a main window. In
Tk, this is accomplished using theMainWindow->new()method. You'll notice that configurable options like the window title are conventionally passed toTkmethods with switch-style hashes( -foo => bar ). - Most windows want a menubar of some sort. This is created using the
Menumethod, which creates a widget object of classMenu. A widget is simply any 'bit' of a GUI: a button, menu, select-box, etc. Since we wish this menu to be a child element of our main window, we simply callMenuas a method of theMainWindowclass:$mw->Menu(). This may be at odds with what you have usually done to instantiate objects (callingnewor similar), but this is how you group objects together inTk, to allow them to be manipulated as one: for example, when the main window is destroyed it ensures all the child widgets inside it get destroyed too. After adding some cascading submenus, we add the menubar to the main window using theconfiguremethod (which we'll explain more in a minute). - There are many
Tkwidgets. This script demonstrates the use of two others, theButtonand theLabel. All widgets have a generic set of attributes that can be set, such as-borderwidthand-relief, which modify how they are displayed. In addition, some widgets define configuration options that can be used to set widget-specific things like-widthand-height, forButtons andLabels. To see which widgets are available, consultperldoc Tk. The most commonly used ones are probably:Button. Buttons.Label. Simple text.Entry. Like a web form input text field.Text. Mini word processor.Radiobutton. Radio buttons, as beloved of web forms.Checkbutton. Checkboxes.Frame. Simple boxes, just there to hold widgets in neatly delimited groups.Toplevel. Subordinate main windows.getOpenFile getSaveFile Dialog. Pop-up open, save and dialogue boxes.Canvas. Generic drawing/graphing tool.Photo. Images (be very careful using this one, it is prone to cause memory leaks if you don't know what you're doing, and often even if you do!).Menu. Menubars.Scale. Sliders.Scrollbar. Scrollbars: many widgets also support the-scrollbars => "nw"configuration for adding scrollbars to the top (n) and right (w) of a widget.
- When we create child widgets they only exist in potentia
until we add them to our window with a geometry manager. There are
three of these in
Tk:pack,gridandplace. The first of these packs widgets into a parent widget in much the same was as a web browser packs things in the the absence of a stylesheet: things get put in approximately the place you expect, from left to right, and top to bottom as they wrap. The pad the widget with space, we can configure-padx(horizontal padding outside the widget),-ipady(vertical padding within the widget: for the button, this ensures the button is larger than the text inside), etc.gridallows you more precision over where things get put. Imagine the window as a grid, and:$widget->grid( -column => 1, -row => 2, -columnspan => 2 );
ensures the widget ends up in grid columns 2-4, grid row 3 (it's zero-based, like arrays). I mostly usegridfor my geometry management; this is probably a hangover from using tables to arrange things in webpages. - Keyboard bindings (and mouse bindings) allow you to bind certain
key presses, and patterns of mouse clicks to events. Most windows users
expect Alt-F4 to close the application, so this is exactly what we
provide here. Creating these bindings is as simple as calling the
bindmethod on the appropriate widget, with a sequence to bind and a callback (I'll explain what this is in a minute). The names of the bindings are mostly quite explanatory (perldoc Tk::bindif they are not):<Alt-F4>,<Control-P>,<Button-2>(right mouse button),<Double-Button-1>(double click left mouse button), etc. Bear in mind when creating keyboard binding that (at least under Windows), most users will expect the following key bindings not to violate the principle of least surprise...
- Ctrl-C, X, V Copy, Cut, Paste
- Ctrl-O, N, S, P Open, New, Save, Print
- Ctrl-B, U, I Bold, Underline, Italic
- Ctrl-A Select all
- Ctrl-Z Undo
- Ctrl-F Find
- Ctrl-F4 Close
- Alt-F4 Exit
- Alt-F File menu
- F1 Help
- F2 Rename
- F5 Refresh
- Windows-R Run
- Alt-Space Alt-N Minimise
- Alt-tab Switch windows
- Ctrl-Alt-Delete Interrupt
- So far we have explored only the static bits of a GUI: the widgets,
and their placement. However, the whole point of a GUI is that it
responds when you press buttons, click on menus and press Alt-F4. The
way we do this is to create callbacks. We have seen several
examples of creating callbacks in the script above:
$mw->bind ( "<Alt-F4>" => sub { exit } ); $file_menu->command ( -label => 'Exit', -command => sub {exit} ); my $button = $mw->Button ( -text => "Press me", -command => \&press );These don't look like they have much in common, but all of them set up a callback. The first one does it usingbind. The second argument tobindis always a callback, which, as you can see, is simply a coderef: either an anonymoussub { exit }, or a reference to a named subroutine\&press. The second two demonstrate the use of the-commandoption, which will also take a reference to a subroutine, and set up the appropriate call back. The upshot is that when you press Alt-F4, or select "Exit" on the menubar, the application will exit, and when you click on the button, it will invoke thepresssubroutine. - So what does this
presssubroutine do? It does three things: the first is to get the value of a widget using thecget(configuration-get) method. All widgets support this method, which will return the value of whatever is requested: here we ask for the current value of the button's-textoption. The next thing the subroutine does is interact with the shell that spawned it: printing things to the console window is quite useful for debugging purposes. If you want to run your GUI without an accompanying black box, you can invoke it withwperlrather thanperl. The final thing the subroutine does is use theconfiguremethod: this is the write version of read-onlycget. Again, this is supported by all widgets, and can be used to change the default values of options that were set when the widget was created. This one simply swaps the text in theLabelwidget. - Finally, once we have set up our window, it is time to run the user
interface. Nothing will actually happen until we call
MainLoop, which is essentially an infinite loopwhile ( 1 ) { process tk events }, which is termed an event loop. The event loop basically sits there, responding to key presses and mouse movements, and calling the appropriate callbacks.
That's largely all there is to a basic Tk application:
you simply create the widgets, geometry them into place, set up bindings
to callbacks, and kick off the event loop. So let's see this in action in
a slightly more interesting fashion.
The cradle of civilisation
I have a difficult time remembering all the political machinations that have occurred in Mesopotamia since 5000 BC. You may have the same problem (if you too have no life). We shall create an aide-mémoire that displays the history in a pretty and comprehensible fashion:
#!/usr/bin/perl
use strict;
use warnings;
use Tk;
my $mw = MainWindow->new( -title => "Mesopotamia" );
my $map = $mw->Photo
( -file => 'map.bmp', -height => 220, width => 300 );
my $map_label = $mw->Label
( -image => $map, -relief => "groove", -borderwidth => 2 )
->grid( -row => 0, -column => 0,
-pady => 20, -padx => 20 );
my $scale = $mw->Scale
( -from => -5000, -to => 2010, -orient => "horiz",
-length => 300, -resolution => 10 )
->grid( -row => 1, -column => 0 );
my $frame = $mw->Frame
( -relief => "groove", -borderwidth => 2 )
->grid( -row => 2, -column => 0, -pady => 20 );
my $label = $frame->Label
( -wraplength => 200, -width => 50, -height => 5 )
->pack();
First we create a Photo, which we will use to display a
map later. Tk natively supports bitmaps and gifs, but if you
want to display jpegs, you;ll need to install Tk::JPEG. Then
we set up a few widgets: a Label, into which we stick our
map image using the -image options: all widgets support the
-image and -text options, which can be used to
give widgets some character. We also set up a Scale to
represent the passing of history (at a resolution of one decade), and a
Framed Label to store some historical data.
Although we can only use one geometry manager to arrange widgets within
another widget (we only use grid to arrange the widgets in
the main window), we are free to use a more convenient one to
e.g. pack widgets in the Frame.
my %data;
my @ordered;
while ( <DATA> )
{
next unless /\w/;
my ( $date, $description ) = / (\S*) \s*:\s* (.*) /x;
unshift @ordered, $date;
my $abs_date = abs $date;
$description = $abs_date .
( $date < 0 ? " BC $description" : " AD $description" );
$data{ $date } = $description;
}
This bit simply reads some data in from the DATA
filehandle and dumps it into a hash.
$mw->bind( "<Alt-F4>" => sub { Tk::exit } );
$mw->repeat( 250 => \&update );
MainLoop();
Here I do my superstitious thing: always have a keyboard shortcut way
out! We also use the repeat function to set up a callback
that will be executed and rescheduled every 250 milliseconds.
Tk allows you to schedule several sorts of event:
perldoc Tk::after for details. This code ensures the text in
the Frame is updated regularly as the slider is moved up and
down.
sub update
{
my $date = $scale->get();
my $chosen;
for ( @ordered )
{
$chosen = $_;
last if $_ <= $date;
}
$label->configure( -text => $data{$chosen} );
}
Here we obtain the scaled value and use it to configure
the text in the Frame. The last bit is our data (note that
the original has unbroken lines, here it is broken up to prevent
overspill):
__DATA__
-5000 : The Sumerians found Uruk, Ur, and other city states in southern
Mesopotamia.
-2330 : The Akkadians, under Sargon I, found the Akkadian Empire.
-2218 : The Gutians, under the 3rd dynasty of Ur are final flowering of Sumeria.
-2000 : The Elamites destroy the city of Ur.
-1800 : The Babylonians, under Hammurabi, found the city of Babylon,
as the Amorites gain control of Assur.
-1595 : The Hittites raid Babylon from their power base in Anatolia, crippling
the empire.
-1580 : The Kassites rule Babylon.
-1580 : The Mittanites rule Assur, having been set up by Hurrians from the
Caucasus.
-1350 : The Assyrians defeat the Mittanites in Assur, and begin empire-building,
with a capital at Nineveh.
-1225 : The Assyrians conquer Babylon briefly, and soon after Aramaean tribes
from Syria and Chaldean tribes overrun Babylonia.
-910 : Assyria expands again.
-612 : An alliance of Medes and Chaldeans cause Assyria to collapse.
-612 : Medes take the hill country, leaving Mesopotamia to the Chaldeans
(Neo-Babylonians) under Nebuchadnezzar II.
-539 : Chaldeans and Medes defeated by Cyrus the Great of Persia, who captures
Babylon.
-539 : Mesopotamia divided into the now Aramaic-speaking satrapies of Babylon
and Assur.
-311 : Alexander the Great conquers Asia Minor.
-323 : Macedonian empire disintegrates on Alexander's death.
-312 : Seleucus I enters Babylon, and Mesopotamia is incorporated into the
Seleucid empire.
-250 : The Arsacid rulers of Parthia take Mesopotamia.
226 : Sassanids from Persia extend their empire from the Euphrates to modern
Afghanistan.
230 : Conflict with the Roman province of Syria, later to become part of the
Byzantine empire (after 395).
635 : Arabs under the Umayyad caliphate in Damascus take Mesopotamia.
750 : Construction of Baghdad as the new capital of a Muslim empire under the
Abbasid caliphs with imported Turkish slaves.
1258 : Mongols sack Baghdad.
1410 : Mesopotamia attacked by Bedouins and Mongols.
1500 : Ottoman Turks and Safavid Persian rulers vie for control of Mesopotamia.
1910 : British troops take the area, Iraq mandated to Great Britain and Syria
to France.
1932 : Iraq independent.
2003 : Iraq invaded by United States of America.
Here is a screenshot of the products of this code. I ran this up in
about an hour, and most of that was unearthing the data: as you can see,
Tk makes creating GUIs as easy as the rest of Perl. Grab the
Mesopotamia script and Mesopotamia bitmap if you want them.

Widgets-a-plenty
So that's how to write Tk scripts. However, sometimes
you'll notice that a bunch of widgets you are using look like they ought
to be somehow amalgamated into one, so you could handle them as a single
unit. Say you want to create an Entry widget that has an associated
Label. What you want to do is create a 'composite widget'. In this case,
the widget you are looking for has already been created, and it's called
the LabEntry widget. It happens to be the smallest composite widget
supplied with Tk, and appears here in more-or-less the form
you'll find it in your library:
package Tk::LabEntry;
use Tk::Frame;
use Tk::Label;
use Tk::Entry;
our @ISA = qw( Tk::Frame );
Tk::Widget->Construct( 'LabEntry' );
sub Populate
{
my( $cw, $args ) = @_;
$cw->SUPER::Populate( $args );
my $e = $cw->Entry();
$e->pack( '-expand' => 1, '-fill' => 'both' );
$cw->Advertise( 'entry' => $e );
$cw->ConfigSpecs( DEFAULT => [ $e ] );
$cw->Delegates( DEFAULT => $e );
}
1;
The package name is, as usual, wherever you want to keep the module in
your library, and we obviously have to use (or
require) any widgets we want to include in our composite.
Most people chose to make their composites from a Frame
containing other subwidgets, and therefore it is sensible to make the
composite widget a subclass of Tk::Frame by putting this in
@ISA. That way, any calls to methods such as
label and scrollbars that you don't define
yourself will be redirected to the parent Frame.
The Construct method from Tk::Widget takes
the name you want people to call your widget when they create one. This
means that if you create a composite called
Bio::Simulation::Photosynthesis people can create it using
$parent->PS(); if you add Tk::Widget->Construct(
'PS' ); here.
Unlike most object-oriented modules, you'll notice a lack of a
new constructor. Composite widgets inherit their constructor
from a base class, all you need to do is provide a Populate
method. The purpose of this method is to create the widgets inside your
composite, pack them in, and then set up a number of
attributes that Tk will use when you call methods like
cget and configure. Populate is
always called with two options: the first is a reference to the composite
widget (here, $cw), the second a hashref of configuration
options (here, $arg). It is generally wise to call the
Populate method of the parent class using
SUPER::Populate($args) at some point, to initialise any
callbacks that the parent widget requires. You can then do the boring bit
(creating widgets and packing them in).
Finally, we need to tell Tk what to do when users call
configure, and other standard utility methods like
cget and Subwidget. That's what these things
do:
$cw->Advertise( 'entry' => $e );
$cw->ConfigSpecs( DEFAULT => [ $e ] );
$cw->Delegates( DEFAULT => $e );
Advertise tells Tk what to do with requests
for subwidgets. The LabEntry composite allows you to get
direct access to its Entry widget using:
my $lab_entry = $mw->LabEntry(); my $entry_widget_inside = $lab_entry->Subwidget( 'entry' )
Delegates tells Tk which method to call when
someone invokes:
$lab_entry->insert( "blah" );
If the LabEntry widget doesn't define its own
insert method, then it clearly wants to tell one of its
subwidgets to do the inserting for it. You can either tell it exactly
which method to delegate to which subwidget:
$cw->Delegates( 'insert' => $e );
And/or set up a default, as this class does: everything gets delegated
to the Entry widget. Finally, the ConfigSpecs
method tells Tk what to do when configure and
cget are called. In this LabEntry case, a
simple default is set up, again delegating all calls to the
Entry subwidget. ConfigSpecs takes a hash of
options: the keys are the names of -options that can be
configured and the values are arrayrefs showing how they should be
handled. To set up a default, you merely need to call:
$cw->ConfigSpecs( DEFAULT => [ $e ] );
with an arrayref containing only the object that will get/set the
configurable options. For something a little more complex, here is a
section from the module I wrote to simulate phototaxis:
ConfigSpecs is told to call the population
method when someone does something like $taxis_widget->cget(
"-population" ), the width method for
$taxis_widget->configure( -width => 20 ), and to
fallback to a Canvas subwidget otherwise. The first element
in the arrayref indicates what you want to do: call a METHOD (the most
general solution), invoke a CALLBACK, PASSIVE-ly store the data
somewhere, etc. The remaining three are the name of the option
in the widget's database, its class, and its default value. Don't worry
too much about these three: see perldoc Tk::ConfigSpecs if
you do want to worry.
$taxis->ConfigSpecs
(
-population => [ 'METHOD', 'population', 'Population', undef ],
-width => [ 'METHOD', 'width', 'Width', undef ],
DEFAULT => [ $canvas ],
);
sub population
{
my ( $taxis, $population ) = @_;
if ( defined $population )
{
$taxis->{ population } = abs $population;
# we never know how stupid people can be
$taxis->_draw_arena();
}
if ( wantarray )
{
my $canvas = $taxis->Subwidget( 'canvas' );
my ( $left, $right ) = ( 0, 0 );
for my $i ( 1 .. $taxis->{ population } )
{
${ $taxis->{ critters } }[ $i ]{ pos }[ 0 ]
<= $canvas->cget( -width ) / 2 ?
$left++ :
$right++;
}
return $left, $right;
}
else
{
return $taxis->{ population };
}
}
sub width
{
my ( $taxis, $width ) = @_;
if ( $width )
{
$taxis->{ width } = $width;
$taxis->_draw_arena();
}
$taxis->{ width };
}
The methods that we call must obviously be able to get and/or set the relevant attributes, and configure any subwidgets appropriately.
There are several other aspects to creating a composite widget:
perldoc Tk::mega if you come across something you think you
should be able to do, but don't yet know how.
Tk gotchas
Tk can be a bit ropy at times. This is the only place in
Perl that if you find something going wrong and you're convinced you're
doing it right, then it may well be Tk's fault rather than
yours! The following are some troubles I have had:
- The configuration and option setting mechanism is a bit flabby, and
all those
-optionscause eyestrain. There doesn't seem to be much way around this: either the code looks impenetrable, or it is enormously long and samey. One thing that helps here is to create%default_options_for_buttonhashes:$mw->Button( %default_options_for_button ); - The documentation is somewhat odd: some methods are documented as
if they were modules, some documents are obtrusive but irrelevant to
the casual programmer, and some of the documentation is hidden in
places you would not necessarily expect to find it. For example, it
took me a good while to find the
getSaveFilemethod, which calls your system's default "Save File As" dialogue box. This was mostly because it is documented in thegetOpenFilemanpage, which was only the obvious place to look in retrospect. Similarly, much of the event loop stuff is not to be found in the event POD, but in the POD forTk::after. - Compounding this, some things aren't very well documented, and STFW
usually fails, since Google will direct you at ten thousand online
copies of the flawed POD documentation that you already own, and
through which you have already searched to your wit's end. The best
best is often to just look though the code in the
widgetsdemo script for things that do approximately what you want, and cannibalise them. - Some implementations are somewhat flaky: in particular,
Tk::Photois apt to leak memory. Violating the principle of least surprisewhile( 1 ){ my $photo = $mw->Photo( -file => $path ) }will leak memory like a sieve, despite the fact that you might well expect the photo to be destroyed at the end of the lexical scope. The reason for this is that thePhotoobject is created in an image manager, and if you want to really want to destroy the photo, you will need to call the image manager'sdeletemethod. Even then, I have found this is not infallible, and in particular, if you create aCanvaswithPhotos in it, it appears to be impossible to destroy them without leaking memory, no matter how many lexical scopes you leave ordestroys anddeletes you scream at the interpreter. The only work around I found was to load the images in, and simply hide them when not required. - If you call
exit, you will often get spoor left behind, and the script will exit ungracefully complaining that certain objects were no longer whatTkexpected them to be. UseTk::exitinstead.
