26
JavaFX GUI: Par Partt 2
Objectives In this chapter you’ll: ■
■
■
■
■
■
■
■
Learn more details of laying out nodes in a scene graph with JavaFX layout panels. Build GUIs with JavaFX Scene Builder 2.0. Create and manipulate RadioButton s and ListViews. Use AnchorPanes and TitledPane s to layout controls. Handle mouse events. Use property binding and property listeners to perform tasks when a control’s control’s property value changes. Programmatically create layouts and controls. Customize a ListView’s cells with a custom cell factory.
© 2017 Pearson Education, Inc., Hoboken, Hoboken, NJ. All rights reserved.
Chap Ch apte terr 26 26
2
Java Ja vaFX FX GU GUI: I: Pa Part rt 2
26.1 Introduction 26.2 Installing JDK 8, NetBeans 8 and
26.5.5 ColorChooserController Class 26.6 Cover Viewer App: App: Data-Drivien
GUIs with JavaFX Collections
JavaFX Scene Builder 2.0 26.3 Laying Out Nodes in a Scene Graph 26.4 Painter App: App: RadioButton s, Mouse Events and Shapes 26. 6.44.1 Techno Technologies logies Overvie Overview w 26. 6.44.2 Creat Creating ing the Project Project 26. 6.44.3 Buildi Building ng the the GUI 26.4.4 Painter Subclass of Application 26.4.5 PainterController Class 26.5 Color Chooser App: App: Property
Bindings and Property Listeners 26. 6.55.1 26. 6.55.2 26. 6.55.3 26.5.4
Technologies Overvie Technologies Overview w Creating Creat ing the Project Project Building Buildi ng the the GUI ColorChooser Subclass of
26.6 .6.1 .1 26.6 .6.2 .2 26.6 .6.3 .3 26.6 .6.4 .4 26.6 .6.5 .5
Technologiess Overview Technologie Overview Creating Creat ing the Project Project Adding Images Images to the Project Project Building Buildi ng the the GUI CoverViewe Cover Viewerr Subclass of Application
26.6.6 CoverViewerController Class 26.7 Cover Viewer App: App: Customizing ListView Cells
26.7 .7.1 .1 Technolgies Technolgies Overview Overview 26.7 .7.2 .2 Copyi Copying ng the CoverViewer Project 26.7.3 ImageTextCell Custom Cell Factory Class 26.7.4 CoverViewerController Class 26.8 Wrap-Up
Application
Summary | Self-Review Exercises | Answers to Self-Review Exercises | Exercises
26.1 Introduction In Chapter 25, you used the NetBeans IDE (version 7.4) to create two JavaFX FXML Application projects—Welcome and Tip Calculator . In both projects, you used JavaFX Scene Builder (version 1.1) to create the GUIs. In the Tip Calculator project project you also implemented a controller containing the event e vent handlers for responding to user interactions with the GUI. In this chapter, you’ll: •
Use ad additi itional layouts (TitledPane and AnchorPane) and controls (RadioButton and ListView).
•
Handle mouse and RadioButton events.
•
Set up up event event handl handlers ers that that resp respond ond to to proper property ty chang changes es on cont control rolss (such (such as the value of a Slider).
•
Display Rectangles and Circles as nodes in the scene graph.
•
Bind Bind a col colle lect ctio ion n of of obj objec ects ts to a ListView so that it automatically displays the collection’s contents.
•
Crea Create te a cus custo tom m cel celll fac facto tory ry for for a ListView so that you can specify the exact layout of a ListView cell.
26.2 Installing JDK 8, NetBeans 8 and JavaFX Scene Builder 2.0 The examples in this chapter were written using Java 8, NetBeans 8 and JavaFX Scene Builder 2.0. © 2017 Pearson Education, Inc., Hoboken, Hoboken, NJ. All rights reserved.
26.3 Laying Out Nodes in a Scene Graph
3
Installing JDK 8 and NetBeans 8 Download the bundle of the latest JDK 8 (Java SE Development Kit 8) and the NetBeans 8 IDE by clicking the NetBeans button at the top of the Java SE Downloads page: http://www.oracle.com/technetwork/java/javase/downloads/index.html
After the download completes, open the installer and follow the on-screen instructions to install both JDK 8 and NetBeans 8.
Installing JavaFX Scene Builder 2.0 Download JavaFX Scene Builder 2.0 from: http://www.oracle.com/technetwork/java/javase/downloads/ sb2download-2177776.html
After the download completes, open the installer and follow the on-screen instructions to install it.
26.3 Laying Out Nodes in a Scene Graph Layout refers to the size and positioning of nodes in the scene graph.
Size of a Node Unless necessary, a node’s size should not be defined explicitly . Doing so often creates a design that looks pleasing when it first loads, but deteriorates when the app is resized or the content updates. In addition to the width and height properties associated with every control, most JavaFX nodes have the properties prefWidth, prefHeight, minWidth, minHeight, maxWidth and maxHeight that specify a node’s range of acceptable sizes as it’s layed out within its parent node: • The miniumum size properties specify a node’s smallest allowed size in points. • The maximum size properties specify a node’s largest allowed size in points. • The preferred size properties specify a node’s preferred width and height that should be used by the layout in most cases. Position of a Node and Layout Panes A node’s position should be defined relative to its parent node and the other nodes in its parent. JavaFX layout panes lay out nodes in a scene graph relative to other nodes based on their sizes and positions. A layout pane is a container node that’s responsible for laying out its child nodes—controls, other containers, shapes and more. To do so, a layout pane takes into account its children’s preferred, minimum and maximum sizes. Most JavaFX layout panes use relative positioning —if a layout-pane node is resized, it adjusts its childrens’ sizes and positions accordingly, based on their properties. Figure 26.1 describes each of the JavaFX layout panes, including those presented in Chapter 25. In this chapter, we’ll use AnchorPane, GridPane and VBox from the javafx.scene.layout package.
© 2017 Pearson Education, Inc., Hoboken, NJ. All rights reserved.
Chapter 26
4
Layout
JavaFX GUI: Part 2
Description
Enables you to set the position of child nodes relative to the pane’s edges. Resizing the pane does not alter the layout of the nodes. Includes five areas—top, bottom, left, center and right—where you can place nodes. The top and bottom regions fill the BorderPane’s width and are vertically sized to their children’s preferred heights. The left and right regions fill the BorderPane’s height and are horizontally sized to their children’s preferred widths. The center area occupies all of the BorderPane’s remaining space. You might use the different areas for tool bars, navigation, a main content area, etc. Lays out nodes consecutively—either horizontally or vertically. When the boundary for the pane is reached, the nodes wrap to a new line in a horizontal FlowPane or a new column in a vertical FlowPane. Create a flexible grid for laying out nodes in rows and columns. The base class for layout panes. This can be used to position nodes at fixed locations—known as absolute positioning. Places nodes in a stack. Each new node is stacked atop the previous node. You might use this to place text on top of images, for example. A horizontal or vertical grid of equally sized tiles. Nodes that are tiled horizontally wrap at the TilePane’s width. Nodes that are tiled vertically wrap at the TilePane’s height. Arranges nodes horizontally in one row. Arranges nodes vertically in one column.
AnchorPane
BorderPane
FlowPane
GridPane Pane
StackPane
TilePane
HBox VBox
Fig. 26.1 | JavaFX layout panes.
26.4 Painter App: RadioButtons, Mouse Events and Shapes In this section, you’ll create a simple Painter app (Fig. 26.2) that allows you to drag the mouse to draw. First, we’ll overview the technologies you’ll use, then we’ll discuss creating the app’s project and building its GUI. Finally, we’ll present the source code f or its Painter and PainterController classes.
26.4.1 Technologies Overview This section presents the JavaFX features you’ll use in the
Painter app.
RadioButton s
and ToggleGroup s RadioButtons function as mutually exclusive options, just like their Swing counterparts. You add multiple RadioButtons to a ToggleGroup to ensure that only one RadioButton in a given group is selected at a time. For this app, you’ll use JavaFX Scene Builder’s capability for specifying each RadioButton’s ToggleGroup in FXML; however, you can also create a ToggleGroup in Java, then use a RadioButton’s setToggleGroup method to specify its ToggleGroup. © 2017 Pearson Education, Inc., Hoboken, NJ. All rights reserved.
26.4 Painter App: RadioButtons, Mouse Events and Shapes
5
TitledPane control RadioButton control
Selected RadioButton control
VBox layout
AnchorPane layout
GridPane with one row and two columns
Fig. 26.2 |
Painter app.
TitledPane
A TitledPane displays a title at its top and is a collapsible panel containing a layout node, which in turn contains other nodes. You’ll use TitledPanes to organize the app’s RadioButtons and to help the user understand the purpose of each RadioButton group.
JavaFX Shapes The javafx.scene.shape package contains various classes for creating 2D and 3D shape nodes that can be displayed in a scene graph. In this app, you’ll programmatically create Circle objects as the user drags the mouse, then attach them to the app’s drawing area so that they’re displayed in the scene graph. AnchorPane
Each Circle you programmatically create is attached to an AnchorPane layout (the drawing area) at a specified x-y coordinate measured from the AnchorPane’s upper-left corner.
Mouse Event Handling When you drag the mouse, the app’s controller responds by displaying a Circle (in the currently selected color and pen size) at the current mouse position in the AnchorPane. JavaFX nodes support various mouse events, which are summarized in Fig. 26.3. For this app, you’ll configure an onMouseDragged event handler for the AnchorPane. JavaFX also supports other types of input events. For example, for touchscreen devices there are various touch-oriented events and for keyboards there are various key events. For a complete list of JavaFX node events, see the Node class’s properties that begin with the word “on” at: http://docs.oracle.com/javase/8/javafx/api/javafx/scene/Node.html © 2017 Pearson Education, Inc., Hoboken, NJ. All rights reserved.
6
Chapter 26
JavaFX GUI: Part 2
Common mouse and keyboard events onMouseClicked
onMouseDragEntered
onMouseDragExited
onMouseDragged
onMouseDragOver
onMouseDragReleased
onMouseEntered onMouseExited onMouseMoved
onMousePressed
onMouseReleased
Occurs for a given node when the user clicks a mouse button within that node—that is, presses and releases a mouse button without moving the mouse. Occurs for a given node when the mouse enters a node’s bounds during a drag operation (that is, the user is moving the mouse with a mouse button pressed). Occurs on a given node when the mouse exits the node’s bounds during a drag operation. Occurs for a given node when the user begins a drag operation within that node and continues moving the mouse with a mouse button pressed. Occurs for a given node when a drag operation that started in a different node continues over the given node. Occurs for a given node when the user completes a drag operation that began in that node. Occurs for a given node when the mouse enters that node’s bounds. Occurs for a given node when the mouse exits that node’s bounds. Occurs for a given node when the mouse moves within that node’s bounds. Occurs for a given node when user presses a mouse button with the mouse within that node’s bounds. Occurs for a given node when user releases a mouse button with the mouse within that node’s bounds.
Fig. 26.3 | Mouse events.
Setting a Control’s User Data Each JavaFX control has a setUserData method that receives an Object. You can use this to store any object you’d like to associate with that control. For the drawing color RadioButtons, we store the specific Color that the RadioButton represents. For the pen size RadioButtons, we store an enum constant for the corresponding pen size. We then use these objects when handling the RadioButton events.
26.4.2 Creating the Project In NetBeans 8, click the New Project… ( ) button on the toolbar or select File > New Project…. In the New Project dialog. Under Categories, select JavaFX and under Projects select JavaFX FXML Application, then click Next > and specify Painter in the Project Name, FXML name and Create Application Class fields. Click Finish to create the project.
26.4.3 Building the GUI In this section, we’ll discuss the Painter app’s GUI. Rather than providing the exact steps as we did in Chapter 25, we’ll provide general instructions for building the GUI and focus on specific details for new concepts. As you build the GUI, recall that it’s often easier to © 2017 Pearson Education, Inc., Hoboken, NJ. All rights reserved.
26.4 Painter App: RadioButtons, Mouse Events and Shapes
7
manipulate layouts and controls via Scene Builder’s Document window than directly in the stage design area. fx:id Property
Values for This App’s Controls Figure 26.4 shows the fx:id properties of the Painter app’s programmatically manipulated controls. As you build the GUI, you should set the corresponding fx:id properties in the FXML document, as we discussed in Chapter 25.
blackRadioButton redRadioButton greenRadioButton blueRadioButton
smallRadioButton mediumRadioButton largeRadioButton undoButton clearButton
drawingAreaAnchorPane
Fig. 26.4 | Painter app’s programmatically manipulated controls labeled with their fx:ids.
Step 1: Changing the Root Layout from an AnchorPane to a GridPane Open Painter.fxml in Scene Builder so that you can build the GUI and delete the default controls, then change from the default AnchorPane to a GridPane: 1. Drag a GridPane from the Library window’s Containers section onto the default AnchorPane in Scene Builder’s content panel. 2. Select Edit > Trim Document to Selection to remove the AnchorPane and make the GridPane the root layout. Step 2: Configuring the GridPane By default, the GridPane contains two columns and three rows. This app’s GridPane requires only one row with two columns, so delete rows 1 and 2 by right clicking the tab containing the row number and selecting Delete. We also set the GridPane’s Pref Width and Pref Height properties to 640 and 480 respectively. Recall that the stage’s size is determined based on the size of the root node in the FXML document. Set the GridPane’s Hgap and Padding properties to 8 to inset the GridPane from the stage’s edges and to provide space between its columns. © 2017 Pearson Education, Inc., Hoboken, NJ. All rights reserved.
8
Chapter 26
JavaFX GUI: Part 2
Step 3: Adding the VBox and AnchorPane Drag a VBox into the GridPane’s left column and an AnchorPane into the right column. Be sure to set the AnchorPane’s fx:id as specified in Fig. 26.4. For the VBox, set its Spacing property to 8 to add some vertical spacing between the controls that will be added to this container. Also set its Pref Width and Pref Height properties to USE_COMPUTED_SIZE and its Max Height property to MAX_VALUE. This will enable the Vbox to be as wide as it needs to be to accomodate its child nodes and occupy the full column height. Set the AnchorPane’s Pref Width and Pref Height properties to USE_COMPUTED_SIZE, and its Max Width and Max Height properties to MAX_VALUE so that it occupies the full width and height of the right column. In the JavaFX CSS section of the Properties window, click the field below Style (which is initially empty) and select -fx-background-color to indicate that you’d like to specify the AnchorPane’s background color. In the field to the right, specify white. Step 4: Adding the TitledPane s to the VBox From the Library window’s Containers section, drag two TitledPanes onto the VBox. For the first TitledPane, double click its title and replace the text with Drawing Color. For the second, replace its title with Pen Size. Step 5: Customizing the TitledPane s By default, a TitledPane contains an AnchorPane to which you can attach controls. For this app, we’ll replace each AnchorPane with a VBox that automatically arranges its children and can provide uniform spacing between them. To do so, select the first TitledPane’s AnchorPane and delete it, then drag a VBox onto that TitledPane in the Document window. For each VBox, set its Spacing property to 8 and its Pref Width and Pref Height to USE_COMPUTED_SIZE so the VBoxes will be sized based on their contents. Step 6: Adding the RadioButton s to the VBox From the Library window’s Controls section, drag four RadioButtons onto the VBox for the first TitledPane, and three RadioButtons onto the VBox for the second, then configure their Text properties and fx:ids as shown in Fig. 26.4. Select the blackRadioButton and ensure that its Selected property is checked, then do the same for the mediumRadioButton. Step 7: Specifying the ToggleGroups s for the RadioButton s Select all four RadioButtons in the first TitledPane’s VBox, then set the Toggle Group property to colorToggleGroup. When the FXML file is loaded, a ToggleGroup object by that name will be created and these four RadioButtons will be associated with it to ensure that only one is selected at a time. Repeat this step for the three RadioButtons in the first TitledPane’s VBox, but set the Toggle Group property to sizeToggleGroup. Step 8: Adding the Button s Add two Buttons below the TitledPanes, then configure their Text properties and fx:ids as shown in Fig. 26.4. Set each Button’s Max Width property to MAX_VALUE so that they fill the width of GridPane’s first column.
© 2017 Pearson Education, Inc., Hoboken, NJ. All rights reserved.
26.4 Painter App: RadioButtons, Mouse Events and Shapes
9
Step 9: Setting the Width of the GridPane ’s First Column We’d like the left column to be only as wide as it needs to be to display the controls in that column. To specify this, select the GridPane in the Document window, then click the tab at the top of column 0 to select the column. Set the column’s Min Width and Pref Width to USE_COMPUTED_SIZE, then set the Max Width to USE_PREF_SIZE (which indicates that the maximum width should be the preferred width). The GUI is now complete and should appear as shown in Fig. 26.4.
26.4.4 Painter Subclass of Application Figure 26.5 shows class Painter, which NetBeans created for you when you created the app’s project in Section 26.4.2 Painter is the Application subclass that launches the app. We reformatted the code to this our conventions, modified the autogenerated comments and added line 18 to specify the title bar String that appears at the top of the app’s window when the app executes. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
// Fig. 26.5: Painter.java // Main application class that loads and displays the Painter's GUI. import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.stage.Stage; public class Painter extends Application { @Override public void start(Stage stage) throws Exception { Parent root = FXMLLoader.load(getClass().getResource("Painter.fxml")); Scene scene = new Scene(root); stage.setTitle("Painter"); // displayed in window's title bar stage.setScene(scene); stage.show();
}
public static void main(String[] args) { launch(args); }
}
Fig. 26.5 | Main application class that loads and displays the Painter 's GUI.
26.4.5 PainterController Class Figure 26.6 shows the final version of class PainterController with this app’s new JavaFX features highlighted. Recall from Chapter 25 that the controller class defines instance variables for interacting with controls programmatically, as well as event-handling © 2017 Pearson Education, Inc., Hoboken, NJ. All rights reserved.
Chapter 26
10
JavaFX GUI: Part 2
methods. The controller class may also declare additional instance variables, ables and methods that support the app’s operation. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
static vari-
// Fig. 26.6: PainterController.java // Controller for the Painter app import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.RadioButton; import javafx.scene.control.ToggleGroup; import javafx.scene.input.MouseEvent; import javafx.scene.layout.AnchorPane; import javafx.scene.paint.Color; import javafx.scene.paint.Paint; import javafx.scene.shape.Circle; public class PainterController { // enum representing pen sizes private enum PenSize { SMALL(2), MEDIUM(4), LARGE(6); private final int radius;
PenSize(int radius) { this.radius = radius; } public int getRadius() { return radius; } }; // instance variables that refer to GUI components @FXML private RadioButton blackRadioButton; @FXML private RadioButton redRadioButton; @FXML private RadioButton greenRadioButton; @FXML private RadioButton blueRadioButton; @FXML private RadioButton smallRadioButton; @FXML private RadioButton mediumRadioButton; @FXML private RadioButton largeRadioButton; @FXML private AnchorPane drawingAreaAnchorPane; @FXML private ToggleGroup colorToggleGroup; @FXML private ToggleGroup sizeToggleGroup; // instance variables for managing Painter state private PenSize radius = PenSize.MEDIUM; // radius of circle private Paint brushColor = Color.BLACK; // drawing color
Fig. 26.6 | Controller for the Painter app. (Part 1 of 3.) © 2017 Pearson Education, Inc., Hoboken, NJ. All rights reserved.
26.4 Painter App: RadioButtons, Mouse Events and Shapes
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
11
// set user data for the RadioButtons public void initialize() { // user data on a control can be any Object blackRadioButton.setUserData(Color.BLACK); redRadioButton.setUserData(Color.RED); greenRadioButton.setUserData(Color.GREEN); blueRadioButton.setUserData(Color.BLUE); smallRadioButton.setUserData(PenSize.SMALL); mediumRadioButton.setUserData(PenSize.MEDIUM); largeRadioButton.setUserData(PenSize.LARGE); } // handles drawingArea's onMouseDragged MouseEvent @FXML private void drawingAreaMouseDragged(MouseEvent e) { Circle newCircle = new Circle(e.getX(), e.getY(), radius.getRadius(), brushColor); drawingAreaAnchorPane.getChildren().add(newCircle); } // handles color RadioButton's ActionEvents @FXML private void colorRadioButtonSelected(ActionEvent e) { // user data for each color RadioButton is the corresponding Color brushColor = (Color) colorToggleGroup.getSelectedToggle().getUserData(); } // handles size RadioButton's ActionEvents @FXML private void sizeRadioButtonSelected(ActionEvent e) { // user data for each size RadioButton is the corresponding PenSize radius = (PenSize) sizeToggleGroup.getSelectedToggle().getUserData(); } // handles Undo Button's ActionEvents @FXML private void undoButtonPressed(ActionEvent event) { int count = drawingAreaAnchorPane.getChildren().size(); // if there are any shapes remove the last one added if (count > 0) drawingAreaAnchorPane.getChildren().remove(count - 1); }
Fig. 26.6 | Controller for the Painter app. (Part 2 of 3.) © 2017 Pearson Education, Inc., Hoboken, NJ. All rights reserved.
Chapter 26
12
102 103 104 105 106 107 108
JavaFX GUI: Part 2
// handles Clear Button's ActionEvents @FXML private void clearButtonPressed(ActionEvent event) { drawingAreaAnchorPane.getChildren().clear(); // clear the canvas }
}
Fig. 26.6 | Controller for the Painter app. (Part 3 of 3.) PenSize enum
Lines 16–33 define the nested enum type PenSize, which specifies three pen sizes— SMALL, MEDIUM and LARGE. Each has a corresponding radius that will be used when creating a Circle object to display in response to a mouse-drag event.
Instance Variables Lines 36–45 declare the @FXML instance variables that the controller uses to programmatically interact with the app’s GUI. Recall from Chapter 25 that the names of these variables must match the corresponding fx:id values that you specified in Painter.fxml; otherwise, the FXMLLoader will not be able to connect the GUI components to the instance variables. Notice that two of the @FXML instance variables are ToggleGroups—in the RadioButton event handlers, we’ll use these to determine which RadioButton was selected. Lines 48–49 define two additional instance variables that store the current drawing Color and the current PenSize, respectively. Method initialize Lines 52–62 define method initialize, which is called to initialize the controller after the GUI is created. For the Painter app, we use this method to specify each RadioButton’s corresponding user data object—either a Color or a PenSize. You’ll use these in the RadioButton event handlers. drawingAreaMouseDragged Event
Lines 65–71 define
Handler
drawingAreaMouseDragged,
which responds to drag events in the drawingAreaAnchoredPane. Each mouse event handler you define must provide one parameter of type MouseEvent (package javafx.scene.input). When the event occurs, this parameter contains information about the mouse event, such as its location, whether any mouse buttons were pressed, which node the user interacted with and more. To specify this event handler in Scene Builder, first select the AnchoredPane. Then in the inspector’s Code section, locate the Mouse subsection and select drawingAreaMouseDragged from the On Mouse Dragged drop-down list. This connects the AnchoredPane to the event handler. Lines 68–69 create a new Circle object using the constructor that takes as arguments the center point’s x -coordinate, the center point’s y -coordinate, the Circle’s radius and the Circle’s Color. Next, line 70 attaches the new Circle to the drawingAreaAnchoredPane at the specified center point. Each layout pane has a getChildren method that returns an ObservableList collection containing the layout’s child nodes. An ObservableList is an © 2017 Pearson Education, Inc., Hoboken, NJ. All rights reserved.
26.4 Painter App: RadioButtons, Mouse Events and Shapes
13
implementation of the List interface (Chapter 16)—as such, it provides methods for adding elements to and removing elements from a list. You’ll learn more about ObservableList later in this chapter. Line 70 uses the ObservableList’s add method to add a new Node to the drawingAreaAnchoredPane—all JavaFX shapes inherit indirectly from class Node in the javafx.scene package. colorRadioButtonSelected Event
Handler Lines 74–80 define colorRadioButtonSelected, which responds to the ActionEvents of the drawing color RadioButtons—these occur each time a new RadioButton is selected. To specify this event handler in Scene Builder, first select the four RadioButtons in the Drawing Color TitledPane. Then in the inspector’s Code section, locate the Main subsection and select colorRadioButtonSelected from the On Action drop-down list. This connects all four RadioButtons to the event handler. Lines 78–79 set the current drawing Color. ColorToggleGroup method getSelectedToggle returns the Toggle that’s currently selected. Class RadioButton is one of several controls (others are RadioButtonMenuItem and ToggleButton) that implements the Toggle interface. We then use the Toggle interface’s getUserData method to get the user data Object that was associated with the corresponding RadioButton in method initialize. For the color RadioButtons, this Object is aways a Color, so we cast the Object to a Color and assign it to brushColor. sizeRadioButtonSelected Event
Handler Lines 83–89 define sizeRadioButtonSelected, which responds to the ActionEvents of the pen size RadioButtons. To specify this event handler in Scene Builder, first select the three RadioButtons in the Pen Size TitledPane. Then in the inspector’s Code section, locate the Main subsection and select sizeRadioButtonSelected from the On Action dropdown list. Lines 87–88 set the current PenSize, using the same approach as setting the current color in method colorRadioButtonSelected. undoButtonPressed Event
Handler
Lines 92–100 define undoButtonPressed, which responds to an ActionEvent from the undoButton by removing the last Circle displayed. To specify this event handler in Scene Builder, first select the undoButton. Then in the inspector’s Code section, locate the Main subsection and select undoButtonPressed from the On Action drop-down list. To undo the last Circle that was displayed, we remove the last child from the drawingAreaAnchoredPane’s collection of child nodes. First, line 95 gets the number of elements in that collection. Then, if that’s greater than 0, line 99 removes the node at the last index in the collection. clearButtonPressed Event
Handler
Lines 103–107 define clearButtonPressed, which responds to the ActionEvent from the clearButton by clearing drawingAreaAnchoredPane’s collection of child nodes. To specify this event handler in Scene Builder, first select the clearButton. Then in the inspector’s Code section, locate the Main subsection and select clearButtonPressed from the On Action drop-down list. Line 106 clears the collection.
© 2017 Pearson Education, Inc., Hoboken, NJ. All rights reserved.
14
Chapter 26
JavaFX GUI: Part 2
26.5 Color Chooser App: Property Bindings and Property Listeners In this section, we present a Color bindings and property listeners.
Chooser app
TextField
(Fig. 26.7) that demonstrates property
Rectangle
a) Using the Red and Green Sliders to create an opaque orange color R: 255 G: 128 B: 0 A: 255
b) Using the Red, Green and Alpha Sliders to create a semi-transparent orange color
Fig. 26.7 |
R: 255 G: 128 B: 0 A: 128
Color Chooser app with opaque and semitransparent orange colors.
26.5.1 Technologies Overview In this section, we introduce the technologies you’ll use to build the Color Chooser .
RGBA Colors The app uses the RGBA color system to display a rectangle of color based on the values of four Sliders. In RGBA, every color is represented by its red, green and blue color values, each ranging from 0 to 255, where 0 denotes no color and 255 full color. For example, a color with a red value of 0 would contain no red component. The alpha value (A)— which ranges from 0.0 to 1.0—represents a color’s opacity , with 0.0 being completely transparent and 1.0 completely opaque . The two colors in Fig. 26.7’s sample outputs have the same RGB values, but the color displayed in Fig. 26.7(b) is semitransparent . You’ll use a Color object that’s created with RGBA values to fill a Rectangle that displays the Color. Properties of a Class JavaFX makes extensive use of properties. A property is defined by creating set and get methods with specific naming conventions. Consider class Time2 (Fig. 8.5), which provided set and get methods for the hour, minute and second. The methods that begin with the following first lines: public void setHour(int hour) public int getHour() © 2017 Pearson Education, Inc., Hoboken, NJ. All rights reserved.
26.5 Color Chooser App: Property Bindings and Property Listeners
15
define a read/write property named hour. Typically, such methods manipulate a corresponding private instance variable that has the same name as the property, but this is not required. In general, the pair of methods that define a read/write property have the form: public void setPropertyName (Type propertyName ) public Type getPropertyName ()
If the property represents a boolean value, its get method name typically begins with “ is” rather than “get”, as in: public boolean isPropertyName ()
Property Bindings JavaFX properties are observable —when a property’s value changes, other objects can be respond accordingly. This is similar to event handling. One way to respond to a property change is via a property binding , which enables a property of one object to be updated when a property of another object changes. For example, you’ll use property bindings to enable a TextField to display the corresponding Slider’s current value when the user moves that Slider’s thumb. Property bindings are not limited to JavaFX controls. Package javafx.beans.property contains many classes that you can use to define bindable properties in your own classes. Property Listeners Property listeners are similar to property bindings. A property listener is an event handler that’s invoked when a property’s value changes. In the event handler, you can respond to the property change in a manner appropriate for your app. In this app, when a Slider’s value changes, a property listener will store the value in a corresponding instance variable, create a new Color based on the values of all four Sliders and set that Color as the fill color of a Rectangle object that displays the current color. For more information on properties, property bindings and property listeners, visit: http://docs.oracle.com/javase/8/javafx/properties-binding-tutorial/ binding.htm
26.5.2 Creating the Project In NetBeans 8, click the New Project… ( ) button on the toolbar or select File > New Project…. In the New Project dialog. Under Categories, select JavaFX and under Projects select JavaFX FXML Application, then click Next > and specify ColorChooser in the Project Name, FXML name and Create Application Class fields. Click Finish to create the project.
26.5.3 Building the GUI In this section, we’ll discuss the Painter app’s GUI. Rather than providing the exact steps as we did in Chapter 25, we’ll provide general instructions for building the GUI and focus on specific details for new concepts. As you build the GUI, recall that it’s often easier to manipulate layouts and controls via Scene Builder’s Document window than directly in the stage design area.
© 2017 Pearson Education, Inc., Hoboken, NJ. All rights reserved.
16
Chapter 26
JavaFX GUI: Part 2
fx:id Property
Values for This App’s Controls Figure 26.8 shows the fx:id properties of the Color Choooser app’s programmatically manipulated controls. As you build the GUI, you should set the corresponding fx:id properties in the FXML document, as you learned in Chapter 25. redTextField
greenTextField
blueTextField
redSlider greenSlider blueSlider colorRectangle
alphaSlider
alphaTextField
GridPane with four rows and four columns
Fig. 26.8 | Color Chooser app’s programmatically manipulated controls labeled with their fx:ids.
Step 1: Changing the Root Layout from an AnchorPane to a GridPane Open ColorChooser.fxml in Scene Builder so that you can build the GUI and delete the default controls, then change from the default AnchorPane to a GridPane: 1. Drag a GridPane from the Library window’s Containers section onto the default AnchorPane in Scene Builder’s content panel. 2. Select Edit > Trim Document to Selection to remove the AnchorPane make the GridPane the root layout. Step 2: Configuring the GridPane This app’s GridPane requires four rows and four columns. Use the techniques you’ve learned previously to add two columns and one row to the GridPane. Set the GridPane’s Hgap and Padding properties to 8 to inset the GridPane from the stage’s edges and to provide space between its columns. Step 3: Adding the Controls Using Fig. 26.8 as a guide, add the Labels, Sliders, TextFields and a Rectangle to the GridPane. Be sure to set the text of the Labels and TextFields as shown and to set all the appropriate fx:id properties. Step 4: Configuring the Slider s For the red, green and blue Sliders, set the Max properties to 255 (the maximum amount of a given color in the RGBA color scheme). For the alpha Slider, set its Max property to 1.0 (the maximum opacity in the RGBA color scheme). Step 5: Configuring the TextField s Set all of the TextFields Pref Width properties to 50. © 2017 Pearson Education, Inc., Hoboken, NJ. All rights reserved.
26.5 Color Chooser App: Property Bindings and Property Listeners
17
Step 6: Configuring the Rectangle Set the Rectangle’s Width and Height properties to 100, then set its Row Span property to Remainder so that it spans all four rows. Step 7: Configuring the Columns Set all four columns’ Pref Width properties to USE_COMPUTED_SIZE so that the columns are only as wide as their content. Your GUI should now appear as shown in Fig. 26.8.
26.5.4 ColorChooser Subclass of Application Figure 26.5 shows class ColorChooser, which NetBeans created for you when you created the app’s project. ColorChooser is the Application subclass that launches the app. We reformatted the code to our coding conventions, modified the autogenerated comments and added line 18 to specify the title bar String that appears at the top of the app’s window when the app executes. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
// Fig. 26.8: ColorChooser.java // Main application class that loads and displays the ColorChooser's GUI. import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.stage.Stage; public class ColorChooser extends Application { @Override public void start(Stage stage) throws Exception { Parent root = FXMLLoader.load(getClass().getResource("ColorChooser.fxml")); Scene scene = new Scene(root); stage.setTitle("Color Chooser"); stage.setScene(scene); stage.show();
}
public static void main(String[] args) { launch(args); }
}
Fig. 26.9 | Main application class that loads and displays the Color Chooser 's GUI.
© 2017 Pearson Education, Inc., Hoboken, NJ. All rights reserved.
Chapter 26
18
JavaFX GUI: Part 2
26.5.5 ColorChooserController Class Figure 26.10 shows the final version of class new JavaFX features highlighted.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
ColorChooserController with
this app’s
// Fig. 26.9: ColorChooserController.java // Controller for the ColorChooser app import javafx.fxml.FXML; import javafx.scene.control.Slider; import javafx.scene.control.TextField; import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; public class ColorChooserController { // instance variables for interacting with GUI components @FXML private Slider redSlider; @FXML private Slider greenSlider; @FXML private Slider blueSlider; @FXML private Slider alphaSlider; @FXML private TextField redTextField; @FXML private TextField greenTextField; @FXML private TextField blueTextField; @FXML private TextField alphaTextField; @FXML private Rectangle colorRectangle; // instance variables for managing private int red = 0; private int green = 0; private int blue = 0; private double alpha = 1.0;
public void initialize() { // bind TextField values to corresponding Slider values redTextField.textProperty().bind( redSlider.valueProperty().asString("%.0f")); greenTextField.textProperty().bind( greenSlider.valueProperty().asString("%.0f")); blueTextField.textProperty().bind( blueSlider.valueProperty().asString("%.0f")); alphaTextField.textProperty().bind( alphaSlider.valueProperty().asString("%.2f")); // listeners that set Rectangle's fill based on Slider changes redSlider.valueProperty().addListener( (observableValue, oldValue, newValue) -> { red = newValue.intValue(); colorRectangle.setFill(Color.rgb(red, green, blue, alpha)); });
Fig. 26.10 | Color-chooser app showing the use of styles (code-behind). (Part 1 of 2.) © 2017 Pearson Education, Inc., Hoboken, NJ. All rights reserved.
26.5 Color Chooser App: Property Bindings and Property Listeners
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
19
greenSlider.valueProperty().addListener( (observableValue, oldValue, newValue) -> { green = newValue.intValue(); colorRectangle.setFill(Color.rgb (red, green, blue, alpha)); }); blueSlider.valueProperty().addListener( (observableValue, oldValue, newValue) -> { blue = newValue.intValue(); colorRectangle.setFill(Color.rgb (red, green, blue, alpha)); }); alphaSlider.valueProperty().addListener( (observableValue, oldValue, newValue) -> { alpha = newValue.doubleValue(); colorRectangle.setFill(Color.rgb (red, green, blue, alpha)); });
} }
Fig. 26.10 | Color-chooser app showing the use of styles (code-behind). (Part 2 of 2.)
Instance Variables Lines 12–26 declare the controller’s instance variables. Variables red, green, blue and alpha store the current values of the redSlider, greenSlider, blueSlider and alphaSlider, respectively. These values are used to update the colorRectangle’s fill color each time the user moves a Slider’s thumb. Method initialize Lines 28–65 define method initialize, which initializes the controller after the GUI is created. In this app, initialize configures the property bindings and property listeners. Property-to-Property Bindings Lines 31–38 set up property bindings between a Slider’s value and the corresponding TextField’s text so that changing a Slider updates the corresponding TextField. Consider lines 31–32, which bind the redSlider’s valueProperty to the redTextField’s textProperty: redTextField.textProperty().bind( redSlider.valueProperty().asString("%.0f"));
Each TextField has a text property that’s returned by its textProperty method as StringProperty (package javafx.beans.property ). StringProperty method bind receives an ObservableValue as an argument. When the ObservableValue changes, the bound property is updated accordingly. In this case the ObservableValue is the result of the expression redSlider.valueProperty().asString("%.0f") . Slider method valueProperty returns the Slider’s value property as an object of class DoubleProperty —an observable double value. Because the TextField’s text property must be bound to a String, we call DoubleProperty method asString. This returns a StringBinding object (which is an ObservableValue) that produces a String representation of the DoubleProperty. This version of asString receives a format-control String specifying the DoubleProperty’s format. © 2017 Pearson Education, Inc., Hoboken, NJ. All rights reserved.
20
Chapter 26
JavaFX GUI: Part 2
Property Listeners To perform an arbitrary task when a propery’s value changes, you can register a property listener. Lines 41–64 register the property listeners for the Sliders’ value properties. Consider lines 41–46: redSlider.valueProperty().addListener( (observableValue, oldValue, newValue) -> { red = newValue.intValue(); colorRectangle.setFill(Color.rgb(red, green, blue, alpha)); });
First, we call the redSlider’s valueProperty method to get the property’s DoubleProperty object (which is an ObservableValue) has an addListener method that registers an object of the functional interface ChangeListener to respond when a property change occurs. The ChangeListener’s changed method has three parameters—the ObservableValue, the property’s old value and the property’s new value. For a DoubleProperty, the method’s parameter types are ObservableValue extends Number>, Number and Number, respectively. In lines 41–64, we implement the ChangeListeners as lambda expressions and the parameter’s types are inferred from the context. Each ChangeListeners stores the int value of the newValue parameter in a corresponding instance variable, then call the colorRectangle’s setFill method to change its color.
26.6 Cover Viewer App: Data-Drivien GUIs with JavaFX Collections Often, an app needs to edit and display data. JavaFX provides a comprehensive model for allowing GUIs to interact with data. In this section, you’ll build the Cover Viewer app (Fig. 26.11), which binds a list of Book objects to a ListView. When the user selects an item in the ListView, the corresponding Book’s cover image is displayed in an ImageView.
26.6.1 Technologies Overview This app uses a ListView control to display a collection of book titles. Though you can individually add items to a ListView, in this app you’ll bind an ObservableList object to the ListView. If you make changes to an ObservableList, its observer (the ListView in this app) will automatically be notified of those changes. Package javafx.collections defines ObservableList and other observable collection interfaces. The package also contains class FXCollections, which provides static methods for creating and manipulating observable collections. You’ll also use a property listener to display the correct image when the user selects an item from the ListView—in this case, the property that changes is the selected item.
26.6.2 Creating the Project In NetBeans 8, click the New Project… ( ) button on the toolbar or select File > New Project…. In the New Project dialog. Under Categories, select JavaFX and under Projects select JavaFX FXML Application, then click Next > and specify CoverViewer in the Project Name, FXML name and Create Application Class fields. Click Finish to create the project. © 2017 Pearson Education, Inc., Hoboken, NJ. All rights reserved.
26.6 Cover Viewer App: Data-Drivien GUIs with JavaFX Collections
21
Fig. 26.11 | Cover Viewer with Java How to Program selected.
26.6.3 Adding Images to the Project From this chapter’s examples folder, drag the images folder (which contains the large and small subfolders) onto your project’s Source Packages node. This creates new packages named images.large and images.small for each subfolder. Though you’ll use only the large images in this example, you’ll copy this project to create the next example, which uses both sets of images.
26.6.4 Building the GUI In this section, we’ll discuss the
Cover Viewer app’s
GUI.
fx:id Property
Values for This App’s Controls Figure 26.12 shows the fx:id properties of the Color Choooser app’s programmatically manipulated controls. As you build the GUI, you should set the corresponding fx:id properties in the FXML document, as we discussed in Chapter 25. Adding and Configuring the Controls Using the techniques you learner earlier, switch the root element of this app’s FXML document to a GridPane, then ensure that it has only one row and two columns. In the left column, place a ListView control, and in the right, place an ImageView control. Set the ListView’s Pref Width property to 200 and its Max Height property to MAX_VALUE. Ensure that its Min Width, Min Height, Pref Height and Max Width properties are all set to USE_COMPUTED_SIZE. Set the ImageView’s Fit Width and Fit Height properties to 370 and 480, respectively. © 2017 Pearson Education, Inc., Hoboken, NJ. All rights reserved.
22
Chapter 26
JavaFX GUI: Part 2
booksListView
coverImageView
GridPane with one row and two columns
Fig. 26.12 | Cover Viewer app’s programmatically manipulated controls labeled with their fx:ids.
For both GridPane columns, set the Pref Width property to USE_COMPUTED_SIZE, and set the Min Width and Max Width properties are all set to USE_PREF_SIZE. This ensures that each column’s width is sized based on its contents. For the GridPane’s row, set the Pref Height property to USE_COMPUTED_SIZE, and set the Min Height and Max Height properties are all set to USE_PREF_SIZE. This ensures that the row is as tall as its tallest child node.
26.6.5 CoverViewer Subclass of Application Figure 26.13 shows class CoverViewer, which NetBeans created for you when you created the app’s project. We reformatted the code to our coding conventions, modified the autogenerated comments and added line 18 to specify the title bar String that appears at the top of the app’s window when the app executes. 1 2 3
// Fig. 26.13: CoverViewer.java // Main application class that loads and displays the CoverViewer's GUI. import javafx.application.Application;
Fig. 26.13 | Main application class that loads and displays the Cover Viewer 's GUI. © 2017 Pearson Education, Inc., Hoboken, NJ. All rights reserved.
26.6 Cover Viewer App: Data-Drivien GUIs with JavaFX Collections
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
23
import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.stage.Stage; public class CoverViewer extends Application { @Override public void start(Stage stage) throws Exception { Parent root = FXMLLoader.load(getClass().getResource("CoverViewer.fxml")); Scene scene = new Scene(root); stage.setTitle("Cover Viewer"); stage.setScene(scene); stage.show();
}
public static void main(String[] args) { launch(args); }
}
Fig. 26.13 | Main application class that loads and displays the Cover Viewer 's GUI.
26.6.6 CoverViewerController Class Figure 26.10 shows the final version of class CoverViewerController with the app’s new JavaFX features highlighted. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
// Fig. 26. 14: CoverViewerController.java // Controller for Cover Viewer application import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.ListView; import javafx.scene.image.Image; import javafx.scene.image.ImageView; public class CoverViewerController { // instance variables for interacting with GUI @FXML private ListView booksListView; @FXML private ImageView coverImageView; // stores the list of Book Objects private final ObservableList books = FXCollections.observableArrayList();
Fig. 26.14 | Controller for Cover Viewer application. (Part 1 of 2.) © 2017 Pearson Education, Inc., Hoboken, NJ. All rights reserved.
Chapter 26
24
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
JavaFX GUI: Part 2
// initialize controller public void initialize() { // populate the ObservableList books.add(new Book("Android How to Program", "/images/small/androidhtp.jpg", "/images/large/androidhtp.jpg")); books.add(new Book("C How to Program", "/images/small/chtp.jpg", "/images/large/chtp.jpg")); books.add(new Book("C++ How to Program", "/images/small/cpphtp.jpg", "/images/large/cpphtp.jpg")); books.add(new Book("Internet and World Wide Web How to Program", "/images/small/iw3htp.jpg", "/images/large/iw3htp.jpg")); books.add(new Book("Java How to Program", "/images/small/jhtp.jpg", "/images/large/jhtp.jpg")); books.add(new Book("Visual Basic How to Program", "/images/small/vbhtp.jpg", "/images/large/vbhtp.jpg")); books.add(new Book("Visual C# How to Program", "/images/small/vcshtp.jpg", "/images/large/vcshtp.jpg")); booksListView.setItems(books); // bind booksListView to books // when ListView selection changes, show large cover in ImageView booksListView.getSelectionModel().selectedItemProperty().addListener( (observableValue, oldValue, newValue) -> coverImageView.setImage(new Image(newValue.getLargeImage()))); }
}
Fig. 26.14 | Controller for Cover Viewer application. (Part 2 of 2.) @FXML Instance
Variables Lines 13–14 declare the controller’s @FXML instance variables. Notice that ListView is a generic class. In this case, the ListView displays Book objects. Class Book contains three String instance variables with corresponding set and get methods: • title—the book’s title • thumbImage—the path to the book’s thumbnail image (used in the next example). • largeImage—the path to the book’s large cover image. The class also provides a toString method that returns the Book’s title and a constructor that initializes the three instance variables. You can add class Book to your project by dragging it from this chapter’s examples folder onto the NetBeans project node that contains the CoverViewer.fxml, CoverViewer.java and CoverViewerController.java files ( if you followed our instructions for setting up the project). Instance Variable books Lines 17–18 define the books instance variable as an ObservableList and initialize it by calling FXCollections static method observableArrayList. This method returns an empty collection object that implements the ObservableList interface. Initializing the books ObservableList Lines 24–36 in method initialize create and add Book objects to the books collection. Line 38 passes this collection to ListView method setItems, which binds the ListView © 2017 Pearson Education, Inc., Hoboken, NJ. All rights reserved.
26.7 Cover Viewer App: Customizing ListView Cells
25
to the ObservableList. By default, each Book’s String representation is displayed in the ListView. (In the next example, you’ll customize this.)
Listening for ListView Selection Changes To synchronize the book cover that’s being displayed with the currently selected book, we listen for changes to the selected item. By default a ListView supports single selection— one item at a time may be selected. ListView’s also support multiple selection. The type of selection is managed by the ListView’s MultipleSelectionModel (a subclass of SelectionModel from package javafx.scene.control), which contains observable properties and various methods for manipulating the corresponding ListView’s items. To respond to selection changes, you register a listener for the MultipleSelectionModel’s selectedItem property (lines 41–43). ListView method getSelectionModel returns a MultipleSelectionModel object. In this example, MultipleSelectionModel’s selectedItemProperty method returns a ReadOnlyObjectProperty and the corresponding ChangeListener lambda expression receives as its oldValue and newValue parameters the previously selected Book and newly selected Book objects, respectively. Line 43 uses newValue’s large image path to initialize a new Image (package javafx.scene.image)—this loads the image from that path. We then pass the new Image to the colorImageView’s setImage method to display the Image.
26.7
Cover Viewer App:
Customizing ListView Cells
In the preceding example, the each ListView displayed a Book’s String representation (i.e., its title). In this example, you’ll create a custom ListView cell factory to create cells that display each book as its thumbnail image and title using a VBox, an ImageView and a Label (Fig. 26.15).
Fig. 26.15 |
Cover Viewer app with Java How to Program selected. © 2017 Pearson Education, Inc., Hoboken, NJ. All rights reserved.
26
Chapter 26
JavaFX GUI: Part 2
26.7.1 Technolgies Overview ListCell Generic Class for Custom ListView Cell Formats As you saw in Section 26.6, ListView cells display the String representations of a ListView’s items by default. To create a custom cell format, you must first define a subclass of the ListCell generic class (package javafx.scene.control) that specifies how to create a ListView cell. As the ListView displays items, it gets ListView cells from its cell factory. You’ll use the ListView’s setCellFactory method to replace the default cell factory with one that returns objects of the ListCell subclass. You’ll override this class’s updateItem method to specify the cells’ custom layout and contents.
Programmatically Creating Layouts and Controls So far, you’ve created GUIs visually using JavaFX Scene Builder. In this app, you’ll also create a portion of the GUI programmatically—similar to what you did with Swing GUIs earlier in this book. In particular, you’l create and configure a VBox layout containing an ImageView and a Label. The VBox represents the custom ListView cell format.
26.7.2 Copying the CoverViewer Project This app’s FXML layout and classes Book and CoverViewer are identical to those in Section 26.6, and the CoverViewerController class has only one new statement, so we’ll show that statement and a new class that implements the custom ListView cell factory. Rather than creating a new project for this example, you’ll copy the existing CoverViewer project. To do so, right click the project in NetBeans and select Copy…, then in the dialog that appears, specify CoverViewerCustomListView as the Project Name and click Copy.
26.7.3 ImageTextCell Custom Cell Factory Class Class ImageTextCell (Fig. 26.16) defines the custom ListView cell layout for this version of the Cover Viewer app. You can add this class to your project, by right clicking the project, selecting New > Java Class…, specifying ImageTextCell as the Class Name and clicking Finish. The class extends ListCell because it defines a customized presentation of a Book in a ListView cell. Method update (lines 13–45) creates the custom presentation. This method is called by the ListView’s cell factory when a ListView cell is required—that is, when the ListView is first displayed and when ListView cells are about to scroll onto the screen. The method receives the Book to display and a boolean indicating whether the cell that’s about to be created is empty. You must call the superclass’s version of update (line 17) to ensure that the custom cells display correctly. 1 2 3 4 5 6 7 8 9
// Fig. 26.16: ImageTextCell.java // Custom ListView cell factory that displays an image and text import javafx.geometry.Pos; import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.VBox; import javafx.scene.text.TextAlignment;
Fig. 26.16 | Custom ListView cell factory that displays an image and text. © 2017 Pearson Education, Inc., Hoboken, NJ. All rights reserved.
26.7 Cover Viewer App: Customizing ListView Cells
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
27
public class ImageTextCell extends ListCell { @Override protected void updateItem(Book item, boolean empty) { // required to ensure that cell displays properly super.updateItem(item, empty); if (empty || item == null) { setGraphic(null); // don't display anything } else { // create layout for cell VBox vbox = new VBox(8.0); // 8 points of gap between controls vbox.setAlignment(Pos.CENTER); // center contents horizontally
// configure thumbnail image ImageView thumbImageView = new ImageView(new Image(item.getThumbImage())); thumbImageView.setPreserveRatio(true); thumbImageView.setFitHeight(100.0); // thumbnail 100 points tall vbox.getChildren().add(thumbImageView); // attach to Vbox
// configure text Label label = new Label(item.getTitle()); label.setWrapText(true); // wrap if text too wide to fit in cell label.setTextAlignment(TextAlignment.CENTER); // center text vbox.getChildren().add(label); // attach to VBox
setGraphic(vbox); // attach custom layout to ListView cell setPrefWidth(USE_PREF_SIZE); // use preferred size for cell width } }
}
Fig. 26.16 | Custom ListView cell factory that displays an image and text.
If the cell is empty or the item parameter is null, then there is no Book to display and line 21 calls the ImageTextCell’s inherited setGraphic method with null. This method receives as its argument the Node that should be displayed in the cell. Any JavaFX Node can be provided, giving you tremendous flexibility for customizing a cell’s appearance. If there is a Book to display, lines 26–43 create the custom layout and use setGraphic to set it as the cell’s presentation. Lines 26–27 create a VBox object using its constructor that takes the gap between child nodes as an argument, then set the VBox’s alignment to center its children horizontally in the layout. Lines 30–34 create an ImageView and attach it to the VBox’s collection of children. First, we create the ImageView and initialize it with an Image that loads the Book’s thumbnail image (lines 30–31). Next, we indicate that the ImageView should preserve the image’s © 2017 Pearson Education, Inc., Hoboken, NJ. All rights reserved.
28
Chapter 26
JavaFX GUI: Part 2
aspect ratio (line 32) and that the ImageView should be 100 points tall (line 33). Line 34 attaches it to the VBox. Lines 37–40 create a Label and attach it to the VBox’s collection of children. First, we create the Label and initialize it with the Book’s title (line 37). Next, we indicate that the Label should wrap its text if its too wide fit in the Label’s width (line 38) and that the text should be centered (line 39). Line 40 attaches the Label to the VBox. Finally, line 42 attaches the VBox to the cell and line 41 indicates that the cell should use its preferred width, which is determined from the width of its parent ListView.
26.7.4 CoverViewerController Class Once you’ve defined the custom cell layout, updating the CoverViewerController to use it requires a single line of code, which you should add as the last statement in the CoverViewerController’s initialize method: booksListView.setCellFactory((listView) -> new ImageTextCell());
In this case, we pass to ListView method setCellFactory a lambda expression that returns a new object of the ImageTextCell class. Each time the ListView requires a new cell, this lambda expression will be invoked to get a new cell and the cell’s update method will be called to create the custom cell presentation. The argument to setCellFactory is an implementation of the functional interface CallBack (package javafx.util). This generic interface provides a call method that receives one argument and returns a value. The lambda’s argument (the ListView in which the custom cells will appear) is used as the argument to this interface’s call method and the lambda’s body (which creates the custom cells) is used as the method’s return value.
26.8 Wrap-Up In this chapter, we continued our presentation of JavaFX. We discussed JavaFX layout panes in more detail. You used TitledPanes to organize RadioButtons and an AnchorPane to display Circles. You learned about the many mouse events supported by JavaFX nodes and we used the onMouseDragged event in a simple Painter app that displayed Circles as the user dragged the mouse across an AnchorPane. The Painter app allowed the user to choose the current color and pen size from groups of mutually exclusive RadioButtons. You used ToggleGroups to manage the relationship between the RadioButtons in each group. You also learned how to provide a so-called user data Object for a control. When a RadioButton was selected, you obtained it from the ToggleGroup, then accessed the RadioButton’s user data Object to determine the drawing color or pen size. We discussed property binding and property listeners, then used them to implement a Color Chooser app. You bound a TextField’s text to a Slider’s value to automatically update the TextField when the user moved the Slider’s thumb. You also used a property listener to allow the app’s controller to update the color of a Rectangle when a Slider’s value changed. In our Cover Viewer app, we showed how to bind a ObservableList collection to a ListView control to populate it with the collection’s elements. By default, each object in © 2017 Pearson Education, Inc., Hoboken, NJ. All rights reserved.
Exercises
29
the collection was displayed as a String in the ListView. You configured a property listener to display an Image in an ImageView when the user selected an item in the ListView. Finally, we modified the Cover Viewer app to use a custom ListView cell factory to specify the exact layout of a ListView cell’s contents. In the next chapter, we’ll demonstrate additional JavaFX features, including graphics, multimedia and customizing a GUI’s look-and-feel with JavaFX’s CSS (Cascading Style Sheets) capabilities.
Exercises ( Painter App Modification) Incorporate the RGBA color chooser you created in the Color Chooser app (Section 26.5) into the Painter app (Section 26.4) so that the user can choose any drawing color. Changing a Slider’s value should update the color swatch displayed to the user and set the brushColor instance variable to the current Color. 26.1
( Contacts App) Create a Contacts app modeled after the Cover Viewer app (Sections 26.6– 26.7). Store the contact information in an ObservableList of Contact objects. A Contact should contain first name, last name, email and phone number properties (you can provide others). When the user selects a contact from the contacts list, its information should display in a Grid of TextFields. As the information is modified (a Contact’s data is updated, a new Contact is added or an existing Contact is deleted, the contacts ListView should display the updates. The ListView should display the Contact’s last names. 26.2
( Contacts App Modification) Modify the Contacts app from the preceding exercises to include an image for each Contact. Provide a custom ListView cell factory that displays the Contact’s full name and picture with the names in sorted order by last name then first name. 26.3
(Tip Calculator Modification) The Tip Calculator app from Section 25.5 does not need a Button to perform its calculations. Reimplement this app to use property listeners to perform the calculations whenever the user modifies the bill amount or changes the custom tip percentage. Also use a property binding to update the Label that displays the tip percentage. 26.4
(Advanced Project: Color Chooser App Modification) The property bindings we created in the Color Chooser app (Section 26.5) allowed a TextField’s text to update when a Slider’s value changed, but not vice versa. JavaFX also supports bi-directional property bindings. Research bi-directional property bindings online, then create bi-directional bindings between the Sliders and the TextFields such that modifying a TextField’s value updates the corresponding slider. 26.5
© 2017 Pearson Education, Inc., Hoboken, NJ. All rights reserved.