Monday, June 23, 2008

A JRuby Swing Library

Well, I haven't really posted anything yet, mostly because there is always something else to do. So, in an effort to actually start writing something, I have decided to post a little Ruby "library" I wrote a while ago. I put in the sarcastic quotes because it's quite a tiny library, if you can even call it that, but I think it has potential to be quite useful. I recently ported it to groovy for another personal project, and maybe I will post that next.

At any rate, before I list the code, I just wanted to say what its purpose is. Call me crazy (I probably am), but I like Java's Swing. Why? Well, it's portable in the sense that Java is portable... that is, you can easily install Java anwhere and thus have Swing at your fingertips... you can't really say that for most other UI libraries I have used (admittedly a small list). Either your window library of choice is X platform only (I'm looking at you Microsoft), or is just not nearly as pervasive as Java. Swing also does an OK job of looking decent, in my non-graphical-arts-critical eye (so take that with a grain of salt), and it can mostly look like your native platform with a special toggling (somewhere buried in the API that I end up always looking up, even though it is 1 or 2 lines of code).

So, with the good comes bad. I'm sure you can site many other bad things that I just don't have the experience or interest to point out, but the leading problem I am aware of, both in experience writing Swing GUIs and in reading other people mention the problem, is that it is an inherently tree structure being expressed in a non-tree syntax. The solution? XML! Well, there are many other options besides XML that provide a tree structure and don't make you want to gouge your eyes out, but XML happens to be pretty easy to parse in that most languages have a pre-packaged XML library. Thus, XML! Besides, XML is a lowest common denominator, everyone basically understands it, right? Isn't that what the XML enthusiasts try to shove down our throats?

Thus SwingXML was born, my tiny "library" that will take an XML definition, and load it into memory as a Swing component tree. Beyond representing the components in their inherent tree structure, it has the added benefit of separating your view definition from your view logic. In the groovy version I added a feature that I have not yet added to the Ruby version.

Without further ado:

require "java"
require "rexml/document"

class SwingXml
def initialize(xml)
document = REXML::Document.new(xml)
@widget_hash = {}
@widget = handle document.root, nil
end

attr_reader :widget

def [](widget_symbol)
@widget_hash[widget_symbol.to_sym]
end

private
def handle(element, parent)
raise ArgumentError, "Element #{element.name} does not have an sxmlId!" unless element.attributes.has_key? "sxmlId"
raise ArgumentError, "Duplicate key found on #{element.name} (#{element.attributes['sxmlId']})!" if @widget_hash.has_key? element.attributes["sxmlId"].to_sym

widget = eval "#{element.name}.new"
@widget_hash[element.attributes["sxmlId"].to_sym] = widget

element.attributes.each do |name, value|
eval "widget.set_#{name} #{value}" unless name.index("sxml") == 0
end

element.elements.each do |child|
handle child, widget
end

if element.attributes.has_key? "sxmlAction"
eval "parent.#{element.attributes['sxmlAction']} widget" unless parent.nil?
else
parent.add widget unless parent.nil?
end

widget
end
end

So, that's it! Pretty small for doing seemingly quite a bit, right? If you are wondering how to use the above "library", think of the SwingXml object as the root of your Swing component tree, while it is also a hash of the contained components. Typically, you would have a JFrame at the root, but you could even break them up more fine grained and have them be a panel or canvas of some sort. The components contained within the tree are accessible via their sxmlId values as indexes, which are an attribute you must specify on each xml element. You can even add more complicated things like layouts and whatnot with an sxmlAction which specifies how the object should be added to its parent.

As a simple example:

frame = SwingXml.new "<javax.swing.JFrame sxmlId=\"frame\" default_close_operation=\"javax.swing.JFrame::EXIT_ON_CLOSE\"><java.awt.FlowLayout sxmlId=\"frameLayout\" sxmlAction=\"set_layout\"/><javax.swing.JButton sxmlId=\"helloButton\" text=\"'Hello World!'\"/><javax.swing.JButton sxmlId=\"goodbyeButton\" text=\"'Goodby World!'\"/></javax.swing.JFrame>"

listener = java.awt.event.ActionListener.new
def listener.actionPerformed(e)
puts "Hello world!"
end
frame[:helloButton].add_action_listener listener

listener = java.awt.event.ActionListener.new
def listener.actionPerformed(e)
puts "Goodbye world!"
end
frame[:goodbyeButton].add_action_listener listener

frame[:frame].pack
frame[:frame].set_visible true

For easier reading, here is the XML but not embedded in the Ruby code:

<javax.swing.JFrame sxmlId=\"frame\" default_close_operation=\"javax.swing.JFrame::EXIT_ON_CLOSE\">
<java.awt.FlowLayout sxmlId=\"frameLayout\" sxmlAction=\"set_layout\"/>
<javax.swing.JButton sxmlId=\"helloButton\" text=\"'Hello World!'\"/>
<javax.swing.JButton sxmlId=\"goodbyeButton\" text=\"'Goodby World!'\"/>
</javax.swing.JFrame>

In the groovy port, I added some code to remove the need for javax.swing.JXXX and java.awt.XXX, but I didn't add this enhancement to the Ruby code, but it should be pretty trivial to add. That small addition makes the XML a lot less of a pain to deal with. Well... less of a pain anyways.

I hope you enjoyed this, and I hope to make these posts more frequent!

Mike

No comments: