Ruby-Tk FAQ

This is my 1st attempt. It will grow based on need. Please email me if you have any good things to add. armin@approximity.com

I had a hard time getting to speed in Ruby-Tk and most you find is was told me by the newsgroup, the Japanese Ruby-TK FAQ (link) and a few good websites. Special thanks to: Kero van Gelder, Martin Weber, Hidetoshi NAGAI, Mike Hall, ..., (who did I forget to list?)

Overview

Links

Mouse-position when a button-1 is pressed (or "d" is pressed).

Each time we draw a tiny rectangle.
require 'tk'
root=TkRoot.new{title "Bug or Feature or Misunderstanding?"}
canvas=TkCanvas.new(root)
canvas.pack
canvas.bind('1',  proc{|e| p "#{e.x}, #{e.y}";
           TkcRectangle.new(canvas, e.x,e.y,e.x-5, e.y-5)})
root.bind('Any-Key-d',  proc{|e| p "#{e.x}, #{e.y}"; 
           TkcRectangle.new(canvas, e.x,e.y,e.x-5, e.y-5)})
Tk.mainloop

Keyboard-event gives wrong mouse coordinates in some cases

If you leave the canvas.focus() out of the code-snipplet, your e.y will be wrong by about 30 pixels (height of menuline).
Thx to Kero van Gelder for helping: You get the (x, y) of the mouse w.r.t. the root window, since that is what you bind to. You can bind to the canvas to get (x, y) w.r.t. to the canvas. You may want to add canvas.focus() to your code.
require 'tk'
root=TkRoot.new{title "Le Gros SA"}
canvas = TkCanvas.new(root)
menuline=TkFrame.new(root)
menuline.pack("side"=>"top")
TkMenubutton.new(menuline) { |mb|
    text "Zoom"
    underline 0
    menu TkMenu.new(mb) {
      add 'command', 'label' => 'Zoom in',
        'underline' => 0,
   	 'command' => proc { p "in"; }	
      add 'command', 'label' => 'Zoom out',
        'underline' => 0,
   	 'command' => proc { p "out" }	  
    }
    pack('side' => 'top', 'padx' => '1m')
   }

canvas.pack
canvas.focus
root.bind('Any-Key-s',  proc{|e| TkcRectangle.new(canvas, e.x,e.y,e.x-5, e.y-5)})
Tk.mainloop

Convert a canvas to postscript

require "tk" 
min_x = 0
min_y = 0
c=TkCanvas.new
TkcRectangle.new(c, 10, 10, 50, 50)
c.pack 
ps = c.postscript('x' => min_x, 'y' => min_y, 
       'width' => c.cget('width'),'height' => c.cget('height')) 
f = open('file.ps', 'w')
f.print ps
f.close 
Tk.mainloop

If you have problems with c.cget, use: TkWinfo.height c and TkWinfo.width c. If you know of a way to convert it directly into gif/png/jpg/whatever without using an external conversion utility, please tell me.

Canvas is enlarged, but canvas does not seem to notice that

I made a canvas to draw rectangles. Then I enlarged it (using the mouse), but still I could draw only in the same old canvas size.
Solution: forgot to add the fill and expand Parameters.
canvas.pack('fill' => 'both', 'expand'=>true)

Group objects; Combining several widgets on a canvas; Tags;

I want to manipulate a group of lines/circles/text and move them as if it were a (single) widget. For example I have rectangles and text in each rectangle. To move that around I could move the rectangle and the text, but I would have to code the "interconnection" between the two items (if I move the rectangle by 20,20 I need to move the text by 20, 20 and vice versa). Can't Ruby-Tk do that work for me? Thanks to Hidetoshi NAGAI and Kero van Gelder for the answer.
require 'tk' class D
  def initialize
    TkRoot.new.title('Tk fun')
    main_canvas = TkCanvas.new.pack
    frame_canvas = TkCanvas.new
    win = TkcWindow.new(main_canvas, 1, 1, 'width'=>200, 'height'=>100,
                        'window'=>frame_canvas, 'anchor'=>'nw')
    TkcRectangle.new(frame_canvas, 5, 5, 50, 50)
    TkcText.new(frame_canvas, 10, 10, 'text'=>'sample text', 'anchor'=>'nw')
    TkFrame.new {|f|
      TkButton.new(f, 'text'=>'move (+10,+10)',
                   'command'=>proc{win.move(10,10)}).pack('side'=>'left')
      TkButton.new(f, 'text'=>'move (-10,-10)',
                   'command'=>proc{win.move(-10,-10)}).pack('side'=>'right')
      pack
    }
  end
