perltkx

Perl Tkx: How to pass a variable as a parameter to a button's callback


Given this Perl/Tkx code fragment:

@itemList = ({'attrib1' => 'name1', 'attrib2' => 'value1'},
             {'attrib1' => 'name2', 'attrib2' => 'value2'});
$row = 0;
foreach $item (@itemList) {
  push(@btn_list, new_ttk__button(-text => $item->{'attrib1'}, -command => sub {do_something($item->{'attrib2'});}));
  $btn_list[-1]->g_grid(-column => 0, -row => $row);
  $row++;
}

(In the real program @itemList is populated from a user editable config file.)

I do see two buttons labeled 'name1' and 'name2'. But when I click on either button it seems that the parameter that is passed to the callback is always $itemList[1]->{'attrib2'}; i.e. 'attrib2' of the last element of the @itemList array. What I would like is to have the first button call do_something($itemList[0]->{'attrib2'} and the second call do_something($itemList[1]->{'attrib2'}.

What am I doing wrong, please and thank you?


Solution

  • You have encountered a subtle feature of for loops in Perl. First the solution: use my in the for loop. Then $item will be able to create a proper closure in the anonymous sub you declare later in the loop.

    for my $item (@itemlist) {
        push(@btn_list, new_ttk__button(
                           -text => $item->{'attrib1'}, 
                           -command => sub {do_something($item->{'attrib2'});}));
        $btn_list[-1]->g_grid(-column => 0, -row => $row);
        $row++;
    }
    

    Further explanation: Perl implicitly localizes the subject variable of a for loop. If you don't use my in the for loop, the loop will be using a localized version of a package variable. That makes your code equivalent to:

    package main;
    $main::item = undef;
    @itemList = ({'attrib1' => 'name1', 'attrib2' => 'value1'},
                 {'attrib1' => 'name2', 'attrib2' => 'value2'});
    $row = 0;
    foreach (@itemList) {
        local $main::item = $_;
        push(@btn_list, new_ttk__button(
            -text => $main::item->{'attrib1'},
            -command => sub {do_something($main::item->{'attrib2'});}));
        $btn_list[-1]->g_grid(-column => 0, -row => $row);
        $row++;
    }
    # at the end of the loop, value of $main::item restored to  undef
    

    Your anonymous subs still refer to the $main::item package variable, whatever value that variable holds at the time that those subroutines are invoked, which is probably undef.

    Shorter solution: use strict

    Additional proof-of-concept. Try to guess what the following program outputs:

    @foo = ( { foo => 'abc', bar => 123 },
             { foo => 'def', bar => 456 } );
    
    my @fn;
    foreach $foo (@foo) {
        push @fn, sub { "42" . $foo->{bar} . "\n" };
    }
    foreach my $foo (@foo) {
        push @fn, sub { "19" . $foo->{foo} . "\n" };
    }
    print $_->() for @fn;
    

    Here's the answer:

    42
    42
    19abc
    19def