Contents


Using advanced widgets in Perl/Tk

Create rich user interfaces in Perl

Comments

In contrast to the widespread use of the Perl language, Perl's GUI toolset, Perl/Tk, is much less popular. This is strange, because it is arguably one of the easiest GUI toolkits to program (at least for UNIX platforms), and therefore suggests itself for user interface prototyping or for quickly providing a user-friendly wrapper around cryptic command-line tools.

One reason for this relative indifference toward Perl/Tk appears to be the perception that it is not very powerful and does not lend itself to sophisticated applications. However, a number of widgets that provide more complex functionality are available as user contributions on CPAN. In this article, I will take a look at some of them and show how they can be used to create a richer user experience. I will also point out some more generally useful techniques for programming with Perl/Tk.

(The other problem with Perl/Tk, of course is, that its widgets look ugly and are not "theme-able." Unfortunately, I cannot do anything about that.)

Prerequisites and availability

A good knowledge of Perl and at least rudimentary experience with Perl/Tk as well as general GUI programming concepts -- events, widgets, callbacks, geometry managers -- are assumed throughout. You can find introductory resources on Perl/Tk in the Related topics section of this article.

You can freely download all the widgets discussed in this article from CPAN, the Comprehensive Perl Archive Network. Most have many more options than can be explained here: the perldoc for each widget and -- in some cases -- the source code, provide the ultimate and complete reference.

Tabbed frames: Tk::NoteBook

A common GUI widget, the Tab helps to group a large number of options into smaller subsets and so add structure to, for instance, complicated dialog boxes.

A widget with Tab-like appearance and semantics for Perl/Tk is Tk::NoteBook. An example, using three tabs, is shown in Figure 1. Note that the third tab is disabled.

Figure 1. The NoteBook widget
Figure 1. The NoteBook widget
Figure 1. The NoteBook widget

This example was produced by the following code:

Listing 1: Using the NoteBook widget
use Tk;
use Tk::NoteBook;

$mw = MainWindow->new();
$mw->geometry( "400x100" );
$book = $mw->NoteBook()->pack( -fill=>'both', -expand=>1 );

$tab1 = $book->add( "Sheet 1", -label=>"Start", -createcmd=>\&getStartTime );
$tab2 = $book->add( "Sheet 2", -label=>"Continue", -raisecmd=>\&getCurrentTime );
$tab3 = $book->add( "Sheet 3", -label=>"End", -state=>'disabled' );

$tab1->Label( -textvariable=>\$starttime )->pack( expand=>1 );
$tab2->Label( -textvariable=>\$raisetime )->pack( expand=>1 );
$tab3->Button( -text=>'Quit', -command=>sub{ exit; } )->pack( expand=>1 );

MainLoop;

sub getStartTime {
  $starttime = "Started at " . localtime;
}

sub getCurrentTime {
  $raisetime = " Last raised at " . localtime;
  $book->pageconfigure( "Sheet 3", -state=>'normal' );
}

As in any Perl/Tk application, we first specify the modules we are going to use and then create a MainWindow. Note that Tk::NoteBook, as well as the other contributed widgets on CPAN, are not part of the standard Tk distribution, and therefore need to be specified explicitly.

We create a NoteBook widget as child to the MainWindow and then add three tabs. The first argument to the add() function is a symbolic name, by which the generated page can be referred to in the notebook: we will make use of it below.

I should make two comments on geometry management when using the NoteBook widget. First, although the tabs are widgets themselves, they do not need to be packed. Their geometry management is handled by the enclosing NoteBook. Second, if the parent window of the NoteBook widget is resizable, it is important to specify both the -fill and the -expand attributes when packing the NoteBook. The latter will make sure that the NoteBook's allocation rectangle will always expand to fill the available space, while the former ensures that the actual NoteBook widget will expand to fill its allocation rectangle.

The tabs accept a variety of attributes. Here we demonstrate -createcmd, -raisecmd, and -state. The first two can be used to register callbacks, which will be invoked when the tab is first created and whenever it is raised, respectively; while the last one can take on the values normal and disabled. We also use the -raisecmd callback on the second tab to switch the third tab from its disabled to its active state once the second tab has been raised for the first time. We do this using the pageconfigure() function on the enclosing NoteBook widget, passing the symbolic name of the referenced tab as the first argument.

