Section 12.4. QtRuby


12.3. FXRuby (FOX)

The FOX system is also relatively new technology; its emphasis is on speed and consistency between platforms. Its extreme consistency is achieved in part by its self-reliance; it is not a wrapper for a native GUI, as some other systems are implemented.

At its heart, it is based on C++, though of course, bindings can be created for essentially any language (as they have been for Ruby). Because its internals are object-oriented from the start, it interfaces well with Ruby and is fairly naturally extensible.

FOX isn't as widespread as Tk or GTK+, but it is popular among Ruby programmers. Part of the reason for this is the excellent Ruby binding called FXRuby (hosted at http://fxruby.org). FXRuby is the work of Lyle Johnson, who has gone to great lengths to maintain and document this library; he also has provided excellent technical support over the years and offered invaluable assistance in the writing of this FXRuby section.

12.3.1. Overview

FXRuby is a Ruby binding to the FOX C++ library; it has many classes for developing full-featured GUI applications. Although FOX stands for Free Objects for X, it has been ported to a variety of platforms including MS Windows. Lyle Johnson created the Ruby binding to FOX as well as much of the Windows port of the FOX C++ toolkit itself. FOX was created by Jeroen van der Zijp with the support of CFD Research Corporation.

FOX widgets provide a modern look and feel. The widget choices rival native GUI interfaces, including MS Windows, and the toolkit also has features beyond many other widget libraries.

The FOX class library is clean and powerful, and can be learned easily by programmers familiar with most other GUI toolkits. Platform dependencies are not apparent in the API. Because FOX itself is implemented in C++, some aspects of the FXRuby API are still influenced by the static nature and programming conventions of C++ (for example, the use of enumerations and bit operations).

A central simplifying mechanism in FOX is the message/target paradigm. A FOX object is an instance of FXObject, or one of its subclasses. User-defined FOX objects must inherit from one of these classes. Every instance of FXObject can send and receive messages; a message is associated with a specific target at runtime, when the message is sent.

A message in FOX is represented internally by a message type, a message identifier, and some message data. Many of the FOX classes use a common set of message definitions to allow widgets to interoperate.

A message handler should return 1 to indicate that the message has been handled or 0 to indicate that it has not. FOX does not implicitly forward unhandled messages to other widgets. The return value is used by FOX to determine whether the GUI requires updating. An FXRuby application could use the return value to forward unhandled messages itself and thus implement the Chain of Responsibility pattern (as mentioned in the book Design Patterns; ISBN:0201633612 by the "Gang of Four"Gamma, Helm, Johnson, and Vlissides).

Another simplifying mechanism in FOX is the automatic update paradigm. The implicit FOX event loop includes an update phase where instances of FOX objects can handle update messages. An update handler is typically used to change the look and feel of a widget based on the new state of some application data. For example, Listing 12.9 later in this chapter has a button that updates its active/deactive status based on an application variable.

12.3.2. A Simple Windowed Application

Here is a minimal FXRuby application, the equivalent of the others you saw for Tk and GTK+ earlier:

require 'fox16'      # Use the FOX 1.6 bindings include Fox application = FXApp.new main = FXMainWindow.new(application, "Today's Date") str = Time.now.strftime("&Today is %B %d, %Y") button = FXButton.new(main, str) button.connect(SEL_COMMAND) { application.exit } application.create main.show(PLACEMENT_SCREEN) application.run


This application is enough to illustrate two fundamental classes of FXRuby: FXApp and FXMainWindow. An application must create and initialize an instance of FXApp before anything can be done with the other FOX classes. FXMainWindow is a subclass of FXTopWindow; every widget in FOX is a kind of "window." An FXTopWindow is a top-level window, or one that appears directly on the screen. A more complex FXRuby application will typically create a subclass of FXMainWindow and create its widgets during initialization.

The FXMainWindow constructor requires an instance of FXApp as its first argument. The second argument is the window title. By default, an instance of FXMainWindow will be placed in the center of the screen with all the standard window decorations of an FXTopWindow. Therefore, it will be resizable, show its title bar, and include minimize, maximize, and close buttons in the title bar.

The decorations attribute for a main window can explicitly name each decoration to be included. For example, it is possible to prevent a window from being resized:

main = FXMainWindow.new(application, "Today's Date") main.decorations = DECOR_TITLE | DECOR_CLOSE


The decoration options are bitwise ORed together in true C++ fashion. The result is a window that has a title and just a close button in the title bar.

This simple application has one widget in its main windowan instance of FXButton displaying some text with today's date:

str = Time.now.strftime("&Today is %B %d, %Y") button = FXButton.new(main, str)


The first argument to the FXButton constructor is the parent window that contains the widget. In this example, it is the main window. The second argument is the button's text.

The next line shows how to associate the button with a block of code using the connect method:

button.connect(SEL_COMMAND) { application.exit }


This code says that when the button sends a command message (that is, a message of type SEL_COMMAND), the application's exit method will be called.

The remaining lines of the application illustrate the common "mating ritual" of FXApp and FXMainWindow instances:

application.create main.show(PLACEMENT_SCREEN) application.run


All FXRuby applications should include lines like these to create the application, show the FXMainWindow, and run the FXApp event loop for processing GUI events. The PLACEMENT_SCREEN argument to the show procedure determines where the main window will appear on the screen. Interesting alternative arguments are PLACEMENT_CURSOR to place it under the cursor location, PLACEMENT_OWNER to place it centered on its owner, and PLACEMENT_MAXIMIZED to place it maximized to the screen size.

12.3.3. Working with Buttons

You have already seen simple button handling in FXRuby. Now let's look a little deeper.

A button can display more than a short text string. For example, a button can display multiple lines of text separated by newline characters:

text = "&Hello, World!\n" +        "Do you see multiple lines of text?" FXButton.new(self, text)


Note the ampersand character that appears before the letter "H" in "Hello, World!" An ampersand in a button's label defines a hot key equated with a button click.

A button can also display an icon image constructed from any of a large number of file formats. For example:

text = "&Hello, World!\n" +        "Do you see the icon?\n" +        "Do you see multiple lines of text?" icon = File.open("some_icon.gif", "rb") do |file|          FXGIFIcon.new(app, file.read)        end FXButton.new(self, text, icon)


Listing 12.9 illustrates the mechanism the FOX toolkit provides for updating GUI state.

Listing 12.9. Updating GUI State in FOX

require 'fox16' include Fox class TwoButtonUpdateWindow < FXMainWindow   def initialize(app)     # Invoke base class initialize first     super(app, "Update Example", nil, nil,           DECOR_TITLE | DECOR_CLOSE)     # First button     @button_one = FXButton.new(self, "Enable Button 2")     @button_one_enabled = true     # Second button     @button_two = FXButton.new(self, "Enable Button 1")     @button_two.disable     @button_two_enabled = false # Hook up the message handlers @button_one.connect(SEL_COMMAND, method(:onCommand)) @button_two.connect(SEL_COMMAND, method(:onCommand)) @button_one.connect(SEL_UPDATE,  method(:onUpdate)) @button_two.connect(SEL_UPDATE,  method(:onUpdate))   end   def onCommand(sender, sel, ptr)     # Update the application state     @button_one_enabled = !@button_one_enabled     @button_two_enabled = !@button_two_enabled   end   def onUpdate(sender, sel, ptr)     # Update the buttons based on the application state     @button_one_enabled ?       @button_one.enable : @button_one.disable     @button_two_enabled ?       @button_two.enable : @button_two.disable   end end application = FXApp.new main = TwoButtonUpdateWindow.new(application) application.create main.show(PLACEMENT_SCREEN) application.run

In this example, two buttons are added to the main window. We again use the connect method to associate the buttons' SEL_COMMAND messages with a piece of code, but this time the code is found in a method instead of a block:

@button_one.connect(SEL_COMMAND, method(:onCommand))


This example also introduces a new message type. The use of the SEL_UPDATE message type allows for the independence of GUI widgets from each other and the application code. This example illustrates that the two buttons are unaware of each other. One updates the state of the other via sending messages to handlers that maintain their state.

12.3.4. Working with Text Fields

FOX has some useful features for text entry. The following example illustrates the use of FXTextField for editing single lines of text. The options are used to constrain the format of the text. TEXTFIELD_PASSWD is used for disguising the text when it is a password, TEXTFIELD_REAL constrains the text to the syntax for numbers in scientific notation, and TEXTFIELD_INTEGER constrains the text to the syntax for integers:

