Advanced GUI Creation
If you want to create a GUI more complex than can be achieved by using the various blocks provided in JISA.GUI
, like the example below, then you will need to create your GUI from scratch. Thankfully, Java provides several fully-fledged GUI libraries for you to use. The most modern of these is JavaFX.
This page aims to provide you with the basics for creating a GUI with JavaFX, assuming that you are using IntelliJ IDEA to create your application.
Structure
Before jumping in, let's go over how a GUI is structured in JavaFX. By-and-large a JavaFX GUI consists of two parts:
- The layout of the GUI (FXML File)
- The Java code that controls the GUI (Controller)
Simply put, the FXML file defines what your GUI should look like, where each button/textbox/image etc goes and their sizes etc. Each layout is then loaded and assigned an object to act as its "controller". This object contains functions that are called when something happens with the GUI (eg when a button is clicked or a key is pressed).
The Layout - FXML File
The first part you should create is the FXML file. In IDEA, you can do this by right-clicking in the side-bar on the folder/package that you want to create the file in, going to "New" the selecting "FXML File":
This will create a file that looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<AnchorPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="ACHall.FXML.FileName"
prefHeight="400.0" prefWidth="600.0">
</AnchorPane>
The first thing to do is remove the line that says fx:controller="..."
. IDEA puts that there assuming that we are going to build our GUI a certain way. However, we are going to build it a slightly different way, hence we should remove it.
Now that we've done that, we can switch to graphically editing our FXML file (instead of directly editing the slightly scary XML code) by clicking the "Scene Builder" tab at the bottom of the page:
After loading this should show you what your GUI currently looks like:
The top-left list is of all the possible elements you can add to your GUI, bottom-left is the list of all elements you currently have in your GUI, the middle is the GUI itself and the right is the properties of the currently selected GUI element.
You will notice that there are different categories of GUI elements: "Containers", "Controls", etc. We shall cover these in the following sections.
The Layout - Elements and Properties
The first type of element to understand are "Containers". They are called this because that is what they do: they contain things. Essentially they are structures for arranging other GUI elements. For example, GridPane
arranges elements added to it in a grid, VBox
arranges them in a vertical list and HBox
arranges them in a horizontal list.
You might have noticed that we have automatically been given an AnchorPane
. This is a container that lets you put elements in it at any arbitrary position and is what we can see in our GUI as the grey rectangle in the centre. Give it a try, drag a Button
from "Controls" into the rectangle.
You should end up with the following:
In the bottom-left you should now be able to see the Button
has been added to our list of elements "inside" the AnchorPane
and when the Button
is selected, we can see all the properties of the Button
on the right-hand side where we can edit them, such as the text to show on the button, font size, text alignment etc as well as size and position properties under "Layout", as shown below:
All width and height properties as set to USE_COMPUTED_SIZE
which means the size of the button will be automatically determined based on how much text is in it and how much space is available etc. You can manually set the size of the button by specifying its dimensions (in pixles) in the Pref Width and Pref Height fieds.
The Layout - Types of Container
The default container we are given is the AnchorPane
but depending on what you want out of your GUI, you may find other container types to be more useful. Here we shall list the most important and describe what they do:
BorderPane
A BorderPane
allows you to add 5 elements to it, one each at the top, bottom, left, right and centre. This type of pane is very useful when you want a layout that calls for a toolbar at the top, side-pane on the left and/or rightm status bar at the bottom, etc.
VBox
A VBox
lets you add an unlimited number of elements which it shall display in a vertical list. Below is the result of putting 4 TextField
elements inside a VBox
(with padding and spacing):
HBox
A HBox
does the same as a VBox
but horizontally, ie it displays its elements in a horizontal list. Below is the result of putting 4 Button
elements inside an HBox
(with padding and spacing):
GridPane
A GridPane
lets you add an unlimited number of elements and displays them in a grid. You can decide where they go by dragging them into the "cell" that you want. You can add new rows and columns by right-clicking on the element in the list (by default it will give you a 2x3 grid).
Mostly you will just have to experiment with the different containers and properties to find what works for you since there's too many types to completely cover here.
The Layout - Preparing for Controller Code
Designing the layout is great, but it's all a bit pointless if it doesn't do anything. To achieve this, we will need to write code to perform actions when events are triggered. To do this, we need to first label important elements so that our code can find the elements that it needs to.
To do this, with give such elements an fx:id
property. Each fx:id
must be unique in the FXML file. For example, let's say we have the following FXML file:
Now, let's say we want the Label
text to change to "Hooray!" when the button is clicked. Therefore, our code will need to be able to access the Label
, so it will need an fx:id
. To set the fx:id
we go to the "Code" section in the properties on the right-hand side. We shall call is message
:
We will also want the button to run a function when it is clicked (to change the label text), so going to its properties we can set the onAction
property to the name of the function we want it to run:
We are now ready to start writing our controller.
Controller Class
We should now create a new Java class where we shall write the code to control our GUI. We are going to do this "the long way round" first then look at a simpler way after. The reason being that the longer version gives you insight into what actually happens whereas the simpler version is a shortcut that cuts all of that out.
To start with:
public class MessageGUI {
}
In this class, you should add a property for each element you gave an fx:id
. For our example, we only have the Label
called message
:
public class MessageGUI {
public Label message;
}
When we connect our FXML file to an object of this class, the property message
will be automatically linked to the Label
we called message
in our FXML file. Now, we can also add our method we specified in the onAction
property of the Button
:
public class MessageGUI {
public Label message;
public void changeMessage() {
message.setText("Hooray!");
}
}
Now we want to make it so that our FXML file is loaded up and linked to our object when it is instantiated, so we add a constructor:
public class MessageGUI {
public Stage stage;
public Label message;
public MessageGUI() throws IOException {
// Create a loader for our FXML file
FXMLLoader loader = new FXMLLoader(getClass().getResource("path/to/FXMLFile.fxml"));
// Tell the loader to link the FXML file to this object
loader.setController(this);
// Load our layout from our FXML file as a "Scene":
Scene scene = new Scene(loader.load());
// Create the stage (window) and add the layout to it in GUI thread.
GUI.runNow(() -> {
stage = new Stage();
stage.setScene(scene);
stage.setTitle("Example GUI");
});
}
public void changeMessage() {
message.setText("Hooray!");
}
}
We add the Stage
as another class property. In JavaFX terms a Stage
is what it calls a window, so essentially we are creating a window and adding the layout from our FXML file into it (and giving it the title "Example GUI"). When creating and setting the stage, we do it inside the GUI.runNow(() -> {...});
method call. This is to make sure that this part is run on the GUI thread (if we're not currently on it) since creating a stage/window MUST always be done on the GUI thread.
In general, to make something run on the GUI thread you use the following method:
Platform.runLater(() -> {
// Code goes here
});
However, this will return immediately and not wait for the code to finish on the GUI thread. Therefore JISA.GUI
provides the GUI.runNow(...);
method which does the same but will wait for the code to finish before returning.
For reference this is what the runNow(...)
method looks like in JISA.GUI
:
public static void runNow(Runnable toRun) {
if (Platform.isFxApplicationThread()) {
toRun.run();
} else {
Semaphore s = new Semaphore(0);
Platform.runLater(() -> {
toRun.run();
s.release();
});
try {
s.acquire();
} catch (InterruptedException ignored) { }
}
}
It is now worth adding the three methods show()
, hide()
and close()
to control our window like so:
public class MessageGUI {
public Stage stage;
public Label message;
public MessageGUI() throws IOException {
// Create a loader for our FXML file
FXMLLoader loader = new FXMLLoader(getClass().getResource("path/to/FXMLFile.fxml"));
// Tell the loader to link the FXML file to this object
loader.setController(this);
// Load our layout from our FXML file as a "Scene":
Scene scene = new Scene(loader.load());
// Create the stage (window) and add the layout to it in GUI thread.
GUI.runNow(() -> {
stage = new Stage();
stage.setScene(scene);
stage.setTitle("Example GUI");
});
}
public void changeMessage() {
message.setText("Hooray!");
}
public void show() {
GUI.runNow(() -> {
stage.show();
});
}
public void hide() {
GUI.runNow(() -> {
stage.hide();
});
}
public void close() {
GUI.runNow(() -> {
stage.close();
});
}
}
Similarly: showing, hiding and closing a window must also be done on the GUI thread, hence why they are all within GUI.runNow(...);
calls.
Alternatively you can simply extend the JFXWindow
class from JISA.GUI
like so:
public class MessageGUI extends JFXWindow {
public Label message;
public MessageGUI() throws IOException {
super("Window Title", "path/to/fxml/file.fxml");
}
public void changeMessage() {
message.setText("Hooray!");
}
}
This does everything we have just covered for you (including providing the show()
, hide()
and close()
methods), which obviously saves you time but is not very instructive for the purposes of understanding what's going on (hence why we did it the long way around first).
Now we can use our GUI:
public class Main extends GUI {
public static void main(String[] args) throws Exception {
MessageGUI gui = new MessageGUI();
gui.show();
}
}
Quick Summary
- Create FXML file, removing the
fx:controller="..."
line - Add elements to FXML file by dragging and dropping in "Scene Builder"
- Give important elements an
fx:id
- Set the function to call when "actioned" (clicked for a button, enter pressed for textfield etc) in the
onAction
field - Create controller class, extending
JFXWindow
public class MyWindow extends JFXWindow {
}
- Make its constructor call the parent constructor with the window title and path to fxml file
public class MyWindow extends JFXWindow {
public MyWindow() throws IOException {
super("Window Title", "path/to/fxml/file.fxml");
}
}
- Add all elements with an
fx:id
as public class properties:
public class MyWindow extends JFXWindow {
public Label myLabel;
public TextField myTextBox;
public Button myButton;
public MyWindow() throws IOException {
super("Window Title", "path/to/fxml/file.fxml");
}
}
- Add all methods referenced in
onAction
properties:
public class MyWindow extends JFXWindow {
public Label myLabel;
public TextField myTextBox;
public Button myButton;
public MyWindow() throws IOException {
super("Window Title", "path/to/fxml/file.fxml");
}
public void onButtonClick() {
// ...
}
public void onTextEntered() {
// ...
}
}
- Use your class in your code
public class Main extends GUI {
public static void main (String[] args) throws Exception {
MyWindow window = new MyWindow();
window.show();
}
}