FX/Ruby (FOX) The FOX system is also relatively new technology; its emphasis is on speed and consistency among 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++, although 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. Although it is not extremely widespread at this time, FOX is growing in popularity. We believe it has a future, and we want to present it to you for that reason. Overview FX/Ruby is a Ruby binding to the FOX C++ library; it has a large number of 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 it 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, enumerations and bit operations as well as message maps based on enumerations. 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 is able to send and receive messages; a message is associated with a specific target at runtime, when the message is sent. A message in FOX is an integer unique to a class and its super classes. Many of the FOX classes use a common set of message definitions to allow widgets to interoperate. An application-specific instance of an FXRuby class should initialize a message map from FOX messages (integers) to message handlers (Ruby methods). A message handler should return 1 to indicate that the message has been handled or 0 to indicate 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 (refer to the book Design Patterns by the "Gang of Four"). 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. An example of this later in this chapter is a button that updates its active/inactive status based on an application variable. A Trivial Windowed Application Here is a minimal FXRuby application, the equivalent of the others you saw for Tk and GTK+ earlier:
require "fox" include Fox application = FXApp.new("Today", "Sample programs") application.init(ARGV) main = FXMainWindow.new(application, "Hello") str = Time.now.strftime("&Today is %B %d, %Y") FXButton.new(main, str, nil, application, FXApp::ID_QUIT) application.create main.show(PLACEMENT_SCREEN) application.run This application is enough to illustrate two fundamental classes of FXRuby: FXApp and FXMainWindow. Each application must have one instance of FXApp created and initialized before anything is done with the other FOX classes. FXMainWindow is a subclass of FXTopWindow; every widget in FOX is a kind of "window." FXTopWindow is a window that appears directly on the screen. A more complex FXRuby application will create a subclass of FXMainWindow and create its widgets during initialization. The FXApp constructor is given an application name and a vendor key as arguments. The vendor key is a unique string for your applications using the FOX registry mechanism, which we describe later. The FXMainWindow constructor requires an instance of FXApp as its first argument. The second argument is the window title. An instance of FXMainWindow will be placed in the center of the screen with all the window decorations of FXTopWindow. Therefore, it will be resizable, show its title bar, and include minimize, maximize, and close buttons in the title bar. The decoration argument in the constructor can explicitly name each decoration to be included. For example, it is possible to prevent a window from being resized:
main = FXMainWindow.new(application, "Hello", nil, nil, DECOR_TITLE | DECOR_CLOSE) The two nil arguments in this example are placeholders for icons to be used on the desktop by the window manager. 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 the text Hello, world!:
FXButton.new(main, "&Hello, world!", nil, application, FXApp::ID_QUIT) The first argument 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 ampersand defines a hot key equated with a button click. The nil argument is a placeholder for a button icon. The final two arguments in the button constructor illustrate the message/target paradigm of FOX. The application argument can be any instance of FXObject. It is the target of the message FXApp::ID_QUIT that the button will send when clicked. In this example, the instance of FXApp will respond by stopping event processing and closing the instance of FXMainWindow. 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 object, 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). These simple examples illustrate the use of messages and targets rather than callbacks in FXRuby. In that example, the target is an object implemented in the FOX library. The application shown in Listing 6.9 illustrates how to implement a simple message handler in a Ruby code. The application will appear similar to the previous ones except that when the button is clicked, Ruby code will print text to the console. Listing 6.9 FOX Messages and Targets require "fox" require "responder" include Fox class SimpleMessageHandlerWindow < FXMainWindow include Responder # Message identifiers for this class ID_HELLO_WORLD = FXMainWindow::ID_LAST def initialize(app) # Invoke base class initialize first super(app, "Simple Message Handler", nil, nil, DECOR_TITLE | DECOR_CLOSE) # Define the message map for the class FXMAPFUNC(SEL_COMMAND, ID_HELLO_WORLD, "onCmdHelloWorld") # The button FXButton.new(self, "&Hello, World", nil, self, ID_HELLO_WORLD) end def onCmdHelloWorld(sender, sel, ptr) puts "Hello, World" end end def run application = FXApp.new("SimpleButton", "Sample programs") application.init(ARGV) main = SimpleMessageHandlerWindow.new(application) application.create main.show(PLACEMENT_SCREEN) application.run end run This application creates a subclass of FXMainWindow. The purpose of this subclass is to define an application-specific message handler. Again, any FXObject instance can handle messages, so the use of FXMainWindow as the superclass is simply a useful convention. Responder is a module that is in the examples directory of the FXRuby distribution. It is included to provide convenience methods for creating the message map that associates an ID with a handler method. This example defines one associationfrom ID_HELLO_WORLD to the handler method onCmdHelloWorld. Evidence of FOX's native C++ code betrays itself again in the use of integer IDs. An ID value has to be unique within an FXObject and its superclasses. The convention is to define ID_LAST as the last ID in a superclass. This ID also serves as the first ID in each subclass. The class in this example will not be used as a superclass, so it does not define its own ID_LAST. The FXMAPFUNC method is a convenience method in the Responder module. The flavor used in this example adds to the sample window's message map the association from ID_HELLO_WORLD to onCmdHelloWorld. Every message has a message type and a message ID. SEL_COMMAND is the type, and ID_HELLO_WORLD the ID in this example. Buttons can send two types of messages: SEL_COMMAND and SEL_UPDATE. An example of an update message is provided later. 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. The following example illustrates the use of an image and multiple lines of text in a button:
text = "&Hello, World\nDo you see the image?\n" + "Do you see multiple lines of text?" gif = File.open("icons/ruby_button.gif", "rb").read()) image = FXGIFIcon.new(app, gif) FXButton.new(self, text, image, self, ID_HELLO_WORLD) The example shown in Listing 6.10 illustrates the mechanism the FOX toolkit provides for updating the GUI state. Listing 6.10 FOX State-Update Example require "fox" require "responder" include Fox class TwoButtonUpdateWindow < FXMainWindow include Responder # Message identifiers for this class ID_TOGGLE_BUTTON = FXMainWindow::ID_LAST def initialize(app) # Invoke base class initialize first super(app, "Update Example", nil, nil, DECOR_TITLE | DECOR_CLOSE) # Define the message map for the class FXMAPFUNC(SEL_COMMAND, ID_TOGGLE_BUTTON, "onCommand") FXMAPFUNC(SEL_UPDATE, ID_TOGGLE_BUTTON, "onUpdate") # First button @button_one = FXButton.new(self, "Enable Button 2", nil, self, ID_TOGGLE_BUTTON) @button_one_enabled = true # Second button @button_two = FXButton.new(self, "Enable Button 1", nil, self, ID_TOGGLE_BUTTON) @button_two.disable @button_two_enabled = false 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 def run application = FXApp.new("UpdateExample", "Sample programs") application.init(ARGV) main = TwoButtonUpdateWindow.new(application) application.create main.show(PLACEMENT_SCREEN) application.run end run This example creates a message map with two associations. The same ID is used (ID_TOGGLE_BUTTON), and two message types are used (SEL_COMMAND and SEL_UPDATE). Two buttons are added to the main window. The same message ID (ID_TOGGLE_BUTTON) is sent from each button. Two types of messages are sent from each button. The SEL_COMMAND type is sent when a button is clicked. The SEL_UPDATE type is sent when a button is updated. Updates occur when there are no higher-priority events being processed by the GUI toolkit. An application's update methods should be "short and sweet" to maintain an interactive feel for the user. 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 by sending messages to handlers that maintain their state. The class FXButton is a subclass of FXLabel. A window can display static text and/or an image very simply using a label. The next example illustrates how to change the font as well. 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.setText("Simple Text Field") passwd = FXTextField.new(main, 20, nil, 0, JUSTIFY_RIGHT|TEXTFIELD_PASSWD| FRAME_SUNKEN|FRAME_THICK| LAYOUT_SIDE_TOP) passwd.setText("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.setText("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.setText("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 displaying a dialog window. Working with Other Widgets The next example illustrates the use of menus and menu bars in FXRuby applications. Instances of FXMenuCommand follow the FOX message/target paradigm. In this example, the message once again is FXApp::ID_QUIT and the target is the FXApp itself, so there is no need to implement a new message handler method:
require "fox" include Fox application = FXApp.new("SimpleMenu", "Sample programs") application.init(ARGV) main = FXMainWindow.new(application, "Simple Menu") menubar = FXMenubar.new(main, LAYOUT_SIDE_TOP | LAYOUT_FILL_X) filemenu = FXMenuPane.new(main) FXMenuCommand.new(filemenu, "&Quit\tCtl-Q", nil, application, FXApp::ID_QUIT) 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 hotkey equivalent and Ctrl+Q as a keyboard shortcut. Typing Alt+F then Alt+Q is equivalent to clicking the File menu and then the Quit menu command. Typing Ctrl+Q is a shortcut equivalent for the entire sequence. Another message, this one understood by FXTopWindow, can be sent from an FXMenuCommand object to iconify the main window. The following line adds that command to the File menu:
FXMenuCommand.new(filemenu, "&Icon\tCtl-I", nil, main, FXTopWindow::ID_ICONIFY) Note also that menu items can be cascaded through the use of the class FXMenuCascade. The example shown in Listing 6.11 illustrates the use of radio buttons. The example also uses the FOX toolkit's message-passing mechanism explicitly. The radio buttons determine the target and the message dynamically. Listing 6.11 FOX Radio Buttons require "fox" require "responder" include Fox class RadioButtonHandlerWindow < FXMainWindow include Responder # Message identifiers for this class ID_EXECUTE_CHOICE, ID_CHOOSE_QUIT, ID_CHOOSE_ICON = enum(FXMainWindow::ID_LAST, 3) def initialize(app) # Invoke base class initialize first super(app, "Radio Button Handler", nil, nil, DECOR_TITLE | DECOR_CLOSE) # Define the message map for the class FXMAPFUNC(SEL_COMMAND, ID_EXECUTE_CHOICE, "onCmdExecuteChoice") FXMAPFUNC(SEL_COMMAND, ID_CHOOSE_QUIT, "onCmdChooseQuit") FXMAPFUNC(SEL_COMMAND, ID_CHOOSE_ICON, "onCmdChooseIcon") group = FXGroupBox.new(self, "Radio Test Group", LAYOUT_SIDE_TOP | FRAME_GROOVE | LAYOUT_FILL_X) FXRadioButton.new(group, "&Quit the application", self, ID_CHOOSE_QUIT, ICON_BEFORE_TEXT | LAYOUT_SIDE_TOP) FXRadioButton.new(group, "&Iconify the window", self, ID_CHOOSE_ICON, ICON_BEFORE_TEXT | LAYOUT_SIDE_TOP) FXButton.new(self, "&Do it now!", nil, self, ID_EXECUTE_CHOICE) @target = app @choice = FXApp::ID_QUIT end def onCmdChooseQuit(sender, sel, ptr) @target = getApp() @choice = FXApp::ID_QUIT end def onCmdChooseIcon(sender, sel, ptr) @target = self @choice = FXTopWindow::ID_ICONIFY end def onCmdExecuteChoice(sender, sel, ptr) @target.handle(self, MKUINT(@choice, SEL_COMMAND), nil) end end def run application = FXApp.new("RadioButton", "Sample programs") application.init(ARGV) main = RadioButtonHandlerWindow.new(application) application.create main.show(PLACEMENT_SCREEN) application.run end run Several application-specific messages are used in this example. The responder.rb module has a convenience method for defining multiple FOX identifiers. The enum method returns an array of sequential integers. The first argument is the first integer of the array. The second argument is the length of the array. This example returns an array of three integers, from FXMainWindow::ID_LAST through FXMainWindow::ID_LAST + 3. The Ruby assignment operator converts the integers into three "rvalues" and assigns those values to the three "lvalues":
ID_EXECUTE_CHOICE, ID_CHOOSE_QUIT, ID_CHOOSE_ICON = enum(FXMainWindow::ID_LAST, 3) Instances of FXRadioButton work together as a group of buttons when they are added to the same parent. This example adds an instance of FXGroupBox to the main window and then adds the radio buttons to the group box:
group = FXGroupBox.new(self, "Radio Test Group", LAYOUT_SIDE_TOP | FRAME_GROOVE | LAYOUT_FILL_X) FXRadioButton.new(group, "&Quit the application", self, ID_CHOOSE_QUIT, ICON_BEFORE_TEXT | LAYOUT_SIDE_TOP) FXRadioButton.new(group, "&Iconify the window", self, ID_CHOOSE_ICON, ICON_BEFORE_TEXT | LAYOUT_SIDE_TOP) The radio buttons are mapped to methods in the application's RadioButtonHandlerWindow class. The pushbutton is mapped to a method that send the message in @choice to the target in @target. The MKUINT method is used to create the message to be sent. A message is a combination of an identifier such as FXApp::ID_QUIT and a message type such as SEL_COMMAND. In this way, the same identifier can be used for multiple types of messages:
def onCmdChooseQuit(sender, sel, ptr) @target = getApp() @choice = FXApp::ID_QUIT end def onCmdChooseIcon(sender, sel, ptr) @target = self @choice = FXTopWindow::ID_ICONIFY end def onCmdExecuteChoice(sender, sel, ptr) @target.handle(self, MKUINT(@choice, SEL_COMMAND), nil) end The FXCheckButton class, as well as the FXRadioButton class, has getCheck and setCheck methods for programmatically inspecting and modifying the widgets. A checkbutton can be added in just a couple of lines to the previous example. This new button, when checked, will cause the pushbutton command to be ignored (see Figure 6.8):
@ignore = FXCheckButton.new(self, "Ig&nore", nil, 0, ICON_BEFORE_TEXT | LAYOUT_SIDE_TOP) Figure 6.8. Radio buttons and checkboxes in FOX. The constructor for the checkbutton initializes the target to nil and the message ID to 0. In this example, the checkbutton will not have to send a message to have an effect on the application. The redefinition of the onCmdExecuteChoice method inspects the state of the check button to decide what to do:
def onCmdExecuteChoice(sender, sel, ptr) unless @ignore.getCheck then @target.handle(self, MKUINT(@choice, SEL_COMMAND), nil) end end The complete example is shown in Listing 6.12. Figure 6.8 shows a screenshot of this example. Listing 6.12 Radio Buttons and Checkboxes in FOX require "fox" require "responder" include Fox class RadioButtonHandlerWindow < FXMainWindow include Responder # Message identifiers for this class ID_EXECUTE_CHOICE, ID_CHOOSE_QUIT, ID_CHOOSE_ICON = enum(FXMainWindow::ID_LAST, 3) def initialize(app) # Invoke base class initialize first super(app, "Radio Button Handler", nil, nil, DECOR_TITLE | DECOR_CLOSE) # Define the message map for the class FXMAPFUNC(SEL_COMMAND, ID_EXECUTE_CHOICE, "onCmdExecuteChoice") FXMAPFUNC(SEL_COMMAND, ID_CHOOSE_QUIT, "onCmdChooseQuit") FXMAPFUNC(SEL_COMMAND, ID_CHOOSE_ICON, "onCmdChooseIcon") group = FXGroupBox.new(self, "Radio Test Group", LAYOUT_SIDE_TOP | FRAME_GROOVE | LAYOUT_FILL_X) FXRadioButton.new(group, "&Quit the application", self, ID_CHOOSE_QUIT, ICON_BEFORE_TEXT | LAYOUT_SIDE_TOP) FXRadioButton.new(group, "&Iconify the window", self, ID_CHOOSE_ICON, ICON_BEFORE_TEXT | LAYOUT_SIDE_TOP) FXButton.new(self, "&Do it now!", nil, self, ID_EXECUTE_CHOICE) @ignore = FXCheckButton.new(self, "Ig&nore", nil, 0, ICON_BEFORE_TEXT | LAYOUT_SIDE_TOP) @target = app @choice = FXApp::ID_QUIT end def onCmdChooseQuit(sender, sel, ptr) @target = getApp() @choice = FXApp::ID_QUIT end def onCmdChooseIcon(sender, sel, ptr) @target = self @choice = FXTopWindow::ID_ICONIFY end def onCmdExecuteChoice(sender, sel, ptr) unless @ignore.getCheck then @target.handle(self, MKUINT(@choice, SEL_COMMAND), nil) end end end def run application = FXApp.new("RadioButton", "Sample programs") application.init(ARGV) main = RadioButtonHandlerWindow.new(application) application.create main.show(PLACEMENT_SCREEN) application.run end run 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, 5, self, ID_SELECT, LIST_BROWSESELECT | LAYOUT_FILL_X) @names = ["Chuck", "Sally", "Franklin", "Schroeder", "Woodstock", "Matz", "Lucy"] @names.each { |each| @list.appendItem(each) } The entire example is shown in Listing 6.13. 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 6.13 FOX List require "fox" require "responder" include Fox class ListHandlerWindow < FXMainWindow include Responder # Message identifiers for this class ID_SELECT = FXMainWindow::ID_LAST def initialize(app) # Invoke base class initialize first super(app, "List Handler", nil, nil, DECOR_TITLE | DECOR_CLOSE) # Define the message map for the class FXMAPFUNC(SEL_COMMAND, ID_SELECT, "onCmdSelect") @list = FXList.new(self, 5, self, ID_SELECT, LIST_BROWSESELECT | LAYOUT_FILL_X) @names = ["Chuck", "Sally", "Franklin", "Schroeder", "Woodstock", "Matz", "Lucy"] @names.each { |each| @list.appendItem(each) } end def onCmdSelect(sender, sel, i) puts i.to_s + " => " + @names[i] end end def run application = FXApp.new("List", "Sample programs") application.init(ARGV) main = ListHandlerWindow.new(application) application.create main.show(PLACEMENT_SCREEN) application.run end 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, 5, self, ID_SELECT, 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:
def onCmdSelect(sender, sel, pos) puts "Clicked on " + pos.to_s + " => " + @names[pos] puts "Currently selected:" for i in 0 .. @names.size-1 if @list.isItemSelected(i) puts " " + @names[i] end end end The second argument of the FXList constructor controls how many items are visible in the widget. 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 here (note that FXListBox can only be used to select a single item, so options such as LIST_EXTENDEDSELECT are ignored):
@list = FXListBox.new(self, 5, self, ID_SELECT, LIST_BROWSESELECT | LAYOUT_FILL_X) @names = ["Chuck", "Sally", "Franklin", "Schroeder", "Woodstock", "Matz", "Lucy"] @names.each { |each| @list.appendItem(each) } The message handler has to change for FXListBox. The third argument is no longer the position of the selected item in the list. The selected item must be inspected directly from the list box:
def onCmdSelect(sender, sel, ptr) puts @list.getCurrentItem end 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 dialog boxes 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 box is displayed using the familiar FXTopWindow.show method. The modal dialog box is displayed in its own event loop, which preempts the application's event loop. This is accomplished with the FXDialogBox.execute method. The method returns 1 if the ID_ACCEPT message is sent to close the dialog box or 0 if the ID_CANCEL message is sent. Here's the example:
def onCmdModalDialog(sender, sel, ptr) dialog = ModalDialogBox.new(self) if dialog.execute(PLACEMENT_OWNER) == 1 puts dialog.getText end return 1 end The nonmodal dialog box runs continuously alongside the other windows of an application. The application should query the dialog box 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 another interesting feature of FXRuby: a timer. When the timer goes off, a message is sent to the main window. The handler for that message, listed here, queries the dialog box for a new value and then reestablishes the timer for another second:
def onCmdTimer(sender, sel, ptr) text = @non_modal_dialog.getText unless text == @previous @previous = text puts @previous end @timer = getApp().addTimeout(1000, self, ID_TIMER); end The complete example for the modal and nonmodal dialog boxes is shown in Listing 6.14. Listing 6.14 FOX Dialog Boxes require "fox" require "responder" 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.setText("") 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 FXButton.new(layout, "&Hide", nil, self, ID_HIDE, options) end def onCmdCancel @text_field.setText("") super end def getText @text_field.getText 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.setText("") 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 FXButton.new(layout, "&Cancel", nil, self, ID_CANCEL, options) FXButton.new(layout, "&Accept", nil, self, ID_ACCEPT, options) end def onCmdCancel @text_field.setText("") super end def getText @text_field.getText end end class DialogTestWindow < FXMainWindow include Responder # Message identifiers ID_NON_MODAL, ID_MODAL, ID_TIMER = enum(FXMainWindow::ID_LAST, 3) def initialize(app) # Invoke base class initialize first super(app, "Dialog Test", nil, nil, DECOR_ALL, 0, 0, 400, 200) # Set up the message map for this window FXMAPFUNC(SEL_COMMAND, ID_NON_MODAL, "onCmdNonModelDialog") FXMAPFUNC(SEL_COMMAND, ID_MODAL, "onCmdModalDialog") FXMAPFUNC(SEL_TIMEOUT, ID_TIMER, "onCmdTimer") 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 FXButton.new(layout, "&Non-Modal Dialog...", nil, self, ID_NON_MODAL, button_options) FXButton.new(layout, "&Modal Dialog...", nil, self, ID_MODAL, button_options) @timer = getApp().addTimeout(1000, self, ID_TIMER); @non_modal_dialog = NonModalDialogBox.new(self) end def onCmdNonModelDialog(sender, sel, ptr) @non_modal_dialog.show(PLACEMENT_OWNER) end def onCmdModalDialog(sender, sel, ptr) dialog = ModalDialogBox.new(self) if dialog.execute(PLACEMENT_OWNER) == 1 puts dialog.getText end return 1 end def onCmdTimer(sender, sel, ptr) text = @non_modal_dialog.getText unless text == @previous @previous = text puts @previous end @timer = getApp().addTimeout(1000, self, ID_TIMER); end def create super show(PLACEMENT_SCREEN) end end def run application = FXApp.new("DialogTest", "Sample programs") application.init(ARGV) DialogTestWindow.new(application) application.create application.run end 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. Ruby's begin/ensure form and yield statement make cursor management even more convenient:
def busy begin getApp().beginWaitCursor yield ensure getApp().endWaitCursor end end The busy example method shown here can be used to change to a wait cursor for the duration of any block of code passed to it. 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 non-graphical features that support cross-platform programming (for example, FXFile and FXRegistry). 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 many engineering applications. FXRuby has been used with OpenGL as well but was not ready for general release at the time of this writing. Because the FOX toolkit is written in C++, there are methods that rely on the C++ overloading syntax. This syntax is incompatible with Ruby, a dynamically typed language. The current documentation is lacking in explanation of what mappings to FOX are still missing. Other language features overlap successfully; for example, both C++ and Ruby have optional arguments, and the FOX toolkit and the FXRuby binding take advantage of this feature. The FOX toolkit was started in 1997. The FXRuby binding dates from early 2001. FXRuby is stable for its age and is more than usable; some core architectural issues are currently being worked out by its creator. The most recent release appears to cooperate with the Ruby garbage collector. Future work is planned for tackling interaction with Ruby threads as well as other Ruby extensions. |