http://www.perlmonks.org?node_id=551990

One thing I don't enjoy much is developing and maintaining Tk GUIs. Not that I have anything against Tk, it's very powerful and, for such a large, complex library, relatively bug free. Writing GUIs does tend to be donkey work though - placing a single widget involves writing 50 chars or so, and one is constantly tweaking widget placing etc.

When I discovered ZooZ a while back, I was delighted. Here was an opportunity to escape some of the donkey work. I was a little disappointed by the resulting code though - ZooZ allows you to assign variables to widgets visually, but defines the vars as globals. The problem of maintaining the code (as discussed in OO circularity) remains as well. Some of my older Tk scripts (I've just returned to a project I worked on 3 years ago) are so littered with Tk related globals & Tk widget construction in random places as to be all but impossible to maintain.

So at that point ZooZ looked like it was fine for just creating a basis for your Tk script and/or prototyping, and not much else.

Since then I've been experimenting with some alternative techniques for keeping things tidy, and worked out what I think is a pretty neat way of handling the problems that works well with ZooZ:

Here's the output from ZooZ for a simple demo script:

###########################zoozdemo.pl########################## #!perl ################## # # This file was automatically generated by ZooZ.pl v1.2 # on Sat May 27 12:43:24 2006. # Project: Project 1 # File: # ################## # # Headers # use strict; use warnings; use Tk 804; # # Global variables # my ( # MainWindow $MW, # Hash of all widgets %ZWIDGETS, ); # # User-defined variables (if any) # my $Label1content = undef; my $check1 = undef; my $check2 = undef; ###################### # # Create the MainWindow # ###################### $MW = MainWindow->new; ###################### # # Load any images and fonts # ###################### ZloadImages(); ZloadFonts (); # Widget Entry1 isa Entry $ZWIDGETS{'Entry1'} = $MW->Entry()->grid( -row => 2, -column => 0, ); # Widget Checkbutton1 isa Checkbutton $ZWIDGETS{'Checkbutton1'} = $MW->Checkbutton( -text => 'Checkbutton1', -variable => \$check1, )->grid( -row => 3, -column => 0, ); # Widget Checkbutton2 isa Checkbutton $ZWIDGETS{'Checkbutton2'} = $MW->Checkbutton( -text => 'Checkbutton2', -variable => \$check2, )->grid( -row => 4, -column => 0, ); # Widget Label2 isa Label $ZWIDGETS{'Label2'} = $MW->Label( -text => 'Label2', -textvariable => \$Label1content, )->grid( -row => 5, -column => 0, ); # Widget Label1 isa Label $ZWIDGETS{'Label1'} = $MW->Label( -text => 'Label1', )->grid( -row => 6, -column => 0, ); # Widget Button1 isa Button $ZWIDGETS{'Button1'} = $MW->Button( -text => 'Update', )->grid( -row => 5, -column => 1, ); # Widget Button2 isa Button $ZWIDGETS{'Button2'} = $MW->Button( -text => 'short loop', )->grid( -row => 6, -column => 1, ); ############### # # MainLoop # ############### MainLoop; ####################### # # Subroutines # ####################### sub ZloadImages { } sub ZloadFonts { }

After thinking about this for a while, I thought 'why not write a worker class to do all the work, that presents an interface to which the Tk script can bind'. ZooZ doesn't provide the facility to use object attributes or accessors instead of variables (although I'm hoping to either convince the author to include it, or write the extension myself), but it will forgive you if you put an attribute/accessor in the scalar variable field like this:

# Widget Label1 isa Label $ZWIDGETS{'Label1'} = $MW->Label( -text => 'Label1', -textvariable => \$worker->{labeltext}, )->grid( -row => 5, -column => 0, );

that results in a file header like this:

# # User-defined variables (if any) # my $worker->{labeltext} = ;

