Laboratory Training 4

Development of GUI Applications

1 Training Tasks

1.1 Individual Task

It is necessary to implement in Java language with the help of JavaFX tools the graphical user interface application, in which the data processing of individual tasks of previous laboratory trainings is carried out. The main window should contain a menu in which you need to implement the following functions:

  • creating a new data set;
  • loading data from an XML document for editing;
  • storing the changed data in an XML document;
  • search according to the criteria specified in the Laboratory training # 3 of the course "Fundamentals of Java Programming";
  • sorting according to the criteria specified in the Laboratory training # 4 of the course "Fundamentals of Java Programming";
  • showing "About" window with short data about the program and the author.

In the left part of the window, you should arrange the boxes for entering the scalar data, the display area for the search results, and the buttons that provide the basic functions of the program. In the middle of the window, you should place a table to display and edit the data.

1.2 Using ObservableList

Create a list (ObservableList) of real numbers of type Double. Change the initial order of numbers so that positive numbers are placed first (without changing their relative order), and then negative numbers in the opposite order. Each change in the state of the list should result in the output of the list items to the console window.

1.3 Mini Calculator

Create a graphical user interface application, which, after entering numbers in two boxes of TextField type, performs one of four arithmetic operations (depending on the radio button selected). The result should be displayed in a different text field.

1.4 Dictionary (Advanced Task)

Develop a GUI application for viewing the words of a small English-Ukrainian (English-Russian, etc.) dictionary. Implement search functions for words, adding new words. Use Map to store data.

2 Instructions

2.1 Using Java to Create GUI Applications

2.1.1 Overview

User interface is a set of means by which people interact with computers. We'll speak about software means of user interface.

A command line interface is a method of interacting with software using a command line interpreter. The result of command execution is displayed on the screen in so-called text mode or in special console window. Applications which support this kind of user interface are called console applications. These applications do not react system messages and do not send messages to other applications.

Graphical user interface (GUI) allows the user to interact with the computer using graphical controls (windows, icons, menus, buttons, lists, etc.) and technical positioning devices, such as mouse device. In contrast to command line interface, GUI provides user random access to screen objects. Applications which implement this kind of interaction are called GUI Applications.

The implementation of GUI is based on so-called event handling mechanism. The whole GUI program consists of initialization part, which contains visual components registration, and main loop of events obtaining and events processing. Events such as moving mouse, pushing mouse buttons, pressing keys, etc., are transferred to appropriate controls by invocation of their functions called event handlers.

Graphical user interface development tools are an integral part of Java technology since the very beginning of Java. The first library that provided tools for creating graphical interface programs was the Abstract Window Toolkit (AWT) library. AWT is part of the Java Foundation Classes (JFC), a standard API for implementing a graphical interface for a Java application. In the early years of its existence, the Java AWT library was used primarily to create applets.

The main disadvantage of the AWT library is the focus on graphical dialog components that provide specific operating systems and graphics environments. This leads, on the one hand, to certain problems with the deployment of the program on different software platforms, on the other hand, restricts the expressive means of the application, since it is necessary to focus only on those visual components that are present on all platforms. Such visual components are called "high weight". This and other AWT drawbacks are fixed in the Swing library. The Swing library also provides some additional visual components, such as a bookmarks bar, drop down lists, tables, trees, and more.

Now standard Java graphical user interface development tools have AWT and Swing libraries, as well as the JavaFX platform, which is an alternative to Swing. In addition, various developers provide alternative non-standard libraries, such as Qt Jambi, Standard Window Toolkit (SWT), XML Window Toolkit (XWT). The last two libraries, along with AWT and Swing, are supported by Eclipse.

2.1.2 Creating and Using Applets

Applets are software components written in Java that are stored on a web server, loaded onto a client computer, and executed using the Java virtual machine built into the client's web browser.

All you need to perform the applet is contained in the <applet> tag in the text of the HTML file. The applet is usually responsible for a certain rectangular area in the browser window. The coordinates of this area can also be specified in the <applet> tag. For example:

<applet codebase = "." code = "test.Applet1.class" width = 400 height = 300>
</applet>

The codebase attribute specifies the location of the class that implements the applet (in the example given, the current folder). The code attribute is the class name. The width and height attributes specify the size of the applet window.

The java.applet.Applet (Java 1) and javax.swing.JApplet (Java 2) classes are used as base classes to implement applets. The life cycle is determined by the following methods of the Applet class:

  • public void init() is invoked by the browser immediately after loading the applet before the first call of the start() method; this method needs to be redefined almost always, if an applet needs at least some kind of initialization;
  • public void start() is called by the browser every time the page is visited;
  • public void stop() is invoked by the browser during the deactivation of the page;
  • public void destroy() always invoked when exiting the browser and rebooting the applet.

For security reasons, applets are subject to certain restrictions:

  • applets downloaded over the network are prohibited from reading and writing files from the local file system;
  • applets should not perform network connections to all hosts, except from which the applet was received;
  • applets are not allowed to run programs on the client system;
  • applets are not allowed to load new libraries and call programs external to the Java machine.

The applet can be viewed using the program appletviewer.exe, which is part of the JDK. Almost any Java application dialog can be implemented so that it can work both as a program and as an applet. However, the applet does not require the main() function.

Problems with using applets are related to the need to ensure the availability of the appropriate version of Java, which is supported by the client's Web browser.

2.1.3 Using the javax.swing Library

The javax.swing library (Swing library) provides a set of standard classes which can be used for development of graphical user interface (GUI) applications. The Swing library was extended previously developed AWT (Abstract Window Toolkit) library. Swing provides an extended set of visual controls and improved event model. In contrast to AWT classes, class names of visual controls defined in Swing library start with J letter (for example, JButton, JPanel, etc.).

Unlike the AWT components, Swing components are lightweight. This means that the Swing components use the Java tools to display graphical user interface elements on the window surface without using the components of the operating system.

Like most GUI libraries, the javax.swing library supports the concept of the main application window. This main window is created as an object of the JFrame class, or derived from it. Next, the visual components are added to the main window - labels (JLabel), buttons (JButton), input strings (JTextField), etc.

Development of the simplest Swing application starts with creation of a new class with main() function. It is necessary to add an import statement:

import javax.swing.*;

We can create a new frame in main() function. The constructor's argument is used for setting of window title:

. . .

public class HelloWorldSwing {
    public static void main(String[] args) {
       JFrame frame = new JFrame("Hello");
       . . .
    }
}

Next, we add a new label to the component that is responsible for the contents of the window. A new JLabel object is created:

frame.getContentPane().add(new JLabel("Hello world!"));

The pack() method adjusts size of main window. The call of setVisible(true) function causes appearance of this window on the screen. Here is the full text of the program:

package ua.inf.iwanoff.java.advanced.fourth;

import javax.swing.*;

public class HelloWorldSwing {
    public static void main(String[] args) {
        JFrame frame = new JFrame("Hello");
        frame.getContentPane().add(new JLabel("Hello world!"));
        frame.pack();
        frame.setVisible(true);
    }
}

Next, you can add other visual components and event handlers to the program.

The visual components of the Swing library are inherited from the javax.swing.JComponent class, the successor of the java.awt.Container class. In turn, this class is the successor of java.awt.Component. The java.awt.Component class is the base class that defines the display on the screen and the behavior of each interface element when interacting with the user. The class methods responsible for managing events allow you to specify the size, color, font, and other attributes of controls. For example, the setBackground(Color) method sets the color of the component background, setFont(Font) sets the font (java.awt.Color and java.awt.Font are classes that allow you to set a certain color and font). The JComponent class extends the capabilities of the base classes by supporting the Look & Feel, using hotkeys, tooltips, and some other features. The Swing library allows you to change visual style of user interface (Look & Feel) according to style conventions of particular operating environment. A special UIManager class is used to control the style. By default, the cross-platform style ("metal") is adopted. In order to set the style adopted on a particular operating system, the following code should be added to the window initialization:

try {
    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
}
catch (Exception ex){
    ex.printStackTrace();
}

The use of visual components is based on the application of Java Bean.

2.2 Overview of JavaFX Platform

2.2.1 Main Concepts

