Skip to content

Advanced GUI Creation

William Wood edited this page Jul 12, 2021 · 16 revisions

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. JISA is able to easily load GUI layouts created using JavaFX and bring them into the JISA gui ecosystem, allowing you to quickly display them in their own windows, or as elements added to other JISA gui container objects (like Grid, Tabs, Pages etc).

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:

  1. The layout of the GUI (FXML File)
  2. 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 - Java

Now that we have our layout and given our important elements their own IDs etc, we can write the code to control it all. To do this, we create a class which is referred to as the "controller". Doing this from scratch can be a bit fiddly, however JISA provides the JFXElement class that we can extend which makes things far simpler.

To begin, let's do that:

public class MessageGUI extends JFXElement {

}

At this point, your java IDE might be putting scary red lines under things. Do not fear! This is simply because we need to create a constructor for our class (ie the code that is run when we create an object of it). All we need to put in this constructor is a call to the constructor of JFXElement. Since JFXElement is the "next level up" in our class hierarchy (ie it's the one MessageGUI inherits from), Java refers to it as the "super" class of MessageGUI. Therefore, in code, it is referred to as super. So to call its constructor:

public class MessageGUI extends JFXElement {

    public MessageGUI() throws IOException {
        super("Window Title", "path/to/fxml/file.fxml");
    }

}

As we can see the JFXElement constructor takes two arguments, the first being the title we want to show in the title-bar of our window and the second being the (relative) path to our FXML file. When called, the JFXElement constructor reads the specified FXML file and builds the GUI layout from it, then puts it in a window and links the whole thing to this MessageGUI object. You may also have noticed that our constructor can throw an IOException. This is because we're having to open a file (i.e. the FXML file) which could throw an IOException if you provide it with an invalid path. If you know your FXML file is definitely there you can isntead pass it a URL resource by using your class' getResource method like so:

public class MessageGUI extends JFXElement {

    public MessageGUI() {
        super("Window Title", MessageGUI.class.getResource("path/to/fxml/file.fxml"));
    }

}

Next up, we want to get a handle on the elements that we gave special fx:id values. To do this, simply give the class public properties of the same type and name as the element. In our case, we had a Label called message, so we would put:

public class MessageGUI extends JFXElement {

    public Label message;

    public MessageGUI() throws IOException {
        super("Window Title", "path/to/fxml/file.fxml");
    }

}

Make sure that when you import the relevant classes for each GUI element that you import the correct ones (i.e. the JavaFX ones and not the Swing or AWT classes with the same names)

How this works is that when we call super(...) (ie the JFXElement constructor), any elements with an fx:id are automatically assigned to a matching class property (if it exists). So, in our case, it finds a Label with fx:id="message", then finds a class property of type Label called message and thus assigns the Label defined in our FXML file to the class property. Clever, innit?

Finally, we just need to create methods to match those we set as the onActions properties of things like buttons etc. In our example we gave the button an onAction value of changeMessage, so we need to create a matching method:

public class MessageGUI extends JFXElement {

    public Label message;

    public MessageGUI() throws IOException {
        super("Window Title", "path/to/fxml/file.fxml");
    }

    public void changeMessage() {
        message.setText("Hooray!");
    }

}

Now, when the button is clicked, the changeMessage() method will be run causing the text displayed in our Label to change to say "Hooray!".

Our controller is now complete and we can use our new GUI window in our existing code by instantiating an object of it and calling show() like so:

MessageGUI window = new MessageGUI();
window.show();

Since we extended the JFXElement class, we inherit some useful methods:

// Makes the window show/open
window.show();

// Makes the window close but keep running
window.hide();

// Makes the window close and not keep running
window.close();

// Makes the window maximised (true) 
// or un-maximised (false)
window.setMaximised(true/false); // default: false

// Makes the programme terminate when 
// this window is closed (true) or not (false)
window.setExitOnClose(true/false); // default: false

Controller Class - Kotlin

Now that we have our layout and given our important elements their own IDs etc, we can write the code to control it all. To do this, we create a class which is referred to as the "controller". Doing this from scratch can be a bit fiddly, however JISA provides the JFXElement class that we can extend which makes things far simpler.

To begin, let's do that:

class MessageGUI : JFXElement {

}

At this point, your java IDE might be putting scary red lines under things. Do not fear! This is simply because we need to give some parameters to the JFXElement constructor. These are the title to use for the window as well as the path to the FXML file to use:

class MessageGUI : JFXElement("Window Title", "path/to/fxml/file.fxml") {

}

When called, the JFXElement constructor reads the specified FXML file and builds the GUI layout from it, then puts it in a window and links the whole thing to this MessageGUI object. If you want the user to specify the title of the window for each MeassageGUI object they create, rather than them all having the same title, then you can just add it as a constructor parameter for MessageGUI instead like so:

class MessageGUI(title: String) : JFXElement(title, "path/to/fxml/file.fxml") {

}

Next up, we want to get a handle on the elements that we gave special fx:id values. To do this, simply give the class lateinit variables of the same type and name as the element. In our case, we had a Label called message, so we would put:

class MessageGUI : JFXElement("Window Title", "path/to/fxml/file.fxml") {

    lateinit var message: Label

}

Make sure that when you import the relevant classes for each GUI element that you import the correct ones (i.e. the JavaFX ones and not the Swing or AWT classes with the same names)

How this works is that when we call JFXElement(...) in the class definition (ie the JFXElement constructor), any elements with an fx:id are automatically assigned to a matching class property (if it exists). So, in our case, it finds a Label with fx:id="message", then finds a class property of type Label called message and thus assigns the Label defined in our FXML file to the class property. Clever, innit?

Finally, we just need to create methods to match those we set as the onActions properties of things like buttons etc. In our example we gave the button an onAction value of changeMessage, so we need to create a matching method:

class MessageGUI : JFXElement("Window Title", "path/to/fxml/file.fxml") {

    lateinit var message: Label
    
    fun changeMessage() {
        message.text = "Hooray!"
    }

}

Now, when the button is clicked, the changeMessage() method will be run causing the text displayed in our Label to change to say "Hooray!".

Our controller is now complete and we can use our new GUI window in our existing code by instantiating an object of it and calling show() like so:

val window = MessageGUI()
window.show()

Since we extended the JFXElement class, we inherit some useful methods:

// Makes the window show/open
window.show()

// Makes the window close but keep running
window.hide()

// Makes the window close and not keep running
window.close()

// Makes the window maximised (true) 
// or un-maximised (false)
window.setMaximised(true/false) // default: false

// Makes the programme terminate when 
// this window is closed (true) or not (false)
window.setExitOnClose(true/false) // default: false
Clone this wiki locally