Zoomable TkCanvas?

J

Josef Wolf

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?
 
J

Joel VanderWerf

Josef said:
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
 
J

Josef Wolf

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!
 
J

Joel VanderWerf

Josef said:
- 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.


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).
 
J

Josef Wolf

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.
Tkar has an option to flip the y-axis (and also an option to use radians
instead of degrees for rotation commands).

Check.

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

Joel VanderWerf

Josef said:
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.

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.
 
J

Joel VanderWerf

Josef said:
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.

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.
 
J

Josef Wolf

Josef said:
Josef Wolf 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.
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.
[ ... ]
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
 
J

Joel VanderWerf

Josef said:
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
 
J

Josef Wolf

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

Thanks for your patience with me, Joel!
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

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?
 
J

Joel VanderWerf

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

Thanks for your patience with me, Joel!


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

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.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,769
Messages
2,569,580
Members
45,055
Latest member
SlimSparkKetoACVReview

Latest Threads

Top