RSS

Category Archives: JavaFX

Visual Feedback on an Abstract Parsing Tree with JavaFX

I honestly didn’t expect to be writing this, but it seems fair.

In the past few editions, I’ve been discussing the AST. It can be overwhelmingly complicated for a complete program; so I’ve been using a simple, single line equation as the sample. Unfortunately, that isn’t very realistic; and it would be very helpful to have a procedurally generated visual tree available. That tree is what this lesson is all about.

At first I considered using a graphical tree style, like javax.swing.JTree; but that can be painfully over-simplistic in itself. I would prefer to outline the material the same way I would draw it on a white board (which, if you’re wondering, I do). The best way to do this? JavaFX.

JavaFX whiteboard abstract syntax tree

Graphical AST tree rendering, through JavaFX/F3

If you aren’t familiar with JavaFX, please do me a favor and tolerate the name. It was originally F3, for Form-Follows-Function. I kind of liked F3, until some marketer decided that “JavaFX” sounded better. Functionally speaking, it’s an excellent revision on how user interfaces are designed in Java. I fully stand behind it. It allows for XML structuring and CSS styling, just like a web page, to more hard-coded controls. This is much, much faster; and it allows for significant beauty in user interfaces. However, it works very differently from things like Swing and AWT; and while I’m certain that it isn’t the first API to do so, it takes some getting used to.

I fully intend to write a true tutorial on all of JavaFX on some point. Do you need to understand it to understand translators? Absolutely not. However, this code does work. It is not part of the Github repository, as it is technically a tangential project; but the same license (GNU GPL) applies to it and you are welcome to copy it token for token. I’ll put it up on Github as I get the chance. I’ll make a few minor comments along the way to help you follow it.

1. The Basic Application

We have exceedingly few needs for our app. It simply reads a program from a stream, parses it, and feeds the parse tree to a custom node, which displays it graphically. Accordingly, the program code is rather small. I’ll begin by displaying it, then I’ll spend a moment piecing it together in English for you.

package oberlin.builder.gui;

import oberlin.algebra.builder.AlgebraicBuilder;
import oberlin.builder.parser.ast.AST;
import javafx.application.*;
import javafx.scene.*;
import javafx.scene.layout.*;
import javafx.stage.*;

public class GUIMain extends Application {

    public static void main(String...args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        Pane root = new Pane();
        root.getStyleClass().add("backing");
        
        Scene scene = new Scene(root);
        scene.getStylesheets().add(GUIMain.class.getResource("tree.css").toExternalForm());
        
        primaryStage.setScene(scene);
        root.setMinWidth(640.0);
        root.setMinHeight(480.0);
        
        populate(root);
        
        primaryStage.show();
        primaryStage.centerOnScreen();
    }
    
    private void populate(Pane p) {
        /*
         * This is simply an example, so I've ignored input for now.
         * In theory, you would replace the line below (containing
         * hard code) with an input loop.
         */
        AST program = (new AlgebraicBuilder()).getParseTree("1+2");
        
        p.getChildren().add(new GUITree(program));
    }
}

All JavaFX/F3 programs begin with Application.launch(String…args). JavaFX programs run in what is effectively their own thread, and more so than with Swing-based programs. Launch parses arguments and stores them in their own object, appropriately called Parameters. They can be accessed, at any point later on, via Application.getParameters(). Our available overloads and customizations cut out for a moment, then come back in in the start(Stage) method.

Stage is basically where Frame would be; but it’s a little more complicated than that. Unlike Swing and AWT, which were designed to be platform independent, JavaFX is designed to be hardware context independent. What you are writing here will work equally well on a PC, tablet, and smart phone; as well as anything else built (now or later) that maintains a JavaFX compatibility standard. Thus, what might otherwise be called a frame or window is referred to as a stage, as it might be neither of those things.

You’ll notice that the instantiated Pane is given a style class. If you aren’t familiar with CSS, a style class is what’s used to differentiate between one element and any number of others which, otherwise, would look exactly like it. Thus, it allows CSS to pick and choose which elements of the layout it is styling at a given moment. I’ve chosen “backing” as the name for this element, as it is the backboard of our tree. You will also note that, two lines later, the CSS file itself is loaded.

Next, a Scene is created. Scenes are critically important, and distinct from stages. While a stage represents the context that the layout is drawn in, the scene represents the actual controls and constraints within that space. Thus, while many aspects of Stage are immutable (and unknowable), Scene allows for greater flexibility. JavaFX sees to it that they correspond, so don’t worry about that.

Scene is styled through its root element, which in this case is our pane. You’ll notice that instead of the stricter setWidth() and setHeight() that you might be familiar with from Swing, we are setting a minimum on these bounds. That minimum is not guaranteed, as the display may not be capable of it, but it is treated as a general rule to be followed if at all possible. In this case, I’m going for classic analog low-def TV resolution, 640 width by 480 height. (Looking back, those numbers might be inadequate, but for now they’re quite functional.) If this is too small for you, the frame—if it is a frame, anyway—is easily resizable.

Populate() is a method I wrote to add the paraphernalia to the scene; but note that afterwards we call show(). This is very important, as otherwise our stage will be constructed in memory, but never displayed to the screen. Additionally, there will be no way to kill the JavaFX thread save for a hard interrupt. Once shown, the closing of the primary stage will flag the program to terminate.

1.1. Populate

It’s a generally good habit, but not a necessary one, to populate your frame in a separate and dedicated method. This is what I do here, even though for the moment, I only have one control to add.

The AST method should be old news; it’s a stand-in, for the moment, for an actual code-reading portion. (I’m assuming that you’re looking to compile more than just “1+2”.) GUITree is a custom JavaFX node, which I will explain next. Note that to add a node to a program, you must take some structure (not yet visible) in the scene graph (stemming from your chosen root), and get its Children as a modifiable list. Then, you must add that node to the list.

Note that after a stage is visible, precious little of the scene can be changed save for through the constraints built into it. I’m not going to touch on Expressions and Bindings here, but know that if you pull something that doesn’t play by JavaFX’s rulebook, it will throw an ApplicationException and your program will not launch. Thankfully, while exceedingly picky, that rulebook is small. If you call show() and then try and add a child, you will have problems; it must be the other way around.

If you’re curious, hiding a rendered stage does not count for making it modifiable. You must give it your entire concept first, then make it visible. If you’re familiar with OpenGL, you’ll already understand why.

2. The Tree Itself

The tree is a custom JavaFX node, which I admit is rarely necessary. Still, most of the entities that make it work are core to the API.

package oberlin.builder.gui;

import oberlin.builder.parser.ast.AST;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.CubicCurve;

import java.util.function.IntSupplier;

public class GUITree extends AnchorPane {
    private Bounds bounds = new BoundingBox(0, 0, 640, 480);
    private AnchorPane framing = new AnchorPane();
    private double edgeSize = 0.10;    //ten percent additional length beyond edges of framing
    
    public GUITree(AST ast) {
        this.setMinWidth(bounds.getWidth() * (1 + edgeSize));
        this.setMinHeight(bounds.getHeight() * (1 + edgeSize));
        
        configureFraming();
        
        addNode(ast);
    }
    
    private void configureFraming() {
        framing.setLayoutX(edgeSize * (bounds.getWidth() / 2.0));
        framing.setLayoutY(edgeSize * (bounds.getHeight() / 2.0));
        framing.setMinWidth(bounds.getWidth());
        framing.setMinHeight(bounds.getHeight());
        
        this.getChildren().add(framing);
    }
    
    private ASTNode addNode(AST ast) {
        return this.addNode(ast, new Marker(0), new Counter(), 0, null);
    }
    
    private ASTNode addNode(AST ast, IntSupplier stepsDown, IntSupplier stepsAcross, int index, ASTNode parent) {
        ASTNode node = new ASTNode(ast, stepsDown.getAsInt(), stepsAcross.getAsInt());
        
        //AnchorPane stuff
        calculateAnchoring(node, parent);
        
        framing.getChildren().add(index ++, node);
        
        final StringBuilder tooltipText = new StringBuilder();
        IntSupplier across = new Counter();
        for(AST kid : ast.getContainedNodes()) {
            tooltipText.append(kid.getClass().getSimpleName()).append(" ");
            ASTNode child = addNode(kid,
                    new Marker(stepsDown.getAsInt() + 1),
                    across,
                    index,
                    node);
            CubicCurve cubic = createCubicCurve(node.getNoodleRoot(), child.getTopCenter());
            framing.getChildren().add(cubic);
        }
        node.getType().setTooltip(new Tooltip(tooltipText.toString()));
        
        return node;
    }
    