which has to be cleaned up later. Note that the code for assigning commands to widgets is a little less forgiving. Give it a method call like $worker->updatelabeltext and it converts it to 'main::_worker__updatelabeltext', but it's easy enough to change this in the code/write a script to replace it with the syntax you want.

Of course, there are times when you want to update the GUI dependant on the outcome of a function. For this reason I pass a reference to %ZWIDGETS into the worker class, like this:

my %ZWIDGETS; my $worker = ZooZDemo->new(\%ZWIDGETS);

and make it an attribute of the worker object.

Once you've built your GUI, export your perl code:

#!perl ################## # # This file was automatically generated by ZooZ.pl v1.2 # on Sat May 27 13:23:54 2006. # Project: Project 1 # File: # ################## # # Headers # use strict; use warnings; use Tk 804; # # Global variables # my ( # MainWindow $MW, # Hash of all widgets %ZWIDGETS, ); # # User-defined variables (if any) # my $worker->{looptext} = undef; my $worker->{check2} = undef; my $check1 = undef; my $worker->{text} = undef; my $check2 = undef; my $Label1content = undef; my $badger->getlabeltext = ; my $worker->getlabeltext = ; my $worker->{check1} = undef; my $worker->looptext = ; ###################### # # Create the MainWindow # ###################### $MW = MainWindow->new; ###################### # # Load any images and fonts # ###################### ZloadImages(); ZloadFonts (); # Widget Entry1 isa Entry $ZWIDGETS{'Entry1'} = $MW->Entry( -textvariable => \$worker->{text}, )->grid( -row => 2, -column => 0, ); # Widget Checkbutton1 isa Checkbutton $ZWIDGETS{'Checkbutton1'} = $MW->Checkbutton( -text => 'Checkbutton1', -variable => \$worker->{check1}, )->grid( -row => 3, -column => 0, ); # Widget Checkbutton2 isa Checkbutton $ZWIDGETS{'Checkbutton2'} = $MW->Checkbutton( -text => 'Checkbutton2', -variable => \$worker->{check2}, )->grid( -row => 4, -column => 0, ); # Widget Label2 isa Label $ZWIDGETS{'Label2'} = $MW->Label( -text => 'Label2', -textvariable => \$worker->{Label2}, )->grid( -row => 5, -column => 0, ); # Widget Label1 isa Label $ZWIDGETS{'Label1'} = $MW->Label( -text => 'Label1', -textvariable => \$worker->{looptext}, )->grid( -row => 6, -column => 0, ); # Widget Button1 isa Button $ZWIDGETS{'Button1'} = $MW->Button( -command => 'main::_worker__updatelabeltext', -text => 'Update', )->grid( -row => 5, -column => 1, ); # Widget Button2 isa Button $ZWIDGETS{'Button2'} = $MW->Button( -command => 'main::_worker__doshortloop', -text => 'short loop', )->grid( -row => 6, -column => 1, ); ############### # # MainLoop # ############### MainLoop; ####################### # # Subroutines # ####################### sub ZloadImages { } sub ZloadFonts { } sub _worker__doshortloop { } sub _worker__updatelabeltext { }

So all that remains is to modify it to remove the sub stubs at the end, replace the function calls with sub{$worker->methodname} and replace the global definition section with your worker instantiation:

#!perl ################## # # This file was automatically generated by ZooZ.pl v1.2 # on Sat May 27 13:23:54 2006. # Project: Project 1 # File: # ################## # # Headers # use strict; use warnings; use Tk 804; # # Global variables # my ( # MainWindow $MW, # Hash of all widgets %ZWIDGETS, ); use ZooZDemo; my $worker = ZooZDemo->new(\%ZWIDGETS); ###################### # # Create the MainWindow # ###################### $MW = MainWindow->new; ###################### # # Load any images and fonts # ###################### ZloadImages(); ZloadFonts (); # Widget Entry1 isa Entry $ZWIDGETS{'Entry1'} = $MW->Entry( -textvariable => \$worker->{text}, )->grid( -row => 2, -column => 0, ); # Widget Checkbutton1 isa Checkbutton $ZWIDGETS{'Checkbutton1'} = $MW->Checkbutton( -text => 'Checkbutton1', -variable => \$worker->{check1}, )->grid( -row => 3, -column => 0, ); # Widget Checkbutton2 isa Checkbutton $ZWIDGETS{'Checkbutton2'} = $MW->Checkbutton( -text => 'Checkbutton2', -variable => \$worker->{check2}, )->grid( -row => 4, -column => 0, ); # Widget Label2 isa Label $ZWIDGETS{'Label2'} = $MW->Label( -text => 'Label2', -textvariable => \$worker->{Label2}, )->grid( -row => 5, -column => 0, ); # Widget Label1 isa Label $ZWIDGETS{'Label1'} = $MW->Label( -text => 'Label1', -textvariable => \$worker->{looptext}, )->grid( -row => 6, -column => 0, ); # Widget Button1 isa Button $ZWIDGETS{'Button1'} = $MW->Button( -command => sub{$worker->updatelabeltext}, -text => 'Update', )->grid( -row => 5, -column => 1, ); # Widget Button2 isa Button $ZWIDGETS{'Button2'} = $MW->Button( -command => sub{$worker->doshortloop}, -text => 'short loop', )->grid( -row => 6, -column => 1, ); ############### # # MainLoop # ############### MainLoop; ####################### # # Subroutines # ####################### sub ZloadImages { } sub ZloadFonts { }

and of course the example code is no use without the demo worker class:

use strict; use warnings; package ZooZDemo; sub new { my $class = shift; my $tk = shift; my %self = ( tk => $tk, text => '', check1 => 0, check2 => 0, Label1 => '', looptext=> '' ); return bless \%self,$class; } sub doshortloop { my $self = shift; for (1..10) { print "$_"; $self->{looptext} = $_; if ($self->{tk}) { $self->{tk}->{Label1}->update(); } sleep 1; } } sub updatelabeltext { my $self = shift; $self->{Label2} = $self->{text}." - ".$self->{check1}." - ".$self- +>{check2}; if ($self->{tk}) { $self->{tk}->{Label2}->update(); } } sub getlabeltext { my $self = shift; return $self->{Label2}; } 1;
So you've now got your Tk interface defined in a script, and all the functioning parts of the code in a separate module. The time comes to make some changes. Although almost all of the Tk code is contained within a mostly autogenerated script, you've still made enough changes to it that you can't just export a new version from ZooZ and expect it to work. The solution to this is twofold: first of all as I said earlier I'm hoping to convince the author to make (or accept from me) some changes to ZooZ to make it more worker class friendly. For the time being though, the following steps will speed things up considerably:

  • Load your GUI definition from the .zooz file (see below)
  • Export it as perl code
  • Make your changes
  • Export the code again, as a new name (increment the version number perhaps)
  • diff the two scripts you've just produced to get a list of changes
  • armed with the diff file, you can quickly and easily find and make the necessary changes to your working code

ZooZ stores projects as .zooz files, and if you want to edit your GUI definition at a later date, it's that file you need to have access to. You can store it in your code repository alongside your working code, or if you aren't controlling code like that (although you should be!) you can cat>> it onto the end of your script, after an __END__ marker.

This approach is not limited to ZooZ, there's no reason why you shouldn't arrange manually created Tk scripts this way for maintainability (and for all I know, more experienced Tk programmers already do something similar - not being a heavy user of Tk I've never read any of the books on the subject). I've yet to explore some of the other facilities of ZooZ (like exporting code as a .pm file), so no doubt there are improvements to this approach that can be made. It's also worth noting that I don't write large applications in Tk - mostly just small user utilities. So far though, this approach has saved me quite a bit of effort.

--------------------------------------------------------------

"If there is such a phenomenon as absolute evil, it consists in treating another human being as a thing."
John Brunner, "The Shockwave Rider".

Can you spare 2 minutes to help with my research? If so, please click here