Laboratory Work 4
Multithreading. Development of GUI Applications
1 Laboratory Work 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 works 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 (deserializing) data from an XML document for editing;
- storing the changed data in an XML document using XStream serialization tools;
- search according to the criteria specified in the Laboratory work # 2 of the Java Basic Programming Course;
- sorting according to the criteria specified in the Laboratory work # 2 of the Java Basic Programming Course;
- showing "About" window with information about the program and its 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 Calculating π in a Separate Thread
Implement the program of calculation π with precision up to a given ε as the sum of a sequence (Leibniz formula):
The calculation should be done in a separate thread. When performing the calculation, you should give the user the opportunity to enter a request for the number of calculated elements of the sum.
1.3 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 to the list should result in the output of the list items to the console window.
1.4 Creating a GUI Application for Getting Prime Factors of Numbers (Additional Task)
Implement JavaFX GUI application, in which the user inputs a range of numbers (from and to), and in the window
the numbers and their prime factors are displayed. Realize the ability to pause, resume the thread, and also complete
termination and re-calculation with new data.
2 Guidelines
2.1 Working with Threads. Overview
A process is an instance of a computer program that is loaded into memory and being executed. In modern operating systems, a process can be executed in parallel with other processes. A process has its own address space, and this space is unavailable for other processes.
Most Java Virtual Machine implementations run within a single process. A Java program can create additional processes
and organize interaction between them using a ProcessBuilder object. In the simplest example below,
the program notepad.exe is launched:
package ua.inf.iwanoff.java.advanced.fourth;public class StartNotepad {static void main()throws java.io.IOException { String[] command = {"notepad"};// the array can also contain program options ProcessBuilder processBuilder =new ProcessBuilder(command); processBuilder.start(); } }
A thread is a separate subtask that can be executed in parallel with other subtasks (threads) within a single process. Each process contains at least one thread called the main thread. All threads created by the process are executed in the address space of this process and have access to process resources. Creating threads is essentially less resource-intensive operation than creating new processes. Threads are sometimes referred to as lightweight processes.
If the process has created multiple threads, then all of them are executed in parallel, while the time of the central processor (or several central processors in multiprocessor systems) is distributed among these threads. Distribution of the CPU time is performed by a special module of the operating system, the so-called scheduler. The scheduler in turn handles the management of individual thread, so even in the one-processor system an illusion of parallel running of threads is created. Time distribution is performed by interruptions of the system timer.
Threads are used to implement independent subtasks within a single process in order to create background tasks, simulate the parallel execution of certain actions, or enhance the user interface's convenience.
2.2 Low-Level Thread Tools
2.2.1 Creation of Threads
There are two ways to create a thread object using the Thread class:
- creation of a new class derived from the
java.lang.Threadclass and creating an object of a new class; - creation of a class that implements the
java.lang.Runnableinterface; an object of this class is passed to thejava.lang.Threadclass constructor as an argument.
When creating a derived class from Thread, it is necessary to override its run() method.
After creating the thread object, it should be started using the start() method. This method performs
some initialization and then calls run(). The following example creates a separate execution thread
that outputs the next integer from 1 to 40:
package ua.inf.iwanoff.java.advanced.fourth;public class ThreadTestextends Thread { @Overridepublic void run() {for (int counter = 1; counter <= 40; counter++) {try { Thread.sleep(1000);// Delay time in milliseconds }catch (InterruptedException e) { e.printStackTrace(); } System.out.println(counter); } }static void main() {new ThreadTest().start();// You can add actions that run in parallel with the run() method. } }
Calling the sleep() method causes the thread to be suspended at the specified number of milliseconds.
Calling the sleep() method requires catching of the InterruptedException exception that is thrown if
this thread is interrupted by another thread. A similar example is with the creation of a class object that implements
the Runnable interface. In this case, you also need to run the thread using the start() method:
package ua.inf.iwanoff.java.advanced.fourth;public class RunnableTestimplements Runnable { @Overridepublic void run() {for (int counter = 1; counter <= 40; counter++) {try { Thread.sleep(1000); }catch (InterruptedException e) { e.printStackTrace(); } System.out.println(counter); } }static void main() {new Thread(new RunnableTest()).start();// You can add actions that run in parallel with the run () method } }
The second approach is better, since in this case we are free to choose the base class.
You can assign names to individual threads (the second parameter of the constructor) and get these names using
the getName() method.
The setPriority() method allows you to modify priority of a thread. It is not recommended to set
the highest priority (Thread.MAX_PRIORITY constant).
2.2.2 Thread States
Any thread can be in several standard states. The thread receives the state "new" (Thread.State.NEW)
when the thread object is created. Calling the start() method moves the thread to the state "runnable" (Thread.State.RUNNABLE).
There are states "blocked" (Thread.State.BLOCKED), "time-limited waiting" (Thread.State.TIMED_WAITING), "waiting" (Thread.State.WAITING)
and "complete" (Thread.State.TERMINATED). When creating a thread, it gets a "new" state
and is not executed. You can call the getState() method to get the state of a thread. The following
example shows some states of the thread within its life cycle:
package ua.inf.iwanoff.java.advanced.fourth;public class StateTest {static void main()throws InterruptedException { Thread testThread =new Thread(); System.out.println(testThread.getState());// NEW testThread.start(); System.out.println(testThread.getState());// RUNNABLE testThread.interrupt();// interrupt the thread Thread.sleep(100);// it takes time to complete the thread System.out.println(testThread.getState());// TERMINATED } }
The wait(long timeout) method, like sleep(long timeout) method,
allows you to pause the thread for a specified number of milliseconds. When executing this method, InterruptedException can
also be thrown. The use of these methods allows you to switch the thread to standby (TIMED_WAITING).
The key difference is that wait() must be called within a synchronized context and releases the object's
lock. In this case, the thread can awake either automatically after the timeout expires, or earlier via notify() or notifyAll() called
by another thread. Conversely, sleep() does not release any locks and does not respond to notify().
The wait() method can be called without parameters. The thread thus gets into the state WAITING.
An alternative way to optimize thread performance is by calling the yield() method. It temporarily
relinquishes CPU time, allowing the scheduler to transfer control to other threads of the same priority.
After the run() method is completed, the thread finishes its work and passes to the state TERMINATED.
In earlier versions of Java, for the forced stop, as well and temporary suspension of the thread and subsequent
resumption, the use class Thread methods stop(), suspend() and resume() was
provided. The stop(), suspend() and resume() methods are considered as deprecated,
since they provoke the creation of unreliable and difficult to debug code. Furthermore, the use of suspend() and resume() can
trigger deadlocks.
You can interrupt the thread that is in the standby state (Thread.State.TIMED_WAITING) by calling
the interrupt() method. In this case, the InterruptedException is thrown. We can modify
the previous example so that it allows us to interrupt the thread:
package ua.inf.iwanoff.java.advanced.fourth;import java.util.Scanner;public class InterruptTestimplements Runnable { @Overridepublic void run() {for (int counter = 1; counter <= 40; counter++) {try { Thread.sleep(1000); }catch (InterruptedException e) { System.out.println("Thread interrupted.");break ; } System.out.println(counter); } } @SuppressWarnings("resource")static void main() { Thread thread =new Thread(new InterruptTest()); thread.start(); System.out.println("Press Enter to interrupt");new Scanner(System.in).nextLine(); thread.interrupt(); } }
2.2.3 Joining and Interrupting Threads
The join() method allows one thread to wait for the completion of another. Calling this method inside
thread t1 for thread t2 will result in the temporary suspension of the current thread
(t1) until the completion of t2, as shown in the following example:
package ua.inf.iwanoff.java.advanced.fourth;public class JoinTest {static Thread t1, t2;static class FirstThreadimplements Runnable { @Overridepublic void run() {try { System.out.println("The first thread started."); Thread.sleep(1000); System.out.println("The primary work of the first thread has been completed."); t2.join(); System.out.println("The first thread completed."); }catch (InterruptedException e) { e.printStackTrace(); } } }static class SecondThreadimplements Runnable { @Overridepublic void run() {try { System.out.println("The second thread started."); Thread.sleep(3000); System.out.println("The second thread completed."); }catch (InterruptedException e) { e.printStackTrace(); } } }static void main() { t1 =new Thread(new FirstThread()); t2 =new Thread(new SecondThread()); t1.start(); t2.start(); } }
The order of message output will be as follows:
The first thread started. The second thread started. The primary work of the first thread has been completed. The second thread completed. The first thread completed.
Like sleep(), join() responds to interrupt, therefore, it is necessary to catch InterruptedException.
You can create a thread whose work is interrupted when all ordinary (non-daemon) threads have finished. In order to create
such a thread, the setDaemon() method is called with the true parameter.
The setDaemon() method must be called after the thread was created, but before the start() method
is called. You can check whether there is a thread by the daemon or not using isDaemon() method. If
the daemon thread creates other threads, they will also receive the status of the daemon thread.
package ua.inf.iwanoff.java.advanced.fourth;public class DaemonThreadextends Thread {public void run() {try {if (isDaemon()) { System.out.println("start of the daemon thread"); sleep(1000); }else { System.out.println("start of the regular thread"); } }catch (InterruptedException e) { System.err.print("Error" + e); }finally {if (!isDaemon()) System.out.println("regular thread termination");else System.out.println("daemon thread termination"); } }static void main() { DaemonThread usual =new DaemonThread(); DaemonThread daemon =new DaemonThread(); daemon.setDaemon(true ); daemon.start(); usual.start(); System.out.println("the last line of the main() method"); } }
After compiling and running, it is possible that the following message output sequence will be obtained:
start of the daemon thread the last line of the main() method start of the regular thread regular thread termination
The daemon thread (due to the call to the sleep(10000) method) did not have time to complete the execution
of its code before the main application thread associated with the main() method was completed. The
basic property of daemon threads is the ability of the main application thread to complete execution with the end
of the code of the main() method, ignoring the fact that the daemon thread is still running. If you
set the delay time also for the main thread, then the daemon thread can have time to complete its
execution before the main thread ends.
start of the daemon thread start of the regular thread regular thread termination daemon thread termination the last line of the main() method
2.2.4 Threads Synchronization
In the case of multiple threads, it is important to eliminate the possibility of using one resource by two threads.
Collisions can be avoided by declaring some blocks as synchronized. Such a method or code fragment
is referred to as a critical section.
The concept of the monitor is used to provide access to such code. A monitor (also known as an intrinsic lock) is an internal structure of a Java object used to control access to critical sections of code. When a thread enters a synchronized block, it acquires the object's monitor, thereby blocking other threads from accessing any code synchronized on that same object until the monitor is released.
If the synchronized keyword is located before
of a method name, the object for which this method is called (this) considered as
monitor.
While a thread is executing this method, it holds the object's monitor, preventing other threads from executing any other
synchronized methods on the same object instance until the monitor is released.
Consider an example. The Adder class allows you to add integers to some accumulator. The AdderThread class,
which implements the execution thread, provides the addition of consecutive five integer values. In the main() method
of the AdderTest class, we create two threads and run them:
package ua.inf.iwanoff.java.advanced.fourth;class Adder {long sum = 0;public void add(long value) {this .sum += value; System.out.println(Thread.currentThread().getName() + " " + sum); } }class AdderThreadextends Thread {private Adder counter =null ;public AdderThread(String name, Adder counter) {super (name);this .counter = counter; }public void run() {try {for (int i = 0; i < 5; i++) { counter.add(i); Thread.sleep(10); } }catch (InterruptedException e) { e.printStackTrace(); } } }public class AdderTest {static void main() { Adder adder =new Adder(); Thread threadA =new AdderThread("A", adder); Thread threadB =new AdderThread("B", adder); threadA.start(); threadB.start(); } }
The program that was started several times will produce various intermediate results due to the incorrect operation
of two threads with the same object. To fix the situation, the synchronized modifier should
be added before the add() method:
synchronized public void add(long value) {this .sum += value; System.out.println(Thread.currentThread().getName() + " " + sum); }
Sometimes declaring the entire method as synchronized is inconvenient, since in this case other
threads could execute other blocking methods that are not related to this one. To solve this problem, use a block
that starts with the word synchronized and contains in brackets the name of the object responsible
for the lock. Any object derived from java.lang.Object (that supports the necessary methods) can act
as a monitor. The following example creates a special object for blocking:
public void add(long value) { Object lock =new Object();synchronized (lock) {this .sum += value; System.out.println(Thread.currentThread().getName() + " " + sum); } }
Note: in our case, this approach may not give the desired result; it is better to implement blocking at the level of the entire method.
When exchanging data through fields, the volatile keyword should be used before describing
the corresponding field. This is due to the presence of a data caching mechanism for each thread separately, and
the value from this cache may differ for each of them. Declaring a field with the volatile keyword
disables such caching for it:
volatile long sum = 0;
The wait() method, called inside a synchronized block or method, suspends the execution of the current
thread and releases the monitor (lock) of that object. Waiting threads can be notified to resume execution using
the notify() method for one thread or notifyAll() for
all threads. These coordinating methods must be invoked by another thread that
currently holds the monitor of the same object. However, the awakened threads do not regain the lock instantly.
They can resume execution only after the notifying thread exits its synchronized block and they successfully re-acquire
the monitor in competition with other threads.
2.3 Using Java to Create GUI Applications
2.3.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 participate in the exchange of 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 "heavyweight". This and other AWT drawbacks are fixed in the Swing library. The Swing library also provides some additional visual components, such as a tabbed pane, 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.3.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. Today, this technology is practically unused, and examining it is of purely historical interest.
All you needed to perform the applet was contained in the <applet> tag in the text of
the HTML file. The applet was usually responsible for a certain rectangular area in the browser window. The
coordinates of this area could also be specified in the <applet> tag. For example:
<applet codebase = "." code = "test.Applet1.class" width = 400 height = 300> </applet>
The codebase attribute specified the location of the class that implements the applet (in the
example given, the current folder). The code attribute was the class name. The width
and height attributes specified the size of the applet window.
The java.applet.Applet (Java 1) and javax.swing.JApplet (Java 2) classes were used as
base classes to implement applets. The life cycle was determined by the following methods of the
Applet class:
public voidinit()is invoked by the browser immediately after loading the applet before the first call of thestart()method; this method needs to be redefined almost always, if an applet needs at least some kind of initialization;public voidstart()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 voiddestroy()always invoked when exiting the browser and rebooting the applet.
For security reasons, certain restrictions were imposed on applets:
- 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 does not require the main() method and can be viewed using the program appletviewer.exe,
which was part of the JDK up to and including version 10.
2.3.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, whose features are used for event handling, working with graphics, and so
on. 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()
method. It is necessary to add an import statement:
import javax.swing.*;
We can create a new frame in main() method. The constructor's argument is used for setting of
window title:
// ... public class HelloWorldSwing {static void main() { 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)
method 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 {static void main() { 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 JavaBeans.
2.4 Overview of JavaFX Platform
2.4.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.
2.4.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 Mainextends Application { @Overridepublic 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(); }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.
JavaFX components form a hierarchy of objects. The application must contain at least one Stage
type object. Stage properties 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.
Unfortunately, in the latest versions of the JDK, compiling and running this program is complicated, because starting with Java 11, JavaFX is not included in the JDK and is developed as a separate OpenJFX project. It must be downloaded and set up separately.
2.4.3 Using JavaFX Tools in IntelliJ IDEA
The IntelliJ IDEA IDE provides tools for creating 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. A program
that shows a window with the title Hello! and the button inside is
automatically created.
The project contains a package with three files:
HelloApplication.javacontains theMainclass, derived fromjavafx.application.Application.HelloController.javacontains an event handleronHelloButtonClick().Launcher.javacontains theLauncherclass with themain()method.
The code of HelloApplication.java file will be as follows:
package ua.inf.iwanoff.java.advanced.fourth.demo;import javafx.application.Application;import javafx.fxml.FXMLLoader;import javafx.scene.Scene;import javafx.stage.Stage;import java.io.IOException;public class HelloApplicationextends Application { @Overridepublic void start(Stage stage)throws IOException { FXMLLoader fxmlLoader =new FXMLLoader(HelloApplication.class .getResource("hello-view.fxml")); Scene scene =new Scene(fxmlLoader.load(), 320, 240); stage.setTitle("Hello!"); stage.setScene(scene); stage.show(); } }
The code of HelloController.java file will be as follows:
package ua.inf.iwanoff.java.advanced.fourth.demo;import javafx.fxml.FXML;import javafx.scene.control.Label;public class HelloController { @FXMLprivate Label welcomeText; @FXMLprotected void onHelloButtonClick() { welcomeText.setText("Welcome to JavaFX Application!"); } }
The code of Launcher.java file will be as follows:
package ua.inf.iwanoff.java.advanced.fourth.demo;import javafx.application.Application;public class Launcher {static void main(String[] args) { Application.launch(HelloApplication.class , args); } }
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. The content of this file is as follows:
<?xml version="1.0" encoding="UTF-8"?> <?import javafx.geometry.Insets?> <?import javafx.scene.control.Label?> <?import javafx.scene.layout.VBox?> <?import javafx.scene.control.Button?> <VBox alignment="CENTER" spacing="20.0" xmlns:fx="http://javafx.com/fxml" fx:controller="ua.inf.iwanoff.java.advanced.fourth.demo.HelloController"> <padding> <Insets bottom="20.0" left="20.0" right="20.0" top="20.0"/> </padding> <Label fx:id="welcomeText"/> <Button text="Hello!" onAction="#onHelloButtonClick"/> </VBox>
Thanks to this division into files, the project supports the MVC pattern, at least two of its components – View
(hello-view.fxml) and Controller (HelloController class).
Additional dependencies are automatically created in the pom.xml file:
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>25.0.3</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>25.0.3</version>
</dependency>
Note: as with all such cases, the version of the artifacts may change.
After starting program and pressing button the text "Welcome to JavaFX Application!" will appear.
Note: in newer versions of the JDK, running a program may cause warnings similar to those caused by using XStream; methods for dealing with these warnings were described in the previous lab.
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>25.0.3</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>25.0.3</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-graphics</artifactId>
<version>25.0.3</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-25
(the corresponding folder is in the compressed file). For the application
to work, a lib folder is required, which is inside the specified, for example, c:\javafx-sdk-25\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-25\lib" --add-modules javafx.controls,javafx.fxml
Next, you should save the created configuration and use it to run the program.
2.5 The Theoretical Basis of Creating JavaFX Applications
2.5.1 Use of JavaFX Properties. Observable
In the broadest sense, a property is an attribute of an object, 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 JavaBeans, which syntax has been
described earlier. By expanding the JavaBeans 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>25.0.3</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 {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.5.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 {static void main() {final IntegerProperty m =new SimpleIntegerProperty(1);final IntegerProperty n =new SimpleIntegerProperty(2); IntegerBinding sum =new IntegerBinding() { {super .bind(m, n); } @Overrideprotected 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 methods 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 {static void main() {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 methods 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 methods, such as Bindings.add(a.multiply(b),
c.multiply(d), etc.
Binding capabilities are often used in graphical user interface programs to automatically coordinate changes to multiple objects or to support the synchronization of the model data.
2.5.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 list's change-notification capabilities are demonstrated in the following example:
package ua.inf.iwanoff.java.advanced.fourth;import javafx.collections.*;public class ListChangeDemo {static void main() {final ObservableList<Integer> list = FXCollections.observableArrayList(1, 2, 4, 8); list.addListener(new ListChangeListener() { @Overridepublic 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 method 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.6 Working with JavaFX Visual Components
2.6.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
JavaFX visual components. The most commonly used derived 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.6.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.6.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;BorderPaneworks similarly to theBorderLayoutof the Swing library; you can add five components:top,bottom,left,right, andcenter; in the latter case, the component tries to occupy the entire free space;FlowPaneautomatically adds items to the horizontal (vertical) row with the continuation in the next row (column);HBoxlays nodes in a horizontal row;VBoxlays nodes in a vertical row;AnchorPaneallows attachment of nodes to different sides of the container, or to its center, determining the appropriate distances;TilePanearranges elements in a grid of the same size;StackPaneplaces each new node on top of the previous node; allows you to create compound forms that support the dynamic change of its content;GridPaneallows you to arrange nodes in a dynamic grid, which allows you to combine adjacent cells; in its capabilities it is close toGridBagLayoutofjavax.swinglibrary.
All panels support the padding property, the GridPane, HBox and VBox
panels also support the spacing property (the gap between the child nodes).
2.6.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 theToggleGroup) - 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
javafx.event.Event 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.Event class.
The best way to learn how to work with visual components and events is to analyze examples. Example 3.3 shows
the work with text fields and a button, in example 3.4 demonstrates the work with the RadioButton
buttons, Example 3.7 also shows working with the menu and TextArea.
2.6.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 method 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 withOKandCancelbuttons (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() methods 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.5 to find out how to use the
FileChooser class and events.
2.7 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 Mainextends Application { @Overridepublic 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(); } }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() method 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() method, 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 method, 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 Mainextends Application {private ObservableList<City> list;private TableView<City> table; @Overridepublic 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) ); }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() methods are lambda expressions in which the invocations of
separate methods 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 Mainextends Application {private ObservableList<City> list;private TableView<City> table; @Overridepublic 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) ); }static void main(String[] args) { launch(args); } }
2.8 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.
2.9 Use Thread Execution in JavaFX Applications
The work of graphical user interface applications is usually associated with threads. After starting the JavaFX application, the application thread is automatically created to handle the events. Only this thread can interact with windows and visual components. Attempting two threads simultaneously to control the visualization of components and information on the window's surface can lead to chaotic appearance and unpredictable behavior of the visual components. However, thanks to the multithreading, you can provide better controllability of the application. In addition, it is advisable to take in a separate thread some actions that are performed for a long time. This will provide an opportunity to monitor the process, pause and continue it, change settings, etc.
JavaFX tools allow you to execute code in the thread, which is responsible for receiving and processing events.
The javafx.application.Platform.runLater() method receives an object that implements the Runnable functional
interface. This object enters the event processing thread, where the object becomes in the queue and, when the opportunity
arises, its run() method is executed. In this method, it is advisable to implement interaction with
visual components. Example 3.6 demonstrates the possibility of calling this method from a child thread.
3 Sample Programs
3.1 Calculating the Square Root
Suppose we need to implement a program to calculate the square root of a number with an accuracy of ε. The algorithm for calculating the square root is to determine the initial approximation (for example, 1) and sequentially obtain new approximations as the arithmetic mean of the previous number and the argument divided by this approximation. The algorithm ends when the difference between two consecutive approximations is less than the specified precision.
The calculations will be performed in a separate thread. During the calculation, the user must be able to receive information about the current calculated value of the square root. Suppose, to obtain intermediate values during the calculations, it is enough to press Enter. The program will be as follows:
package ua.inf.iwanoff.java.advanced.fourth;import java.util.Scanner;class SquareRootextends Thread {private final double x;private final double epsilon;private volatile double result = 0;private volatile boolean finished =false ;public double getResult() {return result; }public boolean isFinished() {return finished; }public SquareRoot(double x,double epsilon) {this .x = x;this .epsilon = epsilon; } @Overridepublic void run() {double previous = 0;double current = 1;while (Math.abs(current - previous) > epsilon) { previous = current; current = (previous + x / previous) / 2; result = current;try { Thread.sleep(1000); }catch (InterruptedException e) { Thread.currentThread().interrupt();break ; } } finished =true ; } }public class SquareRootCalculator {static void main() { Scanner scanner =new Scanner(System.in); System.out.print("Enter x and precision:");double x = scanner.nextDouble();double epsilon = scanner.nextDouble(); SquareRoot task =new SquareRoot(x, epsilon); scanner =new Scanner(System.in);// clean the keyboard buffer task.start();while (!task.isFinished()) { System.out.print("Press Enter if you want to get intermediate result"); scanner.nextLine(); System.out.println(task.getResult()); } System.out.println("Final result:" + task.getResult()); } }
Usage volatile ensures that the current value of the intermediate result is obtained.
3.2 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++; } } }static void main() { 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.3 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 TextFieldsAndButtonextends Application {private Button button;private TextField field1, field2, field3; @Overridepublic 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(); } }static void main(String[] args) { launch(args); } }
3.4 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 ToggleGroupDemoextends Application {private Label label =new Label("No button"); @Overridepublic 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()); }static void main(String[] args) { Application.launch(args); } }
3.5 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 SumWriterextends Application {private double a, b;private FileChooser chooser;private File file; @Overridepublic 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(); } }static void main(String[] args) { launch(args); } }
3.6 Creating a GUI Application for Calculating and Displaying Prime Numbers
Suppose it is necessary to develop a program for obtaining primes in the range from 1 to a certain value, which can be quite large. To find primes we will use the simplest algorithm for sequential checking of all numbers from 2 to the square root of the verifiable number. Such a check may take a long time. To create a user-friendly interface, it's advisable to use a separate execution thread for checking numbers. This will allow user to pause and resume searches, perform standard window manipulations, including closing window before the search will be finished.
The root pane of our application is BorderPane. The source code of the JavaFX application will be
as follows:
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;import java.io.IOException;public class PrimeAppextends Application {static void main(String[] args) { launch(args); } @Overridepublic void start(Stage primaryStage) {try { BorderPane root = (BorderPane)FXMLLoader.load(getClass().getResource("PrimeAppWindow.fxml")); Scene scene =new Scene(root, 700, 500); primaryStage.setScene(scene); primaryStage.setTitle("Prime Numbers"); primaryStage.show(); }catch (IOException e) { e.printStackTrace(); } } }
The user interface will include the top panel (HBox), on which will be successively allocated a label
with the text "To:", a textFieldTo input field, and four buttons: buttonStart, buttonSuspend, buttonResume and buttonStop with
the text "Start", "Suspend", "Resume" and "Stop". After starting a program,
only the buttonStart button is available. The middle part will be occupied by the textAreaResults input
area, for which editing is disabled. The lower part will be occupied by the progressBar component,
in which the percentage of the search process for primes will be displayed. The FXML document file will look like
this:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.control.ProgressBar?>
<BorderPane prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.121"
xmlns:fx="http://javafx.com/fxml/1" fx:controller="ua.inf.iwanoff.java.advanced.fourth.PrimeAppController">
<top>
<HBox prefHeight="3.0" prefWidth="600.0" spacing="10.0" BorderPane.alignment="CENTER">
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
</padding>
<Label text="To:"/>
<TextField fx:id="textFieldTo" text="1000"/>
<Button fx:id="buttonStart" minWidth="80.0" text="Start" onAction="#startClick"/>
<Button fx:id="buttonSuspend" minWidth="80.0" disable="true" text="Suspend" onAction="#suspendClick"/>
<Button fx:id="buttonResume" minWidth="80.0" disable="true" text="Resume" onAction="#resumeClick"/>
<Button fx:id="buttonStop" minWidth="80.0" disable="true" text="Stop" onAction="#stopClick"/>
</HBox>
</top>
<center>
<TextArea fx:id="textAreaResults" editable="false" wrapText="true" BorderPane.alignment="CENTER"/>
</center>
<bottom>
<ProgressBar fx:id="progressBar" maxWidth="Infinity" progress="0"/>
</bottom>
</BorderPane>
We can generate an empty controller class, add fields associated with visual components and methods of handling events:
package ua.inf.iwanoff.java.advanced.fourth;import javafx.event.ActionEvent;import javafx.fxml.FXML;import javafx.scene.control.*;public class PrimeAppController { @FXMLprivate TextField textFieldTo; @FXMLprivate Button buttonStart; @FXMLprivate Button buttonSuspend; @FXMLprivate Button buttonResume; @FXMLprivate Button buttonStop; @FXMLprivate ProgressBar progressBar; @FXMLprivate TextArea textAreaResults; @FXMLprivate void startClick(ActionEvent actionEvent) { } @FXMLprivate void suspendClick(ActionEvent actionEvent) { } @FXMLprivate void resumeClick(ActionEvent actionEvent) { } @FXMLprivate void stopClick(ActionEvent actionEvent) { } }
After starting the program an application window will be the following:

Now, separately from the GUIs, you can create a class that will be responsible for calculating primes in a separate
thread. The PrimeNumbers class will implement the Runnable interface. The main methods
(start(), suspend(), resume() and stop()), which will be called
from the controller, change the state of the object (suspended and stopped fields). To
provide flexibility, the program updates data about simple numbers and percentage of process execution through the
callback mechanism. The fields of type Runnable store appropriate objects. To launch run() methods
in another thread, these methods are called using Platform.runLater() method. The PrimeNumbers class
cheat code will look like this:
package ua.inf.iwanoff.java.advanced.fourth;import javafx.application.Platform;public class PrimeNumbersimplements Runnable {private Thread primesThread;// the thread of the calculation of primes private int to;// the end of the range of the calculation of primes private int lastFound;// last found prime private Runnable displayFunc;// a function that is called to output the found number private Runnable percentageFunc;// a function that updates the percentage of the completed process private Runnable finishFunc;// a function that is called after the finishing private double percentage;private boolean suspended;private boolean stopped;public PrimeNumbers(Runnable addFunc, Runnable percentageFunc, Runnable finishFunc) {this .displayFunc = addFunc;this .percentageFunc = percentageFunc;this .finishFunc = finishFunc; }public int getTo() {return to; }public void setTo(int to) {this .to = to; }public synchronized int getLastFound() {return lastFound; }private synchronized void setLastFound(int lastFound) {this .lastFound = lastFound; }public synchronized double getPercentage() {return percentage; }private synchronized void setPercentage(double percentage) {this .percentage = percentage; }public synchronized boolean isSuspended() {return suspended; }private synchronized void setSuspended(boolean suspended) {this .suspended = suspended; }public synchronized boolean isStopped() {return stopped; }private synchronized void setStopped(boolean stopped) {this .stopped = stopped; } @Overridepublic void run() {for (int n = 2; n <= to; n++) {try { setPercentage(n * 1.0 / to);// Update the percentage: if (percentageFunc !=null ) { Platform.runLater(percentageFunc); }boolean prime =true ;for (int i = 2; i * i <= n; i++) {if (n % i == 0) { prime =false ;break ; } } Thread.sleep(20);if (prime) { setLastFound(n);// Display prime number found: if (displayFunc !=null ) { displayFunc.run(); } } }catch (InterruptedException e) {// Depending on the state of the object, // wait for the continuation or complete the search: while (isSuspended()) {try { Thread.sleep(100); }catch (InterruptedException e1) {// Interrupted by waiting: if (isStopped()) {break ; } } }if (isStopped()) {break ; } } }if (finishFunc !=null ) { Platform.runLater(finishFunc); } }public void start() { primesThread =new Thread(this ); setSuspended(false ); setStopped(false ); primesThread.start(); }public void suspend() { setSuspended(true ); primesThread.interrupt(); }public void resume() { setSuspended(false ); primesThread.interrupt(); }public void stop() { setStopped(true ); primesThread.interrupt(); } }
As you can see from the code, data access methods are implemented with the synchronized modifier,
which prevents simultaneous writing and reading of partially completed data from different threads. Therefore, all
references to class fields from its methods are carried out only through the access methods.
Now we can implement the controller code:
package ua.inf.iwanoff.java.advanced.fourth;import javafx.event.ActionEvent;import javafx.fxml.FXML;import javafx.scene.control.*;public class PrimeAppController { @FXMLprivate TextField textFieldTo; @FXMLprivate Button buttonStart; @FXMLprivate Button buttonSuspend; @FXMLprivate Button buttonResume; @FXMLprivate Button buttonStop; @FXMLprivate ProgressBar progressBar; @FXMLprivate TextArea textAreaResults;private PrimeNumbers primeNumbers =new PrimeNumbers(this ::addToTextArea,this ::setProgress,this ::finish); @FXMLprivate void startClick(ActionEvent actionEvent) {try { primeNumbers.setTo(Integer.parseInt(textFieldTo.getText())); textAreaResults.setText(""); progressBar.setProgress(0); buttonStart.setDisable(true ); buttonSuspend.setDisable(false ); buttonResume.setDisable(true ); buttonStop.setDisable(false ); primeNumbers.start(); }catch (NumberFormatException e) { Alert alert =new Alert(Alert.AlertType.ERROR); alert.setTitle("Error"); alert.setHeaderText("Wrong range!"); alert.showAndWait(); } } @FXMLprivate void suspendClick(ActionEvent actionEvent) { primeNumbers.suspend(); buttonStart.setDisable(true ); buttonSuspend.setDisable(true ); buttonResume.setDisable(false ); buttonStop.setDisable(false ); } @FXMLprivate void resumeClick(ActionEvent actionEvent) { primeNumbers.resume(); buttonStart.setDisable(true ); buttonSuspend.setDisable(false ); buttonResume.setDisable(true ); buttonStop.setDisable(false ); } @FXMLprivate void stopClick(ActionEvent actionEvent) { primeNumbers.stop(); }private void addToTextArea() { textAreaResults.setText(textAreaResults.getText() + primeNumbers.getLastFound() + " "); }private void setProgress() { progressBar.setProgress(primeNumbers.getPercentage()); }private void finish() { buttonStart.setDisable(false ); buttonSuspend.setDisable(true ); buttonResume.setDisable(true ); buttonStop.setDisable(true ); } }
The addToTextArea(), setProgress() and finish() methods are not intended
for direct call from the controller, but for a callback. References to these methods are passed to the PrimeNumbers constructor.
Event handlers call up the appropriate PrimeNumbers class methods and change the accessibility of the
buttons.
3.7 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 CensusesFXextends Application { @Overridepublic void start(Stage primaryStage) { }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 CensusesFXextends Application { @Overridepublic 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(); } }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:
FilesubmenuwithNew,Open...,Save...andExititemsEditsubmenu withAdd rowandRemove last rowitemsRunsubmenu with theSort by populationandSort by commentsitems;Helpsubmenu with theAbout...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. Earlier created classes of the console application will act as a
model. We can add a reference to CountryWithArrayList into 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 methods with annotation and a parameter of ActionEvent
type. References to these methods 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.fourth;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.third.Census;import ua.inf.iwanoff.java.fourth.CountryWithArrayList;import ua.inf.iwanoff.java.advanced.third.FileUtils;import ua.inf.iwanoff.java.third.CensusUtilities;import ua.inf.iwanoff.java.third.CountryUtilities;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 CensusesControllerimplements Initializable {// Reference to the model class: private CountryWithArrayList country =new CountryWithArrayList();// List whose contents will be displayed in the table: private ObservableList<Census> 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: @FXMLprivate TextField textFieldCountry; @FXMLprivate TextField textFieldArea; @FXMLprivate TextField textFieldText; @FXMLprivate TextArea textAreaResults; @FXMLprivate TableView<Census> tableViewCensuses; @FXMLprivate TableColumn<Census, Integer> tableColumnYear; @FXMLprivate TableColumn<Census, Integer> tableColumnPopulation; @FXMLprivate TableColumn<Census, Number> tableColumnDensity; @FXMLprivate TableColumn<Census, String> tableColumnComments;/** * The method of initializing the visual components described in the FXML document * */ @Overridepublic void initialize(URL location, ResourceBundle resources) {// Write an empty string instead of "No content in table": tableViewCensuses.setPlaceholder(new Label("")); }// Event handlers: @FXMLprivate void doNew(ActionEvent event) { country =new CountryWithArrayList(); observableList =null ; textFieldCountry.setText(""); textFieldArea.setText(""); textFieldText.setText(""); textAreaResults.setText(""); tableViewCensuses.setItems(null ); tableViewCensuses.setPlaceholder(new Label("")); } @FXMLprivate 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"); } } } @FXMLprivate void doSave(ActionEvent event) { FileChooser fileChooser = getFileChooser("Save an XML file"); File file;if ((file = fileChooser.showSaveDialog(null )) !=null ) {try { FileUtils.serializeToXML(createUpdatedCountry(), file.getCanonicalPath()); showMessage("Results saved successfully"); }catch (Exception e) { showError("Error writing to file"); } } } @FXMLprivate void doExit(ActionEvent event) { Platform.exit();// correct completion of the JavaFX application } @FXMLprivate void doAdd(ActionEvent event) { country.addCensus(0, 0, ""); updateTable();// create new data } @FXMLprivate 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.isEmpty()) { observableList.removeLast(); }// If there are no rows, we indicate that the data is missing: if (observableList.isEmpty()) { observableList =null ; } } @FXMLprivate void doSortByPopulation(ActionEvent event) { CountryUtilities.sortByPopulation(country); tableViewCensuses.setItems(null ); updateTable(); } @FXMLprivate void doSortByComments(ActionEvent event) { CountryUtilities.sortByComments(country); tableViewCensuses.setItems(null ); updateTable(); } @FXMLprivate 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(); } @FXMLprivate void nameChanged(ActionEvent event) {// When the user changed the data in textFieldCountry, // we automatically update the name: country.setName(textFieldCountry.getText()); } @FXMLprivate 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() + ""); } } @FXMLprivate void doSearchByWord(ActionEvent event) { textAreaResults.setText("");for (int i = 0; i < country.censusesCount(); i++) { Census c = country.getCensus(i);if (CensusUtilities.containsWord(c, textFieldText.getText())) { showResults(c); } } } @FXMLprivate void doSearchBySubstring(ActionEvent event) { textAreaResults.setText("");for (int i = 0; i < country.censusesCount(); i++) { Census c = country.getCensus(i);if (CensusUtilities.containsSubstring(c, textFieldText.getText())) { showResults(c); } } }private void showResults(Census census) { textAreaResults.appendText("Census of the " + census.getYear() + " year.\n"); textAreaResults.appendText("Comments:" + census.getComments() + "\n"); textAreaResults.appendText("\n"); }private CountryWithArrayList createUpdatedCountry() {// Rewriting data into the model from observableList CountryWithArrayList country =new CountryWithArrayList();for (Census c : observableList) { country.addCensus(c); } country.setName(textFieldCountry.getText());double area = Double.parseDouble(textFieldArea.getText()); country.setArea(area);return country; }private void setDensity() {// Determine the mechanism of automatic recalculation of cells // for tableColumnDensity when other data changes: tableColumnDensity.setCellFactory(cell ->new TableCell<>() { @Overrideprotected 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<Census, Integer> t) {// Update year data: Census c = t.getTableView().getItems().get(t.getTablePosition().getRow()); c.setYear(t.getNewValue()); }private void updatePopulation(CellEditEvent<Census, Integer> t) {// Update population data: Census c = t.getTableView().getItems().get(t.getTablePosition().getRow()); c.setPopulation(t.getNewValue()); setDensity();// recalculate population density }private void updateComments(CellEditEvent<Census, String> t) {// Update comments: Census c = t.getTableView().getItems().get(t.getTablePosition().getRow()); c.setComments(t.getNewValue()); }private void updateTable() {// Fill in the observableList: List<Census> list =new ArrayList<>(); observableList = FXCollections.observableList(list);for (int i = 0; i < country.censusesCount(); i++) { list.add((Census) 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(this ::updateYear); tableColumnPopulation.setCellValueFactory(new PropertyValueFactory<>("population")); tableColumnPopulation.setCellFactory( TextFieldTableCell.forTableColumn(new IntegerStringConverter())); tableColumnPopulation.setOnEditCommit(this ::updatePopulation); tableColumnDensity.setSortable(false );// this column cannot be sorted automatically setDensity(); tableColumnComments.setCellValueFactory(new PropertyValueFactory<>("comments")); tableColumnComments.setCellFactory(TextFieldTableCell.forTableColumn()); tableColumnComments.setOnEditCommit(this ::updateComments); } }
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.
Since the JavaFX library is added to a previously created project, additional settings must be made, as described in 2.2.3.
4 Exercises
- 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.
- 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.
- 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 Review Questions
- Give a definition of the process and the thread.
- What are the benefits of using threads?
- What are the ways to create threads in Java?
- What are the states of the thread object?
- When threads are terminated or suspended?
- What means are used for threads synchronization?
- In which cases is the thread blocked?
- What is the usage of the
synchronizedmodifier? - What Java libraries are used for creation of GUI applications?
- What is an applet?
- What is the idea of event-driven programming?
- What is JavaFX? What are the benefits of JavaFX?
- What are JavaFX properties, how are they implemented and what opportunities do they provide?
- How the binding between JavaFX objects is implemented?
- What is
ObservableList? - What is FXML? What are the benefits of FXML?
- What is MVC?
- What is the layout and how is it implemented in JavaFX?
- What standard containers does JavaFX provide and how are they different?
- How are the
RadioButtonbuttons working? - What are the features of modal dialog windows?
- How JavaFX uses standard file choose windows?
- How to work with tabular data in JavaFX?
- How to provide editing table cells in JavaFX?
- How to provide the visual editing of the application window of the graphical user interface?