Programming Ruby: The Pragmatic Programmer's Guide

# Programming Ruby ### The Pragmatic Programmer's Guide
Previous < | Contents ^ | Next >

# Ruby Tk

The Ruby Application Archive contains several extensions that provide Ruby with a graphical user interface (GUI), including extensions for Tcl/Tk, GTK, OpenGL, and others.

The Tk extension is bundled in the main distribution and works on both Unix and Windows systems. To use it, you need to have Tk installed on your system. Tk is a large system, and entire books have been written about it, so we won't waste time or resources by delving too deeply into Tk itself, but instead concentrate on how to access Tk features from Ruby. You'll need one of these reference books in order to use Tk with Ruby effectively. The binding we use is closest to the Perl binding, so you probably want to get a copy of Learning Perl/Tk or Perl/Tk Pocket Reference .

Tk works along a composition model---that is, you start off by creating a container widget (such as a `TkFrame` or `TkRoot`) and then create the widgets that populate it, such as buttons or labels. When you are ready to start the GUI, you invoke `Tk.mainloop`. The Tk engine then takes control of the program, displaying widgets and calling your code in response to GUI events.

## Simple Tk Application

A simple Tk application in Ruby might look something like this:

    require 'tk'
    root = TkRoot.new { title "Ex1" }
    TkLabel.new(root) {
      text  'Hello, World!'
      pack  { padx 15 ; pady 15; side 'left' }
    }
    Tk.mainloop

Let's look at the code a little more closely. After loading in the `tk` extension module, we create a root-level frame using `TkRoot.new`. We then make a label widget as a child of the root frame, setting several options for the label. Finally, we pack the root frame and enter the main GUI event loop.

It's a good habit to specify the root explicitly, but you could leave it out---along with the extra options---and boil this down to a three-liner:

    require 'tk'
    TkLabel.new { text 'Hello, World!' }
    Tk.mainloop

That's all there is to it! Armed with one of the Perl/Tk books we reference at the start of this chapter, you can now produce all the sophisticated GUIs you need. But then again, if you'd like to stick around for some more details, here they come.

## Widgets

Creating widgets is easy. Take the name of the widget as given in the Tk documentation and add a `Tk` to the front of it. For instance, the widgets Label, Button, and Entry become the classes `TkLabel`, `TkButton`, and `TkEntry`. You create an instance of a widget using `new`, just as you would any other object. If you don't specify a parent for a given widget, it will default to the root-level frame. We usually want to specify the parent of a given widget, along with many other options---color, size, and so on. We also need to be able to get information back from our widgets while our program is running by setting up callbacks and sharing data.

### Setting Widget Options

If you look at a Tk reference manual (the one written for Perl/Tk, for example), you'll notice that options for widgets are usually listed with a hyphen---as a command-line option might be. In Perl/Tk, options are passed to a widget in a `Hash`. You can do that in Ruby as well, but you can also pass options using a code block; the name of the option is used as a method name within the block and arguments to the option appear as arguments to the method call. Widgets take a parent as the first argument, followed by an optional hash of options or the code block of options. Thus, the following two forms are equivalent.

    TkLabel.new(parent_widget) {
      text    'Hello, World!'
      pack('padx'  => 5,
           'pady'  => 5,
           'side'  => 'left')
    }
    # or
    TkLabel.new(parent_widget, text => 'Hello, World!').pack(...)

One small caution when using the code block form: the scope of variables is not what you think it is. The block is actually evaluated in the context of the widget's object, not the caller's. This means that the caller's instance variables will not be available in the block, but local variables from the enclosing scope and globals (not that you ever use those) will be. We'll show option passing using both methods in the examples that follow.

Distances (as in the `padx` and `pady` options in these examples) are assumed to be in pixels, but may be specified in different units using one of the suffixes ```c`'' (centimeter), ```i`'' (inch), ```m`'' (millimeter), or ```p`'' (point).

### Getting Widget Data

We can get information back from widgets with callbacks and by binding variables.

Callbacks are very easy to set up. The `command` option (shown in the `TkButton` call in the example that follows) takes a `Proc` object, which will be called when the callback fires. Here we use `Kernel::proc` to convert the `{exit}` block to a `Proc`.

    TkButton.new(bottom) {
      text "Ok"
      command proc { p mycheck.value; exit }
      pack('side'=>'left', 'padx'=>10, 'pady'=>10)
    }