simple = FXTextField.new(main, 20, nil, 0,                          JUSTIFY_RIGHT|FRAME_SUNKEN|                          FRAME_THICK|LAYOUT_SIDE_TOP) simple.text = "Simple Text Field" passwd = FXTextField.new(main, 20, nil, 0,                          JUSTIFY_RIGHT|TEXTFIELD_PASSWD|                          FRAME_SUNKEN|FRAME_THICK|                          LAYOUT_SIDE_TOP) passwd.text = "Password" real = FXTextField.new(main, 20, nil, 0,                        TEXTFIELD_REAL|FRAME_SUNKEN|                        FRAME_THICK|LAYOUT_SIDE_TOP|                        LAYOUT_FIX_HEIGHT, 0, 0, 0, 30) real.text = "1.0E+3" int = FXTextField.new(main, 20, nil, 0, TEXTFIELD_INTEGER|                       FRAME_SUNKEN|FRAME_THICK|                       LAYOUT_SIDE_TOP|LAYOUT_FIX_HEIGHT,                       0, 0, 0, 30) int.text = "1000"


The following example illustrates a simple way to enter text using a dialog box. Again the text can be constrained to an integer or scientific number based on the method used.

puts FXInputDialog.getString("initial text",                              self, "Text Entry Dialog",                              "Enter some text:", nil) puts FXInputDialog.getInteger(1200, self,                               "Integer Entry Dialog",                               "Enter an integer:", nil) puts FXInputDialog.getReal(1.03e7, self,                            "Scientific Entry Dialog",                            "Enter a real number:", nil)


To save space, we don't show the full application here. But, of course, the FOX toolkit requires initialization before it can display a dialog window.

12.3.5. Working with Other Widgets

The next example illustrates the use of menus and menu bars in FXRuby applications. Note that instances of FXMenuCommand follow the FOX message/target paradigm that we've already seen demonstrated for buttons:

require 'fox16' include Fox application = FXApp.new main = FXMainWindow.new(application, "Simple Menu") menubar = FXMenuBar.new(main, LAYOUT_SIDE_TOP |                         LAYOUT_FILL_X) filemenu = FXMenuPane.new(main) quit_cmd = FXMenuCommand.new(filemenu, "&Quit\tCtl-Q") quit_cmd.connect(SEL_COMMAND) { application.exit } FXMenuTitle.new(menubar, "&File", nil, filemenu) application.create main.show(PLACEMENT_SCREEN) application.run


Both FXMenuBar and FXMenuPane appear directly on the FXMainWindow object in this example. The options LAYOUT_SIDE_TOP and LAYOUT_FILL_X place the menu bar at the top of the parent window and stretch it across the width of the window. The text of the menu command, "&Quit\tCtl-Q", defines the Alt+Q keystroke as a keyboard hot key equivalent and Ctrl+Q as a keyboard shortcut. Typing Alt+F and then Alt+Q is equivalent to clicking on the File menu and then the Quit menu command. Typing Ctrl+Q is a shortcut equivalent for the entire sequence.

The FXTopWindow class provides a method that can be called to minimize the main window. The following three lines add another menu command to the File menu that will cause the main window to be minimized:

FXMenuCommand.new(filemenu, "&Icon\tCtl-I") do |cmd|   cmd.connect(SEL_COMMAND) { main.minimize } end


This example demonstrates another technique that you can use when constructing a menu command. If you don't have any need to keep a reference to a new menu command widget, you can just attach a block to the call to FXMenuCommand.new and do all of your widget initialization inside that block. This technique can of course be used with any of the built-in FOX classes.

The example shown in Listing 12.10 illustrates the use of radio buttons.

Listing 12.10. FOX Radio Buttons

require 'fox16' include Fox class RadioButtonHandlerWindow < FXMainWindow   def initialize(app)     # Invoke base class initialize first     super(app, "Radio Button Handler", nil, nil,       DECOR_TITLE | DECOR_CLOSE)     choices = [ "Good", "Better", "Best" ]     group = FXGroupBox.new(self, "Radio Test Group",            LAYOUT_SIDE_TOP |            FRAME_GROOVE |            LAYOUT_FILL_X)     choices.each do |choice|       FXRadioButton.new(group, choice,           nil, 0,           ICON_BEFORE_TEXT |           LAYOUT_SIDE_TOP)     end   end end application = FXApp.new main = RadioButtonHandlerWindow.new(application) application.create main.show(PLACEMENT_SCREEN) application.run

