12. JavaFX Lesson

JavaFX

JavaFX

Lesson Objectives

Motivate and introduce JavaFX for graphical user interfaces.

Part 1: Introduction
Logging into Odin

Remember to log in to Odin using ssh -XYC when working with graphical user interfaces.

Download the Starter Code

Execute the command below to download and extract the files:

sh -c "$(curl -fsSL https://cs1302book.com/_bundle/cs1302-javafx.sh)"
- downloading cs1302-javafx bundle...
- verifying integrity of downloaded files using sha256sum...
- extracting downloaded archive...
- removing intermediate files...
subdirectory cs1302-javafx successfully created

Change to the cs1302-javafx directory that was created using and look at the files that were bundled as part of the starter code using tree. You should see output similar to the following:

Listing 105 Directory structure of starter code
.
├── compile.sh
└── src
    └── cs1302
        └── app
            ├── ImageApp.java
            └── ImageDriver.java
Understanding the Starter Code

The starter code contains a basic template for building a JavaFX application.

In your groups, take a few minutes to explore the starter code and to answer the questions below on your exit tickets:

  • Do we need the ImageDriver class? Explain.

  • What is the parent class of ImageApp? What does our class inherit?

  • How many inherited methods are overridden in ImageApp?

  • How many abstract methods are overridden in ImageApp?

  • What is the Stage and how does it relate to the Scene?

Solutions
  • We don't have to have the ImageDriver class as JavaFX applications can be run directly without the need for an explicit main method. However, the ImageDriver allows us to catch any exceptions thrown by our application and handle them gracefully.

  • The parent class of ImageApp is Application. We inherit all methods and instance variables from Application.

  • We override three inherited methods.

  • We override one abstract method.

  • Stage: Top-level JavaFX container. Constructed by the JavaFX runtime. It is essentially the window we see. Scene: the container for all content in a scene graph. The main container is the root node. It is the content that is shown in the window.

Understanding the Application Lifecycle

Execute the compile script (compile.sh). You will see some output in the terminal and, hopefully, and empty window will pop up on the screen.

In your groups, compare the output of the program to what you see in the API Documentation for the (javafx.applicaton.Application) class.

Part 2: Scene Graph
Our Goal

Our goal is to create a user interface that matches the screenshot below.

The first step is to draw the scene graph and the containment hierarchy.

Image Loader User Interface

In your groups, draw the full containment hierarchy on your exit ticket.

You will need to use the following JavaFX classes:

Stage and Scene

Layout Panes

Visual Components

Solution

Overall Containment Hierarchy: Includes the stage and the scene.

@startmindmap
skinparam defaultFontName monospaced
<style>
   node {
      HorizontalAlignment center
   }
</style>

top to bottom direction

*:Stage
<size:10>this.stage</size>;
**:Scene
<size:10>this.scene</size>;
***:VBox
<size:10>this.vbox</size>;
****:HBox
<size:10>this.urlLayer</size>;
*****:TextField
<size:10>this.url</size>;
*****:Button
<size:10>this.button</size>;
****:ImageView
<size:10>this.iv</size>;
@endmindmap

Fig. 62 Containment Hierarchy: Image Loader

Everything below Scene must be a Node to be pictured in the scene graph. Each node corresponds to an object of some class under the javafx package. The diagram for the scene graph assumes that child nodes are added to their parents in a left-to-right order. For example, the HBox and ImageView objects are added to the collection of child nodes for the VBox object in that order.

Scene Graph: Only includes the components of the scene.

@startmindmap
skinparam defaultFontName monospaced
<style>
   node {
      HorizontalAlignment center
   }
</style>

top to bottom direction

*:VBox
<size:10>this.vbox</size>;
**:HBox
<size:10>this.urlLayer</size>;
***:TextField
<size:10>this.url</size>;
***:Button
<size:10>this.button</size>;
**:ImageView
<size:10>this.iv</size>;
@endmindmap

Fig. 63 Scene Graph: Image Loader

Live Demo: Writing the Code

We are now ready to write the code for our application.

Declare Instance Variables

The contents of the scene represent part of the state of your application. The variables that we use to refer to those objects should be instance variables of your class.

Solution
public class ImageApp extends Application {
    // Instance Variables
    VBox vbox;
    Scene scene;
    HBox urlLayer;
    Button button;
    TextField url;
    ImageView iv;

    // ...
} // ImageApp
Check Imports

Pull up the referenced bookmarks

The classes we need should already be imported. If you get "cannot find symbol" on a JavaFX class, double check your imports.

Write the Constructor

Remember: constructors are responsible for initializing instance variables!

Write the constructor to properly initialize all of the instance variables in your ImageApp class.

For the ImageView component, use the constructor that takes an Image reference. To create the image, you can use this url: "https://webwork.cs.uga.edu/~csci-1302/gui/default.png".

Solution

One possible solution:

private static String DEF_IMG = "https://webwork.cs.uga.edu/~csci-1302/gui/default.png";

public ImageApp() {
    System.out.println("2) Creating an instance of the ImageApp Application");

    vbox = new VBox();
    urlLayer = new HBox();
    button = new Button("Load");
    url = new TextField("https://");
    Image def = new Image(DEF_IMG);
    iv = new ImageView(def);
} // ImageApp
Overriding the init Method

We will use the init method to connect all of our components as seen in the scene graph (as seen below). Note the fact that we left out the stage and the scene. We will connect those later in start.