We can also bind a Ruby variable to a Tk widget's value using a `TkVariable` proxy. We show this in the following example. Notice how the `TkCheckButton` is set up; the documentation says that the `variable` option takes a var reference as an argument. For this, we create a Tk variable reference using `TkVariable.new`. Accessing `mycheck.value` will return the string ```0`'' or ```1`'' depending on whether the checkbox is checked. You can use the same mechanism for anything that supports a var reference, such as radio buttons and text fields.

    mycheck = TkVariable.new

    TkCheckButton.new(top) {
      variable mycheck
      pack('padx'=>5, 'pady'=>5, 'side' => 'left')
    }

### Setting/Getting Options Dynamically

In addition to setting a widget's options when it's created, you can reconfigure a widget while it's running. Every widget supports the `configure` method, which takes a `Hash` or a code block in the same manner as `new`. We can modify the first example to change the label text in response to a button press:

    lbl = TkLabel.new(top) { justify 'center'
      text    'Hello, World!';
      pack('padx'=>5, 'pady'=>5, 'side' => 'top') }
    TkButton.new(top) {
      text "Cancel"
      command proc { lbl.configure('text'=>"Goodbye, Cruel World!") }
      pack('side'=>'right', 'padx'=>10, 'pady'=>10)
    }

Now when the `Cancel` button is pressed, the text in the label will change immediately from ```Hello, World!`'' to ```Goodbye, Cruel World!`''

You can also query widgets for particular option values using `cget`:

`require 'tk'`
`b = TkButton.new {`
`text "OK"`
`justify 'left'`
`border 5`
`}`
`b.cget('text')` | » | `"OK"`
`b.cget('justify')` | » | `"left"`
`b.cget('border')` | » | `5`

### Sample Application

Here's a slightly longer example, showing a genuine application---a ``pig Latin'' generator. Type in the phrase such as ``Ruby rules,'' and the ``Pig It'' button will instantly translate it into pig Latin.

    require 'tk'

    class PigBox
      def pig(word)
        leadingCap = word =~ /^A-Z/
        word.downcase!
        res = case word
          when /^aeiouy/
            word+"way"
          when /^([^aeiouy]+)(.*)/
            $2+$1+"ay"
          else
            word
        end
        leadingCap ? res.capitalize : res
      end

      def showPig
        @text.value = @text.value.split.collect{|w| pig(w)}.join(" ")
      end

      def initialize
        ph = { 'padx' => 10, 'pady' => 10 }     # common options
        p = proc {showPig}

        @text = TkVariable.new
        root = TkRoot.new { title "Pig" }
        top = TkFrame.new(root)
        TkLabel.new(top) {text    'Enter Text:' ; pack(ph) }
        @entry = TkEntry.new(top, 'textvariable' => @text)
        @entry.pack(ph)
        TkButton.new(top) {text 'Pig It'; command p; pack ph}
        TkButton.new(top) {text 'Exit'; command {proc exit}; pack ph}
        top.pack('fill'=>'both', 'side' =>'top')
      end
    end

    PigBox.new
    Tk.mainloop

Sidebar: Geometry Management
In the example code in this chapter, you'll see references to the widget method `pack`. That's a very important call, as it turns out---leave it off and you'll never see the widget. `pack` is a command that tells the geometry manager to place the widget according to constraints that we specify. Geometry managers recognize three commands:
Command | Placement Specification
`pack` | Flexible, constraint-based placement
`place` | Absolute position
`grid` | Tabular (row/column) position
As `pack` is the most commonly used command, we'll use it in our examples.

## Binding Events

Our widgets are exposed to the real world; they get clicked on, the mouse moves over them, the user tabs into them; all these things, and more, generate events that we can capture. You can create a binding from an event on a particular widget to a block of code, using the widget's `bind` method.

For instance, suppose we've created a button widget that displays an image. We'd like the image to change when the user's mouse is over the button.

    image1 = TkPhotoImage.new { file "img1.gif" }
    image2 = TkPhotoImage.new { file "img2.gif" }

    b = TkButton.new(@root) {
      image    image1
      command  proc { doit }
    }

    b.bind("Enter") { b.configure('image'=>image2) }
    b.bind("Leave") { b.configure('image'=>image1) }

