MattHicks.com

Programming on the Edge

I Hate JavaFX; I Love JavaFX!

Published by Matt Hicks under , , , , , on Friday, February 27, 2009
I've posted a few places and gotten into more than a few rants publicly about how awful I think it is that Sun has been pushing JavaFX to the Java community. This has not been based on what JavaFX provides, because I think that's absolutely spectacular, but rather that they created a whole new language (JavaFX Script) that they force you into using rather than Java code itself. I've heard all the well reasoned arguments for why this is from the binding support to simplified UI design concepts, but as a professional ActionScript developer as well as a professional Java developer, I believe that using a language that stinks of ActionScript is a major step backwards for the Java community. However, like I said, JavaFX has done an absolutely wonderful job of adding to the UI power of the Java arsenal though, and I want to use it. Since I know that the JavaFX Script compiles into Java byte-code, I knew there had to be some level of access to the underlying API structure directly from Java, but nobody seems to know how or where to do it. After quite a bit of digging I stumbled upon these two pages:

http://blogs.sun.com/javafx/entry/how_to_use_javafx_in

http://forums.sun.com/thread.jspa?threadID=5354921&tstart=1

They looked pretty ugly to me, but put me on the track to actually using JavaFX directly in Java without scripting. I sort of combined those two together to come up with what I think is a little bit more concise example:


package test;

import java.awt.BorderLayout;

import javax.swing.JFrame;
import javax.swing.JPanel;

import com.sun.scenario.scenegraph.JSGPanel;
import com.sun.scenario.scenegraph.SGNode;

import javafx.scene.Group;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;

public class TestJFX {
public static void main(String[] args) throws Exception {
Group group = new Group(); {
}
Rectangle rect = new Rectangle(); {
rect.$width.setAsFloat(200.0f);
rect.$height.setAsFloat(200.0f);
rect.$fill.set(Color.$BLUE);
}
Text text = new Text(); {
text.$x.setAsFloat(20.0f);
text.$y.setAsFloat(20.0f);
text.$content.set("Greetings Earthling!");
text.$fill.set(Color.$WHITE);
}
group.$content.insert(rect);
group.$content.insert(text);
SGNode node = group.getSGGroup();
JSGPanel sgPanel = new JSGPanel();
sgPanel.setScene(node);

JPanel panel = new JPanel();
panel.setLayout(new BorderLayout());
panel.add(sgPanel, BorderLayout.CENTER);

JFrame frame = new JFrame("Test JavaFX");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(panel);
frame.setSize(800, 600);
frame.setVisible(true);
}
}


This isn't the prettiest or most efficient way to code, but it's a start. Next I decided that I'd like to reproduce the Clock example that JavaFX shows here:

http://java.sun.com/javafx/1/tutorials/build-javafx-nb-app/

This gave me a much more practical run through JavaFX from the programmatic perspective and allowed me to verify that it is in fact possible to write a complete application using Java making calls to JavaFX:


package test;

import java.awt.BorderLayout;
import java.io.IOException;
import java.util.Calendar;

import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;

import com.sun.javafx.functions.Function0;
import com.sun.javafx.runtime.location.FloatBindingExpression;
import com.sun.javafx.runtime.location.FloatVariable;
import com.sun.scenario.scenegraph.JSGPanel;
import com.sun.scenario.scenegraph.SGNode;

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.lang.Duration;
import javafx.scene.Group;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.paint.Color;
import javafx.scene.shape.ArcTo;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Translate;

