Ruby is currently best known as a programming language for building Web applications, primarily with the Ruby on Rails framework. However, the language is more than capable for developing graphical desktop applications as well. In this article, you'll learn more about using Ruby for the desktop, and you'll work through a detailed example that uses Monkeybars — an open source library based on Swing and JRuby — to create a GUI desktop application.
The standard Ruby distribution includes code for bindings for Tk, an open source, cross-platform set of widgets for creating graphical desktop applications. This can be extremely handy. But when you install Ruby from source code, you need to be sure you also have the Tk dependencies and make sure the compilation settings include Tk. If you install Ruby on Windows® using the excellent "one-click" installer package, you still must take extra steps to have Tk working, because it no longer supports automatic installation.
Even with Tk set up for Ruby it is somewhat clunky. Tk applications often look like Tk applications; depending on the target platform they can look rather ugly. And trying to create complex interfaces is daunting. Tk is best used for smaller GUI needs.
The weakness of Tk has prompted the development of other GUI toolkit options for Ruby (see Resources for relevant links). Some of the notable choices are:
- FxRuby: FxRuby is a Ruby binding for Fox, a GUI toolkit written in C++. It is available for installation using RubyGems. A binary gem is available for Windows; the gem for other platforms requires you to compile native code.
- WxRuby: WxRuby is a binding for the cross-platform wxWidgets C++ GUI toolkit, which lets you create native-looking desktop applications. It is available for installation as a gem.
- QtRuby: QtRuby gives you Ruby bindings to the Qt toolkit (the one used in the KDE desktop system). A gem is available for the Windows installation, but only source code for other platforms.
- GTK-Ruby: GTK is the UI toolkit used in GNOME; you need to compile native code to get it running.
- Shoes: Shoes is a recent entry into the Ruby GUI widget world. Unlike the other toolkits mentioned in this list, it is designed specifically for Ruby. It is available for installation using platform-specific installers.
- Swing: Swing? Yes, Swing, the GUI library that is bundled with every installation of a Java runtime environment. If you run JRuby, then you can use Swing.
All but one of these are GUI or widget libraries written in C or C++, with bindings that allow them to be called from other languages, such as Ruby, Python, and Perl. In almost all cases, you face a number of considerations, such as installation and distribution.
Which GUI tool set you decide to use depends, of course, on your particular needs. Criteria to consider include:
- A rich set of widgets or components
- Solid implementation
- Availability on multiple platforms (mostly Macintosh, Win32, KDE, and GNOME)
- Native look-and-feel for hosting platform
- Whether or not it is actively maintained
- Ease of creating custom widgets
- Nonrestrictive license
- Affordable cost
- Existing frameworks and libraries to speed development
- Mature IDEs and form layout tools
- Testing tools and frameworks
- Ease of packaging and deployment
If all you want is to toss up the occasional message box, or ask a user for some simple input, almost any of the toolkits I've listed will do. For simple requirements, you are likely better off focusing on platform availability, a suitable range of widgets, and appropriate cost. If you plan to distribute your application, you'll want to check the toolkit licensing. You also must be sure either that the user already has the required environment or that you can easily bundle all the needed libraries and widgets in either a stand-alone application or an installation package.
Once you move to complex applications, though, the requirements get tougher. For any application that goes beyond a few simple forms, you almost certainly want to have a form-designer tool. You also want a rich set of available widgets; you're usually better off reusing an existing date picker or file browser component, for example, than writing your own.
Each of the various C-based Ruby GUI toolkits has its share of good qualities, but none of them has emerged as a clear winner. They offer no obvious choice for general Ruby cross-platform desktop development. To varying degrees, they all have issues with installation, documentation, design tools, packaging, and deployment. Notably, none of them can beat, feature-for-feature, the one non-C option.
JRuby is an implementation of Ruby for the Java platform (see Resources). It allows you to execute Ruby code through the JVM. Ruby code running under JRuby can also load and use Java libraries, including Swing.
Several aspects of Java the platform make JRuby a good choice:
- It is solid and well tested
- It has strong community and vendor support
- The documentation is good and plentiful
- You have an excellent choice of IDEs and UI layout tools
- It's free to use (both in cost and license)
- The Java runtime is probably already installed on users' machines
If you write an application in (J)Ruby and use Swing for the UI, you need only ensure that the user has a recent version of the Java runtime and package your application to include the JRuby JAR file. And there's a Ruby tool for JRuby application packaging, making that aspect a nonissue.
There are a number of options for using Swing from Ruby:
- Raw, hand-coded, in-line calls to Swing objects: In the simplest case, you can refer to Swing objects much as you would any other Ruby object:
panel = Java::javax::swing::JFrame.new("JRuby panel") panel.show
- "Builder" and domain specific language (DSL)-style libraries: Constructing
panels and forms and then adding components all in hand-rolled code can get tricky
quite fast. Some libraries exist to make the Swing interaction more Ruby-like. For example:
- Cheri::Swing uses Ruby block syntax to generate the Swing code.
- Another library, Profligacy, offers a Ruby wrapper around the raw Swing calls to help you write more Swing code with less raw Java code. You still need to get familiar with the Swing API docs to make proper use of the Swing components.
- Cheri::Swing uses Ruby block syntax to generate the Swing code.
- The "we don't care where the Java class came from" approach: A third approach is simply to punt on trying to ease the creation of Swing objects using Ruby code and start from the assumption that a compiled Java class for the Swing objects already exists.
The last is the approach taken by the Monkeybars library (see Resources). A number of very good, free, graphical Swing UI layout editors exist. As with the use of the previously mentioned GUI toolkits (such as Fox and GTK), you don't need a UI editor for the occasional dialog box. But it's hard to beat such a tool for anything more than that, and it's pointless to skip these tools and code the UI by hand for a sophisticated desktop application.
Monkeybars is an open source Ruby library that connects existing Java Swing classes (that is, compiled Java classes that define your Swing UI) to Ruby code using a form of the Model, View, Controller (MVC) design pattern. MVC aims to separate view logic and UI components from application logic.
Because it uses the Java language and Swing libraries, Monkeybars builds on mature, robust technology. Unlike other current Swing libraries for JRuby, it is well-suited for constructing large, complex, multipaneled applications. As you'll see, creating a Monkeybars application entails some overhead, so it might not be your best choice for simple forms. But it's a reasonable choice for a JRuby desktop application of any complexity when you need:
- Reliable cross-platform deployment (assured to the degree that the user has a recent JVM installed)
- A large choice of UI widgets of arbitrary complexity
- Possibly complex UI form and panel construction and interaction
Like Profligacy, Monkeybars does not hide the Swing API. Nonetheless, because it works with compiled UI classes, you can make full use of any tool or application to generate the actual layout. Depending on your application's complexity, it is almost inevitable that at some point you'll need to reference Swing component API documentation and code examples to know what to do in your Ruby code. (But thanks to the nice integration of JRuby and Java libraries, you can easily wrap such Swing code in a Ruby API for easier reuse.) Programs built using Monkeybars can be arbitrarily complex, but you can follow some basic patterns to keep the code manageable.
The MVC pattern has a long history, with numerous variations. With Monkeybars, the basic premise is that for each Swing frame (that is, the UI object holding assorted components or widgets; in some cases this can be a modal panel) there are three Ruby files: a model, a view, and a controller. The model class holds the essential business logic and manages the data corresponding to that part of the application. It should be ignorant of any view or controller code that exists as a means to interact with the model. Keeping view and controller references out of your model makes it much easier to develop and test.
The view is another Ruby file with a reference to a specific Java class containing the compiled Swing code. The view manages the interaction of Swing components with model data. The view can have direct contact with the model, but it also works with a copy of the model as a means of passing data to the controller. This is important to keep in mind when you design your model class, because it ends up serving a dual purpose. The primary instance of the model is keeping long-term state and providing application logic; the copy used by the view is essentially a disposable data-access object. Models should be relatively cheap to instantiate, with shallow accessors provided for any data used by the view.
The view does not have direct contact with the controller. Instead, a signaling system is in place to abstract the interaction of controller and view. This decoupling makes it easier to test your views and controllers.
The controller class is where you define handlers for Swing events (such as button clicks and text-field changes) and control the state of the model. The controller keeps a reference to the primary instance of the model. It does not communicate directly with the view.
When a controller wants to get data from the view, the view provides a copy of the model populated with the current UI contents. The controller can then decide to update the primary instance of the model with this data or take some action based on these values. The controller can also tell the view to update itself and pass back updated values. You'll see this in action in the example application.
To get a feeling for creating a desktop application with Swing and Ruby, I'll take you on a tour through a simple program created using Monkeybars. (See Download for a link to the complete example source code.)
To get started, you need to have a few things in place:
- Get a copy of the jruby-complete.jar (see Resources for a download link).
- Install the Monkeybars library. You can install it as a gem:
sudo gem install monkeybars
You can also grab the current source code from the repository on gitorious.org (see Resources).
- Install the
sudo gem install rawr
rawris not required for writing a Monkeybars application, but it provides a number of useful
raketasks for turning a JRuby application into an executable JAR file. This article's example uses it.
The example application is a "flash card" program that reads in a text file that defines a number of "cards." It loops until shut down, periodically showing and hiding itself for brief periods. Basically, it's a tool for learning. For this example, the cards are a set of German vocabulary words and phrases. The program also reads a configuration file that defines the location of a cards-definition file and a few settings (show/hide speed, window size).
The goals of this example are to:
- Show the use of Monkeybars code generators, which automate the creation of common files
- Show the basic structure of a Monkeybars application
- Demonstrate the creation of each part of the Monkeybars MVC tuple
- Show how Monkeybars handles the mapping of application data to UI components
- Show packaging the application as an executable JAR file
Once installed, Monkeybars provides a command-line script to create an initial set of application files. To start a new Monkeybars project, execute the
monkeybars script that is installed with the gem. Name your project
$ monkeybars monkey_see
This creates a new directory at the given path (or in the current directory if you only give an application name) and adds core files and directories for the new application.
rawr is another Ruby library that grew out of Monkeybars. It handles assorted packaging tasks and provides a command-line script for creating a base Java class that a Monkeybars application can use to allow execution as a Java program (as opposed to running the application as a Ruby program via JRuby).
You use it with your Monkeybars application by going to your project directory and executing the
$ cd money_see; rawr install
You've seen how Monkeybars splits things up into model, view, and controller. The convention is to place these files into the same directory. To help this along, Monkeybars provides a
rake task to generate these files. You can create any one of the three or a full set (the more common case):
$ rake generate ALL=src/flash
This command creates a subdirectory called flash under src/, with three files: flash_controller.rb, flash_view.rb, and flash_model.rb. The first two have bare-bones classes that inherit from base Monkeybars classes. The model code does not; Monkeybars makes no assumptions about how you manage your application logic and data; that is entirely up to you.
For the application's interface, you need a Swing class that displays the flash-card
data. How you create this is up to you; nothing in Monkeybars ties it to any particular UI tool or Swing code generator. By convention, the Swing file gets placed in the same directory as its related tuple (src/flash/FlashFrame.java). You need to know the class package so you can pass it on to the view class. (Use the
flash package and name the class
Your screen layout should look like Figure 1:
Figure 1. The application's screen layout
A few key points: You should use a
JTextPane for the flash-card content so you can use HTML to format the rendered text. You should also use sensible names for the text pane and the button. It just makes it easier when you're working with the view to know something about the UI components. The program code is in Ruby, so use Ruby method-naming conventions: call the text
pane card_pane and the two menu items
quit_menu_item. Give them accelerator keys, too.
The name of the frame itself is not important; the view class can reference the components directly by name.
The model manages the application logic and data behind a given UI. A Monkeybars program generally has a model for each Java form. The example application has a single model to handle the flash-card data. The model code needs to be able to load data from a known location and offer a public method to provide that data.
For simplicity, you'll store the data in a text file in a subdirectory from where the application is running. Rather than hand-code HTML, you can use Textile markup and transform it using the RedCloth Ruby library. Each card entry will be separated by a delimiter string.
Textile is a text markup format that is intended to define HTML using simple plain-text
conventions. For example, to indicate
<em>italicized</em>, you instead write
_italicized_. RedCloth is a Ruby library, available as a gem, that converts Textile-formatted text into HTML.
Rubygems makes it quite easy to install and use third-party libraries, but because you'll want to package your code in a JAR and potentially distribute it, you need to be sure that all code is included with the application. To do this, unpack the RedCloth gem and copy the redcloth.rb file to the project's ruby/lib/ directory:
$ cd /tmp; gem unpack RedCloth
This creates /tmp/RedCloth-3.0.4 /(unless you have a different version of the gem installed). Copy /tmp/RedCloth-3.0.4/lib/redcloth.rb to the lib/ruby/ directory of your
In general, any Ruby libraries that are not core parts of your application should go (as a convention) under lib/ruby/. If you are using gems, you need to unpack the actual library files and add them to your project. Later in this article, you'll see how to tell your program how to find these files.
load_cards method handles reading in the text file from disk, splitting out each card, and assigning the results to the
@cards instance variable.
select_card method picks a card at random and assigns it the
@current_card instance variable. You'll use
attr_accessor to define methods for reading and setting this variable.
You'll arrange it so that whichever card is being displayed in the UI can be edited in
place. After editing, the
update_current_card method takes the contents of
@current_card and reinserts it into the
@cards array. A
save method writes the
@cards array back to disk.
The value of the
current_card method is what you want to render, and to do that, you need a view class.
A Monkeybars view class is the owner of the Java Swing class. If you open up
flash_view.rb, you can see that it invokes a class method,
set_java_class. This should be set to the Swing class defined for this view. In your code, this is
In general, a Monkeybars view class needs to do three things:
- Pass data in and out of the Swing components
- Manage assorted view-centric behavior (such as size and position)
- Respond to signals sent from the controller
Monkeybars provides a
map method that allows you to define how model methods are wired up to Swing controls. The simplest usage connects a UI component method and a model method:
map :view => :card_pane.text, :model => :current_card
This mapping uses the default behavior of making this a direct, two-way association.
That is, the results of the card_pane component's
method are passed to the model's
current_card= method. When
the view is updated from the model, the process is reversed:
card_pane.text. (Note: JRuby handles the Ruby/Java naming conversion, so the actual Swing method,
setText, can be invoked using
set_text = .)
Quite often, this form of simple mapping works fine, but sometimes, because of differences in data types or formatting or the needs of some application logic, you don't want direct data exchange. Monkeybars allows the use of intermediaries in the data exchange. A mapping can be passed a
:using parameter (that is, a hash key pointing to an array) that indicates the alternative means to use when moving data from the model to the view, and from the view to the model. (Another reason for
:using is to deal with situations when the value or state of a Swing component needs to be manipulated using component methods or child objects that do not fall into the general
For your code, we want to take a Textile-formatted string from the model and convert it
to HTML before assigning it to the
text property. To handle this, you'll create a
to_html method. You also don't want to update the model's
current_card value directly from the view. You'll have some special code for editing cards in the view, so you'll use
nil in place of what would otherwise be some view-to-model method name.
The result is this map:
map :view => :content_pane.text, :model => :current_card, :using => [:to_html, nil ]
You also want your Swing frame to present itself in a specific manner. By default, a Swing frame appears in the top left corner of the screen. For your application, you want it to show in the top right corner. You'll also give it a nice sliding effect so that it does not abruptly come and go.
A view class has a special instance variable called
@main_view_component that references its corresponding Swing class. View code interacts with Swing components through this object. To change the content of the flash-card text pane, for example, you could write:
@main_view_component.card_pane.text = "Some new text"
However, because this kind of code is essentially the reason the view class exists, Monkeybars arranges it so that you can omit explicit use of
@main_view_component and refer directly to its components:
card_pane.text = "Some new text"
Monkeybars::View class uses
method_missing to intercept such code, looks to see if it is a component reference, and if so it delegates the request to
Method calls on the Swing class, though you need the explicit reference:
@main_view_component.width = 500
To achieve a nice sliding effect, the view class has methods that manipulate the height and position of the Swing frame, gradually expanding and contracting it so that, on each rendering cycle, it slides down from the top of the screen, then slides back up.
Monkeybars is designed to decouple key parts of the MVC tuple. Because the view has a direct reference to a Java Swing object, it is typically the hardest part to test. Monkeybars aims to reduce direct view interaction with the model and controller. The controller, however, is responsible for handling UI events. Inevitably, this means the controller needs to direct the view to respond. The controller, though, does not directly communicate with the view class. Instead, it uses signals.
You'll see the controller side of this shortly. In the view, you need to define signal handlers using the
define_signal method. It takes a hash defining a signal name and a view method to handle that signal:
define_signal :name => :initialize, :handler => :do_roll_up
Handler methods must take two arguments: the model (passed in from the controller), and a transfer object. The transfer object is a transient hash used to move data back and forth between controller and view. Your view has signals defined for the initial positioning of the UI, the slide in, slide out sequence, and two for beginning and ending card editing. Each of these signal handlers is quite short. Here's the
def do_roll_up model, transfer hide move_to_top_right roll_up end
The editing sequence is triggered through menu events. The
Edit menu item toggles editing. In the view, the editing sequence means setting
card_pane.editable = true, then swapping out the HTML-rendered content with the raw Textile card text. You also need to change the component's content type so it correctly renders plain text.
When editing is complete, the reverse is done. The pane is given HTML, and
editable is set to
false. The view is concerned only with managing the Swing component's state; the controller handles instructing the model to perform the text updating and saving.
Your Swing object has some menu items, but you haven't put any code for them in the view class. That code belongs in the controller. The controller handles all UI events, such as button clicks, menu selection, and text-field changes. Monkeybars arranges it so that by default all such events coming from the Swing code are quietly swallowed. You need to define event handlers for just those things you care about. In the example application, those are menu clicks.
Event handlers take this form:
def your_component_name_action_performed # code end
(You can also define the handler to take the actual Swing event as a parameter should you want your code to use it.)
To handle the Quit menu item, you just need to exit:
def quit_menu_item_action_performed java.lang.System.exit(0) end
The Edit menu action needs a bit more:
def edit_menu_item_action_performed if @editing @editing = false signal :end_edit update_card else @editing = true signal :begin_edit update_model view_model, :current_card end end
The code handles the toggling of editing modes, using signals to drive the view. The key thing to note is how card text is moved using the controller's model instance (implicitly passed to the view by way of the signal), and the view's copy of the model, provided by the
Whenever a controller needs the current state of the user interface, it can use the
view_state method to reference the view's copy of the model and the current transfer object. Because grabbing the model copy from
view_state is so common, Monkeybars provides the
Your controller also has a method to kick off the initial rendering, and another that handles the show/hide display sequence. Both use signals to defer the actual presentation code to the view.
In addition to one or more MVC tuples, a Monkeybars application uses two key helper files to prepare and run your code.
Both are in the src/ directory. The manifest.rb file sets up library load paths and allows you to define which files are to be included based on whether the program is run straight from the file system or from a JAR file.
Earlier, you added
redcloth.rb to lib/ruby/. In order for
your application to locate this file, you need to add that directory to the load path. The same goes for the lib/java/ directory. So, make sure that manifest.rb includes these lines:
add_to_load_path "../lib/java" add_to_load_path "../lib/ruby"
Also in src/ is main.rb. This is the Ruby entry point for the application. Among other things, it defines a global error handler, and it is where you would place any platform-specific code to run before executing main application logic.
The example program uses a simple loop:
begin flash_card = FlashController.instance flash_card.init_view :flash_interval_seconds => 8, :show_for_seconds => 20, :window_height => 200, :data_src => 'data/cards.rc' while true do flash_card.present end rescue => e show_error_dialog_and_exit(e) end
With the code in place and a suitable data file, you can run the program. Use a
rawr rake task to create an executable JAR file. When you ran
rawr install at the start of the project, it created a Main.java file
under src/org/rubyforge/rawr/. Running the program from a JAR requires a Main Java class;
rawr generates this file, which contains basic code that looks for and interprets a main.rb file. (Or, if one is not found, it invents one in-line and uses that instead.)
rawr:jar task compiles this code and packages up your files into a JAR. The build_configuration.yaml file coordinates this. Before you create the JAR, edit this file to reflect the details of the application.
To kick off your program, first build the JAR file:
$ rake rawr:jar
Then invoke it:
$ java -jar package/deploy/monkey_see.jar
You should see the flash-card screen roll down from the top right corner, stay for a bit, then roll back up.
When the window is visible, you can use the menu items to edit the currently displayed card. To quit, use the Quit menu item (Alt+Q, if you've added the accelerator key).
The development of JRuby as a robust, viable alternative to the traditional C implementation of Ruby means that Ruby GUI toolkits can move beyond C-based options and use UI tools available to the Java platform. Because Swing is a standard part of a Java runtime installation, Swing components give (J)Ruby a mature and readily available graphical toolkit. Using the Java platform means that such applications can be readily built, packaged, and distributed to users on multiple platforms. By using the Monkeybars library, Ruby developers can build testable, maintainable, and complex desktop applications with increased ease.
This article's example was deliberately small, intended mainly as an introduction to what is possible for JRuby Swing GUI development. You can find more information and larger examples at the Monkeybars site (see Resources).
|Sample code for this article||j-monkeybars.zip||25KB||HTTP|
Monkeybars: API documentation, tutorials, and examples for the Monkeybars library.
JRuby: Visit the JRuby site.
"Ruby off the Rails" (Andrew Glover, developerWorks, December 2005): An overview of what Java developers can do with Ruby outside of Ruby on Rails.
Shoes: Find out more about these GUI toolkits for Ruby.
Profligacy and Cheri::Swing: These libraries make Swing interaction more Ruby-like but can't handle complex UIs.
Swing: Check out the Swing package documentation.
technology bookstore for books on these and other technical topics.
developerWorks Java technology zone: Find hundreds of articles about every aspect of Java programming.
Get products and technologies
James Britt is a principal at Happy Camper Studios, a Ruby application development company in Phoenix, Arizona. An active voice in the Ruby community, James has presented at Ruby conferences in the United States and Europe and written numerous technical articles for publications such as Dr. Dobbs and Linux Journal. He wrote most of the Web development section in Hal Fulton's book, The Ruby Way, 2nd ed. James also created and maintains Ruby-doc.org, the main documentation site for the Ruby language.