perfectxml.com
Focus Info Bank Free Library Software News About Us
  You are here: home »» Free Library » Syngress Publishing Saturday, 23 February 2008

Sample Chapter from the book Ruby Developer’s Guide . Reprinted by permission, Syngress Publishing © 2002.

Chapter 2

GUI Toolkits for Ruby

Solutions in this chapter:

·         Using the Standard Ruby GUI: Tk

·         Using the GTK+ Toolkit

·         Using the FOX Toolkit

·         Using the SWin/VRuby Extensions

·         Other GUI Toolkits

·         Choosing a GUI Toolkit

·         Summary

·         Solutions Fast Track

·         Frequently Asked Questions

Introduction

Although Ruby is an excellent tool for writing low-level scripts for system administration tasks, it is equally useful for writing end-user applications. And because graphical user interfaces (GUIs) are a must for modern end-user applications, you need to learn how to develop GUIs for Ruby. One of the benefits of Ruby programming that you’ve no doubt come to appreciate is that it enables rapid application development. In contrast to the time-consuming code-compile-test cycle of traditional programming languages, you can quickly make changes to your Ruby scripts to try out new ideas. This benefit becomes all the more evident when you start developing GUI applications with Ruby; it’s both instructive and rewarding to build up the user interface incrementally, adding new elements and then re-running the program to see how the user interface has changed as a result.

You may already know that the standard Ruby source code distribution includes an interface to Tk, which is an established cross-platform GUI toolkit. If you peruse the Ruby Application Archive (RAA) however, you’ll quickly discover that there is a large number of other GUI toolkit choices for use with Ruby (www.ruby-lang.org/en/raa.html). Why wouldn’t you want to stick with Tk if it’s the standard? Well, as you work through this chapter you’ll learn about some of the considerations that might prompt you to look at alternatives. Like all software, these packages are in various stages of development: some are new and unstable, while others are older and quite robust, but most fall somewhere in-between. Most of the GUI toolkits for Ruby are cross-platform, meaning that applications built around them will work on multiple operating systems, while others are targeted towards a single operating system.

Every GUI toolkit has its own unique feel and feature-set, but there are some things that are true of almost any GUI toolkit with which you may work:

·         GUI applications are event-driven. Many programs you’ll write proceed from start to finish in a very predictable path, and for a given set of inputs they’ll produce the same outputs with little or no user intervention. For example, consider a Ruby script written to process a large number of text files in batch mode, perhaps updating selected text in those files. Such a program will always produce the same results for a given set of input files, and it does so without any user intervention.

In contrast, event-driven programs spend most of their time waiting for user inputs (events) that drive the program’s flow.

·         Every toolkit has its own way of communicating these user interface events to your application code, which boils down at some point to your registering certain functions with the toolkit to “handle” the event.

·         GUI toolkits consist of a large number of basic user interface objects (called widgets), like buttons, labels and text fields, as well as more complex widgets, like spreadsheets, calendars or text editors.

·         User interface windows are constructed using a “parent-child” composition; the top-level main window contains one or more child windows; each child window may in turn contain child windows; and so on. This is an application of the Composite pattern, in that operations applied to parent windows (such as hiding it) usually affect the window’s children as well ( they are hiddenas well).

·         GUI toolkits offer one or more geometry (or layout) management options for arranging child windows (or widgets) inside other container windows.

The purposes of this chapter are to introduce some of the most popular GUI toolkits for Ruby, demonstrate how some of the common GUI programming idioms discussed above are implemented, and help you decide which might be the best choice for your applications. To do this, we’ll first describe a simple application that has a lot of features and functionality that you would use in any GUI application. Then we’ll take a look at how you would develop this application in each of the GUI toolkits. Along the way, we’ll discuss other related topics, such as how to download, compile and install the toolkit on your system, and auxiliary tools (such as GUI builders) that can make application development with that GUI toolkit easier. Sources for additional information and resources can be found in the discussion of each respective toolkit.

Using this Book’s Sample Applications

For each of the four major toolkits covered (Tk, GTK+, FOX and SWin/VRuby) we’ll develop a similar sample application so that you can easily identify the similarities and differences among the toolkits while learning how to use them. The application is a simple XML viewer that uses Jim Menard’s NQXML module as its document model, so you’ll need to obtain and install that extension before you can actually run the sample applications on your system. For your convenience, the CD accompanying this book provides the source code for each of the four sample applications:

·         tk-xmlviewer.rb is the Ruby/Tk version of the sample application;

·         gtk-xmlviewer.rb is the Ruby/GTK version of the sample application;

·         fxruby-xmlviewer.rb is the FXRuby version of the sample application; and,

·         vruby-xmlviewer.rb is the VRuby version of the sample application.

To give you a head start on developing your own GUI applications, the user interface for this application will demonstrate the following common features:

·         Displaying a menu bar, with several pull-down menus and choices for opening XML files or quitting the application.

·         Using common dialog boxes, like a file-open dialog, to request information from the user.

·         Using geometry-layout managers to automatically arrange widgets.

·         Using various widgets to display the XML document nodes (or entities) and their attributes.

Using the Standard Ruby GUI: Tk

The standard graphical user interface (GUI) for Ruby is Tk. Tk started out as the GUI for the Tcl scripting language developed by John Ousterhout in the mid-eighties, but has since been adopted as a cross-platform GUI by all of the popular scripting languages (including Perl and Python). Although Tk’s widget set is a bit limited as compared to some of the more modern GUIs, it has the unique distinction of being the only cross-platform GUI with a strong Mac OS port.

Obtaining Tk

One of the primary advantages of using Tk with Ruby is that, because it is the standard, it’s very easy to get started. Developing GUI applications with Ruby/Tk requires both a working installation of Tk itself as well as the Ruby/Tk extension module.

You’re welcome to download the source code for Tk from the Tcl/Tk home page at www.tcltk.org and build it yourself, but precompiled binaries for Tk are available for most operating systems (including Linux and Microsoft Windows). To make life even easier, if you’re running the standard Ruby for Windows distribution from the Pragmatic Programmers’ site (www.pragmaticprogrammer.com/ruby/downloads/ruby-install.html), you already have a working Tk installation. Similarly, most Linux distributions include Tcl/Tk as a standard installation option.

The other piece of the puzzle, the extension module that allows Ruby to access Tk, is included in the Ruby source code distribution. If you built Ruby from its source code, the Ruby/Tk extension was automatically built as well and should be installed along with the rest of the Ruby library on your system. The standard Ruby installer for Windows also includes the Ruby/Tk extension.

Ruby/Tk Basics

Ruby/Tk provides a number of classes to represent the different Tk widgets. It uses a consistent naming scheme and in general you can count on the class name for a widget being Tk followed by the Tk widget name. For example, Tk’s Entry widget is represented by the TkEntry class in Ruby/Tk.

A typical structure for Ruby/Tk programs is to create the main or “root” window (an instance of TkRoot), add widgets to it to build up the user interface, and then start the main event loop by calling Tk.mainloop. The traditional “Hello, World!” example for Ruby/Tk looks something like this:

require ‘tk’

 

root = TkRoot.new

button = TkButton.new(root) {

  text “Hello, World!”

  command proc { puts “I said, Hello!” }

}

button.pack

Tk.mainloop

The first line just loads the Ruby/Tk extension into the interpreter and the second line creates a top-level window for the application. Finally, we get to the interesting part:

button = TkButton.new(root) {

  text “Hello, World!”

  command proc { puts “I said, Hello!” }

}

Here we’re creating a button whose parent widget is the main window. Like all of the GUI toolkits we’ll look at, Ruby/Tk uses a composition-based model where parent widgets can contain one or more child widgets, some of which may themselves be containers. This code fragment also demonstrates one way to specify various configuration options for a widget by following it with a code block. Inside the code block, you can call methods that change aspects of the widget’s appearance or behavior; in this example, the text method is used to set the text displayed on the button, while the command method is used to associate a procedure with the button’s callback (more on this in the next section). An alternate (but equivalent) form for specifying widget options is to pass them as hash-style (key, value) pairs, for the second and following arguments to the widget’s new function, as follows:

