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

Dear monks, I have started writing a music program in which I have to create a number of repetitive elements. In the attached example, I want to produce a range of colours for each musical degree. The button labeled "CLR" is used to call the clrChooser subroutin, and the button labeled "CHK" is used to check the current value. It seems like the iteration is "overflowing". The for loop still has the correct values, but the subroutine has the maximum number + 1. Try reducing the iteration by one (or more) and see the result. Sorry for asking such a lame question here, this must be very beginner level, but I can't ask for advice elsewhere. Thank you, Zsolt

#!/usr/bin/perl -w use utf8; use Tk; require Tk::LabFrame; our ($degreeClr1, $degreeClr2, $degreeClr3, $degreeClr4, $degreeClr5) += ("magenta", "red", "blue", "green", "darkred"); my $mw = MainWindow -> new(); $mw -> title($0); $mw -> geometry("1100x150+0+0"); $mw -> minsize(1100, 150); # MAIN FRAME $frame01 = $mw -> Frame -> pack(-fill => 'both', -expand => 1, -padx = +> 4, -pady => 4); $frameParam1 = $frame01 -> LabFrame(-label => "Settings", -labelside = +> "acrosstop") -> pack(-fill => 'x', -expand => 1, -padx => 10, -anch +or => 'n'); $frameParam1_0 = $frameParam1 -> Frame -> pack(-fill => 'x', -expand = +> 1); $frameParam1_0 -> Label(-text => "") -> pack(-fill => 'x', -expand => +1, -side => 'left'); # FRAME COLORS $frameParam1_3 = $frameParam1 -> Frame -> pack(-fill => 'x', -expand = +> 1); $frameParam1_3 -> Label(-text => "Colors:") -> pack(-side => 'left'); for (my $i = 1; $i <= 5; $i++) { ${"frameParam1_3_$i"} = $frameParam1_3 -> Frame -> pack(-padx => 5, +-side => 'left'); ${"frameParam1_3_$i"} -> Label(-text => "$i. degree") -> pack(-side +=> 'left'); ${"clrButton$i"} = ${"frameParam1_3_$i"} -> Button(-text => "CLR", - +cursor => 'hand2', -command => sub{&clrChooser("Choose color for $i d +egree!", ${"degreeClr$i"}, ${"clrCanvas$i"}, ${"clrShow$i"}, ${"clrBu +tton$i"}, \${"degreeClr$i"})}) -> pack(-side => 'left'); ${"clrCanvas$i"} = ${"frameParam1_3_$i"} -> Canvas(-background => "b +lack", -width => 20, -height => 20, -state => 'disabled') -> pack(-si +de => 'left'); ${"clrShow$i"} = ${"clrCanvas$i"} -> createRectangle(20, 20, 4, 4, - +fill => ${"degreeClr$i"}, -outline => 'white', -width => '2'); ${"frameParam1_3_$i"} -> Button(-text => 'CHK', -command => sub{prin +t "degreeClr$i: " . ${"degreeClr$i"} . "\n"})-> pack(-side => 'left') +; print "degreeClr$i: " . ${"degreeClr$i"} . "\n"; } MainLoop; sub clrChooser { my ($title, $initClr, $clrCanvas, $clrCanvasItem, $clrButton, $initC +lrRef) = @_; print "title: $title\ninitClr: $initClr\nclrCanvas: $clrCanvas\ninit +ClrRef: $initClrRef\n"; my $color = $mw -> chooseColor(-title => $title, -initialcolor => $i +nitClr); if ($color) { $$clrCanvas -> itemconfigure(${$clrCanvasItem}, -fill => $color); ${$initClrRef} = $color; $clrButton -> focus; } }
  • Comment on Strange behavior of iteration while creating Perl/Tk widgets dynamically
  • Download Code

