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:
- The API documentation for FXRuby, which is freely available.
- Book: FXRuby: Create Lean and Mean GUIs with Ruby.
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.