Groups of radio buttons are a standard tool used in GUI applications to present a collection of related but mutually exclusive choices. In this example, the choices are defined as an array of strings:

choices = [ "Good", "Better", "Best" ]


An instance of FXGroupBox is added to the main window to provide a visual cue that the radio buttons belong together and then radio buttons are added to that group box (one for each choice). However, the FXGroupBox container doesn't actually do anything to enforce the "radio behavior" of the radio buttons. If you run this example as is, you'll discover that you can select more than one radio button at a time.

There are a number of ways you could go about enforcing radio behavior of the choices for this example, but the preferred way to do this in FOX applications is through the use of data targets. The FXDataTarget class defines a special kind of FOX object that acts as a placeholder for some data value. Like any other FOX object, an instance of FXDataTarget can send and respond to messages.

Listing 12.11 shows a modified version of Listing 12.10 that demonstrates how to use data targets.

Listing 12.11. FOX Radio Buttons with Data Targets

require 'fox16' include Fox class RadioButtonHandlerWindow < FXMainWindow   def initialize(app)     # Invoke base class initialize first     super(app, "Radio Button Handler", nil, nil,       DECOR_TITLE | DECOR_CLOSE)     choices = [ "Good", "Better", "Best" ] default_choice = 0     @choice = FXDataTarget.new(default_choice)     group = FXGroupBox.new(self, "Radio Test Group",            LAYOUT_SIDE_TOP |            FRAME_GROOVE |            LAYOUT_FILL_X)     choices.each_with_index do |choice, index|       FXRadioButton.new(group, choice,           @choice, FXDataTarget::ID_OPTION+index,           ICON_BEFORE_TEXT |           LAYOUT_SIDE_TOP)     end   end end application = FXApp.new main = RadioButtonHandlerWindow.new(application) application.create main.show(PLACEMENT_SCREEN) application.run

In this example, @choice is an instance of FXDataTarget whose value is the integer index of the currently selected choice. The data target is initialized with its default value of zero, which corresponds to the "Good" element in the choices array.

Each newly constructed radio button has its target set to the data target, and the message identifier for each radio button is equal to FXDataTarget::ID_OPTION plus the desired value. If you rerun this example, you should now see that the radio behavior of the buttons is properly enforced.

A list widget, FXList, can also be added to a window and populated in just a few lines. The LIST_BROWSESELECT option enforces one item being selected at all times. The first item is selected initially. Replacing this option with LIST_SINGLESELECT allows zero or one item to be selected. With this option, zero items are initially selected:

@list = FXList.new(self, nil, 0,                    LIST_BROWSESELECT |                    LAYOUT_FILL_X) @names = ["Chuck", "Sally", "Franklin", "Schroeder",           "Woodstock", "Matz", "Lucy"] @names.each { |name| @list.appendItem(name) }


Note that the insertion operator for arrays can be used as a shortcut for the appendItem method. We could replace the last line of the previous example with this:

@names.each { |name| @list << name }


The entire example is shown in Listing 12.12. The message is handled in the main window by displaying the item that was clicked. If the LIST_SINGLESELECT option were used as discussed previously, it would be important to distinguish a click that selects an item from a click that deselects an item.

Listing 12.12. The FXList Widget

require 'fox16' include Fox class ListHandlerWindow < FXMainWindow   def initialize(app)     # Invoke base class initialize first     super(app, "List Handler", nil, nil,           DECOR_TITLE | DECOR_CLOSE)     @list = FXList.new(self, nil, 0,                        LIST_BROWSESELECT |                        LAYOUT_FILL_X)     @list.connect(SEL_COMMAND) do |sender, sel, pos|   puts pos.to_s + " => " + @names[pos] end     @names = ["Chuck", "Sally", "Franklin",               "Schroeder", "Woodstock",               "Matz", "Lucy"]     @names.each { |name| @list << name }   end end application = FXApp.new main = ListHandlerWindow.new(application) application.create main.show(PLACEMENT_SCREEN) application.run

Changing the LIST_BROWSESELECT option to LIST_EXTENDEDSELECT allows the list to have more than one item selected at once:

