Zoomable TkCanvas?

Hello,

It looks like TkCanvas has no methods for zooming in and out. For Perl,
I
have found the Tk::Worldcanvas and Tk::Abstractcanvas modules. Anybody
knows
about something like that for ruby?

Josef W. wrote:

It looks like TkCanvas has no methods for zooming in and out. For Perl, I
have found the Tk::Worldcanvas and Tk::Abstractcanvas modules. Anybody knows
about something like that for ruby?

Those perl modules sound interesting–maybe it would be useful to have
them in ruby if someone hasn’t done that yet.

I’ve used Tk’s Canvas from tcl and from ruby, and I’ve always had to
implement zooming myself in wrapper classes. One way to do it is tag all
canvas objects (or just the ones that zoom), and use the Canvas#scale
method on that tag. You have to keep track of the current zoom level (Tk
doesn’t), and use that to calculate the arguments to #scale. You also
have to adjust the scroll bars (using ‘configure :scrollregion => …’).
Then use xview/yview to keep the current view position in sync with
the zoom level.

I think that covers it, but if you’re interested, take a look at
canvas.rb in my tkar project. Tkar is a process, rather than a library,
but it abstracts out details like zooming and provides a basic user
interface for controlling zoom, pan, etc.

http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/343516

http://rubyforge.org/projects/tkar

On Tue, Sep 08, 2009 at 04:26:32AM +0900, Joel VanderWerf wrote:

Josef W. wrote:

It looks like TkCanvas has no methods for zooming in and out. For Perl,
I have found the Tk::Worldcanvas and Tk::Abstractcanvas modules. Anybody
knows about something like that for ruby?

Those perl modules sound interesting–maybe it would be useful to have
them in ruby if someone hasn’t done that yet.

Yeah, they make life a whole lot easier. In addition to zooming, they:

  • maintain original coordinates at all zoom factors. Thus even after
    zoom operations, events as well as querying/moving/adding items are
    done as if the canvas is at zoom factor 1.0. So zoom is handled
    completely transparent to the user of the module…
  • turn around the y-axis (origin is at left bottom)
  • handle scroll bars + panning

I’ve used Tk’s Canvas from tcl and from ruby, and I’ve always had to
implement zooming myself in wrapper classes.

Strange, that such a powerful widget is missing such a basic
functionality.

One way to do it is tag all
canvas objects (or just the ones that zoom), and use the Canvas#scale
method on that tag. You have to keep track of the current zoom level (Tk
doesn’t), and use that to calculate the arguments to #scale. You also have
to adjust the scroll bars (using ‘configure :scrollregion => …’). Then
use xview/yview to keep the current view position in sync with the zoom
level.

Sounds easy enough. Is it really that easy? I think this would work only
if you do not add or move any items after you have made zoom operations.
Looks like above mentioned modules do a whole lot more of work. They
override all of the item creation and modification methods to fix
movement
or addition of new items to the current zoom factor.

I think that covers it, but if you’re interested, take a look at canvas.rb
in my tkar project. Tkar is a process, rather than a library, but it
abstracts out details like zooming and provides a basic user interface for
controlling zoom, pan, etc.

http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/343516

http://rubyforge.org/projects/tkar

Sounds very interesting.

Thanks, I’ll check that out!

On Tue, Sep 08, 2009 at 07:35:31AM +0900, Joel VanderWerf wrote:

Josef W. wrote:

  • maintain original coordinates at all zoom factors. Thus even after
    zoom operations, events as well as querying/moving/adding items are
    done as if the canvas is at zoom factor 1.0. So zoom is handled
    completely transparent to the user of the module…

Yep, tkar does that too–object coordinates are independent of zoom level.
TkCanvas coordinates are hidden in the abstraction.

Hmm, somehow I fail to see how this is supposed to work.

I see zoom_by does the scaling, adjusts the scrollregion and updates the
view. IMHO, to keep coordinates independent from zoom level, you would
need
to intercept query/movement/creation of the items. Without that

TkcRectangle.new(canvas, [100,100], [300, 200])
canvas.zoom_by(2.0)
TkcRectangle.new(canvas, [100,100], [300, 200])

