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
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.
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:
Open build.gradle
on an editor.
Locate the griffon
configuration block and enable the inclusion of Groovy dependencies, like so
griffon {
disableDependencyResolution = false
includeGroovyDependencies = true
version = '2.16.0'
toolkit = 'javafx'
}
Remove compileGroovy.enabled = false
.
Add a dependency definition for ikonli-javafx
and its FontAwesome icon pack.
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:
.e("app", map()
.e("view", "org.example.AppView")
)
Finally edit AppView.java
; place the following content on it
* 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
.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.
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
* 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);
}
}
* 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
* 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));
}
}
}
#
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.
* 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.
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:
<?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
* 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.
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.
* 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.
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
<?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.
* 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"].