|
|||||||||||||
|
|||||||||||||
|
Using the Glade GUI Builder
Glade (http://glade.gnome.org) is a GUI building tool for GTK+ and GNOME. Its authors are Damon Chaplin and Martijn van Beers. You can obtain the source code for Glade from the Glade home page, and it comes as a standard installation option for many popular Linux distributions. This section does not cover the use of Glade in general, but good documentation is freely available. A plain text version of the Glade FAQ List is at http://glade.gnome.org/FAQ and the GNOME version of Glade includes on-line copies of the Glade FAQ List, Quick-Start Guide and Manual.
Glade’s project file (the .glade file) is an XML file that includes all of the information about the user interface. James Henstridge developed a supporting library, libglade, that allows you to read Glade project files and dynamically create your user interface at run-time. This is significant because it allows you to use Glade to design user interfaces for a number of programming languages that Glade doesn’t support directly. The home page for libglade is www.daa.com.au/~james/gnome, but like Glade itself, libglade comes as a standard installation option with most Linux distributions.
Ruby/LibGlade is an extension module, developed by Avi Bryant, that provides a wrapper for libglade. There is currently no home page for Ruby/LibGlade, but it is listed in the RAA and you should check there for the latest version and download site. The Ruby/LibGlade source code distribution includes installation and usage instructions, as well as a sample project file for testing purposes.
Ruby/LibGlade defines a single class, GladeXML. The new method for GladeXML takes the file name of the Glade project file and, optionally, the name of the root widget for the section of the user interface in which you’re interested. If you want to load the entire user interface, you can omit the second argument. Finally, GladeXML.new also expects an iterator-style code block, which it uses to associate signal handler names with Ruby procedures or methods. While libglade starts loading information about your user interface from the Glade project file, it calls this iterator code block for each handler name that it encounters. Your code block should return either a Ruby Proc or Method object, which provides the code used to handle that GTK+ signal. For example, a version that returns Proc objects would look like this:
GladeXML.new(‘myproject.glade’) { |handler_name|
case handler_name
when “on_button1_clicked”
proc { puts “Goodbye, World!”; exit }
when “on_button2_clicked”
proc { puts “button2 was clicked” }
end
}
If you’ve structured your code such that the Ruby methods that handle signals have the same names as the handler names you assigned in Glade, an even cleaner approach would be to use Ruby’s method Kernel#method to automatically return references to those handler methods:
def on_button1_clicked
puts “Goodbye, World!”
exit
end
def on_button2_clicked
puts “button2 was clicked”
end
GladeXML.new(‘myproject.glade’) { |handler_name|
method(handler_name)
}
The GladeXML class provides two other instance methods, getWidget and getWidgetByLongName. Both methods return a reference to a specific widget in the user interface, and both take a single string argument as input. The getWidget method takes the short name for a widget (for example, “button1”) while getWidgetByLongName takes the full widget path name (for example, “mainWindow.hbox.button1”).
Figure 2.10 shows a sample Glade session, with our original Hello, World! user interface consisting of a top-level window and a button as the child of that window. Of particular importance is the Properties window for the button widget. As shown in the Figure, we’ve added a signal handler for the button’s clickedsignal, and have named that signal handler “on_button1_clicked”. The names of the signal handlers that you assign in Glade are significant for connecting the user interface to Ruby code using Ruby/LibGlade.
Figure 2.10 A Sample Glade Session

After saving this Glade project to a file (say, helloworld.glade) we can write a short Ruby program that uses Ruby/LibGlade to display this user interface:
require ‘gtk’
require ‘lglade’
def on_button1_clicked
puts “Goodbye, World!”
exit
end
GladeXML.new(‘helloworld.glade’) { |handler_name|
method(handler_name)
}
Gtk::main
The first two lines of this program import the Ruby/GTK and Ruby/LibGlade extensions, respectively (where “lglade” is the feature name for Ruby/LibGlade). The next section of the program defines the on_button1_clicked method which we’ll use to handle the button’s clicked signal. The new code for Ruby/LibGlade comes next, when we create a GladeXML object for the helloworld.glade project and associate the handler name, “on_button1_clicked”, with the appropriate handler method, on_button1_clicked. Finally, as for all Ruby/GTK programs, the last line in the program kicks off the GTK+ main event loop. Figure 2.11 shows the results of running this program.
Figure 2.11 Resulting Ruby/GTK GUI Generated by Glade

Using the FOX Toolkit
Free Objects for X (FOX ) is a cross-platform GUI toolkit developed by Jeroen van der Zijp. Compared to Tk and GTK+, FOX is the new kid on the block, but it is quickly gaining recognition among software developers looking for a cross-platform GUI toolkit.
FXRuby is the Ruby extension module that provides an interface to FOX from Ruby programs. It was developed by Lyle Johnson and its home page is at http://fxruby.sourceforge.net.
Obtaining FOX and FXRuby
A prerequisite for programming with FXRuby is to have a working FOX installation. If you’re using the standard Ruby installation for Windows from the Pragmatic Programmers’ site, you can download a compatible precompiled binary distribution of FXRuby from the FXRuby home page. Further, the most recent versions of the Windows installer for Ruby even include FXRuby as a standard installation option. Regardless of which source you use, the shared library in this distribution already incorporates the FOX library, so after downloading and installing the distribution you’re ready to get started; There’s no need to download and build FOX separately.
If you’re running some other version of Ruby (including other non-Windows operating systems), you will most likely have some more work to do. Unlike Tk and GTK+, none of the Linux distributions include FOX as a standard installation package, and as of this writing, precompiled binaries for FOX aren’t available at all; so you’ll need to download, build and install FOX on your system. The FOX source code can be downloaded from the FOX home page (www.cfdrc.com/FOX/fox.html) and includes comprehensive build and installation instructions. For Linux and other Unix operating systems, the process is the standard configure, make and make install.
Once you have a working FOX installation, you can download the FXRuby source code from the FXRuby home page, and build and install that extension module. The build process for FXRuby begins by configuring the build by typing:
ruby setup.rb config
Then launch the build by typing:
ruby setup.rb setup
Once the build is completed, you can install FXRuby by typing:
ruby setup.rb install
For more detailed instructions about the build and installation options, check the FXRuby documentation.
FXRuby Basics
FXRuby’s API follows FOX’s C++ API very closely and, for the most part, you should be able to use the standard FOX class documentation to learn about the FXRuby class hierarchy and interfaces. All of the FXRuby classes, methods and constants live in a single Ruby module named Fox, and most of FXRuby is implemented in a C++ extension module whose feature name is “fox”. The minimal FXRuby program would look something like this:
require ‘fox’
include Fox
application = FXApp.new("Hello", "FoxTest")
application.init(ARGV)
main = FXMainWindow.new(application, "Hello", nil, nil, DECOR_ALL)
FXButton.new(main, "&Hello, World!", nil, application, FXApp::ID_QUIT)
application.create()
main.show(PLACEMENT_SCREEN)
application.run()
This program loads the Fox module by requiring the fox feature. Despite the fact that all FXRuby classes live in the Fox module’s namespace, the class names begin with the FX prefix to avoid clashes with other class names. It’s for this reason that most FXRuby applications can safely mix the Fox module’s contents directly into the global namespace (using the include Fox statement).
The program begins by creating an FXApp object, with the application name “Hello” and the vendor name “FoxTest”. Of the toolkits that we’ll examine in this chapter, FOX is the only one that requires you to explicitly create and refer to an application object, which is used as a kind of central repository for global application resources (like default application fonts, colors and so on). It’s also the entity responsible for managing the event loop, as we’ll see toward the end.
The next step is to initialize the application. We call the application object’s init method and pass in the command line arguments (Ruby’s ARGV array), from which FOX can pick out selected meaningful options. For example, to enable FOX’s tracing output from your FXRuby application, you can specify the –tracelevel option on the command line:
ruby hello.rb –tracelevel 301
For more information about which command line options FOX recognizes, consult the FOX documentation.
It’s at this point that we finally get around to creating our first widget. The main window (named main) is an instance of the Fox::FXMainWindow class, and its new method expects a reference to the application object as well as a window title.
The next widget is a button (an instance of Fox::FXButton), and it’s created as a child of the main window. This button will display the string “Hello, World!” and the first letter will be underlined because we placed an ampersand character (“&”) before it. This is a special signal to the FXButton widget that this character should be underlined, and that the Ctrl+H accelerator key combination should have the same affect as directly clicking the button.
The fourth and fifth arguments to FXButton’s new method are significant in terms of how FOX processes user interface events. The fourth argument is a message target object (an instance of FXObject or one of its subclasses) and the fifth argument is a message identifier. In this case, the application is the message target for the button; any events generated on or by the button (button clicks, for example) will result in a message sent to the application. The message’s identifier serves to distinguish between similar but different messages that the target object might receive; any given object can be the message target for multiple widgets.
The server-side resources (like windows, fonts and cursors) are created during the call to FXApp’s create method. FOX’s distinction between the client side representation of objects (such as their instance variables) and server side resources associated with those objects is also unique to FOX. Once the windows and other resources have been created, the main window is shown centered onscreen and we enter the main event loop by calling the application’s run method.
Targets and Messages
FOX’s event model is based on the idea of application objects sending messages to other objects (their targets) when something interesting happens. It’s your job as the application developer to decide how a target responds to a message. Because this target-message system is inherently bidirectional, part of that response often involves sending a message back to the original sender.
Every widget in your application has the ability to send messages, but some messages are more meaningful than others; for example, you’ll probably want the application to respond in some way when the user types new text in a text field, or selects a new item from a tree list. You should specify a message target object for those widgets that you expect to send meaningful messages. Almost all FOX widgets allow you to specify their target as an argument to their constructors. For example, to construct a new FXList instance and specify that anObject is its target, you’d write:
myList = FXList.new(parent, numItems, anObject, …)
You can also change the target after the widget has been constructed by calling the widget’s setTarget method:
myList.setTarget(anotherObject)
Every message has a type that indicates its significance. Some messages represent low-level events generated by the window system; for example, the SEL_LEFTBUTTONPRESS message is sent when the left mouse button is pressed down while the SEL_LEFTBUTTONRELEASE message is sent when the same button is released. Other messages are generated by FOX and indicate more useful user interface events, like deleting text from a text widget or selecting a cell in a table. The message types are just integers (unlike the strings used for Ruby/Tk’s events or Ruby/GTK’s signals) but you’ll always use the symbolic constants with names of the form SEL_name.
One message type that you’ll encounter frequently in FOX applications is the SEL_COMMAND message; it generically means that the widget has just completed its primary action. For example, an FXButton widget will send a SEL_COMMAND message to its target after the user clicks on the button, while an FXTextField widget will send a SEL_COMMAND message after the user presses the Enter key in the text field or clicks outside of the text field.
It’s a common practice in FOX applications to make one object the target of multiple widgets’ messages. For example, your application might include multiple menu buttons for operations like “Open File”, “Save File”, and “Print File”, all sending their SEL_COMMAND messages to a single target. But you might wonder how that target object is able to distinguish between similar messages from different widgets. When the target receives a SEL_COMMAND message, how does it know which button sent the message? The answer is that each message includes an identifier (in addition to its type) to provide additional information about the source or significance of the message.
The message identifier is just an integer, usually represented by a symbolic constant that is defined by the receiver of the message. A class defines the different message identifiers it understands and, since FOX is an object-oriented toolkit, an object also understands all of the message identifiers defined by its ancestor classes. For example, since FXButton is a subclass of FXLabel, it inherits the message identifiers defined by FXLabel and FXLabel’s base class.
In order for an object to receive and respond to messages, it needs to register message handler functions for the different message types and identifiers. This registering is taken care of in the class initialize function, using the FXMAPFUNC method to associate a message type and identifier with the name of an instance method for that class. For example, if we wanted to catch the SEL_COMMAND message with an ID_OPEN_FILE identifier, and use the onOpenFile method to handle that message, we’d write:
FXMAPFUNC(SEL_COMMAND, ID_OPEN_FILE, “onOpenFile”)
Message handler functions (like our onOpenFile method) always take three arguments:
def onOpenFile(sender, sel, ptr)
… handle this message …
end
The first argument (sender) is just a reference to the sender of the message, which is some other object in your application. It’s often useful to know who sent the message, especially if part of the response involves sending a message back to the original sender.
The second argument (sel) is a number, sometimes referred to as the selector, which encodes both the message type and the message identifier. If necessary, you can extract the message type and identifier from the selector using the SELTYPE and SELID functions:
def onOpenFile(sender, sel, ptr)
messageType = SELTYPE(sel)
messageId = SELID(sel)
… handle this message …
end
The last argument passed to the message handler (ptr) contains message-specific data; the type of this data depends on both the sender and the message type. For example, when an FXTextField widget sends the SEL_COMMAND message to a target, the data sent along with the message is a string containing the text field’s contents. When an FXColorWell widget sends a SEL_COMMAND message to a target, however, the message data is an integer indicating the color well’s current color. Our sample application will demonstrate some of the kinds of messages that get sent during a FOX application, but for a complete listing you should consult the FOX reference documentation (available at the FOX home page).
Working with FOX’s Layout Managers
FOX’s choices of layout managers are very similar to those for Tk and GTK+, with a few differences. We’re going to focus on the four powerhouse layout managers: FXPacker, FXHorizontalFrame, FXVerticalFrame and FXMatrix. Like their counterparts in Tk and GTK+, these are the layout managers you’ll use most often in real-world applications. For information about how to use some of the more special-purpose layout managers (like FXSwitcher, FXSplitter and FX4Splitter) check the FOX documentation.
As in GTK+, FOX layout managers are themselves just invisible container widgets that hold one or more child widgets. They’re not strictly “invisible”, since you have some control over how the outer border of the container is drawn (its frame style), but we’re mostly interested in how they arrange their child widgets.
As with Tk, FOX widgets are always constructed by passing in the parent widget as the first argument. You can later reparent a child window (that is, move it from one parent window to another) but unlike in Ruby/GTK, a child window cannot exist without a parent.
Unlike either of the other toolkits, FOX child widgets specify their layout preferences (or layout “hints”) as part of their constructors. You can change a widget’s layout settings after it exists by calling its setLayout instance method, but it’s still a different model than those used in Tk and GTK+. As a FOX layout manager works its way through its unique layout strategy, it requests the layout hints from each of its children and uses those to assign the positions and sizes of those widgets.
We’ll start by looking at FXPacker since it is both the most general of all the layout managers and it serves as the base class for the other three we’ll cover (FXHorizontalFrame, FXVerticalFrame and FXMatrix). FXPacker uses roughly the same layout strategy as Tk’s packer, and the names of the layout hints reflect its heritage. The new method for FXPacker goes like this:
aPacker = FXPacker.new(parent, opts=0,
x=0, y=0, w=0, h=0,
pl=DEFAULT_SPACING, pr=DEFAULT_SPACING,
pt=DEFAULT_SPACING, pb=DEFAULT_SPACING,
hs=DEFAULT_SPACING, vs=DEFAULT_SPACING)
You might take a few moments to recover from the shock at seeing such a long argument list. On closer examination, it should give you some relief to see that all but the first of its arguments are optional and have reasonable default values. As you start taking a look at the new methods for other FOX widgets, you’ll see this pattern repeated: long argument lists with default values for most of the arguments. And in fact, most or all of these arguments can be changed after the widget has been constructed using its accessor methods, so that a call such as the following:
aPacker = FXPacker.new(parent, LAYOUT_EXPLICIT,
0, 0, 150, 80)
is equivalent to these four lines of code:
aPacker = FXPacker.new(parent) # accept default values
aPacker.width = 150 # fixed width (in pixels)
aPacker.height = 80 # fixed height (in pixels)
aPacker.layoutHints = LAYOUT_EXPLICIT # make sure width and height
# values are actually enforced!
But we need to say more about the meanings of the arguments. Let’s temporarily skip over the second argument (opts) and consider the remaining ones.
The x, y, w and h arguments are integers indicating the preferred position (in its parent’s coordinate system) and size for a widget, with the caveat that any of these parameters is ignored if we don’t also set the corresponding layout hint (LAYOUT_FIX_X, LAYOUT_FIX_Y, LAYOUT_FIX_WIDTH or LAYOUT_FIX_HEIGHT). These arguments show up in almost every widget’s new method and they are not unique to FXPacker.new. In the above code example, we used a shortcut option, LAYOUT_EXPLICIT, that simply combines the four LAYOUT_FIX options; this makes sense, since a layout that uses fixed positions and sizes for its child widgets will need all of these options set.
The next four arguments to FXPacker.new are the left, right, top and bottom padding values, in pixels. Padding refers to the extra space placed around the inside edges of the container; if the layout of a particular packer in your program looks like it could use some extra breathing room around its edges, try increasing the padding values from their default value of DEFAULT_SPACING (a constant equal to 4 pixels).
The last two arguments for FXPacker.new indicate the horizontal and vertical spacing, in pixels, to be placed between the packer’s children. As with the internal padding, this spacing defaults to four pixels.
Now let’s come back to the second argument for FXPacker.new, its options flag (opts). Most FOX widgets’ new methods use this value to turn on or off different bitwise flags describing their appearance or behavior. We’ve already seen that some of the flags include layout hints, hints from a child widget to its parent container about how it should be treated during the layout procedure. In addition to the LAYOUT_FIX hints, you can use:
· LAYOUT_SIDE_LEFT or LAYOUT_SIDE_RIGHT, and LAYOUT_SIDE_TOP or LAYOUT_SIDE_BOTTOM indicate which side(s) of the packing cavity this child widget should be packed against
· LAYOUT_FILL_X or LAYOUT_CENTER_X, and LAYOUT_FILL_Y or LAYOUT_CENTER_Y indicate how the child widget should make use of any leftover space assigned to it (should it grow to fill the space, or merely center itself in that space?)
There are two packer-specific options (or packing styles) that can be binary OR-ed into the optionsflag: PACK_UNIFORM_WIDTH and PACK_UNIFORM_HEIGHT. Similar to the “homogeneous” property for Ruby/GTK’s layout managers, these two options constrain the layout manager to assign equal widths (and/or heights) for its children. These packing styles apply to all of the packer’s child widgets and override their preferences (including LAYOUT_FIX_WIDTH and LAYOUT_FIX_HEIGHT). These options are more appropriate for the other layout managers derived from FXPacker, but you can use them with the general packer if you know what you’re doing.
We’ll consider the next two layout managers, FXHorizontalFrame and FXVerticalFrame, together since they’re so similar. As you might expect by now, these two arrange their child widgets horizontally and vertically, respectively. The new method for FXHorizontalFrame looks like this:
aHorizFrame = FXHorizontalFrame.new(parent, opts=0,
x=0, y=0, w=0, h=0,
pl=DEFAULT_SPACING,
pr=DEFAULT_SPACING,
pt=DEFAULT_SPACING,
pb=DEFAULT_SPACING,
hs=DEFAULT_SPACING,
vs=DEFAULT_SPACING)
By default, the child widgets for a horizontal frame are arranged from left to right, in the order they’re added. To request that a particular child should be packed against the right side of the horizontal frame’s cavity, pass in the LAYOUT_RIGHT layout hint. Vertical frames arrange their children from top to bottom by default, and the LAYOUT_BOTTOM hint can be used to alter this pattern.
The last layout manager we’ll review is FXMatrix, which arranges its children in rows and columns. The new method for FXMatrix is:
aMatrix = FXMatrix.new(parent, size=1, opts=0,
x=0, y=0, w=0, h=0,
pl=DEFAULT_SPACING, pr=DEFAULT_SPACING,
pt=DEFAULT_SPACING, pb=DEFAULT_SPACING,
hs=DEFAULT_SPACING, vs=DEFAULT_SPACING)
FXMatrix introduces two options, MATRIX_BY_ROWS and MATRIX_BY_COLUMNS, to indicate how the size argument for FXMatrix.new should be interpreted. For MATRIX_BY_ROWS (the default), size indicates the number of rows; the number of columns is ultimately determined by the total number of children for the matrix. In this configuration, the first size child widgets added to the matrix make up its first column: the first child becomes the first widget on the first row, the second child becomes the first widget on the second row, and so on. Alternately, the MATRIX_BY_COLUMNS option means that size is the number of columns and the number of rows varies.
Fox Sample Application
Figure 2.12 shows the complete source code for the FXRuby version of our sample application. The source code for this application appears on the CD accompanying this book, under the file name fox-xmlviewer.rb.
Figure 2.12 Source Code for Sample Application—FXRuby Version (fox-xmlviewer.rb)
#!/bin/env ruby
require "fox"
require "fox/responder"
require "nqxml/treeparser"
include Fox
class XMLViewer < FXMainWindow
include Responder
# Define message identifiers for this class
ID_ABOUT, ID_OPEN, ID_TREELIST =
enum(FXMainWindow::ID_LAST, 3)
def createMenubar
menubar = FXMenubar.new(self, LAYOUT_SIDE_TOP|LAYOUT_FILL_X)
filemenu = FXMenuPane.new(self)
FXMenuTitle.new(menubar, "&File", nil, filemenu)
FXMenuCommand.new(filemenu,
"&Open...\tCtl-O\tOpen document file.", nil, self, ID_OPEN)
FXMenuCommand.new(filemenu,
"&Quit\tCtl-Q\tQuit the application.", nil,
getApp(), FXApp::ID_QUIT, MENU_DEFAULT)
helpmenu = FXMenuPane.new(self)
FXMenuTitle.new(menubar, "&Help", nil, helpmenu, LAYOUT_RIGHT)
FXMenuCommand.new(helpmenu,
"&About FOX...\t\tDisplay FOX about panel.",
nil, self, ID_ABOUT, 0)
end
def createTreeList
listFrame = FXVerticalFrame.new(@splitter,
LAYOUT_FILL_X|LAYOUT_FILL_Y|FRAME_SUNKEN|FRAME_THICK)
@treeList = FXTreeList.new(listFrame, 0, self, ID_TREELIST,
(LAYOUT_FILL_X|LAYOUT_FILL_Y|
TREELIST_SHOWS_LINES|TREELIST_SHOWS_BOXES|TREELIST_ROOT_BOXES))
end
def createAttributesTable
tableFrame = FXVerticalFrame.new(@splitter,
LAYOUT_FILL_X|LAYOUT_FILL_Y|FRAME_SUNKEN|FRAME_THICK)
@attributesTable = FXTable.new(tableFrame, 5, 2, nil, 0,
(TABLE_HOR_GRIDLINES|TABLE_VER_GRIDLINES|
FRAME_SUNKEN|FRAME_THICK|LAYOUT_FILL_X|LAYOUT_FILL_Y))
end
def initialize(app)
# Initialize base class first
super(app, "XML Editor", nil, nil, DECOR_ALL, 0, 0, 800, 600)
# Set up the message map
FXMAPFUNC(SEL_COMMAND, ID_ABOUT, "onCmdAbout")
FXMAPFUNC(SEL_COMMAND, ID_OPEN, "onCmdOpen")
FXMAPFUNC(SEL_COMMAND, ID_TREELIST, "onCmdTreeList")
# Create the menu bar
createMenubar
@splitter = FXSplitter.new(self, LAYOUT_FILL_X|LAYOUT_FILL_Y)
# Create the tree list on the left
createTreeList
# Attributes table on the right
createAttributesTable
# Make a tool tip
FXTooltip.new(getApp(), 0)
end
# Create and show the main window
def create
super
show(PLACEMENT_SCREEN)
end
def loadDocument(filename)
@document = nil
begin
@document = NQXML::TreeParser.new(File.new(filename)).document
rescue NQXML::ParserError => ex
FXMessageBox.error(self, MBOX_OK, "Error",
"Couldn't parse XML document")
end
if @document
@treeList.clearItems()
populateTreeList(@document.rootNode, nil)
end
end
def populateTreeList(docRootNode, treeRootNode)
entity = docRootNode.entity
if entity.instance_of?(NQXML::Tag)
treeItem = @treeList.addItemLast(treeRootNode, entity.to_s, nil,
nil, entity)
docRootNode.children.each do |node|
populateTreeList(node, treeItem)
end
elsif entity.instance_of?(NQXML::Text) &&
entity.to_s.strip.length != 0
treeItem = @treeList.addItemLast(treeRootNode, entity.to_s, nil,
nil, entity)
end
end
def onCmdOpen(sender, sel, ptr)
dlg = FXFileDialog.new(self, "Open")
dlg.setPatternList([
"All Files (*)",
"XML Documents (*.xml)"])
if dlg.execute() != 0
loadDocument(dlg.getFilename())
end
return 1
end
def onCmdTreeList(sender, sel, treeItem)
if treeItem
entity = treeItem.getData()
if entity.kind_of?(NQXML::NamedAttributes)
keys = entity.attrs.keys.sort
@attributesTable.setTableSize(keys.length, 2)
keys.each_index { |row|
@attributesTable.setItemText(row, 0, keys[row])
@attributesTable.setItemText(row, 1, entity.attrs[keys[row]])
}
end
end
return 1
end
# About box
def onCmdAbout(sender, sel, ptr)
FXMessageBox.information(self, MBOX_OK, "About XMLViewer",
"FXRuby Sample Application")
return 1
end
end
if $0 == __FILE__
# Make application
application = FXApp.new("XMLViewer", "FoxTest")
# Open the display
application.init(ARGV)
# Make window
mainWindow = XMLViewer.new(application)
# Create the application windows
application.create
# Run the application
application.run
end
The FXRuby version of our sample application begins by requiring the fox and fox/responder features, which correspond to the main FOX library as well as the code used by widgets to register their message handler methods:
require "fox"
require "fox/responder"
require "nqxml/treeparser"
include Fox
class XMLViewer < FXMainWindow
include Responder
# Define message identifiers for this class
ID_ABOUT, ID_OPEN, ID_TREELIST =
enum(FXMainWindow::ID_LAST, 3)
As we’ll see shortly, we want the main window class (XMLViewer) to respond to three message identifiers: ID_ABOUT, which is related to the About… menu command; ID_OPEN, which is related to the Open… menu command, and ID_TREELIST, which is related to selections in the tree list. In order to register message handler functions for a class, you need to first mix-in the Responder module. The enum function is a helper provided by this module, and it simply constructs an array of integers beginning with its first input and continuing for a number of elements equal to its second argument. In this case, we make sure that the enumerated values begin with FXMainWindow::ID_LAST so that we don’t clash with any of FXMainWindow’s message identifiers. This is a standard programming idiom for FXRuby applications.
The first step in the initialize method is to initialize the base class (FXMainWindow):
super(app, "XML Editor", nil, nil, DECOR_ALL, 0, 0, 800, 600)
It’s important not to omit this step! Here, the second argument to FXMainWindow’s initialize method is the window title (“XML Editor”). The fifth argument is a set of flags that provide hints to the window manager about which window decorations (for example, a title bar or resize handles) should be displayed for this window. In our case, we’ll request all possible decorations, DECOR_ALL. The last two arguments specify the initial width and height for the main window.
The next step is to set up the message map for this object:
FXMAPFUNC(SEL_COMMAND, ID_ABOUT, "onCmdAbout")
FXMAPFUNC(SEL_COMMAND, ID_OPEN, "onCmdOpen")
FXMAPFUNC(SEL_COMMAND, ID_TREELIST, "onCmdTreeList")
The FXMAPFUNC method is mixed-in from the Responder module and its arguments are the message type, identifier, and message handler method name. The first call, for example, declares that if an XMLViewer object is asked to handle a SEL_COMMAND message with the message identifier XMLViewer::ID_ABOUT, it should do so by calling its onCmdAbout method.
The remainder of the initialize method sets up the contents and layout of the main window. The first point of interest is the application’s menu bar, created in the createMenubar method:
def createMenubar
menubar = FXMenubar.new(self, LAYOUT_SIDE_TOP|LAYOUT_FILL_X)
filemenu = FXMenuPane.new(self)
FXMenuTitle.new(menubar, "&File", nil, filemenu)
FXMenuCommand.new(filemenu,
"&Open...\tCtl-O\tOpen document file.", nil, self, ID_OPEN)
FXMenuCommand.new(filemenu,
"&Quit\tCtl-Q\tQuit the application.", nil,
getApp(), FXApp::ID_QUIT, MENU_DEFAULT)
helpmenu = FXMenuPane.new(self)
FXMenuTitle.new(menubar, "&Help", nil, helpmenu, LAYOUT_RIGHT)
FXMenuCommand.new(helpmenu,
"&About FOX...\t\tDisplay FOX about panel.",
nil, self, ID_ABOUT, 0)
end
An FXMenubar acts as a horizontally-oriented container for one or more FXMenuTitle widgets. The FXMenuTitle has a text string associated with it for the menu title (like ‘File’) as well as an associated popup FXMenuPane that contains one or more FXMenuCommand widgets. The text for the menu title can include an ampersand character (“&”) in front of one of the title’s characters; when this is present, that letter will be underlined and FOX will install a keyboard accelerator to activate that menu. For example, the FXMenuTitle for the File menu:
FXMenuTitle.new(menubar, "&File", nil, filemenu)
will display the text “File” with the “F” underlined, and the Alt+F keyboard combination can be used to post that menu. Similarly, the text for menu commands can contain special control characters and delimiters:
FXMenuCommand.new(filemenu,
"&Open...\tCtl-O\tOpen document file.", nil, self, ID_OPEN)
The ampersand in front of the “O” in “Open…” defines a hot key for that menu command; if the File menu is already posted, you can press the O key to activate the Open… command. The tab characters (“\t”) are recognized by FOX as field separators. The first field is the primary text displayed on the FXMenuCommand. The second field is an optional string indicating an accelerator key combination that can be used to directly access that command, even if the menu isn’t posted. The optional last field’s string is the status line help text for this menu command.
Although we don’t use any menu separators in this example, it is often helpful to add one or more horizontal separators to a pulldown menu to group together closely related menu commands. To add a menu separator to an FXMenuPane, simply create an instance of the FXMenuSeparator class in the desired position:
FXMenuSeparator.new(filemenu)
The left-hand side of the main window houses a tree listing of the XML document nodes; it’s created in the createTreeList method:
def createTreeList
listFrame = FXVerticalFrame.new(@splitter,
LAYOUT_FILL_X|LAYOUT_FILL_Y|FRAME_SUNKEN|FRAME_THICK)
@treeList = FXTreeList.new(listFrame, 0, self, ID_TREELIST,
(LAYOUT_FILL_X|LAYOUT_FILL_Y|
TREELIST_SHOWS_LINES|TREELIST_SHOWS_BOXES|TREELIST_ROOT_BOXES))
end
Because the FXTreeList class isn’t derived from FXFrame, it doesn’t directly support any of the frame style flags. In order to get a nice-looking sunken border around the tree list, we need to enclose it in some kind of FXFrame-derived container; here, we’re using an FXVerticalFrame.
From the third and fourth arguments passed to FXTreeList.new we can see that the message target for this FXTreeList is the main window (self) and its message identifier is XMLViewer::ID_TREELIST. As we saw in the initialize function when we were setting up the message map, a SEL_COMMAND message sent with this identifier should lead to the onCmdTreeList method being invoked:
def onCmdTreeList(sender, sel, treeItem)
if treeItem
entity = treeItem.getData()
if entity.kind_of?(NQXML::NamedAttributes)
keys = entity.attrs.keys.sort
@attributesTable.setTableSize(keys.length, 2)
keys.each_index { |row|
@attributesTable.setItemText(row, 0, keys[row])
@attributesTable.setItemText(row, 1, entity.attrs[keys[row]])
}
end
end
return 1
end
When FXTreeList sends a SEL_COMMAND to its message target, the message data (the third argument passed to the message handler method) is a reference to the selected tree item, if any. Assuming that the selected item (named treeItem) is not nil, we get a reference to the user data associated with this tree item. As we’ll see later, when we review the populateTreeList method that created these tree items, the user data associated with the tree items are references to the XML entities that the tree items represent. If the entity has attributes associated with it, we modify the table’s row count by calling its setTableSize method and then iterate over the attributes to update the table cells’ contents.
The attributes table on the right-hand side of the main window is created by the createAttributesTable method, and it consists of an FXTable widget, which is also enclosed in an FXVerticalFrame:
def createAttributesTable
tableFrame = FXVerticalFrame.new(@splitter,
LAYOUT_FILL_X|LAYOUT_FILL_Y|FRAME_SUNKEN|FRAME_THICK)
@attributesTable = FXTable.new(tableFrame, 5, 2, nil, 0,
(TABLE_HOR_GRIDLINES|TABLE_VER_GRIDLINES|
FRAME_SUNKEN|FRAME_THICK|LAYOUT_FILL_X|LAYOUT_FILL_Y))
end
For some of the other GUI toolkits we’ve looked at, the name table refers to a layout manager that arranges its children in rows and columns (what FOX calls the FXMatrix layout manager). For FOX, the FXTable is more of a spreadsheet-like widget. We’re creating an FXTable widget that initially has 5 visible rows and 2 visible columns, although, as we’ll see later, the table dimensions are changed dynamically while the program’s running.
The onCmdOpen method handles the SEL_COMMAND message generated when the user clicks the Open… menu command from the File menu:
def onCmdOpen(sender, sel, ptr)
dlg = FXFileDialog.new(self, "Open")
dlg.setPatternList([
"All Files (*)",
"XML Documents (*.xml)"])
if dlg.execute() != 0
loadDocument(dlg.getFilename())
end
return 1
end
We construct a new FXFileDialog object, initialize its pattern list and then display the dialog by calling its execute method. The execute method returns non-zero if the user pressed the OK button, and when this is the case, we query the filename entered by the user and call loadDocument to load that XML document:
def loadDocument(filename)
@document = nil
begin
@document = NQXML::TreeParser.new(File.new(filename)).document
rescue NQXML::ParserError => ex
FXMessageBox.error(self, MBOX_OK, "Error",
"Couldn't parse XML document")
end
if @document
@treeList.clearItems()
populateTreeList(@document.rootNode, nil)
end
end
If the XML parser raises an exception while trying to create the NQXML::Document object, we call the FXMessageBox.error singleton method to display a simple dialog box explaining to the user what happened. The first argument to FXMessageBox.error identifies the owner window for this dialog box, i.e. the window above which this message box should float until it’s dismissed. The second argument is a flag indicating which terminator buttons should be displayed on the message box; other options include MBOX_OK_CANCEL, MBOX_YES_NO, MBOX_YES_NO_CANCEL, MBOX_QUIT_CANCEL and MBOX_QUIT_SAVE_CANCEL. The FXMessageBox class supports a handful of other convenient singleton methods for displaying common messages (information, question and warning). If there were no errors, we clear the tree list’s contents and then build up the tree list’s contents with recursive calls to populateTreeList:
Parameter “self” in parens looks as if it is an argument to FXMessageBox.errordef populateTreeList(docRootNode, treeRootNode)
entity = docRootNode.entity
if entity.instance_of?(NQXML::Tag)
treeItem = @treeList.addItemLast(treeRootNode, entity.to_s, nil,
nil, entity)
docRootNode.children.each do |node|
populateTreeList(node, treeItem)
end
elsif entity.instance_of?(NQXML::Text) &&
entity.to_s.strip.length != 0
treeItem = @treeList.addItemLast(treeRootNode, entity.to_s, nil,
nil, entity)
end
end
Here, we’re using FXTreeList’s addItemLast method to add new tree items to the tree list. The first argument to addItemLast is a reference to a tree item, the parent for the item to be created; to create tree items at the top level, you can instead pass in nil for this argument. The second argument to addItemLast is the text to be displayed on the tree item and the third and fourth (optional) arguments are the “opened” and “closed” icons to be displayed alongside the tree item’s text. If no icons are provided, FXTreeList will draw either a plus or minus sign in a square as the default icons. The last argument to addItemLast is optional user data; it’s any Ruby object that you’d like to associate with the newly created tree item. In our case, we’ll store a reference to the XML entity that this tree item represents.
Figure 2.13 shows the FXRuby version of our sample application, running under Microsoft Windows 2000.
Figure 2.13 The FXRuby Version of the XML Viewer Application

Using the SWin / VRuby Extensions
SWin and VRuby are the two primary components of the VisualuRuby project. SWin is a Ruby extension module, written in C, that exposes much of the Win32 API to the Ruby interpreter. VRuby is a library of pure Ruby modules built on top of SWin, that provides a higher-level interface to the Win32 programming environment.
As is the case with many Ruby extensions, most of the documentation for SWin and VRuby is only available in Japanese. And although the example programs from the VisualuRuby Web site will give you some idea about what’s possible with SWin and VRuby, there’s an implicit assumption that you’re already familiar with basic Windows programming concepts. This section provides enough of an overview of the VRuby API to develop our sample application, but for a real appreciation of SWin/VRuby you need a stronger background in Windows programming. There are some excellent books on this subject, and although they may discuss the Win32 API from a C programming standpoint, they should help you to fill in a lot of the blanks about programming with VRuby as well. Along those lines, another invaluable reference is the Win32 API reference from the Microsoft Developer Network (MSDN) CDs and DVDs.
Obtaining SWin and VRuby
SWin and VRuby are included in the Ruby installer for Windows from the Pragmatic Programmers’ Web site. The source code for SWin can be downloaded from the VisualuRuby project home page, and you should be able to find precompiled binaries there as well.
VRuby Library Basics
Because there is so little English-language documentation available for SWin and VRuby, we’re going to take an additional section here to provide a brief overview of the VRuby library and the classes and modules it provides. Most of this information has been extracted from the source file comments, which themselves are pretty sketchy, but this may give you a head start when you set out to begin working with VRuby.
The VRuby library is organized as a set of Ruby source files installed under the vr package directory. Table 2.1 lists some of the most important files that you’ll be using in your applications (and the corresponding require statements). Note that most or all of the non-core files depend on the core classes and modules from vr/vruby.rb, so you probably won’t need to import this file explicitly.
Table 2.1 VRuby Library Contents
|
Description |
How to Import |
|
Core Classes and Modules |
require ‘vr/vruby’ |
|
Layout Managers |
require ‘vr/vrlayout’ |
|
Standard Controls |
require ‘vr/vrcontrol’ |
|
Common Controls |
require ‘vr/vrcomctl’ |
|
Multipane Windows |
require ‘vr/vrtwopane’ |
The core classes and modules for VRuby (Table 2.2) consist of very high-level base classes and mix-in modules that you won’t use directly in most programs. Two notable exceptions, however, are VRForm and VRScreen. VRForm is the base class used for top-level windows, such as your application’s main window. As we’ll see in the sample application, you typically want to derive your main window class from VRForm, possibly mixing in some other behaviors, and handling most customization in that class’ construct method.
Table 2.2 Core Classes and Modules for VRuby
|
Class/Module Name |
Description |
|
VRMessageHandler |
This module should be mixed in to any class that needs to receive Windows messages; it provides module methods acceptEvents and addHandler. |
|
VRParent |
This module provides module methods used by parent windows (windows that can function as containers of other child windows). It is mixed in to VRForm and VRPanel (among others) since they’re the most popular container classes you’ll use. |
|
VRWinComponent |
This is the base class of all windows. |
|
VRControl |
Base class for all controls (widgets); a subclass of VRWinComponent. |
|
VRCommonDialog |
This module offers several convenience functions for using common Windows dialogs; provides module methods openFilenameDialog, saveFilenameDialog, chooseColorDialog and chooseFontDialog. |
|
VRForm |
This is the base class for all top-level windows, like your application’s main window, and it mixes in the VRMessageHandler, VRParent and VRCommonDialog modules. |
|
VRPanel |
This is the base class for all child windows (like controls); a subclass of VRControl. |
|
VRDrawable |
This module should be mixed in to any class that needs to handle the paint message (indicating that the window’s contents need to be redrawn); that class should override the self_paint method. |
|
VRResizeable |
This module should be mixed in to any class that needs to intercept window resize events; that class should override self_resize method. |
|
VRUserMessageUseable |
This module should be mixed in to any class that needs to register user-defined messages with the operating system. |
|
VRScreen |
This class represents the Windows desktop, and provides methods to create and show new top-level windows and manage the event loop. VRuby defines exactly one global instance of this class, VRLocalScreen. |
Some of the Windows controls have existed since the pre-Windows 95 days, and this set has come to be known as the standard controls. The standard controls (like buttons, labels and text fields), are accessible when you require ‘vr/vrcontrol.rb’. Table 2.3 lists the classes exposed by this module.
Table 2.3 Standard Controls for VRuby
|
Class/Module Name |
Description |
|
VRStdControl |
This class (a subclass of VRControl) is just the base class of all the standard controls. |
|
VRButton |
This is a standard push button; you can assign the button’s text using the caption= instance method. |
|
VRGroupbox |
Group box |
|
VRCheckbox |
This is a check box control. You can set or remove the check mark using the check instance method, and test for its current setting using the checked? instance method. |
|
VRRadioButton |
This is a radio button, usually displayed as one of a group of radio buttons used to indicate mutually exclusive choices. Because it is a special case (a subclass) of VRCheckbox, it also supports the check and checked? instance methods. |
|
VRStatic |
This is a static text control, which simply displays a non-editable text label. The label text can be assigned using its caption= instance method. |
|
VREdit |
This is a single-line, editable text control. It provides a number of instance methods related to getting and setting the text, as well as working with the selected (highlighted) text and transferring text between the VREdit control and the Windows clipboard. |
|
VRText |
This is a multi-line editable text control. It’s similar to the single-line version (VREdit) but provides a number of additional instance methods related to navigating through the text. |
|
VRListbox |
This is a vertically-oriented list of strings from which the user can select one or more items. |
|
VRCombobox |
This is a drop-down list of strings, similar to the VRListbox but with the difference that only one string can be selected. |
|
VRMenu |
This is a menu bar, populated with pulldown menus, that typically appears along the top edge of the application’s main window. |
|
VRMenuItem |
This is a single menu item that appears in a VRMenu (for example, the Save As… command in an application’s File menu). You’ll usually create these implicitly when you call a menu’s set method. |
|
VRMenuUseable |
This module should be mixed in to any window class that needs to use menus; it provides the newMenu and setMenu module methods. |
|
VRBitmapPanel |
This is a special kind of panel used to display a (static) bitmap image. |
|
VRCanvasPanel |
This is a special kind of panel used to display drawable bitmap images; you specify its width and height and can then use GDI functions to draw into it. |
Windows 95 introduced a host of new user interface controls known as the common controls. These included controls such as the list views and tree views used extensively in the Windows Explorer. The VRuby classes representing common controls are listed in Table 2.4.
Table 2.4 Common Controls for VRuby
|
Class Name |
Description |
|
VRNotifyControl |
This class (a subclass of VRControl) is just the base class of all the common controls. |
|
VRListview |
This is an enhanced list control that can be configured to display its data in a variety of formats ( as icons with simple descriptions, or in a more detailed list form). The most familiar use of this control is in the Windows Explorer. |
|
VRTreeview |
This control, also commonly known as a tree list, is used to display hierarchically structured data. It’s a popular choice for many Windows programs, used, for example, in the Registry Editor utility. |
|
VRProgressbar |
This control is used during time-consuming operations to provide the user with some feedback on how far the operation has progressed and how much time is left before it’s completed. |
|
VRTrackbar |
This control consists of a slider and optional tick marks. It is useful when you want the user to select a discrete value from a range of values. |
|
VRUpdown |
This control consists of a pair of arrow buttons (usually one pointing up and one pointing down) that the user can use to increment or decrement some value in the application. It is almost always paired with a buddy window, such as a VREdit control, to display its current setting. |
|
VRStatusbar |
This control is a horizontal strip that can appear at the bottom of the main window and is used to display status information. |
|
VRStatusbarDockable |
This module should be mixed in to a VRForm that will include a status bar control. It defines an addStatusbar method for adding a statusbar to the form. |
|
VRTabControl |
This control displays one or more tabs which can be used as the basis of a property sheet or tabbed panel. |
|
VRTabbedPanel |
This control combines the VRTabControl with a series of panels to create a tabbed notebook control. |
Layout Managers
In contrast to all the other GUI toolkits we’ve seen so far, VRuby doesn’t offer much in terms of layout managers. This is primarily due to the fact that the Win32 API on which SWin and VRuby are based doesn’t include any layout management at all. Although the layout managers currently packaged with VRuby are somewhat limited, one would expect this situation to improve as VRuby matures.
The three layout managers for VRuby are VRHorizLayoutManager, VRVertLayoutManager and VRGridLayoutManager. Instead of serving as standalone container classes, these layout managers are modules that you mix in to a VRuby container class like VRPanel. Like their counterparts in Ruby/GTK and FXRuby, the first two arrange their children either horizontally or vertically. To use them, first mix the desired layout manager module into your container class:
require ‘vr/vrlayout’
class MyPanelClass < VRPanel
include VRHorizLayoutManager
end
Then add one or more child widgets (or, in Windows-speak, controls) to the container using addControl:
aPanel = MyPanelClass.new ...
aPanel.addControl(VRButton, “button1”, “Caption for First Button”)
aPanel.addControl(VRButton, “button2”, “Caption for Second Button”)
The full argument list for VRHorizLayoutManager#addControl (or VRVertLayoutManager#addControl) is:
addControl(type, name, caption, style=0)
where type is the VRuby class for the control, name is its name, and caption is the text to display on the control (for controls that support captions). The last argument, style, can be used to pass in additional Windows style flags for this control. By default, VRuby will create a control with only the basic style flags; for example, VRButton controls are created with the WStyle::WS_VISIBLE and WStyle::BS_PUSHBUTTON flags. Unlike their counterparts in other GUI toolkits, you’d don’t really have any control over the child controls’ resizing parameters. For VRHorizLayoutManager, each child’s height is the same as the container’s height and the width of the container is equally divided amongst the child controls. Furthermore, each child is automatically stretched (horizontally and vertically) to fill its assigned space.
The VRGridLayoutManager roughly corresponds to Tk’s grid layout, GTK’s Gtk::Table and FOX’s FXMatrix, but with the same kinds of limitations of child control sizing that we saw for VRHorizLayoutManager and VRVertLayoutManager. Before adding any controls to a container using the VRGridLayoutManager layout manager, you should first set the number of rows and columns using VRGridLayoutManager#setDimension:
require ‘vr/vrlayout’
class MyPanel < VRPanel
include VRGridLayoutManager
end
aPanel = MyPanel.new ...
aPanel.setDimension(5, 3)
where the two arguments are the number of rows and number of columns, respectively. After that you can add child controls using VRGridLayoutManager#addControl:
addControl(type, name, caption, x, y, w, h, style=0)
Here, the first three arguments are the same as for the previous layout managers’ addControl methods. The next two arguments (x and y) are used to indicate the upper-left cell of the range of cells occupied by this control, and the w and h arguments indicate the width and height (in numbers of table cells) of the range. For a control that only takes up one table cell’s space, you’d use a width and height of one.
Event Handling
VRuby uses a callback-based approach for event handling that is quite unlike any of the others we’ve looked at. You may have wondered about why you specify a name for each control that you add to a form. For example, when adding a button control you might use:
aForm.addControl(VRButton, “button1”, “Caption for Button”, …)
The third argument is the caption string that is displayed on the button. The name string doesn’t appear to be used anywhere, but in fact, whenever you add a new control to a VRForm or VRPanel (actually, any class that mixes in the VRParent module), that container object also adds new instance variable and accessor methods with the name you specified in the call to addControl. This new instance variable is just a reference to the child control. So, after executing the previous line of code, you could later change the button’s caption with:
aForm.button1.caption = “New Caption for Button”
The controls’ names are also used when they generate callbacks, and these callback methods must have names of the form:
def controlname_eventname
… handle this event …
end
So, for example, if you wanted to catch the clicked event for the button1 control, you’d need a method named button1_clicked:
def button1_clicked
puts “Button 1 was clicked!”
end
These callback methods must be defined as instance methods for the container window, that is, if we had created button1 as a child of aForm, button1_clicked would need to be an instance method of aForm. We’ll see a few examples of how this works in the sample application, but Tables 2.5 and 2.6 provide listings of the event names for all the VRuby controls you might use.
Table 2.5 Event Names for Standard Controls
|
Control Class |
Event Name(s) |
|
VRButton |
clicked, dblclicked |
|
VREdit |
Changed |
|
VRListbox |
Selchange |
|
VRCombobox |
Selchange |
Table 2.6 Event Names for Common Controls
|
Control Class |
Event Name(s) |
|
All Common Controls |
clicked, dblclicked, gotfocus, hitreturn, lostfocus, rclicked, rdblclicked |
|
VRListview |
itemchanged, itemchanging, columnclick, begindrag, beginrdrag |
|
VRTreeview |
selchanged, itemexpanded, deleteitem, begindrag, beginrdrag |
|
VRUpdown |
Changed |
|
VRTabControl |
Selchanged |
VRuby Sample Application
Figure 2.15 (found at the close of this section) shows the VRuby version of the sample application in its entirety. The source code for this application appears on the CD accompanying this book, under the file name vruby-xmlviewer.rb. It begins by importing the various VRuby library files that we’ll need:
require 'vr/vruby'
require 'vr/vrcontrol'
require 'vr/vrcomctl'
require 'vr/vrtwopane'
Next, we define the global constants that we’ll need to display message boxes later in the program:
# The values of these constants were lifted from <winuser.h>
MB_OK = 0x00000000
MB_ICONEXCLAMATION = 0x00000030
MB_ICONINFORMATION = 0x00000040
The Win32 API uses a large number of named constants (like MB_OK, MB_ICONEXCLAMATION and MB_ICONINFORMATION) to specify style flags for controls. In our case, the last argument of VRuby’s messageBox function specifies message box options such as the displayed buttons and icon. Since these named constants are not yet exposed by SWin/VRuby, you need to dig around in the Windows header files to determine the actual numeric values of those constants. As you might have guessed, this is one of those times that a Windows programming background is a must!
The next major block of code defines the XMLViewerForm class and its instance methods; this is the focal point of the program. Before we jump into this, let’s skip ahead for a moment and take a look at the last few lines of the program:
mainWindow = VRLocalScreen.newform(nil, nil, XMLViewerForm)
mainWindow.create
mainWindow.show
# Start the message loop
VRLocalScreen.messageloop
After defining the XMLViewerForm class (which is just a subclass of VRForm) we call VRLocalScreen’s newform method to create an instance of XMLViewerForm. Recall that VRLocalScreen is a special variable in VRuby; it’s the single, global instance of VRScreen and it loosely corresponds to the Windows desktop. The first two arguments to VRScreen#newform are the parent window and the style flags for this window. In our case, this is the application’s top-level main window and so we pass nil for the parent window. We also pass nil for the style flags, to indicate that we want the default style.
The call to mainWindow.create actually creates the real window backing this Ruby object. Since this is also a container window, the call to create triggers a call to mainWindow’s construct method (which we’ll see in a moment) to create its child controls and menus. The last step before entering the main event loop is to make the main window visible by calling show.
Now let’s go back and look at the code for our form class, XMLViewerForm. We begin by mixing-in two modules that add useful functionality to the basic VRForm:
include VRMenuUseable
include VRHorizTwoPane
The VRMenuUseable module gives us the newMenu and setMenu methods for setting up the application’s pulldown menus. The VRHorizTwoPane module adds the addPanedControl method and enables a horizontally-split paned layout for the main window contents. Next, we define the form’s construct method, which actually creates the menus and adds child controls to the main window:
def construct
# Set caption for application main window
self.caption = "XML Viewer"
# Create the menu bar
@menu = newMenu()
@menu.set([ ["&File", [ ["&Open...", "open"], ["Quit", "quit"] ] ],
["&Help", [ ["About...", "about"] ] ]
])
setMenu(@menu)
# Tree view appears on the left
addPanedControl(VRTreeview, "treeview", "")
# List view appears on the right
addPanedControl(VRListview, "listview", "")
@listview.addColumn("Attribute Name", 150)
@listview.addColumn("Attribute Value", 150)
end
The call to newMenu simply creates an empty menu and assigns it to our @menu instance variable; the call to set actually defines the menu’s contents. It’s a bit difficult to read, but set takes a single argument that is an array of arrays, one per pulldown menu. The sub-array for each pulldown menu is itself a two-element array. Consider the first nested array, corresponding to the File menu:
["&File", [ ["&Open...", "open"], ["Quit", "quit"] ] ]
The first array element is the name of the pulldown menu. By placing an ampersand (“&”) before the “F” in “File”, we can make the Alt-F keyboard combination an accelerator for this menu choice. The second element in this array is another array, this time of the different menu items. Each element in the menu items array is either a two-element array providing the caption and name for the menu item, or the caption and yet another array of menu items (for defining nested or cascading menus). All of these nested arrays were making me a little dizzy and so for this example I stayed with single-level (non-cascading) menus.
In the earlier section on event handling, we saw that the names of controls are significant because they are used in the names of VRuby’s callback methods. In the same way, the names you assign to menu items are significant; when the user clicks on one these menu items, VRuby will look for a callback method named name_clicked. Soon we’ll see that the XMLViewerForm class defines callback methods open_clicked, quit_clicked and about_clicked to handle these three menu commands.
After setting up the application’s menus in XMLViewerForm’s construct method, we add exactly two controls to our horizontal pane:
# Tree view appears on the left
addPanedControl(VRTreeview, "treeview", "")
# List view appears on the right
addPanedControl(VRListview, "listview", "")
Recall that the names of child controls added to a VRParent become the names of instance variables. We’ll take advantage of this to further configure the list view widget (named listview):
@listview.addColumn("Attribute Name", 150)
@listview.addColumn("Attribute Value", 150)
Next, let’s take a look at the three callback methods that handle the Open…, Quit and About… menu commands. Because the name associated with the Open… menu command was open, its callback method is named open_clicked:
def open_clicked
filters = [["All Files (*.*)", "*.*"],
["XML Documents (*.xml)", "*.xml"]]
filename = openFilenameDialog(filters)
loadDocument(filename) if filename
end
The openFilenameDialog method is a convenience function for displaying a standard Windows file dialog. It is a module method for the VRCommonDialog module, which is mixed in to the VRForm class, so you can call this method for any form. Its input is an array of filename patterns (or filters) that will be displayed in the file dialog and returns the name of the selected file (or nil if the user cancelled the dialog). The callback for the About… menu command is handled by the about_clicked method:
def about_clicked
messageBox("VRuby XML Viewer Example", "About XMLView",
MB_OK|MB_ICONINFORMATION)
end
The messageBox method is another kind of convenience function, and is actually an instance method of the SWin::Window class (a distant ancestor class of our form). As you can probably surmise, the three arguments are the message box’s message, its window title, and the style flags indicating which icon and terminating buttons to display. The callback for the Quit menu command is the easiest to understand, since it just calls Ruby’s exit method:
def quit_clicked
exit
end
Figure 2.14 shows the VRuby version of our sample application, running under Microsoft Windows 2000.
Figure 2.14 The VRuby Version of the XML Viewer Application

Figure 2.15 Source Code for Sample Application—VRuby Version (vruby-xmlviewer.rb)
require 'vr/vruby'
require 'vr/vrcontrol'
require 'vr/vrcomctl'
require 'vr/vrtwopane'
require 'nqxml/treeparser'
# The values of these constants were lifted from <winuser.h>
MB_OK = 0x00000000
MB_ICONEXCLAMATION = 0x00000030
MB_ICONINFORMATION = 0x00000040
class XMLViewerForm < VRForm
include VRMenuUseable
include VRHorizTwoPane
def construct
# Set caption for application main window
self.caption = "XML Viewer"
# Create the menu bar
@menu = newMenu()
@menu.set([ ["&File", [ ["&Open...", "open"], ["Quit", "quit"] ] ],
["&Help", [ ["About...", "about"] ] ]
])
setMenu(@menu)
# Tree view appears on the left
addPanedControl(VRTreeview, "treeview", "")
# List view appears on the right
addPanedControl(VRListview, "listview", "")
@listview.addColumn("Attribute Name", 150)
@listview.addColumn("Attribute Value", 150)
end
def populateTreeList(docRootNode, treeRootItem)
entity = docRootNode.entity
if entity.instance_of?(NQXML::Tag)
treeItem = @treeview.addItem(treeRootItem, entity.to_s)
@entities[treeItem] = entity
docRootNode.children.each do |node|
populateTreeList(node, treeItem)
end
elsif entity.instance_of?(NQXML::Text) &&
entity.to_s.strip.length != 0
treeItem = @treeview.addItem(treeRootItem, entity.to_s)
@entities[treeItem] = entity
end
end
def loadDocument(filename)
@document = nil
begin
@document = NQXML::TreeParser.new(File.new(filename)).document
rescue NQXML::ParserError => ex
messageBox("Couldn't parse XML document", "Error",
MB_OK|MB_ICONEXCLAMATION)
end
if @document
@treeview.clearItems()
@entities = {}
populateTreeList(@document.rootNode, @treeview.root)
end
end
def open_clicked
filters = [["All Files (*.*)", "*.*"],
["XML Documents (*.xml)", "*.xml"]]
filename = openFilenameDialog(filters)
loadDocument(filename) if filename
end
def quit_clicked
exit
end
def about_clicked
messageBox("VRuby XML Viewer Example", "About XMLView",
MB_OK|MB_ICONINFORMATION)
end
def treeview_selchanged(hItem, lParam)
entity = @entities[hItem]
if entity and entity.kind_of?(NQXML::NamedAttributes)
keys = entity.attrs.keys.sort
@listview.clearItems
keys.each_index { |row|
@listview.addItem([ keys[row], entity.attrs[keys[row]] ])
}
end
end
end
mainWindow = VRLocalScreen.newform(nil, nil, XMLViewerForm)
mainWindow.create
mainWindow.show
# Start the message loop
VRLocalScreen.messageloop
Other GUI Toolkits
There are a number of other GUI toolkits under development for Ruby, and as always, you should check the RAA for the latest word.
The Fast Light Toolkit (FLTK) (www.fltk.org) is a nice cross-platform GUI developed in part by Bill Spitzak. FLTK is very efficient in terms of memory use and speed, and provides excellent support for OpenGL-based applications as well. It is currently available for both Windows and X (Unix) platforms. A Ruby interface to FLTK is being developed by Takaaki Tateishi and Kevin Smith, and the home page for this effort is at http://ruby-fltk.sourceforge.net.
Qt (www.trolltech.com) is an excellent cross-platform GUI toolkit that has been ported to Unix, Microsoft Windows and, most recently, Mac OS X. It is the basis of the popular KDE desktop for Linux. The Ruby language bindings for Qt (http://sfns.u-shizuoka-ken.ac.jp/geneng/horie_hp/ruby/index.html) are developed by Nobuyuki Horie.
Apollo (www.moriq.com/apollo/index-en.html), developed by Yoshida Kazuhiro, is a project whose goal, in the author’s words, is to provide a “dream duet” of Delphi and Ruby. Delphi is a commercial application development environment from Borland/Inprise. The specific interest for Ruby GUI developers is in the Ruby extension that provides access to Delphi’s Visual Component Library (VCL). As of this writing, Delphi is only available on Windows, but very soon, Kylix (the Unix port of Delphi) should be available for Linux and other platforms.
Choosing a GUI Toolkit
It can be both a blessing and a curse to have so many options when choosing a GUI for your Ruby application. Ultimately, there is no magic formula to make this decision for you, but here are a few considerations to keep in mind:
· Upon which platforms will your application need to run? If you need support for the Macintosh, Tk is really your only choice at this time. Similarly, if you need to support platforms other than Microsoft Windows, you probably don’t want to develop the GUI using SWin/VRuby.
· If you do intend for your application to run on different platforms, do you prefer a uniform look-and-feel for the GUI, or would you rather have a native look-and-feel for each target platform? Tk provides a native look-and-feel on each platform, but “theme-able” toolkits like GTK can provide extremely customizable interfaces (possibly different from any platform’s native GUI). FOX provides a consistent look-and-feel for both Unix and Windows (but that look-and-feel is decidedly Windows-like).
· Software licensing issues can be a significant concern, especially if you’re developing commercial software applications. Most of the GUI toolkits for Ruby use some kind of open-source software license, but you should study their licenses carefully to understand the terms.
All other issues aside, the great intangible factor is how comfortable you are developing programs with a given toolkit. From a programmer’s standpoint, every GUI toolkit has its own unique character and feel and you may not be able to put a finger on just what it is that you like about a particular toolkit. If you have some free time, take the opportunity to learn more about all the toolkits we’ve introduced in this chapter before settling on the one or couple that you like best.
Summary
This chapter has taken you on a tour of some of the most popular GUI toolkits for Ruby. It’s good to have a number of options at your disposal, but to be an effective application developer it’s in your best interests to experiment with several GUI toolkits and then pick the one that seems like the best fit for what you’re trying to accomplish. Most GUI programming wisdom can’t be taught in a book; it requires some trial and error.
We started out by looking at Ruby/Tk, the standard for Ruby and a sentimental favorite for many application developers. Tk was one of the first cross-platform GUIs, and the easy application development afforded by Tcl/Tk opened up the world of GUI programming to a lot of programmers who were struggling with earlier C-based GUI libraries like Motif and the Windows Win32 API. Of all the toolkits we’ve looked at, it’s also usually true that Ruby/Tk will require the least amount of code to get the GUI up and running. This simplicity, however, is at the expense of more recent GUI innovations like drag and drop, or advanced widgets like spreadsheets and tree lists.
The next GUI toolkit we considered was Ruby/GTK. For developers who work primarily on the Linux operating system and are already familiar with GTK+ and GNOME-based applications in that environment, this is an obvious choice. The Ruby/GTK bindings are quite complete and there’s extensive online documentation to get you started, including tutorial exercises. The only drawback seems to be for Windows developers, where it’s sometimes difficult to get GTK+ and Ruby/GTK to work properly.
FXRuby is a strong cross-platform GUI toolkit for Ruby, and it works equally well under Unix and Windows. In addition to a full complement of modern widgets, FOX and FXRuby provide a lot of infrastructure for features like OpenGL-based 3-D graphics, drag and drop, and a persistent settings registry. In its relatively short time on the Ruby GUI scene, FXRuby has become one of the most popular GUI toolkits for Ruby. Its disadvantages can’t be ignored, however: due to its close conformance to the C++ library, FXRuby’s event handling scheme is awkward compared to that used by most other Ruby GUI toolkits. The lack of comprehensive user manuals and reference documentation for FOX and FXRuby is also a sore spot for many new developers.
Speaking of poor documentation, SWin/VRuby is a hard sell for anyone other than experienced Windows programmers. On the other hand, if your programming experience is such that you are already well-versed in the fine art of Win32 programming, and you’d like to transfer that knowledge to Ruby applications development on Windows, SWin and VRuby may be the right choice for you.
Finally, while these are four of the more popular choices, this is only the tip of the iceberg. There are a number of other choices for GUI toolkits and new ones may have appeared by the time you read this. Take the time to check the RAA as well as newsgroup and mailing list posts to learn about the latest developments.
Solutions Fast Track
Using the Standard Ruby GUI: Tk
· Tk is still the standard GUI for Ruby, and this alone is a compelling reason to consider Tk for your application’s user interface. It offers the path of least resistance in terms of distributing your Ruby applications, because you’re almost guaranteed that your end users will already have a working Ruby/Tk installation in place.
· The most serious problem with Tk is its lack of more modern widgets like combo-boxes, tree lists, and the like. While it’s true that Tk can be extended with third-party widget sets like BLT and Tix, and at least one Ruby extension module exists to take advantage of these Tk extensions, the build and installation efforts are non-trivial.
Using the GTK+ Toolkit
· Because it serves as one of the core components of the popular GNOME desktop for Linux, GTK+ development should be strong for the foreseeable future. The Ruby/GTK extension is likewise under ongoing development and already exposes most or all of the GTK+ functionality.
· One potential source of problems for GTK+ (and hence Ruby/GTK) is the weakness of the Windows port of GTK+, which typically lags behind the main X Window version. It is likely, however, that these problems will be sorted out at some point with the redesigned GTK+ 2.0.
Using the FOX Toolkit
· FOX provides an excellent cross-platform GUI solution, and unlike GTK+, it works very well out of the box on both Linux and Windows. In addition to its extensive collection of modern widgets, FOX offers built-in support for drag-and-drop, OpenGL, and a wide variety of image file formats.
· One drawback for choosing FOX is the lack of printed documentation. Most of the large chain bookstores (or online booksellers) will have a large selection of reference books for both Tk and GTK+, but you’re not going to find any books on FOX programming.
Using the SWin/VRuby Extensions
· SWin and VRuby provide a fast, native solution for developing graphical user interfaces on Windows. If you don’t need to run your Ruby application on non-Windows systems, or have some alternative user interface plan for those systems, this may be the right solution for you.
· Documentation is a bit of a problem when you’re getting started with these extensions, especially if you’re not already an experienced Windows programmer. It will probably help immensely to first educate yourself about the basics of Windows programming using one of the many fine reference books on Win32 programming.
Other GUI Toolkits
· We chose to cover a handful of popular GUI toolkits for Ruby in this chapter, but that shouldn’t discourage you from investigating any of the others that look interesting to you. You should pay attention to posts on the Ruby newsgroup and mailing list, and check the Ruby Application Archive (RAA) regularly, because you never know when new choices will become available.
Choosing a GUI Toolkit
· Although Ruby is a powerful programming language for any single platform, many programmers are drawn to it because of its cross-platform nature. If you want your GUI applications written in Ruby to be similarly cross-platform, you need to be mindful of the target platforms when choosing a GUI toolkit.
· The bottom line is that there is no one-size-fits-all solution when choosing a GUI toolkit. Instead of being swayed by the hype about one toolkit versus another, invest some time to try out two or three that look promising and decide for yourself which is the best fit.
Frequently Asked Questions
Q: Are any of the GUI toolkits for Ruby thread-safe?
A: The answer depends very much on your definition of “thread-safe.” In the most general sense, none of the GUI toolkits we’ve looked at are thread-safe; that is to say, the GUI objects’ instance methods don’t provide proper support for reentrancy. A good practice for multithreaded GUI applications is to let the GUI operate in the main thread and reserve non-GUI “worker” threads for background tasks whenever possible.
Q: Ruby/GTK and FXRuby come with some good example programs but I didn’t find much for Ruby/Tk. What’s a good source for additional Ruby/Tk example programs?
A: Check the Ruby Application Archives for the latest version of a set of “Ruby/Tk Widget Demos” maintained by Jonathan Conway. These are a Ruby/Tk port of the original Tcl/Tk widget demos and should give you a good head start on writing your own Ruby/Tk applications. You should also scan the Ruby Application Archives for other Ruby/Tk applications from which you can learn.
Q: I built and installed FOX, and then built and installed FXRuby, and both appeared to build without errors. When I try to run any of the FXRuby example programs under Linux, Ruby responds with an error message that begins “LoadError: libFOX.so: cannot open shared object file”. I checked the FOX installation directory and confirmed that libFOX.so is indeed present, so why does Ruby report this error?
A: The problem has to do with how the operating systems locate shared libraries that Ruby extensions like FXRuby depend on. Ruby is finding the FXRuby extension properly, but it cannot find the FOX shared library that FXRuby needs because it’s not in the standard path searched for dynamically-loaded shared libraries. To correct the problem you simply need to add the FOX library’s installation directory (usually, /usr/local/lib) to your LD_LIBRARY_PATH environment variable. See the FXRuby installation instructions for more information about this problem.
| Contact Us | E-mail Us | Site Guide | About PerfectXML | Advertise | | Privacy |