Agenda

This tutorial describes how to create JavaFX views using different techniques:

  • JavaFX API.

  • FXML.

  • GroovyFX.

  • GroovyFX and FXML.

The goal of the application is to build the same MVCGroup using different techniques for each View. Each MVCGroup defines a Tab that the main View will host. The application looks like the following picture

javafx app
Figure 1. JavaFX Views

1. Creating the Application

We’ll follow similar steps as explained in Tutorial 1::Getting Started to create a brand new Griffon 2 application. Assuming you’ve got SDKMAN, Maven and Gradle already installed on your system, execute the following command on a console prompt, paying attention to the selections we’ve made

$ mvn archetype:generate \
      -DarchetypeGroupId=org.codehaus.griffon.maven \
      -DarchetypeArtifactId=griffon-javafx-java-archetype \
      -DarchetypeVersion=2.16.0 \
      -DgroupId=editor \
      -DartifactId=editor\
      -Dversion=1.0.0-SNAPSHOT \
      -Dgradle=true

There should be a new directory named app with the freshly created application inside. At this point you can import the project in your favourite IDE. We’ll continue with Gradle on the command line to keep things simple. Verifying that the application is working should be our next step. Execute the following command

$ gradle run

A window should pop up after a few seconds. Quit the application, we’re ready to begin customizing the application.

Top

2. Customizing the Application

Because we’ll be making use of GroovyFX we have to tweak the build file a little bit. Follow these steps to enable compilation of Groovy sources in this application:

  1. Open build.gradle on an editor.

  2. Locate the griffon configuration block and enable the inclusion of Groovy dependencies, like so

build.gradle
griffon {
    disableDependencyResolution = false
    includeGroovyDependencies = true
    version = '2.16.0'
    toolkit = 'javafx'
}
  1. Remove compileGroovy.enabled = false.

  2. Add a dependency definition for ikonli-javafx and its FontAwesome icon pack.

build.gradle
dependencies {
    compile 'org.kordamp.ikonli:ikonli-javafx:1.8.0'
    compile 'org.kordamp.ikonli:ikonli-fontawesome-pack:1.8.0'
}

Next, we’ll setup the main MVC group (app). We only need the View, which means we can delete the following files

  • griffon-app/controllers/org/example/AppController.java

  • griffon-app/models/org/example/AppModel.java

  • griffon-app/resources/org/example/app.fxml

  • src/test/java/org/example/AppControllerTest.java

With these files gone we have to update the application’s configuration so that the MVCGroup does not refer to them. Open up Config.java and make sure that the configuration of the app group looks like this:

griffon-app/conf/Config.java
.e("app", map()
    .e("view", "org.example.AppView")
)

Finally edit AppView.java; place the following content on it

griffon-app/views/org/example/AppView.java
 * limitations under the License.
 */
package org.example;

import griffon.core.artifact.GriffonView;
import griffon.metadata.ArtifactProviderFor;
import javafx.scene.Scene;
import javafx.scene.control.TabPane;
import javafx.stage.Stage;
import org.codehaus.griffon.runtime.javafx.artifact.AbstractJavaFXGriffonView;

import javax.annotation.Nonnull;
import java.util.Collections;
import java.util.Map;

@ArtifactProviderFor(GriffonView.class)
public class AppView extends AbstractJavaFXGriffonView {
    private TabPane tabPane;

    @Nonnull
    public TabPane getTabPane() {
        return tabPane;
    }

    @Override
    public void mvcGroupInit(@Nonnull Map<String, Object> args) {
        createMVCGroup("tab1");
        createMVCGroup("tab2");
        createMVCGroup("tab3");
        createMVCGroup("tab4");
    }

    @Override
    public void initUI() {
        Stage stage = (Stage) getApplication()
            .createApplicationContainer(Collections.<String, Object>emptyMap());
        stage.setTitle(getApplication().getConfiguration().getAsString("application.title"));
        tabPane = new TabPane();
        Scene scene = new Scene(tabPane);
        scene.getStylesheets().add("bootstrapfx.css");
        stage.setScene(scene);
        stage.sizeToScene();
        getApplication().getWindowManager().attach("mainWindow", stage);
    }
}

The main view creates a TabPane that will hold each one of the tabs. It also creates 4 MVCGroups during its initialization. We’ll setup each one of these groups, but before we forget we’ll setup their configuration in Config.java