Versatile graphical display: Tk::ProgressBar

The Tk::ProgressBar is a widget that displays a graphical representation of a scalar value. Progress bars are commonly used to provide feedback to the user when downloading large files or performing similar long-running tasks. The corresponding widget in Perl/Tk offers some special features that can make it attractive for other uses as well.

Figure 2. The ProgressBar widget
Figure 2. The ProgressBar widget

This example contains two ProgressBar widgets, coupled to a standard Scale. Moving the slider changes the length of the displayed color bar by invoking the callback named fct, which calls the value function on the ProgressBars to set the new length. A value of 100 corresponds to the full length of the visible color bar. This value can be changed, as can be the value corresponding to a zero-length color bar, using the -to and -from attributes, respectively.

Listing 2 demonstrates some of the ways the appearance of the color bar can be customized. The bottom ProgressBar is broken into ten blocks, which are separated by from one another by one-pixel gaps. These are the default values; they can be set by explicitly specifying values for the blocks and gap attributes. Some experimentation with the -padx, -pady, -length, and -borderwidth attributes is generally required.

The -colors attribute accepts a reference to an array containing pairs of positions and colors. (Note that the positions must be sorted in ascending order!) The last specified color will be used for the color bar, until the next position is reached, at which point the color changes. So, defining @colors = ( 0, "red", 50, "green" ); yields a color bar that is red on the left side and green on the other. Here, I provide 100 distinct values, spanning the colors of the rainbow for the top ProgressBar. Note that the top ProgressBar changes its length when the window is resized.

Listing 2: Using the ProgressBar widget
use Tk;
use Tk::ProgressBar;

@colors = (  0, '#ff002a',  1, '#ff0014',  2, '#ff000a',  3, '#ff0500',  4, '#ff1000',
	     5, '#ff1b00',  6, '#ff3000',  7, '#ff3b00',  8, '#ff4600',  9, '#ff5100',
	    10, '#ff6100', 11, '#ff7600', 12, '#ff8100', 13, '#ff8c00', 14, '#ff9700',
	    15, '#ffa100', 16, '#ffbc00', 17, '#ffc700', 18, '#ffd200', 19, '#ffdd00',
	    20, '#ffe700', 21, '#fffd00', 22, '#f0ff00', 23, '#e5ff00', 24, '#dbff00',
	    25, '#d0ff00', 26, '#baff00', 27, '#afff00', 28, '#9fff00', 29, '#95ff00',
	    30, '#8aff00', 31, '#74ff00', 32, '#6aff00', 33, '#5fff00', 34, '#54ff00',
	    35, '#44ff00', 36, '#2eff00', 37, '#24ff00', 38, '#19ff00', 39, '#0eff00',
	    40, '#03ff00', 41, '#00ff17', 42, '#00ff21', 43, '#00ff2c', 44, '#00ff37',
	    45, '#00ff42', 46, '#00ff57', 47, '#00ff67', 48, '#00ff72', 49, '#00ff7d',
	    50, '#00ff87', 51, '#00ff9d', 52, '#00ffa8', 53, '#00ffb8', 54, '#00ffc3',
	    55, '#00ffcd', 56, '#00ffe3', 57, '#00ffee', 58, '#00fff8', 59, '#00faff',
	    60, '#00eaff', 61, '#00d4ff', 62, '#00c9ff', 63, '#00bfff', 64, '#00b4ff',
	    65, '#00a9ff', 66, '#008eff', 67, '#0083ff', 68, '#0079ff', 69, '#006eff',
	    70, '#0063ff', 71, '#004eff', 72, '#003eff', 73, '#0033ff', 74, '#0028ff',
	    75, '#001dff', 76, '#0008ff', 77, '#0200ff', 78, '#1200ff', 79, '#1d00ff',
	    80, '#2800ff', 81, '#3d00ff', 82, '#4800ff', 83, '#5300ff', 84, '#5d00ff',
	    85, '#6e00ff', 86, '#8300ff', 87, '#8e00ff', 88, '#9900ff', 89, '#a300ff',
	    90, '#ae00ff', 91, '#c900ff', 92, '#d400ff', 93, '#df00ff', 94, '#e900ff',
	    95, '#f400ff', 96, '#ff00f3', 97, '#ff00e3', 98, '#ff00d9', 99, '#ff00ce' );