button = TkButton.new(root, text => “Hello, World!”,

  command => proc { puts “I said, Hello!” })

The second line is important because it instructs the button’s parent container (the root window) to place it in the correct location. For this example that’s not too difficult, since the root window only has one child widget to deal with. As we’ll see later, real applications have much more complicated layouts with deeply nested structures of widgets contained within other container widgets. For those cases, we’ll pass additional arguments to the widget’s pack method to indicate where it should be placed in the parent container, how its size should change when its parent’s size changes, and other aspects related to the layout.

This example program “ends”, as most Ruby/Tk programs do, with a call to Tk.mainloop; but this is actually where the action begins. At this point, the program loops indefinitely, waiting for new user interface events and then dispatching them to the appropriate handlers. A lot of your work in developing GUI applications with Ruby/Tk is deciding which events are of interest and then writing code to handle those events; this is the topic of the following section.

Creating Responses to Tk’s Callbacks and Events

Tk’s event model is split along two closely-related lines. On one hand, the window system generates low-level events such as “the mouse cursor just moved into this window” or “the user just pressed the S key on the keyboard”. At a higher level, Tk invokes callbacks in your program to indicate that something significant happened to a widget (a button was clicked, for example). For either case, you can provide a code block or a Ruby Proc object that specifies how the application responds to the event or callback.

First, let’s take a look at how to use the bind method to associate basic window system events with the Ruby procedures that handle them. The simplest form of bind takes as its inputs a string indicating the event name and a code block that Tk uses to handle the event. For example, to catch the ButtonRelease event for the first mouse button on some widget, you’d write:

someWidget.bind(‘ButtonRelease-1’) {

  … code block to handle this event …

}

For some event types, it’s sufficient to use the basic event name, like “Configure” or “Destroy”, but for others you’ll want to be more specific. For this reason the event name can include additional modifiers and details. A modifier is a string like “Shift”, “Control” or “Alt”, indicating that one of the modifier keys was pressed. The detail is either a number from 1 to 5, indicating a mouse button number, or a character indicating a keyboard character. So, for example, to catch the event that’s generated when the user holds down the Ctrl key and clicks the right mouse button (sometimes known as Button 3) over a window, you’d write:

aWindow.bind(‘Control-ButtonPress-3’, proc { puts “Ouch!” })