First, we create two GIF image objects from files on disk, using `TkPhotoImage`. Next we create a button (very cleverly named ``b''), which displays the image `image1`. We then bind the ``Enter'' event so that it dynamically changes the image displayed by the button to `image2`, and the ``Leave'' event to revert back to `image1`.

This example shows the simple events ``Enter'' and ``Leave.'' But the named event given as an argument to `bind` can be composed of several substrings, separated with dashes, in the order modifier-modifier-type-detail. Modifiers are listed in the Tk reference and include `Button1`, `Control`, `Alt`, `Shift`, and so on. Type is the name of the event (taken from the X11 naming conventions) and includes events such as `ButtonPress`, `KeyPress`, and `Expose`. Detail is either a number from 1 to 5 for buttons or a keysym for keyboard input. For instance, a binding that will trigger on mouse release of button 1 while the control key is pressed could be specified as:

`Control-Button1-ButtonRelease`
or
`Control-ButtonRelease-1`

The event itself can contain certain fields such as the time of the event and the x and y positions. `bind` can pass these items to the callback, using event field codes. These are used like `printf` specifications. For instance, to get the x and y coordinates on a mouse move, you'd specify the call to `bind` with three parameters. The second parameter is the `Proc` for the callback, and the third parameter is the event field string.

    canvas.bind("Motion", proc{|x, y| do_motion (x, y)}, "%x %y")

## Canvas

Tk provides a Canvas widget with which you can draw and produce PostScript output. Here's a simple bit of code (adapted from the distribution) that will draw straight lines. Clicking and holding button 1 will start a line, which will be ``rubber-banded'' as you move the mouse around. When you release button 1, the line will be drawn in that position. Pressing button 2 on the mouse will dump out a PostScript representation of the drawing canvas, suitable for printing.

    require 'tk'

    class Draw
      def do_press(x, y)
        @start_x = x
        @start_y = y
        @current_line = TkcLine.new(@canvas, x, y, x, y)
      end
      def do_motion(x, y)
        if @current_line
          @current_line.coords @start_x, @start_y, x, y
        end
      end
      def do_release(x, y)
        if @current_line
          @current_line.coords @start_x, @start_y, x, y
          @current_line.fill 'black'
          @current_line = nil
        end
      end
      def initialize(parent)
        @canvas = TkCanvas.new(parent)
        @canvas.pack
        @start_x = @start_y = 0
        @canvas.bind("1", proc{|e| do_press(e.x, e.y)})
        @canvas.bind("2", proc{ puts @canvas.postscript({}) })
        @canvas.bind("B1-Motion", proc{|x, y| do_motion(x, y)}, "%x %y")
        @canvas.bind("ButtonRelease-1",
                     proc{|x, y| do_release (x, y)}, "%x %y")
      end
    end

    root = TkRoot.new{ title 'Canvas' }
    Draw.new(root)
    Tk.mainloop

A few mouse clicks, and you've got an instant masterpiece:

Missing screenshots/canvas.ps

``We couldn't find the artist, so we had to hang the picture....''

## Scrolling

Unless you plan on drawing very small pictures, the previous example may not be all that useful. `TkCanvas`, `TkListbox`, and `TkText` can be set up to use scrollbars, so you can work on a smaller subset of the ``big picture.''

Communication between a scrollbar and a widget is bidirectional. Moving the scrollbar means that the widget's view has to change; but when the widget's view is changed by some other means, the scrollbar has to change as well to reflect the new position.

Since we haven't done much with lists yet, our scrolling example will use a scrolling list of text. In the following code fragment, we'll start off by creating a plain old `TkListbox`. Then, we'll make a `TkScrollbar`. The scrollbar's callback (set with `command`) will call the list widget's `yview` method, which will change the value of the visible portion of the list in the y-direction.

After that callback is set up, we make the inverse association: when the list feels the need to scroll, we'll set the appropriate range in the scrollbar using `TkScrollbar#set`. We'll use this same fragment in a fully functional program in the next section.

    list_w = TkListbox.new(frame, 'selectmode' => 'single')

    scroll_bar = TkScrollbar.new(frame,
                      'command' => proc { |*args| list_w.yview *args })

    scroll_bar.pack('side' => 'left', 'fill' => 'y')

    list_w.yscrollcommand(proc { |first,last|
                                 scroll_bar.set(first,last) })