From the beginning, JavaFX tools were created as a software platform for creating rich internet applications, defining the architecture, framework and application development style. Since JavaFX provides a large number of interfaces and classes for developing graphical user interface applications, JavaFX is actually a modern alternative to the javax.swing library. The first version of the platform (JavaFX 1.0) came out in 2008 and included a special scripting language JavaFX Script for describing the graphical interface. In 2011, JavaFX 2.0 was released under the leadership of Oracle. The developers of this version have abandoned a special scripting language in favor of Java. The version of JavaFX 8, which was released in 2014, is developed in accordance with the capabilities and style of Java 8, provides opportunities to work with 3D graphics, and also offers new visual components. The version number corresponds to Java 8, so versions 3, 4, 5, 6 and 7 are missing.

The main features that distinguish JavaFX from previous graphical user interface libraries are the following:

  • built-in support for MVC design pattern (Model-View-Controller)
  • ability to declare a visual description of the components (FXML language)
  • modern style of visual components
  • support for enhanced user interaction with the application
  • the ability to use css styles to stylize elements of the user interface
  • the ability to use 3D graphics
  • simplified application deployment model

There are also a number of additional features related to graphics, text, interaction with previously created libraries, and more.

The JavaFX platform can be considered an alternative to previous GUI libraries, which is intended to completely replace them later.

2.2.2 Creating the Simplest Application. JavaFX Application Structure

The simplest application for the graphical user interface that uses the JavaFX library can be created by including all the necessary components directly in the Java code. For example, the following class derived from javafx.application.Application allows you to create a window with a button in the middle:

package ua.inf.iwanoff.java.advanced.fourth;

import javafx.application.Application;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.layout.FlowPane;
import javafx.geometry.Pos;
import javafx.scene.control.Button;

public  class Main extends Application {

    @Override
    public  void start(Stage primaryStage) throws Exception {
        // Set the window title:
        primaryStage.setTitle("First Java FX Application");

        // Create a root container and set the centering of the child elements
        FlowPane rootNode = new FlowPane();
        rootNode.setAlignment(Pos.CENTER);

        // Create a scene and set it up in the stage
        Scene scene = new Scene(rootNode, 300, 200);
        primaryStage.setScene(scene);

        // Create a button, define the action when it is pressed
        // and insert the button in the root container:
        Button button = new Button("Press me");
        button.setOnAction(event -> button.setText("Do not press me"));
        rootNode.getChildren().add(button);

        // Show the window:
        primaryStage.show();
    }

    public  static  void main(String[] args) {
        launch(args);
    }
}

In the example above, the function that handles the event associated with the button click is implemented using the lambda expression.

Resource files (e.g. bitmap images) can be placed in a separate directory of the project.

JavaFX components form a hierarchy of objects. The application must contain at least one Stage type object. Stage fields determine the properties of the window that is its owner: the style of the window, its type (for example, modal / non-modal), the title, etc. Stage type object is the top level container containing the scene (Scene component). The scene is a container for other visual components.

2.2.3 Using JavaFX Tools in IntelliJ IDEA

The IntelliJ IDEA IDE provides a special plugin that allows you to create JavaFX projects. You need to do some preparatory steps first:

  • make sure the JDK version is not less than 11
  • check whether the required JavaFX plugin is turned on; this can be done in the following way:
    • in the Settings window (File | Settings.. menu command) of IntelliJ IDEA select the Plugins position
    • find plugin JavaFX in the list of plugins; if it is disabled, turn it on.

Now you can create a new JavaFX project by selecting the JavaFX project wizard on the left side of the window. Specify the name of the project, for example, FirstFX. You can change the parameters of the Maven project (Group and Artifact). On the next page of the wizard, you can add third-party libraries related to JavaFX. For the first application, adding these libraries is not advisable.

On the next page of the wizard you should enter the name of the project, for example, FirstFX. A program that shows a window with the title Hello! and the button inside is automatically created.

The project contains a package with two files:

  • Main.java contains the Main class, derived from javafx.application.Application. This class contains the main() function.
  • HelloController.java contains an empty class, to which you'll add event handlers.

In addition, in the resources branch in the same package, the hello-view.fxml file is located, which contains a description of the user interface elements. The file contains a reference to the HelloController class.

After starting program and pressing button the text "Welcome to JavaFX Application!" will appear.

Sometimes it is necessary to add JavaFX facilities to a project that was created earlier. In addition to directly adding code that uses JavaFX components, you need to add dependencies to the pom.xml file:

        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-controls</artifactId>
            <version>21</version>
        </dependency>
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-fxml</artifactId>
            <version>21</version>
        </dependency>
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-graphics </artifactId>
            <version>21</version>
            <classifier>win</classifier>
        </dependency>

If a file with markup (*.fxml) is used in the project, you should create a folder for it in the project directory src\main\resources\ and then recreate the package hierarchy, including the package with the application class. The markup file is written into the folder created in this way.

But even after successful compiling, the application will require a runtime library. An error message will be displayed on the console:

Error: JavaFX runtime components are missing, and are required to run this application

The necessary runtime components can be obtained by downloading and manually deploying the JavaFX SDK on your computer. The necessary tools for your operating system can be downloaded from https://gluonhq.com/products/javafx/. The downloaded archive should be opened in any folder on the computer, for example, c:\javafx-sdk-21.0.3 (відповідна тека є в архіві). For the application to work, a lib folder is required, which is inside the specified, for example, c:\javafx-sdk-21.0.3\lib.

Now to run the application in the IntelliJ IDEA environment, you can create a new runtime configuration via the main menu: Run | Edit Configurations, then in the Run/Debug Configurations window we add a new configuration (Add New Configuration), by clicking the "plus". Next, select Application and configure the configuration options in the window: Name and Main class. The most important thing is to specify additional options of the Java virtual machine. From the Modify options list, select Add VM options. An additional input line appears. For the previously specified location of the JavaFX SDK, you should enter the following text:

--module-path "c:\javafx-sdk-21.0.3\lib" --add-modules javafx.controls,javafx.fxml

Next, you should save the created configuration and use it to run the program.

2.3 The Theoretical Basis of Creating JavaFX Applications

2.3.1 Use of JavaFX Properties. Observable

In the broadest sense, a property is an attribute of its data, while changing the value of which (and sometimes during reading) some actions can automatically be performed. Some programming languages, such as Visual Basic, Object Pascal, C#, etc., maintain properties at the syntax level. In these languages, the syntax of working with properties coincides with the syntax of work with class fields.

In Java technologies, properties on the logical level are presented in Java Beans, which syntax has been described earlier. By expanding the Java Beans model, JavaFX provides a special generic Property interface and a large number of abstract classes such as BooleanProperty, DoubleProperty, FloatProperty, IntegerProperty, StringProperty, etc. There are also default implementations of these classes, such as, for example, SimpleBooleanProperty, SimpleDoubleProperty, SimpleFloatProperty, SimpleIntegerProperty, SimpleStringProperty etc.

Working with properties requires connecting JavaFX tools. The corresponding dependency can be added to the pom.xml file:

        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-controls</artifactId>
            <version>17.0.2</version>
        </dependency>

In order to define property in some class, it is necessary to define a private field of the corresponding type, as well as create one setter and two getters:

package ua.inf.iwanoff.java.advanced.fourth;

import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;

public class LiveNumber {
    private IntegerProperty number = new SimpleIntegerProperty();

    public void setNumber(int number) {
        this.number.set(number);
    }

    public int getNumber() {
        return number.get();
    }

    public IntegerProperty numberProperty() {
        return number;
    }

}

Note: IntelliJ IDEA automatically generates necessary setters and getters for standard JavaFX properties (Code | Generate | Getter and Setter).

Thus, you can create classes with JavaFX properties instead of standard data types for modeling entities of the real world. But most important is that use of Property allows you to receive messages about changing the property value. In other words, JavaFX properties are implemented by built-in support for Observer design pattern.

Observable is the entity that wraps the contents and allows you to observe content in terms of loss of relevance. The relevant base interface is defined in the javafx.beans package. This interface declares two methods:

public interface Observable {
    void addListener(InvalidationListener listener);
    void removeListener(InvalidationListener listener);
}

Classes that implement the Property interface also implement an Observable interface. This allows you to track the value changes due to the events processing mechanism.

package ua.inf.iwanoff.java.advanced.fourth;

import javafx.beans.value.ObservableValue;

public class LiveNumberDemo {
    public static void main(String[] args) {
        LiveNumber liveNumber = new LiveNumber();
        liveNumber.numberProperty().addListener(LiveNumberDemo::listen);
        liveNumber.setNumber(100);
        liveNumber.setNumber(200);
    }

    private static void listen(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
        System.out.printf("Old: %d  \tNew: %d\n", oldValue.intValue(), newValue.intValue());
    }
}

