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:
Post a Comment