griffon-app/conf/Config.java
.e("tab1", map()
    .e("model", "org.example.SampleModel")
    .e("view", "org.example.Tab1View")
    .e("controller", "org.example.SampleController")
)
.e("tab2", map()
    .e("model", "org.example.SampleModel")
    .e("view", "org.example.Tab2View")
    .e("controller", "org.example.SampleController")
)
.e("tab3", map()
    .e("model", "org.example.SampleModel")
    .e("view", "org.example.Tab3View")
    .e("controller", "org.example.SampleController")
)
.e("tab4", map()
    .e("model", "org.example.SampleModel")
    .e("view", "org.example.Tab4View")
    .e("controller", "org.example.SampleController")
)

Notice that every group uses the same model and controller configuration. This is possible because we only need to change the view for each tab; the behavior remains exactly the same. We’ll setup these classes with the first tab.

Top

3. Tab1: JavaFX API

Let’s generate the first MVCGroup by copying an existing one. We’ll create a new set of files for the sample MVC group, as follows:

griffon-app/models/org/example/SampleModel.java
griffon-app/views/org/example/SampleView.java
griffon-app/controllers/org/example/SampleController.java

Alright, good thing we updated the MVC configurations in Config.java already. The behavior of each tab is to process an input value whenever the "Say Hello" button is clicked. This behavior is identical to the one described at the JavaFX section of the sample applications appendix found at the Griffon Guide. We’ll reuse the Model, Controller, Service and resources described in that section; so the application should have the following contents

griffon-app/models/org/example/SampleModel.java
 * limitations under the License.
 */
package org.example;

import griffon.core.artifact.GriffonModel;
import griffon.metadata.ArtifactProviderFor;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import org.codehaus.griffon.runtime.core.artifact.AbstractGriffonModel;

import javax.annotation.Nonnull;

@ArtifactProviderFor(GriffonModel.class)
public class SampleModel extends AbstractGriffonModel {
    private StringProperty input;
    private StringProperty output;

    @Nonnull
    public final StringProperty inputProperty() {
        if (input == null) {
            input = new SimpleStringProperty(this, "input", "");
        }
        return input;
    }

    @Nonnull
    public String getInput() {
        return input == null ? null : inputProperty().get();
    }

    public void setInput(String input) {
        inputProperty().set(input);
    }

    @Nonnull
    public final StringProperty outputProperty() {
        if (output == null) {
            output = new SimpleStringProperty(this, "output", "");
        }
        return output;
    }

    @Nonnull
    public String getOutput() {
        return output == null ? null : outputProperty().get();
    }

    public void setOutput(String output) {
        outputProperty().set(output);
    }
}
griffon-app/controllers/org/example/SampleController.java
 * limitations under the License.
 */
package org.example;

import griffon.core.artifact.GriffonController;
import griffon.inject.MVCMember;
import griffon.metadata.ArtifactProviderFor;
import org.codehaus.griffon.runtime.core.artifact.AbstractGriffonController;

import javax.annotation.Nonnull;
import javax.inject.Inject;

@ArtifactProviderFor(GriffonController.class)
public class SampleController extends AbstractGriffonController {
    private SampleModel model;

    @Inject
    private SampleService sampleService;

    @MVCMember
    public void setModel(@Nonnull SampleModel model) {
        this.model = model;
    }

    public void sayHello() {
        model.setOutput(sampleService.sayHello(model.getInput()));
    }
}

You can manually create a SampleService.java

griffon-app/services/org/example/SampleService.java
 * limitations under the License.
 */
package org.example;

import griffon.core.artifact.GriffonService;
import griffon.core.i18n.MessageSource;
import griffon.metadata.ArtifactProviderFor;
import org.codehaus.griffon.runtime.core.artifact.AbstractGriffonService;

import static griffon.util.GriffonNameUtils.isBlank;
import static java.util.Arrays.asList;

@ArtifactProviderFor(GriffonService.class)
public class SampleService extends AbstractGriffonService {
    public String sayHello(String input) {
        MessageSource messageSource = getApplication().getMessageSource();
        if (isBlank(input)) {
            return messageSource.getMessage("greeting.default");
        } else {
            return messageSource.getMessage("greeting.parameterized", asList(input));
        }
    }
}
griffon-app/i18n/messages.properties
#

name.label = Please enter your name
greeting.default = Howdy stranger!
greeting.parameterized = Hello {0}

The final piece is to update the View. We’ll make direct use of the JavaFX API. Of course the code will look a bit verbose however the important thing to remember here is that you can use the JavaFX API at any time, that is, you’re not forced to build the UI using FXML only.

griffon-app/views/org/example/Tab1View.java
 * limitations under the License.
 */
package org.example;