@startmindmap
skinparam defaultFontName monospaced
<style>
   node {
      HorizontalAlignment center
   }
</style>

top to bottom direction

*:VBox
<size:10>this.vbox</size>;
**:HBox
<size:10>this.urlLayer</size>;
**:ImageView
<size:10>this.iv</size>;
***:TextField
<size:10>this.url</size>;
***:Button
<size:10>this.button</size>;
@endmindmap

Fig. 64 Scene Graph: Image Loader

Solution

One possible solution:

@Override
public void init() {
    System.out.println("3) Executing the init method");

    urlLayer.getChildren().addAll(url, button);
    vbox.getChildren().addAll(urlLayer, iv);

} // init
Discussion

Where does the getChildren method come from? What does it return?

Look in the Button class API. Look at the parent classes.

Overriding the start Method

In start, we create a new Scene, set its root, and put the scene on the stage.

With your groups, look through the existing start method. Try to understand each line and how it accomplishes our goals.

You will not need to change the code in start.

Run the Code!

We will not use stop in this example, so we can leave it with a simple print statement.

With your groups, run the compile script. If there are any compilation errors, work together to resolve them. You should see a window that looks similar to our goal (below):

Image Loader User Interface

Ensure that your code passes checkstyle and then commit your changes using Git.

Congratulations on making a nice-looking JavaFX Application!

Part 3: Adding Functionality
Getting the Starter Code

Execute the command below to download and extract the files:

bundle1302 javafx-v2
- downloading cs1302-javafx-v2 bundle...
- verifying integrity of downloaded files using sha256sum...
- extracting downloaded archive...
- removing intermediate files...
subdirectory cs1302-javafx-v2 successfully created

Note: bundle1302

The command above is only available on Odin with the CSCI 1302 Shell Profile activated.

If you don't have the finished code from parts 1 and 2, you can download the code for parts 3 and 4 using the command above.

Discussion: Button

Look at the API documentation for Button. Near the top:

When a button is pressed and released a ActionEvent is sent. Your application can perform some action based on this event by implementing an EventHandler to process the ActionEvent.

  • When a user clicks a Button, an action event object is created;

  • When JavaFX notices that an event is created, it executes event handlers based on where the event came from.

  • Different objects may create the same kind of event objects (e.g., different Button objects).

  • To make all of this work nicely, you need to associate event handlers and objects.

  1. Identify the type of Event an object creates.

  2. Create an EventHandler that handles the event in whatever way you see fit.

  3. Associate the event handler with the object.

Creating an EventHandler
  1. In the init method of your ImageApp class, declare a variable of type EventHandler<ActionEvent> called loadHandler.

  2. Using an anonymous class, assign to loadHandler an implementation of EventHandler<ActionEvent> that prints out the text of the TextField to standard output (the terminal).

  3. Create another EventHandler<ActionEvent variable called loadHandlerLambda and assign it the same functionality, but this time, use a lambda expression.

  • Take special care that you import the correct ActionEvent class, as a quick Internet search may recommend the wrong one! Consult the referenced bookmarks to determine the import statements that are needed.

Important

Creating the EventHandler<ActionEvent> object does not connect that handler to the Button object in the scene graph. Before your button will function properly, you will need to set the handler to execute when the button is clicked.

Solution
  • Here are two different ways to associate the event handler with the button.

    // general method to add event handler
    loadButton.addEventHandler(ActionEvent.ACTION, loadHandler);
    
    // specific method to add just event handlers for action events
    loadButton.setOnActon(loadHandler);
    
Updating the Image

Once your app is able to print the text from the TextField to standard output, amend the code so that it also creates an Image object using the supplied URL, then sets the image property of the ImageView using the appropriate setter method.

Solution

One possible solution:

@Override
public void init() {
    System.out.println("3) Executing the init method");

    urlLayer.getChildren().addAll(url, button);
    vbox.getChildren().addAll(urlLayer, iv);

    // Think of the event handler as the code that runs when the button is pressed.

    EventHandler<ActionEvent> loadHandlerLambda = (ActionEvent event) -> {

        Image img = new Image(url.getText());
        iv.setImage(img);
    };

    button.setOnAction(loadHandlerLambda);
} // init
Part 4: Optional: Error Handling
  1. Input an invalid URL like "abcdefg" then catch the exception that's thrown. Read the stack trace that's printed for a hint on what to catch.

  2. Add exception handling for invalid URLs.

    One Approach

    Leverage the URL(String) constructor, which throws a checked exception under various scenarios…

    import java.io.IOException
    import java.net.URL;
    import javafx.scene.control.TextArea;
    import javafx.scene.control.Alert;
    import javafx.scene.control.Alert.AlertType;
    
    try {
        ...
        Image image = new Image(...);
        if (image.isError()) {
            throw new IOExeption(image.getException());
        } // if
        ...
    } catch (IOException|IllegalArgumentException e) {
        alertError(e);
    } finally {
        // undo anything we might have changed
        // in the try
    } // try
    
    /**
     * Show a modal error alert based on {@code cause}.
     * @param cause a {@link java.lang.Throwable Throwable} that caused the alert
     */
    public static void alertError(Throwable cause) {
        TextArea text = new TextArea(cause.toString());
        text.setEditable(false);
        Alert alert = new Alert(AlertType.ERROR);
        alert.getDialogPane().setContent(text);
        alert.setResizable(true);
        alert.showAndWait();
    } // alertError