### Just One More Thing

We could go on about Tk for another few hundred pages, but that's another book. The following program is our final Tk example---a simple GIF image viewer. You can select a GIF filename from the scrolling list, and a thumb nail version of the image will be displayed. There are just a few more things we'd like to point out.

Have you ever seen an application that creates a ``busy cursor'' and then forgets to reset it to normal? There's a neat trick in Ruby that will prevent this from happening. Remember how `File.new` uses a block to ensure that the file is closed after it is used? We can do a similar thing with the method `busy`, as shown in the next example.

This program also demonstrates some simple `TkListbox` manipulations---adding elements to the list, setting up a callback on a mouse button release,[You probably want the button release, not the press, as the widget gets selected on the button press.] and retrieving the current selection.

So far, we've used `TkPhotoImage` to only display icons directly, but you can also zoom, subsample, and show portions of images as well. Here we use the subsample feature to scale down the image for viewing.

    require 'tk'

    def busy
      begin
        $root.cursor "watch" # Set a watch cursor
        $root.update # Make sure it updates  the screen
        yield # Call the associated block
      ensure
        $root.cursor "" # Back to original
        $root.update
      end
    end

    $root = TkRoot.new {title 'Scroll List'}
    frame = TkFrame.new($root)

    list_w = TkListbox.new(frame, 'selectmode' => 'single')

    scroll_bar = TkScrollbar.new(frame,
                      'command' => proc { |*args| list_w.yview *args })

    scroll_bar.pack('side' => 'left', 'fill' => 'y')

    list_w.yscrollcommand(proc { |first,last|
                                 scroll_bar.set(first,last) })
    list_w.pack('side'=>'left')

    image_w = TkPhotoImage.new
    TkLabel.new(frame, 'image' => image_w).pack('side'=>'left')
    frame.pack

    list_contents = Dir["screenshots/gifs/*.gif"]
    list_contents.each {|x|
      list_w.insert('end',x) # Insert each file name into the list
    }
    list_w.bind("ButtonRelease-1") {
      index = list_w.curselection[0]
      busy {
        tmp_img = TkPhotoImage.new('file'=> list_contents[index])
        scale   = tmp_img.height / 100
        scale   = 1 if scale < 1
        image_w.copy(tmp_img, 'subsample' => [scale,scale])
        tmp_img = nil # Be sure to remove it, the
        GC.start      # image may have been large
      }
    }

    Tk.mainloop

Finally, a word about garbage collection---we happened to have a few very large GIF files lying about[They were technical documents! Really!] while testing this code. We didn't want to carry these huge images around in memory any longer then necessary, so we set the image reference to `nil` and call the garbage collector immediately to remove the trash.

## Translating from Perl/Tk Documentation

That's it, you're on your own now. For the most part, you can easily translate the documentation given for Perl/Tk to Ruby. But there are a few exceptions; some methods are not implemented, and there is undocumented extra functionality. Until a Ruby/Tk book comes out, your best bet is to ask on the newsgroup or read the source code.

But in general, it's pretty easy to see what's going on. Remember that options may be given as a hash, or in code block style, and the scope of the code block is within the `TkWidget` being used, not your class instance.

### Object Creation

    Perl/Tk:  $widget = $parent->Widget( [ option => value ] )
    Ruby:     widget = TkWidget.new(parent, option-hash)
              widget = TkWidget.new(parent) { code block }

You may not need to save the returned value of the newly created widget, but it's there if you do. Don't forget to pack a widget (or use one of the other geometry calls), or it won't show up.

### Options

    Perl/Tk:  -background => color
    Ruby:     'background' => color
              { background color }

Remember that the code block scope is different.

### Variable References

    Perl/Tk:  -textvariable => \$variable
              -textvariable => varRef
    Ruby:     ref = TkVariable.new
              'textvariable' => ref
              { textvariable ref }

Use `TkVariable` to attach a Ruby variable to a widget's value. You can then use the `value` accessors in `TkVariable` (`TkVariable#value` and `TkVariable#value=`) to affect the contents of the widget directly.

Previous < | Contents ^ | Next >

Extracted from the book "Programming Ruby - The Pragmatic Programmer's Guide"
Copyright © 2000 Addison Wesley Longman, Inc. Released under the terms of the Open Publication License V1.0.
This reference is available for download.