import griffon.core.artifact.GriffonView;
import griffon.inject.MVCMember;
import griffon.javafx.beans.binding.UIThreadAwareBindings;
import griffon.javafx.support.JavaFXAction;
import griffon.javafx.support.JavaFXUtils;
import griffon.metadata.ArtifactProviderFor;
import javafx.beans.property.StringProperty;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TextField;
import javafx.scene.layout.AnchorPane;
import org.codehaus.griffon.runtime.javafx.artifact.AbstractJavaFXGriffonView;
import org.kordamp.ikonli.fontawesome.FontAwesome;
import org.kordamp.ikonli.javafx.FontIcon;

import javax.annotation.Nonnull;

import static javafx.scene.layout.AnchorPane.setLeftAnchor;
import static javafx.scene.layout.AnchorPane.setTopAnchor;

@ArtifactProviderFor(GriffonView.class)
public class Tab1View extends AbstractJavaFXGriffonView {
    @MVCMember @Nonnull private SampleController controller;
    @MVCMember @Nonnull private SampleModel model;
    @MVCMember @Nonnull private AppView parentView;

    private StringProperty uiInput;
    private StringProperty uiOutput;

    @Override
    public void initUI() {
        AnchorPane anchorPane = new AnchorPane();
        anchorPane.setPrefHeight(90.0);
        anchorPane.setPrefWidth(384.0);

        Label label = new Label(getApplication().getMessageSource().getMessage("name.label"));
        TextField input = new TextField();
        input.setPrefWidth(200.0);

        Button button = new Button();
        button.setPrefWidth(200.0);
        button.getStyleClass().addAll("btn", "btn-primary");
        JavaFXUtils.configure(button, (JavaFXAction) actionFor(controller, "sayHello").getToolkitAction());

        Label output = new Label();
        label.setPrefWidth(360.0);

        uiInput = UIThreadAwareBindings.uiThreadAwareStringProperty(model.inputProperty());
        uiOutput = UIThreadAwareBindings.uiThreadAwareStringProperty(model.outputProperty());
        input.textProperty().bindBidirectional(uiInput);
        output.textProperty().bind(uiOutput);

        anchorPane.getChildren().addAll(label, input, button, output);

        setLeftAnchor(label, 14.0);
        setTopAnchor(label, 14.0);
        setLeftAnchor(input, 172.0);
        setTopAnchor(input, 11.0);
        setLeftAnchor(button, 172.0);
        setTopAnchor(button, 45.0);
        setLeftAnchor(output, 14.0);
        setTopAnchor(output, 80.0);

        Tab tab = new Tab("Java");
        tab.setGraphic(new FontIcon(FontAwesome.COFFEE));
        tab.setClosable(false);
        tab.setContent(anchorPane);
        parentView.getTabPane().getTabs().add(tab);
    }
}

Of particular note, apart from the standard JavaFX API usage is the configuration of the button’s properties using a JavaFXGriffonAction. Actions define a container for both behavior and visual clues that buttons and other action enabled widgets may use to configure themselves. This view also uses the same trick we saw earlier at the Tutorial 3::MVC Groups (JavaFX) tutorial, that is, a reference to the parentView allows the tab to be attached at the right location and time. We’ll use the same mechanism for the other tabs.

We’re ready to move to the next tab: FXML.

Top

4. Tab2: FXML

FXML is an XML format used to describe a JavaFX UI declaratively. You may use SceneBuilder or write the file by hand, at the end it doesn’t matter as long as you follow some naming conventions. The first is the name and location of the FXML file. If the view is named org/example/Tab2View.java then the FXML file should be named org/example/tab2.fxml. You may place this file anywhere on your sources as long as it ends in the application’s classpath. We recommend you use the griffon-app/resources directory though. Here’s how the tab2.fxml should look like:

griffon-app/resources/org/example/tab2.fxml
<?xml version="1.0" encoding="UTF-8"?>
<!--

    SPDX-License-Identifier: Apache-2.0

    Copyright 2016-2021 the original author or authors.

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

        http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.

-->
<?import griffon.javafx.support.JavaFXUtils?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane id="AnchorPane" maxHeight="-Infinity" maxWidth="-Infinity"
            minHeight="-Infinity" minWidth="-Infinity"
            prefHeight="90.0" prefWidth="384.0"
            xmlns:fx="http://javafx.com/fxml"
            fx:controller="org.example.Tab2View">
    <Label layoutX="14.0" layoutY="14.0" text="%name.label"/>
    <TextField fx:id="input" layoutX="172.0" layoutY="11.0"
               prefWidth="200.0"/>
    <Button layoutX="172.0" layoutY="45.0"
            mnemonicParsing="false"
            prefWidth="200.0"
            styleClass="btn, btn-primary"
            JavaFXUtils.griffonActionId="sayHello"/>
    <Label layoutX="14.0" layoutY="80.0" prefWidth="360.0" fx:id="output"/>