    private CubicCurve createCubicCurve(Point2D p1, Point2D p2) {
        CubicCurve curve = new CubicCurve();
        
        curve.setStartX(p1.getX());
        curve.setStartY(p1.getY());
        
        curve.setEndX(p2.getX());
        curve.setEndY(p2.getY());
        
        curve.setControlX1(p1.getX());
        curve.setControlY1(p2.getY());
        
        curve.setControlX2(p2.getX());
        curve.setControlY2(p1.getY());
        
        curve.setStroke(Color.BLACK);
        curve.setStrokeWidth(2.0);
        curve.setFill(Color.TRANSPARENT);
        
        return curve;
    }
    
    private void calculateAnchoring(ASTNode node, ASTNode parent) {
        node.setOrigin(new Point2D(parent == null ? (bounds.getWidth() - node.getBounds().getWidth())/2.0 :
            justifyX(node, parent), justifyY(node)));
        AnchorPane.setTopAnchor(node, node.getOrigin().getY());
        AnchorPane.setLeftAnchor(node, node.getOrigin().getX());
    }
    
    private Double justifyX(ASTNode node, ASTNode parent) {
        final double parentCenter = (parent.getOrigin().getX() + (parent.getBounds().getWidth() / 2.0)
                + parent.getNoodleRoot().getX()) / 2.0;
        final double center = parentCenter
                - node.getBounds().getWidth()
                        * (parent.getAST().getElementCount()) / 2.0; 
        return center + node.getOrigin().getX();
    }
    
    private Double justifyY(ASTNode node) {
        return node.getOrigin().getY();
    }
}

That was a bit much at once, I know. The central pane, called “framing”, is 640 by 480. Framing is offset in each direction by a 5% inset, via the convenient features of AnchorPane.

AnchorPane is one of the few prepared ways to control where a node is rendered, with precision, in JavaFX. You may often need to keep your own tabs on where it is rendered, as getMinX() and getMaxX() will return zero more often than you will believe. However, through direct layout control, you can still manage them.

The method addNode(…) adds a custom object called ASTNode. I’ll cite it for you here.

package oberlin.builder.gui;

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.TextAlignment;
import oberlin.builder.parser.ast.AST;

class ASTNode extends VBox {
    private Bounds bounds = new BoundingBox(0, 0, 100, 40);
    private Point2D origin = new Point2D(0, 0);
    private final double expanse = 1.10;
    
    private final AST ast;
    
    private Label type;
    private Label hash;
    
    private ObservableList<ASTNode> kids = FXCollections.observableArrayList();
    
    public ASTNode(AST ast) {
        this.ast = ast;
        type = new Label(ast.getClass().getSimpleName().toString());
        type.setTextAlignment(TextAlignment.CENTER);
        type.setAlignment(Pos.CENTER);
        
        hash = new Label(Long.toHexString(ast.hashCode()).toUpperCase());
        hash.setTextAlignment(TextAlignment.CENTER);
        hash.setAlignment(Pos.CENTER);
        
        VBox vbox = new VBox(new StackPane(type), new StackPane(hash));
        vbox.getStyleClass().add("node");
        vbox.setMinWidth(bounds.getWidth());
        vbox.setMinHeight(bounds.getHeight());
        this.getChildren().add(vbox);
        
        for(AST kid : ast.getContainedNodes()) {
            addKid(new ASTNode(kid));
        }
        
    }
    
    public Point2D getNoodleRoot() {
        return new Point2D(getOrigin().getX() + (getBounds().getWidth() / 2),
                getOrigin().getY() + getBounds().getHeight());
    }

    public ASTNode(AST ast, int level) {
        this(ast);
        
        origin = new Point2D(0, level * bounds.getHeight() * expanse);
    }
    