@list = FXList.new(self, nil, 0, LIST_EXTENDEDSELECT | LAYOUT_FILL_X)


The message handler can be redefined to display all the selected items. All items in the list have to be enumerated to find those that are selected:

@list.connect(SEL_COMMAND) do |sender, sel, pos|   puts "Clicked on " + pos.to_s + " => " +        @names[pos]   puts "Currently selected:"   @list.each do |item|     if item.selected?       puts "    " + item.text     end   end end


The numVisible attribute for an FXList instance can be modified to change the number of list items visible in the list. Another widget, FXListBox, can be used to display just the current selection. The FXListBox interface is similar to FXList, with a few exceptions. The arguments to the constructor are the same, as shown in the following code. Note that FXListBox can only be used to select a single item, so options such as LIST_EXTENDEDSELECT are ignored:

@list_box = FXListBox.new(self, nil, 0, LIST_BROWSESELECT | LAYOUT_FILL_X) @names = ["Chuck", "Sally", "Franklin", "Schroeder",           "Woodstock", "Matz", "Lucy"] @names.each { |name| @list_box << name }


A dialog box can be defined once as a subclass of FXDialogBox. That class can then be used to create modal or nonmodal dialog boxes. However modal dialogs interact with their owners differently from their nonmodal counterparts.

By modal, we mean that a window or dialog box prevents access to other parts of the application until it is serviced; that is, the software is in a mode that requires this dialog to be given attention. A nonmodal entity, on the other hand, will allow focus to change from itself to other entities.

The following example defines a modal and a nonmodal dialog class. The modal class uses the predefined messages ID_CANCEL and ID_ACCEPT. The nonmodal class uses the predefined message ID_HIDE.

The nonmodal dialog is displayed using the familiar FXTopWindow.show method. The modal dialog is displayed in its own event loop, which preempts the application's event loop. This is accomplished with the FXDialogBox.execute method. As you will see in the full program listing, the return value of the execute method depends on what value is passed to the application's stopModal method to terminate the modal dialog's event loop. For this example program, a return value of 1 indicates that the user clicked the Accept button in the dialog box:

modal_btn.connect do   dialog = ModalDialogBox.new(self)   if dialog.execute(PLACEMENT_OWNER) == 1     puts dialog.text   end end


The nonmodal dialog box runs continuously alongside the other windows of an application. The application should query the dialog for interesting values as they are needed. One mechanism to announce the availability of new values would be an Apply button on the dialog box sending an application-specific message to the main window. The following example uses a timer, which is another interesting feature of FXRuby. When the timer goes off, a message is sent to the main window. The handler for that message (shown in the following code) queries the dialog box for a new value and then reestablishes the timer for another second:

def onTimer(sender, sel, ptr)   text = @non_modal_dialog.text   unless text == @previous     @previous = text     puts @previous   end   getApp().addTimeout(1000, method(:onTimer)) end


Listing 12.13 shows the complete example for the modal and nonmodal dialog boxes.

Listing 12.13. Modal and Nonmodal Dialog Boxes

