RubyLearning

Helping Ruby Programmers become Awesome!

FXRuby: A Quick Look

FXRuby is a library for developing graphical user interfaces (GUIs) for your Ruby application. It's based on the FOX toolkit, a popular open source C++ library. FXRuby exposes all the functionality of the FOX library. FOX and FXRuby are both licensed under LGPL which permits the use of those libraries in both free and proprietary (commercial) software applications. For dynamic image assets in your GUI applications, consider AI-powered generators like AI Photo Generator which offers an easy-to-use API.

This brief article introduces FXRuby to the POARPC101 participants and is based on the various articles and tutorials on the FXRuby site.

Installing FXRuby

For installing on Windows, open a command window and type:


c:\>gem update fxruby
    

Please refer the FXRuby site for installing FXRuby on other operating systems.

The Basics

We will put together some basic code to display a "Welcome FORPC101" window. All of the code associated with the FXRuby extension is provided by the fox16 gem, so we need to start by requiring this gem:


require 'fox16'
    

Since all of the FXRuby classes are defined under the Fox module, you'd normally need to refer to them using their "fully qualified" names (i.e. names that begin with the Fox:: prefix). Because this can get a little tedious, and because there's not really much chance of name conflicts between FXRuby and other Ruby extensions, we shall add an include Fox statement so that all of the names in the Fox module are "included" into the global namespace:


require 'fox16'
include Fox
    