</AnchorPane>

It looks like any other FXML file except that the button’s id follows a naming convention. Notice that the controller attribute is set to org.example.Tab2View. An instance of this type will receive all injections based on fields annotated with @FXML. However the controller (SampleController) will react to actions matching a naming convention, in our case it will be a single action name sayHello. The Griffon runtime is able to match an action to a corresponding UI node by using this naming convention. The Tab2View.java file defines the second half of this view

griffon-app/views/org/example/Tab2View.java
 * limitations under the License.
 */
package org.example;

import griffon.core.artifact.GriffonView;
import griffon.inject.MVCMember;
import griffon.javafx.beans.binding.UIThreadAwareBindings;
import griffon.metadata.ArtifactProviderFor;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TextField;
import org.codehaus.griffon.runtime.javafx.artifact.AbstractJavaFXGriffonView;
import org.kordamp.ikonli.fontawesome.FontAwesome;
import org.kordamp.ikonli.javafx.FontIcon;

import javax.annotation.Nonnull;

@ArtifactProviderFor(GriffonView.class)
public class Tab2View extends AbstractJavaFXGriffonView {
    @MVCMember @Nonnull private SampleController controller;
    @MVCMember @Nonnull private SampleModel model;
    @MVCMember @Nonnull private AppView parentView;

    @FXML private TextField input;
    @FXML private Label output;

    private StringProperty uiInput;
    private StringProperty uiOutput;

    @Override
    public void initUI() {
        Node node = loadFromFXML();
        uiInput = UIThreadAwareBindings.uiThreadAwareStringProperty(model.inputProperty());
        uiOutput = UIThreadAwareBindings.uiThreadAwareStringProperty(model.outputProperty());
        input.textProperty().bindBidirectional(uiInput);
        output.textProperty().bind(uiOutput);
        connectActions(node, controller);

        Tab tab = new Tab("FXML");
        tab.setGraphic(new FontIcon(FontAwesome.COG));
        tab.setContent(node);
        tab.setClosable(false);

        parentView.getTabPane().getTabs().add(tab);
    }
}

The code is much simpler as we rely on the FXML file to define the UI; this file only needs to be concerned to finish up the bindings and setup the tab, in a similar fashion as it was done with the first tab.

Top

5. Tab3: GroovyFX

In a few words, GroovyFX can be seen as a Groovy DSL for writing JavaFX UIs. Instead of XML you use Groovy. This allows you to leverage a real programming language instead of just markup. We can make use of Groovy support in a straight Java application such as this because Java and Groovy can be mixed without any problems, as opposed to other popular JVM languages. This is also the reason why we updated the build file back at step 2. The DSL resembles a lot the now defunct JavaFX Script language.

griffon-app/views/org/example/Tab3View.groovy
 * limitations under the License.
 */
package org.example

import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.javafx.beans.binding.UIThreadAwareBindings
import griffon.metadata.ArtifactProviderFor
import javafx.beans.property.StringProperty
import javafx.scene.control.Tab
import org.kordamp.ikonli.fontawesome.FontAwesome
import org.kordamp.ikonli.javafx.FontIcon

import javax.annotation.Nonnull

@ArtifactProviderFor(GriffonView)
class Tab3View {
    @MVCMember @Nonnull FactoryBuilderSupport builder
    @MVCMember @Nonnull SampleModel model
    @MVCMember @Nonnull AppView parentView

    private StringProperty uiInput
    private StringProperty uiOutput

    void initUI() {
        uiInput = UIThreadAwareBindings.uiThreadAwareStringProperty(model.inputProperty())
        uiOutput = UIThreadAwareBindings.uiThreadAwareStringProperty(model.outputProperty())

        builder.with {
            content = anchorPane {
                label(leftAnchor: 14, topAnchor: 14,
                    text: application.messageSource.getMessage('name.label'))
                textField(leftAnchor: 172, topAnchor: 11, prefWidth: 200,
                    text: bind(uiInput))
                button(leftAnchor: 172, topAnchor: 45, prefWidth: 200,
                    styleClass: ['btn', 'btn-primary'],
                    sayHelloAction)
                label(leftAnchor: 14, topAnchor: 80, prefWidth: 200,
                    text: bind(uiOutput))
            }
        }

        Tab tab = new Tab('GroovyFX')
        tab.graphic = new FontIcon(FontAwesome.FLASH)
        tab.content = builder.content
        tab.closable = false
        parentView.tabPane.tabs.add(tab)
    }
}

