Steve Lidie
Well, hello! This is the first of several articles on the Tk toolkit, a marvelous object oriented Perl extension that provides a comprehensive widget collection for all kinds of spiffy graphical applications. Tk was developed by John K. Ousterhout and adapted and extended for Perl by Nick Ing-Simmons.
The Tk extension for Perl is referred to as Perl/Tk, and runs under the X Window System found on most Unix computers. X uses a client/server model, where clients (such as the one you are about to see) communicate with a server that manages the computer's display, keyboard, and mouse. For every display there is a window manager that provides a consistent "look and feel", at least at a high level, for all client's sharing the machine's display. There are many different window managers, but they all provide similar facilities, such as iconifying, moving, resizing windows, and framing them in decorative borders. You'll see window manager commands in later columns.
This article contains a gentle introduction to the fundamentals of Perl/Tk, after which it develops, step by step, a real application. As is typical of my writing style I often generate more questions than I answer, so you will want to keep this URL handy:
The current version of Perl/Tk is Tk800.012 and builds successfully against Perl 5.004_04 or 5.005. To obtain the latest Tk distribution link to a CPAN site near you by visting http://www.perl.com/
Perl/Tk programs are written using the object oriented syntax $object->method, where $object refers to a Tk widget, perhaps a Buttom or Menu, and method names an action to be performed. We'll learn more about objects and such in the next column, but now, without further ado, here is your prototypical "hello world" program written in Perl/Tk, swiped from the Tk distribution:
#
# Simple Tk script to create a button that prints "Hello, world". Click on
# the button to terminate the program.
#
# The first line below imports the Tk objects into the application, the
# second line creates the main window, the third through fifth lines create
# the button and define the code to execute when the button is pressed, the
# sixth line asks the packer to shrink-wrap the application's main window
# around the button, and the seventh line starts the event loop.
use Tk;
$MW = MainWindow->new;
$hello = $MW->Button(
-text => 'Hello, world',
-command => sub {print STDOUT "Hello, world\n"; exit;} );
$hello->pack;
MainLoop;
The program may or may not be self-explanatory! The main window, $MW, is the program's first toplevel window - the primary "container" for most, if not all, descendant widgets, which form a hierarchy (each widget always has a parent, and might have children as well).
This particular toplevel widget has a single child object belonging to the Button class. All widgets are objects derived from a base class, inheriting its characteristics. You might have several instances of button objects that look quite different, but share the distinguishing characteristics of class Button: they display a text label or bitmap, and "do something" when selected. When the button in the example is pressed the anonymous subroutine is executed which prints "Hello, world" and exits. The subroutine is called because it is bound to the button click. Almost all widget classes have default button and keypress bindings established by Perl/Tk, and you can add, delete or modify bindings on a class or per-widget basis as you see fit.
The statement:
Loading DB routines from perl5db.pl version 1.02
Emacs support available.
Enter h or `h h' for help.
main::(-e:1): 0
DB<1> use Tk
DB<2> $ref = {}
DB<3> $MW = MainWindow->new
DB<4> $oref = $MW->Button
DB<5> p $ref
HASH(0x200f78c8)
DB<6> p $oref
Tk::Button=HASH(0x2021c780)
The statement:
Perl/Tk programs are event driven, meaning that you do not write a "main loop" in the standard sense, but rather delegate that job to Tk. Instead, you write small code sections, referred to as callbacks, a fancy name for a subroutine, to process those events and which Tk invokes as required. There are many Tk events that need to be processed in a timely fashion: timers, file input and output, and motion and button events generated by your mouse. You activate the Tk event loop with a MainLoop() statement, which should be the last line called by your program.
In summary, most Perl/Tk applications share these common features:
- A use Tk statement at the beginning of the program which imports the base Tk definitions.
- A primary MainWindow as the root of the widget hierarchy.
- A series of widget creation commands.
- Optional binding and callback creation and registration commands. (More about these soon.)
- A series of geometry commands to pack widgets in a pleasing and user friendly manner.
- A MainLoop() command to begin program execution. (Actually, there are times when you must control event processing yourself; we'll see an example of this in a later column.)
Tk provides 15 standard widgets; Perl/Tk provides additional composite widgets like ColorEditor, Dial, FileSelect, LabEntry and Table. Composite widgets, also called megawidgets, are complex objects built from these standard widgets.
- Button widgets execute a callback when invoked. They're derived from the Label widget.
- Canvas widgets provide a drawing surface for text and graphics.
- Checkbutton widgets select one or more items from a list. They're derived from the Label widget.
- Entry widgets allow users to enter and edit a single text string.
- Frame widgets are primarily used as containers to group other widgets; for instance, during packing. Frames might be arranged inside an application's main window, with other widgets inside them. Frames are also used as spacers and to add colored borders.
- Label widgets display a text or image label. Button, Checkbutton and Radiobutton widgets are derived from the Label widget.
- Listbox widgets display a list of strings and allow users to select one, a range, or a scattered set of the strings.
- Menu widgets are special widgets which work in conjunction with Menubuttons. Invoking a Menubutton displays its associated menu. There are various kinds of menu items, such as buttons, checkbuttons, radiobuttons, separators and cascades.
- Menubutton widgets display a label (just like Buttons) but when selected display a Menu.
- Message widgets are similar to Labels except they display multiline strings.
- Radiobutton widgets select a single item from a list. They're derived from the Label widget.
- Scale widgets consist of a slider which allows users to specify a value by moving the slider.
- Scrollbar widgets control the view of other widgets, like Canvas, Entry, Listbox, and Text. Users can scroll the widget by dragging the slider.
- Text widgets display lines of editable text. Characters in a text widget can be colored, given specific fonts, spacing, margins and more.
- Toplevel widgets are essentially secondary MainWindows. They resemble Frames in that they act as container widgets, except they aren't internal widgets.
The Perl/Tk application that I am going to develop is called Plot Program, or plop for short, featuring Button, Canvas, Dialog, Frame, Label, LabEntry, Menu, Menubutton, Scrollbar and Text widgets. Plop plots a list of mathematical functions of the form $y = f($x), where $x iterates from the graph's X-minimum to X-maximum. Each function is evaluated in turn for a particular value of $x, a Y value is computed and then a point is painted on the canvas. (Plop emphasizes the canvas widget because I've noticed that new Tk users, after watching around 2000 lines of canvas documentation roll by, tend to place "exploring the canvas widget" at the end of their to-do list! I think you'll find that feature-rich does not mean difficult to use.)
Remembering that a canvas widget can be thought of as an artist's canvas for free-hand drawing of graphics and text, we'll treat it as a classical Cartesian coordinate system. A key difference is that the canvas origin, coordinate position (0,0), is defined to be the top-left corner of the canvas window, and that canvas X coordinates increase when moving right (as you'd expect) and Y coordinates increase when moving down (as you wouldn't). Also, canvas coordinates can't have negative values. For these reasons, we'll use an equation to transform between canvas and Cartesian coordinates.
Here's the very first version of the program:
use strict;
use Tk;
my($o, $s) = (250, 20);
my($pi, $x, $y) = (3.1415926, 0);
my $mw = MainWindow->new;
my $c = $mw->Canvas(-width => 500, -height => 500);
$c->pack;
$c->createLine(50, 250, 450, 250);
$c->createText(10, 250, -fill => 'blue', -text => 'X');
$c->createLine(250, 50, 250, 450);
$c->createText(250, 10, -fill => 'blue', -text => 'Y');
for ($x = -(3*$pi); $x <= +(3*$pi); $x += 0.1) {
$y = sin($x);
$c->createText( $x*$s+$o, $y*$s+$o, -fill => 'red', -text => '.');
$y = cos($x);
$c->createText( $x*$s+$o, $y*$s+$o, -fill => 'green', -text => '.');
}
MainLoop;
The statements:
$c->createText(10, 250, -fill => 'blue', -text => 'X');
The for statement varies from -3P to + 3P radians, and even old biology-types like myself know that sine() and cosine() return a value in the range [-1,1]. Such tiny values aren't especially useful unless you're looking for a graph one pixel high, so a transform is required:
$c->createText($x*$s+$o, $y*$s+$o, -fill => 'red', -text => '.');
So much for the ugly plop prototype; with a lot of work I can turn this code into a first-rate Perl/Tk application. For starters I want to eliminate every single hardcoded value and use variables instead. Then I'll add these features:
- A menu across the top. Like all respectable applications, it'll have File and Help menubuttons.
- Atitle for the graph.
- Adjustable minimum and maximum X and Y values.
- An editable list of functions.
- The option to read in functions from a file. Heck, let's just do it: eval {require "plop.fnc";}. Store your private functions in the file plop.fnc and they'll be available for plotting. For instance, plop.fnc might contains these lines if you wanted to graph the hyperbolic arctangent:
- sub atanh {
return undef if ($_[0] < -1 or $_[0] > 1);
.5 * log((1 + $_[0]) / (1-$_[0]));
}
Below you'll see a sample run of the new plop. The complete program is available at TPJ web site:
The main window is divided into three major regions, a top frame with menubuttons (containing the File and Help menus), the canvas in the middle (including the title and boundary values), and a bottom area containing a series of other widgets (including a scrollable text widget with the list of functions). The Perl code has been modularized and looks something like this:
initialize_dialogs;
initialize_menus;
initialize_canvas;
initialize_functions;
-title => 'About',
-text => "plot_program $VERSION\n\n 95/12/04",
-bitmap => 'info',
-buttons => ['Dismiss']);
To create the plop menus, initialize_menus() reuses some old code that generates menubuttons from a data structure, mainly because I'm lazy and menus always take time to get just right. My next column goes into details on menus, cascades, etcetera, but for now examine this code:
$MBF->pack(-fill => 'x');
make_menubutton($MBF, 'File', 0, 'left', [ ['Quit', \&exit, 0]]);
make_menubutton($MBF, 'Help', 0, 'right', [
['About', [$DIALOG_ABOUT => 'Show'], 0],
['', undef, 0],
['Usage', [$DIALOG_USAGE => 'Show'], 0]]);
- The parent widget.
- The menubutton label.
- The shortcut character index. All our menubuttons have a shortcut character index of zero. For example, the 0th (first) character of "File" is 'f', which means that users can type Alt-f to activate the File menu.
- The side of the menu frame to pack the menubutton.
- A list of lists describing the menu items. Each inner list has three components, a label, a callback that is executed when the menu item is invoked and a shortcut underline character. Null labels are treated as separators - do-nothing menu items that appear as lines.
Callbacks come in various flavors, and we'll see more of them in later columns, but in plop's case there are just two: an explicit reference to a subroutine (also called a code reference), and a reference to an array. An example of the first form is the Quit menu item, which calls exit(). The Help menu items use the second form, where the first array element is an object (widget reference) and the second is the name of the method to invoke. Thus, when the user selects "About" the about dialog widget appears. Note that widgets used in callbacks must exist before they are referred too - that's precisely why the dialog widgets were created first.
Subroutine initialize_canvas() generates the middle area of plop's main window, but is slightly different than the prototype version because it has a title line, embedded widgets with editable X and Y values, and axes moved to the borders of the area to reduce visual clutter.
-width => $MAX_PXL + $MARGIN * 2,
-height => $MAX_PXL,
-relief => 'sunken');
$CANV->pack;
$CANV->CanvasBind('
-text => 'Plot Continuous Functions Of The Form y=f($x)',
-fill => 'blue');
# minimum and maximum X values and draw tick marks to indicate where they
# fall. The axis limits are LabEntry widgets embedded in Canvas windows.
$CANV->createLine(
$MIN_PXL + $MARGIN, $MAX_PXL - $MARGIN,
$MAX_PXL - $MARGIN, $MAX_PXL - $MARGIN);
$CANV->createWindow(
$MIN_PXL + $MARGIN, $MAX_PXL - $label_offset,
-window => $MW->LabEntry(
-textvariable => \$X_MIN,
-label => 'X Minimum'));
$CANV->createLine(
$MIN_PXL + $MARGIN, $MAX_PXL - $MARGIN - $tick_length,
$MIN_PXL + $MARGIN, $MAX_PXL - $MARGIN + $tick_length);
$CANV->createWindow(
$MAX_PXL - $MARGIN, $MAX_PXL - $label_offset,
-window => $MW->LabEntry(
-textvariable => \$X_MAX,
-label => 'X Maximum'));
$CANV->createLine(
$MAX_PXL - $MARGIN, $MAX_PXL - $MARGIN - $tick_length,
$MAX_PXL - $MARGIN, $MAX_PXL - $MARGIN + $tick_length);
Subroutine initialize_functions() creates plop's remaining widgets, which are, in top-to-bottom packing order, a spacer frame, a label providing rudimentary instructions, a text widget with an attached scrollbar, and finally another container frame to hold a button or so.
$MW->Label(
-text => 'Enter your functions here',
-foreground => 'blue')->pack;
# Create a Frame with a scrollable Text widget that displays the function
# list, and a Button to initiate plot activities.
my $functions_frame = $MW->Frame;
$functions_frame->pack;
$TEXT = $functions_frame->Scrolled(qw/Text -height 6 -scrollbars e/ );
$TEXT->pack;
update_functions;
my $buttons_frame = $MW->Frame;
$buttons_frame->pack(-padx => 10, -pady => 5, -expand => 1, -fill => 'x');
my @pack_attributes = qw/-side left -fill x -expand 1/;
$buttons_frame->Button(
-text => 'Plot',
-command => \&plot_functions)->pack(@pack_attributes);
The graphical interface in now complete, and when the user invokes the "Plot" button, the callback plot_functions() is executed. Before actually plotting the function list plop tidies up the text window and ensures that each function is assigned its proper color. Plop provides for up to nine simultaneous functions before the colors cycle. Here's the code:
my $i = 0;
foreach (@FUNCTIONS) {
$TEXT->insert('end', "$_\n", [$i]);
$TEXT->tagConfigure($i,
-foreground => $COLORS[$i % $NUM_COLORS],
-font => '9x15');
$i++;
}
$TEXT->yview('end');
Now that the text widget is in synch with the function list, let's plot some functions:
$canv_x = $MIN_PXL + $MARGIN; # X minimun
$DX = $X_MAX - $X_MIN; # update delta X
$DY = $Y_MAX - $Y_MIN; # update delta Y
ALL_X_VALUES:
for ($x = $X_MIN; $x <= $X_MAX; $x += ($X_MAX - $X_MIN) / $ALEN) {
ALL_FUNCTIONS:
foreach (0 .. $#FUNCTIONS) {
$y = eval $FUNCTIONS[$_];
$canv_y = (($Y_MAX - $y) / $DY) * $ALEN + $MARGIN;
$CANV->createText($canv_x, $canv_y,
-fill => $COLORS[$_ % $NUM_COLORS],
-tags => ['plot'],
-text => '.') if $canv_y > $MIN_PXL + $MARGIN and $canv_y < $MAX_PXL - $MARGIN;
} # forend ALL_FUNCTIONS
$canv_x++; # next X pixel
} # forend ALL_X_VALUES
But there is one stone left unturned: the button binding established during canvas creation. Since we already know how to convert a Cartesian coordinate to a canvas coordinate, I thought it would be fun to do the opposite: click anywhere on the canvas to display a Cartesian coordinate. The following code demonstrates how to handle an X event structure, in this case a button press:
my($canvas) = @_;
my $e = $canvas->XEvent;
my($canv_x, $canv_y) = ($e->x, $e->y);
my($x, $y);
$x = $X_MIN + $DX * (($canv_x - $MARGIN) / $ALEN);
$y = $Y_MAX - $DY * (($canv_y - $MARGIN) / $ALEN);
print "\n Canvas x = $canv_x, Canvas y = $canv_y.\n";
print "Plot x = $x, Plot y = $y.\n";
} # end display_coordinates
That's it for this time, if you have questions about this article, or ideas for upcoming ones, feel free to contact me. Next time we'll look more into objects, build a composite widget and examine menus in more detail.
Steve Lidie is a Systems Programmer at Lehigh University.