/**
* @author Matt Hicks (matt@matthicks.com)
*/
public class Clock {
private float radius;
private float centerX;
private float centerY;

private Calendar calendar;
private int hours;
private int minutes;
private int seconds;

private FloatVariable hoursVariable;
private FloatVariable minutesVariable;
private FloatVariable secondsVariable;

public Clock() throws IOException {
// Initial setup
calendar = Calendar.getInstance();
radius = 77;
centerX = 144;
centerY = 144;

nextTick();

// Build JavaFX clock
Group group = new Group(); {
ImageView imageView = new ImageView(); {
Image image = new Image(); {
image.$bufferedImage.set(ImageIO.read(getClass().getClassLoader().getResource("resource/clock_background.png")));
}
imageView.$image.set(image);

group.$content.insert(imageView);
}

Group face = new Group(); {
Translate translate = new Translate(); {
translate.$x.setAsFloat(centerX);
translate.$y.setAsFloat(centerY);
}
face.$transforms.insert(translate);

// Every third hour
for (int i = 3; i <= 12; i += 3) {
Text text = new Text(); {
translate = new Translate(); {
translate.$x.setAsFloat(-5.0f);
translate.$y.setAsFloat(5.0f);
}
text.$transforms.insert(translate);

text.$font.set(Font.font("Arial", 16));

text.$x.setAsFloat(radius * ((i + 0) % 2 * (2 - i / 3)));
text.$y.setAsFloat(radius * ((i + 1) % 2 * (3 - i / 3)));
text.$content.set(String.valueOf(i));
}
face.$content.insert(text);
}

// Black circle for the rest of the hours
for (int i = 1; i < 12; i++) {
if (i % 3 == 0) {
continue; // Don't show a circle on every third hour
}

Circle circle = new Circle(); {
Rotate rotate = new Rotate(); {
rotate.$angle.setAsFloat(30.0f * i);
}
circle.$transforms.insert(rotate);
circle.$centerX.setAsFloat(radius);
circle.$radius.setAsFloat(3.0f);
circle.$fill.set(Color.$BLACK);
}
face.$content.insert(circle);
}

// Center circles
Circle circle = new Circle(); {
circle.$radius.setAsFloat(5.0f);
circle.$fill.set(Color.$DARKRED);
}
face.$content.insert(circle);
circle = new Circle(); {
circle.$radius.setAsFloat(3.0f);
circle.$fill.set(Color.$RED);
}
face.$content.insert(circle);

// Second hand
Line line = new Line(); {
Rotate rotate = new Rotate(); {
FloatBindingExpression exp = new FloatBindingExpression() {
public float computeValue() {
return seconds * 6;
}
};
secondsVariable = FloatVariable.make(exp);
rotate.$angle.bind(false, secondsVariable);
}
line.$transforms.insert(rotate);

line.$endY.setAsFloat(-radius - 3.0f);
line.$strokeWidth.setAsFloat(2.0f);
line.$stroke.set(Color.$RED);
}
face.$content.insert(line);

// Hour hand
Path path = new Path(); {
Rotate rotate = new Rotate(); {
FloatBindingExpression exp = new FloatBindingExpression() {
public float computeValue() {
return (hours + minutes / 60) * 30 - 90;
}
};
hoursVariable = FloatVariable.make(exp);
rotate.$angle.bind(false, hoursVariable);
}
path.$transforms.insert(rotate);

path.$fill.set(Color.$BLACK);

MoveTo e1 = new MoveTo(); {
e1.$x.setAsFloat(4.0f);
e1.$y.setAsFloat(4.0f);
}
path.$elements.insert(e1);
ArcTo e2 = new ArcTo(); {
e2.$x.setAsFloat(4.0f);
e2.$y.setAsFloat(-4.0f);
e2.$radiusX.setAsFloat(1.0f);
e2.$radiusY.setAsFloat(1.0f);
}
path.$elements.insert(e2);
LineTo e3 = new LineTo(); {
e3.$x.setAsFloat(radius - 15.0f);
e3.$y.setAsFloat(0.0f);
}
path.$elements.insert(e3);
}
face.$content.insert(path);

// Minute hand
path = new Path(); {
Rotate rotate = new Rotate(); {
FloatBindingExpression exp = new FloatBindingExpression() {
public float computeValue() {
return minutes * 6 - 90;
}
};
minutesVariable = FloatVariable.make(exp);
rotate.$angle.bind(false, minutesVariable);
}
path.$transforms.insert(rotate);

path.$fill.set(Color.$BLACK);

MoveTo e1 = new MoveTo(); {
e1.$x.setAsFloat(4.0f);
e1.$y.setAsFloat(4.0f);
}
path.$elements.insert(e1);
ArcTo e2 = new ArcTo(); {
e2.$x.setAsFloat(4.0f);
e2.$y.setAsFloat(-4.0f);
e2.$radiusX.setAsFloat(1.0f);
e2.$radiusY.setAsFloat(1.0f);
}
path.$elements.insert(e2);
LineTo e3 = new LineTo(); {
e3.$x.setAsFloat(radius);
e3.$y.setAsFloat(0.0f);
}
path.$elements.insert(e3);
}
face.$content.insert(path);

group.$content.insert(face);
}
}

Timeline timeline = new Timeline(); {
timeline.$repeatCount.setAsFloat(Timeline.$INDEFINITE);

KeyFrame kf = new KeyFrame(); {
kf.$time.set(Duration.valueOf(1000.0f));
kf.$canSkip.set(true);
kf.$action.set(new Function0() {
public Void invoke() {
nextTick();
return null;
}
});
}
timeline.$keyFrames.insert(kf);
}

// Display in Swing
SGNode node = group.getSGGroup();
JSGPanel sgPanel = new JSGPanel();
sgPanel.setScene(node);

JPanel panel = new JPanel();
panel.setLayout(new BorderLayout());
panel.add(sgPanel, BorderLayout.CENTER);

JFrame frame = new JFrame("JavaFX Clock Example");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(panel);
frame.setSize(800, 600);
frame.setVisible(true);

timeline.play();
}

public void nextTick() {
calendar.setTimeInMillis(System.currentTimeMillis());
seconds = calendar.get(Calendar.SECOND);
minutes = calendar.get(Calendar.MINUTE);
hours = calendar.get(Calendar.HOUR_OF_DAY);

if (secondsVariable != null) {
secondsVariable.invalidate();
minutesVariable.invalidate();
hoursVariable.invalidate();
}
}

public static void main(String[] args) throws Exception {
new Clock();
}
}


