How to bind canvas events to tags?

Hello,

In Perl/Tk, I can use

$canv->bind("foobar", '<1>' => sub {
    xxxx;
});

to bind a canvas event to all items with the “foobar” tag. This is a
very
powerful construct, so I’d like to use it in ruby, too. But ruby’s
canvas.bind method seems to accept only two arguments: the event and the
proc to be called. How would I do the same in ruby?

BTW: How do I find out which arguments are accepted by Ruby/Tk methods?
There seem to be no man pages and the “Translating from Perl/Tk
Documentation” in the pragmatic programmers book is not very
helpful,
also.

Josef W. wrote:

$canv->bind("foobar", '<1>' => sub {
   canvas.itembind(tag, '1', '%x %y') do |x, y|

In this case, we’re binding mouse single click events (I assume that’s
the same as perl’s ‘<1>’) and we’re grabbing the pointer location into
block args x and y.

On Thu, Sep 10, 2009 at 10:27:54AM -0700, Joel VanderWerf wrote:

Josef W. wrote:

$canv->bind("foobar", '<1>' => sub {
  canvas.itembind(tag, '1', '%x %y') do |x, y|

In this case, we’re binding mouse single click events (I assume that’s the
same as perl’s ‘<1>’) and we’re grabbing the pointer location into block
args x and y.

Yeah, that works. Thanks!

BTW: do you have a description of the “%x %y” formatting specification?

Josef W. wrote:

BTW: do you have a description of the “%x %y” formatting specification?

http://www.tcl.tk/man/tcl8.4/TkCmd/bind.htm#M41

On Sat, Sep 12, 2009 at 02:23:30AM +0900, Joel VanderWerf wrote:

Josef W. wrote:

BTW: do you have a description of the “%x %y” formatting specification?

Tk Built-In Commands - bind manual page

Ough. Every time I think I get closer (remember? I am trying to
implement
a zoomable canvas), I find there is a lot more work to do :-()

My first attempt to adopt event handling to the zoom operation was like
this:

class TkEvent::Event
# FIXME: need a better name than “nonzoomed”. Recommendations?
attr_accessor :x_nonzoomed, :y_nonzoomed
end

class TkZoomCanvas < TkCanvas
def itembind(tag, ev, cb)
super(tag, ev, proc{ |e|
if e.x then e.x_nonzoomed = e.x/@zoom ; end
if e.y then e.y_nonzoomed = e.y/@zoom ; end
cb.call e
})
end

def bind(ev, cb)
  # looks similar to itembind
end

end

Now, itembind (and bind?) seem to accept various forms:

  1. canvas.itembind(“mytag”, “1”, proc{|e| … })
    This form works fine with above code

  2. canvas.itembind(“mytag”, “1”, “%x %y”) {|x, y| … }
    In this case, I will have to build the argument list. Seems to be
    a
    tedious task. Maybe there already exist a Tk method I could use to
    build that list?

Are this the only two existing forms?

Josef W. wrote:

Ough. Every time I think I get closer (remember? I am trying to implement
a zoomable canvas), I find there is a lot more work to do :-()

Do you need to exactly replicate the TkCanvas interface in the zoomable
canvas? Why not get creative and develop a class with a new (possibly
better) interface? (And maybe use delegation rather than inheritance.)
The TkCanvas interface is close to Tcl/Tk’s canvas, which is great for
documentation and porting, but it’s not necessarily very ruby-like.

On Mon, Sep 14, 2009 at 03:41:34AM +0900, Joel VanderWerf wrote:

Josef W. wrote:

Ough. Every time I think I get closer (remember? I am trying to implement
a zoomable canvas), I find there is a lot more work to do :-()

Do you need to exactly replicate the TkCanvas interface in the zoomable
canvas? Why not get creative and develop a class with a new (possibly
better) interface? (And maybe use delegation rather than inheritance.) The
TkCanvas interface is close to Tcl/Tk’s canvas, which is great for
documentation and porting, but it’s not necessarily very ruby-like.

Thanks for your suggestion (and sorry for my late reply).

Well, as you might have already guessed, I’m very new to ruby, so I
don’t
yet have a good feeling about what is “very ruby like”. I’d love to see
suggestions.

For the particular topic about the binding, after some thought, I’ve
come
to the conclusion that extending the format string is not the best idea.
The format string consists of one-letter formats. I’d pollute this
one-letter-namespace if I would introduce my own letters. Thus, I
decided
to let the base class do the job if a string is passed.

Attached is my current implementation of the zoomable+scrollable canvas.
It works fine, AFAICS. I’d love to hear any suggestions about how to
make
it more ruby-like.

#!/usr/bin/ruby

require ‘tk’

class TkEvent::Event
attr_accessor :x_nonzoom, :y_nonzoom
end

class TkScrolledCanvas < TkCanvas
include TkComposite
attr_reader :zoom

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', @frame, @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
configure :scrollregion => bbox("all")

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

end

def zoom_to z
zoom_by(z/@zoom)
end

def bind(ev, cb)
if cb.class == String
super
else
super(ev, proc{ |e| process_event(e, cb) })
end
end

def itembind(tag, ev, cb)
if cb.class == String
super
else
super(tag, ev, proc{ |e| process_event(e, cb) })
end
end

def coords(tag, *args)
newargs = adjust_coords(@zoom, args)
ret = super(tag, *newargs)
return ret unless ret.class == Array
ret.collect { |v| v / @zoom }
end

def move(tag, x, y)
super(tag, x*@zoom, y*@zoom)
end

def create(type, *args)
newargs = adjust_coords(@zoom, args)
super(type, *newargs)
end

private

def process_event(e, cb)
if e.x then e.x_nonzoom=e.x/@zoom ; end
if e.y then e.y_nonzoom=e.y/@zoom ; end
cb.call e
end

def adjust_coords(mul, args)
args.collect do |arg|
arg.class == Array ? arg.collect { |v| v * mul } : arg
end
end
end

class TkcItem
alias orig_initialize initialize

def initialize(parent, *args)
if parent.class == TkScrolledCanvas
zoom = parent.zoom
newargs = args.collect do |arg|
arg.class == Array ? arg.collect { |v| v * zoom } : arg
end
else
newargs = args
end
orig_initialize parent, *newargs
end

def bind(ev, cb)
super(ev, proc{ |e|
if @parent.class == TkScrolledCanvas
zoom = @parent.zoom
if e.x then e.x_nonzoom=e.x/zoom ; end
if e.y then e.y_nonzoom=e.y/zoom ; end
cb.call e
end
})
end
end