$mw = MainWindow->new();
$mw->geometry( '250x150' );
$mw->resizable( 1, 0 );

$bar1 = $mw->ProgressBar( -borderwidth=>2, -blocks=>100, -gap=>0,
			  -troughcolor=>'white',-colors=>\@colors,
			  -length=>106 )->pack( -padx=>5, -pady=>5, -fill=>'x' );

$slide = $mw->Scale( -orient=>'horizontal', -length=>150,
		     -showvalue=>0, -tickinterval=>20 )->pack;

$bar2 = $mw->ProgressBar( -padx=>2, -pady=>2, -borderwidth=>2,
			  -troughcolor=>'#BFEFFF', -colors=>[ 0, '#104E8B' ],
			  -length=>106 )->pack;

$mw->Button( -text=>'Quit', -command=>sub{ exit } )->pack( -padx=>15, -pady=>15,
							   -anchor=>'se' );

$slide->configure( -command=>[ \&fct, $slide, $bar1, $bar2 ] );

MainLoop;

sub fct {
  my ( $slide, $bar1, $bar2 ) = @_;
  my $val = $slide->get();
  $bar1->value( 100 - $val );
  $bar2->value( $val );
}

We specify the callback for the Scale widget not in its constructor, but later, using the configure function. The reason is that we need references to both ProgressBars as arguments to the callback, and at the time the Scale constructor is invoked, they are not all defined yet. Changing the order in which the widget constructors are called is not possible, since this would change the order in which the widgets are packed.

This code also demonstrates one way to specify arguments to a callback: as an anonymous list, with a reference to the callback invoked as the first element, followed by the required parameters. If a callback does not require arguments, we do not need to create the anonymous list, since Perl treats scalar values, evaluated in list context, as single-element arrays. (For more information on lists in Perl, see Learning Perl, 2nd edition, listed in Related topics.) We will encounter a different method for sending parameters to a callback below; check the Sidebar for a discussion of the differences between the two methods.

The freedom to choose arbitrary colors for any segment of the color bar makes the ProgressBar interesting for uses as a general display widget: for instance, the colors can be used to indicate whether the displayed variable is within the "normal" parameter range or not. Unfortunately, the widget does not support an -orient attribute; ProgressBars are always oriented horizontally.

Simplified data entry: Tk::DateEntry and Tk::PathEntry

The Tk::DateEntry and the Tk::PathEntry widgets simplify the input of structured data (namely dates and file paths, respectively), by providing a display of valid input and allowing the user to select from them.

The DateEntry widget displays a text input field with an adjacent button. Clicking the button displays a calendar in a drop down menu, and selecting a date using the mouse enters the corresponding string into the text input field.

Figure 3. The DateEntry widget
Figure 3. The DateEntry widget

Listing 3 shows the code associated with this example. Selecting Convert calculates the number of seconds since the beginning of the Unix epoch and displays them in the Label widget atop the text input field.

Listing 3: Using the DateEntry widget
use Tk;
use Tk::DateEntry;

use Time::Local;

%idx_for_mon = ( JAN=>1, FEB=>2, MAR=>3, APR=> 4, MAY=> 5, JUN=> 6,
		 JUL=>7, AUG=>8, SEP=>9, OCT=>10, NOV=>11, DEC=>12 );

$input = '01-APR-2004'; # Initial value for display

$mw = MainWindow->new();
$mw->geometry( '200x80' );
$mw->resizable( 0, 0 );

$label = $mw->Label( -text=>'' )->pack;
$entry = $mw->DateEntry( -textvariable=>\$input, -width=>11,
			 -parsecmd=>\&parse, -formatcmd=>\&format )->pack;

$mw->Button( -text=>'Quit', -command=>sub{ exit } )->pack( -side=>'right' );
$mw->Button( -text=>'Convert',
	     -command=>sub{ convert( $input, $label ) } )->pack( -side=>'left' );

MainLoop;

# called on dropdown with content of \$textvariable, must return ( $yr, $mon, $day )
sub parse {
  my ( $day, $mon, $yr ) = split '-', $_[0];
  return ( $yr, $idx_for_mon{$mon}, $day );
}

# called on user selection with ($yr, $mon, $day), must return formatted string
sub format {
  my ( $yr, $mon, $day ) = @_;
  my %mon_for_idx = reverse %idx_for_mon;
  return sprintf( "%02d-%s-%2d", $day, $mon_for_idx{ $mon }, $yr );
}

# perform the conversion to epoch seconds when the corresponding button is pressed
sub convert {
  my ( $input, $label ) = @_;
  my ( $yr, $mon, $day ) = parse( $input );
  my $output = "Epoch seconds: " . timelocal( 0, 0, 0, $day, $mon-1, $yr-1900 );
  $label->configure( -text => $output );
}

Date input is hard because there are so many ways to represent the same date in string form. The DateEntry provides three standard date formats (MM/DD/YYYY, YYYY/MM/DD, and DD/MM/YYYY), which can be selected using the -dateformat option. If a different date format is desired, the programmer has to provide the conversion routines explicitly, using the callbacks -parsecmd and -formatcmd. In the example above, we use a custom date format, displaying months using 3-letter acronyms. When parsing the input string into its numerical constituents, we use the hash %idx_for_mon, which holds the numerical index of each month (1..12) given its acronym. When a user selects a date from the drop down menu, it has to be formatted into the corresponding string, requiring the opposite lookup, namely the acronym given the index. We build up such a data structure on the fly in the format routine, using the reverse command. Since this command expects an array, the original hash is unwound into an array in such a way that the values follow their respective keys in the array. This array is then reversed (so that now the former keys follow their values) and transformed back into a hash. This trick works here, because both keys and values are unique (again, for more information on this, see Learning Perl, which is listed in Related topics).

The convert function takes the variable containing the input string, as well as a reference to the Label widget as parameters, so that it can change the value shown by the Label. Here, we do not pass a reference to the callback and the values of the parameters in an anonymous array to the -command attribute; instead, we invoke the callback function directly, from within an anonymous subroutine (a closure). The reason has to do with variable scoping; see the Sidebar for a full explanation.

Finally, the Tk::PathEntry widget is very simple: it provides a text input field for path names -- but with a twist! Similar to the behavior of tcsh or the Emacs mini-buffer, pressing the Tab key will complete the contents of the input field as far as possible, and will pop up a list box of possible choices if the current contents cannot be completed unambiguously. Bizarrely, the color of the list box cannot be changed -- unless one wants to edit the code of the underlying Perl module.

Figure 4. The PathEntry widget
Figure 4. The PathEntry widget
Figure 4. The PathEntry widget
Listing 4: Using the PathEntry widget
use Tk;
use Tk::PathEntry;

use Cwd;

$path = cwd();

$mw = MainWindow->new();
$mw->geometry( '300x80' );
$mw->resizable( 0, 0 );

$mw->PathEntry( -textvariable=>\$path )->pack;
$mw->Label( -textvariable=>\$path, -foreground=>'blue' )->pack;
$mw->Button( -text=>'Quit', -command=>sub{ exit } )->pack;

MainLoop;

What makes PathEntry so interesting is that it is merely a widget, and not a dialog box. It can (in fact, it has to) be combined with other widgets in a program. Therefore, it provides a very lightweight way to add file selection capabilities to an application.

Conclusion

These are just a few of the more "advanced" widgets for the Perl/Tk GUI toolkit that allow developers to create richer, more powerful user interfaces using Perl. All the widgets discussed here are freely available as user contributions from CPAN.


Downloadable resources


Related topics


Comments

Sign in or register to add and subscribe to comments.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Linux, Open source
ArticleID=15046
ArticleTitle=Using advanced widgets in Perl/Tk
publish-date=08102004