Now, I did make some slight alterations as the original example uses java.util.Date calls that are deprecated, so I updated to using Calendar instead. Also, I had a little trouble with the binding functionality and had to make calls to invalidate in the nextTick() method. Other than that I stayed quite true to the example and the code works quite nicely.

This is the first step in what I'm ultimately planning as a wrapper around Swing and JavaFX to make more powerful UIs without using a crappy scripting language. :)

See this re-output as an Applet:
http://captiveimagination.com/download/clock/clock.html

11 comments:

patrickb said... @ March 6, 2009 at 1:19 AM

Thank you for this nice example
of Java/JavaFX integration
I shall have a look at the code !!!

Jim Connell said... @ May 6, 2009 at 12:18 PM

Interesting. I have a small tweak you can make to your examples so that they are even more concise. Using a lesser know syntax for constructing anonymous inner classes, you can drop the need for variable names when setting the properties (see below). You can refer to anything inside the blocks that would be accessible to a subclass of that type's constructor. The result is something even closer to scene graph syntax of JavaFX script. Enjoy!

Group group = new Group(){{
}};

Rectangle rect = new Rectangle() {{
$width.setAsFloat(200.0f);
$height.setAsFloat(200.0f);
$fill.set(Color.$BLUE);
}};

Text text = new Text() {{
$x.setAsFloat(20.0f);
$y.setAsFloat(20.0f);
$content.set("Greetings Earthling!");
$fill.set(Color.$WHITE);
}};

Matt Hicks said... @ May 6, 2009 at 1:35 PM

Good suggestion Jim, that does make it look a little bit nicer.

iam_peter said... @ May 26, 2009 at 6:23 AM

Thanks for this example. isearched the web for days now and did not find a good solution to create javafx objects in java classes. i came here from this post http://forums.sun.com/thread.jspa?forumID=932&threadID=5370054 .

but i have a problem with the code. my enviroment is mac os X leopard and i develope java with eclipse. pure java and javafx projects compile fine, but your class wouldn't.

first the two imports were marked red:

import com.sun.scenario.scenegraph.JSGPanel;
import com.sun.scenario.scenegraph.SGNode;

but i searched the web and found this http://download.java.net/javadesktop/scenario/releases/0.6/ and included the jar into my project. now it is not marked red at all, but when i want to run it the console tells me:

Exception in thread "main" java.lang.NoClassDefFoundError: javafx/scene/Group
Caused by: java.lang.ClassNotFoundException: javafx.scene.Group
at java.net.URLClassLoader$1.run(URLClassLoader.java:200)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:188)
at java.lang.ClassLoader.loadClass(ClassLoader.java:316)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:288)
at java.lang.ClassLoader.loadClass(ClassLoader.java:251)
at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:374)

i already tried what jim wrote, but it did not helped.
it would be great if you can help me with that.

regards
henning

Matt Hicks said... @ May 26, 2009 at 9:58 PM

henning, I would recommend making sure you have all the JARs for JavaFX 1.1.1 in your classpath as Group is one of the basic features of JavaFX.

Corinne said... @ June 8, 2009 at 8:19 AM

Hi I have tried out your code snippets to create a group directly in Java, but the command:

group.$content.insert(...)

is not available in javaFX 1.2?! Which JavaFX version do you use? Do you have any idea to set the group content in JavaFX 1.2?

Matt Hicks said... @ June 8, 2009 at 10:12 AM

Corinne, I was using JavaFX 1.1 in this post. Not sure what they've changed it to in the most recent version.

Corinne said... @ June 10, 2009 at 3:09 PM

Hi it's me again ... I've asked this question about setting the content of a group directly in Java with JavaFX 1.2 in several forums, but it seems that nobody can help me, thats why I come back to you. Where did you find the information to write the code above? I don't find any information in the web about using JavaFX directly in Java. Can you recommend me any page or forum?

Matt Hicks said... @ June 10, 2009 at 5:14 PM

Corinne, I just took a look at the changes they've made in JavaFX. It would appear that on Group you have to call group.getFXNode().loc$content.insert(child.getFXNode()) now instead of what I stated in my blog entry.

Though I still believe that programmatically you can make use of JavaFX directly from Java I have abandoned this idea as it is a hack in its best case and I really think they did a very poor job in the implementation details and it has been extremely buggy for me.

If you're interested in my current endeavor: http://jseamless.googlecode.com/svn/framework/branches/2.0/

I'll be making a blog post about this in the next few weeks, but it's a completely new UI framework built on JOGL (OpenGL bindings for Java) and in my opinion provides what JavaFX *should* have done in the first place.

Corinne said... @ June 18, 2009 at 5:30 AM

Thx a lot for your help.
But unfortunately it doesn't work my group node doesn't have the method getFXNode(). Are you really using JavaFX 1.2. ? Or whats my fault?
greetz

Matt Hicks said... @ June 18, 2009 at 8:29 AM

I pulled down the JavaFX source was digging through it, are you sure you have all the dependencies?

No, I don't use JavaFX at all anymore. I think they did a horrible job on many aspects, so I'm developing my own framework as I mentioned above.

Post a Comment