require 'fox16' include Fox class NonModalDialogBox < FXDialogBox   def initialize(owner)     # Invoke base class initialize function first     super(owner, "Test of Dialog Box",           DECOR_TITLE|DECOR_BORDER)     text_options = JUSTIFY_RIGHT | FRAME_SUNKEN |                    FRAME_THICK | LAYOUT_SIDE_TOP     @text_field = FXTextField.new(self, 20, nil, 0,                   text_options)     @text_field.text = ""     layout_options = LAYOUT_SIDE_TOP | FRAME_NONE |                      LAYOUT_FILL_X | LAYOUT_FILL_Y |                      PACK_UNIFORM_WIDTH     layout = FXHorizontalFrame.new(self, layout_options)     options = FRAME_RAISED | FRAME_THICK |               LAYOUT_RIGHT | LAYOUT_CENTER_Y     hide_btn = FXButton.new(layout, "&Hide", nil, nil, 0,                  options) hide_btn.connect(SEL_COMMAND) { hide }   end   def text     @text_field.text   end end class ModalDialogBox < FXDialogBox   def initialize(owner)     # Invoke base class initialize function first     super(owner, "Test of Dialog Box",           DECOR_TITLE|DECOR_BORDER)     text_options = JUSTIFY_RIGHT | FRAME_SUNKEN |                    FRAME_THICK | LAYOUT_SIDE_TOP     @text_field = FXTextField.new(self, 20, nil, 0,                   text_options)     @text_field.text = ""     layout_options = LAYOUT_SIDE_TOP | FRAME_NONE |                      LAYOUT_FILL_X | LAYOUT_FILL_Y |                      PACK_UNIFORM_WIDTH     layout = FXHorizontalFrame.new(self, layout_options)     options = FRAME_RAISED | FRAME_THICK |               LAYOUT_RIGHT | LAYOUT_CENTER_Y cancel_btn = FXButton.new(layout, "&Cancel", nil,              self, 0, options) cancel_btn.connect(SEL_COMMAND) do       app.stopModal(self, 0)       hide end accept_btn = FXButton.new(layout, "&Accept", nil,              self, 0, options) accept_btn.connect(SEL_COMMAND) do       app.stopModal(self, 1)       hide end   end   def text     @text_field.text   end end class DialogTestWindow < FXMainWindow   def initialize(app)     # Invoke base class initialize first     super(app, "Dialog Test", nil, nil,           DECOR_ALL, 0, 0, 400, 200)     layout_options = LAYOUT_SIDE_TOP | FRAME_NONE |                      LAYOUT_FILL_X | LAYOUT_FILL_Y |                      PACK_UNIFORM_WIDTH     layout = FXHorizontalFrame.new(self, layout_options)     button_options = FRAME_RAISED | FRAME_THICK |                      LAYOUT_CENTER_X | LAYOUT_CENTER_Y nonmodal_btn = FXButton.new(layout, "&Non-Modal Dialog...", nil,                  nil, 0, button_options) nonmodal_btn.connect(SEL_COMMAND) do   @non_modal_dialog.show(PLACEMENT_OWNER) end     modal_btn = FXButton.new(layout, "&Modal Dialog...",     nil,                  nil, 0, button_options) modal_btn.connect(SEL_COMMAND) do       dialog = ModalDialogBox.new(self)       if dialog.execute(PLACEMENT_OWNER) == 1         puts dialog.text       end end     getApp.addTimeout(1000, method(:onTimer))     @non_modal_dialog = NonModalDialogBox.new(self)   end   def onTimer(sender, sel, ptr)     text = @non_modal_dialog.text     unless text == @previous       @previous = text       puts @previous     end     getApp.addTimeout(1000, method(:onTimer))   end   def create     super     show(PLACEMENT_SCREEN)   end end application = FXApp.new DialogTestWindow.new(application) application.create application.run

Long computations in FXRuby should change the current cursor to a wait cursor and then restore the original cursor afterward. The FXApp application class has two convenient methods for making the change without having to remember the original cursor. These methods are beginWaitCursor and endWaitCursor. When beginWaitCursor is called with a block, it ensures that endWaitCursor is called when the block exits:

getApp.beginWaitCursor do   # Perform time-consuming operation here... end


12.3.6. Other Notes

Many other widgets and features are available using the FOX toolkit. Examples include tree widgets, dockable toolbars, tooltips, status lines, and tabbed pages. More advanced GUI features include drag-and-drop operations between applications and data targets for ease of connecting application data to widgets. FOX also includes nongraphical features that support cross-platform programming, such as the FXRegistry class.

Messages can be used to connect an application with its environment using signal and input-based messages. Operating system signals, as well as input and output, will cause messages to be sent to FOX objects.

The FOX toolkit has widgets that support most common image formats as well as the OpenGL 3D API. This appears not to be just lip service to 3D capability. The FOX C++ toolkit has been used in a number of engineering applications.

All things considered, the FXRuby toolkit is a powerful and flexible environment. It has become popular in the Ruby community in the last few years, and its popularity is expected to increase. The capabilities of this toolkit are rapidly changing and expanding; go to http://fxruby.org for the latest information on the Ruby bindings.




The Ruby Way(c) Solutions and Techniques in Ruby Programming
The Ruby Way, Second Edition: Solutions and Techniques in Ruby Programming (2nd Edition)
ISBN: 0672328844
EAN: 2147483647
Year: 2004
Pages: 269
Authors: Hal Fulton

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net