Professional Documents
Culture Documents
Coding in JavaFX
Step by Step
Build Graphics Toolkit
VOLUME
IMAGE VIEWER
in Java 8 (JDK 8u66)
Shufen Kuo
Bing-Chao Huang
Copyright 2016 Shufen Kuo & Bing-Chao Huang.
All rights reserved.
No part of this book may be reproduced or distributed in any form or by any means, or
stored in a database or retrieval system, without the prior written permission from the
authors (Shufen Kuo & Bing-Chao Huang), with the exception that the source codes that
come with the book may be entered and executed in a computer system for learning
purpose, but they may not be reproduced or modified for publication or commercial use
without prior written permission from the authors.
TABLE OF CONTENTS
ABOUT THE AUTHORS
ACKNOWLEDGMENT
PREFACE
How this book is organized
Why Choose This Book
What You Need for This Book
INTRODUCTION
VOLUME : DEVELOPING IMAGE VIEWING COMPONENTS
Chapter 1: Basic Image Viewer
1.1 Create ImageViewer Class as Subclass of Application
1.1.1 JavaFX Application Thread vs. Java Launcher Thread
1.1.2 Catch Resize Event of Image Rendering Area
1.4 Implement Fit Width, Fit Height and Original Size Viewing Options
1.4.1 Approach One: Bind ImageViews fitWidth/fitHeight
Properties to Scenes width/height Properties Respectively
1.4.1.1 Implement Fit Width Viewing Option
1.4.1.2 Implement Fit Height Viewing Option
1.4.1.3 Implement Original Size Viewing Option
1.4.1.4 Complete Source Codes of ImageViewer Class
1.5 Summary
Chapter 2: Enhanced Image Viewer with Browsing Buttons
2.1 Add Next Button
2.1.1 Use Group as Parent Container
2.1.2 Use Shape Class for Rendering 2D Geometric Primitives
2.1.3 Use Rectangle Shape as Bounding Box of Custom-Made
Button
2.1.4 Paint Background and Border of Shape
2.1.5 Set Value of arcHeight and arcWidth Properties to Render
Rounded Rectangle
2.1.6 Use Polygon Shape as Visual Sign of Next Icon
2.1.7 Set Value of Cursor Property for Node
2.1.8 Use Convenience Methods to Register Event Handlers to Handle
Mouse Events
2.1.9 Complete Source Codes of createNextButton() Method
2.1.10 Install Tooltip for Node
2.6 Summary
EPILOGUE
ABOUT THE AUTHORS
Shufen Kuo
Shufen Kuo relocated to San Francisco Bay Area in summer of 1988, and has been a
software engineer ever since. She has extensive hands-on experience with various
platforms, from PC DOS to numerous Unix/Linux workstations, and from PC Linux to PC
Windows.
She started writing Java programs in 2001. She has been using Abstract Windowing
Toolkit (AWT) and Swing, the GUI Components of Java Foundation Classes (JFC), to
develop GUIs for her Java projects for years. And since 2012, she has immersed herself in
JavaFX; exploring its essence inspires her to write books publicizing the strength of
JavaFX.
Before developing Java applications, she had more than 12 years experience in the
development of C/C++ applications on UNIX/Linux/Solaris running X Window system.
Shufen Kuo got her M.S. in Computer Science from Washington State University in 1987.
Currently, she is developing Java applications with rich GUIs using JavaFX, as well as
writing tutorials about coding in JavaFX, utilizing her expertise on designing and
developing object oriented graphics tools.
Her publications include:
Bing-Chao Huang
Dr. Bing-Chao Huang received his Ph.D. in Computer Science from Washington State
University in 1987. He completed an M.S. degree in Computer Science from Stanford
University in 1984.
His publications include:
A useful Summary section is available at the end of each chapter; it lists all the key
aspects of JavaFX library featured in the chapter. It helps you to look up fundamental
capabilities of JavaFX engaged in this book series. Here are some of essentials among
others:
Contents of VOLUME
1. Install Calibre, a free and open source E book Management software, to your PC.
2. Use Add books function in Calibre to add the .mobi file of the e-Book to Calibre
library.
3. Open the book from Calibre and copy the source codes to Java files.
INTRODUCTION
This book series is a tutorial for software developers to build GUIs of Java applications
using JavaFX 8 which has become a part of Java SE Development Kit 8 (JDK 8).
The primary objective of this book series is to provide a comprehensive handbook, which
brings forward the frequently used features and the essence of JavaFX. The usages of
APIs provided in JavaFX packages are illustrated through the step-by-step development of
a sophisticated graphics toolkit.
Complete source codes of the graphics toolkit, a set of packages with reusable classes as
well as embeddable JavaFX applications, are included in the book series. Download and
install JDK 8 before you compile and run these applications. Heres the website to
download the latest version of JDK,
http://www.oracle.com/technetwork/java/javase/downloads/index.html.
JavaFX History
Now let us glance through the timeline of JavaFX evolving history and obtain glimpses of
the predecessor of JavaFX:
Chris Oliver of SeeBeyond Technology Corporation developed a script language called
F3, the acronym for Form Follows Function; it allows developers accessing Swing classes
and creating graphics user interfaces (GUIs) for rapid development of rich internet
applications.
Sun Microsystems acquired SeeBeyond in September 2005, F3 renamed to JavaFX Script
in May 2007.
In December 2008, JavaFX 1.0 released, developers had relied on JavaFX Script to
develop JavaFX applications until three years later when JavaFX 2.0 released.
In January 2010, Oracle completed the acquisition of Sun Microsystems, and continued
maintaining JavaFX.
In September 2010 JavaOne conference, Oracle announced JavaFX Script would be
discontinued.
In October 2011, JavaFX 2.0 released, developers began to engage in standard Java
language, instead of JavaFX Script, to access APIs of JavaFX library. However, JavaFX
SDK and JavaFX Runtime, in addition to Java SE JDK and JRE, must be installed for
developing and executing applications compiled with JavaFX 2.0.
In February 2013, JavaFX 2.2.7 released, JavaFX SDK and JavaFX Runtime are included
within JDK/JRE 7. The integration of JavaFX and JDK results in great conveniency for
JavaFX developers. No additional installations are needed for JavaFX applications
compiled with JDK 7 and later releases.
On March 18, 2014, JavaFX 8 released as part of Java SE Development Kit 8 (JDK 8),
a major feature release.
As of the publishing of this book on March, 2016, the latest JDK releases are:
JDK 8u66 on October 20, 2015, and JDK 8u72 on January 2016.
The schedule of general availability (GA) for JDK 9 is March 23rd, 2017.
Prerequisite
JavaFX applications presented in this book using APIs provided in JavaFX packages to
build GUIs. Knowledge of Swing packages is not a must, however, readers must be
familiar with the basics of Java programming language. If you are new to Java, visit this
website,
http://docs.oracle.com/javase/tutorial/index.html, to obtain preliminary knowledge of Java
technologies. Read Oracle Java online documentation in this order:
1. Getting Started with Simple Java Application Hello World! and Installations:
Visit http://docs.oracle.com/javase/tutorial/getStarted/index.html
2. Concepts and features of the Java Programming Language:
Visit http://docs.oracle.com/javase/tutorial/java/index.html
3. Java Collections Framework:
Visit http://docs.oracle.com/javase/tutorial/collections/index.html
4. Essential Java Classes:
Visit http://docs.oracle.com/javase/tutorial/essential/index.html
A Glimpse of JavaFX Applications
The following example is for readers who are new to JavaFX to have a quick preview of
how to create a JavaFX application.
To get started, you create a class that extends javafx.application.Application, the
entry point of JavaFX applications, and override the start method that is abstract must be
overridden. Heres what a typical start method looks like:
package imageviewer;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
/**
* File Name: ImageViewer.java
*
* @author Shufen Kuo
*/
@Override
public void start(Stage stage) {
BorderPane rootPane = new BorderPane();
StackPane imageArea = new StackPane();
rootPane.getChildren().add(imageArea);
There are two important classes, Scene and Stage, appears within the start method. In
general, the main task is to create graphical user interfaces (GUIs) managed in hierarchical
tree structure called a scene graph, associate the root node of the scene graph to a Scene
object, then place the scene on a Stage object which stands for the top-level container of
the FavaFX application, acting as the window of the scene graph to interact with
application users.
Following snapshot gives you an initial idea of how the GUIs of a JavaFX application we
are going to build looks like:
Figure 1. Snapshot of a JavaFX application.
Nodes
JavaFXs layout panes are containers that automatically perform various types of
placement on nodes. You can place any number of nodes in a layout pane, which is a node
itself and thus can be nested in another layout pane as well. The major advantage of using
layout panes is to avoid the tediousness of manually specifying size and location of each
node. As these nodes in a scene graph are managed in a tree structure, the layout algorithm
recursively computes size and location of each node based on its layout type and specified
layout properties, and the re-calculation is set off dynamically as the windows resize
event happens.
There are 8 types of built-in layout panes, located in javafx.scene.layout package:
BorderPane
A BorderPane places nodes in five locations respectively to create a classic look-
and-feel of main windows, typically the top region for a menu bar or a tool bar, the
bottom region for a status bar, left and right regions for navigation panels, and the
center region for a working area.
HBox
An HBox places nodes in one row (horizontally).
VBox
A VBox places nodes in one column (vertically).
GridPane
A GridPane places nodes in a grid with multiple rows and columns. This is a
handy user interface for displaying a series of name-value pair properties.
StackPane
A StackPane places all nodes in a stack with center as default alignment. All
children overlap each other in the center of the container, with the later created
node placed on the top of previous ones. But you can use static methods,
StackPane.setAlignment(Node child, Pos value) and
StackPane.setMargin(Node child, Insets value), to adjust the mandatory
position of each node.
FlowPane
You can specify orientation property of a FlowPane. For a horizontal FlowPane,
nodes are placed in rows, from left to right, wrapping at the boundary of its
containers width. For a vertical FlowPane, nodes are placed in columns, from top
to bottom, wrapping at the boundary of its containers height.
TilePane
A TilePane is similar to a FlowPane except each cell in the grid of a TilePane
has the same size.
AnchorPane
An AnchorPane places nodes relative to their specified anchor points, there are
four of them: topAnchor, rightAnchor, bottomAnchor and leftAnchor.
Reference to Oracle Online Documentation Working with Layouts in JavaFX:
http://docs.oracle.com/javase/8/javafx/layout-tutorial/index.html
FXML is a markup language which complies with the XML (Extensible Markup
Language) format, with additional rules, for creating user interfaces of JavaFX
applications. Considering hand coding FXML is error prone, a feasible approach is to
generate FXML codes by means of GUI builders.
In this book series, we present a useful application JFXDrawTools, its sort of a GUI
builder specifically for creating instances of subclasses derived from Shape. Users can
interactively create a sketch, either a Shape or an ImageView, manipulate its
properties and save the editing results to files in FXML format, the files can later be
loaded back to continue the editing process, or be loaded by other JavaFX applications
dynamically.
It is considered a good programming practice to separate applications GUIs (the View)
from its business logic (the Model). The FXML file represents the view, while controller is
a java class which passes information between view and model. Constructing GUIs using
FXML enables application developers to adopt the MVC (Model-View-Controller)
architecture effectively.
Reference to Oracle Online Documentation JavaFX: Mastering FXML:
http://docs.oracle.com/javase/8/javafx/fxml-tutorial/index.html
The concept of Properties featured in JavaFX follows the design conventions introduced
in the JavaBeans component, it usually works in connection with Binding mechanism, as a
whole providing powerful yet sleek and succinct solutions to a variety of programming
needs.
In the JFXDrawTools application presented in this book series, we demonstrate the
usage of Properties and Binding considerably. In the implementation of Properties
Window, suitable UI controls, such as text field, choice box, combo box, etc., are used for
viewing the properties of a concerned object. Binding mechanism is employed to bi-
directionally link the UI controls to their respective properties of the object, the
relationship is set up as the Properties Window is about to show. When users changed data
displayed in UI controls, the changes instantly reflect to the corresponding properties of
the related object.
Reference to Oracle Online Documentation JavaFX: Properties and Binding Tutorial:
http://docs.oracle.com/javase/8/javafx/properties-binding-tutorial/index.html
The JavaFX build-in capacity of transformations for nodes are so convenient for
developing graphics packages such as graphics editors, drawing tools, image viewing
applications, GUI builders, etc.
Here are the classes that have dealings with transformations: Translate, Rotate, Scale
and Shear. They are all derived from Transform class and located in
javafx.scene.transform package.
The JavaFX transitions provide concise approaches for creating animations. There are a
host of applications in need of animation features, here are some cases: to draw attention
in an alert situation, to throw objects into recycle bin during the removing operation, to
indicate intermediate state during a data loading,
In the ImageViewer application presented in this book series, we apply
FadeTransition and ParallelTransition onto image objects in a slideshow function to
fade in the new image and fade out the old image simultaneously.
Here are the classes that have dealings with transitions: FadeTransition,
RotateTransition, ScaleTransition, TranslateTransition, FillTransition,
StrokeTransition, PathTransition, ParallelTransition, SequentialTransition,
PauseTransition. They are all derived from Transition class, an abstract class defines
basic functionality of animations, and located in javafx.animation package.
Reference to Oracle Online Documentation JavaFX: Transformations, Animations, and Visual Effects:
http://docs.oracle.com/javase/8/javafx/visual-effects-tutorial/index.html
JavaFX provides a Shape class which extends javafx.scene.Node class and is the
superclass of basic forms of 2D geometry. The object-oriented concept imposed on the
geometric primitives facilitates the development of graphics applications.
Here are classes derived from Shape class: Arc, Circle, CubicCurve, Ellipse, Line,
Path, Polygon, Polyline, QuadCurve, Rectangle, SVGPath, Text. Except the
Text class, all the others are located in javafx.scene.shape package.
Please note that Text class, located in javafx.scene.text package, considered a shape
as well, powers the development of text drawing tools. All features common to JavaFX
nodes can be applied to text objects. You handle it just like any geometric form. Text
objects can be placed in a layout pane to utilize the automatic placement abilities, applying
transformations and transitions, as well as customizing the presentation style using CSS,
etc.
Reference to Oracle Online Documentation Using Text in JavaFX:
http://docs.oracle.com/javase/8/javafx/user-interface-tutorial/text-settings.htm
Chart class, the base class for all chart components, extends
javafx.scene.layout.Region class, and thus inherits all applicable features
from javafx.scene.Node class.
Reference to Oracle Online Documentation JavaFX: Working with JavaFX UI Components Working with
JavaFX Charts:
http://docs.oracle.com/javase/8/javafx/user-interface-tutorial/charts.htm
The following UI components are usually described in the same category with the above
UI controls.
Tooltip
resides in javafx.scene.control package, but is derived from
javafx.scene.control.PopupControl class.
MenuItem, Menu, CheckMenuItem, RadioMenuItem
resides in javafx.scene.control package. The MenuItem which extends
java.lang.Object is the base class of the rest three.
FileChooser
resides in javafx.stage package and extends java.lang.Object.
Reference to Oracle Online Documentation JavaFX: Working with JavaFX UI Components:
http://docs.oracle.com/javase/8/javafx/user-interface-tutorial/ui_controls.htm
Chapter 1
Basic Image Viewer
Developing Image Viewer V1.0
Preface
In this chapter, well illustrate usages of JavaFX APIs by means of step-by-step
development of an image viewing application, ImageViewer V1.0. From detailed
descriptions and well-documented example codes excerpted from ImageViewer V1.0x,
you will become familiar with fundamental capabilities of JavaFX library recounted in the
Summary section.
The complete development process of ImageViewer V1.0 is characterized by the
following steps:
1.4 Implement Fit Width, Fit Height and Original Size Viewing Options
1.4.1 Approach One: Bind ImageViews fitWidth/fitHeight Properties to
Scenes width/height Properties Respectively
1.4.1.1 Implement Fit Width Viewing Option
1.4.1.2 Implement Fit Height Viewing Option
1.4.1.3 Implement Original Size Viewing Option
1.4.1.4 Complete Source Codes of ImageViewer Class
1.4.2 Approach Two: Change Values for fitWidth Property and fitHeight
property
1.4.2.1 Catch Resize Event of Scene to Adjust Viewing Size of Image
1.4.2.2 Complete Source Codes of ImageViewer Class
1.5 Summary
1.1 Create ImageViewer Class as Subclass of
Application
The graphical user interfaces of a JavaFX application are managed in hierarchical tree
structure, called a scene graph. In ImageViewer application, we employ a
BorderPane as the root node of a scene graph, and place a StackPane in the center of
BorderPane as image rendering area. Both BorderPane and StackPane are among
build-in layout panes, reside in javafx.scene.layout package and are direct subclasses
of javafx.scene.layout.Pane class.
BorderPane class allows you to create graphical user interfaces featuring classic look-
and-feel of main windows. In the next section youll see a menu bar added at the top of
BorderPane, and in the later chapter a status bar added at the bottom.
StackPane class lays out its children in a stack, placing the later created child at the top
of previous one. In ImageViewer application, since only one image is viewed at one
time, the StackPane has only one child. If the size of an image is smaller than the size of
StackPane, the image is placed in the center of StackPane. If the size of an image is
larger than that of StackPane, StackPane is expanded automatically to accommodate
the whole image, and if the viewing space in the application window is not larger enough,
only center portion of the image is visible by default. In later section, well demonstrate
how to avoid a StackPane being resized by its content, see here.
A Scene object is created as the container of a scene graph, given a root node as
parameter. In ImageViewer application, the root node is a BorderPane, and initial
width and height of the Scene object is set to 600 pixels and 400 pixels. Both
BorderPane and StackPane are resizable layout panes; therefore, if the application
window is dynamically resized by a user, the scene is resized and a resize event is passed
on to its root node, resulting in re-calculation of the size of image rendering area
accordingly.
You place a scene on a Stage to make it alive, using the setScene method defined in
Stage class. Stage is a direct subclass of javafx.stage.Window; it is the top-level
window of a JavaFX application.
JavaFX system creates a Stage object, referred as primary stage, and passed to the start
method, an abstract method defined in Application class. Developers can construct
additional Stage objects as needed for their applications.
The following codes demonstrate how to create ImageViewer class as a direct subclass
of Application, and override the start method. Here are codes:
Listing 1-1. Create ImageViewer class which extends Application class, override
start method, create BorderPane as the root node of a scene, and place StackPane in
the center of BorderPane as image rendering area:
package imageviewer;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
/**
* File Name: ImageViewer.java
*
* This application creates a BorderPane as root node of a scene, and
* add a StackPane in the center of BorderPane
* The initial size of the scene is set to 600 by 400
* The size of StackPane will be automatically set to fit the size of scene.
* StackPane will be resized when window size is adjusted by a user
*/
@Override
public void start(Stage stage) {
BorderPane rootPane = new BorderPane();
StackPane imageArea = new StackPane();
rootPane.setCenter(imageArea);
Application class is the entry point of a JavaFX application. It defines the following
significant methods:
The init method, which does nothing, is called from Java launcher thread. While the
start method, an abstract and must be overridden, is called from JavaFX Application
Thread after init method has returned. If you need do some initial works before start
method, you can do that by overriding init method; however it is crucial to notice that
nodes that are members of a live scene must be accessed from JavaFX Application
Thread. And the construction of a Scene object or a Stage object must be accessed
from JavaFX Application Thread as well.
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
Listing 1-2. Add listeners to width property and height property of image rendering area:
imageArea.widthProperty().addListener(new ChangeListener<Object>() {
public void changed(ObservableValue o, Object oldVal,
Object newVal) {
System.out.format("imageArea width changed from %.1f to %.1f%n",
oldVal, newVal);
}
});
imageArea.heightProperty().addListener(new ChangeListener<Object>() {
public void changed(ObservableValue o, Object oldVal,
Object newVal) {
System.out.format("imageArea height changed from %.1f to %.1f%n",
oldVal, newVal);
}
});
Listing 1-3. Complete source codes of ImageViewer class become like this:
package imageviewer;
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
/**
* File Name: ImageViewer.java
* (in directory: ImageViewer1.0/src/example1/imageviewer/)
* This application creates a BorderPane as root node of a scene, and
* add a StackPane in the center of BorderPane
* The initial size of the scene is set to 600 by 400
* The size of StackPane will be automatically set to fit the size of scene.
* StackPane will be resized when window size is adjusted by a user
*
* @author Shufen Kuo
*/
@Override
public void start(Stage stage) {
BorderPane rootPane = new BorderPane();
StackPane imageArea = new StackPane();
rootPane.setCenter(imageArea);
imageArea.widthProperty().addListener(new ChangeListener<Object>() {
public void changed(ObservableValue o, Object oldVal,
Object newVal) {
System.out.format("imageArea width changed from %.1f to %.1f%n",
oldVal, newVal);
}
});
imageArea.heightProperty().addListener(new ChangeListener<Object>() {
public void changed(ObservableValue o, Object oldVal,
Object newVal) {
System.out.format("imageArea height changed from %.1f to %.1f%n",
oldVal, newVal);
}
});
stage.setTitle("ImageViewer V1.0");
stage.setScene(scene);
stage.show();
}
F:\My Documents\DocRoot\SHUFEN_JAVA\MyBookJFX\VolumeOne\ImageViewer1.0\src\exampl
javac -encoding utf-8 -Xlint:unchecked imageviewer\*.java
To add a Menu to a MenuBar, first we call the getMenus() method, which returns an
ObservableList of Menus. The ObservableList is a subinterface of java.util.List,
thus we can use the add method, one of the basic utilities defined in Java Collection
interface, to append a menu to the list. The codes are as follows:
Listing 1-4. Add a Menu to a MenuBar:
In this application at this point, MenuBar contains a File menu. The File menu contains
two menu items: Load and Exit. The following snippets of codes show how to
accomplish these tasks.
Import Statements:
import javafx.application.Platform;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
Listing 1-6. Add a menu bar to ImageViewer application, place it at the top of root
node which is a BorderPane:
1.2.2 Select Image File from File Open Dialog and Display Image on
StackPane
import java.io.File;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.stage.FileChooser;
Listing 1-7. Create FileChooser Object, ImageView Object and add EventHandler
for Load menu Item to load an image:
Please remember to declare the variable stage as final since it is accessed within inner
class, the anonymous EventHandler class of Load menu item.
Now, we need to deal with a significant matter regarding the resizability of image
rendering area, the StakePane. To avoid resizing the StakePane when displaying an
image of larger size, we need to set minimum size for the StakePane. Heres the code to
do this:
imageArea.setMinSize(0, 0);
Without adding the above line of code, the size of StakePane is changeable when
loading an image with size larger than the current size of StakePane. Heres an
example:
Suppose the current size of StakePane is 600 by 375; after displaying an image of size
1280 by 960, the size of StakePane is enlarged to become 1280 by 960; however, the
viewable portion is still 600 by 375.
Setting minimum size for the StakePane results in no size changing event occurred
whenever a new image is opened, and also simplify the codes to be developed in the
following chapters.
Listing 1-8. Complete source codes of ImageViewer class become like this:
package imageviewer;
import java.io.File;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
/**
* File Name: ImageViewer.java
* (in directory: ImageViewer1.0/src/example2/imageviewer/)
* This application creates a BorderPane as root node of a scene, and
* add a StackPane in the center of BorderPane
* The initial size of the scene is set to 600 by 400
* The size of StackPane will be automatically set to fit the size of scene.
* StackPane will be resized when window size is adjusted by a user
*
* Add a menu bar in the top of BorderPane
* Create File Menu, and using FileChooser's Open Dialog to
* select an image file, load and display the image in image rendering area.
*
* @author Shufen Kuo
*/
@Override
public void start(final Stage stage) {
BorderPane rootPane = new BorderPane();
StackPane imageArea = new StackPane();
// Set min size for StackPane to avoid resizing
// when loading large size image
imageArea.setMinSize(0, 0);
rootPane.setCenter(imageArea);
stage.setTitle("ImageViewer V1.0");
stage.setScene(scene);
stage.show();
}
Compile and run the application. Click Load menu item in File menu, navigate to a
directory that contains image files, select an image file, click Open button in
FileChoosers Open Dialog, the image file will be loaded and displayed in the center of
application window.
Figure 1-2. Snapshot of menu bar with File menu when it is clicked
Figure 1-3. Snapshot of selecting an image file from file chooser
Fit Width
Render an image on an area by scaling fitWidth property of an ImageView,
while preserving original width/height ratio, to fit the width of current available
image viewing space.
Fit Height
Render an image on an area by scaling fitHeight property of an ImageView,
while preserving original width/height ratio, to fit the height of current available
image viewing space.
Original Size
Render an image in its original size, the outer portion of image that exceeds the
boundary of current available viewing space will be blocked. This is set as the
initial viewing option.
Please note that the available image viewing space is determined by current width and
height of the application window, called a Stage, which subsequently is subject to the
current size of a Scene, the container of a scene graph. Initially the scene is set to 600
pixels by 400 pixels, thus, the size of available image rendering space will be same as the
size of the scene. After we added a menu bar at the top of BorderPane, which is the root
node of the scene, the height of available image rendering space becomes height of scene
minus height of menu bar, while the width remains the same.
1.3.1 Create Toggle Group, Radio Menu Items and Add Listener for
selectedToggle Property
Listing 1-10. Complete source codes of ImageViewer class become like this:
package imageviewer;
import java.io.File;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.control.RadioMenuItem;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
/**
* File Name: ImageViewer.java
* (in directory: ImageViewer1.0/src/example3/imageviewer/)
* This application creates a BorderPane as root node of a scene, and
* add a StackPane in the center of BorderPane
* The initial size of the scene is set to 600 by 400
* The size of StackPane will be automatically set to fit the size of scene.
* StackPane will be resized when window size is adjusted by a user
*
* Add a menu bar in the top of BorderPane
* Create File Menu, and using FileChooser's Open Dialog to
* select an image file, load and display the image in image rendering area.
*
* Create Option menu contains View submenu, let a user chooses image viewing
* criteria: Fit Width, Fit Height, or the default option Original Size.
*
* @author Shufen Kuo
*/
@Override
public void start(final Stage stage) {
BorderPane rootPane = new BorderPane();
StackPane imageArea = new StackPane();
// Set min size for StackPane to avoid resizing
// when loading large size image
imageArea.setMinSize(0, 0);
rootPane.setCenter(imageArea);
imageArea.widthProperty().addListener(new ChangeListener<Object>() {
public void changed(ObservableValue o, Object oldVal,
Object newVal) {
System.out.format("imageArea width changed from %.1f to %.1f%n",
oldVal, newVal);
}
});
imageArea.heightProperty().addListener(new ChangeListener<Object>() {
public void changed(ObservableValue o, Object oldVal,
Object newVal) {
System.out.format("imageArea height changed from %.1f to %.1f%n",
oldVal, newVal);
}
});
// Add menu bar
MenuBar menuBar = new MenuBar();
Menu menuFile = new Menu("File");
MenuItem load = new MenuItem("Load");
MenuItem exit = new MenuItem("Exit");
menuFile.getItems().addAll(load, exit);
menuBar.getMenus().add(menuFile);
rootPane.setTop(menuBar);
stage.setTitle("ImageViewer V1.0");
stage.setScene(scene);
stage.show();
}
F:\My Documents\DocRoot\SHUFEN_JAVA\MyBookJFX\VolumeOne\ImageViewer1.0\src\exampl
javac -encoding utf-8 -Xlint:unchecked imageviewer\*.java
Now, lets explore two approaches to render image according to the viewing option
currently selected.
We use bind and unbind methods defined in Property interface to implement Fit
Width, Fit Height, and Original Size viewing options.
What enticing here is that, once the binding becomes effective, an images viewing width
and/or height will be changed accordingly whenever width and/or height of Scene is
changed dynamically.
1.4.1.1 Implement Fit Width Viewing Option
If a user selects Fit Width as viewing option, well have fitWidth property of
ImageView bind with width property of Scene, and unbind fitHeight property. Here
are codes:
Listing 1-12. Bind fitWidth property of ImageView object with width property of
Scene:
imageView.fitWidthProperty().bind(scene.widthProperty());
imageView.fitHeightProperty().unbind();
// Besides unbind, must also set fit height to 0
imageView.setFitHeight(0);
If Original Size is selected as viewing option, well have fitWeight property as well as
fitHeight property of the ImageView object unbind. Here are codes:
Listing 1-14. Unbind fitWidth property as well as fitHeight property of the
ImageView object:
imageView.fitWidthProperty().unbind();
imageView.fitHeightProperty().unbind();
// Besides unbind, must also set fit height/height to 0
imageView.setFitWidth(0);
imageView.setFitHeight(0);
...
...
groupOption.selectedToggleProperty().addListener(new ChangeListener<Toggle>() {
public void changed(ObservableValue<? extends Toggle> ov,
Toggle old_toggle, Toggle new_toggle) {
if (groupOption.getSelectedToggle() != null) {
RadioMenuItem choiceItem = (RadioMenuItem) groupOption.getSelectedToggle();
if (choiceItem == fitWidth) {
System.out.println("fitwidth");
imageView.fitWidthProperty().bind(scene.widthProperty());
imageView.fitHeightProperty().unbind();
// Besides unbind, must also set fit height to 0
imageView.setFitHeight(0);
} else if (choiceItem == fitHeight) {
System.out.println("fitheight");
// Available height for rendering image is
// subtract menubar's height from scene height
imageView.fitHeightProperty().bind(
scene.heightProperty().subtract(menuBar.getHeight()));
imageView.fitWidthProperty().unbind();
// Besides unbind, must also set fit width to 0
imageView.setFitWidth(0);
} else {
System.out.println("original size");
imageView.fitWidthProperty().unbind();
imageView.fitHeightProperty().unbind();
// Besides unbind, must also set fit height/height to 0
imageView.setFitWidth(0);
imageView.setFitHeight(0);
}
}
}
});
Complete source codes of ImageViewer class for Approach One become these:
Listing 1-16. Complete source codes of ImageViewer class become like this:
package imageviewer;
import java.io.File;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.control.RadioMenuItem;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
/**
* File Name: ImageViewer.java
* (in directory: ImageViewer1.0/src/example4/imageviewer/)
* This application creates a BorderPane as root node of a scene, and
* add a StackPane in the center of BorderPane
* The initial size of the scene is set to 600 by 400
* The size of StackPane will be automatically set to fit the size of scene.
* StackPane will be resized when window size is adjusted by a user
*
* Add a menu bar in the top of BorderPane
* Create File Menu, and using FileChooser's Open Dialog to
* select an image file, load and display the image in image rendering area.
*
* Create Option menu contains View submenu, let a user chooses image viewing
* criteria: Fit Width, Fit Height, or the default option Original Size.
*
* Adjust image viewing size using bind and unbind methods:
* Bind fitWidth/fitHeight of image view to width/height of scene,
* so that, size of image view will be changed automatically
* whenever scene is resized
*
* @author Shufen Kuo
*/
@Override
public void start(final Stage stage) {
BorderPane rootPane = new BorderPane();
final StackPane imageArea = new StackPane();
// Set min size for StackPane to avoid resizing
// when loading large size image
imageArea.setMinSize(0, 0);
rootPane.setCenter(imageArea);
// Create image view, add to image area
final ImageView imageView = new ImageView();
imageView.setPreserveRatio(true);
imageArea.getChildren().add(imageView);
imageArea.widthProperty().addListener(new ChangeListener<Object>() {
public void changed(ObservableValue o, Object oldVal,
Object newVal) {
System.out.format("imageArea width changed from %.1f to %.1f%n",
oldVal, newVal);
}
});
imageArea.heightProperty().addListener(new ChangeListener<Object>() {
public void changed(ObservableValue o, Object oldVal,
Object newVal) {
System.out.format("imageArea height changed from %.1f to %.1f%n",
oldVal, newVal);
}
});
// Add menu bar
final MenuBar menuBar = new MenuBar();
Menu menuFile = new Menu("File");
MenuItem load = new MenuItem("Load");
MenuItem exit = new MenuItem("Exit");
menuFile.getItems().addAll(load, exit);
menuBar.getMenus().add(menuFile);
rootPane.setTop(menuBar);
}
}
});
// Create View submenu for Option menu
Menu menuView = new Menu("View");
menuView.getItems().addAll(fitWidth, fitHeight, original);
// Set Original Size as default choice
original.setSelected(true);
stage.setTitle("ImageViewer V1.01");
stage.setScene(scene);
stage.show();
}
Please notice that we upgrade version number from V1.0 to V1.01 for approach one.
Compile and run the application. Testing viewing options as follows: First, load a small
size image, select all possible viewing options back and forth, also dynamically resize the
application window using mouse-dragging the edge or corner of the window. Then, load a
larger size image and test all the possible criteria again.
Figure 1-7. Snapshot of rendering an image in original size (320 pixels by 363 pixels).
Figure 1-8. Snapshot of rendering an image scaled up to fit width of scene.
Figure 1-9. Snapshot of rendering an image scaled up to fit available viewing space of
scene height.
1.4.2 Approach Two: Change Values for fitWidth Property and
fitHeight Property
ImageView class provides two properties that allow us to scale the viewing size of an
image:
fitWidth
fitHeight
To change value for these two properties, we use the setFitWidth method and the
setFitHeight method.
If Fit Width viewing option is selected, the viewing width will be set in accordance with
the current width of scene. Heres the snippet of codes:
Listing 1-17. Call setFitWidth method to set image viewing width same as the width of
scene:
imageView.setFitWidth(scene.getWidth());
// Besides setFitWidth, must also set fit height to 0
imageView.setFitHeight(0);
If Fit Height viewing option is selected, the viewing height will be set in accordance
with the current height of scene, subtracting the height of menu bar. Heres snippet of
code:
Listing 1-18. Call setFitHeight method to set the image viewing height same as the
height of scene minus the height of menu bar:
imageView.setFitHeight(scene.getHeight() - menuBar.getHeight());
// Besides setFitHeight, must also set fit width to 0
imageView.setFitWidth(0);
If Original Size option is selected, we need to clear values set previously by giving 0 as
value to both fitWidth property and fitHeight property. Heres the snippet of codes:
Listing 1-19. Call setFitHeight and setFitHeight methods, given 0 as the value:
To complete approach two, there is one more issue to be resolved: the viewing size of
image is subject to the size of scene, initially set to 600 pixels by 400 pixels, which might
be changed dynamically. When a user drags the edge or corner of the application window,
the scene is resized. We need to catch this resize event to adjust the viewing size of an
image accordingly. That is, if current selected viewing option is Fit Width, the new
width of scene will be adopted as the value of fitWidth property of the image view
object. And, if Fit Height is the selected one, the new height of scene minus height of
menu bar will be adopted as the value of fitHeight property.
To response to the change of scenes size, we add a listener for width Property and
height property respectively to catch the resize event of scene.
The following snippets of codes accomplish these tasks:
Listing 1-21. Listen to the change of scenes width and height, adjust fitWidth and
fitHeight of an image view:
// Listen to the width change of scene and adjust fitWidth of image view
scene.widthProperty().addListener(new ChangeListener<Object>() {
public void changed(ObservableValue o, Object oldVal, Object newVal) {
if (fitWidth.isSelected()) {
imageView.setFitWidth((double)newVal);
}
}
});
// Listen to the height change of scene and adjust fitHeight of image view
scene.heightProperty().addListener(new ChangeListener<Object>() {
public void changed(ObservableValue o, Object oldVal, Object newVal) {
if (fitHeight.isSelected()) {
imageView.setFitHeight((double) newVal - menuBar.getHeight());
}
}
});
Complete source codes of ImageViewer class for Approach Two become these:
Listing 1-22. Complete source codes of ImageViewer become as follows:
package imageviewer;
import java.io.File;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.control.RadioMenuItem;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
/**
* File Name: ImageViewer.java
* (in directory: ImageViewer1.0/src/example5/imageviewer/)
* This application creates a BorderPane as root node of a scene, and
* add a StackPane in the center of BorderPane
* The initial size of the scene is set to 600 by 400
* The size of StackPane will be automatically set to fit the size of scene.
* StackPane will be resized when window size is adjusted by a user
*
* Add a menu bar in the top of BorderPane
* Create File Menu, and using FileChooser's Open Dialog to
* select an image file, load and display the image in image rendering area.
*
* Create Option menu contains View submenu, let a user chooses image viewing
* criteria: Fit Width, Fit Height, or the default option Original Size.
*
* Adjust image viewing size using setFitWidth method and setFitHeight method
* provided in ImageView class
* Add listeners for scene's width property and height property
* Responds to the dynamic resize event of scene by scaling the image view when
* viewing option is set as Fit Width or Fit Height
*
* @author Shufen Kuo
*/
@Override
public void start(final Stage stage) {
BorderPane rootPane = new BorderPane();
final StackPane imageArea = new StackPane();
// Set min size for StackPane to avoid resizing
// when loading large size image
imageArea.setMinSize(0, 0);
rootPane.setCenter(imageArea);
// Create image view, add to image area
final ImageView imageView = new ImageView();
imageView.setPreserveRatio(true);
imageArea.getChildren().add(imageView);
imageArea.widthProperty().addListener(new ChangeListener<Object>() {
public void changed(ObservableValue o, Object oldVal,
Object newVal) {
System.out.format("imageArea width changed from %.1f to %.1f%n",
oldVal, newVal);
}
});
imageArea.heightProperty().addListener(new ChangeListener<Object>() {
public void changed(ObservableValue o, Object oldVal,
Object newVal) {
System.out.format("imageArea height changed from %.1f to %.1f%n",
oldVal, newVal);
}
});
// Add menu bar
final MenuBar menuBar = new MenuBar();
Menu menuFile = new Menu("File");
MenuItem load = new MenuItem("Load");
MenuItem exit = new MenuItem("Exit");
menuFile.getItems().addAll(load, exit);
menuBar.getMenus().add(menuFile);
rootPane.setTop(menuBar);
// Listen to the width change of scene and adjust fitWidth of image view
scene.widthProperty().addListener(new ChangeListener<Object>() {
public void changed(ObservableValue o, Object oldVal,
Object newVal) {
if (fitWidth.isSelected()) {
imageView.setFitWidth((double)newVal);
}
}
});
// Listen to the height change of scene and adjust fitHeight of image view
scene.heightProperty().addListener(new ChangeListener<Object>() {
public void changed(ObservableValue o, Object oldVal,
Object newVal) {
if (fitHeight.isSelected()) {
imageView.setFitHeight((double) newVal - menuBar.getHeight());
}
}
});
// Create View submenu for Option menu
Menu menuView = new Menu("View");
menuView.getItems().addAll(fitWidth, fitHeight, original);
// Set Original Size as default choice
original.setSelected(true);
Please notice that we upgrade the version number from V1.01 to V1.02 for approach two.
Compile and run the application. Try the following steps to test the application:
1. Load an image with size smaller than the initial scenes size (600 pixels by 400
pixels), view the image in its original size.
2. Select Fit Width option, the image view is scaled up to fit the width of scene.
3. Select Fit Height option, the image view is scaled up to fit the height of scene.
4. Select Original Size option, the image view is back to its original size.
5. Load a larger size image, larger than the current scenes size, since there is not
enough space in the scene to view the whole image, you will only see the center
portion of the image.
6. Select Fit Width and Fit height options respectively to see the different image
viewing result.
7. Resize scene width by mouse dragging the window edge, try both enlarging and
reducing width to see the different results.
8. Resize scene height by mouse dragging the window edge, try both enlarging and
reducing height to see the different results.
9. Resize scene width and height by mouse dragging the window corner, testing all
three viewing options respectively.
The approach one (V1.01) is adopted to continue the development of ImageViewer V1.1
in the following chapters.
Figure 1-10. Snapshot of viewing image in its original size (1280 pixels by 960 pixels),
outer portion of image is blocked.
Figure 1-11. Snapshot of image view scaled down to fit width of scene, a few portions of
image, from top and from bottom, are blocked.
Figure 1-12. Snapshot of image view scaled down to fit height of available space in scene
window.
Figure 1-13. Snapshot of image view scaled to fit width while scenes height is enlarged
dynamically.
Figure 1-14. Snapshot of image view scaled to fit height while scenes size is changed
dynamically.
Output from Compiling and Running ImageViewer in command line:
F:\My Documents\DocRoot\SHUFEN_JAVA\MyBookJFX\VolumeOne\ImageViewer1.0\src\exampl
javac -encoding utf-8 -Xlint:unchecked imageviewer\*.java
Preface
In previous chapter, we have demonstrated the incremental development of a JavaFX
application, ImageViewer V1.0. So far it contains minimum capabilities, nevertheless
is a ready-to-use and convenient accessory, easily be bundled with a variety of software.
Additional functions will be added gradually throughout this chapter and the next chapter.
This chapter features the implementation of actions as well as proper placement for two
buttons, Next and Previous, two custom-made buttons to open and view the next or
previous available image.
The version number is upgraded to 1.1. From detailed descriptions and well-documented
example codes excerpted from ImageViewer V1.1, you will become familiar with
fundamental capabilities of JavaFX library recounted in the Summary section.
The complete development process of ImageViewer V1.1 is characterized by the
following steps:
2.6 Summary
2.1 Add Next Button
Instead of using Button class from built-in JavaFX UI controls, we implement our own
utilities to design a custom-made button by employing the below classes:
Group
Served as the parent container.
Shape
Rectangle
Served as the bounding box.
Polygon
Served as the visual sign of Next icon.
JavaFX provides a very enticing class, Shape, for rendering 2D geometric primitives. It
is an abstract class which extends javafx.scene.Node class and serves as the base class
of some of basic forms of 2D geometry. Here are its direct subclasses: Arc, Circle,
CubicCurve, Ellipse, Line, Path, Polygon, Polyline, QuadCurve, Rectangle,
SVGPath, Text. The Shape class defines common properties of 2D geometric
primitives, such as fill, stroke, strokeType, strokeWidth, etc.
It is very common to set value of fill and stroke properties respectively for a shape. The
setFill method is used to specify a color to render the background of a shape, and
setStroke method is used to specify a color to render the border of a shape. Please
notice that the default value of fill property is null for Line, Polyline, and Path, while
Color.BLACK for the others; and default value of stroke property is Color.BLACK for
Line, Polyline, and Path, while null for the others.
Now well show you how to exploit capacities of Shape to implement our custom-made
Next button.
Here are codes to accomplish these: a Rectangle is created using the constructor with
parameters: x, y, width and height, and proper value is set for each desired property.
Listing 2-1. Create a Rectangle and set value of these properties: fill, stroke,
arcHeight, arcWidth:
The following snapshot shows the rendering of a Rectangle from the above codes:
Figure 2-1. Snapshot of a rounded rectangle filled with LinearGradient color.
is one of many static methods provided in Color class for easily creating Color objects.
These are examples of some of the supported format for the colorString parameter, they all
create the same color:
"#5173A8"
"0x5173A8"
"rgb(81,115,168)"
The color string #5173A8, or 0x5173A8 is the hex representation of RGB value (81,
115,168).
A Color has a default alpha value, 1.0, representing completely opaque. You can
explicitly specify alpha value, in the range of 0.0 to 1.0, when constructing a Color. An
alpha value of 0.0 means the color is completely transparent. Here are many ways to
create the same color with alpha value 0.5:
Learn more about the topic of color in later volumes of this book series.
arcHeight and arcWidth are properties defined in Rectangle class, for specifying
height and width diameters of arcs at four corners of a rectangle shape. Their default value
is 0.0.
The following figure illustrates how to set correct value for arcHeight and arcWidth
properties for rendering a rounded rectangles. The circles and ellipses, overlapping with
the arcs at the corner of rounded rectangles, are for easy comparison.
Figure 2-2. Snapshot of illustrations of rounded rectangles with circles and ellipses placed
at the corner of rectangles.
Instead of using image as visual sign of Next button, a Polygon of arrow shape is
instantiated. The getPoints() method is used to obtain a list which is type of
ObservableList<Double>, then the basic utilities, defined in Collection interface, is
used to manipulate the content of the list. A series of x and y coordinates, stored in an
array of Double value, are added to the list, representing points of the Polygon.
Color.WHITE, one of many static fields of type Color, defined in Color class, is the
color we picked to fill the background of the Polygon, and a dark blue color is created,
using the static method Color.web, to paint the border of the Polygon.
Here is the snippet of codes to accomplish these tasks:
Listing 2-2. Create a Polygon and set value for properties fill and stroke:
The following snapshot shows the rendering of a Polygon from the above codes:
Figure 2-3. Snapshot of the polygon as Next icon.
The cursor property, defined in Node class, allows you to change mouse curser for this
node and all its descendants. The Cursor class, resides in javafx.scene package and
extends java.lang.Object, defines some applicable mouse cursors as static fields of
Cursor type such as CLOSED_HAND, CROSSHAIR, DEFAULT, HAND,
OPEN_HAND, TEXT, WAIT, etc. If value of cursor property is not explicitly set, the
default mouse cursor, an arrow shape, is used.
We employ Cursor.OPEN_HAND as mouse curser of a Next button. Heres the snippet
of codes:
Group buttonGroup = new Group();
buttonGroup.setCursor(Cursor.OPEN_HAND);
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.control.Tooltip;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Polygon;
import javafx.scene.shape.Rectangle;
/**
* Create next button using a Rectangle as background,
* a Polygon as icon, a Group as container,
* size of Rectangle is 25 by 35,
* thickness of polygon is 8 pixels,
* padding of polygon is (2, 2, 2, 5),
* change rectangle appearance when mouse entered.
*
* @return
*/
Node createNextButton() {
// Create a group as button
Group buttonGroup = new Group();
// Set cursor
buttonGroup.setCursor(Cursor.OPEN_HAND);
buttonGroup.getChildren().addAll(rect, polygon);
return buttonGroup;
}
Since the Next button is a custom-made node acting like a button, not derived from
javafx.scene.control.Control class, well use install method, provided in Tooltip
class, to associate a Tooltip with the Next button. Heres the install method:
The following snippets of codes show the creation of a Next node by calling
createNextButton() method, install a Tooltip for the node, and add it to the children
list of a StackPane which is the image rendering area.
Import Statement:
import javafx.scene.control.Tooltip;
Listing 2-5. Create a Next button, install a Tooltip and add the node to image rendering
area:
...
// Install tooltip
Tooltip t = new Tooltip("Next Image");
Tooltip.install(nextButton, t);
// Add next button to stack pane
imageArea.getChildren().add(nextButton);
The following snapshots show a Next button laid out in its default location, the center of
a StackPane.
Figure 2-4. Snapshot of a Next button placed in the default position.
Figure 2-5. Snapshot of a Next button placed in the default position, it is taken as mouse
entered the button, the filled color gradient of a rectangle is changed and the installed
tooltip is popped up.
(Figures 2-1 to 2-5 are snapshots of applications using jdk1.7.0_25 in Windows XP; the
rest are using jdk1.8.0_66 in Windows 8.1)
2.2 Adjust Next Buttons Default Position in StackPane
In this section, we will demonstrate how to place a child node of a StackPane in the
desired location.
A StackPane places all its children in a stack with the center as default alignment. That
is, by default StackPanes children overlap each other in the center of the pane.
Placing a Next button in the center of a StackPane is not feasible in our application.
Fortunately, StackPane provides two static methods:
allowing us to adjust individual child nodes position. The preferred position of a Next
button is on the right side of the window, within the visible boundary of a scene. The
following snippets of codes adjust the default location of a next button.
Import Statement:
import javafx.geometry.Insets;
import javafx.geometry.Pos;
Listing 2-6. Use StackPanes static Methods setAlignment and setMargin to adjust
the default position of a Next button:
StackPane.setAlignment(nextButton, Pos.CENTER_RIGHT);
StackPane.setMargin(nextButton, new Insets(8));
The following snapshot shows a Next button positioned on the right side of application
window.
Figure 2-6. Snapshot of a Next button on the right side of window, within the visible
boundary of a scene.
Remember that, we set minimum size of StakePane to (0, 0) to avoid the automatic
changing of its size, by its child node ImageView. Failure to do so may lead to complex
situation regarding the position of Next button whenever an image larger than the visible
boundary of the StackPane is loaded and displayed. The setting simplifies the solution
of adjusting the position of Next button.
The next snapshot shows a large size image displayed by its original size, the portions of
image that exceed the boundary of StakePane are invisible. Had we not set minimum
size for the StakePane, then its size would have been enlarged by its contents, and the
Next button would have been fallen beyond the visible boundary.
Figure 2-7. Snapshot of Next button positioned on the right side of window after
dynamically resizing the application window.
2.3 Add Previous Button and Adjust Its Position in
StackPane
In this section, a Previous button is added and placed on the left side of the
StakePane. The alignment is set to Pos.CENTER_LEFT, and the left margin is set to 8
pixels. The procedures are similar to how we create the Next button and adjust its
position described in previous sections.
Listing 2-7. Define createPrevButton method to create a Previous button:
/**
* Create previous button using a Rectangle as background,
* a Polygon as icon, a Group as container,
* size of Rectangle is 25 by 35,
* thickness of polygon is 8 pixels,
* padding of polygon is (2, 5, 2, 5),
* change rectangle appearance when mouse entered.
*
* @return
*/
Node createPrevButton() {
// Set cursor
buttonGroup.setCursor(Cursor.OPEN_HAND);
buttonGroup.getChildren().addAll(rect, polygon);
return buttonGroup;
}
Newly added codes for creating Previous button are shown in bold face in the following
snippets of codes.
Listing 2-8. Codes added to the entry point of ImageViewer V1.1 for creating
Previous button:
...
...
// Install tooltip for nextButton
Tooltip t = new Tooltip("Next Image");
Tooltip.install(nextButton, t);
...
Figure 2-8. Snapshot of ImageViewer with Next button and Previous button,
positioned within the visible boundary of a StackPane
2.4 Implement On Mouse Clicked Event Handlers
In section Use Convenience Methods to Register Event Handlers to Handle Mouse Events,
we have demonstrated how to register an event handler for mouse clicked events. In this
section, well register the event handlers and implement the handle methods which are
invoked when mouse clicked on the buttons. If Next button is clicked, the image file next
to the current loaded file will be loaded, and if Previous button is clicked, the image file
before the current loaded file will be loaded.
Before we proceed these tasks properly, well configure a FileChooser to show only
files whose extensions matching the specified filters, and establish a list of files existing in
the current directory, the list is created whenever an image file is opened from the File
Open Dialog of a FileChooser.
package imageviewer;
import javafx.stage.FileChooser;
/**
* File Name: FileUtils.java
* (in directory: ImageViewer1.1/src/example4/imageviewer/)
*
* @author Shufen Kuo
*/
/**
* Define file extension filters to filtering supported image files,
* matching files whose extensions are:
* .bmp, .gif, .jpeg, .jpg and .png
* @param fileChooser
*/
static void configure(FileChooser fileChooser) {
fileChooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("All Images",
"*.bmp", "*.gif", "*.jpeg", "*.jpg", "*.png"),
new FileChooser.ExtensionFilter("All Files", "*.*"),
new FileChooser.ExtensionFilter("BMP", "*.bmp"),
new FileChooser.ExtensionFilter("GIF", "*.gif"),
new FileChooser.ExtensionFilter("JPEG", "*.jpeg"),
new FileChooser.ExtensionFilter("JPG", "*.jpg"),
new FileChooser.ExtensionFilter("PNG", "*.png"));
// Set "." as default dir
fileChooser.setInitialDirectory(new File("."));
}
}
The functionality of a FileChooser and its appearance depend on the operation system.
Figure 2-9 shows a FileChooser in Windows XP, and Figure 2-10 shows a
FileChooser in Windows 8.1.
Figure 2-9. Snapshot of a File Open Dialog (in Windows XP), the first filter added in the
list of extension filters, "All Images", is shown in the field "Files of types:" when
the ComboBox is clicked.
Figure 2-10. Snapshot of a File Open Dialog (in Windows 8.1), the name "GIF", which is
associated with regular expression *.gif in the codes, is shown in the field follows the
file name when the item is clicked.
Generally, the applications users would appreciate a FileChooser remembers its last
visited directory. Heres how to do this:
Listing 2-10. Set last visited directory as initial directory of FileChooser inside the
handle method of event handler registered for Load menu item:
To establish a list of files exists in the current directory whenever an image file is opened
from a File Open Dialog, well use APIs provided in java.nio.file package which is
introduced in the JDK 7 release.
The following static method provided in java.nio.file.Files class,
The followings are the complete codes of the static method listIterator which reads a
directory, adds file one by one to a List, sort files in alphabetical order, and return a
ListIterator that points to the next available file in the directory. The method is defined
in FileUtils class.
Import Statements:
import java.io.IOException;
import java.nio.file.DirectoryIteratorException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.ListIterator;
import javafx.stage.FileChooser;
Listing 2-11. The static method listIterator defined in FileUtils class, reads a directory,
adds file one by one to list, sorts the list, and returns an instance of ListIterator:
/**
* Using Files.newDirectoryStream(Path, String) to read directory,
* add files to a list, return the listIteratior that points to
* the file specified in argument, that is, the first call of next()
* returns the specified file,
* the file list is sorted in alphabetical order,
* the second argument in newDirectoryStreamis method is
* glob pattern for filtering files matching the pattern:
* "*.{bmp,gif,jpeg,jpg,png}",
* we are only interested in files ending with:
* .bmp, .gif, .jpeg, .jpg and .png.
*
* @param file
* @return
* @throws IOException
*/
static ListIterator<Path> listIterator(Path file) throws IOException {
Path dir = file.getParent();
List<Path> list = new ArrayList<>();
In this section well demonstrate how to define properties in a class. In the entry point of
the application, ImageViewer class, we define three properties as follows:
listIterator
This is type of ObjectProperty<ListIterator<Path>>. The
listIterator(Path file) method, a static method defined in FileUtils class, is
called from the action handler of Load menu item to get an instance of
ListIterator and to set as the value of this property.
lastClickedButton
This is type of StringProperty for keeping track of last clicked button so that we
can properly move the cursor of a ListIterator specified in listIterator
property. Youll see how the value of lastClickedButton property, either NEXT
or PREVIOUS, effecting the calls of next() method and previous() method
within mouse clicked event handlers registered for Next button and Previous
button.
curImage
This is type of ObjectProperty<Image> and binds to image property of an
ImageView, the value of the property is the current loaded image.
Properties usually work in connection with binding mechanism. This is the bind method
defined in Property<T> interface,
void bind(ObservableValue<? extends T> observable)
to create a unidirectional binding for this property. The parameter observable is type of
ObservableValue. Both ObjectProperty and StringProperty implement
Property, a subinterface of ObservableValue.
Heres the snippet of codes to create a binding for image property of an ImageView
using the bind method:
Listing 2-13. Bind image property of an ImageView to curImage property:
Once the binding is created, then whenever the value of curImage property is changed,
the value of image property in ImageView is changed as well.
The set method or the setValue method can be used to set value for properties. Heres
the snippet of codes excerpted from the handle method within an event handler
registered to handle ActionEvent of Load menu item:
Listing 2-14. Set values to listIterator property, curImage property and
lastClickedButton property :
ListIterator<java.nio.file.Path> it = FileUtils.listIterator(file.toP
listIterator.set(it);
file = it.next().toFile();
String url = file.toURI().toString();
curImage.set(new Image(url));
lastClickedButton.setValue(NEXT);
Please notice that the value of curImage property is set when one of the following
actions occurs:
As we mentioned above, once the value of curImage property is changed, the value of
image property of ImageView is changed as well.
The changes of codes made in ImageViewer class, the entry point of application, are
shown in bold face as follows:
Import Statements:
import java.io.IOException;
import java.util.ListIterator;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
Listing 2-15. Codes added to ImageViewer class, the entry point of application, shown
in bold face:
...
// For traverse files in current selected directory
final ObjectProperty<ListIterator<java.nio.file.Path>> listIterator =
new SimpleObjectProperty<>();
@Override
public void start(final Stage stage) {
...
...
Listing 2-16. Codes of mouse clicked event handler for Next button becomes:
Node createNextButton() {
// Create a group as button
Group buttonGroup = new Group();
...
...
}
Listing 2-17. Codes of mouse clicked event handler for Previous button becomes:
Node createPrevButton() {
// Create a group as button
Group buttonGroup = new Group();
...
...
}
2.5 Complete Source Codes of Image Viewer V1.1
2.5.1 Complete Source Codes of FileUtils Class
package imageviewer;
import java.io.File;
import java.io.IOException;
import java.nio.file.DirectoryIteratorException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.ListIterator;
import javafx.stage.FileChooser;
/**
* File Name: FileUtils.java
* (in directory: ImageViewer1.1/src/example4/imageviewer/)
*
* @author Shufen Kuo
*/
/**
* Define file extension filters to filtering supported image files,
* matching files whose extensions are:
* .bmp, .gif, .jpeg, .jpg and .png
* @param fileChooser
*/
static void configure(FileChooser fileChooser) {
fileChooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("All Images",
"*.bmp", "*.gif", "*.jpeg", "*.jpg", "*.png"),
new FileChooser.ExtensionFilter("All Files", "*.*"),
new FileChooser.ExtensionFilter("BMP", "*.bmp"),
new FileChooser.ExtensionFilter("GIF", "*.gif"),
new FileChooser.ExtensionFilter("JPEG", "*.jpeg"),
new FileChooser.ExtensionFilter("JPG", "*.jpg"),
new FileChooser.ExtensionFilter("PNG", "*.png"));
// Set "." as default dir
fileChooser.setInitialDirectory(new File("."));
}
/**
* Using Files.newDirectoryStream(Path, String) to read directory,
* add files to a list, return the listIteratior that points to
* the file specified in argument, that is, the first call of next()
* returns the specified file,
* the file list is sorted in alphabetical order,
* the second argument in newDirectoryStreamis method is
* glob pattern for filtering files matching the pattern:
* "*.{bmp,gif,jpeg,jpg,png}",
* we are only interested in files ending with:
* .bmp, .gif, .jpeg, .jpg and .png.
*
* @param file
* @return
* @throws IOException
*/
static ListIterator<Path> listIterator(Path file) throws IOException {
Path dir = file.getParent();
List<Path> list = new ArrayList<>();
import java.io.File;
import java.io.IOException;
import java.util.ListIterator;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.control.RadioMenuItem;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Polygon;
import javafx.scene.shape.Rectangle;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
/**
* File Name: ImageViewer.java V1.1
* (in directory: ImageViewer1.1/src/example4/imageviewer/)
* This application creates a BorderPane as root node of a scene, and
* add a StackPane in the center of BorderPane
* The initial size of the scene is set to 600 by 400
* The size of StackPane will be automatically set to fit the size of scene.
* StackPane will be resized when window size is adjusted by a user
*
* Add a menu bar in the top of BorderPane
* Create File Menu, and using FileChooser's Open Dialog to
* select an image file, load and display the image in image rendering area.
*
* Create Option menu contains View submenu, let a user chooses image viewing
* criteria: Fit Width, Fit Height, or the default option Original Size.
*
* Adjust image viewing size using bind and unbind methods:
* Bind fitWidth/fitHeight properties of an image view to
* width/height properties of a scene,
* so that, size of the image view will be changed automatically
* whenever the scene is resized.
*
* Add Next and Previous buttons to a scene, then
* after the first image is loaded, a user can use these buttons to
* view next or preview image without opening FileChoose again.
*
* Adjust Next button's default position in StackPane
*
* Add Previous button and adjust its position in a StackPane
*
* Implement on mouse clicked event Handlers for Next and Previous buttons
*
* @author Shufen Kuo
*/
imageArea.widthProperty().addListener(new ChangeListener<Object>() {
public void changed(ObservableValue o, Object oldVal,
Object newVal) {
System.out.format("imageArea width changed from %.1f to %.1f%n",
oldVal, newVal);
}
});
imageArea.heightProperty().addListener(new ChangeListener<Object>() {
public void changed(ObservableValue o, Object oldVal,
Object newVal) {
System.out.format("imageArea height changed from %.1f to %.1f%n",
oldVal, newVal);
}
});
// Add menu bar
final MenuBar menuBar = new MenuBar();
Menu menuFile = new Menu("File");
MenuItem load = new MenuItem("Load");
MenuItem exit = new MenuItem("Exit");
menuFile.getItems().addAll(load, exit);
menuBar.getMenus().add(menuFile);
rootPane.setTop(menuBar);
// Install tooltip
Tooltip t = new Tooltip("Next Image");
Tooltip.install(nextButton, t);
// Set cursor
buttonGroup.setCursor(Cursor.OPEN_HAND);
buttonGroup.getChildren().addAll(rect, polygon);
return buttonGroup;
}
/**
* Create previous button using a Rectangle as background,
* a Polygon as icon, a Group as container,
* size of Rectangle is 25 by 35,
* thickness of polygon is 8 pixels,
* padding of polygon is (2, 5, 2, 5),
* change rectangle appearance when mouse entered.
*
* @return
*/
Node createPrevButton() {
// Set cursor
buttonGroup.setCursor(Cursor.OPEN_HAND);
buttonGroup.getChildren().addAll(rect, polygon);
return buttonGroup;
}
Figure 2-11. Snapshot of Image Viewer V1.1, mouse hovers over Previous button.
Compile and run the program from command line, testing the program by loading both
large size and smaller size images from FileChoosers File Open Dialog, selecting
different view options, resizing the application window dynamically, clicking on Next
button to load next image, clicking on Previous button to load previous image, , etc.
The output is similar as follows:
Output from Compiling and Running ImageViewer in command line:
F:\My Documents\DocRoot\SHUFEN_JAVA\MyBookJFX\VolumeOne\ImageViewer1.1\src\exampl
javac -encoding utf-8 -Xlint:unchecked imageviewer\*.java
Stop
Define an offset and a color to be applied to color gradient.
(See sections: Use Rectangle Shape as Bounding Box of Custom-Made Button, and
Paint Background and Border of Shape)
arcHeight and arcWidth are properties defined in Rectangle class, for rendering
rounded rectangles.
(See section Set Value of arcHeight and arcWidth Properties to Render Rounded
Rectangle)
Cursor class, resides in javafx.scene package and extends java.lang.Object,
defines some useful mouse cursors as static fields of Cursor type such as
CLOSED_HAND, CROSSHAIR, HAND, OPEN_HAND, TEXT, WAIT, etc.
(See section Set Value of Cursor Property for Node)
EventHandler<T extends Event>, an interface which extends
java.util.EventListener, defines a handle(T event) method to be invoked when
the specified event happens.
(See section Use Convenience Methods to Register Event Handlers to Handle Mouse
Events)
MouseEvent, resides in javafx.scene.input package, is a subclass of
javafx.scene.input.InputEvent. It defines event types such as
MOUSE_CLICKED, MOUSE_DRAGGED, MOUSE_ENTERED,
MOUSE_EXITED, MOUSE_MOVED, MOUSE_PRESSED,
MOUSE_RELEASED, etc., and provides methods such as getX(), getY(), etc., to
get position of mouse cursor while event occurs.
(See section Use Convenience Methods to Register Event Handlers to Handle Mouse
Events)
Some of convenience methods provided in Node class to register EventHandlers
to handle mouse events.
public final void setOnMouseClicked(EventHandler<? super
MouseEvent> value)
public final void setOnMouseEntered(EventHandler<? super
MouseEvent> value)
public final void setOnMouseExited(EventHandler<? super
MouseEvent> value)
(See section Use Convenience Methods to Register Event Handlers to Handle Mouse
Events)
static void install(Node node, Tooltip t) method, provided in Tooltip class
which resides in javafx.scene.control package and extends
javafx.scene.control.PopupControl class, shows help information when
mouse hovers on the given Node.
(See section Install Tooltip for Node)
Static methods, provided in StackPane, for customizing layout constraints on the
specified child node:
static void setAlignment(Node child, Pos value)
static void setMargin(Node child, Insets value)
(See sections: Adjust Next Buttons Default Position in StackPane, and Add
Previous Button and Adjust Its Position in StackPane)
Pos, resides in javafx.geometry package, is an enumerated type defines
constants for position and alignment such as CENTER, CENTER_RIGHT,
TOP_RIGHT, etc.
(See sections: Adjust Next Buttons Default Position in StackPane, and Add
Previous Button and Adjust Its Position in StackPane)
Insets class, resides in javafx.geometry package, is often used in the
setMargin method or in the setPadding method for specifying offset of 4 sides:
top, right, bottom and left of a rectangle area.
(See section Adjust Next Buttons Default Position in StackPane, and here)
FileChooser.ExtensionFilter is the nested class defined in the FileChooser
class, for defining filters to show only files whose extensions matching the given
filters.
(See section Configure FileChooser)
Some of Classes reside in javafx.beans.property package:
ObjectProperty<T>
is an abstract class, derived from
javafx.beans.binding.ObjectExpression<T> class, implements
Property<T> and WritableObjectValue<T>.
SimpleObjectProperty<T>
derived from ObjectProperty<T> class.
StringProperty
is an abstract class, derived from
javafx.beans.binding.StringExpression class, implements
Property<java.lang.String> and WritableStringValue.
WritableStringValue extends
WritableObjectValue<java.lang.String>.
SimpleStringProperty
derived from StringProperty class.
void unbind()
Unbind for this property.
Preface
At this stage, the ImageViewer application has contained adequate abilities as an
effective image viewing tool. Based on the fundamental structure built so far, more
functionality may be incrementally developed with ease.
This chapter features the implementation of slide show capabilities as well as fine-tuning
overall look and feel of GUIs. Well demonstrate how to develop a multi-threaded JavaFX
application using classes for concurrency, reside in javafx.concurrent package.
The version number of ImageViewer is upgraded to 1.2. From detailed descriptions and
well-documented example codes excerpted from ImageViewer V1.2, you will become
familiar with fundamental capabilities of JavaFX library recounted in the Summary
section.
The complete development process of ImageViewer V1.2 is characterized by the
following steps:
3.1.1 Create HBox Pane as Status Bar and Text Node to Show Image
Name
An HBox, one of build-in layout panes, placing its children in one row horizontally, is
created as status bar. And a Text node is created to display image name. Heres the
snippet of codes to do these:
Please notice that Text class derives from Node class, and is one of direct subclasses of
Shape class that carries out common needs for rendering geometric primitives.
A Text being considered a Node as well as a Shape comes with significant benefits. All
features common to JavaFX nodes can be applied to Text objects. They can be placed in a
layout pane utilizing the automatic placement capabilities, applying transformations and
transitions, customizing the presentation style using CSS, etc. Also they possess common
properties defined in Shape class such as fill, stroke, strokeType, strokeWidth,
etc.
Wed like to set preferred value of properties as below for the HBox created as a status
bar:
padding
The padding property, inherited from Region class, defines padding of top,
right, bottom and left for a region. The void setPadding(Insets value) method
is called to set value for this property.
spacing
The spacing property, defined in HBox class, specifies the horizontal space
between children in an HBox pane. The void setSpacing(double value)
method is called to set value for this property.
style
The style property, defined in Node class, specifies CSS style for this node. The
value of this property is a string complying with the format used in the style
attribute of a HTML element. The void setStyle(String value) method is called
to set the background color of a status bar, specifying a property-value pair in a
string with format like this:
"-fx-background-color: linear-gradient(#F0FFFF, #708090)"
As for the Text object, values for font property and fill property are set. Heres the
snippet of codes to do these:
Please notice that, theres an unusual aspect in JavaFX 8 (JDK 8u66 running on Windows
8.1), regarding background of a Scene; to see through the background color of a Scene,
we need set root panes background color to transparent. Heres the line of code:
rootPane.setStyle("-fx-background-color: transparent;");
The status bar, a region added at the bottom of the root pane, affects the height of image
rendering area, a StackPane. Thus the line of code which binds fitHeight property of
an ImageView with height property of a Scene need be changed to this:
imageView.fitHeightProperty().bind(
scene.heightProperty().subtract(
menuBar.getHeight() + statusBar.getHeight()));
import javafx.scene.layout.HBox;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
Listing 3-1. Add an HBox, the status bar, with a Text node in it for displaying image
name, specify background color for the Scene, and alter codes that bind fitHeight
property:
...
final StringProperty imageName = new SimpleStringProperty();
...
@Override
public void start(final Stage stage) {
BorderPane rootPane = new BorderPane();
final StackPane imageArea = new StackPane();
// Set min size for StackPane to avoid resizing
// when loading large size image
imageArea.setMinSize(0, 0);
rootPane.setCenter(imageArea);
statusBar.getChildren().add(statusText);
rootPane.setBottom(statusBar);
...
rootPane.setStyle("-fx-background-color: transparent;");
final Scene scene = new Scene(rootPane, 600, 400, Color.BLACK);
...
The text property of a Text node, created for displaying image name in a status bar,
binds with the imageName property defined in ImageViewer class. Whenever a new
image file is opened and displayed on image rendering area, an image name is obtained
from a File object, and set as value of imageName property. Heres the snippet of codes
to do these:
File file = fileChooser.showOpenDialog(stage);
String url = file.toURI().toString();
imageName.set(url.substring(url.lastIndexOf("/")+1).replaceAll("%20", " "));
The following line of code, obtaining an image name from a url of string representation,
is inserted to the action handler of Load menu item as well as mouse clicked event
handlers of Next and Previous buttons. Since the text property of statusText, a Text
object, binds with imageName property, whenever value of imageName property is
changed, the text rendered on a status bar will be changed accordingly.
Listing 3-2. Action handler for Load menu item becomes like this:
Listing 3-3. Mouse clicked handler for Next button becomes this:
lastClickedButton.set(NEXT);
}
}
});
Listing 3-4. Mouse clicked handler for Previous button becomes this:
The following snapshot is taken when Fit Height is the selected view option, the current
image file name is displayed on a status bar and the background color of a scene is black.
Figure 3-1. Snapshot of current image file name displayed on the status bar located at the
bottom of scene window.
3.2 Improve Buttons Reaction Aspect
In this section, we take a little effort to fine-tune buttons look and feel, also to enhance
the user-friendliness.
First, well set buttons disable status to true when there is no next or previous image to
open. Once a button is disabled, it will not receive any mouse or keyboard event. A
buttons disable status is initialized to false immediately after it is created.
Listing 3-5. Initialize buttons disable status:
...
/**
* This method takes advantage of node's opacity property to
* distinguish disable status,
* set opacity value to 0.4 if it is disabled.
*
* @param button
* @param isDisable
*/
void disableButton(Node button, boolean isDisable) {
// Change the opacity value to 0.4 if no more image to open
double opacity = (isDisable) ? 0.4 : 1;
button.setOpacity(opacity);
button.setDisable(isDisable);
}
To determine whether next image or previous image is available or not, hasNext() and
hasPrevious() methods defined in ListIterator interface are called to do the
verification during the following occasions:
When a list of files that exist in the current directory is first created from the action
event handler of Load menu item.
When an image is loaded as a mouse clicked event occurred on Next or Previous
button.
Well define a loadFile(File file) method to modularize the tasks that deal with the
action event of Load menu item:
Listing 3-7. Define loadFile method to create a list iterator, load the specified file and
update button status:
/**
* Static method FileUtils.listIterator(Path) is called to
* get a list iterator to iterate over files in the directory,
* disable buttons if no more files to open.
*
* @param file
*/
void loadFile(File file) {
// Create list iterator
try {
ListIterator<java.nio.file.Path> it =
FileUtils.listIterator(file.toPath());
file = it.next().toFile();
String url = file.toURI().toString();
curImage.set(new Image(url));
imageName.set(url.substring(
url.lastIndexOf("/") + 1).replaceAll("%20", " "));
lastClickedButton.setValue(NEXT);
} catch (IOException ex) {
}
}
The above loadFile method is called from the action event handler of load menu item.
Changes are shown in bold face in the following Listing.
Listing 3-8. loadFile method is called when a file is selected and opened from
FileChoosers File Open Dialog:
Node createNextButton() {
...
...
}
When mouse clicked event occurs on the Previous button, the previous available image
is loaded and displayed on the image rendering area. In the mean time, we need to check if
there is no more previous image to be loaded. Here are snippets of codes, changes are
shown in bold face:
Listing 3-10. Update Previous buttons disable status inside mouse clicked event
handler:
Node createPrevButton() {
...
...
}
Figure 3-3. Snapshot of Previous button disabled when theres no previous file to open.
Figure 3-4. Snapshot of Next button disabled when theres no next file to open.
Now, well change buttons background paint when mouse pressed event occurred on
them. In the mean time, codes for creating Next button and Previous button are tidied
up by modularized codes for mouse event handling. The registerButtonEvent method
is defined to register event handlers for the following EventTypes:
MOUSE_ENTERED, MOUSE_PRESSED, MOUSE_RELEASED, and
MOUSE_EXITED.
When mouse pressed on a button, the paint to fill a rectangle is changed, the paint to fill
polygon is same as the one used when mouse entered. The fill and stroke restores to
original values when mouse released or exited a button.
Import Statements:
import javafx.scene.paint.Paint;
import javafx.scene.shape.Shape;
/**
* This method registers mouse entered/pressed/released/exited events for butto
* look and feel of the button is changed to distinguish between different even
*
* @param buttonGroup
* @param rect
* @param polygon
* @param bg
* @param entered
* @param pressed
*/
void registerButtonEvent(Node buttonGroup,
final Shape rect, final Shape polygon,
final Paint bg, final Paint entered, final Paint pressed) {
buttonGroup.setOnMouseExited(exited);
/**
* Create next button using a Rectangle as background,
* a Polygon as icon, a Group as container,
* size of Rectangle is 25 by 35,
* thickness of polygon is 8 pixels,
* padding of polygon is (2, 2, 2, 5),
* change button appearance when mouse entered/pressed/released/exited.
*
* @return
*/
Node createNextButton() {
// Set cursor
buttonGroup.setCursor(Cursor.OPEN_HAND);
lastClickedButton.set(NEXT);
// Disable next button when there's no next image
disableButton(nextButton, !iterator.hasNext());
if (prevButton.isDisable())
disableButton(prevButton, false);
}
}
});
buttonGroup.getChildren().addAll(rect, polygon);
return buttonGroup;
}
/**
* Create previous button using a Rectangle as background,
* a Polygon as icon, a Group as container,
* size of Rectangle is 25 by 35,
* thickness of polygon is 8 pixels,
* padding of polygon is (2, 5, 2, 5),
* change button appearance when mouse entered/pressed/released/exited.
*
* @return
*/
Node createPrevButton() {
// Set cursor
buttonGroup.setCursor(Cursor.OPEN_HAND);
buttonGroup.getChildren().addAll(rect, polygon);
return buttonGroup;
}
Figure 3-5. Snapshot of Previous buttons appearance during mouse entered event.
Figure 3-6. Snapshot of Previous buttons appearance during mouse pressed event.
Figure 3-7. Snapshot of Next buttons appearance during mouse entered event.
Figure 3-8. Snapshot of Next buttons appearance during mouse pressed event.
3.3 Add Slide Show Capabilities
Once an image file is opened from a FileChoosers File Open Dialog, the application
obtains a list of image files that exist in the current directory. One favorable feature is to
allow users to browse over these images one by one without manually clicking the Next
button. The steps to implement slide show mechanism are illustrated as follows.
The simplest GUI employed here is MenuItem. Two menu items, Start Slideshow
and Stop Slideshow, are added to Option menu of a menu bar, initially they are both
disabled. To distinguish if there is a slide show running or not, a variable
isSlideshowOn of type BooleanProperty is defined as class field of ImageViewer
class and is initialized to false. The following snippets of codes show how to accomplish
these:
Import Statements:
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
Listing 3-14. Add Start Slideshow and Stop Slideshow menu items:
...
...
@Override
public void start(final Stage stage) {
...
startSlide.disableProperty().bind(isSlideshowOn.or(
nextButton.disableProperty()));
stopSlide.disableProperty().bind(isSlideshowOn.not().or(
nextButton.disableProperty()));
// Add action handler for Start Slideshow menu item
startSlide.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent ae) {
System.out.println("Start Slideshow");
}
});
...
Initially startSlide and stopSlide menu items are disabled because of the following
bind statements.
startSlide.disableProperty().bind(isSlideshowOn.or(
nextButton.disableProperty()));
stopSlide.disableProperty().bind(isSlideshowOn.not().or(
nextButton.disableProperty()));
In above binding statements, two logical operationsor and notare used to compute
value for disable property. The javafx.beans.binding.BooleanExpression class,
an abstract class that implements ObservableBooleanValue, provides these methods:
The Start Slideshow menu item is disabled if either a slide show is currently running
or the Next button is disabled. Similarly, the Stop Slideshow menu item is disabled if
either a slide show is not running or the Next button is disabled. Remember that the
Next button is disabled when there is no next image to view.
Figure 3-9. Snapshot of initial Option menus appearance, both Start Slideshow and
Stop Slideshow menu items are disabled.
Figure 3-10. Snapshot of Option menus appearance after an image file is loaded, Start
Slideshow menu item is enabled because a slide show is not on and there is at lease one
next file to view, and Stop Slideshow menu item is disabled because a slide show is
not on.
3.3.2 Execute Slide Show on Another Thread
Its a good programming practice to delegate a long-running task to another thread so that
the UI thread (also called Java FX Application thread) is always responsive to users
actions.
javafx.concurrent package provides classes, Task and Service both of which
implement Worker interface, for developing multi-threaded JavaFX applications with
ease.
Here are classes for concurrency, reside in javafx.concurrent package:
Task
An abstract class that extends java.util.concurrent.FutureTask and
implements Worker<V> interface, is a one-time Worker object, cannot be
reused. Its also a Runnable object since its superclass FutureTask implements
Runnable interface.
To start executing a Task, you use either APIs from java.lang.Thread class or
APIs that are derived from Executor and ExecutorService interfaces, both
reside in java.util.concurrent package. You create a Thread object given a
Task as parameter, and call start() method to start this thread. Or, use the
submit(Runnable task) method defined in ExecutorService to submit a task.
Override the call() method while creating a Task object, or creating a subclass of
which. The call() method is invoked when a task is started.
Service
An abstract class that extends java.lang.Object and implements Worker<V>
interface, is a reusable Worker object. The Service class provides methods such
as start(), cancel(), reset(), and restart() to manipulate this service when
needed.
Override the createTask() method while creating a Service object, or creating a
subclass of which. The createTask() method is invoked when a service is started.
ScheduledService
A direct subclass of Service class, allows specifying the duration of delay to start
and restart a service automatically.
WorkerStateEvent
A subclass of javafx.event.Event, defines static fields of EventTypes such as
WORKER_STATE_CANCELLED, WORKER_STATE_FAILED,
WORKER_STATE_READY, WORKER_STATE_RUNNING,
WORKER_STATE_SUCCEEDED, etc. A WorkerStateEvent occurs when
the state of a Worker is changed.
Consider our situation: a file list is created when a file is first loaded, suppose the file list
contains one or more next file(s) to view, then the Start Slideshow menu item is
enabled, a user can start a slide show, cancel it while the slide show is on, and later restart
it again. To fulfill this scenario, we prefer a reusable Service object to a one-time Task
object.
We need to create a class that extends Service<V> class. When a Service is started, it
executes a Task. The createTask method below need be overridden:
In the createTask() method, the call() method is overridden while a Task object is
created to be returned to the caller.
The call() method in a Task, undertakes the task of a slide show, basically contains a
while loop that loads images one by one until either one of the following conditions
becomes true: theres no more next image in the file list, or the slide show is canceled by a
user.
The following listing demonstrates how to create a reusable SlideshowService, a
subclass of Service class, to carry on a slide show task.
Listing 3-15. Create SlideshowService class:
package imageviewer;
import java.nio.file.Path;
import java.util.ListIterator;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.scene.image.Image;
/**
* File Name: SlideshowService.java
* (in directory: ImageViewer1.2/src/example4/imageviewer/)
*
* @author Shufen Kuo
*/
listIterator
Type of ObjectProperty<ListIterator<Path>>, specifies a ListIterator
that iterates the file list created when an image file is opened from a File Choosers
File Open Dialog.
curImage
Type of ObjectProperty<Image>, binds with the image property of an
ImageView object.
imageName
Type of StringProperty, binds with the text property of a Text object,
statusText, for displaying image name in a status bar.
3.3.2.2 Configure Slide Show Service Object
And one single EventHandler is created to handle Worker State Event of multiple event
types. The same event handler registered to handle WORKER_STATE_SUCCEEDED
is also registered to handle WORKER_STATE_FAILED and
WORKER_STATE_CANCELLED. The tasks to be performed while the Workers
state transited to SUCCEEDED, FAILED, or CANCELLED are:
1. Disable Next button if there is no next file in the list iterator, Otherwise, enable it.
2. Enable Previous button if it is disabled.
import javafx.concurrent.WorkerStateEvent;
/**
*
*/
void configureSlideshow() {
// If service is running, then value of isSlideshowOn is true,
// otherwise, it's false
isSlideshowOn.bind(service.runningProperty());
service.setOnRunning(new EventHandler<WorkerStateEvent>() {
@Override
public void handle(WorkerStateEvent we) {
System.out.println("Service is transited to running status");
lastClickedButton.set(NEXT);
if (prevButton.isDisable()) {
disableButton(prevButton, false);
}
}
});
service.setOnSucceeded(taskDone);
service.setOnFailed(taskDone);
service.setOnCancelled(taskDone);
}
3.3.2.3 Implement Event Handlers of Action Events for Start Slideshow and Stop Slideshow Menu Items
The scenario follows the action of Start Slideshow is like this: when a user clicks the
Start Slideshow menu item, the service is started, its state transits from READY to
SCHEDULED to RUNNING, the value of isSlideshowOn property is changed to true
as soon as the state is transited to RUNNING(see here), and Stop Slideshow menu
items disable status is changed to false as a result(see here).
We are using the void start() method provided in Service class to start a service. It is
called from the event handler that handles action event of Start Slideshow menu item.
It is important to check the state of a service before invoking the start() method, this is
to assure a service is in READY state.
The Worker.State getState() method defined in javafx.concurrent.Worker
interface, is used to check the state of a service. A service can only be started successfully
when it is in READY state. if it is not ready, reset it first, then start the service.
Heres the snippet of codes to do these:
startSlide.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent ae) {
System.out.println("Start Slideshow");
if (service.getState() != Worker.State.READY)
service.reset(); // transit to READY
service.start();
}
});
A user can cancel a slide show while the slide show service is running. The boolean
cancel() method is called from the event handler that handles action event of Stop
Slideshow menu item.
The following codes demonstrate how to implement event handlers of action event for
Start Slideshow and Stop Slideshow menu items.
Import Statement:
import javafx.concurrent.Worker;
Listing 3-17. Implement event handlers of action events for Start Slideshow and Stop
Slideshow menu items:
startSlide.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent ae) {
System.out.println("Start Slideshow");
if (service.getState() != Worker.State.READY)
service.reset(); // transit to READY
// If last clicked button is previous button,
// then next() image is current image,
// must move cursor one step forward before starting service
if (lastClickedButton.get().equals(PREVIOUS) &&
listIterator.get().hasNext()) {
listIterator.get().next();
}
service.start();
}
});
stopSlide.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent ae) {
System.out.println("Stop Slideshow");
service.cancel();
}
});
Following listing shows the complete start method of ImageViewer V1.2 at this
phase, changes are in bold type face.
Listing 3-18. start method of ImageViewer V1.2 becomes like this:
...
@Override
public void start(final Stage stage) {
BorderPane rootPane = new BorderPane();
final StackPane imageArea = new StackPane();
// Set min size for StackPane to avoid resizing
// when loading large size image
imageArea.setMinSize(0, 0);
rootPane.setCenter(imageArea);
// Add status bar and status text
final HBox statusBar = new HBox();
statusBar.setPadding(new Insets(4, 12, 4, 12));
statusBar.setSpacing(4);
statusBar.setStyle(
"-fx-background-color: linear-gradient(#F0FFFF, #708090)");
final Text statusText = new Text("");
statusText.textProperty().bind(imageName);
statusText.setFont(Font.font("Verdana", FontWeight.BOLD, 12));
statusText.setFill(Color.DARKSLATEBLUE);
statusBar.getChildren().add(statusText);
rootPane.setBottom(statusBar);
startSlide.disableProperty().bind(isSlideshowOn.or(
nextButton.disableProperty()));
stopSlide.disableProperty().bind(isSlideshowOn.not().or(
nextButton.disableProperty()));
configureSlideshow();
// Install tooltip
Tooltip t = new Tooltip("Next Image");
Tooltip.install(nextButton, t);
Since a slide show is running on its own thread, the application remains responsive to
users actions such as mouse clicked on Load menu item or Previous button.
To make a slide show running smoothly, well refine the action handler of Load menu
item as well as the mouse clicked event handler of Previous button. That is, well
proceed to the loading of current selected file or previous file only if a slide show is not
running. Changes are shown in bold type face in the following listings.
Please note that if a user clicks the Next button while a slide show is running, the overall
flow of image rendering doesnt look too awkward, therefore no extra care is imposed on
it.
Listing 3-19. loadFile method becomes like this:
/**
*
* @param file
*/
void loadFile(File file) {
// Create iterator
try {
ListIterator<java.nio.file.Path> it =
FileUtils.listIterator(file.toPath());
file = it.next().toFile();
String url = file.toURI().toString();
curImage.set(new Image(url));
imageName.set(url.substring(
url.lastIndexOf("/") + 1).replaceAll("%20", " "));
lastClickedButton.setValue(NEXT);
}
} catch (IOException ex) {
}
}
lastClickedButton.set(PREVIOUS);
disableButton(prevButton, !iterator.hasPrevious());
if (nextButton.isDisabled())
disableButton(nextButton, false);
}
}
}
});
Compile and run the program, testing the followings to apprehend how the application
deals with concurrency of events:
Load an image file in an image directory from a FileChoosers File Open Dialog.
Start slide show.
While a slide show is running, do below actions:
Select different View options back and forth.
Click Previous button or Next button.
Load another image file in different image directory from a FileChoosers File
Open Dialog.
Figure 3-11. Snapshot of Option menus appearance when a slide show is running,
Start Slideshow menu item is disabled and Stop Slideshow menu item is enabled.
3.4 Add Fade Transition between Slides
To accent the movement of a slide show, well add fade transition between slides. Here are
classes from javafx.animation package for animations of fade transitions:
Animation
An abstract class extends java.lang.Object, provides common properties and
methods required among animations.
Transition
An abstract class extends Animation, provides common properties and methods
required among transition animations. Here are available transitions:
FadeTransition, FillTransition, ParallelTransition, PathTransition,
PauseTransition, RotateTransition, ScaleTransition,
SequentialTransition, StrokeTransition, TranslateTransition.
FadeTransition
A direct subclass of Transitionchanges the opacity of a node over the given
duration.
ParallelTransition
A direct subclass of Transition, conducts transitions over a list of Animation
objects, type of ObservableList<Animation>, in a parallel manner.
This is the constructor well use to create FadeTransition objects:
Heres the snippet of codes to create a FadeTransition for an image to fade in, the
value of opacity for this image is changing from 0 to 1 through the duration of 2000
milliseconds:
Now, we have two FadeTransition objects, one for performing fade in transition,
another for performing fade out transition, we prefer both transitions to process in
parallel. Since each transition has its own target node, the following constructor of
ParallelTransition is employed:
Heres the snippet of codes to create a ParallelTransition and start the animation
using the playFromStart() method provided in Animation class:
...
...
@Override
public void start(final Stage stage) {
BorderPane rootPane = new BorderPane();
final StackPane imageArea = new StackPane();
// Set min size for StackPane to avoid resizing
// when loading large size image
imageArea.setMinSize(0, 0);
rootPane.setCenter(imageArea);
...
...
The following listing shows the complete source codes of SlideshowService class
at this phase, in which changes are shown in bold type face.
Listing 3-22. SlideshowService class becomes like this:
package imageviewer;
import java.nio.file.Path;
import java.util.ListIterator;
import javafx.animation.FadeTransition;
import javafx.animation.ParallelTransition;
import javafx.animation.Transition;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.util.Duration;
/**
* File Name: SlideshowService.java
* (in directory: ImageViewer1.2/src/example5/imageviewer/)
*
* Add fade transition between slides
*
* @author Shufen Kuo
*/
while (it.hasNext()) {
if (isCancelled()) {
updateMessage("Cancelled");
break;
}
return null;
}
};
}
}
3.5 Complete Source Codes of Image Viewer V1.2
Listing 3-23. Complete source codes of ImageViewer class:
package imageviewer;
import java.io.File;
import java.io.IOException;
import java.util.ListIterator;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker;
import javafx.concurrent.WorkerStateEvent;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.control.RadioMenuItem;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Paint;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Polygon;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
/**
* File Name: ImageViewer.java V1.2
* (in directory: ImageViewer1.2/src/example5/imageviewer/)
* This application creates a BorderPane as root node of a scene, and
* add a StackPane in the center of BorderPane
* The initial size of the scene is set to 600 by 400
* The size of StackPane will be automatically set to fit the size of scene.
* StackPane will be resized when window size is adjusted by a user
*
* Add a menu bar in the top of BorderPane
* Create File Menu, and using FileChooser's Open Dialog to
* select an image file, load and display the image in image rendering area.
*
* Create Option menu contains View submenu, let a user chooses image viewing
* criteria: Fit Width, Fit Height, or the default option Original Size.
*
* Adjust image viewing size using bind and unbind methods:
* Bind fitWidth/fitHeight properties of an image view to
* width/height properties of a scene,
* so that, size of the image view will be changed automatically
* whenever the scene is resized.
*
* Add Next and Previous buttons to a scene, then
* after the first image is loaded, a user can use these buttons to
* view next or preview image without opening FileChoose again.
*
* Adjust Next button's default position in StackPane
* Add Previous button and adjust its position in a StackPane
* Implement on mouse clicked event Handlers for Next and Previous buttons
*
* Add status bar to show image name
* Disable buttons to indicate no more image to open
* Distinguish button's appearance between different event types:
* entered/pressed/released/exited
* Execute slideshow on thread
* Coordinate with event handlers for Load menu item and Previous button
* Add fade transition for slideshow
*
* @author Shufen Kuo
*/
statusBar.getChildren().add(statusText);
rootPane.setBottom(statusBar);
startSlide.disableProperty().bind(isSlideshowOn.or(
nextButton.disableProperty()));
stopSlide.disableProperty().bind(isSlideshowOn.not().or(
nextButton.disableProperty()));
configureSlideshow();
// Add action handler for Start Slideshow menu item
startSlide.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent ae) {
System.out.println("Start Slideshow");
if (service.getState() != Worker.State.READY)
service.reset(); // transit to READY
// If last clicked button is previous button,
// then next() image is current image,
// must move cursor one step forward before starting service
if (lastClickedButton.get().equals(PREVIOUS) &&
listIterator.get().hasNext()) {
listIterator.get().next();
}
service.start();
}
});
// Install tooltip
Tooltip t = new Tooltip("Next Image");
Tooltip.install(nextButton, t);
// Set cursor
buttonGroup.setCursor(Cursor.OPEN_HAND);
lastClickedButton.set(NEXT);
// Disable next button when there's no next image
disableButton(nextButton, !iterator.hasNext());
if (prevButton.isDisable())
disableButton(prevButton, false);
}
}
});
buttonGroup.getChildren().addAll(rect, polygon);
return buttonGroup;
}
/**
* Create previous button using a Rectangle as background,
* a Polygon as icon, a Group as container,
* size of Rectangle is 25 by 35,
* thickness of polygon is 8 pixels,
* padding of polygon is (2, 5, 2, 5),
* change button appearance when mouse entered/pressed/released/exited.
*
* @return
*/
Node createPrevButton() {
// Set cursor
buttonGroup.setCursor(Cursor.OPEN_HAND);
lastClickedButton.set(PREVIOUS);
disableButton(prevButton, !iterator.hasPrevious());
if (nextButton.isDisabled())
disableButton(nextButton, false);
}
}
}
});
buttonGroup.getChildren().addAll(rect, polygon);
return buttonGroup;
}
/**
* This method takes advantage of node's opacity property to
* distinguish disable status,
* set opacity value to 0.4 if it is disabled.
*
* @param button
* @param isDisable
*/
void disableButton(Node button, boolean isDisable) {
// Change the opacity value to 0.4 if no more image to open
double opacity = (isDisable) ? 0.4 : 1;
button.setOpacity(opacity);
button.setDisable(isDisable);
}
/**
* Static method FileUtils.listIterator(Path) is called to
* get a list iterator to iterate over files in the directory,
* disable buttons if no more files to open.
*
* @param file
*/
void loadFile(File file) {
// Create list iterator
try {
ListIterator<java.nio.file.Path> it =
FileUtils.listIterator(file.toPath());
file = it.next().toFile();
String url = file.toURI().toString();
curImage.set(new Image(url));
imageName.set(url.substring(
url.lastIndexOf("/") + 1).replaceAll("%20", " "));
lastClickedButton.setValue(NEXT);
}
} catch (IOException ex) {
}
}
/**
* This method registers mouse entered/pressed/released/exited events for b
* look and feel of the button is changed to distinguish between different
*
* @param buttonGroup
* @param rect
* @param polygon
* @param bg
* @param entered
* @param pressed
*/
void registerButtonEvent(Node buttonGroup,
final Shape rect, final Shape polygon,
final Paint bg, final Paint entered, final Paint pressed) {
buttonGroup.setOnMouseExited(exited);
service.setOnRunning(new EventHandler<WorkerStateEvent>() {
@Override
public void handle(WorkerStateEvent we) {
System.out.println("Service is transited to running status");
lastClickedButton.set(NEXT);
if (prevButton.isDisable()) {
disableButton(prevButton, false);
}
}
});
service.setOnSucceeded(taskDone);
service.setOnFailed(taskDone);
service.setOnCancelled(taskDone);
}
public static void main(String[] args) {
launch(args);
}
}
The following snapshots show fade in/fade out transition. Since we use
ParallelTransition instead of SequentialTransition, youll see the fade out
image blending with the fade in image.
Figure 3-12. Snapshot of an image to be faded out
Figure 3-13. Snapshot of fade in and fade out transitions executed in parallel manner.
Figure 3-14. Snapshot of an image that is faded in.
Compile and run the application. Try these steps to test it:
Load an image file in an image directory from a FileChoosers File Open
Dialog.
Start slide show.
While a slide show is running, do below actions:
Select different View options back and forth.
Click Previous button or Next button.
Load another image file in different image directory from a
FileChoosers File Open Dialog.
F:\My Documents\DocRoot\SHUFEN_JAVA\MyBookJFX\VolumeOne\ImageViewer1.2\src\ex
javac -encoding utf-8 -Xlint:unchecked imageviewer\*.java
SimpleStringProperty
is derived from StringProperty class.
(See here)
void bind(ObservableValue<? extends T> observable) method
defined in Property interface, resides in javafx.beans.property package,
to bind this property unidirectional to the given ObservableValue.
(See here | here | here)
DoubleBinding subtract(double other) method, defined in
javafx.beans.binding.DoubleExpression class, calculates the difference
of this NumberExpression and the given constant value.
(See here)
void setDisable(boolean value) method, provided in Node class, specifies
the disable status of this node.
(See section Disable Button to Indicate No More Image to Open and here)
void setOpacity(double value) method, provided in Node class, specifies
the extent of transparency of this node. (See here)
MouseEvent, resides in javafx.scene.input package, is a subclass of
javafx.scene.input.InputEvent. It defines event types such as
MOUSE_CLICKED, MOUSE_DRAGGED, MOUSE_ENTERED,
MOUSE_EXITED, MOUSE_MOVED, MOUSE_PRESSED,
MOUSE_RELEASED, etc., and provides methods such as getX(), getY(),
etc., to get position of mouse event.
(See section Change Button Appearance When Mouse Is Pressed)
void setOnMousePressed(EventHandler<? super MouseEvent>
value), a convenience method provided in Node class, registers an
EventHandler to handle MOUSE_PRESSED event type of MouseEvent.
(See section Change Button Appearance When Mouse Is Pressed)
MenuBar, Menu, and MenuItem are among fundamental JavaFX UI
components, reside in javafx.scene.control package, for designing compact
look-and-feel graphical user interfaces using less space.
(See section Add Start Slideshow and Stop Slideshow Menu Items)
Some classes from javafx.beans.property package:
BooleanProperty
is an abstract class, derived from
javafx.beans.binding.BooleanExpression class, implements
Property<java.lang.Boolean> and WritableBooleanValue.
WritableBooleanValue extends
WritableValue<java.lang.Boolean>.
SimpleBooleanProperty
derived from BooleanProperty class.
(See section Add Start Slideshow and Stop Slideshow Menu Items and here)
BooleanBinding class is an abstract class resides in javafx.beans.binding
package, it extends BooleanExpression class and implements
Binding<java.lang.Boolean>.
(See here)
BooleanExpression class, resides in javafx.beans.binding package, is
an abstract class that implements ObservableBooleanValue, an interface
that extends ObservableValue<java.lang.Boolean>. Convenience
methods that performs logical operations like AND, OR, and NOT are provided
in BooleanExpression class.
(See here)
Some convenience methods from BooleanExpression class:
BooleanBinding not()
performs the logical NOT operation on this BooleanExpression, and
returns an object of type BooleanBinding which is a direct subclass of
BooleanExpression
Service
An abstract class implements Worker<V> interface, is a reusable
Worker object.
ScheduledService
A direct subclass of Service class, allows specifying the duration of
delay to start and restart a service automatically.
WorkerStateEvent
A subclass of javafx.event.Event, defines static fields of event types
such as WORKER_STATE_CANCELLED,
WORKER_STATE_FAILED, WORKER_STATE_READY,
WORKER_STATE_RUNNING,
WORKER_STATE_SUCCEEDED, etc. A WorkerStateEvent
occurs when the state of a Worker is transited.
(See section Execute Slide Show on Thread)
protected abstract V call() method, defined in Task class, must be
overridden, is invoked when the task is executed. (See here | here | here)
protected abstract Task<V> createTask() method, defined in Service
class, must be overridden, is invoked when the service is started. (See here | here
| here)
Some of convenience methods provided in Service class to register
EventHandlers to handle Worker State Events:
public final void
setOnRunning(EventHandler<WorkerStateEvent> value)
public final void
setOnSucceeded(EventHandler<WorkerStateEvent> value)
public final void
setOnFailed(EventHandler<WorkerStateEvent> value)
public final void
setOnCancelled(EventHandler<WorkerStateEvent> value)
(See section Configure Slide Show Service Object)
Worker.State, nested class from javafx.concurrent.Worker interface, is
an enumerated type defines constants for states of a Worker, they are:
CANCELLED, FAILED, READY, RUNNING, SCHEDULED, and
SUCCEEDED.
(See here and here)
Some of methods provided in Service class, reside in javafx.concurrent
package:
public void start()
Start this service; a service can only be started successfully if it is in
READY state.
Worker.State getState()
Defined in Worker interface, get the value of state property which
represents the current state of the Worker.
More interactive sketching tools: Free Hand Drawer, Rubber Band Drawer, Text
Writer, Color Gradienter, Arc Drawer, Cursor Key Mover, etc.
Color palette tools
Image clipping tools
Path transition animater
Dynamic context menu controller
And more