The given mechanism can be used in various programs, including not associated with JavaFX.

2.3.2 Use of Binding

Binding in JavaFX is built on the capabilities of properties and allows you to update objects synchronously with data of objects associated with them. For example, you can change the size of the visual components, depending on the size of other components, automatically update data in tables, etc.

There are two approaches to implementing the binding mechanism: low-level and high-level. Low-level binding is more universal. In the example below, the sum variable automatically receives the value of the sum of two integers:

package ua.inf.iwanoff.java.advanced.fourth;

import javafx.beans.binding.IntegerBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;

public class LowLevelBinding {
    public static void main(String[] args) {
        final IntegerProperty m = new SimpleIntegerProperty(1);
        final IntegerProperty n = new SimpleIntegerProperty(2);
        IntegerBinding sum = new IntegerBinding() {
            {
                super.bind(m, n);
            }
            @Override
            protected int computeValue() {
                return (m.get() + n.get());
            }
        };
        System.out.println(sum.get());
        n.set(3);
        System.out.println(sum.get());
    }
}

For most simple calculations, a high-level approach is used. There are static functions of the Bindings class, which provide the necessary binding mechanism. The previous example can be implemented using high-level binding mechanisms:

package ua.inf.iwanoff.java.advanced.fourth;

import javafx.beans.binding.Bindings;
import javafx.beans.binding.NumberBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;

public class HighLevelBinding {
    public static void main(String[] args) {
        final IntegerProperty m = new SimpleIntegerProperty(1);
        final IntegerProperty n = new SimpleIntegerProperty(2);
        NumberBinding sum = Bindings.add(n, m);
        System.out.println(sum.getValue());
        n.set(3);
        System.out.println(sum.getValue());
    }
}