would result in two rectangles with different sizes.

  • turn around the y-axis (origin is at left bottom)
    Tkar has an option to flip the y-axis (and also an option to use radians
    instead of degrees for rotation commands).
  • handle scroll bars + panning
    Check.

This is done with the help of the Window class, AFAICS. So it is not as
transparent as the Tk::AbstractCanvas module.

Josef W. wrote:

I see zoom_by does the scaling, adjusts the scrollregion and updates the
view. IMHO, to keep coordinates independent from zoom level, you would need
to intercept query/movement/creation of the items. Without that

TkcRectangle.new(canvas, [100,100], [300, 200])
canvas.zoom_by(2.0)
TkcRectangle.new(canvas, [100,100], [300, 200])

would result in two rectangles with different sizes.

Tkar is intended to be used as a process not as a library. Another
process (doesn’t have to be ruby, doesn’t have to be a Tk gui) sends
commands to tkar over a pipe or socket. Those commands use the abstract
coordinate system.

If you use the Tk methods such as TkcRectangle.new, they will use Tk’s
native coordinates.

The corresponding methods in Tkar are in the primitives.rb file. For
example the #rect method. This method understands scaling. It also
understands rotation, which Tk primitives do not. These methods aren’t
designed to be used as a library, though.

This is done with the help of the Window class, AFAICS. So it is not as
transparent as the Tk::AbstractCanvas module.

Different kind of abstraction here–tkar implements a little language to
drive animations over IO, it’s not a library API.

I think a ruby port of the perl Tk::AbstractCanvas would be useful, but
in a different way from tkar. I wrote tkar primarily so that I could do
2D animations in simulink–a ruby library isn’t much use for that, but a
socket interface is fine (and has the advantage of distributing
workload). Also, with a little munging, you can pipe the output of
real-time log files and get useful animations. See ps.rb for an
example–it filters the output of ps to show a graphical representation
of the cpu usage of running processes.

Josef W. wrote:

I see zoom_by does the scaling, adjusts the scrollregion and updates the
view. IMHO, to keep coordinates independent from zoom level, you would need
to intercept query/movement/creation of the items. Without that

TkcRectangle.new(canvas, [100,100], [300, 200])
canvas.zoom_by(2.0)
TkcRectangle.new(canvas, [100,100], [300, 200])

would result in two rectangles with different sizes.

Tkar is intended to be used as a process not as a library. Another
process (doesn’t have to be ruby, doesn’t have to be a Tk gui) sends
commands to tkar over a pipe or socket. Those commands use the abstract
coordinate system.

If you use the Tk methods such as TkcRectangle.new, they will use Tk’s
native coordinates.

The corresponding methods in Tkar are in the primitives.rb file. For
example the #rect method. This method understands scaling. It also
understands rotation, which Tk primitives do not. These methods aren’t
designed to be used as a library, though.

This is done with the help of the Window class, AFAICS. So it is not as
transparent as the Tk::AbstractCanvas module.

Different kind of abstraction here–tkar implements a little language to
drive animations over IO, it’s not a library API.

I think a ruby port of the perl Tk::AbstractCanvas would be useful, but
in a different way from tkar. I wrote tkar primarily so that I could do
2D animations in simulink–a ruby library isn’t much use for that, but a
socket interface is fine (and has the advantage of distributing
workload). Also, with a little munging, you can pipe the output of
real-time log files and get useful animations. See ps.rb for an
example–it filters the output of ps to show a graphical representation
of the cpu usage of running processes.

Josef W. wrote:

  • maintain original coordinates at all zoom factors. Thus even after
    zoom operations, events as well as querying/moving/adding items are
    done as if the canvas is at zoom factor 1.0. So zoom is handled
    completely transparent to the user of the module…

Yep, tkar does that too–object coordinates are independent of zoom
level. TkCanvas coordinates are hidden in the abstraction.

  • turn around the y-axis (origin is at left bottom)

Tkar has an option to flip the y-axis (and also an option to use radians
instead of degrees for rotation commands).

  • handle scroll bars + panning

Check.

