-
Notifications
You must be signed in to change notification settings - Fork 30
Getting Started
This guide assumes you have basic knowledge of Ruby, and have JRuby 9.3.4 or greater and Java 8 or greater installed. Java 9+ will also work, but see the notes under Modules. JavaFX is not built into all jdks, so we recommend the JDK+FX builds of Zulu OpenJDK.
The first thing you need to do is to install the JRubyFX gem.
$ gem install jrubyfx
Success! JRubyFX should be installed now!
JRuby was harmed by java adoping modules. If you get strange method errors, ensure all JavaFX modules are opened for JRuby.
The file module-opens.txt
in the root folder of this repository should be appended to bin/.jruby.java_opts
in your JRuby install. Alternatively, for simple applications only, you can add the flags: -J--add-opens=javafx.graphics/com.sun.javafx.application=org.jruby.dist -J--add-opens=javafx.graphics/javafx.stage=org.jruby.dist
To generate that list for another version of java, run:
# in a folder with all the javafx jars
$ for f in *.jar; do b=`basename $f .jar`; unzip $f -d $b/; done
$ (for file in `find . -type f`; do echo `dirname $file`;done) |sort -u | grep -v META-INF |sed -n 's,./\([^/]\+\)/\(.\+\),--add-opens=\1V\2=org.jruby.dist,p' | tr / . | tr V /
Lets creating a JavaFX app that has the text "Hello World".
Create a new ruby file (this tutorial will call it hello.rb
). To use JRubyFX, we must require it in ruby, so add require 'jrubyfx'
at the top of the file. Now since JavaFX was originally for Java, we must create a class that inherits from javafx.application.Application
, however using it raw is no fun, so inherit from the ruby class JRubyFX::Application
to gain ruby's super power:
class HelloWorldApp < JRubyFX::Application
end
When we launch this application, JavaFX will call the start
method, and will pass in a stage that we can put our content on, so lets add that:
class HelloWorldApp < JRubyFX::Application
def start(stage)
end
end
With that stage (which you can think of as the window border), we can set the title and size:
def start(stage)
stage.title = "Hello World!"
stage.width = 800
stage.height = 600
stage.show()
end
The stage.show()
call shows the window. At this point, we can actually see something: the title and window size. Lets launch our app:
$ ruby hello.rb
Wait, what? Nothing happened! We never actually launched the app, we only defined the class. So add HelloWorldApp.launch()
to the end of hello.rb, and save. Now if you run it, it will work.
Code listing so far:
require 'jrubyfx'
class HelloWorldApp < JRubyFX::Application
def start(stage)
stage.title = "Hello World!"
stage.width = 800
stage.height = 600
stage.show()
end
end
HelloWorldApp.launch
Cool, we made an empty window, but usually you want something in it. Lets add a label that says "Hello World!". There are three ways to do everything in JRubyFX: the straight-up Java way, the generic JRubyFX way, and using a specific DSL (Domain Specific Language). As it sounds like, the Java way is basically copy-paste Java style, and the JRubyFX way is much more elegant, though its good to know both.
So far we have been doing it the Java way, so lets continue:
label = Label.new()
label.text = "Hello World!"
label = build(Label, text: "Hello World!")
Whoa! So what does build
do? Build takes a name of a class (Label
), creates a new instance, and sets the properties specified on it. Note that I used Ruby 1.9 hash style, text: "Hello World!"
is identical to the Ruby 1.8 hash of :text => "Hello World!"
. build
can also accept a ruby block to be executed against the object, so we could write the build like:
label = build(Label) do
text = "Hello World!"
end
For this single contrived example, it makes no sense, but for certain things (like animations and file save dialogs), it can save some serious typing. build
also supports constructors, and since Label
has a constructor that takes the text, we can use it:
label = build(Label, "Hello World")
The DSL is very similar to the build
way:
label_variable = label(text: "Hello World!")
or using the constructor variant:
label_variable = label("Hello World!")
Basically, the DSL way converts build(MyClass, _ctor_args_, *hash_args*) { *block* }
to my_class( _ctor_args_, *hash_args*) { *block* }
Now we can't just put the Label on the Stage, we must put it in a Scene so JavaFX knows how to layout the window. What is a Scene you ask? The Stage does not directly handle controls, it passes them onto the Scene object, which is the root of the UI tree. Scene normally contains at least one layout manager (like HBox, GridPanel, etc), and often more, however for the purposes of this demo, we will use the basic default layout manager Scene provides. The generated FXML later in this guide will use a proper layout manager. If you've used Java Swing before, JFrame is basically the Stage and the Scene combined. Search around the internet for more information on Stage, Scene and JavaFX, almost all information is applicable to JRubyFX.
Java always creates a scene of itself. Oh sorry, right:
scene = Scene.new(label)
stage.scene = scene
Yes, they could be on the same line like so:
stage.scene = Scene.new(label)
build()
has a cousin with()
that works the exact same way, except it does not create an object, only sets properties on it. Using with
, we can rewrite the first bit of our function that sets the title and width to:
def start(stage)
with(stage, title: "Hello World!", width: 800, height: 600)
stage.show()
end
Fancy, huh? but now that we've used with
, we can create our scene inside using the DSL:
def start(stage)
with(stage, title: "Hello World!", width: 800, height: 600) do
layout_scene do
label("Hello World!")
end
end
stage.show # Tip: most of the time, () can be removed from method calls
end
layout_scene
creates a scene with whatever is inside it as the root of the scene. It can also take arguments to the Scene constructor, so we could also write the above as:
def start(stage)
with(stage, title: "Hello World!") do
layout_scene(800, 600) do
label("Hello World!")
end
end
stage.show # we could also put the method call inside the block
end
If you run it now, you should find a large, white window with the tiny words Hello World! somewhere inside.
So now lets say your hello world program goes viral, and everyone wants it. You decide to hire a real designer for version 2.0 so it looks nice and professional. Unfortunatly, however, all the layout is in the code, mixed in with the business logic (hmm, just pretend it does awesome computations to show Hello World). The solution to this is to not put the layout and UI in the code. How? FXML. If you have ever played with .NET or QT, you might have come across WPF (Windows Presentation Foundation) or QML, respectively. WPF, QML, and JavaFX and very similar in that you can describe the layout of the UI completely declaratively in an xml (or json for QML) file (XAML for WPF, FXML for JavaFX). The good thing about this is that both WPF and JavaFX have visual designers for the XML files, which means designers don't have to worry about code!
JavaFX's designer is called JavaFX Scene Builder, and is a free download from the main JavaFX site (Tip: use Scene Builder 1.1 developer preview). Go install it now, and play with it for a bit. (or not. All fxml code needed it copy-pastable from this guide, but then you wouldn't learn anything)
Done? Good, copy and paste this code into a new file called Hello.fxml
:
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.paint.*?>
<?import javafx.scene.text.*?>
<HBox alignment="CENTER" xmlns:fx="http://javafx.com/fxml">
<children>
<Label text="Hello World!!" underline="true">
<font>
<Font size="66.0" />
</font>
</Label>
</children>
</HBox>
You can open this file with the Scene Builder and easily edit it. This is similar to the code we had before, with two exceptions: I set the font to a larger size and the label is in a HBox
that is centered. Most real UI's have some sort of root container to layout the controls such as an HBox
or a GridPanel
.
Once you've saved the Hello.fxml
file in the same directory as Hello.rb
, lets use the FXML file instead of ruby code to draw the UI. FXML files have controllers to handle events and such. Since this is a simple example, we don't need any events to be handled, so we will use the default JRubyFX::Controller
class. Modify the start method to look like this:
def start(stage)
with(stage, title: "Hello World!", width: 800, height: 600) do
fxml "Hello.fxml"
show
end
end
Then, right below the require 'jrubyfx'
add fxml_root File.dirname(__FILE__)
. This tells JRubyFX that all fxml file references are in the same dir as Hello.rb
. If you run it now, you should see the large Hello World! text centered in the middle of the window. So whats this about controllers? Read on...
Without interaction, most programs are useless. FXML lets you specify what method should be called when something happens, like a button click or key press. However, in order to call code, it needs to know where its located, which is where the controller comes in. FXML allows multiple types of actions: script actions in embedded javascript, and controller actions in Java/JRuby. If you want to use embedded script actions in javascript, this is not the guide for you; look it up on the internet.
In the Scene Builder, drag a Button
onto the surface of the designer, and click the Code section at the bottom of the properties on the right side. This is all the events that it supports. Yes, quite a few! Find the On Action one (it should be the first one), and set its value to #say_clicked
(note scene builder has the leading # already). Lets have it so when we click this button, the "Hello World!" text changes to "You clicked me!".
NOTE: If you are using the sample, you won't be able to move the button around, only in front of or after the label. This is how HBox'es work. Don't panic. If you really want absoloute positiong (not really a good idea for proper apps), then use an AnchorPane
.
CAUTION: When setting event handlers in FXML, the name MUST be prefixed with #. If you don't, JavaFX will think it is a script handler instead of a controller handler and complain (and crash your app). Also note that Scene Builder usually puts this in for you. Double check your names!
Now, to change the text of the label, we must somehow get access to the label in code. To do this, we must set the fx:id
property on it (first at the top of the code pane). Set the fx:id
value to "hello_label" and save the FXML file.
In the code, we need to create a new class that inherits from JRubyFX::Controller
class HelloWorldController
include JRubyFX::Controller
end
Reference the fxml file:
class HelloWorldController
include JRubyFX::Controller
fxml "Hello.fxml"
end
We need to add our event handlers:
class HelloWorldController
include JRubyFX::Controller
fxml "Hello.fxml"
def say_clicked
@hello_label.text = "You clicked me!"
end
end
Notice that we never set @hello_label
anywhere? When the FXML file is loaded, it finds all the fx:id
values and sets instance variables with the given names to the object the fx:id
is on.
The only thing needed to use our custom controller is to modify the call to fxml
in the stage setup and use HelloWorldController
instead of "Hello.fxml"
fxml HelloWorldController
Run it, and click the button.
Code listing for Hello.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.paint.*?>
<?import javafx.scene.text.*?>
<VBox alignment="CENTER" xmlns:fx="http://javafx.com/fxml">
<children>
<Label fx:id="hello_label" text="Hello World!" underline="true">
<font>
<Font size="66.0" />
</font>
</Label>
<Button mnemonicParsing="false" onAction="#say_clicked" prefHeight="49.0" prefWidth="166.0" text="Click Me!">
<font>
<Font size="23.0" />
</font>
</Button>
</children>
</VBox>
Code listing for Hello.rb:
require 'jrubyfx'
fxml_root File.dirname(__FILE__)
class HelloWorldApp < JRubyFX::Application
def start(stage)
with(stage, title: "Hello World!", width: 800, height: 600) do
fxml HelloWorldController
show
end
end
end
class HelloWorldController
include JRubyFX::Controller
fxml "Hello.fxml"
def say_clicked
@hello_label.text = "You clicked me!"
end
end
HelloWorldApp.launch
Now you know the basics of FXML and JRubyFX! If you haven't already, I suggest looking over samples/fxml/Demo.rb for a bit more detail. JavaFX help is all around, and most of it is applicable to JRubyFX. You should also check out the syntax notes wiki page.
All JavaFX enums can be written as either Enum::VALUE or :value. The ruby-style symbols should be lowercase snake (my_long_value) and will be automatically translated. In some cases, you can shorten the name even further (such as Modality::APPLICATION_MODAL can be :application_modal, :application, or just :app. See the syntax notes wiki page for more detail.
NOTE: To take advantage of the automatic translation, you must use javaclass.my_property= and NOT javaclass.setMyProperty
Most classes can be used 100% the Java way, but several have fun overrides/new methods to make them more ruby-ish. All the RDoc for Java::** classes shows these extensions (also visible in lib/javafx/core_ext/*.rb).
Got a large FXML file with dozens of fx:id's and events? Assuming you only have a FXML file:
$ jrubyfx-generator YourComplex.fxml NewAppFile.rb MyComplexAppName
And just like that, NewAppFile.rb
contains all the asserts and method declarations you need! Note that the generator is not well tested on complex documents, so if it fails, please send the FXML so we can try to fix it.
Note that all instances of using Scene Builder are replacable by writing FXML by hand, and in some cases it is less appropriate or even impossible to use Scene Builder to create FXML files (like changing root element). I HIGHLY suggest you start by using the Scene Builder, and looking at the FXML files it generates. Once you know enough FXML, you get rid of the Scene Builder from your workflow. On the other hand, it is very useful for tweaking values and getting immediate feedback. Unless you are allergic to it, I suggest keeping it around.