    public ASTNode(AST ast, int levelDown, int levelAcross) {
        this(ast);
        
        origin = new Point2D(getStepAcrossSize(levelAcross), getStepDownSize(levelDown));
    }
    
    public double getStepDownSize(int steps) {
        return steps * bounds.getHeight() * expanse;
    }
    
    public double getStepAcrossSize(int steps) {
        return steps * bounds.getWidth() * expanse;
    }
    public void addKid(ASTNode astNode) {
        this.kids.add(astNode);
    }
    
    public ObservableList<ASTNode> getKids() {
        return kids;
    }
    
    public Bounds getBounds() {
        return bounds;
    }
    
    public Point2D getOrigin() {
        return origin;
    }
    
    public Point2D getTopCenter() {
        return new Point2D(
                getOrigin().getX() + (getBounds().getWidth()/2),
                getOrigin().getY());
    }
    
    public Label getType() {
        return type;
    }
    
    public void setOrigin(Point2D p) {
        this.origin = p;
    }
    
    public AST getAST() {
        return this.ast;
    }
}

ASTNode is a JavaFX Node as well. It simply maintains a reference to the AST itself, and the general presentation of that AST on the tree. There isn’t a lot here. If you’re wondering what VBox is, it’s an abbreviation for “vertical box”. (Naming a class after an abbreviation is bad practice, but it’s long since done by powers above me; I tolerate it as much as I do “AST”.)

Speaking of bad practice, this would ideally actually use Bindings, but I wrote this in a bit of a rush today and will have to correct that in the future. It is also bad practice to repeat data, which is exactly what this program is doing by re-storing the label text in a separate field. All the same…

I’m going to gloss over a lot of the configuration of the labels, as it’s relatively standard. Know that like any other pane in JavaFX, a VBox can be initialized with a list of its bounded nodes; also, a StackPane has the default behavior of centering its own bounded nodes.

The last thing done in the constructor is the creation of additional ASTNodes for each child node of the abstract syntax tree.  Each of them, in turn, renders their own children. This is not perfect, there is a substantial chance that two lists of nodes will overlap one another; however, it is already excellent for debugging visitor pattern based content. In the end, the GUITree renders each node in an assigned place, with a curved cubic line (technically called a “noodle”) connecting it to its parent and its children.

How does it do that? With IntSuppliers.

3. The IntSuppliers

There are only two of these.

package oberlin.builder.gui;

import java.util.function.IntSupplier;

/**
 * For downward counts; always returns provided number.
 * 
 * @author © Michael Eric Oberlin Dec 23, 2014
 *
 */
class Marker implements IntSupplier {
    private int fix;
    
    public Marker(int fix) {
        this.fix = fix;
    }
    
    @Override
    public int getAsInt() {
        return fix;
    }
}


package oberlin.builder.gui;

import java.util.function.IntSupplier;

/**
 * For counts across; always returns next consecutive number.
 * 
 * @author © Michael Eric Oberlin Dec 23, 2014
 *
 */
class Counter implements IntSupplier {
    private int count;
    
    @Override
    public int getAsInt() {
        return count++;
    }
    
}

IntSuppliers (and really all Suppliers) are part of the java.util.function package, new to Java 8. The great advantage of this package is that a function, or any functional interface, allows you to specify a method that serves as a primitive with conditionally defined values. I know that’s a leap, but I’ve been doing it since long before it was formally adopted into the language and it’s a central totem of functional languages.

We could, in theory and practice, use incrementing and decrementing integers in place of either of these. The problem is that the code gets a lot longer and a lot more cluttered when you do. I prefer the sublime simplicity of packing such behavior into an interface.

Of course, these are not everything. There is one, final, issue.

4. What was that that you said about “CSS”?

The CSS is specific to JavaFX; a complete listing of all of the properties is available here. If you are unfamiliar with the syntax of CSS, you can find an excellent tutorial on it (for HTML, at least) at W3Schools. It isn’t as versatile as Java or C, but its creators pulled many of its properties from C-like languages.

tree.css:

.backing {
    -fx-background-color: lightyellow;
    -fx-insets: 0;
    -fx-padding: 15;
    -fx-spacing: 10;
}

