Difference between revisions of "Efxclipse/Tooling/FXGraph"

From Eclipsepedia

Jump to: navigation, search
(You are not a Java bean?)
(Calling static methods)
Line 350: Line 350:
 
The result looks like this (using the e(fx)clipse preview):
 
The result looks like this (using the e(fx)clipse preview):
  
[[Image(wiki:FXGraph:grid_static.2.png)]][[br]]
+
[[Image(wiki:FXGraph:grid_static.2.png)]]<br>
  
 
= Referencing other FXGraph files =
 
= Referencing other FXGraph files =

Revision as of 18:26, 6 July 2013

Contents

What is FXGraph?

A short introduction

FXGraph is a simple DSL for the definition of a JavaFX 2.x object graph. Instead of generating Java source or byte code, it generates FXML, a special XML format supported by the JavaFX 2.x library.

The language supports the following concepts:

  • JSON-like object graph notation
  • static "properties" access (used mainly to set layout properties)
  • binding expressions
  • (Java) controller binding (used for event listeners)
  • translation support
  • support for CSS referencing
  • live preview of the GUI

Hello world

A first example

Like with any other language, let's start with a "Hello World" application to explain the main building blocks.

package at.bestsolution.efxclipse.tutorial.fxgraph
 
import javafx.scene.control.*
import javafx.scene.layout.*
 
component HelloWorld {
  BorderPane {
    center : Button {
      text : "Hello World"
    }
  }
}

As you can see, the definition of an object graph with FXGraph looks quite similar to JSON, but because Java (unlike !JavaScript) has the concept of a package, you can import classes from a package so that you don't have to specify the fully qualified name.

Another difference to JSON is that the definition starts with the {{{component}}} keyword which typically matches the filename - so the above definition would be stored in a file called HelloWorld.fxgraph.

The attribute and its value are separated by the ":" (colon) and the following simple values are supported directly:

  • String
  • Number (integer and decimal numbers)
  • Object definition

More complex values (method references, object references, ...) are described in a following section of this document as they require a special syntax.

Run 'Hello World'

Among the attachments at the bottom of the page, you will find a simple Java project to run the "Hello_world" example ([[1]]).

Think of the FXGraph file as a source file for your UI. The FXGraph needs to be loaded as a FXML file JavaFX scene. The e(fx)clipse project will translate the FXGraph "Hello_world.fxgraph" specification file to a "Hello_world.fxml" file.

At runtime, your program will look along the directories in the CLASSPATH for program resources. The most simple way to get your UI to display is:

  • Resolve the result .fxml file to a uri for the program
  • The resource can be loaded with the FXMLLoader:
URL    uri = Launch.class.getResource( "/Hello_world.fxml" );
Parent p   = FXMLLoader.load( uri );

At this point, you have a loaded Java class that matches your FXGraph UI description. To see it on the screen, is the same as any other JavaFX scene:

Scene s = new Scene(p);
primaryStage.setScene(s);
primaryStage.show();

That along with the factory code in [);] shown in [[2]] is enough to show the Hello_world window. Give it a go and then read further. (Please note that the [[3]] demo is a minimal example to launch an FXGraph specification). Have fun ...

Externalising strings

There's more than one language spoken

Often, a UI has to support multiple locales so instead of putting the strings used in the UI directly into the UI definition one puts them into an external resource and loads the correct one based upon the users locale.

A standard Java way for providing such locale information is to put the values into a property file and load them at runtime using java.lang.ResourceBundle.

Let's suppose you have a message.properties file located next to your !HelloWorld.fxgraph file with the following content:

mybutton.label = Hello World

You can reference the file and the property in your FXGraph file like this:

package at.bestsolution.efxclipse.tutorial.fxgraph
 
import javafx.scene.control.*
import javafx.scene.layout.*
 
component HelloWorld resourcefile "messages.properties" {
  BorderPane {
    center : Button {
      text : rstring "button.label"
    }
  }
}

Connecting to a controller

How to handle UI callbacks

Every modern UI has to respond to user interaction: if a user clicks a button, for instance, we'd like to react on the button click. One typically implements such UI callbacks in a programming language like Java, !JavaScript, etc.