There are also various options for functions subtract(), multiply(), divide(), equal(), greaterThan(), lessThan(), min(), max() and many others. In addition, the corresponding non-static methods are declared in the NumberExpressionBase class, which is indirect base class for IntegerProperty, DoubleProperty and other wrappers for numerical values. To define the sequence of actions, you can use superposition of functions, such as Bindings.add(a.multiply(b), c.multiply(d), etc.

Binding capabilities are often used in graphical user interface programs for synchronous changes in multiple objects or to support the synchronization of the model data.

2.3.3 Working with JavaFX Collections

In addition to properties and binding, JavaFX provides special collections, the main difference in which is the mechanism of automatic notification of the change of elements' state. The generic javafx.collections.ObservableList interface, which extends interfaces java.util.List and javafx.beans.Observable, combines functions of working with standard lists with opportunities for tracking information changes in lists. The response capabilities in the list state are shown in the following example:

package ua.inf.iwanoff.java.advanced.fourth;

import javafx.collections.*;

public class ObservableListDemo {
    public static void main(String[] args) {
        final ObservableList<Integer> list = FXCollections.observableArrayList(1, 2, 4, 8);
        list.addListener(new ListChangeListener() {
            @Override
            public void onChanged(ListChangeListener.Change change) {
                while (change.next()) {
                    if (change.wasAdded()) {
                        System.out.println("Item was added " + list);
                        return;
                    }
                    if (change.wasRemoved()) {
                        System.out.println("Item was removed " + list);
                        return;
                    }
                    System.out.println("Detected changes " + list);
                }
            }
        });
        list.add(16);   // Item was added [1, 2, 4, 8, 16]
        list.remove(0); // Item was removed [2, 4, 8, 16]
    }
}

As can be seen from the example, it is necessary to create a loop to analyze the event.

To create an object that implements an ObservableList interface, you can also use the FXCollections.observableList() static function with the "traditional" list as parameter:

List<String> list = new ArrayList<String>();
ObservableList<String> observableList = FXCollections.observableList(list);

In addition to the ObservableList interface, ObservableSet, ObservableMap, ObservableArray interfaces are also provided, as well as wrappers for conventional arrays of float and int ( ObservableFloatArray and ObservableIntegerArray).

2.4 Working with JavaFX Visual Components

2.4.1 Overview

The JavaFX graphical interface components are represented by classes that are indirectly derived from javafx.scene.Node. This class implements the basic component behavior that includes standard transformations: translation, rotation, scaling and shearing, as well as styling support. The derived class javafx.scene.Parent supports the mechanism of creating a hierarchy of objects and provides a list of subsidiary components in the form of a list (ObservableList). The javafx.scene.layout.Region class is basic for all JavaFX visual components. The most used derivatives of classes are javafx.scene.layout.Pane and Control. The Pane class is basic for all types of panels. The Control class is basic for visual components that are located inside the panels.

One of the approaches to the design of a graphical user interface is to create the objects of the required types and addition of them in the appropriate software containers. This approach provokes the creation of very large classes in which the creation of components, processing of events and calculation will be mixed. An alternative approach to designing a graphical user interface is to use FXML.

2.4.2 Using FXML to Markup GUI Elements

Modern concepts of designing a graphical user interface provide a declarative way of defining the contents of windows (frames, activities) and the properties of visual components. The most popular approach is the use of XML, which provides an adequate description of the hierarchy of visual objects through the mechanism of nested tags and the definition of properties of components through the use of attributes. Different languages built on XML are used in Android, .NET (WPF), and more.

In addition to the advantages of a declarative markup, the use of FXML is of great importance in terms of performing the Model-View-Controller design pattern. The main idea of this pattern is to separate the data structures of the subject area and the algorithms of their processing (model) from the means of interaction with the user (view) with the removal of dependencies between these parts. The controller is a special module (class) that provides a connection between the model and view through the implementation of events that arise when the user interacts with the program and calling the functions of the model. JavaFX Application Architecture, which relates to FXML, includes views through FXML and style files (*.css) and a controller class, the code of which can be generated automatically. The controller contains references to model classes that represent the entities of the domain.

In addition, the declarative language for describing the appearance and controls allows use designers for which XML-like declarative language is more acceptable than programming languages.

The first version of JavaFX included a separate scripting language, the so-called JavaFX Script, which allowed declaratively describe the components of the user interface. Starting with the second version of JavaFX, the authors of the platform abandoned JavaFX Script and added to the specification an XML-based user interface description language called FXML. The use of FXML is not the only, but recommended approach.

The use of FXML is automatically expected when creating a new JavaFX project in IntelliJ IDEA.

2.4.3 Layout in JavaFX

JavaFX provides mechanisms for composing visual components (nodes), in many ways similar to the corresponding mechanisms of the javax.swing library, but unlike swing, layout elements are nodes in the user interface element tree. There are several types of standard containers (panels) that differ in the layout rules and formatting of child visual components. In the previous examples, the BorderPane and FlowPane containers have already been used. In general, standard containers can include the following:

  • Pane: the simplest panel with absolute positioning;
  • BorderPane works similarly to the BorderLayout of the Swing library; you can add five components: top, bottom, left, right, and center; in the latter case, the component tries to occupy the entire free space;
  • FlowPane automatically adds items to the horizontal (vertical) row with the continuation in the next row (column);
  • HBox lays nodes in a horizontal row;
  • VBox lays nodes in a vertical row;
  • AnchorPane allows attachment of nodes to different sides of the container, or to its center, determining the appropriate distances;
  • TilePane arranges elements in a grid of the same size;
  • StackPane places each new node on top of the previous node; allows you to create compound forms that support the dynamic change of its content;
  • GridPane allows you to arrange nodes in a dynamic grid, which allows you to combine adjacent cells; in its capabilities it is close to GridBagLayout of javax.swing library.

All panels support the padding property, the GridPane, HBox and VBox panels also support the spacing property (the gap between the child nodes).

2.4.4 Using Controls. Processing of Events

Classes of visual elements that are used in JavaFX are derived from the javafx.scene.layout.Region class, which is derived from the javafx.scene.Parent abstract class, whose base class is javafx.scene.Node. The properties of this class determine the size and location of components on the container, the ability to create a hierarchy of objects.

Most visual controls based on their names and basic functionality are similar to the corresponding components of the javax.swing library. Unlike Swing, the properties of controls can be defined not only in the code, but also in the FXML document.

For example, the simplest control is the button for which the javafx.scene.control.Button class should be applied. The title of the button can be defined in the constructor, or set by using the setText() method. Properties can be defined in the function code or in the FXML document.

The most commonly used elements of the user interface defined in the javafx.scene.control package are the following:

  • panels: ToolBar, Accordion, SplitPane, TabPane, ScrollPane, TitledPane, MenuBar
  • non-editable text: Label
  • buttons: Button, MenuButton, SplitMenuButton, ToggleButton,
  • text controls: TextField, TextArea, PasswordField
  • switches: CheckBox, RadioButton (used together with the ToggleGroup)
  • lists: ChoiceBox, ComboBox, ListView
  • menus: Menu, ContextMenu, MenuItem
  • dialog windows: ColorPicker, DatePicker,
  • tables: TableView, TreeTableView
  • indicators: ProgressBar, ProgressIndicator
  • an HTML like label: Hyperlink
  • a horizontal or vertical separator line: Separator
  • a continuous or discrete range of valid numeric choices: Slider
  • a view on to a tree root: TreeView.

Additional elements can be found in packages javafx.scene.chart, javafx.scene.image, javafx.scene.media, etc. The javafx.scene.shape package provides the drawing of geometric shapes.

Like almost all graphical user interface libraries, JavaFX supports both the main and the context menus. The main menu is located inside the javafx.scene.control.MenuBar component. To create separate submenus, the javafx.scene.control.Menu class is used. You can create menu items using the javafx.scene.control.MenuItem class.

All Java SE Graphical User Interface Libraries support similar event handling mechanisms. All JavaFX event classes come from the java.util.EventObject class. The most common is the derived ActionEvent class that represents an event associated with the main use of the control (for example, by pressing a button). In order to process an event, you first have to register the handler by calling the setOnAction() method. The parameter of this method is the class object that implements the javafx.event.EventHandler<T extends Event> interface. The only method to be implemented is void handle(T eventObj). Since this interface is functional, lambda expressions are used in most cases to create nameless classes that implement it.

Sometimes it is advisable to get the source of an event (the object that initialized the event) for more accurate processing of the event. This object can be obtained by calling the getSource(), defined in java.util.EventObject class.

The best way to learn how to work with visual components and events is to analyze examples. Example 3.2 shows the work with text fields and a button, in example 3.3 demonstrates the work with the RadioButton buttons, Example 3.5 also shows working with the menu and TextArea.

2.4.5 Child and Dialog Windows

Like other libraries for creating graphical user interface applications, JavaFX provides tools for creating child windows: windows that appear after performing certain actions in the main application window. Typically, these windows contain controls for dialogue with the user. Such windows are called dialog windows (dialog boxes).

Dialog windows (dialog boxes) are special types of windows that allow the user to enter a limited number of data, contain a limited number of controls, and allow the user to choose options for action, or inform the user. Dialog boxes are usually displayed when the program needs an answer for further work. Unlike regular windows, most dialog boxes cannot be expanded or collapsed, as well as resized. A special type of dialog boxes includes so-called modal dialogs. They do not allow you to continue working with the application until the modal window is closed. Using modal dialogs, for the most part, notify the user about some intermediate results, show warnings, error messages, enter separate data rows, etc.

To create a child window, you need to create a separate Stage type object. You should call the show() method for this object. If a new child window is a modal dialog, the showAndWait() method should be called. This function shows a new window and allows you to return to the previous one only after closing the new window.

JavaFX 8 (starting with JDK version 8.4) provides javafx.scene.control.Alert class that allows you to create standard dialogs. The windows options are defined in the constructor using the javafx.scene.control.Alert.AlertType enumeration. The following types of windows are defined:

  • AlertType.INFORMATION - standard information box with message;
  • AlertType.WARNING - warning box with corresponding icon;
  • AlertType.ERROR - error box with corresponding icon;
  • AlertType.CONFIRMATION - question box with OK and Cancel buttons (you can also add other buttons).

There are also special classes for text input (javafx.scene.control.TextInputDialog), and javafx.scene.control.ChoiceDialog<T>, which allows the user to select a T-type element from the list. All of these classes are derived from the generic class javafx.scene.control.Dialog<T>.

Modal dialogs are often used to select files. The javafx.stage.FileChooser class allows you to select files for reading or writing. For example, if the current stage is called stage, so you can create a file dialog to open and receive an object of type java.io.File:

FileChooser chooser = new FileChooser();
File file;
// Show file selection window and check whether the user has confirmed his (her) choice:
if ((file = chooser.showOpenDialog(stage)) != null) { 
    // Read from file
}

The parameter both of showOpenDialog() and showSaveDialog() functions is a reference to a window in the center of which the file dialog is located. If null is specified, the dialog box appears centered on the screen. You can use Example 3.4 to find out how to use the FileChooser class and events.

2.5 Working with Tabular Data in JavaFX

Work with tabular data is carried out using the component TableView. The main work of this component is a reflection of the properties of objects that are stored in the list of the ObservableList type. Using the TableView can be seen by example. Suppose a City class has been created:

package ua.inf.iwanoff.java.advanced.fourth;

public class City {
    private String name;
    private int population;

    public City(String name, int population) {
        this.name = name;
        this.population = population;
    }

    public String getName() {
        return name;
    }

    public int getPopulation() {
        return population;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setPopulation(int population) {
        this.population = population;
    }

}

We need to create and fill out a list of cities and display this list in the main application window.

We create a new JavaFX application. In the Main class, we initialize several objects of the City type, create a table, add columns, bind them to the properties of the City class and add a table to the main window. The source code will be like that:

package ua.inf.iwanoff.java.advanced.fourth;
    
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.BorderPane;

public class Main extends Application {

    @Override
    public void start(Stage primaryStage) {
        // Fill in the list of cities:
        ObservableList<City> list = FXCollections.observableArrayList(
            new City("Kharkiv", 1_451_132), 
            new City("Poltava", 295_950),
            new City("Kyiv", 2_868_702)
        );
        
        try {
            primaryStage.setTitle("Cities");
            BorderPane root = new BorderPane();
            
            // Create a table and add columns to it:
            TableView<City> table = new TableView<>();
            table.setItems(list);
            table.getColumns().clear();
            // The columnName column is associated with the name property:
            TableColumn<City, String> columnName = new TableColumn<>("Name");
            columnName.setCellValueFactory(new PropertyValueFactory<>("name"));
            // The columnPopulation column is associated with the population property:
            TableColumn<City, Integer> columnPopulation = new TableColumn<>("Population");
            columnPopulation.setCellValueFactory(new PropertyValueFactory<>("population"));
            // Add columns to the table:            
            table.getColumns().add(columnName);
            table.getColumns().add(columnPopulation);
            // Add a table to the panel center:
            root.setCenter(table);
            
            Scene scene = new Scene(root, 300, 200);
            primaryStage.setScene(scene);
            primaryStage.show();
        } 
        catch(Exception e) {
            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) {
        launch(args);
    }
}

It should be noted that by pressing the headers of the corresponding columns of the table can automatically sort data on the corresponding feature.

In many applications, tabular data should be edited. Editing requires some changes to the program structure, adding new visual components.

The new program structure involves transferring a list of cities from the local visibility of the start() function into Main class fields. This can be done by means of refactoring. The list initialization can also be transferred to a separate method with the help of the refactoring. The method call is added in the code of start() function, where the list initialization was performed. Similarly, the local variable table should be converted to the field, and the filling of the table should be carried out in a separate function, for example, initTable(). Now the Main class code has this look like this:

package ua.inf.iwanoff.java.advanced.fourth;
    
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.BorderPane;

public class Main extends Application {   
    private ObservableList<City> list;
    private TableView<City> table;

    @Override
    public void start(Stage primaryStage) {
        initList();        
        try {
            primaryStage.setTitle("Cities");
            BorderPane root = new BorderPane();
            initTable();
            root.setCenter(table);
            Scene scene = new Scene(root, 300, 200);
            primaryStage.setScene(scene);
            primaryStage.show();
        } 
        catch(Exception e) {
            e.printStackTrace();
        }
    }

    private void initTable() {
        table = new TableView<>();
        table.setItems(list);
        table.getColumns().clear();
        // The columnName column is associated with the name property:
        TableColumn<City, String> columnName = new TableColumn<>("Name");
        columnName.setCellValueFactory(new PropertyValueFactory<>("name"));
        // The columnPopulation column is associated with the population property:
        TableColumn<City, Integer> columnPopulation = new TableColumn<>("Population");
        columnPopulation.setCellValueFactory(new PropertyValueFactory<>("population"));
        // Add columns to the table:            
        table.getColumns().add(columnName);
        table.getColumns().add(columnPopulation);
    }

    private void initList() {
        list = FXCollections.observableArrayList(
            new City("Kharkiv", 1_451_132), 
            new City("Poltava", 295_950),
            new City("Kyiv", 2_868_702)
        );
    }
    
    public static void main(String[] args) {
        launch(args);
    }
}

A button for reloading data should be added. We can also add a button to add a new blank row to further fill. The corresponding buttons can be placed on top and bottom of the window. The code of buttons addition inside the start() method can be as follows:

    Button buttonReload = new Button("Reload data");
    buttonReload.setMaxWidth(Double.MAX_VALUE);
    buttonReload.setOnAction(event -> reload());
    root.setTop(buttonReload);
    Button buttonAddCity = new Button("Add city");
    buttonAddCity.setMaxWidth(Double.MAX_VALUE);
    buttonAddCity.setOnAction(event -> addCity());
    root.setBottom(buttonAddCity);

In the given code, each of the buttons is created with the text specified in the constructor. Setting the maximum width to Double.MAX_VALUE provides stretching the button to the entire window width. The parameters of the setOnAction() functions are lambda expressions in which the invocations of separate functions are performed:

    private void reload() {
        initList();
        initTable();
    }

    private void addCity() {
        list.add(new City("", 0));
        initTable();
    }

Buttons are added to the panel, respectively, from the top and bottom.

Editing a table is done through the use of editing features provided by other visual components, such as TextField. The javafx.scene.control.cell.TextFieldTableCell class allows you to work with the table cell as with the text input field. Calling the setCellFactory() method for a particular column redefines the mechanism for manipulating columns in the column. In addition, you should add a callback function that handles the entered (changed) data.

The entire text of the program may look like this:

package ua.inf.iwanoff.java.advanced.fourth;
    
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.BorderPane;
import javafx.util.converter.IntegerStringConverter;

public class Main extends Application {   
    private ObservableList<City> list;
    private TableView<City> table;

    @Override
    public void start(Stage primaryStage) {
        initList();        
        try {
            primaryStage.setTitle("Cities");
            BorderPane root = new BorderPane();
            table = new TableView<>();
            initTable();
            root.setCenter(table);
            Button buttonReload = new Button("Reload data");
            buttonReload.setMaxWidth(Double.MAX_VALUE);
            buttonReload.setOnAction(event -> reload());
            root.setTop(buttonReload);
            Button buttonAddCity = new Button("Add city");
            buttonAddCity.setMaxWidth(Double.MAX_VALUE);
            buttonAddCity.setOnAction(event -> addCity());
            root.setBottom(buttonAddCity);
            Scene scene = new Scene(root, 300, 200);
            primaryStage.setScene(scene);
            primaryStage.show();
        } 
        catch(Exception e) {
            e.printStackTrace();
        }
    }

    private void reload() {
        initList();
        initTable();
    }

    private void addCity() {
        list.add(new City("", 0));
        initTable();
    }

    private void initTable() {
        table.setItems(list);
        table.getColumns().clear();
        table.setEditable(true);
        // The columnName column is associated with the name property:
        TableColumn<City, String> columnName = new TableColumn<>("Name");
        columnName.setCellValueFactory(new PropertyValueFactory<>("name"));
        columnName.setCellFactory(TextFieldTableCell.forTableColumn());
        columnName.setOnEditCommit(t -> 
            ((City) t.getTableView().getItems().get(t.getTablePosition().getRow())).
            setName(t.getNewValue()));
        // The columnPopulation column is associated with the population property:
        TableColumn<City, Integer> columnPopulation = new TableColumn<>("Population");
        columnPopulation.setCellValueFactory(new PropertyValueFactory<>("population"));
        columnPopulation.setCellFactory(TextFieldTableCell.<City, Integer>forTableColumn(
            new IntegerStringConverter()));
        columnPopulation.setOnEditCommit(t ->
            ((City) t.getTableView().getItems().get(t.getTablePosition().getRow())).
            setPopulation(t.getNewValue()));

        // Add columns to the table:            
        table.getColumns().add(columnName);
        table.getColumns().add(columnPopulation);
    }

    private void initList() {
        list = FXCollections.observableArrayList(
            new City("Kharkiv", 1_451_132), 
            new City("Poltava", 295_950),
            new City("Kyiv", 2_868_702)
        );
    }
    
    public static void main(String[] args) {
        launch(args);
    }
}

2.6 Visual Designing Graphical User Interface Applications Using Scene Builder

Different environments use the same application, the so-called Scene Builder, for the visual editing of windows and components of JavaFX. This software product is provided by Oracle and can be downloaded from https://www.oracle.com/java/technologies/javafxscenebuilder-1x-archive-downloads.html. You can download the installation program, then you agree to the license terms, choose version 2 for your operating system and install the application.

In the IntelliJ IDEA environment, in the Settings window, set the path to the JavaFX Scene Builder application. To do this, in the Settings window on the Languages & Frameworks | JavaFX tab select the path to the program, for example C:\Program Files\Oracle\JavaFX Scene Builder 2.0\JavaFX Scene Builder 2.0.exe

You can start the editor through the Package Explorer context menu associated with the FXML document (the Open in SceneBuilder function in IntelliJ IDEA). The main editor window consists of three columns: a component hierarchy on the left (Library), the main pane of editing the scene and the edit subwindow of the properties on the right side of the window (Inspector).

The tabs according to the groups of components are allocated on the left column, the Properties, Layout and Code tabs are allocated on the right column. You can drag components and define their properties on the form. You can change some of the properties directly with the mouse (drag, resize, etc.), you can specify necessary values in the Properties and Layout tabs. In the Code tab, you can, if necessary, define the object's name fx:id), as well as for each component, you can specify the response to events.

In the preview mode (Preview | Show Preview in Window) you can interact with the layout to check the functionality of the future code.

3 Sample Programs

3.1 Working with ObservableList

Suppose, in some list of integers, odd numbers should be arranged first, then even numbers, while maintaining the relative order of numbers. We can propose an algorithm in which the even number we found is removed from the list and added to and at the end of the list. It is important to create two counters (i and j) so as not to transfer numbers several times.

The program will look like this:

package ua.inf.iwanoff.java.advanced.fourth;

import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;

public class ObservableListDemo {
    static void orderList(ObservableList<Integer> list) {
        for (int i = 0, j = 0; j < list.size(); j++) {
            if (list.get(i) % 2 == 0) {
                list.add(list.get(i));
                list.remove(i);
            }
            else {
                i++;
            }
        }
    }

    public static void main(String[] args) {
        ObservableList<Integer> list = FXCollections.observableArrayList();
        list.addListener((ListChangeListener<? super Integer>) c -> System.out.println(list));
        list.addAll(1, 12, 2, 37, 6, 8, 11);
        orderList(list);
        list.clear();
    }
}
      

The text in the console window will look like this:

[1, 12, 2, 37, 6, 8, 11]
[1, 12, 2, 37, 6, 8, 11, 12]
[1, 2, 37, 6, 8, 11, 12]
[1, 2, 37, 6, 8, 11, 12, 2]
[1, 37, 6, 8, 11, 12, 2]
[1, 37, 6, 8, 11, 12, 2, 6]
[1, 37, 8, 11, 12, 2, 6]
[1, 37, 8, 11, 12, 2, 6, 8]
[1, 37, 11, 12, 2, 6, 8]
[]

3.2 Text Fields and Buttons

Assume that we want to create GUI application that allows user to enter two integer numbers in text fields and then obtains their sum in the third text field after clicking button.

The root container of our application will be FlowPane. So, it's enough to create three text boxes and one button and add them sequentially to the pane. The program will be as follows:

package ua.inf.iwanoff.java.advanced.fourth;

import javafx.application.Application;
import javafx.event.Event;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.layout.FlowPane;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.stage.Stage;

public class TextFieldsAndButton extends Application {
    private Button button;
    private TextField field1, field2, field3;
    
    @Override
    public void start(Stage stage) throws Exception {
        stage.setTitle("Sum");
        FlowPane rootNode = new FlowPane(10, 10);// determine the size of the horizontal
                                                 // and vertical gaps between the elements
        rootNode.setAlignment(Pos.CENTER);
        Scene scene = new Scene(rootNode, 200, 200); // window size
        stage.setScene(scene);
        button = new Button("Find the sum");  // define the text on the button
        button.setOnAction(this::buttonClick);// define the function that handles the event
        field1 = new TextField(); 
        field2 = new TextField();
        field3 = new TextField();
        rootNode.getChildren().addAll(field1, field2, button, field3);
        stage.show();
    }

    private void buttonClick(Event event) {
        try {
            int i = Integer.parseInt(field1.getText());
            int j = Integer.parseInt(field2.getText());
            int k = i + j;
            field3.setText(k + "");
        }
        catch (NumberFormatException e1) {
            Alert alert = new Alert(AlertType.ERROR);
            alert.setTitle("Error");
            alert.setHeaderText("Invalid data!");
            alert.showAndWait();
        }
    }
    
    public static void main(String[] args) {
        launch(args);
    }

}

3.2 Using Radio Buttons

In the example below, along with the selection of the RadioButton button, the text of the selected button is displayed in the Label. In order for the buttons to work consistently, they are combined into a ToggleGroup group:

package ua.inf.iwanoff.java.advanced.fourth;

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.RadioButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;


public class ToggleGroupDemo extends Application {
    private Label label = new Label("No button");

    @Override
    public void start(Stage primaryStage) throws Exception {
        primaryStage.setTitle("Toggle Group Demo");

        RadioButton radioButtonFirst = new RadioButton("First");
        radioButtonFirst.setOnAction(this::showButtonText);
        RadioButton radioButtonSecond = new RadioButton("Second");
        radioButtonSecond.setOnAction(this::showButtonText);
        RadioButton radioButtonThird = new RadioButton("Third");
        radioButtonThird.setOnAction(this::showButtonText);

        ToggleGroup radioGroup = new ToggleGroup();
        radioButtonFirst.setToggleGroup(radioGroup);
        radioButtonSecond.setToggleGroup(radioGroup);
        radioButtonThird.setToggleGroup(radioGroup);
        VBox vbox = new VBox(radioButtonFirst, radioButtonSecond, radioButtonThird, label);
        vbox.setSpacing(10);
        vbox.setPadding(new Insets(10, 10, 10, 10));
        Scene scene = new Scene(vbox, 150, 120);
        primaryStage.setScene(scene);
        primaryStage.show();

    }

    private void showButtonText(ActionEvent actionEvent) {
        label.setText(((RadioButton)actionEvent.getSource()).getText());
    }

    public static void main(String[] args) {
        Application.launch(args);
    }

}

3.3 Working with the File Dialogs

Suppose we need to read two numbers from a text file, and write down their sum in another text file. The window will contain two buttons - to select the input and output files respectively. We can offer such a program:

package ua.inf.iwanoff.java.advanced.fourth;

import java.io.File;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.util.Scanner;

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.layout.FlowPane;
import javafx.stage.FileChooser;
import javafx.stage.Stage;

public class SumWriter extends Application {
    private double a, b;
    private FileChooser chooser;
    private File file;
    
    @Override
    public void start(Stage stage) throws Exception {
        stage.setTitle("Sum");
        FlowPane rootNode = new FlowPane(10, 10);// determine the size of the horizontal
                                                 // and vertical gaps between the elements
        rootNode.setAlignment(Pos.CENTER);
        Scene scene = new Scene(rootNode, 200, 100); // window size
        stage.setScene(scene);
        Button buttonOpen = new Button("Load data");  // define the text on the button
        buttonOpen.setOnAction(this::buttonOpenClick);// define the function that handles an event
        Button buttonSave = new Button("Save sum");   // define the text on the button
        buttonSave.setOnAction(this::buttonSaveClick);// define the function that handles an event
        rootNode.getChildren().addAll(buttonOpen, buttonSave);
        chooser = new FileChooser();
        chooser.setInitialDirectory(new File("."));
        stage.show();
    }

    private void buttonOpenClick(ActionEvent event) {
        if ((file = chooser.showOpenDialog(null)) != null) {
            readFromFile();
        }
    }

    private void buttonSaveClick(ActionEvent event) {
        if ((file = chooser.showSaveDialog(null)) != null) {
            writeToFile();
        }
    }

    private void readFromFile() {
        try (Scanner scanner = new Scanner(file)) {
            a = scanner.nextDouble();
            b = scanner.nextDouble();
        }
        catch (Exception e) {
            Alert alert = new Alert(AlertType.ERROR);
            alert.setTitle("Error");
            alert.setHeaderText("Error reading from file!");
            alert.showAndWait();
        }
    }

    private void writeToFile() {       
        try (PrintWriter out = new PrintWriter(new FileWriter(file))) {
            out.println(a + b);
            out.close();
        }
        catch (Exception e) {
            Alert alert = new Alert(AlertType.ERROR);
            alert.setTitle("Error");
            alert.setHeaderText("Cannot create a file!");
            alert.showAndWait();
        }
    }
  
    public static void main(String[] args) {
        launch(args);
    }

}

3.4 GUI Application for Processing Data about Censuses

Suppose we need to create a graphical user interface program that allows the user to enter and edit population census data, store data in an XML file, read data from previously created XML files, edit data, search for data according to the specific criteria, sort data, and storing data in a new file.

To the previously created project, we add a new package that will contain graphical user interface classes, including the main class controller, FXML documents, and additional controllers.

We create a new class named CensusesFX. Its content will be as follows:

package ua.inf.iwanoff.java.advanced.fourth;

import javafx.application.Application;
import javafx.stage.Stage;

public class CensusesFX extends Application {

    @Override
    public void start(Stage primaryStage) {
        
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Also, a new FXML document named CensusesForm.fxml should be added to the project. In order for this file to be copied to the required location after compiling, it should be placed in a folder src\main\resources\ua\inf\iwanoff\java\advanced\fourth (relative to the project folder). The content of the file can be as follows:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.layout.BorderPane?>

<BorderPane xmlns:fx="http://javafx.com/fxml/1">
    <!-- TODO Add Nodes -->
</BorderPane>

Add a reference to the FXML document class and create a scene (the corresponding statements can be copied from the code of the previous examples):

package ua.inf.iwanoff.java.advanced.fourth;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class CensusesFX extends Application {

    @Override
    public void start(Stage primaryStage) {
        try {
            BorderPane root = (BorderPane)FXMLLoader.load(getClass().getResource("CensusesForm.fxml"));
            Scene scene = new Scene(root, 700, 500);
            primaryStage.setScene(scene);
            primaryStage.setTitle("Population censuses");
            primaryStage.show();
        } 
        catch(Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        launch(args);
    }
}

It is also necessary to add a controller class:

package ua.inf.iwanoff.java.advanced.fourth;

public class CensusesController {

}

Further work involves adding and adjusting visual components using the Scene Builder application. We start the application using the context menu concerned with CensusesForm.fxml file in Package Explorer (Open with SceneBuilder). First, we add a reference to the controller class. On the Controller tab in the left part of the Scene Builder window, in the ControllerClass text field, we enter the name of the controller class, specifying all subfolders. In our case, this is ua.inf.iwanoff.java.advanced.fourth.CensusesController.

Then we add the main menu. On the Controls panel, in the left part of the window (palette), we find the MenuBar component and add it to the top of the root container (the insert TOP position in the hierarchy of objects). The main menu that we have added already contains the File submenu (with the Close item), Edit (with the Delete item), and Help (with the About item). From the Menu tab we can add new submenus (Menu) and individual items (MenuItem). In our case, we'll also add the submenu Run and get the following main menu:

  • File submenu with New, Open..., Save... and Exit items
  • Edit submenu with Add row and Remove last row items
  • Run submenu with the Sort by population and Sort by comments items;
  • Help submenu with the About... item.

We can add a Separator component between the Save... and Exit items.

On the left side of the root panel, we need to place another panel (AnchorPane) for allocation of some controls: buttons, input lines, and the area of the search results. In particular, we'll add a Label with the text Text to search:, the TextField line for entering words (sequence of letters) for the search in comments, as well as the corresponding buttons with the text Search for a word and Search the letter sequence. At the bottom of the added panel, we place the TextArea component to output the search results. For TextField and TextArea components, we need to define the names, respectively textFieldCountry, textFieldArea, textFieldText, and textAreaResults, since the contents of these components need to be accessed in the program.

For the textAreaResults results output area, we specify the value of the AnchorPane.bottomAnchor property, which will automatically change the height if the main window is resized. You can set this property directly in the text of an FXML document or with the help of Scene Builder by defining the required value in the lower text field of the Anchor Pane Constraints symbolic image in the Layout tab in the right part of the window.

In the central part of the root pane, we place the TableView component, which will display and edit the population census data. From the beginning, this component contains two columns (TableColumn) with headings C1 and C2. This text should be changed to Year and Population. Two more columns (Population density and Comments) should be added to the table. The table and columns should also be renamed, respectively, tableViewCensuses, tableColumnYear, tableColumnPopulation, tableColumnDensity, and tableColumnComments. For the entire table (tableViewCensuses), and for all columns, besides tableColumnDensity, the editable property should be set to true, and for tableColumnDensity, it should be set to false. In the preview (Preview | Show Preview in Window) of the Scene Builder editor, the future main window of the application will look like this:

Now we can develop controller. An earlier created CountryWithStreams class will act as a model. We can add a reference to the controller class. Named components must match private fields that can be generated automatically. The mechanism for generating code using Quick Fix context menu has been described earlier.

It is also necessary to add event handler methods related to menu items, buttons, and text changes. To add an event, we create the corresponding functions with annotation and a parameter of ActionEvent type. References to these functions can be added in Scene Builder or manually. The text of the CensusesForm.fxml file may be as follows:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<BorderPane prefHeight="500.0" prefWidth="700.0"
            xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"
            fx:controller="ua.inf.iwanoff.java.advanced.fourth.CensusesController">
    <top>
        <MenuBar BorderPane.alignment="CENTER">
            <Menu mnemonicParsing="false" text="File">
                <MenuItem mnemonicParsing="false" text="New" onAction="#doNew"/>
                <MenuItem mnemonicParsing="false" text="Open..." onAction="#doOpen"/>
                <MenuItem mnemonicParsing="false" text="Save..." onAction="#doSave"/>
                <MenuItem mnemonicParsing="false" text="Exit" onAction="#doExit"/>
            </Menu>
            <Menu mnemonicParsing="false" text="Edit">
                <MenuItem mnemonicParsing="false" text="Add row" onAction="#doAdd"/>
                <MenuItem mnemonicParsing="false" text="Remove last row" onAction="#doRemove"/>
            </Menu>
            <Menu mnemonicParsing="false" text="Run">
                <MenuItem mnemonicParsing="false" text="Sort by population" onAction="#doSortByPopulation"/>
                <MenuItem mnemonicParsing="false" text="Sort by comments" onAction="#doSortByComments"/>
            </Menu>
            <Menu mnemonicParsing="false" text="Help">
                <MenuItem mnemonicParsing="false" text="About..." onAction="#doAbout"/>
            </Menu>
        </MenuBar>
    </top>
    <left>
        <AnchorPane prefHeight="472.0" prefWidth="200.0" BorderPane.alignment="CENTER">
            <Label text="Country" AnchorPane.leftAnchor="11.0"
                   AnchorPane.rightAnchor="11.0" AnchorPane.topAnchor="14.0" />
            <TextField fx:id="textFieldCountry" prefHeight="22.0" AnchorPane.leftAnchor="11.0"
                       AnchorPane.rightAnchor="11.0" AnchorPane.topAnchor="35.0" onAction="#nameChanged" />
            <Label text="Area" AnchorPane.leftAnchor="11.0"
                   AnchorPane.rightAnchor="11.0" AnchorPane.topAnchor="69.0" />
            <TextField fx:id="textFieldArea" prefHeight="22.0" AnchorPane.leftAnchor="11.0"
                       AnchorPane.rightAnchor="11.0" AnchorPane.topAnchor="90.0" onAction="#areaChanged" />
            <Label text="Text to search:" AnchorPane.leftAnchor="11.0"
                   AnchorPane.rightAnchor="11.0" AnchorPane.topAnchor="154.0" />
            <TextField fx:id="textFieldText" prefHeight="22.0" AnchorPane.leftAnchor="11.0"
                       AnchorPane.rightAnchor="11.0" AnchorPane.topAnchor="175.0" />
            <Button mnemonicParsing="false" prefHeight="22.0"
                    text="Search for a word" AnchorPane.leftAnchor="11.0"
                    AnchorPane.rightAnchor="11.0" AnchorPane.topAnchor="210.0" onAction="#doSearchByWord"/>
            <Button mnemonicParsing="false" prefHeight="22.0"
                    text="Search the letter sequence" AnchorPane.leftAnchor="11.0"
                    AnchorPane.rightAnchor="11.0" AnchorPane.topAnchor="245.0" onAction="#doSearchBySubstring" />
            <TextArea fx:id="textAreaResults" AnchorPane.bottomAnchor="11.0"
                      AnchorPane.leftAnchor="11.0" AnchorPane.rightAnchor="11.0"
                      AnchorPane.topAnchor="280.0" />
        </AnchorPane>
    </left>
    <center>
        <TableView fx:id="tableViewCensuses" prefHeight="473.0" prefWidth="114.0"
                   BorderPane.alignment="CENTER" editable="true" >
            <columns>
                <TableColumn fx:id="tableColumnYear" prefWidth="50.0" text="Year" editable="true" />
                <TableColumn fx:id="tableColumnPopulation" prefWidth="100.0" text="Population"  editable="true" />
                <TableColumn fx:id="tableColumnDensity" prefWidth="140.0" text="Population density"  editable="false" />
                <TableColumn fx:id="tableColumnComments" prefWidth="205.0" text="Comments"  editable="true" />
            </columns>
        </TableView>
    </center>
</BorderPane>

Some auxiliary methods are useful for implementing event handlers. We can list all the code for the CensusesController class:

package ua.inf.iwanoff.java.advanced.third;

import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.TableColumn.CellEditEvent;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.stage.FileChooser;
import javafx.util.converter.IntegerStringConverter;
import ua.inf.iwanoff.java.advanced.first.CensusWithStreams;
import ua.inf.iwanoff.java.advanced.first.CountryWithStreams;
import ua.inf.iwanoff.java.advanced.third.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle;

/**
 * Controller class associated with the CensusesForm.fxml document
 *
 * Implementation of the Initializable interface provides the ability to initialize
 * the visual components described in the FXML document in the initialize() method
 *
 */
public class CensusesController implements Initializable {
    // Reference to the model class:
    private CountryWithStreams country = new CountryWithStreams();

    // List whose contents will be displayed in the table:
    private ObservableList<CensusWithStreams> observableList;

    /**
     * Dialog box for an arbitrary message
     *
     * @param message message text
     */
    public static void showMessage(String message) {
        Alert alert = new Alert(AlertType.INFORMATION);
        alert.setTitle("");
        alert.setHeaderText(message);
        alert.showAndWait();
    }

    /**
     * Error dialog box
     *
     * @param message message text
     */
    public static void showError(String message) {
        Alert alert = new Alert(AlertType.ERROR);
        alert.setTitle("Error");
        alert.setHeaderText(message);
        alert.showAndWait();
    }

    /**
     * Create a file selection dialog
     *
     * @param title window title
     */
    public static FileChooser getFileChooser(String title) {
        FileChooser fileChooser = new FileChooser();
        // We are starting to search from the current folder:
        fileChooser.setInitialDirectory(new File("."));
        // Set filters to find files:
        fileChooser.getExtensionFilters().add(
                new FileChooser.ExtensionFilter("XML files (*.xml)", "*.xml"));
        fileChooser.getExtensionFilters().add(
                new FileChooser.ExtensionFilter("All files (*.*)", "*.*"));
        // Specify the title of the window:
        fileChooser.setTitle(title);
        return fileChooser;
    }

    // Fields related to visual elements:

    @FXML
    private TextField textFieldCountry;
    @FXML
    private TextField textFieldArea;
    @FXML
    private TextField textFieldText;
    @FXML
    private TextArea textAreaResults;
    @FXML
    private TableView<CensusWithStreams> tableViewCensuses;
    @FXML
    private TableColumn<CensusWithStreams, Integer> tableColumnYear;
    @FXML
    private TableColumn<CensusWithStreams, Integer> tableColumnPopulation;
    @FXML
    private TableColumn<CensusWithStreams, Number> tableColumnDensity;
    @FXML
    private TableColumn<CensusWithStreams, String> tableColumnComments;

    /**
     * The method of initializing the visual components described in the FXML document
     *
     */
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        // Write an empty string instead of "No content in table":
        tableViewCensuses.setPlaceholder(new Label(""));
    }

    // Event handlers:

    @FXML
    private void doNew(ActionEvent event) {
        country = new CountryWithStreams();
        observableList = null;
        textFieldCountry.setText("");
        textFieldArea.setText("");
        textFieldText.setText("");
        textAreaResults.setText("");
        tableViewCensuses.setItems(null);
        tableViewCensuses.setPlaceholder(new Label(""));
    }

    @FXML
    private void doOpen(ActionEvent event) {
        FileChooser fileChooser = getFileChooser("Open an XML file");
        File file;
        if ((file = fileChooser.showOpenDialog(null)) != null) {
            try {
                country = FileUtils.deserializeFromXML(file.getCanonicalPath());
                // Fill in the text fields with read data:
                textFieldCountry.setText(country.getName());
                textFieldArea.setText(country.getArea() + "");
                textAreaResults.setText("");
                // Clear and update the table:
                tableViewCensuses.setItems(null);
                updateTable();
            }
            catch (IOException e) {
                showError("File not found");
            }
            catch (Exception e) {
                showError("Invalid file format");
            }
        }
    }

    @FXML
    private void doSave(ActionEvent event) {
        FileChooser fileChooser = getFileChooser("Save an XML file");
        File file;
        if ((file = fileChooser.showSaveDialog(null)) != null) {
            try {
                updateSourceData(); // update the data in the model
                nameChanged(event);
                areaChanged(event);
                FileUtils.serializeToXML(country, file.getCanonicalPath());
                showMessage("Results saved successfully");
            }
            catch (Exception e) {
                showError("Error writing to file");
            }
        }
    }

    @FXML
    private void doExit(ActionEvent event) {
        Platform.exit(); // correct completion of the JavaFX application
    }

    @FXML
    private void doAdd(ActionEvent event) {
        country.addCensus(0, 0, "");
        updateTable(); // create new data
    }

    @FXML
    private void doRemove(ActionEvent event) {
        // We cannot remove the row if there is no data:
        if (observableList == null) {
            return;
        }
        // If there are rows, delete the last one:
        if (observableList.size() > 0) {
            observableList.remove(observableList.size() - 1);
        }
        // If there are no rows, we indicate that the data is missing:
        if (observableList.size() <= 0) {
            observableList = null;
        }
    }

    @FXML
    private void doSortByPopulation(ActionEvent event) {
        updateSourceData();
        country.sortByPopulation();
        updateTable();
    }

    @FXML
    private void doSortByComments(ActionEvent event) {
        updateSourceData();
        country.sortByComments();
        updateTable();
    }

    @FXML
    private void doAbout(ActionEvent event) {
        Alert alert = new Alert(AlertType.INFORMATION);
        alert.setTitle("About...");
        alert.setHeaderText("Population censuses data");
        alert.setContentText("Version 1.0");
        alert.showAndWait();
    }


    @FXML
    private void nameChanged(ActionEvent event) {
        // When the user changed the data in textFieldCountry,
        // we automatically update the name:
        country.setName(textFieldCountry.getText());
    }

    @FXML
    private void areaChanged(ActionEvent event) {
        // When the user changed the data in textFieldArea, we automatically
        // update the values of the territory and population density:
        try {
            double area = Double.parseDouble(textFieldArea.getText());
            country.setArea(area);
            setDensity();
        }
        catch (NumberFormatException e) {
            // If an error, we return, as it was:
            textFieldArea.setText(country.getArea() + "");
        }
    }

    @FXML
    private void doSearchByWord(ActionEvent event) {
        // Updating data:
        updateSourceData();
        textAreaResults.setText("");
        for (int i = 0; i < country.censusesCount(); i++) {
            CensusWithStreams c = (CensusWithStreams) country.getCensus(i);
            if (c.containsWord(textFieldText.getText())) {
                showResults(c);
            }
        }
    }

    @FXML
    private void doSearchBySubstring(ActionEvent event) {
        // Updating data:
        updateSourceData();
        textAreaResults.setText("");
        for (int i = 0; i < country.censusesCount(); i++) {
            CensusWithStreams c = (CensusWithStreams) country.getCensus(i);
            if (c.containsSubstring(textFieldText.getText())) {
                showResults(c);
            }
        }
    }

    private void showResults(CensusWithStreams census) {
        textAreaResults.appendText("Census of the " + census.getYear() + " year.\n");
        textAreaResults.appendText("Comments:" + census.getComments() + "\n");
        textAreaResults.appendText("\n");
    }

    private void updateSourceData() {
        // Rewriting data into the model from observableList
        country = new CountryWithStreams();
        for (CensusWithStreams c : observableList) {
            country.addCensus(c);
        }
    }

    private void setDensity() {
        // Determine the mechanism of automatic recalculation of cells
        // for tableColumnDensity when other data changes:
        tableColumnDensity.setCellFactory(cell -> new TableCell<CensusWithStreams, Number>() {
            @Override
            protected void updateItem(Number item, boolean empty) {
                int current = this.getTableRow().getIndex();
                if (observableList != null && current >= 0 &&
                        current < observableList.size() && country.getArea() > 0) {
                    double population = observableList.get(current).getPopulation();
                    double density = population / country.getArea();
                    setText(String.format("%7.2f", density));
                }
                else {
                    setText("");
                }
            }
        });
    }

    private void updateYear(CellEditEvent<CensusWithStreams, Integer> t) {
        // Update year data:
        CensusWithStreams c = t.getTableView().getItems().get(t.getTablePosition().getRow());
        c.setYear(t.getNewValue());
    }

    private void updatePopulation(CellEditEvent<CensusWithStreams, Integer> t) {
        // Update population data:
        CensusWithStreams c = t.getTableView().getItems().get(t.getTablePosition().getRow());
        c.setPopulation(t.getNewValue());
        setDensity(); // recalculate population density
    }

    private void updateComments(CellEditEvent<CensusWithStreams, String> t) {
        // Update comments:
        CensusWithStreams c = t.getTableView().getItems().get(t.getTablePosition().getRow());
        c.setComments(t.getNewValue());
    }

    private void updateTable() {
        // Fill in the observableList:
        List<CensusWithStreams> list = new ArrayList<>();
        observableList = FXCollections.observableList(list);
        for (int i = 0; i < country.censusesCount(); i++) {
            list.add((CensusWithStreams) country.getCensus(i));
        }
        tableViewCensuses.setItems(observableList);

        // We specify the columns associated with their properties 
        // and the mechanism of editing, depending on the type of cells:
        tableColumnYear.setCellValueFactory(new PropertyValueFactory<>("year"));
        tableColumnYear.setCellFactory(
               TextFieldTableCell.forTableColumn(new IntegerStringConverter()));
        tableColumnYear.setOnEditCommit(t -> updateYear(t));
        tableColumnPopulation.setCellValueFactory(new PropertyValueFactory<>("population"));
        tableColumnPopulation.setCellFactory(
                TextFieldTableCell.forTableColumn(new IntegerStringConverter()));
        tableColumnPopulation.setOnEditCommit(t -> updatePopulation(t));
        tableColumnDensity.setSortable(false); // this column cannot be sorted automatically
        setDensity();
        tableColumnComments.setCellValueFactory(new PropertyValueFactory<>("comments"));
        tableColumnComments.setCellFactory(TextFieldTableCell.forTableColumn());
        tableColumnComments.setOnEditCommit(t -> updateComments(t));
    }

}

The work of the program can be improved by switching the availability of individual elements using the setDisable() method depending on the state of the program.

4 Exercises

  1. Create a graphical user interface application in which user enters two strings in two text fields, and after clicking the button receives the concatenated string in the third text filed.
  2. Create a graphical user interface application in which user enters two floating point numbers in two text fields, and after clicking the button receives the product of these numbers in the third text filed.
  3. Create a graphical user interface application in which user enters two integer numbers in two text fields, and after clicking the button receives the product of these numbers in a separate dialog box.

5 Quiz

  1. What Java libraries are used for creation of GUI applications?
  2. What is an applet?
  3. What is the idea of event-driven programming?
  4. What is JavaFX? What are the benefits of JavaFX?
  5. What are JavaFX properties, how are they implemented and what opportunities do they provide?
  6. How is the binding between JavaFX objects is implemented?
  7. What is ObservableList?
  8. What is FXML? What are the benefits of FXML?
  9. What is MVC?
  10. What is the layout and how is it implemented in JavaFX?
  11. What standard containers does JavaFX provide and how are they different?
  12. How are the RadioButton buttons working?
  13. What are the features of modal dialog windows?
  14. How JavaFX uses standard file choose windows?
  15. How to work with tabular data in JavaFX?
  16. How to associate table columns with properties of objects in a list?
  17. How to provide editing table cells in JavaFX?
  18. How to provide the visual editing of the application window of the graphical user interface?

 

up