The names of these events are derived from the names of the corresponding X11 event types, for mostly historical reasons; Tcl/Tk was originally developed for the Unix operating system and the X Window system. The Tk ports for Windows, Macintosh and other platforms use the same event names to represent their “native” windowing system events. The sample application we’ll develop later uses a few other event types, but for a complete listing of the valid event names, modifiers and details you should consult the manual pages for Tk’s bind command. A good online source for this kind of reference information is the Tcl/Tk documentation at the Tcl Developer Xchange Web site (http://tcl.activestate.com/doc).

It’s useful to be able to intercept these kinds of low-level events, but more often you’re interested in the higher-level actions. For example, you’d simply like to know when the user clicks on the Help button; you don’t really need to know that the user pressed the left mouse button down on the Help button and then, a few milliseconds later, released the mouse button. Many Ruby/Tk widgets can trigger callbacks when the user activates them, and you can use the command callback to specify that a certain code block or procedure is invoked when that happens. As with any other widget option, you can specify the command callback procedure when you create the widget:

helpButton = TkButton.new(buttonFrame) {

  text “Help”

  command proc { showHelp }

}

or you can assign it later, using the widget’s command method:

helpButton.command proc { showHelp }

Note that since the command method accepts either procedures or code blocks, you could also write the previous code example as:

helpButton = TkButton.new(buttonFrame) {

  text “Help”

  command { showHelp }

}

Some widgets, like TkCanvas, TkListbox and TkText, may not be able to display all of their contents in the space allotted to them. For example, if you’re using a TkText widget to display a very long document, at best you’ll only be able to see a screen’s worth of text at a time. For this reason you’ll typically attach TkScrollbar widgets to one or more sides of the main widget to allow the user to scroll through the widget’s contents. In order to properly interact with scrollbars, widgets like TkText or TkListbox can also generate callbacks when their contents are scrolled horizontally or vertically. To associate code blocks with these callbacks you can use the widget’s xscrollcommand or yscrollcommand methods (for horizontal or vertical scrolling). We’ll see an example of how this works in the sample application later in this chapter.

Working with Ruby/Tk’s Layout Managers

When you’re designing the behavior of your application, it’s critical to understand how Ruby/Tk’s events and callbacks work. An equally important aspect of the design is the layout of the user interface. Many widgets serve as interactive components upon which the user clicks on or types to get the program to perform an action. Other widgets, however, are more passive and should be used as containers (or parents) for yet other widgets. We’ve already seen that the top-level root window is one such container widget, and inside the main window you can use TkFrame widgets to group child widgets together in an organized way.

The layout manager for a container basically defines the strategy it’s going to use when assigning positions and sizes to its child widgets. As you’ll come to see, even after you understand how layout managers work, it takes some experimenting to translate your mental image of the user interface layout into code that correctly implements that layout. With Ruby/Tk, you can choose from three different layout managers, although you don’t have to use the same layout manager for every container. In fact, for non-trivial user interfaces it’s quite likely that you’ll use more than one of them.

The simplest layout manager is the placer, which just places the child widgets using the positions and sizes that you specify. At first glance, this might sound reasonable; after all, a lot of the “GUI builder” tools that let you drag widgets off of a tool palette and drop them on to a work window use this approach in the source code that they generate. The drawbacks of this fixed layout become apparent as soon as you try to run the program on other computers, with different configurations and possibly running other operating systems. For example, if the system fonts are different, a button that requires only 40 pixels’ width to display its text on your system might require 60 pixels on another system. If you had placed a text field at some fixed position immediately to the right of that button, those two widgets are now going to overlap each other. Because it’s so inflexible, you probably won’t use the placer much in practice.

The next layout manager is the grid layout manager, which places its child widgets in a table-like arrangement. When you add a child widget to a container using the grid layout manager, you specify the column and row in which it should be placed. The child widgets are arranged so that all of the widgets in the same column have the same width, and all of the widgets in the same row have the same height. For a quick example, here’s a grid layout with 3 rows and 4 columns of label widgets:

require 'tk'

root = TkRoot.new

3.times { |r|

  4.times { |c|

    TkLabel.new(root) {

      text "row #{r}, column #{c}"

    }.grid('row' => r, 'column' => c, 'padx' => 10, 'pady' => 10)

  }

}

Tk.mainloop

Figure 2.1 shows the results when you run this program. The grid layout manager is more powerful than this simple example suggests, however. You can see that by default, the grid centers each child widget in its cell, but you can provide additional arguments to the grid method to specify that one or more sides of the child widget stretches to “stick” to the side(s) of its cell. Additionally, grid cells can span multiple rows or columns. For more information about advanced options for the grid layout manager, consult your favorite Tk reference.

Figure 2.1 Grid Layout Manager

The last layout manager we’ll discuss is the packer, which is the one you’ll use most often because it is very flexible yet easy to use. The packer uses what is known as a “cavity” model for assigning the positions and sizes of its children. Imagine an empty frame widget, before any child widgets have been added to it. When you add the very first child widget to the frame, you specify which side (left, right, top or bottom) of the rectangular cavity against which the child widget should be packed (or aligned). The packer allocates that entire side of the cavity to this widget, and reduces the size of the cavity by the amount of space taken up by that first child. It’s important to note that the packer allocates the entire side of the cavity to the newly added widget, even if that widget doesn’t need the space. Successive child widgets are also packed against selected sides of the remaining cavity space until you’re done.

It’s also important to understand that the packer layout manager distinguishes between packing space and display space. The display space is the amount of space that a particular child widget would prefer to have to display itself properly. For example, a label or button widget’s display space is a little wider than the space needed to display the text on that label or button. The packing space is the entire space that’s available when positioning a widget in the cavity, which may be more or less than the display space for the widget.

When the amount of packing space exceeds the needed display space for a widget, the default behavior is to just center the child widget in that packing space, leaving gaps on the other sides. If you would instead like for the child widget to fill all the available packing space, you can set the fill parameter to one of three values (x, y or both), indicating the direction(s) in which the widget should fill. We’ll see examples of this later, in the sample application.

A related parameter, expand, has to do with how the child widgets resize themselves when the parent container (the packer) itself is resized. By default, expand is false, meaning that even if the parent container grows (or shrinks) the child widgets will maintain their current positions and sizes. If you instead set expand to true, the child widgets will resize according to the new size of the parent. In general, if you’ve specified fill => “both” for a particular child widget, you’ll also want to specify expand => true for that child widget.

It may help to work through a more concrete exercise to demonstrate how the packer’s layout algorithm works. Remember that we start out with an empty, rectangular cavity. Let’s start by adding a widget to the top side of the cavity (see Figure 2.2 [A]):

After this first step, the top section of the cavity is now claimed by the first child widget. Regardless of how we pack the remaining child widgets, this is the only one that can be adjacent to the top edge of the container; the bottom edge of this first widget has become the “top” of the remaining cavity space. Next, let’s add a widget to the left side of the cavity (B).

Once again, the remaining space in the cavity shrinks, this time by the width of the second widget. The bottom edge of the first widget is still the top of the cavity, but the right edge of this second widget becomes the new left side of the cavity. Now let’s add a third widget (C), this time to the bottom.

After adding this widget, the remaining space shrinks by the widget’s height and its top edge becomes the new bottom side of the cavity. Finally, add the last widget (D), this time to the right side.

Figure 2.2 Tk Packer Layout Manager

Ruby/Tk Sample Application

Figure 2.3 shows the source code for the Ruby/Tk version of our sample application. The source code for this application appears on the CD accompanying this book, under the file name tk-xmlviewer.rb.

Figure 2.3 Ruby/Tk Source Code for Sample Application (tk-xmlviewer.rb)

#!/usr/bin/env ruby

 

require 'tk'

require 'nqxml/treeparser'

 

class XMLViewer < TkRoot

  def createMenubar

    menubar = TkFrame.new(self)

    fileMenuButton = TkMenubutton.new(menubar,

                                      'text' => 'File',

                                      'underline' => 0)

    fileMenu = TkMenu.new(fileMenuButton, 'tearoff' => false)

 

    fileMenu.add('command',

                 'label' => 'Open',

                 'command' => proc { openDocument },

                 'underline' => 0,

                 'accel' => 'Ctrl+O')

    self.bind('Control-o', proc { openDocument })

 

    fileMenu.add('command',

                 'label' => 'Quit',

                 'command' => proc { exit },

                 'underline' => 0,

                 'accel' => 'Ctrl+Q')

    self.bind('Control-q', proc { exit })

 

    fileMenuButton.menu(fileMenu)

    fileMenuButton.pack('side' => 'left')

 

    helpMenuButton = TkMenubutton.new(menubar,

                                      'text' => 'Help',

                                      'underline' => 0)

    helpMenu = TkMenu.new(helpMenuButton, 'tearoff' => false)

 

    helpMenu.add('command',

                 'label' => 'About...',

                 'command' => proc { showAboutBox })

 

    helpMenuButton.menu(helpMenu)

    helpMenuButton.pack('side' => 'right')

    menubar.pack('side' => 'top', 'fill' => 'x')

  end

 

  def createContents

    # List

    listBox = TkListbox.new(self) {

      selectmode 'single'

      background 'white'

      font 'courier 10 normal'

    }

    scrollBar = TkScrollbar.new(self) {

      command proc { |*args|

        listBox.yview(*args)

      }

    }

    rightSide = TkFrame.new(self)

    attributesForm = TkFrame.new(rightSide)

    attributesForm.pack('side' => 'top', 'fill' => 'x')

    TkFrame.new(rightSide).pack('side' => 'top', 'fill' => 'both',

      'expand' => true)

    listBox.yscrollcommand(proc { |first, last|

      scrollBar.set(first, last)

    })

    listBox.bind('ButtonRelease-1') {

      itemIndex = listBox.curselection[0]

      if itemIndex

        # Remove currently displayed attributes

        TkGrid.slaves(attributesForm, nil).each { |slave|

          TkGrid.forget(attributesForm, slave)

        }

 

        # Add labels and entry widgets for this entity's attributes

        entity = @entities[itemIndex]

        if entity.kind_of?(NQXML::NamedAttributes)

          keys = entity.attrs.keys.sort

          keys.each_index { |row|

            TkLabel.new(attributesForm) {

              text keys[row] + ":"

              justify 'left'

            }.grid('row' => row, 'column' => 0, 'sticky' => 'nw')

            entry = TkEntry.new(attributesForm)

            entry.grid('row' => row, 'column' => 1, 'sticky' => 'nsew')

            entry.value = entity.attrs[keys[row]]

            TkGrid.rowconfigure(attributesForm, row, 'weight' => 1)

          }

          TkGrid.columnconfigure(attributesForm, 0, 'weight' => 1)

          TkGrid.columnconfigure(attributesForm, 1, 'weight' => 1)

        else

        end

      end

    }

 

    listBox.pack('side' => 'left', 'fill' => 'both', 'expand' => true)

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

    rightSide.pack('side' => 'left', 'fill' => 'both', 'expand' => true)

 

    @listBox = listBox

    @attributesForm = attributesForm

  end

 

  def initialize

    # Initialize base class

    super

 

    # Main Window Title

    title 'TkXMLViewer'

    geometry '600x400'

 

    # Menu bar

    createMenubar

    createContents

  end

 

  def populateList(docRootNode, indent)

    entity = docRootNode.entity

    if entity.instance_of?(NQXML::Tag)

      @listBox.insert('end', ' '*indent + entity.to_s)

      @entities.push(entity)

      docRootNode.children.each do |node|

        populateList(node, indent + 2)

      end

    elsif entity.instance_of?(NQXML::Text) &&

          entity.to_s.strip.length != 0

      @listBox.insert('end', ' '*indent + entity.to_s)

      @entities.push(entity)

    end

  end

 

  def loadDocument(filename)

    @document = nil

    begin

      @document = NQXML::TreeParser.new(File.new(filename)).document

    rescue NQXML::ParserError => ex

      Tk.messageBox('icon' => 'error', 'type' => 'ok',

                    'title' => 'Error', 'parent' => self,

                    'message' => "Couldn't parse XML document")

    end

    if @document

      @listBox.delete(0, @listBox.size)

      @entities = []

      populateList(@document.rootNode, 0)

    end

  end

 

  def openDocument

    filetypes = [["All Files", "*"], ["XML Documents", "*.xml"]]

    filename = Tk.getOpenFile('filetypes' => filetypes,

                              'parent' => self)

    if filename != ""

      loadDocument(filename)

    end

  end

 

  def showAboutBox

      Tk.messageBox('icon' => 'info', 'type' => 'ok',

        'title' => 'About TkXMLViewer',

        'parent' => self,

        'message' => 'Ruby/Tk XML Viewer Application')

  end

end

 

# Run the application

root = XMLViewer.new

Tk.mainloop

The first few lines simply import the required Tk and NQXML modules, and the majority of the code consists of the XMLViewer class definition. The main application window class, XMLViewer, is derived from TkRoot. Its initialize method looks like this:

def initialize

  # Initialize base class

  super

 

  # Main Window Title

  title 'TkXMLViewer'

  geometry '600x400'

 

  # Menu bar

  createMenubar

  createContents

end

The first line of initialize calls super to initialize the base class; don’t forget to do this! The next two lines call TkRoot’s title and geometry methods, respectively, to set the main window title string and its initial width and height in pixels. These two methods are actually provided by Ruby/Tk’s Wm module, which defines a number of functions for interacting with the window manager.

The last two lines of the initialize method call out to other XMLViewer methods to create the window’s menu bar and contents area. We could have included the code from these methods directly in the initialize method, but breaking up different parts of the GUI construction into different methods is a common way to organize larger, more complicated applications and so we’ll use it here for consistency. Unlike some of the other tookits we’ll look at, Ruby/Tk doesn’t have a specific class for the “menu bar”; instead, we just use a TkFrame container widget stretched along the top of the main window.

A Ruby/Tk pulldown menu consists of a TkMenubutton object associated with a TkMenu object. The TkMenubutton is the widget that you see on the menu bar itself; its text is the name of the menu, such as File, Edit or Help. When the user clicks this button, the associated TkMenu will be displayed. You can add one or more menu options to a TkMenu using its add method. Let’s look at the setup for our sample application’s File menu:

fileMenuButton = TkMenubutton.new(menubar,

                                  'text' => 'File',

                                  'underline' => 0)

Menu buttons are created as child widgets of the menu bar itself. The underline attribute for TkMenubutton widgets is an integer indicating which letter in the menu button title should be underlined. Underlining a letter in the menu title is a commonly-used visual cue in GUI applications to identify the accelerator key that can be used to activate the menu; for example, in most Windows applications the Alt+F keyboard combination activates the File menu.

Next, we’ll create the TkMenu associated with this TkMenubutton and add the first entry for that menu:

fileMenu = TkMenu.new(fileMenuButton, 'tearoff' => false)

 

fileMenu.add('command',

             'label' => 'Open',

             'command' => proc { openDocument },

             'underline' => 0,

             'accel' => 'Ctrl+O')

self.bind('Control-o', proc { openDocument })

Note that the TkMenu is created as a child of the menu button. Here we’re specifying that it’s not a tear-off style menu. The first entry we’ll add to the menu is a command entry, for the Open command. The first argument to add is a string indicating the type of the menu entry; in addition to command, there are types for checkbutton entries (check), radiobutton entries (radio), separators (separator), and cascading sub-menus (cascade). The command attribute for this menu entry is a Ruby Proc object that calls a different XMLViewer instance method, openDocument, which we’ll see shortly. Note that the accel attribute defines the keyboard accelerator string that is displayed alongside the Open menu entry but it doesn’t automatically set up the keyboard binding for that accelerator; we need to call bind on the main window to do this ourselves.

Now let’s take a look at the createContents method. This method sets up the main contents area of the application main window, which is divided into a listing of the XML document nodes on the left side and a listing of the node attributes on the right.

listBox = TkListbox.new(self) {

  selectmode 'single'

  background 'white'

  font 'courier 10 normal'

}

scrollBar = TkScrollbar.new(self) {

  command proc { |*args|

    listBox.yview(*args)

  }

}

listBox.yscrollcommand(proc { |first, last|

  scrollBar.set(first, last)

})

Tk’s list-box widget displays a list of strings that the user can select from. Our TkListbox instance sets the selectmode attribute to single, indicating that only one item can be selected at a time (other selection modes allow for multiple selections at the same time). We also set the font attribute to a fixed-pitch Courier font instead of the default GUI font, which is usually a system-dependent, proportionally-spaced font. Since the number of list items may become very large (too many to fit onscreen) we’ll also attach a TkScrollbar widget to use with this listbox. The code block or procedure passed to the scrollbar’s command method modifies the listbox’s “view”, the range of items displayed in its window. The number of parameters passed to the scrollbar’s command method varies depending on what the user does to the scrollbar. For example, if the user adjusts the scrollbar position by clicking on one of the arrow buttons, the command method will receive three arguments (scroll, 1, units). For more details about the different arguments that can be passed, see the Tk reference documentation for Tk’s scrollbar command. In general, you don’t need to concern yourself with which set of arguments get passed to the command method, and you simply pass them along to the scrolled widget’s xview or yview method. Similarly, the code block or procedure passed to the list’s yscrollcommand method can adjust the position and range of the scrollbar when the list contents are modified. The next section of code sets up the right-hand side of the main window:

rightSide = TkFrame.new(self)

attributesForm = TkFrame.new(rightSide)

attributesForm.pack('side' => 'top', 'fill' => 'x')

TkFrame.new(rightSide).pack('side' => 'top', 'fill' => 'both',

                            'expand' => true)

For now, this isn’t very interesting, because the attributes form is still empty. But the next section of code, which handles list selections, makes its purpose a bit clearer:

listBox.bind('ButtonRelease-1') {

  itemIndex = listBox.curselection[0]

  if itemIndex

    # Remove currently displayed attributes

    TkGrid.slaves(attributesForm, nil).each { |slave|

      TkGrid.forget(attributesForm, slave)

    }

    # Add labels and entry widgets for this entity's attributes

    entity = @entities[itemIndex]

    if entity.kind_of?(NQXML::NamedAttributes)

      keys = entity.attrs.keys.sort

      keys.each_index { |row|

        TkLabel.new(attributesForm) {

          text keys[row] + ":"

          justify 'left'

        }.grid('row' => row, 'column' => 0, 'sticky' => 'nw')

        entry = TkEntry.new(attributesForm)

        entry.grid('row' => row, 'column' => 1, 'sticky' => 'nsew')

        entry.value = entity.attrs[keys[row]]

        TkGrid.rowconfigure(attributesForm, row, 'weight' => 1)

      }

      TkGrid.columnconfigure(attributesForm, 0, 'weight' => 1)

      TkGrid.columnconfigure(attributesForm, 1, 'weight' => 1)

    end

  end

}

This entire code block is bound to the ButtonRelease event for the left mouse button on the list box. This is the event that will be generated when the user selects a list item by first pressing, and then releasing, the left mouse button over the list. We start by calling the TkListbox’s curselection method to get an array of the selected items’ indices; since this is a single-selection list, we only expect one selected item (as the zeroth element of this array). Next, we clear the current attributes form’s contents by calling TkGrid.slaves to get an array of the child widgets for the form, and then calling TkGrid.forget on each as we iterate through them. Whereas most GUI toolkits use the “parent-child” terminology to refer to the hierarchical composition of GUI containers, Tk often uses the terms “master” and “slave”, especially when referring to layout managers like TkGrid.

The next section of this event handler loops over all of the attributes for the currently selected XML document node, and adds a TkLabel and TkEntry to the form for each. Note that we can override the default justification for the TkLabel through its justify attribute; valid values for this attribute are left, center and right, but the default label justification is centered text. Also, since we’d like grid columns and rows to be equally weighted, we’re calling the TkGrid.rowconfigure and TkGrid.columnconfigure module methods to set the weight attribute for each row and column to 1.

We need to look at some of the lower-level methods for the XMLViewer class. For starters, there’s the openDocument method which is invoked when the user selects the Open entry from the File menu (or presses the Ctrl+O accelerator):

def openDocument

  filetypes = [["All Files", "*"], ["XML Documents", "*.xml"]]

  filename = Tk.getOpenFile('filetypes' => filetypes,

                            'parent' => self)

  if filename != ""

    loadDocument(filename)

  end

end

The important parts of this function are the call to Tk.getOpenFile and loadDocument. Tk.getOpenFile is a Tk module method that displays a system-specific file dialog box for selecting an existing file’s name; a similar function, Tk.getSaveFile, can be used to get either an existing or new file name when saving documents. The filetypes attribute specifies a list of file type descriptions and patterns, while the parent attribute specifies the owner window for the file dialog. Assuming the user didn’t cancel the dialog and provided a legitimate file name, we call the XMLViewer method loadDocument to actually read the XML document:

def loadDocument(filename)

  @document = nil

  begin

    @document = NQXML::TreeParser.new(File.new(filename)).document

  rescue NQXML::ParserError => ex

    Tk.messageBox('icon' => 'error', 'type' => 'ok',

                  'title' => 'Error', 'parent' => self,

                  'message' => "Couldn't parse XML document")

  end

  if @document

    @listBox.delete(0, @listBox.size)

    @entities = []

    populateList(@document.rootNode, 0)

  end

end

If the XML parser raises an exception while reading the XML file, we can display a simple dialog box stating this fact using the Tk.messageBox module method. The icon attribute can be one of the four strings error, info, question or warning, to provide a visual cue of the kind of message. The type attribute indicates which terminator buttons should be displayed on this message box. For our simple case, we’ll just display the OK button to dismiss the dialog, but other options for type include abortretrycancel, okcancel, retrycancel, yesno, and yesnocancel.

Assuming there were no errors in reading the document, the instance variable @document should now hold a reference to an NQXML::Document object. We call the delete method to erase all of the current list entries and then call populateList to start filling the list with the new document’s entities:

def populateList(docRootNode, indent)

  entity = docRootNode.entity

  if entity.instance_of?(NQXML::Tag)

    @listBox.insert('end', ' '*indent + entity.to_s)

    @entities.push(entity)

    docRootNode.children.each do |node|

      populateList(node, indent + 2)

    end

  elsif entity.instance_of?(NQXML::Text) &&

        entity.to_s.strip.length != 0

    @listBox.insert('end', ' '*indent + entity.to_s)

    @entities.push(entity)

  end

end

Other GUI toolkits we’ll cover in this chapter have tree widgets that are well-suited for displaying hierarchical data like an XML document. Tk doesn’t have such a widget, although Tix, a popular Tk extension does. For more information on Tix, see “Obtaining Tk Extensions: Tix and BLT” later in this section. Since Tix isn’t always available, we’ll approximate the tree widget using a regular TkListbox with the list items indented to indicate their depth in the tree. The populateList method is called recursively until the entire document is represented. In the earlier event handler code, we saw how the application handles selection of list items corresponding to different document entities.

Figure 2.4 shows the Ruby/Tk version of our sample application, running under Microsoft Windows 2000.

Figure 2.4 Ruby/Tk Version of Sample Application

Using the SpecTcl GUI Builder

SpecTcl (http://spectcl.sourceforge.net) is a GUI building tool for Tk. It was originally developed by Sun Microsystems and has more recently been developed and maintained by a group of volunteers led by Morten Jensen. Although the original intent of SpecTcl was to generate Tcl source code based on the user interface design, people have since developed code generation backends for other scripting languages (such as Perl, Python and Ruby) that use Tk as a GUI. An experimental version of the Ruby backend for SpecTcl, known as “specRuby,” was developed by Conrad Schneiker and is currently maintained by Jonathan Conway. Figure 2.5 shows a sample specRuby session and the layout of a simple Tk user interface, while Figure 2.6 shows the result when you test this GUI.

Figure 2.5 A Sample SpecTcl Session

Figure 2.6 Resulting Ruby/Tk GUI Generated by SpecTcl

There is currently no home page for specRuby, but it is listed in the RAA and you should check there for the latest version and download site. Note that since SpecTcl is a Tcl/Tk application, you will need a working Tcl/Tk installation on your system.

Obtaining Tk Extensions: Tix and BLT

In addition to Tk’s core widgets, there are many third-party widgets (and widget collections) that are compatible with Tk. While the level of effort required to obtain these Tk extensions can be intimidating, the end result is often a much more powerful toolkit than the one provided by Tk alone.

One of the most popular Tk-compatible extensions is Tix, the Tk Interface Extension (http://tix.sourceforge.net). Tix offers a hierarchical list (a.k.a. “tree” list) widget, a notebook widget and a combo-box, as well as others. You can always download the source code for Tix from the Tix home page, but be aware that in order to build Tix from its source code you also need to have downloaded and built Tcl/Tk from its source code, as the Tix build process uses these source code directories directly.

Another popular Tk extension is BLT (www.tcltk.com/blt). Like Tix, BLT adds a variety of new functionality, most notably for creating charts and graphs. You can download the source code or precompiled binaries for Windows from the BLT home page, and unlike Tix, the build procedure for BLT is the standard configure, make and make install cycle common to many free software programs.

To use Tix or BLT with Ruby/Tk, you’ll also need Hidetoshi Nagai’s TclTk-ext package; you should be able to find a download link for the latest version in the RAA. Note that as of this writing, all of the documents for this extension are in Japanese.

Using the GTK+ Toolkit

GTK+ is a cross-platform GUI originally developed for use with the GNU Image Manipulation Program (GIMP) toolkit (available at www.gimp.org). Although GTK+ is primarily designed for the X Window system, it has also been ported to Microsoft Windows. As a part of the GNU project, it is used for a lot of popular free software and is a core component of the GNU project’s GNU Network Object Model Environment (GNOME) desktop environment.

Ruby/GTK is a Ruby extension module written in C that provides an interface from Ruby to GTK+. This extension was originally developed by Yukihiro Matsumoto (the author of Ruby) and is currently maintained by Hiroshi Igarashi.

Obtaining Ruby/GTK

The home page for Ruby/GTK is www.ruby-lang.org/gtk. If you’re using the standard Ruby for Windows installation from the Pragmatic Programmers’ site, there’s a good chance that it includes precompiled binaries for Ruby/GTK. For other platforms (including Unix and Linux) you’ll need to build Ruby/GTK from the source code.

In order to build Ruby/GTK from the source code, you will need a working installation of GTK+ itself. Most Linux distributions include a GTK+ development package as an installation option; in Red Hat Linux, for example, this is the gtk+-devel package. If your operating system doesn’t already have a working GTK+ installation, the GTK+ home page (www.gtk.org) has plenty of information about how to download the source code and build it yourself. You can also download the sources for Ruby/GTK from its home page.

Once you’ve established a working GTK+ installation, you can download the Ruby/GTK sources from the Ruby/GTK home page. As of this writing, the latest version of Ruby/GTK is 0.25. The source code is distributed as a gzipped tar file named ruby-gtk-0.25.tar.gz and the build procedure is similar to that for other Ruby extensions. To extract this archive’s contents on a Unix system, type:

gzip –dc ruby-gtk-0.25.tar.gz | tar xf -

These commands create a new directory named gtk-0.25 (not ruby-gtk-0.25) containing the source code. To configure the build, change to the gtk-0.25 directory and type:

ruby extconf.rb

This command automatically generates the Makefile for this extension. To start compiling Ruby/GTK, just type:

make

The Ruby/GTK source code distribution includes several example programs that you can use to verify that it’s working properly. Once you’re satisfied that it’s working properly, you can install it by typing:

make install

Ruby/GTK Basics

Although the GTK+ library is written in C, its design is very object-oriented, and the Ruby/GTK extension reflects its class hierarchy. If you’re already familiar with the GTK+ C API and its widget names (GtkLabel, GtkButton, etc.) then the transition to programming with Ruby/GTK should be very smooth. GTK+ widget names of the form GtkWidgetName become Gtk::WidgetName in Ruby/GTK; that is, the Ruby module name is Gtk and the widget’s class name is WidgetName. Similarly, the Ruby/GTK instance methods are similar to the corresponding C function names; the C function gtk_label_set_text() becomes the Gtk::Label instance method set_text.

The minimal Ruby/GTK program will create a main window with one or more child windows, set up one more signal handlers and then start the main GTK+ event loop. Without further ado, we present the Ruby/GTK version of “Hello, World”:

require ‘gtk’

 

window = Gtk::Window::new

button = Gtk::Button::new("Hello, World!")

button.signal_connect(Gtk::Button::SIGNAL_CLICKED) {

  puts “Goodbye, World!”

  exit

}

window.add(button)

button.show

window.show

Gtk::main

The program begins by requiring the feature associated with Ruby/GTK; its name is “gtk”. Next we create two new widgets: a GtkWindow widget, which by default is a top-level “main” window; and a GtkButton widget with the label “Goodbye, World!”. Note that so far, there’s no connection between these two widgets; with Ruby/GTK, you compose complex user interfaces by explicitly adding child widgets to parent widgets.

The next line demonstrates one kind of event handler used by Ruby/GTK. The code inside the code block isn’t executed immediately; instead, Ruby/GTK associates this code block with the button’s Gtk::Button::SIGNAL_CLICKED signal which may be generated later, once the program’s running. We’ll get into this in more depth in the next section, but for the time being take it on faith that when this GtkButton widget is clicked, the program’s response will be to print the string “Goodbye, World!” to the standard output and then exit.

The next two lines are critical, and they’re somewhat unique to Ruby/GTK as far as the other toolkits are concerned. By default, newly-created Ruby/GTK widgets are not visible and you must explicitly make them visible by calling their show method. This step is easily forgotten by new Ruby/GTK programmers.

The last line of the program starts the GTK+ main event loop. At this point, GTK+ will wait for your inputs, paying special attention to those signals (like the button click) for which you’ve defined signal handler methods.

Now let’s take a more detailed look at some of these basics and see how they work in real programs.

Programming Signals and Signal Handlers

Ruby/GTK’s event model is based on the idea of user interface objects (widgets) emitting signals when something interesting happens. Some of these signals are low-level events generated by the window system, and indicate general information such as “the mouse moved” or “the left mouse button was clicked”. Other signals are synthesized by GTK+ itself and provide more specific information, such as “a list item was selected”. A widget can emit any number of signals, and every signal in Ruby/GTK has a name that indicates its significance. For example, a GtkButton widget emits a “clicked” signal when the button is clicked. Because GTK+ is an object-oriented toolkit, a given widget can emit not only its widget-specific signals, but also the signals emitted by all of its ancestor classes.

To associate a signal from a widget with some specific action, you can call the widget’s signal_connect method. This method takes a string argument indicating the signal name (like “clicked”) and a code block that will be evaluated in the caller’s context. For example, if your Ruby/GTK-based spreadsheet program should save the spreadsheet’s contents whenever the Save button is clicked, you might include the lines:

saveButton.signal_connect(‘clicked’) {

  saveSpreadsheetContents if contentsModified?

}

Each class defines symbolic constants for the names of the signals it can emit, and these can be used instead of the literal strings. For example, we could have written the above code snippet as:

saveButton.signal_connect(Gtk::Button::SIGNAL_CLICKED) {

  saveSpreadsheetContents if contentsModified?

}

The advantage of using the symbolic constants like Gtk::Button::SIGNAL_CLICKED instead of literal strings like clicked is that if you make a typographical error, you’re likely to discover the mistake much more quickly when you try to run your program. If you attempt to reference a constant and misspell its name, Ruby will raise a NameError exception; if you use a literal string, Ruby has no way to verify whether it is a valid signal name before passing it on to signal_connect.

As an application developer, it is up to you to decide which widgets’ signals are of interest, and how your program should react when those signals are emitted. Also note that if it makes sense for your application, you can connect the same signal (from the same widget) to more than one signal handler code block. In this case, the signal handlers are executed in the order they were originally connected.

Once we start developing the sample application, you’ll see a few more examples of how to connect signals to code blocks. For a complete listing of the signals emitted by different Ruby/GTK widgets, refer to the online API reference documentation at the Ruby/GTK home page (www.ruby-lang.org/gtk).

Working with Ruby/GTK’s Layout Managers

Like Ruby/Tk, Ruby/GTK offers a variety of flexible layout managers. Each of the three layout managers we’ll look at is a container widget to which you add one or more child widgets; the container itself is invisible for all practical purposes. The first two layout managers, the horizontal packing box (Gtk::HBox) and vertical packing box (Gtk::VBox) arrange their child widgets in a row or column, respectively. The third, Gtk::Table, arranges its child widgets in a tabular format like Ruby/Tk’s grid layout.

The horizontal packing box (Gtk::HBox) arranges its children horizontally. All of the child widgets will have the same height, but their widths can vary according to the packing box parameters. Conversely, Gtk::VBox arranges its children vertically, and all of them will have the same width. Since the two packing boxes are so similar, we’ll focus on Gtk::HBox.

The new method for Gtk::HBox takes two arguments:

hBox = Gtk::HBox.new(homogeneous=false, spacing=0)

The first argument is a Boolean value indicating whether the child widgets’ sizes are ”homogeneous” or not. If this argument is true (homogeneous), this simply means that the box will divide its width equally among its child widgets; if false (non-homogeneous), each child widget is allocated as much space as it needs, but no more. The second argument is the spacing (in pixels) placed between child widgets.

You add child widgets to a packing box using either its pack_start or pack_end instance methods. You may recall that in Ruby/Tk we always passed the parent widget in to as the first argument of the new function for its child widgets. Ruby/GTK takes a different approach: child widgets are created and then added (or packed) into container widgets. To pack child widgets into a Gtk::HBox or Gtk::VBox, you can call its pack_start method:

hBox.pack_start(child, expand=true, fill=true, padding=0)

The first argument to pack_start is just a reference to the child widget you want to add to this packing box; the last three arguments require some additional discussion. The second argument (expand) is an instruction to the packing box that this widget is willing to take extra space if some becomes available (e.g. if the user resizes the window to make it wider). We’ve already commented on the difference between homogeneous and non-homogeneous packing boxes; homogeneous packing boxes divide their space evenly among the child widgets. Another way to think of this is that each child widget will be allocated as much space as the widest child widget. A side effect of this setting is that the child widgets that would have preferred a narrower space are now centered in their assigned spot in the packing box. By passing true for the fill argument of pack_start, you can instruct that child widget to grow and fill its assigned space. The last argument to pack_start simply indicates the padding (in pixels) you’d like to place around this child widget; this is in addition to the spacing already specified for the packing box in Gtk::HBox.new. If this child widget is either the first or last widget in the packing box, this is also the amount of space that will appear between the edge of the packing box and this child.

Ruby/GTK also provides the Gtk::Table layout manager for arranging widgets in rows and columns. The new method for Gtk::Table takes three arguments:

table = Gtk::Table.new(numRows, numColumns, homogeneous=false)

The first two arguments are the number of rows and columns for the table, and the homogeneous argument has the same meaning as it did for Gtk::HBox and Gtk::VBox. To specify the spacing between rows and columns, use one of the four instance methods:

table.set_row_spacing(row, spacing)

table.set_row_spacings(spacing)

table.set_column_spacing(column, spacing)

table.set_column_spacings(spacing)

The set_row_spacings and set_column_spacings methods assign a global spacing amount (in pixels) that applies to all table rows (or columns). If you need more fine-grained control, you can use set_row_spacing or set_column_spacing to assign the space that should appear below a particular row (or to the right of a particular column). The spacing amounts specified by calls to set_row_spacing and set_column_spacing override the global spacing amounts specified by set_row_spacings and set_column_spacings.

To add child widgets to a Gtk::Table, use its attach method:

table.attach(child, left, right, top, bottom,

             xopts=GTK_EXPAND|GTK_FILL, yopts=GTK_EXPAND|GTK_FILL,

             xpad=0, ypad=0)

This appears more complex than the argument lists for pack_start and pack_end (and it is) but observe that the last four arguments have default values. The first argument is again a reference to the child widget to be added. The next four arguments (left, right, top and bottom) are integers that indicate where in the table to place this child and how many rows and columns it spans. To understand how these arguments are used, it’s better to think of the bounding lines of the table and its cells instead of the table cells themselves. For example, consider a table with 5 columns and 3 rows (see Figure 2.7). To draw this table, you’d need to draw 6 vertical lines (one on the left and right side of each of the 5 columns) and 4 horizontal lines (one on the top and bottom of the each of the 3 rows).

Figure 2.7 Edge Numbering for Sample GtkTable

 

With this picture in mind, the meanings of the left, right, top and bottom arguments of Gtk::Table.new are as follows:

·         Left indicates which vertical line of the table the left edge of this child widget is attached to.

·         Right indicates which vertical line of the table the right edge of this child widget is attached to.

·         Top indicates which horizontal line of the table the top edge of this child widget is attached to.

·         Bottom indicates which horizontal line of the table the bottom edge of this child widget is attached to.

For widgets that occupy only one table cell, the value for right will always be one more than the value for left and the value for bottom will always be one more than the value for top. But for a widget that occupies the fourth and fifth columns of the second and third rows of a table, you’d use something like:

table.attach(child, 3, 5, 1, 3)

If your user interface layout looks incorrect, double-check the values you’re passing to attach. Ruby/GTK will raise an exception if the arguments to attach would create a cell with zero width or height (i.e., if left is less than or equal to right, or top is less than or equal to bottom). But in many cases, Ruby/GTK will not raise an exception if the values for left, right, top or bottom are incorrect, even if they cause table cells to overlap one another.

The xopts and yopts arguments of Gtk::Table#attach specify how the table should allocate any additional space to this child widget. Valid values for xopts and yopts are GTK_EXPAND, GTK_FILL or GTK_EXPAND|GTK_FILL (both expand and fill). The meanings of these flags are the same as the corresponding parameters for the pack_start and pack_end methods for packing boxes. The final two arguments for Gtk::Table#attach specify the horizontal and vertical padding (in pixels) to apply around the outside of this widget, in addition to the spacing settings previously applied to the table in Gtk::Table.new.

Ruby/GTK Sample Application

Figure 2.8 shows the source code for the Ruby/GTK version of our sample application. The source code for this application appears on the CD accompanying this book, under the file name gtk-xmlviewer.rb.

Figure 2.8 Ruby/GTK Source Code for Sample Application (gtk-xmlviewer.rb)

require 'gtk'

require 'nqxml/treeparser'

 

class XMLViewer < Gtk::Window

  def initialize

    super(Gtk::WINDOW_TOPLEVEL)

    set_title('Ruby/Gtk XML Viewer')

    set_usize(600, 400)

 

    menubar = createMenubar

 

    @treeList = Gtk::Tree.new

    @treeList.show

   

    @columnList = Gtk::CList.new(['Attribute', 'Value'])

    @columnList.show

   

    bottom = Gtk::HBox.new(false, 0)

    bottom.pack_start(@treeList, true, true, 0)

    bottom.pack_start(@columnList, true, true, 0)

    bottom.show

 

    contents = Gtk::VBox.new(false, 0)

    contents.pack_start(menubar, false, false, 0)

    contents.pack_start(bottom, true, true, 0)

    add(contents)

    contents.show

   

    signal_connect(Gtk::Widget::SIGNAL_DELETE_EVENT) { exit }

  end

 

  def createMenubar

    menubar = Gtk::MenuBar.new

   

    fileMenuItem = Gtk::MenuItem.new("File")

    fileMenu = Gtk::Menu.new

 

    openItem = Gtk::MenuItem.new("Open...")

    openItem.signal_connect(Gtk::MenuItem::SIGNAL_ACTIVATE) {

      openDocument

    }

    openItem.show

    fileMenu.add(openItem)

 

    quitItem = Gtk::MenuItem.new("Quit")

    quitItem.signal_connect(Gtk::MenuItem::SIGNAL_ACTIVATE) { exit }

    quitItem.show

    fileMenu.add(quitItem)

 

    fileMenuItem.set_submenu(fileMenu)

    fileMenuItem.show

   

    helpMenuItem = Gtk::MenuItem.new("Help")

    helpMenu = Gtk::Menu.new

 

    aboutItem = Gtk::MenuItem.new("About...")

    aboutItem.signal_connect(Gtk::MenuItem::SIGNAL_ACTIVATE) {

      showMessageBox('About XMLViewer', 'Ruby/GTK Sample Application')

    }

    aboutItem.show

    helpMenu.add(aboutItem)

 

    helpMenuItem.set_submenu(helpMenu)

    helpMenuItem.show

   

    menubar.append(fileMenuItem)

    menubar.append(helpMenuItem)

    menubar.show

    menubar

  end

 

  def selectItem(entity)

    @columnList.clear

    if entity.kind_of?(NQXML::NamedAttributes)

      keys = entity.attrs.keys.sort

      keys.each { |key|

        @columnList.append([key, entity.attrs[key]])

      }

    end

  end

 

  def populateTreeList(docRootNode, treeRoot)

    entity = docRootNode.entity

    if entity.instance_of?(NQXML::Tag)

      treeItem = Gtk::TreeItem.new(entity.to_s)

      treeRoot.append(treeItem)

      if docRootNode.children.length > 0

        subTree = Gtk::Tree.new

       treeItem.set_subtree(subTree)

        docRootNode.children.each do |node|

          populateTreeList(node, subTree)

        end

      end

      treeItem.signal_connect(Gtk::Item::SIGNAL_SELECT) {

        selectItem(entity)

      }

      treeItem.show

    elsif entity.instance_of?(NQXML::Text) &&

          entity.to_s.strip.length != 0

      treeItem = Gtk::TreeItem.new(entity.to_s)

      treeRoot.append(treeItem)

      treeItem.signal_connect(Gtk::Item::SIGNAL_SELECT) {

        selectItem(entity)

      }

      treeItem.show

    end

  end

 

  def loadDocument(filename)

    @document = nil

    begin

      @document = NQXML::TreeParser.new(File.new(filename)).document

    rescue NQXML::ParserError => ex

      showMessageBox("Error", "Couldn't parse XML document")

    end

    if @document

      @treeList.children.each { |child|

        @treeList.remove_child(child)

      }

      populateTreeList(@document.rootNode, @treeList)

    end

  end 

 

  def openDocument

    dlg = Gtk::FileSelection.new('Open File')

    dlg.ok_button.signal_connect(Gtk::Button::SIGNAL_CLICKED) {

      dlg.hide

      filename = dlg.get_filename

      loadDocument(filename) if filename

    }

    dlg.cancel_button.signal_connect(Gtk::Button::SIGNAL_CLICKED) {

      dlg.hide

    }

    dlg.show

  end

 

  def showMessageBox(title, msg)

    msgBox = Gtk::Dialog.new

 

    msgLabel = Gtk::Label.new(msg)

    msgLabel.show

 

    okButton = Gtk::Button.new('OK')

    okButton.show

    okButton.signal_connect(Gtk::Button::SIGNAL_CLICKED) { msgBox.hide }

 

    msgBox.set_usize(250, 100)

    msgBox.vbox.pack_start(msgLabel)

    msgBox.action_area.pack_start(okButton)

    msgBox.set_title(title)

    msgBox.show

  end

end

 

if $0 == __FILE__

  mainWindow = XMLViewer.new

  mainWindow.show

  Gtk::main

end

Since most of the code is taken up by the XMLViewer class definitions, we’ll start by examining the  initialize method for that class.

XMLViewer is a subclass of Gtk::Window and so the first step is to initialize the base class by calling super. Note that the single argument to Gtk::Window.new is an optional integer indicating the window type; the default is Gtk::WINDOW_TOPLEVEL, but other valid values are Gtk::WINDOW_POPUP and Gtk::WINDOW_DIALOG. The next two lines set the window title and its initial width and height.

The next task is to create the application’s menu bar and pulldown menus; we’ve purposely put this into a separate method createMenubar to keep things clear. Creating menus in Ruby/GTK requires creating a Gtk::MenuBar widget and then adding one or more Gtk::MenuItem objects to it. A menu item can represent an actual menu command or it can be used to display a sub-menu of other menu items contained in a Gtk::Menu widget. This excerpt from the createMenubar method illustrates the key points:

fileMenuItem = Gtk::MenuItem.new("File")

fileMenu = Gtk::Menu.new

 

openItem = Gtk::MenuItem.new("Open...")

openItem.signal_connect('activate') { openDocument }

openItem.show

fileMenu.add(openItem)

 

fileMenuItem.set_submenu(fileMenu)

fileMenuItem.show

menubar.append(fileMenuItem)

The File menu item (fileMenuItem) is a Gtk::MenuItem instance whose purpose is to display a sub-menu (fileMenu) containing other menu items. We call the set_submenu method to create this association between fileMenuItem and fileMenu. In contrast, the Open… menu item (openItem) represents a command for the application; we connect its activate signal to the openDocument method, which we’ll see later.

Returning to the initialize method, we create and show the tree list widget:

@treeList = Gtk::Tree.new

@treeList.show

As well as the columned list widget:

@columnList = Gtk::CList.new(['Attribute', 'Value'])

@columnList.show

Here, we’re using a constructor for Gtk::CList that specifies an array of column titles; an alternate constructor allows you to simply specify the number of columns and then set their titles later using the set_column_title method.

The entire layout of the main window widgets is handled using a horizontal packing box nested in a vertical packing box. The horizontal packing box (named bottom) holds the Gtk::Tree widget on the left and the Gtk::CList on the right. The vertical packing box (named contents) holds the menu bar along its top edge, and the rest of the space is taken up by the horizontal packing box. Note that the arguments to pack_start for the menu bar direct the vertical packing box to not stretch the menu bar vertically, even if there is extra space to do so:

contents.pack_start(menubar, false, false, 0)

The last line of initialize sets up a signal handler for the main window itself. When the main window is “deleted” (usually, by the user clicking the X button in the upper right-hand corner of the window), GTK+ will fire off the Gtk::Widget::SIGNAL_DELETE_EVENT (a symbolic constant for the delete_event). We’d like to catch this event and exit the program at that time.

Digging down to the next level of the application, we need to look at the signal handlers for the menu commands; these were assigned when we created the menu items in the createMenubar method. We can quickly see that the Quit menu command simply exits the application:

quitItem = Gtk::MenuItem.new("Quit")

quitItem.signal_connect(Gtk::MenuItem::SIGNAL_ACTIVATE) { exit }

The About… menu command displays a little dialog box containing a message about the application:

aboutItem = Gtk::MenuItem.new("About...")

aboutItem.signal_connect(Gtk::MenuItem::SIGNAL_ACTIVATE) {

  showMessageBox('About XMLViewer', 'Ruby/GTK Sample Application')

}

Here, showMessageBox is a helper method for the XMLViewer class that displays a dialog with a specified title and message string, plus an OK button to dismiss the dialog:

def showMessageBox(title, msg)

  msgBox = Gtk::Dialog.new

 

  msgLabel = Gtk::Label.new(msg)

  msgLabel.show

 

  okButton = Gtk::Button.new('OK')

  okButton.show

  okButton.signal_connect(Gtk::Button::SIGNAL_CLICKED) { msgBox.hide }

 

  msgBox.set_usize(250, 100)

  msgBox.vbox.pack_start(msgLabel)

  msgBox.action_area.pack_start(okButton)

  msgBox.set_title(title)

  msgBox.show

end

A more general-purpose method would give you more control over the message box’s size, contents and layout, but this simple approach serves our purposes. As an aside, the GNOME library (built on top of GTK+) provides much more powerful and easy-to-use classes for putting message boxes and About boxes into your applications. You should be able to find information about Ruby bindings for GNOME at the Ruby/GTK home page.

The menu command that really gets things going, however, is the Open… command, which invokes XMLViewer’s openDocument method:

def openDocument

  dlg = Gtk::FileSelection.new('Open File')

  dlg.ok_button.signal_connect(Gtk::Button::SIGNAL_CLICKED) {

    dlg.hide

    filename = dlg.get_filename

    loadDocument(filename) if filename

  }

  dlg.cancel_button.signal_connect(Gtk::Button::SIGNAL_CLICKED) {

    dlg.hide

  }

  dlg.show

end

The new method for Gtk::FileSelection takes a single string argument indicating the title for the file-selection dialog box. We’re interested in catching the “clicked” signals for both the OK and Cancel buttons on this dialog, and we can use Gtk::FileSelection’s ok_button and cancel_button accessor methods to set up those signal handlers. In particular, we’d like to retrieve the file name selected by the user (by calling get_filename) and then load that XML document by calling XMLViewer’s loadDocument method:

def loadDocument(filename)

  @document = nil

  begin

    @document = NQXML::TreeParser.new(File.new(filename)).document

  rescue NQXML::ParserError => ex

    showMessageBox("Error", "Couldn't parse XML document")

  end

  if @document

    @treeList.children.each { |child|

      @treeList.remove_child(child)

    }

    populateTreeList(@document.rootNode, @treeList)

  end

end 

If the XML parser raises an exception while creating the NQXML::Document object, we’ll again use our showMessageBox helper method to alert the user to this error. Assuming the document loads successfully, we’ll clear out the tree’s previous contents and then refill it by calling populateTreeList. To clear the tree list’s contents, we make a call to the children method (inherited from Gtk::Container) which returns an array containing the top-level tree items. Then we iterate over the child items and remove each of them in turn by calling the tree list’s remove_child method.

The populateTreeList method calls itself recursively to build up the tree contents. The process of populating a Gtk::Tree widget is similar to the process for building pulldown menus that we saw in createMenubar. You can add Gtk::TreeItem objects to a Gtk::Tree and attach signal handlers to those items to receive notification when they are selected or deselected, expanded or collapsed, etc. But just as Gtk::MenuItem objects can have sub-menus associated with them (for cascading pulldown menus), Gtk::TreeItem objects can have sub-trees (that is, other Gtk::Tree objects) associated with them. In this excerpt from the populateTreeList method, we use this construct to model the XML document’s nested nodes:

    treeItem = Gtk::TreeItem.new(entity.to_s)

    treeRoot.append(treeItem)

    if docRootNode.children.length > 0

      subTree = Gtk::Tree.new

      treeItem.set_subtree(subTree)

      docRootNode.children.each do |node|

        populateTreeList(node, subTree)

      end

    end

Here, treeItem is a child of the current treeRoot (which is itself a Gtk::Tree instance). If we see that this XML entity has one or more child entities, we spin off a new Gtk::Tree instance (named subTree) and make this the sub-tree of treeItem by calling its set_subtree method.

Whenever a tree item is selected, we want to update the attributes list (our Gtk::CList object) on the right-hand side of the main window. To do this, we attach a handler to each of the tree items that handles the Gtk::Item::SIGNAL_SELECT signal by invoking our selectItem method:

def selectItem(entity)

  @columnList.clear

  if entity.kind_of?(NQXML::NamedAttributes)

    keys = entity.attrs.keys.sort

    keys.each { |key|

      @columnList.append([key, entity.attrs[key]])

    }

  end

end

This handler begins by clearing out the old list contents and then, if there are some attributes associated with the currently-selected XML entity, it appends list items for each of them.

Figure 2.9 shows the Ruby/GTK version of our sample application, running under the X Window system on Linux.

Figure 2.9 Ruby/GTK Version of the Sample Application



Goto Page 2 >>


  Contact Us | E-mail Us | Site Guide | About PerfectXML | Advertise ©2004 perfectxml.com. All rights reserved. | Privacy