Introduction
Despite it being a scripting language, there is no shortage of Ruby GUI toolkits. Apart of Tk, its standard toolkit, many other GUI toolkits have Ruby bindings (like FXRuby, qtRuby and wxRuby). Ohers are listed here:
Ruby Programming/GUI Toolkit Modules
Except maybe for Shoes (designed by _why) other GUI toolkits are developed independently from Ruby and their respective Ruby bindings may or may not implement the GUI toolkit latest features. For instance, qtRuby, is available for QT4, but not for QT5, the toolkit's latest version.
Java ads another set of options for Ruby GUI development. With JRuby you can access Java standard GUI modules, Swing and SWT as well as Java FX, the latest addition to the Java GUI family.
The advantage of using Swing for your Ruby GUI is that it will run whereever Java is installed. Another advantage is you get all Java libraries available to your Ruby scripts for free. The disadvantage is that you will need to install JRuby, the Ruby version that runs on JVM.
Once you decide to use Java for your Ruby application GUI you still have several options to choose from. You can use plain Swing or one of its JRuby wrappers (like MonkeyBars or Cheri - these wrappers are found at various levels of completeness). You can use raw SWT. Lastly, you can use projects like Limelight, Profligacy or Swiby - all these projects attempt to make JRuby GUI development a more pleasant experience, but are also at various levels of completeness.
For the Web application people there is Rails, of cource.
Using raw Swing in your Ruby scripts can be intimidating for a programmer without experience in UI programming, on the other hand, I believe that people that have had experience in other GUI frameworks like Windows Forms will not have much trouble picking it up.
The basic structure of a JRuby Swing script
A possible outline of JRuby Swing script could be:
- use java_import to import all the necessary Java classes. For brevity, put all imports in a separate file called 'java_imports.rb' and require that file in each of your scripts.
- Create a subclass of the JFrame Java class named TopFrame as top application container. In the TopFrame#initialize you need to call super(), otherwise JFrame doesn't get properly initialized. Application components are initialized in a separate method, TopFrame#init_components(). set_default_close_operation() tells the application to exit when the TopFrame instance is closed.
- Back in TopFrame#initialize() you call init_components(), pack() and set_visible().
- Use SwingUtilities#invoke_later() to create a new TopFrame instance on the Swing Event Dispatch Thread.
Here is the 'java_imports.rb' code:
# java_imports.rb
java_import javax.swing.JFrame
java_import javax.swing.SwingUtilities
java_import java.awt.Dimension
java_import javax.swing.JSplitPane
java_import javax.swing.JTextArea
java_import java.awt.BorderLayout
java_import javax.swing.JMenuBar
java_import javax.swing.JMenu
java_import javax.swing.JMenuItem
java_import java.lang.System
java_import javax.swing.JToolBar
java_import javax.swing.JButton
java_import javax.swing.ImageIcon
java_import java.awt.Toolkit
java_import java.awt.datatransfer.StringSelection
java_import java.awt.datatransfer.Transferable
java_import java.awt.datatransfer.DataFlavor
java_import javax.swing.JScrollPane
java_import javax.swing.JTree
java_import javax.swing.tree.DefaultTreeModel
java_import javax.swing.tree.DefaultMutableTreeNode
java_import javax.swing.tree.TreeModel
java_import javax.swing.event.TreeWillExpandListener
java_import javax.swing.tree.TreeSelectionModel
java_import javax.swing.JFileChooser
java_import javax.swing.JProgressBar
java_import java.awt.event.ActionListener
java_import javax.swing.UIManager
JavaFile = java.io.File
JavaThread = java.lang.Thread
You'll notice that there are more Java classes imported that needed in the below JRuby script - this is because I use the same imports file for all my Ruby scripts in this project.
01_swing_template.rb code:
#01_swing_template.rb
require 'java_imports.rb'
class TopFrame < JFrame
def initialize
super
init_components()
pack()
set_visible(true)
end
def init_components()
set_default_close_operation(JFrame::EXIT_ON_CLOSE)
set_preferred_size(Dimension.new(400, 300))
end
end
SwingUtilities.invoke_later do
TopFrame.new
end
JFrame::EXIT_ON_CLOSE lets your application exit when the JFrame instance is closed.
The pack() method sizes the frame so that all its contents are at or above their preferred sizes
set_visible() shows the JFrame on the screen.
If everything is all right running this script displays an empty Swing JFrame on the screen:
Why not forget about SwingUtilities.invoke_later() and just instantiate TopFrame right there in the main script thread?
There are numerous links about Swing threading issues, here are some of them:
To invokeLater() or not
I have made a couple of scripts, one that doesn't use SwingUtilities#invokeLater() and one that does. Here is the first one:
#02_without_swing_utilities.rb
require 'java_imports.rb'
puts '----------------------------------------'
puts 'Before JFrame.new():'
puts "Thread name: #{JavaThread.current_thread.name}"
puts "Is event dispatch thread: #{SwingUtilities.event_dispatch_thread?}"
JavaThread.current_thread.thread_group.list
frame = JFrame.new()
frame.set_default_close_operation(JFrame::EXIT_ON_CLOSE)
frame.set_preferred_size(Dimension.new(400, 300))
button = JButton.new('Button with action listener')
button.add_action_listener do |event|
puts '----------------------------------------'
puts 'In action listener:'
puts "Thread name: #{JavaThread.current_thread.name}"
puts "Is event dispatch thread: #{SwingUtilities.event_dispatch_thread?}"
JavaThread.current_thread.thread_group.list
end
frame.get_content_pane.add(button, BorderLayout::WEST)
frame.pack()
puts '----------------------------------------'
puts 'Before JFrame.setVisible():'
puts "Thread name: #{JavaThread.current_thread.name}"
puts "Is event dispatch thread: #{SwingUtilities.event_dispatch_thread?}"
JavaThread.current_thread.thread_group.list
frame.set_visible(true)
The output is:
----------------------------------------
Before JFrame.new():
Thread name: main
Is event dispatch thread: false
java.lang.ThreadGroup[name=main,maxpri=10]
Thread[main,5,main]
Thread[Ruby-0-JIT-1,1,main]
Thread[Ruby-0-JIT-2,1,main]
java.lang.ThreadGroup[name=Ruby Threads#11483240,maxpri=10]
----------------------------------------
Before JFrame.setVisible():
Thread name: main
Is event dispatch thread: false
java.lang.ThreadGroup[name=main,maxpri=10]
Thread[main,5,main]
Thread[Ruby-0-JIT-1,1,main]
Thread[Ruby-0-JIT-2,1,main]
Thread[AWT-EventQueue-0,6,main]
java.lang.ThreadGroup[name=Ruby Threads#11483240,maxpri=10]
----------------------------------------
In action listener:
Thread name: AWT-EventQueue-0
Is event dispatch thread: true
java.lang.ThreadGroup[name=main,maxpri=10]
Thread[AWT-EventQueue-0,6,main]
Thread[DestroyJavaVM,5,main]
java.lang.ThreadGroup[name=Ruby Threads#11483240,maxpri=10]
Before JFrame constructor call the current thread is main and there is no EDT. Before JFrame#setVisible() the current thread is still main, but the EDT appears in the list. In the button action listener EDT is the current thread.
The other script uses SwingUtilities#invoke_later() to initialize JFrame:
#03_with_swing_utilities.rb
require 'java_imports.rb'
class TopFrame < JFrame
def initialize
super
init_components()
pack()
set_visible(true)
puts '----------------------------------------'
puts 'In JFrame constructor:'
puts "Thread name: #{JavaThread.current_thread.name}"
puts "Is event dispatch thread: #{SwingUtilities.event_dispatch_thread?}"
JavaThread.current_thread.thread_group.list
end
def init_components()
set_default_close_operation(JFrame::EXIT_ON_CLOSE)
set_preferred_size(Dimension.new(400, 300))
button = JButton.new('Button with action listener')
button.add_action_listener do |event|
puts '----------------------------------------'
puts 'In action listener:'
puts "Thread name: #{JavaThread.current_thread.name}"
puts "Is event dispatch thread: #{SwingUtilities.event_dispatch_thread?}"
JavaThread.current_thread.thread_group.list
end
get_content_pane.add(button, BorderLayout::EAST)
end
end
puts '----------------------------------------'
puts 'Before SwingUtilties.invokeLater():'
puts "Thread name: #{JavaThread.current_thread.name}"
puts "Is event dispatch thread: #{SwingUtilities.event_dispatch_thread?}"
JavaThread.current_thread.thread_group.list
SwingUtilities.invoke_later do
TopFrame.new
end
puts '----------------------------------------'
puts 'After SwingUtilities.invokeLater():'
puts "Thread name: #{JavaThread.current_thread.name}"
puts "Is event dispatch thread: #{SwingUtilities.event_dispatch_thread?}"
JavaThread.current_thread.thread_group.list
The output is:
----------------------------------------
Before SwingUtilties.invokeLater():
Thread name: main
Is event dispatch thread: false
java.lang.ThreadGroup[name=main,maxpri=10]
Thread[main,5,main]
Thread[Ruby-0-JIT-1,1,main]
Thread[Ruby-0-JIT-2,1,main]
java.lang.ThreadGroup[name=Ruby Threads#11483240,maxpri=10]
----------------------------------------
After SwingUtilities.invokeLater():
Thread name: main
Is event dispatch thread: false
java.lang.ThreadGroup[name=main,maxpri=10]
Thread[main,5,main]
Thread[Ruby-0-JIT-1,1,main]
Thread[Ruby-0-JIT-2,1,main]
Thread[AWT-EventQueue-0,6,main]
java.lang.ThreadGroup[name=Ruby Threads#11483240,maxpri=10]
----------------------------------------
In JFrame constructor:
Thread name: AWT-EventQueue-0
Is event dispatch thread: true
java.lang.ThreadGroup[name=main,maxpri=10]
Thread[AWT-EventQueue-0,6,main]
Thread[DestroyJavaVM,5,main]
java.lang.ThreadGroup[name=Ruby Threads#11483240,maxpri=10]
----------------------------------------
In action listener:
Thread name: AWT-EventQueue-0
Is event dispatch thread: true
java.lang.ThreadGroup[name=main,maxpri=10]
Thread[AWT-EventQueue-0,6,main]
Thread[DestroyJavaVM,5,main]
java.lang.ThreadGroup[name=Ruby Threads#11483240,maxpri=10]
Before and after calling SwingUtilities#invoke_later() the current thread is main, but after SwingUtilities#invoke_later() the EDT appears in the list. Both on JFrame constructor and JButton action listener EDT is the current thread.
JRUby Swing application look and feel
Swing applications have several options regarding GUI elements, known as 'look and feel'. You can list available application skins using this script:
# 4_look_and_feel_options.rb
java_import javax.swing.UIManager
UIManager.get_installed_look_and_feels().each do |info|
puts info.get_name()
end
On my Linux machine the output is:
Metal
Nimbus
CDE/Motif
GTK+
You can use any of these names to set (and change!) your application skin. Here is how:
# 05_set_lool_and_feel.rb
java_import javax.swing.JFrame
java_import javax.swing.SwingUtilities
java_import java.awt.Dimension
java_import java.awt.BorderLayout
java_import javax.swing.JButton
java_import javax.swing .JLabel
java_import javax.swing.UIManager
class TopFrame < JFrame
def initialize
super
init_components()
pack()
set_visible(true)
end
def init_components()
set_default_close_operation(JFrame::EXIT_ON_CLOSE)
set_preferred_size(Dimension.new(400, 300))
button = JButton.new('Swing JButton look and feel')
get_content_pane.add(button, BorderLayout::NORTH)
label = JLabel.new(BorderLayout::NORTH)
label.text = 'JLabel look and feel'
get_content_pane.add(label)
end
end
SwingUtilities.invoke_later do
UIManager.get_installed_look_and_feels().each do |info|
if info.name == 'GTK+'
UIManager.set_look_and_feel(info.class_name)
end
end
TopFrame.new
end
Here is how the above listed look and feel options look on my machine:
Adding Widgets
When it comes to adding widgets to your forms this is the standard procedure:
- Create an instance of the widget you want to use on your form
- Set desired properties for the widget to be added
- Get the parent container content pane. In case of JFrame you obtain the reference to its content pane using JFrame#get_content_pane()
- Use the content panes add() method to add your witget to the parent container
As for creating widget instances and their scope this depends on what you need. If your widget is never to be changed or used after it has had been added to the container you can place its constructor call directly in the add() method. If you need to set or modify some of your widget attributes you can store a widget instance into a local variable, then customize its properties and finally add the local variable representing the widget using the add() method. If the widgets on your form will need to communicate among each other you can make them instance variables of your class that inherits JFrame.
Here is a simple example of adding two widgets, a JButton and a JLabel to a JFrame:
# 06_adding_widgets.rb
require 'java_imports.rb'
class TopFrame < JFrame
attr_accessor :button, :label
def initialize
super
init_components()
pack()
set_visible(true)
end
def init_components()
set_default_close_operation(JFrame::EXIT_ON_CLOSE)
content_pane = get_content_pane()
@button = JButton.new('Button text')
@label = JLabel.new('Label text')
content_pane.add(@button, BorderLayout::NORTH)
content_pane.add(@label, BorderLayout::SOUTH)
end
end
SwingUtilities.invoke_later do
TopFrame.new
end
Here is what the form looks like:
The TopFrame class inherits JFrame and has two instance variables @button and @label (just for show - there is no interaction between them). All the widgets are set up in TopFrame#init_components().
JFrame.content_pane has a layout manager that helps you place widgets on it. The default layout manager is BorderLayout - you can use it to place widgets to the NORTH, EAST, SOUTH, WEST and CENTER of the frame. There are other, more flexible layout managers.
Jruby does some conversions of Java methods to let you write more Ruby-like code. For instance, Java's JFrame.getContentPane() becomes get_content_pane() in JRuby. In the same vein Java's SwingUtilities.invokeLater() becomes SwingUtilities.invoke_later() in Ruby. This is not a rule - you can use the option you prefer.
Adding a menu to your application
Menus are ubiquotus in desktop (and not just desktop) applications. Adding a menu bar to a Swing application is a bit different than adding other widgets. Here is the code:
# 7_menu_bar.rb
require 'java_imports.rb'
class TopFrame < JFrame
attr_accessor :main_menu_bar
def initialize
super
init_components()
pack()
set_visible(true)
end
def init_components()
set_default_close_operation(JFrame::EXIT_ON_CLOSE)
set_preferred_size(Dimension.new(600, 400))
@main_menu_bar = MainMenuBar.new()
set_jmenu_bar(@main_menu_bar)
end
end
class MainMenuBar < JMenuBar
attr_accessor :file_menu, :new_menu_item, :exit_menu_item,
:edit_menu, :cut_menu_item, :copy_menu_item
def initialize
super()
init_components()
end
def init_components
@file_menu = JMenu.new('File')
@new_menu_item = JMenuItem.new('New')
@new_menu_item.add_action_listener do |event|
puts 'the New menu item clicked'
end
@file_menu.add(@new_menu_item)
@exit_menu_item = JMenuItem.new('Exit')
@exit_menu_item.add_action_listener do |event|
System.exit(0)
end
@file_menu.add(@exit_menu_item)
add(@file_menu)
@edit_menu = JMenu.new('Edit')
@cut_menu_item = JMenuItem.new('Cut')
@cut_menu_item.add_action_listener do |event|
puts 'The Cut menu item clicked'
puts 'Source: ' + event.source.to_s
puts 'Action command: ' + event.action_command
end
@edit_menu.add(@cut_menu_item)
@copy_menu_item = JMenuItem.new('Copy')
@copy_menu_item.add_action_listener do |event|
puts 'The Copy menu item clicked'
end
@edit_menu.add(@copy_menu_item)
add(@edit_menu)
end
end
SwingUtilities.invoke_later do
TopFrame.new
end
Jframe with the main menu:
As you can see you use JFrame#setJMenuBar() instead of JFrame#getComponentPane#add(). The rest is as expected. JMenuItem instances (like 'Open', 'Copy' or 'Exit') are added to JMenu instances (like 'File' or 'Edit') which in turn are added to JMenuBar to construct the meny hierarchy.
More on JRuby fiddling with Java methods:
In TopFrame#init_components() set_default_close_operation() is a Java setter method just as set_preferred_size(). JRuby lets you use getter and setter methods like properties and in that case that snippet of code would like:
def init_components()
self.default_close_operation = JFrame::EXIT_ON_CLOSE
self.preferred_size = Dimension.new(600, 400)
@main_menu_bar = MainMenuBar.new()
self.jmenu_bar = @main_menu_bar
end
I've qualified the method calls with self and I can use them as properties. All the lookups (jmeny_bar => set_jmenu_bar() => setJMenuBar()) are done by JRuby.
Another thing JRuby will do to help you is to convert its code blocks to anonymous Java classes. For instance the JButton#addActionListener() expects a class that implements the ActionListener Java interface. ActionListener is specified to have just one method: actionPerformed() . Instead of creating a separate JRuby class that implements ActionListener and passing its instance to JButton, just pass a code block to the add_action_listener():
@cut_menu_item.add_action_listener do |event|
puts 'The Cut menu item clicked'
puts 'Source: ' + event.source.to_s
puts 'Action command: ' + event.action_command
end
The code between do and end becomes actionPerformed method body and |event| of an ActionEvent instance. Note that the method name is not specified.
Toolbars
Toolbars are also common elements of a desktop application. Here is an example:
# 08_toolbar.rb
require 'java_imports.rb'
class TopFrame < JFrame
attr_accessor :main_toolbar, :text_area
def initialize
super
init_components()
pack()
set_visible(true)
end
def init_components()
set_default_close_operation(JFrame::EXIT_ON_CLOSE)
set_preferred_size(Dimension.new(600, 400))
content_pane = get_content_pane()
puts get_layout().class
@text_area = JTextArea.new()
content_pane.add(@text_area, BorderLayout::CENTER)
@main_toolbar = MainToolBar.new(@text_area)
content_pane.add(@main_toolbar, BorderLayout::NORTH)
end
end
class MainToolBar < JToolBar
attr_accessor :copy_button, :paste_button, :text_container, :clipboard
def initialize(text_container)
super()
init_components()
set_floatable(false)
@text_container = text_container
@clipboard = Toolkit.default_toolkit.system_clipboard
end
def init_components()
copy_icon = ImageIcon.new('assets/edit-copy.png')
@copy_button = JButton.new(copy_icon)
@copy_button.add_action_listener do |event|
selection = StringSelection.new(@text_container.selected_text)
@clipboard.set_contents(selection, selection)
end
add(@copy_button)
paste_icon = ImageIcon.new('assets/edit-paste.png')
@paste_button = JButton.new(paste_icon)
@paste_button.add_action_listener do |event|
transferable = @clipboard.get_contents(nil)
if transferable.is_data_flavor_supported(DataFlavor::stringFlavor)
text = transferable.get_transfer_data(DataFlavor::stringFlavor)
position = @text_container.caret_position
@text_container.insert(text, position)
end
end
add(@paste_button)
end
end
SwingUtilities.invoke_later do
TopFrame.new
end
Toolbars in Java are represented by the JToolBar class. JToolBar is a container and can contain other widgets like JButtons, JCheckBoxes, JRadioButtons, JLabels, JComboBoxes etc. The above toolbar has two buttons for which I used icons from Tango Icon LIbrary. The icons are located in the /assets subdirectory. The first toolbar button (@copy_button) copies the selected text from the JTextArea located on the right hand side of the JFrame to the system clipboard. The second toolbar button (@paste_button) copies the clipboard contents to the JTextArea. Here is what it looks like.
Using JTree
The JTree widget is on of more complex Swing components - it lets you represent hierarchical data. The real strength of JTree is its underlying model. Here is a custom model that can be used with a JTree instance:
require 'java_imports.rb'
class FileSystemTreeModel
include TreeModel
attr_accessor :root
def initialize(path)
@root = JavaFile.new(path)
end
def get_root()
return @root
end
def is_leaf(node)
return node.file?
end
def get_child_count(parent)
children = parent.list
if children == nil
return 0
else
return children.length
end
end
def get_child(parent, index)
children = parent.list
if children == nil or children.length <= index
return nil
else
return parent.list_files[index]
end
end
def get_index_of_child(parent, child)
entries = parent.list
hash = Hash[entries.map.with_index.to_a]
puts hash[child]
return hash[child]
end
def value_for_path_changed(path, new_value)
end
def add_tree_model_listener(listener)
end
def remove_tree_model_listener(listener)
end
end
The model implements the TreeModel interface to reflect a file system tree on a computer disk. The first method the class has to implement is #get_root(). I implemented root as public so that it can be changed after the model instantiation, but this is not a requirement. The next implemented method returns true if a model node is lief (i.e. has no subnodes). In case a node is a file then it is also a leaf node because it has no children.
#get_child_count() returns subnode count on a given node. This allows for lazy JTree initialization. #get_child(parent, index) returns a child node by index and get_index_of_child(parent, child) returns a childs index. The last three methods are not implemented because this JTree is not changeable.
Note that the FileSystemTreeModel class doesn't explicitly implement recursion needed to traverse the file system.
The JTree class itself is simple enough:
require 'pathname'
require 'java_imports.rb'
require 'file_system_tree_model.rb'
class TopFrame < JFrame
attr_accessor :fs_tree, :scroll_pane
def initialize
super
init_components()
pack()
set_visible(true)
end
def init_components()
set_default_close_operation(JFrame::EXIT_ON_CLOSE)
set_preferred_size(Dimension.new(300, 400))
content_pane = get_content_pane()
@fs_tree = FileSystemTree.new()
@scroll_pane = JScrollPane.new(@fs_tree)
content_pane.add(@scroll_pane, BorderLayout::CENTER)
end
end
class FileSystemTree < JTree
def initialize
super()
init_components()
end
def init_components()
home_dir = System.get_property('user.home')
model = FileSystemTreeModel.new(home_dir)
set_model(model)
end
def convertValueToText(value, selected, expanded, leaf, row, has_focus)
return value.name
end
end
SwingUtilities.invoke_later do
UIManager.get_installed_look_and_feels().each do |info|
if info.name == 'GTK+'
UIManager.set_look_and_feel(info.class_name)
end
end
TopFrame.new
end
JTree is embedded in a JScrollPane. One thing to mention is that the FileSystemTree overrides the JTree#convertValueToText() method to display a file name as a node text (instead of its path). In this case the method first argument is Java File class and you can use value.name() to display the underlying file name as the node text instead of using the full path.
Here is the JTree in action:
A more involved example
Lastly, a more complex example, a JFrame that has a menu bar, a toolbar, a split pane with a scrool pane on the left hand side and a text area on the right hand side. A JTree is embedded in the scroll pane.
# 10_hand_made_frame.rb
require 'java_imports.rb'
require 'file_system_tree_model.rb'
class TopFrame < JFrame
attr_accessor :split_pane, :main_menu_bar, :toolbar, :clipboard
def initialize
super
init_components()
pack()
set_visible(true)
end
def init_components()
set_default_close_operation(JFrame::EXIT_ON_CLOSE)
set_preferred_size(Dimension.new(600, 400))
content_pane = get_content_pane()
@main_menu_bar = MainMenuBar.new()
set_jmenu_bar(@main_menu_bar)
@split_pane = TreeSplitPane.new()
content_pane.add(@split_pane, BorderLayout::CENTER)
@toolbar = MainToolBar.new()
content_pane.add(@toolbar, BorderLayout::NORTH)
@clipboard = Toolkit.default_toolkit.system_clipboard
copy_listener = ActionListener.impl do |event|
selection = StringSelection.new(@split_pane.text_area.text)
@clipboard.set_contents(selection, selection)
end
@toolbar.copy_button.add_action_listener(copy_listener)
@main_menu_bar.copy_menu_item.add_action_listener(copy_listener)
open_directory_listener = ActionListener.impl do |event|
file_chooser = JFileChooser.new()
file_chooser.file_selection_mode = JFileChooser::DIRECTORIES_ONLY
file_chooseraccept_all_file_filter_used = false
return_value = file_chooser.show_open_dialog(self)
if return_value == JFileChooser::APPROVE_OPTION
root_dir = file_chooser.selected_file.path
model = FileSystemTreeModel.new(root_dir)
@split_pane.tree.set_model(model)
end
end
@main_menu_bar.open_menu_item.add_action_listener(open_directory_listener)
@toolbar.open_button.add_action_listener(open_directory_listener)
end
end
class TreeSplitPane < JSplitPane
attr_accessor :tree, :text_area, :scroll_pane
def initialize
super()
init_components()
set_left_component(@scroll_pane)
set_right_component(@text_area)
java_send :setDividerLocation, [Java::int], 250
end
def init_components()
@tree = FileSystemTree.new()
@tree.selection_model.selection_mode = TreeSelectionModel::SINGLE_TREE_SELECTION
@tree.add_tree_selection_listener do |event|
node = @tree.last_selected_path_component
if node.instance_of?(JavaFile)
info = <<-INFO
Path: #{node.path}
Name: #{node.name}
Size: #{node.length}
INFO
else
info = ''
end
@text_area.text = info
end
@scroll_pane = JScrollPane.new(@tree)
@text_area = JTextArea.new()
@text_area.editable = false
end
end
class MainMenuBar < JMenuBar
attr_accessor :file_menu, :open_menu_item, :exit_menu_item,
:edit_menu, :copy_menu_item
def initialize
super()
init_components()
end
def init_components
@file_menu = JMenu.new('File')
@open_menu_item = JMenuItem.new('Open')
@file_menu.add(@open_menu_item)
@exit_menu_item = JMenuItem.new('Exit')
@exit_menu_item.add_action_listener do |event|
System.exit(0)
end
@file_menu.add(@exit_menu_item)
add(@file_menu)
@edit_menu = JMenu.new('Edit')
@copy_menu_item = JMenuItem.new('Copy')
@edit_menu.add(@copy_menu_item)
add(@edit_menu)
end
end
class MainToolBar < JToolBar
attr_accessor :open_button, :copy_button
def initialize()
super()
init_components()
set_floatable(false)
end
def init_components()
open_file_icon = ImageIcon.new('assets/document-open.png')
@open_button = JButton.new(open_file_icon)
add(@open_button)
copy_icon = ImageIcon.new('assets/edit-copy.png')
@copy_button = JButton.new(copy_icon)
add(@copy_button)
end
end
class FileSystemTree < JTree
def initialize
super()
init_components()
end
def init_components()
home_dir = System.get_property('user.home')
model = FileSystemTreeModel.new(home_dir)
set_model(model)
end
def convertValueToText(value, selected, expanded, leaf, row, has_focus)
return value.name
end
end
SwingUtilities.invoke_later do
UIManager.get_installed_look_and_feels().each do |info|
if info.name == 'GTK+'
UIManager.set_look_and_feel(info.class_name)
end
end
TopFrame.new
end
Here is the frame:
Most of the features are seen in the previous examples, the only thing to mention is:
java_send :setDividerLocation, [Java::int], 250
to resolve ambiguous Java method call. Note that in this case you need to use the Java method name verbatim.
Designer generated forms
Many people prefer using WYSIWYG designer tools for designing Swing forms. It is possible to use a tool such as Eclipse Window Builder to design a form, compile the generated code into a class file, include the file in your JRuby script.