end D.new
Tk.mainloop
----------------------------------------------------
No need 'pack' for canvas objects.
They are put on the canvas as soon as creation.
To define a widget on a canvas window object,
please use 'window' option of TkcWindow object. The other method is to use canvas tags.
Please read the following sample script.
In the script, I give tags to canvas objects by 'tag' option.
By Canvas tags, you can make some groups of canvas objects.
By adding some tags to same canvas object,
the object can belong to some groups.
----------------------------------------------------
require 'tk' class D
  def initialize(canvas, tag = TkcTag.new(canvas))
    @canvas = canvas
    @tag = tag
    TkcOval.new(@canvas, 20, 20, 40, 40, 'tag'=>tag)
    TkcRectangle.new(@canvas, 5, 5, 50, 50, 'tag'=>[@tag, 'rectangle'])
    TkcRectangle.new(@canvas, 10, 10, 60, 60, 'tag'=>[@tag, 'rectangle'])
    @tag_text = TkcTag.new(@canvas)
    TkcText.new(@canvas, 10, 10, 'text'=>'sample text', 'anchor'=>'nw',
                'tag'=>[@tag, @tag_text])
    TkcText.new(@canvas, 30, 30, 'text'=>'XXXXXXX', 'anchor'=>'nw',
                'tag'=>[@tag, @tag_text])
  end   def move(dx, dy)
    @tag.move(dx, dy)
  end   def move_text(dx, dy)
    @tag_text.move(dx, dy)  # or @canvas.move(@tag_text, dx, dy)
  end   def move_rectangle(dx, dy)
    @canvas.move('rectangle', dx, dy)
  end
end TkRoot.new.title('Tk fun')
canvas = TkCanvas.new.pack
#ctag = TkcTag.new(canvas)
#objs = D.new(canvas, ctag)
objs = D.new(canvas)
TkFrame.new {|f|
  TkButton.new(f, 'text'=>'move (+10,+10)',
               'command'=>proc{objs.move(10,10)}).pack('side'=>'left')
  TkButton.new(f, 'text'=>'move (-10,-10)',
               'command'=>proc{objs.move(-10,-10)}).pack('side'=>'right')
  pack
}
TkFrame.new {|f|
  TkButton.new(f, 'text'=>'move rectangle (+10,+10)',
               'command'=>proc{objs.move_rectangle(10,10)}) {
    pack('side'=>'left')
  }
  TkButton.new(f, 'text'=>'move rectangle (-10,-10)',
               'command'=>proc{objs.move_rectangle(-10,-10)}) {
    pack('side'=>'right')
  }
  pack
}
TkFrame.new {|f|
  TkButton.new(f, 'text'=>'move text (+10,+10)',
               'command'=>proc{objs.move_text(10,10)}).pack('side'=>'left')
  TkButton.new(f, 'text'=>'move text (-10,-10)',
               'command'=>proc{objs.move_text(-10,-10)}).pack('side'=>'right')
  pack
} Tk.mainloop

Display image from a gif-file on a canvas

the 100,100 are the x and y coordinates where you want the pic to be.
require 'tk'
root=TkRoot.new{title "Ex1"}
canvas = TkCanvas.new(root)  
canvas.pack
image = TkPhotoImage.new('file' => 'up16.gif')
t=TkcImage.new(canvas, 100, 100,
                         'image' => image)
Tk.mainloop

Window-size; TkWinfo

p TkWinfo.geometry Tk.root;

if used on other widgets and the result is not uptodate call update first.

Fileevent

from Hidetoshi NAGAI:
I'm sorry but 'fileevent' command is not implemented on Ruby/Tk. Because, Ruby has better way to treat streams than Tcl/Tk. I recommend you to use threads for treatment of streams. But attention!

Sometimes Ruby/Tk is running too slow, if the event loop, Tk.mainloop, is running in the sub thread. Please use sub threads for stream access, and use the main thread for Tk.mainloop.

Suse 7.3 ruby rpm does not come with Tk bundeled

from Rik Hemsley :
if you're using SuSE Linux 7.3, Ruby doesn't come with Tk support by default. I suppose this was to remove a dependency. You would have to compile Ruby yourself, after installing the Tk devel package and dependencies.

On binding for keyboard events

from Hidetoshi NAGAI:

If you want to do binding for keyboard events, you should set focus on the target widget. Therefore, 1st answer for the problem is, "set forcus on the canvas, and bind to the canvas for keyboard events."

   canvas.focus
   canvas.bind('d', proc{|x,y| TkcRectangle.new(canvas, x,y,x-5,y-5)}, 
               '%x %y')

f you don't want to set focus on the canvas widget for some reason, one of the ansers was shown by some people. And, I'll show the other.

The problem is that size of the canvas widget doesn't match to size of the root widget which gets keyboard events. Then, the answer is to make those sizes equal. It needs a little tricky way. That is to use two geometry managers, 'pack' and 'place', on the root widget. I'll show two variations for your 2nd example.

require 'tk'
root = TkRoot.new
root.geometry '400x300'
canvas = TkCanvas.new.place('relwidth'=>1.0, 'relheight'=>1.0)
menuline = TkFrame.new(root, 'relief'=>'raised', 'borderwidth'=>2) \
                  .pack('side'=>'top', 'fill'=>'x')
TkMenubutton.new(menuline) { |mb|
    text "Zoom"
    underline 0
    menu TkMenu.new(mb) {
      add 'command', 'label' => 'Zoom in',
        'underline' => 5,
         'command' => proc { p "in"; }
      add 'command', 'label' => 'Zoom out',
        'underline' => 5,
         'command' => proc { p "out" }
    }
    pack('side' => 'left', 'padx' => '1m')
   }
root.bind('d', proc{|x,y| TkcRectangle.new(canvas, x,y,x-5,y-5)}, '%x %y')
f = TkFrame.new(root, 'relief'=>'raised', 'borderwidth'=>2) \
           .pack('side'=>'bottom', 'fill'=>'x')
TkButton.new(f, 'text'=>'EXIT', 'command'=>proc{exit}).pack('side'=>'right')
Tk.mainloop
---------------------------------------
---------------------------------------
require 'tk'
root = TkRoot.new
canvas = TkCanvas.new.pack('fill'=>'both', 'expand'=>'true')
menuline = TkFrame.new(root, 'relief'=>'raised', 'borderwidth'=>2) \
                  .place('anchor'=>'nw', 'relx'=>0.0, 'rely'=>0.0, 
                         'relwidth'=>1.0)
TkMenubutton.new(menuline) { |mb|
    text "Zoom"
    underline 0
    menu TkMenu.new(mb) {
      add 'command', 'label' => 'Zoom in',
        'underline' => 5,
         'command' => proc { p "in"; }
      add 'command', 'label' => 'Zoom out',
        'underline' => 5,
         'command' => proc { p "out" }
    }
    pack('side' => 'left', 'padx' => '1m')
   }
root.bind('d', proc{|x,y| TkcRectangle.new(canvas, x,y,x-5,y-5)}, '%x %y')
f = TkFrame.new(root, 'relief'=>'raised', 'borderwidth'=>2) \
           .place('anchor'=>'sw', 'relx'=>0.0, 'rely'=>1.0, 
                  'relwidth'=>1.0)
TkButton.new(f, 'text'=>'EXIT', 'command'=>proc{exit}).pack('side'=>'right')
Tk.mainloop

Alarm; beeping;

from Guy Decoux:

Tk::bell

'pack' question

Answer by Mathieu Bouchard:
Trying to figure out how to do this *without* using nested frames. What I want: A window with (conceptually) "three rows":
1. label + entry field
2. label + entry field
3. three buttons
Now, I'm trying to figure out what combination of side/anchor/fill/expand (for these seven pack operations) will give me this.

That's because you don't understand what the pack() layout-manager allows. it allows two columns of widgets on top of each other, one from the top to the middle and one from the bottom to the middle, and a row of widgets on each side of that, from outer to inner. Therefore, you can only do this using 4 packs, or 1 grid, or 1 grid and 1 pack, depending on your aesthetics. Here's an example using 1 grid and 1 pack.

require 'tk'
root=TkRoot.new
2.times {|y| TkLabel.new(root) {text "hello"}.grid "column"=>0,"row"=>y }
2.times {|y| TkEntry.new(root) {width 20    }.grid "column"=>1,"row"=>y }
TkFrame.new(root) {
  3.times {|x| TkButton.new(self) {text x}.pack "side"=>"left" }
}.grid "column"=>0, "row"=>2, "columnspan"=>2
Tk.mainloop

Tk Popup Menu in a TkText widget

Ichimunki solved this puzzel. His mail on ruby-talk.

ed_menu = MakeMenu.editor_bind_menu( ed )
ed.bind('ButtonPress-3',
        proc{ |x,y| ed_menu.popup( x, y ) },
        "%X %Y"
        )

TK/IO performance problems

I'm experiencing some rather significant performance problems with IO.popen when embedding the logic in a Tk app.

MENON Jean-Francois suggested this solution on the ruby-talk mailinglist. The main problem, I suppose, is that threads are implemented in the ruby interpreter while Tk.mainloop is running in the binary library tcltk. the solution I found is to run Tk.mainloop, and then call the reads from inside the Tk.mainloop using Tk.After:

  inp=Thread.new(){
            timer=TkAfter.new(100,-1,proc {$igsClient.getlines})
        timer.start
     }
(100ms, -1 == loop)

Prevent a user from closing a window with the 'x' button

Thanks to Hidetoshi NAGAI for the answer: Try TkRoot/TkToplevel#protocol method (See Wm module).
require 'tk'
TkRoot.new.protocol('WM_DELETE_WINDOW', proc{p "Ouch!!\n"})
Tk.mainloop

Scrollbar causes crashing

Thx to matz for the answer:
|I'm having some problems with the following code. When I click on either 
|of the buttons on the scrollbar or move the thumb within the scrollbar 
|the program crashes out on me. The error message that follows is from 
|Windows 1.6.5-2 but the same error turns up on two seperate Linux 
|installs. As the code is mostly cut and paste from the Pickaxe book I am 
|somewhat baffled. BTW the Pickaxe code works fine but then it is not in 
|a class.
|
|Any pointers?

Since Ruby/Tk's "self swap" hack, you can't access instance variables
from the block to the "new" method.

|                scroll = TkScrollbar.new(root) {
|                        command proc { |*args| @text.yview *args}
|                        pack('side' => 'right', 'fill' => 'y')
|                }

"@text" in the block is referring the instance variable of the new
TkScrollbar.  You can avoid this by

!                text = @text = TkText.new(root) {
|                        width 60
|                        height 30
|                }
| 
|                scroll = TkScrollbar.new(root) {
!                        command proc { |*args| text.yview *args}
|                        pack('side' => 'right', 'fill' => 'y')
|                }

Binding Keys

Thx to Albert Wagner for the answer:
Here's a snippet from one of mine that works:

                # Keyboard and Button Bindings
                @doc.bind('Key-Tab')                    {|e| keyTab(e)}
                @doc.bind('Key-Return')                 {|e| keyTab(e)}
                #@doc.bind('Shift-Key-Tab')             {|e| keyLeftTab(e)} #Windows
                @doc.bind('Key-ISO_Left_Tab')   {|e| keyLeftTab(e)} #Linux

                @doc.bind('Key-Up')                     {|e| keyUpArrow(e)}
                @doc.bind('Key-Down')                   {|e| keyDownArrow(e)}

                @doc.bind('B1-ButtonRelease')   {|e| Button1Release(e)}

                @doc.bind('Control-Key-c')              {|e| clearCell(e)}
                @doc.bind('Control-Key-C')              {|e| clearCell(e)}
                @doc.bind('Control-Key-U')              {|e| undoCell(e)}
                @doc.bind('Control-Key-u')              {|e| undoCell(e)}

                @doc.bind('Key')                                {|e| anyKeyPress(e)}

List of available fonts

Thanks to Mat Gushee for the answer.

require "tkfont"
TkFont.families.each{|f| p f}

Opening a new window when selecting menu items

Thanks to kryptik for the answer.

filemenu.add('command', 'label'=>"Blah", 'command'=>proc {
  toplevelwindow = TkToplevel.new(root) {
    title "New window"
    geometry("50x50+100+100")
    configure('bg'=>"black")
  }

  toplevelbutton = TkButton.new(toplevelwindow) {
    text "Close"
    command proc { toplevelwindow.destroy }
  }.place('x'=>10, 'y'=>10)
}

GTK versus Tk

There's a good Wiki Entry on RubyGarden. GTKvsTk.

Test-First Programming

Simply leave out the call to Tk.mainloop and then call the Events yourself.
Example

Stay tuned, Phlip is writing a book on GUI TDD, using Ruby and the TkCanvas as the preferred example platform.


Articles/slides/tutorials.
Ruby slides.
Back to Approximity.