.node {
    -fx-background-color: lightblue;
    -fx-background-radius: 5.0;
    -fx-border-color: black;
    -fx-border-radius: 5.0; 
}

Keep this in the same folder as GUIMain, and it will find it as written.

The CSS styling of JavaFX controls is capable of everything HTML 5 is and then some. It’s an excellent fusion of programming and markup. I encourage you to play with the layout of GUIMain’s scene, and the actual program fed to the builder.

 

Tags: , , , , , , , , , , , , , , , ,

WebView

So, on with my adventures in JavaFX. I had a brilliant idea when I was arguing with Chrome and Opera recently. I decided that browser “tabs” would be much better managed as a lazily loaded tree, which would, in theory, be easily manageable (and testable) with JavaFX’s WebView class. It turned out that it wasn’t so simple as I had imagined.

WebView is a JavaFX parent node with a purpose. (There are a few other WebViews out there that have similar properties, such as the one for Android, but they are not directly related to JavaFX and are only tangentially connected to this article.) It takes a URL in its location property and loads a web page from the internet, then displays that page, just like a browser, in its content.

Holy hell. Could it get any more all-purpose? Immediate access to just about any page, with a cut & paste browser based off of Google’s WebKit. Unfortunately, my verdict is either that WebKit is incomplete, or it is drastically misrepresented. The following is code for an adequate web browser.

package example;

import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFieldBuilder;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.BorderPaneBuilder;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.scene.web.WebViewBuilder;
import javafx.stage.Stage;

public class ExampleBrowser extends Application {

    private TextField url;
    private WebView webView;

    /**
     * @param args
     *            the command line arguments
     */
    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        //set up url to act, more or less, like an address bar
        url = TextFieldBuilder.create()
                .onKeyTyped(new EventHandler<KeyEvent>() {

                    @Override
                    public void handle(KeyEvent keyEvent) {
                        if (keyEvent.getCode().equals(KeyCode.ENTER))
                            webView.getEngine().load(url.getText());
                    }
                }).onAction(new EventHandler<ActionEvent>() {

                    @Override
                    public void handle(ActionEvent arg0) {
                        webView.getEngine().load(url.getText());
                    }
                }).build();
        
        //get a lasting hold on the web engine
        webView = WebViewBuilder.create().build();
        final WebEngine webEngine = webView.getEngine();

        //keep track of the current page
        webEngine.locationProperty().addListener(new ChangeListener<String>() {
            @Override
            public void changed(ObservableValue<? extends String> ov,
                    String oldVal, String newVal) {
                url.setText(newVal);
            }
        });

        //this will update both the engine, and the address bar
        webEngine.load("http://java-buddy.blogspot.com/");
        
        //establish the root node
        BorderPane root = BorderPaneBuilder.create().center(webView).top(url)
                .build();

        //and, action.
        primaryStage.setScene(new Scene(root, 640, 480));
        primaryStage.show();
    }}