One way to do it is tag all
canvas objects (or just the ones that zoom), and use the Canvas#scale
method on that tag. You have to keep track of the current zoom level (Tk
doesn’t), and use that to calculate the arguments to #scale. You also have
to adjust the scroll bars (using ‘configure :scrollregion => …’). Then
use xview/yview to keep the current view position in sync with the zoom
level.

Sounds easy enough. Is it really that easy? I think this would work only
if you do not add or move any items after you have made zoom operations.

You are right, that’s just the basic idea… tkar does coordinate
transforms for all operations (move, rotate, add).

On Wed, Sep 09, 2009 at 02:05:34AM +0900, Joel VanderWerf wrote:

view. IMHO, to keep coordinates independent from zoom level, you would need
to intercept query/movement/creation of the items. Without that

TkcRectangle.new(canvas, [100,100], [300, 200])
canvas.zoom_by(2.0)
TkcRectangle.new(canvas, [100,100], [300, 200])

would result in two rectangles with different sizes.

[ … ]
I think a ruby port of the perl Tk::AbstractCanvas would be useful, but in
a different way from tkar. [ … ]

So OK. I thought, although I’m a complete newbie to ruby, I’d try to
roll
my own. At least, that would result in a good exercise. So I started by
stealing the basics for a scrolled canvas from

http://blade.nagaokaut.ac.jp/cgi-bin/vframe.rb/ruby/ruby-talk/122597?122482-123428

and applying this patch:

— lib/scrolledcanvas.rb.orig
+++ lib/scrolledcanvas.rb
@@ -6,6 +6,8 @@
include TkComposite

def initialize_composite(keys={})

  • @zoom = 1.0 # need this for the zoom_by method
  • @h_scr = TkScrollbar.new(@frame)
    @v_scr = TkScrollbar.new(@frame)

@@ -23,7 +25,7 @@
@v_scr.grid(:row=>0, :column=>1, :sticky=>‘ns’)

 delegate('DEFAULT', @canvas)
  • delegate(‘background’, @text, @h_scr, @v_scr)
  • delegate(‘background’, @frame, @h_scr, @v_scr) # looked like a typo
    delegate(‘activeforeground’, @h_scr, @v_scr)
    delegate(‘troughcolor’, @h_scr, @v_scr)
    delegate(‘repeatdelay’, @h_scr, @v_scr)

Then, I copied the zoon_by, xview and yview methods from your tkar
package
and commented the call to adjust_scrollregion to avoid access to the
uninitialized @bounds array.

So at this stage, I have a canvas that can be scrolled and zoomed. Fine.

But how do I override the methods to create the items? In Perl/Tk, that
would be easy, since item creation is done via canvas methods. But in
Ruby/Tk, items are created via their own classes (e.g. TkcLine.new(args)
or something). There don’t seem to exist methods in the Canvas class to
create items, which could easily be overridden.

Any hints?

PS: here’s the current state of affairs:

#!/usr/bin/env ruby

require ‘tk’

class TkScrolledCanvas < TkCanvas
include TkComposite

def initialize_composite(keys={})
@zoom = 1.0

@h_scr = TkScrollbar.new(@frame)
@v_scr = TkScrollbar.new(@frame)

@canvas = TkCanvas.new(@frame)
@path = @canvas.path

@canvas.xscrollbar(@h_scr)
@canvas.yscrollbar(@v_scr)

TkGrid.rowconfigure(@frame, 0, :weight=>1, :minsize=>0)
TkGrid.columnconfigure(@frame, 0, :weight=>1, :minsize=>0)

@canvas.grid(:row=>0, :column=>0, :sticky=>'news')
@h_scr.grid(:row=>1, :column=>0, :sticky=>'ew')
@v_scr.grid(:row=>0, :column=>1, :sticky=>'ns')

delegate('DEFAULT', @canvas)
delegate('background', @text, @h_scr, @v_scr)
delegate('activeforeground', @h_scr, @v_scr)
delegate('troughcolor', @h_scr, @v_scr)
delegate('repeatdelay', @h_scr, @v_scr)
delegate('repeatinterval', @h_scr, @v_scr)
delegate('borderwidth', @frame)
delegate('relief', @frame)

delegate_alias('canvasborderwidth', 'borderwidth', @canvas)
delegate_alias('canvasrelief', 'relief', @canvas)

delegate_alias('scrollbarborderwidth', 'borderwidth', @h_scr, 

@v_scr)
delegate_alias(‘scrollbarrelief’, ‘relief’, @h_scr, @v_scr)

configure(keys) unless keys.empty?

end

def zoom_by zf
zf = Float(zf)
@zoom *= zf

vf = (1 - 1/zf) / 2

x0, x1 = xview ;  xf = x0 + vf * (x1-x0)
y0, y1 = yview ;  yf = y0 + vf * (y1-y0)

scale 'all', 0, 0, zf, zf
adjust_scrollregion

xview "moveto", xf
yview "moveto", yf

end

def adjust_scrollregion

configure :scrollregion => @bounds.map {|u|u*@zoom}

## if all of canvas can be shown, hide the scroll bars

end

def xview(mode=nil, *args)
if mode and mode == “scroll” and @follow_xdelta
number, what = args
x_pre, = xview
r = super(mode, *args)
x_post, = xview
x0,y0,x1,y1 = @bounds
@follow_xdelta += (x_post - x_pre) * (x1-x0)
r
elsif not mode
super()
else
super(mode, *args)
end
end

def yview(mode=nil, *args)
if mode and mode == “scroll” and @follow_ydelta
number, what = args
y_pre, = yview
r = super(mode, *args)
y_post, = yview
x0,y0,x1,y1 = @bounds
@follow_ydelta += (y_post - y_pre) * (y1-y0)
r
elsif not mode
super()
else
super(mode, *args)
end
end
end

root = TkRoot.new { title “zoomcanvas” }

c = TkScrolledCanvas.new(:scrollregion=>[0,0,500,400],
:relief=>“sunken”).pack(:expand=>1,:fill=>“both”)
TkcRectangle.new(c, [100,100], [300, 200])
c.bind(“1”, proc{|e| TkcRectangle.new(c, [100,100], [300, 200]) })
root.bind(“z”) { c.zoom_by(1.5) }
root.bind(“Z”) { c.zoom_by(1/1.5) }

Tk.mainloop

On Wed, Sep 09, 2009 at 05:29:47AM +0900, Joel VanderWerf wrote:

Thanks for your patience with me, Joel!

def initialize(x,y,w,h)
super(…) # adjust args depending on zoom level etc.
end
end

That would result in:

  • lots of new subclasses, polluting namespaces
  • lots of code duplication
  • it would not be a drop-in replacement: one would have to change the
    class names of the items when switching from standard canvas to the
    improved canvas

Maybe extending TkcItem would be a better solution. Something like:

class TkcItem
alias orig_initialize initialize
def initialize(parent, *args)
# do whatever we need
orig_initialize parent, args
end
end

Opinions?

Josef W. wrote:

But how do I override the methods to create the items? In Perl/Tk, that
would be easy, since item creation is done via canvas methods. But in
Ruby/Tk, items are created via their own classes (e.g. TkcLine.new(args)
or something). There don’t seem to exist methods in the Canvas class to
create items, which could easily be overridden.

Maybe subclass (or delegate to) the Tk classes:

class MyRectangle < TkcRectangle
def initialize(x,y,w,h)
super(…) # adjust args depending on zoom level etc.
end
end

Josef W. wrote:

Maybe subclass (or delegate to) the Tk classes:

  • it would not be a drop-in replacement: one would have to change the
    class names of the items when switching from standard canvas to the
    improved canvas

It’s a matter of taste, I suppose. Preserving the existing Tk classes
has an advantage: if you want to place objects on the canvas that are
not affected by zoooming (foreground, OSD-type display, controls, etc),
you can still use the base classes.

Keep namespaces clean by putting everything in a module:

module MyTk
class TkcRectangle < ::TkcRectangle

That way, you have both:

MyTk::TkcRectangle.new # new kind
TkcRectangle # old kind
::TkcRectangle # old kind even when in MyTk scope

I don’t see a problem with new subclasses or code duplication.