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

Hi,
Please have a look on the folowing code, it is based on some code I was kindly offered by Rob (user: rcseege) for a previous post of mine.
The question is:
How can I change the 'AlignAll' subroutine so it will move the scrollbars of all 4 canvases so I will get all 4 rectangles aligned in the middle of the viewable area?
Few notes:
1. the user can first zoom in/out and than hit the 'Align' button.
2. You can assume the user hasn't zooomed out too much (in which case it might be impossible to align the rectangles).
3. I tried playing with the scanMark and scanDragto method but could not get it to work. Any explanation on these is more than welcome (the basic doc. on that left me with some big ???...).
Thanks!
use strict; use Tk; use Tk::Pane; my @tile; my @sc; my $x; my $i; my $initial_width=800; my $scale=1; my $mw = MainWindow->new; my $frame = $mw->Scrolled("Frame", -scrollbars => 's', -sticky => 'nsew', -bg => 'black', -width => 450, -height => 650 )->pack(qw/-expand 1 -fill both/); foreach $i (0 .. 3) { $tile[$i] = $frame->Frame( -bg => 'black' )->pack(); $sc[$i] = $tile[$i]->Scrolled('Canvas', -scrollbars => 'os', -scrollregion => [0, 0, $initial_width, 125], -width => 400, -height => 125, -background => randColor() )->pack(qw/-fill both -side top -expand 1/); $x = randNumber(); $sc[$i]->createRectangle($x, 50, $x+20, 90, -fill => randColor(), -outline => 'black', -width => 2, -tags => 'rect', ); $sc[$i]->createLine( 10,100, 790,100, -arrow => 'last'); for($x=50;$x<800;$x+=50){ $sc[$i]->createLine( $x,95, $x,105 ); } } $frame->Button( -text => " Zoom Out ", -width => 10, -command => sub { $scale*=0.8; foreach $i (0 .. 3) { $sc[$i]->scale(qw/all 0 0 .8 1/); $sc[$i]->configure(-scrollregion => [0, 0, int +($scale*$initial_width), 125]); } } )->pack(qw/-side left -padx 25/); $frame->Button( -text => " Zoom In ", -width => 10, -command => sub { $scale*=1.25; foreach $i (0 .. 3) { $sc[$i]->scale(qw/all 0 0 1.25 1/); $sc[$i]->configure(-scrollregion => [0, 0, int +($scale*$initial_width), 125]); } } )->pack(qw/-side left -padx 25/); $frame->Button( -text => " Align ", -width => 10, -command => [\&AlignAll], )->pack(qw/-side left -padx 25/); MainLoop; sub AlignAll{ my ($x1, $y1, $x2, $y2); my $i; my $location; foreach $i (0 .. 3) { ($x1, $y1, $x2, $y2) = $sc[$i]->bbox('rect'); $location =int(($x1+$x2)/2); warn("$location"); } } sub randColor { my @colors = qw(red yellow blue orange green purple cyan pink); return $colors[rand($#colors + 1)]; } sub randNumber { my ($max, $min) = (600, 200); my $size = int(rand($max)); $size += $min if $size < $min; return $size; }

Replies are listed 'Best First'.
Re: Tk: Aligning the items in seperate canvases
by liverpole (Monsignor) on Nov 03, 2006 at 16:15 UTC
    Hi tcarmeli,

    Update:  My following "solution" wasn't adequate.  I was thinking you wanted to reposition the scrollbars and rectangles to the center, but of course that wasn't what you wanted at all.  I'm still trying to figure out a concise way to do this, but please don't go by what I originally wrote below ...

    Update 2:  My reponse to zentara below gives a much better solution, but it's still not as good as the one by BrowserUK below.  His answer is clearly the best if you're willing to allow scrolling outside of the scrollable region; the rectangles stay centered after multiple zoom-in and zoom-out operations.  (Nice job, BrowserUK)

    One possible way would be to retrieve the current coordinates of each scrollbar, in your AlignAll subroutine, calculate its width, and then use the same width but place it centered at 0.5, using the methods get and set:

    sub AlignAll{ my ($x1, $y1, $x2, $y2); my $i; my $location; + foreach $i (0 .. 3) { # Reposition scrollbar to the middle my $sbar = $sc[$i]->Subwidget('xscrollbar'); my @range = $sbar->get(); my $size = ($range[1] - $range[0]) / 2; $sbar->set(0.5 - $size, 0.5 + $size); + ($x1, $y1, $x2, $y2) = $sc[$i]->bbox('rect'); $location =int(($x1+$x2)/2); warn("$location"); } }

    There may well be a better/easier way to do this (if there is, my money's on the the Tk master :-)), but I believe this will do what you want.


    s''(q.S:$/9=(T1';s;(..)(..);$..=substr+crypt($1,$2),2,3;eg;print$..$/
      Good try, but that just moves the scroll bars themselves without aligning the rectangles... . kind of wierd though, repositioning the scrollbars programaticly does not seem to change the screen that they control. Wish I knew enough about Tk to help. Will play with this... can see some neat applications for this. Cool!

      ...the majority is always wrong, and always the last to know about it...

      Thanks for all of you!!!
      That was extremely helpful.
Re: Tk: Aligning the items in seperate canvases
by BrowserUk (Patriarch) on Nov 03, 2006 at 20:58 UTC

    Try this.

    sub AlignAll{ foreach my $i (0 .. 3) { my( $x1, $y1, $x2, $y2 ) = $sc[$i]->bbox('rect'); my $location =int(($x1+$x2)/2); ## The location minus half the width of the window ## Over the scaled width of the drawing my $abs = ( $location - 200 ) / ( $initial_width * $scale ); $sc[$i]->xviewMoveto( $abs ); } }

    With the addition of -confine => 0 when you create your canvases,

    $sc[$i] = $tile[$i]->Scrolled('Canvas', -confine => 0, -scrollbars => 'os', -scrollregion => [0, 0, $initial_width, 125], -width => 400, -height => 125, -background => randColor() )->pack(qw/-fill both -side top -expand 1/);

    it aligns and centres the boxes at all scales from when the entire arrow is just a blob through to the box being bigger than the window.


    Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
    Lingua non convalesco, consenesco et abolesco. -- Rule 1 has a caveat! -- Who broke the cabal?
    "Science is about questioning the status quo. Questioning authority".
    In the absence of evidence, opinion is indistinguishable from prejudice.
Re: Tk: Aligning the items in seperate canvases
by zentara (Cardinal) on Nov 03, 2006 at 19:59 UTC
    Without alot of fancy logic, which tests all the positions, you can't perfectly align them with a program. Why? Because of the random placement, some of the canvases will hit the scroll limit and will not be able to moved in one direction(unless you do some trickery like expanding the scrollregion).

    Check out the following sub, and test it a few times. Sometimes it will align, as long as there are not any random placements in the high range. Then find the line I've commented out, and uncomment it. That will always bring all 4 into the range, but not perfectly aligned. Like I said, you might be able to expand the scrollregion, so the scrollbar dosn't get locked into the far right position.

    Another option which you might try, ( I'm thru fiddling with this :-) ) is to pick a x position on the screen, then scroll all canvases to the 0 position, then in a loop, scroll them 1 unit at a time until the boxe's screen position line up.

    Someone else may figure something out, but unless you expand your scrollregion, the scrolled-window sliders will lock you up occaisionally.

    sub AlignAll{ my ($x1, $y1, $x2, $y2); my $i; my $location; my %pos; foreach $i (0 .. 3) { ($x1, $y1, $x2, $y2) = $sc[$i]->bbox('rect'); $location =int(($x1+$x2)/2); warn("$location"); $pos{$i} = $location; $sc[$i]->xviewMoveto( $location/$initial_width ); } my @pos; foreach my $key (keys %pos ){ push @pos, $pos{$key}; } print "@pos\n"; my $max = &max(@pos); print "$max\n"; #move them all to the most right one, #since those are already at right scroll limit foreach $i (0 .. 3) { print $i," ",join ' ',$sc[$i]->xview(),"\n"; my $diff = $pos{$i} - $max; ############################################### ############################################### #try it with this line # $sc[$i]->xviewScroll($diff, 'units'); ############################################### } } sub max { my $max; for (@_) { $max = $_ if $_ > $max } return $max; } ########################################

    I'm not really a human, but I play one on earth. Cogito ergo sum a bum
      Sorry, Master,

      I have to disagree with you on this one :-)

      After working on it for a while, I got what seems to be a feasible solution.  It took a little extra work to get it to function for zoom-in and zoom-out, but here's how to do it:

      First, near the top of your program with the other global variable declarations, add two variables, an array @box and a scalar $viewable_width:

      my @box; # Tracks the IDs of each rectangle my $viewable_width=400; # How wide is the viewable Canvas?

      You may also want to use $viewable_width when constructing the Canvas:

      $sc[$i] = $tile[$i]->Scrolled('Canvas', -scrollbars => 'os', -scrollregion => [0, 0, $initial_width, 125], -width => $viewable_width, -height => 125, -background => randColor() )->pack(qw/-fill both -side top -expand 1/);

      Now, when you create a rectangle, be sure to save its ID in the @box array:

      $box[$i] = $sc[$i]->createRectangle($x, 50, $x+20, 90, -fill => randColor(), -outline => 'black', -width => 2, -tags => 'rect', );

      Here's where things get interesting ...  You have to calculate what x-coordinate in the virtual window (whose width is identified by $initial_width) should be the first point to appear in the visual window (identified by $visual_window).

      To calculate this, take the midpoint of the rectangle's coordinates (($x1 + $x2) / 2), and subtract from it one half of the visual window (($x1 + $x2 - $viewable_width) / 2).

      Here's the code which implements the alignment:

      sub AlignAll{ my ($x1, $y1, $x2, $y2); foreach my $i (0 .. 3) { my $sc = $sc[$i]; # Get Canvas objec +t my $box = $box[$i]; # Get box id (rect +angle) my ($x1, $y1, $x2, $y2) = $sc->bbox($box); # Get box coordina +tes my $mid = ($x1 + $x2) / 2; # Get midpoint, mi +nus $mid -= $viewable_width / 2; # 1/2 of visible + window my $newx = $mid / $initial_width; # Get delta x-dist +ance $sc->xviewMoveto($newx); # Move Canvas obje +ct } }

      Note that this will work in the absence of zooming.

      To make it work for zoom-in and zoom-out, you'll need to make one last set of modifications.  In the anonymous subroutines defined for the first 2 buttons, when you calculate scale, you should then adjust $initial_window by that factor:

      $frame->Button( -text => " Zoom Out ", -width => 10, -command => sub { $scale*=0.8; $initial_width *= $scale; foreach $i (0 .. 3) { $sc[$i]->scale(qw/all 0 0 .8 1/); $sc[$i]->configure(-scrollregion => [0, 0, $initial_width, + 125]); } } )->pack(qw/-side left -padx 25/); $frame->Button( -text => " Zoom In ", -width => 10, -command => sub { $scale*=1.25; $initial_width *= $scale; foreach $i (0 .. 3) { $sc[$i]->scale(qw/all 0 0 1.25 1/); $sc[$i]->configure(-scrollregion => [0, 0, $initial_width, + 125]); } } )->pack(qw/-side left -padx 25/);

      Now it should all work the way you want!


      s''(q.S:$/9=(T1';s;(..)(..);$..=substr+crypt($1,$2),2,3;eg;print$..$/
        Congratulations. :-) I knew there was some sort of hyberbolic relation in there, but I give up easy. :-)

        In my defense, I was close enough for government work, I had

        $sc[$i]->xviewMoveto( ($location /$initial_width ); # where I needed $sc[$i]->xviewMoveto( ($location - 400/2)/$initial_width );
        Of course, your work on zooming gets you an A++.

        This definitely is a snippet to save, since it is a general purpose formula for centering a canvas item in a scrolled window.


        I'm not really a human, but I play one on earth. Cogito ergo sum a bum