CPSC 225, Spring 2019
Lab 6: GUI Tutorial

This lab is a little tutorial about creating a JavaFX application from scratch. There are no starter files. The program that you will make is a simple paint program that allows the user to draw shapes on a canvas. Here's what it will look like:

You can copy-and-paste a lot of code from this web page. However, you should try to understand anything that you copy. There will be a few exercises for you to complete at the end that will require some original coding. You should also add appropriate comments to the code before turning in your program.

The work for this lab is due by the beginning of next week's lab. You should create a folder named lab6 inside your homework folder in /classes/cs225/homework. Copy your program file into that folder. It doesn't have to have a particular name. I will print out whatever .java file is in the lab6 folder.

A Basic Application

A JavaFX program is represented by a subclass of the class Application (from package javafx.application.Application. Application is an abstract class that defines the abstract method public void start(Stage stage). When you write a program, you need to provide a definition for that method. The parameter, stage, will be provided by the system when the program is run; it represents a window that is meant to be the main window for the program. The start() method should set up the window to show the GUI for the program, and it should set up event handling for the components in the window. At the end, it should call stage.show() to make the window visible on the screen.

The program needs a main() routine to launch the application. Typically, the main routine simply calls launch().

As discussed, the stage contains a scene which contains a root node. The root node in turn contains all of the components for the GUI. Often, the root node is a BorderPane, which can contain up to five components in its center, top, bottom, left, and right positions. However, the root can actually be any kind of node.

Here then is a starting point for a typical JavaFX application. You should create a class and copy this code into it (except that you don't need to name the program "MyPaintProgram."

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

public class MyPaintProgram extends Application {

    public void start(Stage stage) {
        BorderPane root = new BorderPane();
        
        // Set up the contents of the root node!
        
        Scene scene = new Scene(root);
        stage.setScene(scene);
        stage.setResizable(false);
        stage.setTitle("My Paint Program");
        stage.show();
    }
    
    public static void main(String[] args) {
        launch();
    }

}

The remainder of the programming is just adding components and events. And imports — JavaFX programs use a large number of imports, and many JavaFX classes have names that are the same as classes in other packages. If you let Eclipse import the JavaFX classes for you, make sure that you select a class that is actually part of JavaFX, in one of the subpackage of package javafx. It can get really confusing if you import the wrong class. Here is a list of all the imports used by my paint program:

import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
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.stage.Stage;

Adding Components

The paint program GUI has three sections: The menu bar at the top, the strip of controls along the bottom, and the large drawing area in the center. Each section is actually a container that contains other nodes. The menu bar is a container of type MenuBar that contains menus. The strip of controls is a container of type HBox that contains controls. And the drawing area is a container of type StackPane that contains two canvases, one on top of the other. We can start by setting up the GUI, and worry about events later.

For drawing on the canvases, the program will need a GraphicsContext for each canvas. The graphics contexts need to be global variables, since they will be used throughout the program:

    private GraphicsContext imageGraphics;
    private GraphicsContext overlayGraphics;

The canvases should be created in the start() method. They don't have to be global variables:

Canvas picture = new Canvas(800,600);
imageGraphics = picture.getGraphicsContext2D();
imageGraphics.setFill(Color.WHITE);
imageGraphics.fillRect(0,0,800,600);

Canvas overlay = new Canvas(800,600);
overlayGraphics = overlay.getGraphicsContext2D();

The "picture" canvas is the canvas that actually holds the image. The "overlay" canvas is used when the user is drawing a shape. It is completely transparent except for the one shape that is actually being drawn. When the user releases the mouse, the shape is drawn permanently to the picture canvas, and the overlay canvas is made completely transparent again. (This idea was discussed in class.) The code also creates the graphics contexts and fills the picture canvas with a white background. A canvas is transparent by default, so there is no need to clear the overlay canvas here.

To get the canvases into the window, they have to be added to a StackPane, and the StackPane has to be placed into the center position of the root node. A StackPane arranges its child components by stacking them up one on top of the other. The StackPane constructor allows you to specify a list of components that it will contain:

StackPane canvasHolder = new StackPane(picture,overlay);
root.setCenter(canvasHolder);

The top and bottom positions in the BorderPane are similar. Two Menus are created, a MenuBar is created to hold the two menus, and the MenuBar is then placed in the top section of the root pane. We will have to come back later to add menu items to the menus.

Menu colorMenu = new Menu("Color");
Menu toolMenu = new Menu("Tool");
MenuBar menubar = new MenuBar(colorMenu, toolMenu);
root.setTop(menubar);

Finally, make a Button and two CheckBoxes, make an HBox to hold them, and place the HBox in the bottom section of the root pane. We will need to be able to test the state of the checkboxes elsewhere in the program, so they need to be stored in global variables:

private CheckBox outlined;
private CheckBox translucent;

The constructor that is used for the HBox has a first parameter that specifies the size of a gap, in pixels, that will be placed between the components that are added to the hbox. The parameters to the button and checkbox constructors are the text that will appear in the button or next to the checkbox. I want one of the checkboxes to be checked initially, so I call outlined.setSelected(true):

Button clear = new Button("Erase the Picture");
outlined = new CheckBox("Outlined Shapes");
outlined.setSelected(true);
translucent = new CheckBox("Translucent Fill");

HBox bottom = new HBox( 20, clear, outlined, translucent );
root.setBottom(bottom);

At this point, you should be able to run the program and see what the GUI looks like. You will be disappointed with the appearance of the strip of controls along the bottom. There are many options for configuring the appearance of a component. In this case, I told the HBox to place its components in the center, rather than at the left end, of the box by calling:

bottom.setAlignment(Pos.CENTER);

I set the other aspects of its appearance using CSS. CSS is a standard that is used for specifying the appearance of web pages, and it has been adopted in other contexts as well. JavaFX has a lot of support for CSS, and it is often the easiest way to specify appearance. It is, however, outside the scope of this tutorial. (See Section 6.2.5 in the textbook for a little more information.) Here is the command that I use to set the CSS style for the hbox:

bottom.setStyle(
   "-fx-border-color:black; -fx-background-color:lightgray; -fx-padding:4px");

Implementing Mouse Drags

If you want to respond when the user drags the mouse on a component, you usually need to three kinds of event: mousePressed, which is generated when the user presses a button on the mouse; mouseDragged, which occurs each time the mouse moves as the user drags the mouse with button depressed; and mouseReleased, which occurs when the user releases a button on the mouse. Usually you will need some global variables to keep track of information between events. Often, you will need the location of the point where the mouse button was pressed, as well as the current location of the mouse. Here, we want to draw a shape between the starting position of the mouse and its current position. There is also a variable to keep track of the color that is being used to draw shapes. I wrote methods to handle the three kinds of mouse event. The parameter to each method is a MouseEvent object, which has methods evt.getX() and evt.getY() that give the coordinates of the mouse when the event occurred:

private Color currentColor = Color.RED;
private double startX, startY;
private double endX, endY;

private void mousePressed(MouseEvent evt) {
    startX = evt.getX();
    startY = evt.getY();
}

private void mouseDragged(MouseEvent evt) {
    endX = evt.getX();
    endY = evt.getY();
    overlayGraphics.clearRect(0,0,800,600);
    drawShape(overlayGraphics,startX,startY,endX,endY);
}

private void mouseReleased(MouseEvent evt) {
    overlayGraphics.clearRect(0,0,800,600);
    drawShape(imageGraphics,startX,startY,endX,endY);
}

private void drawShape(GraphicsContext g, 
                 double x1, double y1, double x2, double y2) {
    double left = Math.min( x1,x2 );
    double top = Math.min( y1,y2 );
    double width = Math.abs( x1 - x2 );
    double height = Math.abs( y1 - y2 );
    g.setFill(currentColor);
    g.fillRect( left, top, width, height );
    if (outlined.isSelected()) {
        g.setLineWidth(2);
        g.setStroke(Color.BLACK);
        g.strokeRect( left, top, width, height );
    }
}

Note that the mousePressed() method simply records the starting position of the mouse. The mouseReleased() method records the current position of the mouse, clears the entire overlay canvas by calling overlayGraphics.clearRect(), and draws the current shape to the overlay canvas. The mouseReleased() method clears the overlay canvas again, leaving it totally transparent, and draws the shape to the main canvas.

It was convenient to write a method for drawing the shape to either graphics context. The shape is specified by its two corners, (x1,y1) and (x2,y2), but to draw a rectangle we need the know the top-left corner of the rectangle and its width and height. The drawShape() method takes care of that detail. It also implements the outlined checkbox: If that box is selected (that is, if it is checked), then the method draws a black border around the shape.

All that code is not enough to implement mouse handling! You also need to arrange for the event-handling methods to be called when the user drags the mouse on the canvas. This is done by setting event handlers for the events. It is actually the overlay canvas that sees the events, since it is the top component in the StackPane. The code goes in the start() method, after creating the overlay canvas. The event-handlers can be specified as lambda expressions that simply call the corresponding event-handling method, as defined above:

overlay.setOnMousePressed( evt -> mousePressed(evt) );
overlay.setOnMouseDragged( evt -> mouseDragged(evt) );
overlay.setOnMouseReleased( evt -> mouseReleased(evt) );

At this point, you should be able to run the program and draw red rectangles, either with or without black borders.

The Color Menu

To give the user some control over the drawing color, you should add some menu items to the "Color" menu. We can have menu items such as "Red", "Green", and "Blue". Choosing a menu item sets the currentColor to the the corresponding color.

A menu item is represented by an object of type MenuItem. When the user selects a menu item, an action event is generated, and you can set an event handler to respond to that event. All we need is a lambda expression that changes the value of currentColor. The menu item actually has to be added to the list of items in the menu. So:

MenuItem item;

item = new MenuItem("Red");
item.setOnAction( e -> currentColor = Color.RED );
colorMenu.getItems().add(item);

item = new MenuItem("Green");
item.setOnAction( e -> currentColor = Color.GREEN );
colorMenu.getItems().add(item);

item = new MenuItem("Blue");
item.setOnAction( e -> currentColor = Color.BLUE );
colorMenu.getItems().add(item);

The code for setting up the menus can get quite long. You migh want to move it into one or more subroutines, instead of just dumping it all into the start() method.

At this point, when you run the program, you should be able to change the drawing color.

Exercises

To finish the program, you should make the following additions to the all the code that you have simply copy-and-pasted. And don't forget to add comments.

Exercise 1: Add a few more colors to the "Color" menu.

Exercise 2: Implement the "Erase the Picture" button. A button, like a menu item, generates an action event when it is clicked. The object that represents the button is named clear. You can set the event handler with clear.setOnAction().

Exercise 3: Implement the "Translucent Fill" button. When that checkbox is selected, the color that is used to fill the shapes should be made somewhat translucent. You can create the fill color using the red, green, and blue components from currentColor along with an opacity value of, say, 0.5.

Exercise 4: Implement the "Tool" menu, to enable the user to draw ovals and lines as well as rectangles. The menu needs three menu items, "Rectangle", "Oval", and "Line". Selecting an item simply changes the current drawing tool. The line tool simply strokes a line between the point (x1,y1) and the point (x2,y2) in the drawShape() method. You might also consider having a "RoundRect" tool.

(If you are interested in making a freehand draw tool or an erase tool, ask me about it! Other options could be a "Stroke Width" menu and a "Stroke Color" menu.)