In section 14.12 of the textbook the author shows an extended example of a JavaFX custom pane class, the ClockPane class.
If you have access to the textbook you should start by reading section 14.11, which covers the basics of drawing in JavaFX panes. Then, read the extended example in section 14.12 of a pane class that draws an analog clock. If you don't have access to the textbook I will give a brief overview of the ClockPane example in these notes.
We have already seen that JavaFX offers a wide range of classes to implement common user interface elements, such as text fields, buttons, and menus. For most applications the user interface elements provided by JavaFX will be sufficient to meet your needs. For some applications you will discover the need for a user interface element that JavaFX does not provide. To meet the needs of those applications, JavaFX offers programmers the ability to construct their own custom user interface elements.
In almost every case the best way to create your own user interface element in JavaFX is to construct a class based on the JavaFX Pane class. The Pane class is designed to integrate with the broader JavaFX system, and is designed to be placed in a user interface alongside other common JavaFX elements. At the same time, the Pane class offers a blank slate into which we can place our own custom drawing and event handling.
The author's ClockPane example starts by creating a class that extends the javafx.scene.layout.Pane class:
public class ClockPane extends Pane { }
Each custom pane class that you create in JavaFX will have its own unique appearance. To do this we will add a collection of JavaFX shape objects as children of the pane. When JavaFX draws your pane it will automatically tell all of the shape objects contained in the pane to draw themselves.
Some of the standard shape objects that are available in JavaFX include the Line, Rectangle, and Circle classes for creating basic shapes, along with a Text class for displaying text.
Each shape that you place in a pane has a location. To set the location of an element you will use a graphics coordinate system. The basic unit of measure in this coordinate system is the pixel. The origin of the coordinate system is the top left corner of the pane, with the positive x direction running to the right from that corner and the positive y direction running down from that corner.
Each different shape that you will work with uses a slightly different method for setting its location. For example, to place a Line in a pane you set the coordinates of the two points at either end of the line segment. To place a Rectangle you specify the location of the top left corner of the rectangle and then set the width and height of the rectangle. To place a circle you specify the location for the center of the circle and then set the radius of the circle. To place text you specify a location for the start of the text baseline, which is the imaginary line that the text sits on.
Once you set the location of a shape you can then also set other important properties such as its color. For example, on a Rectangle or a Circle you can set the color for the boundary of the shape via a call to setStroke()
and the color for the interior of the shape via a call to setFill()
.
Here is the code for the most important method in the author's ClockPane class, the method that sets up the necessary shapes to draw the clock face:
private void paintClock() { // Initialize clock parameters double clockRadius = Math.min(getWidth(), getHeight()) * 0.8 * 0.5; double centerX = getWidth() / 2; double centerY = getHeight() / 2; // Draw circle Circle circle = new Circle(centerX, centerY, clockRadius); circle.setFill(Color.WHITE); circle.setStroke(Color.BLACK); Font font = Font.font ("sans-serif", 12); Text t1 = new Text(centerX - 5, centerY - clockRadius + 12, "12"); t1.setFont(font); Text t2 = new Text(centerX - clockRadius + 3, centerY + 5, "9"); t2.setFont(font); Text t3 = new Text(centerX + clockRadius - 10, centerY + 3, "3"); t3.setFont(font); Text t4 = new Text(centerX - 3, centerY + clockRadius - 3, "6"); t4.setFont(font); // Draw second hand double sLength = clockRadius * 0.8; double secondX = centerX + sLength * Math.sin(second * (2 * Math.PI / 60)); double secondY = centerY - sLength * Math.cos(second * (2 * Math.PI / 60)); Line sLine = new Line(centerX, centerY, secondX, secondY); sLine.setStroke(Color.RED); // Draw minute hand double mLength = clockRadius * 0.65; double xMinute = centerX + mLength * Math.sin(minute * (2 * Math.PI / 60)); double minuteY = centerY - mLength * Math.cos(minute * (2 * Math.PI / 60)); Line mLine = new Line(centerX, centerY, xMinute, minuteY); mLine.setStroke(Color.BLUE); // Draw hour hand double hLength = clockRadius * 0.5; double hourX = centerX + hLength * Math.sin((hour % 12 + minute / 60.0) * (2 * Math.PI / 12)); double hourY = centerY - hLength * Math.cos((hour % 12 + minute / 60.0) * (2 * Math.PI / 12)); Line hLine = new Line(centerX, centerY, hourX, hourY); hLine.setStroke(Color.GREEN); getChildren().clear(); getChildren().addAll(circle, t1, t2, t3, t4, sLine, mLine, hLine); }
This method will get called any time that the ClockPane decides it needs to redraw its contents. For example, when you call the setCurrentTime()
method to set the time displayed on the clock that method will call paintClock()
to redraw the clock face. If the user resizes the window containing the pane JavaFX will call the pane's setWidth()
and setHeight()
methods, which in turn will call paintClock()
.
I have further updated the author's clock example to demonstrate putting a custom JavaFX pane into an FXML user interface. At the top of these notes you will find a button to download the project.
As you can see, I have included an updated version of the author's ClockPane class as one of the classes in this project.
The part of the project that goes beyond what the author demonstrates in section 14.12 is the code needed to integrate the ClockPane into the FXML interface of the application.
To provide support for a custom pane, we start as usual by setting up the rest of the user interface in Scene Builder. The main window for the example application uses a BorderPane as the top level pane for the main window. The important thing to note about this BorderPane is the fact that I have intentionally left the center area in the BorderPane empty: we will be inserting a ClockPane into that area when the application starts up. The only other element in the user interface initially is a button that appears in the bottom portion of the BorderPane. As usual, I have set up an action method in the main controller to respond to clicks on that button.
Here now is the full code for the application's main controller class:
public class PrimaryController implements Initializable { @FXML BorderPane mainPane; ClockPane clock; @FXML private void setTime(ActionEvent evt) { clock.setCurrentTime(); } @Override public void initialize(URL url, ResourceBundle rb) { clock = new ClockPane(); mainPane.setCenter(clock); } }
The most important code in this simple class is the initialize()
method. This is where we create the ClockPane object, store it in a member variable, and then set the ClockPane into the center area in the BorderPane.
Once the ClockPane has been set up as a member variable, we can call methods on it. For example, in the code for the button's action method I tell the ClockPane to set a new time value.