Let's react on the button click using a controller like this:

package at.bestsolution.efxclipse.tutorial.fxgraph;
 
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
 
public class HelloWorldController {
  @FXML
  public void clicked(ActionEvent event) {
    System.out.println("Button clicked");
  }
}

Referencing methods like the above is quite easy using the controllermethod keyword:

package at.bestsolution.efxclipse.tutorial.fxgraph
 
import javafx.scene.control.*
import javafx.scene.layout.*
import at.bestsolution.efxclipse.tutorial.fxgraph.*
 
component HelloWorld controlledby HelloWorldController resourcefile "messages.properties" {
  BorderPane {
    center : Button {
      text : rstring "button.label",
      onAction : controllermethod clicked
    }
  }
}

Accessing objects from the controller

Ids are your friend

FXML propagates a clean separation between the UI defined in the FXML file and business logic implemented in a programming language like Java, !JavaScript, etc.

A controller could look like this:

package at.bestsolution.efxclipse.tutorial.fxgraph;
 
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
 
public class HelloWorldController {
  @FXML
  public Button myButton;
  @FXML
  public void clicked(ActionEvent event) {
    System.out.println("Button clicked");
  }
}

You can make the FXMLLoader set the myButton field using the id keyword:

package at.bestsolution.efxclipse.tutorial.fxgraph
 
import javafx.scene.control.*
import javafx.scene.layout.*
import at.bestsolution.efxclipse.tutorial.fxgraph.*
 
component HelloWorld controlledby HelloWorldController resourcefile "messages.properties" {
  BorderPane {
    center : Button id myButton {
      text : rstring "button.label",
      onAction : controllermethod clicked
    }
  }
}

Theme it

Use CSS to get a real WYSIWYG

FXGraph files can be previewed while editing. As most most UI theming information in JavaFX applications is configured by CSS the preview needs to know what CSS file it should apply.

package at.bestsolution.efxclipse.tutorial.fxgraph
 
import javafx.scene.control.*
import javafx.scene.layout.*
import at.bestsolution.efxclipse.tutorial.fxgraph.*
 
component HelloWorld controlledby HelloWorldController styledwith ["hello.css"] resourcefile "messages.properties" {
  BorderPane {
    center : Button id myButton {
      text : rstring "button.label",
      onAction : controllermethod clicked
    }
  }
}

You can provide multiple CSS files inside the square brackets. A path can be defined in several ways:

  • relative to the FXGraph file
 e.g. hello.css
  • absolute to the project by starting the path with a "/"
 e.g. /css/test.css
  • absolute to the workspace using "platform:/resource/${projectname}/..."
 e.g. platform:/resource/at.bestsolution.efxclipse.testcases.fxgraph/css/test.css

When you want to reference only a single CSS file (a common use case), you can omit the square brackets:

package at.bestsolution.efxclipse.tutorial.fxgraph
 
import javafx.scene.control.*
import javafx.scene.layout.*
import at.bestsolution.efxclipse.tutorial.fxgraph.*
 
component HelloWorld controlledby HelloWorldController styledwith "hello.css" resourcefile "messages.properties" {
  BorderPane {
    center : Button id myButton {
      text : rstring "button.label",
      onAction : controllermethod clicked
    }
  }
}

Objects outside the UI tree

How to define and reference

FXML represents the tree structure of your UI but there are some objects living outside this tree. One such example is a toggle group instance that is used to link, e.g., radio buttons together. The creation of an object that is not part of the tree structure and referencing it inside the UI later on is done through so called defines.

Defines can be added at the beginning of the component definition:

package at.bestsolution.efxclipse.tutorial.fxgraph
 
import javafx.scene.control.*
import javafx.scene.layout.*
import at.bestsolution.efxclipse.tutorial.fxgraph.*
 
component HelloWorld controlledby HelloWorldController styledwith "hello.css" resourcefile "messages.properties" {
  define ToggleGroup id myRadioGroup
  BorderPane {
    center : Button id myButton {
      text : rstring "button.label",
      onAction : controllermethod clicked
    }
  }
}

