Clachair has asked for the wisdom of the Perl Monks concerning the following question:

For aesthetic reasons, I'd like to use a window menu bar and configure it on startup using the following code. Ideally I would like to write only one subroutine to handle all the menu requests as the directory contents and therefore the menu list can vary (I hope I'm making this clear).
1. #Read a list of files from the cur/wkg/dir and insert them in the 'Load' menu bar ; 2. opendir(DIR, $dir); 3. @dirList = readdir(DIR); 4. closedir(DIR); 5. # Sort the list; 6. @dirList = sort @dirList; 7. foreach $fn (@dirList) { 8. $Load_mb -> command(-label => $fn, 9. -command => sub {&Load ($fn)} ); 10. } ; # End foreach; 11. 12. sub Load { 13. $Title = "Loading $_[0] "; 14. $Main_Window -> title($Title); 15. #Do something clever with whichever menu item was requested; 16. }
This works OK, except for Line 9 which passes to the subroutine whatever was in $fn the last time it was used, regardless of which menu item was selected. I've tried all sorts of alternatives but nothing works as intended, and unless some clever monk has a really ingenious suggestion I think I'm going to be stuck with several dozen short subroutines and a hash table. I've searched, but cannot find an equivalent to the cget instruction used in listboxes.

Replies are listed 'Best First'.
Re: Perl/Tk Menus
by danger (Priest) on Jun 09, 2001 at 04:57 UTC

    Your problem is that $fn isn't a lexical variable --- you'll want to use my() so the anonymous subroutine acts as a closure around each lexical $fn. See the difference between these two snippets:

    my @subs; foreach $fn (1,2) { push @subs, sub{print "$fn\n"}; } $fn = 42; foreach $sub (@subs) { $sub->(); } @subs = (); foreach my $fn (1,2) { push @subs, sub{print "$fn\n"}; } $fn = 42; foreach $sub (@subs) { $sub->(); }
      Well, that was a simple fix for a very vexing problem. Many thanks for pointing me in the right direction.
Re: Perl/Tk Menus
by ariels (Curate) on Jun 09, 2001 at 16:01 UTC
    The answer given is essentially correct. However, it is important to note that the problem is unrelated to $fn being or not being is lexical variable. The problem is simply that the sub being created (sub {&Load ($fn)}; why the ampersand?) says "pass Load the value of $fn". But that's the value when the sub is executed, not when it was defined!

    The fix is to change that by making each iteration of the loop refer to a different lexical variable! But it still creates many subroutines and closures.

    Perl/Tk provides another syntax for callbacks that is better in this case; see Tk::callbacks for the details.

    Meanwhile, here's some sample code:

    #!/usr/local/bin/perl -w use strict; use Tk; my $mw = new Tk::MainWindow; my $mb = $mw->Menubutton(-text => 'Test')->pack(-side => 'left'); sub Select { my $val = shift; my $top = $mw->Toplevel(-title => "Selection $val"); $top->Button(-text => "Close Select($val)", -command => ['destroy', $top])->pack; } for(1..10) { $mb->command(-label => $_, -command => [\&Select, $_]); } MainLoop;
    Build your callbacks by giving a list reference of the sub and the arguments you'd like passed. So it's the same sub each time, and only a list of the arguments is stored.