Replies are listed 'Best First'.
Re: Strange behavior of iteration while creating Perl/Tk widgets dynamically
by kcott (Archbishop) on Oct 29, 2021 at 10:16 UTC

    G'day Zsolt,

    You have written your for loop like this:

    for (INIT; COND; INCR) { ... }

    The following is very important. I'm not attempting to be patronising; you need to fully understand this. Pay close attention to the numbers.

    INIT
    This is an initialisation which only occurs once. In your case, it sets $i to 1.
    COND
    This is a condition which determines whether the loop is entered. When $i is 1, 2, 3, 4 or 5 the condition $i <= 5 is TRUE and the loop is entered. When $i is 6, the condition is FALSE, and the loop ends.
    INCR
    This increments $i at the end of the loop before another iteration is attempted. $i is set to 2 at the end of the 1st iteration. It continues to be incremented. $i is set to 6 at the end of the 5th iteration. The last value that $i has is 6!

    In each iteration, you set up two callbacks with the -command option. In both cases, the value of the callback is a coderef (an anonymous subroutine: sub { ... }); You redefine those coderefs on each iteration. The code in those coderefs contains the variable $i. On the final redefinition, the last value that $i has is 6!

    This is why, regardless of which CLR button you use, you always see "title: Choose color for 6 degree!"; and similarly, regardless of which CHK button you use, you always see "degreeClr6".

    Using (non-reference) variables in a callback is almost always a bad idea in Tk applications: use references instead. Here's a somewhat contrived Tk application using two scalarrefs and an arrayref in a callback. It's possibly a little over the top; however, it's intended to make a point.

    #!/usr/bin/env perl use strict; use warnings; use Tk; my $mw = MainWindow::->new(); my @colours = qw{magenta red blue green darkred}; my $swatch = $mw->Label(-bg => 'black')->pack(-fill => 'x'); for my $i (0 .. 4) { $mw->Button( -bg => $colours[$i], -command => sub { colour_swatch(\$swatch, \@colours, \$i); }, )->pack(-fill => 'x'); } sub colour_swatch { my ($swatch_ref, $colours_ref, $i_ref) = @_; my $bg = ${$colours_ref}[$$i_ref]; $$swatch_ref->configure(-bg => $bg); return; } MainLoop;

    The swatch at the top starts off black. As you click on any of the five coloured buttons below, it takes on the colour of that button.

    You have no end of other problems with the code you posted: package variables springing into existence all over the place; symbolic references which you should really avoid (arrays and hashes are often a better choice — see @colours in my code above); lines so long that they're almost unreadable (one statement has 262 characters); and other code which I suspect you just threw at it in the hope that it would somehow fix something (for instance, what did think the utf pragma would achieve?).

    I strongly recommend that you read perlintro to get a basic grounding in Perl coding. Following that, you may want to look at: "Symbolic references", "Anonymous Subroutines", and Tk::callbacks.

    — Ken

Re: Strange behavior of iteration while creating Perl/Tk widgets dynamically
by bliako (Abbot) on Oct 29, 2021 at 08:50 UTC

    Your problem can be demonstrated by following simple code:

    my @subs; for(my $i=1;$i<=5;$i++){ push @subs, sub { print "i=$i\n"; }; } $subs[3]->();

    Inside the sub, $i is evaluated at the time the sub is called in the context/scope of the for-loop which has now terminated, so $i=5+1=6, which is the expected behaviour.

    A workaround would be to create a sub via eval which will force it to consider $i's value at the time of the eval. But I am not sure if this is an elegant solution:

    my @subs; for(my $i=1;$i<=5;$i++){ push @subs, eval <<EOE; sub { print "i=$i\n"; }; EOE } $subs[3]->();

    btw, once you sort this out, you should start using arrays instead of ${"xx$i"}!

    Update: another solution would be to introduce another variable local to the loop scope and use that:

    my @subs; for(my $i=1;$i<=5;$i++){ my $j = $i; push @subs, sub { print "i=$i, j=$j\n"; }; } $subs[3]->();

    Question: what's the difference between $i and $j, don't they both have the same scope?

    bw, bliako

      The perl foreach (or its alias for) loop does not have this problem.
      my @subs; #for(my $i=1;$i<=5;$i++){ for my $i (1..5) { push @subs, sub { print "i=$i\n"; }; } $subs[3]->();

      OUTPUT:

      i=4
      Bill

        And this code confirms your finding:

        my @subs; for(my $i=1;$i<=2;$i++){ print \$i."\n" } print "----\n"; for my $i (1..2) { print \$i."\n" }

        OUTPUT:

        SCALAR(0x55a429b66a28) SCALAR(0x55a429b66a28) ---- SCALAR(0x56399466d4d8) SCALAR(0x5639946924a0)

        Which made me try this:

        my @subs; my $i; for($i=1;$i<=5;$i++){ push @subs, sub { print "i=$i\n"; }; } $i = 100; $subs[3]->();

        It prints i=100

        bw, bliako

Re: Strange behavior of iteration while creating Perl/Tk widgets dynamically
by BillKSmith (Monsignor) on Nov 02, 2021 at 15:23 UTC
    My previous reply provides a solution to your 'overflowing' problem. The difference is in the scope of $i. I admit that the documentation does not make this clear although it does address it.
    #for (my $i = 1; $i <= 5; $i++) { for my $i (1..5) {

    However, your script still does not execute correctly. (Error when pressing any of the CLR buttons.) Symbolic references are almost always a poor design choice. They are error prone and very hard to debug. (Largely because you have given up the benefit of use strict but also because of their strange syntax.) Your error message points to the offending line. After correcting two symbolic error bugs in that line, your program runs without error. I have no idea if it does what you intend.

    #$$clrCanvas -> itemconfigure(${$clrCanvasItem}, -fill => $color); $clrCanvas -> itemconfigure($clrCanvasItem, -fill => $color);
    Bill