When you now have to reference such an element, you do this using the idref keyword.

package at.bestsolution.efxclipse.tutorial.fxgraph
 
import javafx.scene.control.*
import javafx.scene.layout.*
import at.bestsolution.efxclipse.tutorial.fxgraph.*
 
component HelloWorld controlledby HelloWorldController styledwith "hello.css" resourcefile "messages.properties" {
  define ToggleGroup id myRadioGroup
  BorderPane {
    top : HBox {
      children : [
        RadioButton {
          text : "Option 1",
          toggleGroup : idref myRadioGroup
        },
        RadioButton {
          text : "Option 2",
          toggleGroup : idref myRadioGroup
        }
      ]
    },
    center : Button id myButton {
      text : rstring "button.label",
      onAction : controllermethod clicked
    }
  }
}

The usage of idref is not restricted to elements created through defines. It can be used to reference any other element marked with id.

Using factory methods

You are not a Java bean?

FXGraph allows you to define observable collections (set, list, map) using the factory methods of the FXCollection class as described here. For instance, an observable list that serves as input for a choice box can be defined in the following way:

package at.bestsolution.efxclipse.tutorial.fxgraph
 
import javafx.scene.layout.*
import javafx.scene.control.*
import javafx.collections.*
 
component FactoryExample {
  HBox {
    children : [
      ChoiceBox {
	items : FXCollections createdby observableArrayList {
          String("Hello World"),
	  String("Hello World extra long"), 
	  String("Hello World extra extra long")
	}
      }
    ]
  }
}

The result looks like this (using the e(fx)clipse preview):

Image(wiki:FXGraph:hello_world_factory.png)

Layout properties

Calling static methods

Layout containers use static methods to determine the exact layout of their children. These static methods can be called in your FXGraph file using the {{{static}}} keyword:

package at.bestsolution.efxclipse.tutorial.fxgraph
 
import javafx.scene.layout.*
import javafx.scene.control.Label
import javafx.scene.control.TextField
 
component StaticProperties {
  GridPane {
    children: [
      Label {
        text: "First name:",
        static columnIndex: 0,
        static rowIndex: 0
      },
      TextField {
        static columnIndex: 1,
        static rowIndex: 0
      },
      Label {
        text: "Last name:",
        static columnIndex: 0,
        static rowIndex: 1
      },
      TextField {
        static columnIndex: 1,
        static rowIndex: 1
      }
    ]
  }
}