You could just use url.textProperty().bind(webEngine.locationProperty()); but as locationProperty is read-only this would make the text field uneditable. Run this program, and you will have, more or less, a rudimentary web browser (minus the need to type “http://&#8221; every time, but that’s just a question of editing the URL field with regular expressions, well outside of this topic).

Perfect HTML 5. Nice.

Perfect HTML 5. Nice.

So, generally, pretty easy. But what about expansions to it? Let’s try watching a YouTube video, as an example. These are generally rendered in Flash by default. Unfortunately, there is no known way (as of yet) to support Flash through JavaFX. Generally I’m fine with that, I hate Flash; and YouTube does have a setting that allows you to stream videos in HTML 5 format, which is great; but not all sites do and let’s face it, video and audio are important as often as they are annoying. So, no Pandora through WebView—yet. Believe me, when I find a hack for this, or when a relevant update is made, I’ll be all over it.

And yet, there is another problem. Try right-clicking on the WebView. You’ll get a drop down menu with a few options regarding the page itself; for me they aren’t entirely useless. I’ve got “copy image to clipboard”, “open link in new window”, you name it. Generally, they’re functional, too. However, note that you can’t, from anywhere in the API, determine the address of a node right beneath a screen coordinate. That’s a problem for me. You can even copy the link to the clipboard, but at no time can you say “I want the URL of the node beneath the mouse cursor”. In my mind, that should be at tops a two or three line operation. I’ve been told (but have not yet verified) that WebView and its peripherals account for half of the JavaFX download size, which leads me to wonder why it is that it’s the least versatile. My conclusion is that it’s incomplete.

But all is not lost. There’s another thing that I’ve been doing a lot of lately, which is Java Scripting Management. Some time back, an engine called Rhino made a show in compiling and running ECMAScript (some of you may still know it as JavaScript) at near native speeds. It happens to be included in the JDK. One of the great advantages is, ECMAScript is capable of bouncing information up to the JavaFX application in a (highly inappropriately named) JSObject. This includes everything that ECMAScript has access to, which is, more or less, the entire universe, inclusive of what the mouse is over.

Part two will be my proxy to ECMA, which will keep an essential script on board to insert into the header of the page; a property to express what the mouse is over (by URL) at all times, among other things; and depending on what I find, possibly a hook for Chrome plug-ins, but don’t count on that last one. I don’t feel that this should be necessary, but it is, and the resultant power will be appreciable.

In WebView’s defense, it was generally designed to be a quick way to pull in a web app from the internet and run it without a third party browser. It does that nicely, and I fully intend to take advantage of it in the future. For those looking for a WYSIWYG editor, you might consider an HTMLEditor node, which does not provide all of the features of a browser but is easily adaptable to HTML production. Until next time, I bid you adieu. Drop me a line for comments and questions.

 
Leave a comment

Posted by on September 24, 2013 in JavaFX, Programming

 

Tags: , , , , , , , , , , , , ,

JavaFX

So, initially I was considering a whole run on guiding established OOP programmers to binding and property oriented material; but honestly, I’m not feeling qualified for that just yet. I’m still learning a lot myself. There have been a few major changes in the software community lately, and many of these changes run directly against long held design patterns. The majority of these, for me, have been implemented in my quest to learn JavaFX, which Oracle has announced will be replacing Swing in the near future.

Honestly, I can understand why. It’s lightning fast, completely compatible with a number of other JVM-based languages (such as Scala and Jython), uses the graphics hardware to its full potential (with one exception that I’ll discuss momentarily), and can be coded extremely quickly. It uses bindings and properties in a manner that makes even the old and tested design of Java Beans look amateurish. Additionally, the long-held notion that programming objects must, in some manner, be visualizable as literal objects, has rightly been taken into question. A property-oriented object is more like a gear; what it does is an inherent and inseparable part of what it is.

This will be a chronicle of my adventures and experiments with JavaFX. For a decent read on modern JavaFX, I recommend “Pro JavaFX 2: A Definitive Guide to Rich Clients with Java Technology“, published by Apress. It’s just getting your feet wet, not teaching you to swim; but it’s a magnificent start.

Where GUIs are concerned, I come from two universes, that of Swing and that of OpenGL. JavaFX is a fusion between the two, implemented as a raw API. It cuts straight from JVM virtual machine code to the GPU. Whether that’s an nVidia Quadro K6000 or a 2008-era Intel Graphics Chipset, that is a significant amount of red tape cut. The only caveat is that on some architectures, a few features have not yet been implemented. It’s not a lack of planning or concern that has done this, but a lack of available man-hours; the situation will resolve itself. But, as an example, the amazing Perspective 3D mode, on Linux, is only available for nVidia GPUs with the proprietary driver; otherwise, the Java2D pipeline is defaulted to instead. So, if you have an amazing Radeon GPU in your machine, you will have to wait for that particular feature to be implemented. (Or, join up and implement it yourself, if you have the time and the know-how.) Thankfully, the era of wondering about GPU compatibility seems to be coming to a close with the imminently released JavaFX 8.0.

There are a thousand very good basic tutorials on the web alone; this is my focus on the issues that I have run into with the bindings of JavaFX, and how to get around them. I look forward to seeing the absolutely beautiful GUIs that people will construct with this technology.

 
Leave a comment

Posted by on August 17, 2013 in JavaFX, Programming

 

Tags: , , , , , , , , , , , , , , , , , , , , ,