GroovyFX provides a builder class that can be used to instantiate nodes using the DSL capabilities delivered by the Groovy syntax. In this particular view, the builder is used to recreate the same layout found in the other tabs. The code looks like a cross between FXML (defining nodes and their layout properties) and the direct JavaFX API (defining bindings right on the spot). The code for setting up the tab is exactly the same, albeit it looks a bit different, actually, more streamlined, due to Groovy’s property access feature.

Top

6. Tab4: GroovyFX and FXML

The last tab deals with the use case of reusing existing FXML assets. If you already have an FXML file but you’d also want to use Groovy to setup bindings and do additional work, you may use the fxml() node from GroovyFX to load the FXML file. Then you can further enhance the view by using the GroovyFX DSL. Let’s see the FXL file first

griffon-app/resources/org/example/tab4.fxml
<?xml version="1.0" encoding="UTF-8"?>
<!--

    SPDX-License-Identifier: Apache-2.0

    Copyright 2016-2021 the original author or authors.

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

        http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.

-->
<?import griffon.javafx.support.JavaFXUtils?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane id="AnchorPane" maxHeight="-Infinity" maxWidth="-Infinity"
            minHeight="-Infinity" minWidth="-Infinity"
            prefHeight="90.0" prefWidth="384.0"
            xmlns:fx="http://javafx.com/fxml">
    <Label layoutX="14.0" layoutY="14.0" fx:id="inputLabel"/>
    <TextField fx:id="input" layoutX="172.0" layoutY="11.0"
               prefWidth="200.0"/>
    <Button layoutX="172.0" layoutY="45.0"
            mnemonicParsing="false"
            prefWidth="200.0"
            styleClass="btn, btn-primary"
            JavaFXUtils.griffonActionId="sayHello"/>
    <Label layoutX="14.0" layoutY="80.0" prefWidth="360.0" fx:id="output"/>
</AnchorPane>

This file does not define a controller nor makes use of ResourceBundle resources, the reason being is that the fxml() node is quite spartan at the moment: it does not aware of the Griffon conventions nor does it setup a ResourceBundle, thus i18n resources can not be resolved properly. We’ll rely on the Groovy view to fix these shortcomings.

griffon-app/views/org/example/Tab4View.groovy
 * limitations under the License.
 */
package org.example

import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.javafx.beans.binding.UIThreadAwareBindings
import griffon.metadata.ArtifactProviderFor
import javafx.beans.property.StringProperty
import javafx.scene.control.Tab
import org.codehaus.griffon.runtime.javafx.artifact.AbstractJavaFXGriffonView
import org.kordamp.ikonli.fontawesome.FontAwesome
import org.kordamp.ikonli.javafx.FontIcon

import javax.annotation.Nonnull

@ArtifactProviderFor(GriffonView)
class Tab4View extends AbstractJavaFXGriffonView {
    @MVCMember @Nonnull FactoryBuilderSupport builder
    @MVCMember @Nonnull SampleController controller
    @MVCMember @Nonnull SampleModel model
    @MVCMember @Nonnull AppView parentView

    private StringProperty uiInput
    private StringProperty uiOutput

    void initUI() {
        uiInput = UIThreadAwareBindings.uiThreadAwareStringProperty(model.inputProperty())
        uiOutput = UIThreadAwareBindings.uiThreadAwareStringProperty(model.outputProperty())

        builder.with {
            content = builder.fxml(resource('/org/example/tab4.fxml')) {
                inputLabel.text = application.messageSource.getMessage('name.label')
                bean(input, text: bind(uiInput))
                bean(output, text: bind(uiOutput))
            }
        }

        connectActions(builder.content, controller)

        Tab tab = new Tab('Hybrid')
        tab.graphic = new FontIcon(FontAwesome.ROCKET)
        tab.content = builder.content
        tab.closable = false
        parentView.tabPane.tabs.add(tab)
    }
}

Notice that all widgets with a proper id attribute can be accessed directly inside the block associated with the fxml() node. These happen to be the widgets we’re interested in, thus bindings can be set accordingly. Another change seen in Tab4View is that this class extends directly from AbstractJavaFXGriffonView, allowing this view to invoke the connectActions() method. Finally the tab is setup in exactly the same way as we’ve seen before.

This is the final tab that we need to setup in order for the application to work. You can run the application once more by invoking the following command:

$ ./gradlew run

The full code for this application can be found link:https://github.com/griffon/griffon/tree/ development_2_x/tutorials/javafx-app[here, window="_blank"].

Top