CSC1120: FXML
Contents
Introduction
FXML is a declarative language provided by JavaFX to define the layout and appearance of user interface elements. It's an XML-based language, which means it uses tags to describe the UI components, much like how HTML describes web pages. Note that FXML is NOT Java source code. It is not able to be compiled. It is a way to describe how the components of a JavaFX GUI will appear and function. The FXML file will be read in and translated by an object in the start()
method, the FXMLLoader
, which will load all the components described in the FXML file into memory and link them to the Scene
that is being displayed in the Stage
.
There are several advantages to using FXML when creating a JavaFX application. First, the layout and appearance of the GUI is completely separated from the programming logic of the application, allowing for the development to be split amongst different teams so designers can focus on the look and feel of the application and developers can concentrate on implementing the behavior of the program in Java. Secondly, FXML makes it easier for non-programmers and people with experience in web design to work on JavaFX projects, as the syntax and structure of FXML is very similar to HTML, the default markup language for web pages. Thirdly, the split between the appearance and the logic of the program and the class that is used to connect the two sides, the Controller
follow a well-known software design patter, the Model-View-Controller (MVC) pattern, whose benefits will be discussed in the next section. Finally, adding styling to the components using FXML is nearly identical to styling of web pages, namely Cascading Style Sheets (CSS), which are style directives that are overlaid onto the existing markup and can be easily swapped out or changed to suit a specfic look or feel.
The Model-View-Controller pattern
The Model-View-Controller (MVC) pattern is a design pattern often used in software development for organizing code in an application to separate concerns, thus making it more modular and maintainable. It divides the application into three interconnected components: Model, View, and Controller.
The Components of MVC
- Model: Represents the core data and the business rules of the application. It's the internal representation of the data entities and logic. Changes in the model are typically reflected in the view, and it can notify the view about these changes.
- View: Represents the user interface (UI) elements of the application, responsible for displaying data to the user and presenting it in a readable and intuitive format. It does not handle user input directly but relies on the controller to manage that interaction.
- Controller: Acts as an intermediary between the model and the view. It listens for user input (from the view), processes it (possibly updating the model), and returns the output display (to the view). The controller decides how the model needs to change when a user interacts with the view.
MVC Example
An example would be a to-do list application
- The Model would handle the data representing the tasks, their status, and operations to add, modify, or remove tasks. In Java, that would be instance variables for each task, some collection of the tasks, such as an
ArrayList
, and the methods used to create, delete, or update the task variables. - The View would display the list of tasks, showing their names and statuses, possibly in a list format. The View would not store any of the data related to the tasks, but would be passed that information by the
Controller
. In Java, this would be the FXML file. - The Controller would handle user input, such as completing a task or adding a new task, by calling the appropriate model methods, getting the data from the Model and sending the data to the view to be displayed. In Java, this would be, unsurprisingly, the
Controller
class.
Benefits of using MVC
There are many good reasons to use the MVC pattern both within a JavaFX application and elsewhere. Below is a non-exhaustive list of benefits that the MVC pattern provides software engineers
-
Separation of Concerns: By separating application concerns (data, display, and actions), MVC ensures that each component has a distinct responsibility. This makes the codebase cleaner, more organized, and easier to manage.
-
Reusability: The clear separation means that components, especially the model, can be reused across different parts of an application or even in different applications.
-
Flexibility: Since the view is separated from the data, it's easier to redesign the UI without touching the underlying logic. Similarly, changes to the business logic don't necessarily require changes to the UI.
-
Scalability: With a modular structure, it's easier to scale and evolve the application. You can change or expand one component without needing to rework the entire system.
-
Testability: Due to the separation, unit testing becomes more straightforward. The model can be tested independently of user input or UI presentation, and vice versa.
-
Parallel Development: Designers can work on the view while developers work on the model and controller. Since these components are decoupled, they can be developed simultaneously with minimal overlap.
The FXML File
An FXML file is a plain text file that is structured hierarchically by using nested tags. A tag is a pair of opening <>
and closing </>
set of angle brackets. Inside the brackets are the descriptions of the component type and properties. Between the opening and closing brackets will be any information that is contained inside the component. Depending on the component, this could be some text, other components, or even other containers filled with more components. Below is an example of a Button
tag in FXML that describes a Button
that, when clicked, will open a selected file by calling the openFile
method. Note that the properties are separated by a single whitespace.
<Button onAction="#openFile text="Open"></Button>
When there are no components contained inside a given tag, we can use a shortcut that combines the opening and closing tag into a single tag:
<Button onAction="#openFile text="Open" />
Here is an example of a container (in this case a Vertical Box, or VBox
) that holds a few different Button
objects. A VBox will store components vertically so they will appear on the screen from top to bottom. A Horizontal Box (HBox
) would display they left to right on the screen. Note that the Button
tags are inside the opening and closing tags for the VBox
. Also note all of the layout information stored in the VBox tag. Most properties may be defined inside a tag of an object, but the most common properties, since FXML defines the View, are layout, location, and information the Controller needs to update the component, such as the type of listener, the handler method, and sometimes the variable name of the object in the Controller
class (if needed).
<VBox alignment="TOP_CENTER" prefHeight="200.0" prefWidth="100.0" spacing="20.0">
<Button onAction="#openFile text="Open" />
<Button onAction="#closeFile text="Close" />
<Label onMouseClicked="#formatHardDrive" text="DO NOT CLICK!!!" />
</VBox>
The top level tag of every FXML file is the Parent
container and has some specific properties that are needed for the FXMLLoader
to properly read the file. Here is what it would look like:
<BorderPane xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="jones.Lab9Controller">
After the type of container, the top level tag must contain the following properties:
- xmlns: The XML Namespace. This defines this file as an FXML file, as opposed to some other markup language file. Since there are many markup languages and they are very similar to each other, there is a need to distinguish which language this file will be representing. An example of why this matters is that the html and fxml tags for a
Button
are practically identical. If you were to build a web app that uses FXML and did not specify that this file was JavaFX, the interpreter would not know whether it should treat the tag as HTML or FXML. The namespace will always be http://javafx.com/javafx. - xmlns:fx This property defines the prefix
fx
to be used for any information that describes an FXML-specific feature and distinguishes them from standard JavaFX components. Elements and attributes with thefx:
prefix are interpreted as FXML-specific commands, properties, or features. For example,fx:controller
is an FXML-specific property that wouldn't be recognized without the appropriate namespace declaration. Another example would be thefx:id
property, which defines the Java variable name of the component being described in the tag. Without thefx:
prefix, theid
tag has a different meaning. Essentially, if you need the property to be used to interact with Java code, it will need to have thefx
prefix added to the property name. This namespace will always be http://javafx.com/fxml - fx:controller: As mentioned above, this is an FXML specific property that is used by Java to know which class is the Controller class for this FXML file. Every FXML file has exactly one Controller class (though it may not be called
Controller
). TheFXMLLoader
will read the top level tag and use this information to know where to instantiate the components. Note that the package name and the class are separated by a dot.
, not a slash.
Here is an example of a complete FXML file. The first line is the header information and tells the FXMLLoader
what type of file it is looking at. The import statements that follow are identical in use to Java import statements. They tell the FXMLLoader
where to find the source code for the components used in the FXML file.
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.VBox?>
<BorderPane xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="jones.Lab9Controller">
<right>
<VBox alignment="TOP_CENTER" prefHeight="200.0" prefWidth="100.0" spacing="20.0" BorderPane.alignment="CENTER">
<Button onAction="#load" prefWidth="87.0" text="Open" />
<Button onAction="#save" prefWidth="87.0" text="Save" />
<Button onAction="#reload" prefWidth="87.0" text="Reload" />
<Button onAction="#grayscale" prefWidth="87.0" text="Grayscale" />
<Button onAction="#negative" prefWidth="87.0" text="Negative" />
<Button onAction="#red" prefWidth="87.0" text="Red" />
<Button onAction="#redGray" prefWidth="87.0" text="Red-Gray" />
<Button fx:id="filterButton" onAction="#filter" prefWidth="87.0" text="Show Filter" />
</VBox>
</right>
<bottom>
<Label fx:id="mouseColorLabel" BorderPane.alignment="CENTER" />
</bottom>
</BorderPane>
The Controller Class
The Controller
class is the bridge between the Model and View. It is a normal Java class that will be instantiated by the FXMLLoader
when the program starts. When the Controller
is instantiated, the FXMLLoader
will look through the FXML file and look for any information needed to add to the Controller
. This includes any variable names defined in the tags using an fx:id
property and any event handler method names. For the program to compile, all handler methods and variable names defined in the FXML file MUST exist in the Controller
. These variables and methods should use the @FXML
annotation to denote to the compiler that these are defined in the FXML file. Here is an example of what a portion of the Controller
for the above FXML file would look like.
@FXML
private Button filterButton;
@FXML
private void filter() {
...
}
Note that both the variable and handler method are private. As stated before, this is a normal Java object class. Instance variables should be private unless absolutely necessary, and the handler method, unless it will be called by some other class, should also remain private. Without the @FXML
annotation, the FXMLLoader
, which is in the start()
method of the main class, would not have access to the variable and method and would not be able to instantiate the component or link the method.
It is important to note that the Controller
class should only contain code directly related to interacting with the View and/or bridging between the Model and View. Any data or other program logic should be in another class related to the Model and passed into the Controller
via a method. Private helper methods and variables are often necessary when an event handler has a complicated or multi-step task to perform. These methods and variables are normal Java source code and should NOT use the @FXML
notation. Only information referencing the FXML file should use the annotation.
The FXMLLoader
The last piece of the FXML puzzle is the FXMLLoader. This is a Java class that reads the FXML file and instantiates all of the components defined in the FXML as well as the Controller
class, storing everything in memory and linking the Parent
object defined in the FXML file to the Scene
that gets put into the Stage
. The FXMLLoader
is instantiated at the beginning of the start()
method and is linked to the FXML file.
FXMLLoader fxmlLoader = new FXMLLoader();
fxmlLoader.setLocation(getClass().getResource("MyProgram.fxml"));
Since the FXML file is plain text and not source code, we have to load it as a Resource
object, which is what the getClass().getResource()
code is doing. It is wrapping the text inside of a Java object so we can pass it as a parameter into the setLocation()
method. A link to the FXML file is now stored as an instance variable inside the fxmlLoader
object. At this point we are ready to load all the information from the FXML file into out program. We now will call the load()
method of the FXMLLoader
,
Parent root = fxmlLoader.load();
which will take everything defined inside the FXML file and instantiate and link the contents to the rest of the program. The way this work is the loader will read the FXML file as if it were a Scanner
reading one line at a time:
- Header: The loader reads the first line of the FXML file to ensure it is an XML file and can be read like one. The character encoding for the loader is set so we will properly read the contents of the file.
- import: All of the necessary classes are imported into memory. This is necessary because some of the classes defined in the FXML file may not be present anywhere else in the source code of the program, thus the code will not have been imported. An example in the above FXML file would be the VBox. It is never altered by code in the
Controller
so an instance variable would never need to be created. - Parent: The top level container is instantiated and assigned to the
Parent
object in thestart()
method. All of the properties we mentioned earlier are stored in the loader, including the location of theController
class. - Controller: The
Controller
class is instantiated and stored as an instance variable of the loader class. - Components: At this point the header, imports and top level tag has been read. The loader will now go one tag at a time, instantiate the component described in the tag, add whatever properties are defined, including linking the components to their defined handler methods. These components are stored, in the order the are read, as children of their containing component. This means there are no direct variables pointing to them by default. All components that can hold other components inside themselves have an instance variable
List
calledchildren
where these components will be saved. Only components that have defined anfx:id
property will be assigned a variable in theController
.- For example, using the above FXML, all the
Button
objects will be stored in thechildren
variable of theVBox
. TheVBox
will be stored in thechildren
variable of theBorderPane
. ThefilterButton
will be stored in theVBox
'schildren
List
but will also have an instance variable in theController
class pointing to it. All theButton
objects will also have their listener methods assigned and the handler methods defined, so thefilterButton
object will have anActionListener
defined that will call thefilter
method in theController
when triggered.
- For example, using the above FXML, all the
- Initialize: Once the FXML file has been read and all the components have been instantiated, the loader will call the
initialize()
method of theController
class. This is initially an empty method, but if you have something you need to do to a component defined in the FXML file, such as change it's color, it's text, etc., this is where you would do that. The reason for this is until the loader goes through the FXML and instantiates the components, they don't yet exist and their variables will benull
. As a result, the loader waits until everything is instantiated, then callsinitialize
to do any final work on the components before it sends the code on to be rendered in the window.