In this example, the static methods GridPane.setRowIndex(Node child, Integer value) and GridPane.setColIndex(Node child, Integer value are called from the nodes that need to be laid out in the grid pane. You do not have to specify explicitly the child parameter or the class on which the static methods are called since they are implied from the tree structure.

The result looks like this (using the e(fx)clipse preview):

Image(wiki:FXGraph:grid_static.2.png)

Referencing other FXGraph files

Reusability is the key

It's easy to reference a component defined in an FXGraph file from another component: simply use the include keyword.

package at.bestsolution.efxclipse.tutorial.fxgraph
 
import javafx.scene.control.*
import javafx.scene.layout.*
import at.bestsolution.efxclipse.tutorial.fxgraph.*
 
component HelloWorld controlledby HelloWorldController {
  VBox {
    children : [
      Label id hello {
        text : "Hello"
      },
      include World as world
    ]
  }
}

Here, World is an FXGraph file defined as follows:

package at.bestsolution.efxclipse.tutorial.fxgraph
 
import javafx.scene.layout.*
import javafx.scene.control.*
import at.bestsolution.efxclipse.tutorial.fxgraph.*
 
component World controlledby WorldController {
  HBox id worldBox {
    children : [
      Button id world {
        text : "World"
      }
    ]
  }
}

You can look at the result in the preview:

Image(wiki:FXGraph:hello_world_include.png)

The HelloWorldController can reference the top-level HBox of the World component by the name defined with the as keyword, i.e., world. Furthermore, the WorldController can be referenced from the HelloWorldController using the implied worldController variable:

package at.bestsolution.efxclipse.tutorial.fxgraph
 
import javafx.fxml.FXML;
import javafx.scene.layout.HBox;
import javafx.scene.control.Label;
 
public class HelloWorldController {
  @FXML
  HBox world;
  @FXML
  WorldController worldController;
  @FXML
  Label hello;
 
  @FXML
  public void initialize() {
    System.out.println(hello.getText() + " " + worldController.world.getText());
  }
}

(Note that this example assumes there is a non-private field {{{world}}} of type Button in the {{{WorldController}}} class.)

You can omit the {{{as}}} keyword if you do not want to define a variable name for the included component.

Using constants

You can use Java constants in FXGraph as you use them in Java code. Simply replace the dot ('.') between the class name and the field name by a hash ('#') and put the const keyword in front of it:

package at.bestsolution.efxclipse.tutorial.fxgraph
 
import javafx.scene.control.*
import javafx.scene.layout.BorderPane
component ConstantTest {
  BorderPane {
    Button {
      minHeight : const Double#NEGATIVE_INFINITY
    }
  }
}

Defining locations

Resource locations like URLs can be defined in FXGraph using the {{{location}}} keyword. In equivalence with the corresponding CSS element, the location can be specified either absolute (e.g. "!http://...") or relative to the location of the file it is defined in. Here's an example of a button whose image is defined in a local file:

package at.bestsolution.efxclipse.tutorial.fxgraph
 
import javafx.scene.layout.HBox
import javafx.scene.control.Button
import javafx.scene.image.ImageView
import javafx.scene.image.Image
 
component LocationTest {
  HBox {
    children : [
      Button id saveButton {
        graphic : ImageView {
	  image : Image {
	    url : location "icons/save.png"
	  }
        }
      }
    ]
  }
}

Referencing a root element

FXML allows for defining a <fx:root> element with which you can reference a previously defined root element (see here). This feature is particularly useful when working with custom controls. In order to do the same with your FXGraph component, use the dynamic keyword:

package at.bestsolution.efxclipse.tutorial.fxgraph
 
import javafx.scene.layout.HBox
import javafx.scene.control.Button
 
dynamic component RootTest {
  HBox {
    children : [
      Button {
        text : "Hello World."
        }
      }
    ]
  }
}

This will result in the following FXML code:

<?xml version="1.0" encoding="UTF-8"?>
 
<?import java.lang.*?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.HBox?>
 
<fx:root xmlns:fx="http://javafx.com/fxml" type="HBox">
  <children>
    <Button text="Hello World"/> 
  </children>
</fx:root>

Pimping up the preview

The preview keyword allows you to make elements appear in the live preview without adding them to FXML code. For instance, the following example creates three string items for the choice box that will be shown only in the preview.

package at.bestsolution.efxclipse.tutorial.fxgraph
 
import javafx.scene.layout.HBox
import javafx.scene.control.ChoiceBox
import javafx.collections.FXCollections
 
component PreviewTest {
  HBox {
    children : [
      ChoiceBox {
	preview items : FXCollections createdby observableArrayList {
          "Hello World",
	  "Hello World extra long", 
	  "Hello World extra extra long"
	}
      }
    ]
  }
}

The corresponding generated FXML code does not contain these items:

<?xml version="1.0" encoding="UTF-8"?>
 
<?import java.lang.*?>
<?import javafx.scene.control.ChoiceBox?>
<?import javafx.scene.layout.HBox?>
 
<HBox xmlns:fx="http://javafx.com/fxml">
  <children>
    <ChoiceBox/> 
  </children>
</HBox>

Binding property values

You can define a binding of a property value to an expression or a variable using the bind keyword. This code binds the text value of the text field field2 to the corresponding value of field1:

package at.bestsolution.efxclipse.tutorial.fxgraph
 
import javafx.scene.layout.HBox
import javafx.scene.control.Label
import javafx.scene.control.TextField
 
component BindingTest {
  HBox {
    children : [
      Label {
        text : "Binding"  
      },
      TextField id field1 {
      },
      TextField id field2 {
        text : bind field1#text
      }
    ]
  }
}

For more information on bindings in FXML, see here.