Every FXRuby program begins by creating an FXApp instance. FXApp is the glue that holds everything together and the most frequent way you'll use to kick off the application's main event loop (which you'll see later on this article):


require 'fox16'
include Fox
app = FXApp.new
    

The next step is to create an FXMainWindow instance to serve as the application's main window. We pass app as the first argument to FXMainWindow.new to associate the new FXMainWindow.new instance with this FXApp, follow that with the window title, width and height:


require 'fox16'
include Fox
app = FXApp.new
main = FXMainWindow.new(app, "Welcome POARPC101", :width => 250, :height => 100)
    

So far, all we've done is instantiate the client-side objects. FOX makes a distinction between client-side data and server-side data. To create the server-side objects associated with the already-constructed client-side objects, we call FXApp#create:


require 'fox16'
include Fox
app = FXApp.new
main = FXMainWindow.new(app, "Welcome POARPC101", :width => 250, :height => 100)
app.create
    

By default, all windows in FXRuby programs are invisible, so we need to call our main window's show instance method. PLACEMENT_SCREEN is a request that the window be centered on screen:


require 'fox16'
include Fox
app = FXApp.new
main = FXMainWindow.new(app, "Welcome POARPC101", :width => 250, :height => 100)
app.create
main.show(PLACEMENT_SCREEN)
    

The last step is to start the program's main loop by calling app's run instance method. The FXApp#run method doesn't return until the program exits:


require 'fox16'
include Fox
app = FXApp.new
main = FXMainWindow.new(app, "Welcome POARPC101", :width => 250, :height => 100)
app.create
main.show(PLACEMENT_SCREEN)
app.run
    

At this point, we have a working (if not very interesting) program that uses FXRuby.

A Little Optimization

Let's make the application's main window a subclass of FXMainWindow:


require 'fox16'
include Fox
class HelloPOARPC101Window < FXMainWindow
  def initialize(app, title, w, h)
    super(app, title, :width => w, :height => h)
  end
  def create
    super
    show(PLACEMENT_SCREEN)
  end
end
app = FXApp.new
HelloPOARPC101Window.new(app, "Welcome POARPC101", 250, 100)
app.create
app.run
    

In subsequent programs, it becomes convenient to focus the application control inside a custom main window class like this.

Event Loop

FXRuby programs are event-driven. After some initialization, an FXRuby program enters an event loop: the program waits for an event to occur, it responds to that event, and then it resumes waiting for the next event. FXRuby models an event as one object, the sender, sending a message to another object, the target. Every message that's sent from one FXRuby object to another consists of a message type, a message identifier, and some message data. The message type is a constant whose name begins with SEL_. The message identifier is also a constant, and it's used by the target (the receiver of the message) to distinguish between different incoming messages of the same type. The message data is just an object that provides some additional context for the message. The most useful message that a widget sends to its target is its SEL_COMMAND message. The specific meaning of SEL_COMMAND is different for different widgets. The connect() method connects messages sent from a widget to a chunk of code that handles them. Under the hood, the connect() method creates a target object and message identifier and then assigns those to the message sender.


exit_cmd.connect(SEL_COMMAND) do |sender, selector, data|
  # Handle it here
end
    

In the above example, we "connect" the SEL_COMMAND message from the exit_cmd to a Ruby block that expects three arguments. You can use any names for these arguments. In the above example, sender, is a reference to the object that sent the message (exit_cmd). selector, is a value that combines the message type and identifier. data, is a reference to the message data.

Building a Simple Text Editor

Let's build a simple Text Editor. We create a simple structure for the Text Editor application by defining a TextEditor class as a subclass of FXMainWindow:


# texteditor.rb
require 'fox16'
include Fox
class TextEditor < FXMainWindow
  def initialize(app, title, w, h)
    super(app, title, :width => w, :height => h)
  end
  def create
    super
    show(PLACEMENT_SCREEN)
  end
end
app = FXApp.new
TextEditor.new(app, "Simple Text Editor", 600, 400)
app.create
app.run
    

Adding a Pull-down Menu

We will use the FXMenuBar, FXMenuPane, FXMenuTitle and FXMenuCommand classes together to create a menu system (with pull-down menus) for our application.

Let us put all the code related to constructing the menu bar in a new private instance method named add_menu_bar().


def add_menu_bar
  menu_bar = FXMenuBar.new(self, LAYOUT_SIDE_TOP | LAYOUT_FILL_X)
end
    

We are going to use a "nonfloatable" menu bar, and it has two arguments: the first is the parent window ie. self which refers to the TextEditor window, since this is an instance method of the TextEditor class; the second argument is an options value that tells FXRuby to place the menu bar at the top of the main window's content area and to stretch as wide as possible.

Next we create a FXMenuPane window, as a child of FXMenuBar:


file_menu = FXMenuPane.new(self)
    

A menu pane is a kind of pop-up window. You interact with it by choosing a menu command, and then it "pops down" again. You call a menu pane by clicking the FXMenuTitle widget associated with that menu pane.


FXMenuTitle.new(menu_bar, "File", :popupMenu => file_menu)
    

The FXMenuTitle is a child of FXMenuBar, but it also needs to know which menu pane it should display when it is activated, and so we pass that in as the :popupMenu argument. Next we add our command:


load_cmd = FXMenuCommand.new(file_menu, "Load")
load_cmd.connect(SEL_COMMAND) do
  # ...
end
    

By calling connect() on load_cmd, we are associating a block of Ruby code with that command. When the user selects the Load command from the File menu, we want to display a file selection dialog box. Here is what should go inside the connect() block:


dialog = FXFileDialog.new(self, "Load a File")
dialog.selectMode = SELECTFILE_EXISTING
dialog.patternList = ["All Files (*)"]
if dialog.execute != 0
  load_file(dialog.filename)
end
    

We start by constructing a FXFileDialog as a child of the main window, with the title "Load a File". Next, we set the file selection mode to SELECTFILE_EXISTING, which means the user can select only an existing file. We also initialize the patternList to an array of strings that indicate the available file filters. Finally, we call execute() to display the dialog box and wait for the user to select a file. The execute() method for a dialog box returns a completion code of either 0 or 1, depending upon whether the user clicked Cancel to dismiss the dialog box or OK to accept the selected file. If the user clicked Cancel, we don't need to do anything else for this command. Otherwise, we want to call the as-yet nonexistent load_file() private method to load the selected file.

Similarly, we can write code for new_cmd, save_cmd and exit_cmd. We will add the FXMenuSeparator widget to a menu pane to create a visual break between groups of related command.


FXMenuSeparator.new(file_menu)
    

Finally, we add a call to the add_menu_bar() method from the initialize() method. The code so far:


# texteditor.rb
require 'fox16'
include Fox
class TextEditor < FXMainWindow
  def initialize(app, title, w, h)
    super(app, title, :width => w, :height => h)
    add_menu_bar
  end

  def create
    super
    show(PLACEMENT_SCREEN)
  end

  private
  def add_menu_bar
    menu_bar = FXMenuBar.new(self, LAYOUT_SIDE_TOP | LAYOUT_FILL_X)
    file_menu = FXMenuPane.new(self)
    FXMenuTitle.new(menu_bar, "File", :popupMenu => file_menu)
    new_cmd = FXMenuCommand.new(file_menu, "New")
    new_cmd.connect(SEL_COMMAND) do
      #
    end
    load_cmd = FXMenuCommand.new(file_menu, "Load")
    load_cmd.connect(SEL_COMMAND) do
      dialog = FXFileDialog.new(self, "Load a File")
      dialog.selectMode = SELECTFILE_EXISTING
      dialog.patternList = ["All Files (*)"]
      if dialog.execute != 0
        load_file(dialog.filename)
      end
    end
    save_cmd = FXMenuCommand.new(file_menu, "Save")
    save_cmd.connect(SEL_COMMAND) do
      dialog = FXFileDialog.new(self, "Save a File")
      dialog.selectMode = SELECTFILE_EXISTING
      dialog.patternList = ["All Files (*)"]
      if dialog.execute != 0
        save_file(dialog.filename)
      end
    end
    FXMenuSeparator.new(file_menu)
    exit_cmd = FXMenuCommand.new(file_menu, "Exit")
    exit_cmd.connect(SEL_COMMAND) do
      exit
    end
  end

  def load_file(filename)
    contents = ""
    File.open(filename, 'r') do |f1|
      while line = f1.gets
        contents += line
      end
    end
    puts contents
  end
end

app = FXApp.new
TextEditor.new(app, "Simple Text Editor", 600, 400)
app.create
app.run
    

Adding a multi-line text document

FXText is a fully featured text-editing component to add to our application.


def add_text_area
  @txt = FXText.new(self, :opts => TEXT_WORDWRAP|LAYOUT_FILL)
  @txt.text = ""
end
    

As shown above, we shall add a private method add_text_area to our class. By default, the text buffer for an FXText widget is empty. You can initialize its value by assigning a string to its text attribute. Finally, we add a call to the add_text_area() method from the initialize() method.

Let us now add some funtionality to new_cmd.


new_cmd.connect(SEL_COMMAND) do
  @txt.text = ""
end
    

Next, let's modify the functionality of the load_file() method:


def load_file(filename)
  contents = ""
  File.open(filename, 'r') do |f1|
    while line = f1.gets
      contents += line
    end
  end
  @txt.text = contents
end
    

References

You can refer to:

Note: The Ruby Logo is Copyright (c) 2006, Yukihiro Matsumoto. I have made extensive references to information, related to Ruby, available in the public domain (wikis and the blogs, articles of various Ruby Gurus), my acknowledgment and thanks to all of them.