Section 6.4
Basic Controls
In preceding sections, you've seen how to use a graphics context to draw on the screen and how to handle mouse events and keyboard events. In one sense, that's all there is to GUI programming. If you're willing to program all the drawing and handle all the mouse and keyboard events, you have nothing more to learn. However, you would either be doing a lot more work than you need to do, or you would be limiting yourself to very simple user interfaces. A typical user interface uses standard GUI components such as buttons, scroll bars, text-input boxes, and menus. These components have already been written for you, so you don't have to duplicate the work involved in developing them. They know how to draw themselves, and they can handle the details of processing the mouse and keyboard events that concern them.
Consider one of the simplest user interface components, a push button. The button has a border, and it displays some text. This text can be changed. Sometimes the button is disabled, so that clicking on it doesn't have any effect. When it is disabled, its appearance changes. When the user clicks on the button, it changes appearance when the mouse button is pressed and changes back when the mouse button is released. In fact, it's more complicated than that. If the user moves the mouse outside the push button before releasing the mouse button, the button changes to its regular appearance, and releasing the mouse at that time will not trigger the button. To implement this, it is necessary to respond to mouse exit or mouse drag events. Furthermore, on many platforms, a button can receive the input focus. The button changes appearance when it has the focus. If the button has the focus and the user presses the space bar, the button is triggered. This means that the button must respond to keyboard and focus events as well.
Fortunately, you don't have to program any of this, provided you use an object belonging to the standard class javafx.scene.control.Button. A Button object draws itself and processes mouse, keyboard, and focus events on its own. You only hear from the Button when the user triggers it by clicking on it or pressing the space bar while the button has the input focus. When this happens, the Button object creates an event object belonging to the class javafx.event.ActionEvent. The event object is sent to any registered listener to tell it that the button has been pushed. Your program gets only the information it needs—the fact that a button was pushed.
Many standard components that are defined as part of the JavaFX graphical user interface API are defined by subclasses of the class Control, from package javafx.scene.control. Controls (with just a couple of exceptions) can be manipulated by the user to generate input and events for the program. A number of useful methods are defined for controls. I begin by mentioning three methods that can be used with any Control control:
- control.setDisable(true) can be called to disable the control. The control can be re-enabled with control.setDisable(false). When a control is disabled, its appearance might change, and it cannot be the target of mouse or key events. This function can actually be called with any scene graph node, not just controls; when you disable a node, any nodes contained inside that node are also disabled. There is a boolean-valued function, control.isDisabled() that you can call to discover whether the control is disabled, either because it was explicitly disabled or because it is inside a container node that was explicitly disabled.
- control.setTooltip(new Tooltip(string)) sets the specified string as a "tool tip" for the control. The tool tip is displayed if the mouse cursor is inside the control and the mouse is not moved for a few seconds. The tool tip should give some information about the meaning of the control or how to use it.
- control.setStyle(cssString) sets the CSS style of the control. CSS was discussed in Subsection 6.2.5.
Note that using a control, or any scene graph node, is a multi-step process. The component object must be created with a constructor. It must be added to a container. In many cases, a listener must be registered to respond to events from the component. And in some cases, a reference to the component must be saved in an instance variable so that the component can be manipulated by the program after it has been created. In this section, we will look at a few of the basic standard control components that are available in JavaFX. They are all defined by classes in the package javafx.scene.control. In the next section we will consider the problem of laying out components in containers.
6.4.1 ImageView
But before we turn to controls, I want to mention one other node type: ImageView from package javafx.scene.image. Recall from Subsection 6.2.3 that an Image object represents a picture, and that images can be loaded from resource files. An Image can be drawn on a Canvas, but an Image is not a Node. That is, it can't be part of a scene graph.
An ImageView is a scene graph node that is a simple wrapper for an image. Its purpose is simply to display the image. It makes it possible to add the image to a scene graph without having to draw the image on a canvas. The image can be specified as a parameter to the ImageView constructor. For example, suppose that "icons/tux.png" is the path to an image resource file. Then an ImageView can be created to display the image like this:
Image tux = new Image("icons/tux.png"); ImageView tuxIcon = new ImageView( tux );
In this case, I am thinking of the image as an "icon," that is, a small image that is typically used on a button, label, or menu item to add a graphical element to the usual plain text. In fact, we'll see that you can do exactly that in JavaFX.
6.4.2 Label and Button
The first four controls that we will look at have something in common: They all display a string of text to the user, which the user can view but not edit. Furthermore they can all display a graphical element in addition to or instead of the text. The graphic can be any Node but is usually a small icon, implemented as an object of type ImageView. In fact, all four types of controls inherit their behavior from a common superclass named Labeled. In Subsection 6.6.2, we look at menu items, which also inherit from Labeled. That class defines a number of instance methods that can be used with labels, buttons, and other labeled controls. Those methods include:
- setText(string) for setting the text that is displayed on the control. The text can be multi-line. The new line character, "\n", in the string represents a line break.
- setGraphic(node) for setting the control's graphical element.
- setFont(font) for setting the font that is used for the text.
- setTextFill(color) for setting the paint that is used for drawing the text.
- setGraphicTextGap(size) for setting the amount of space that is put between the text and the graphic. The parameter is of type double.
- setContentDisplay(displayCode) for setting where the graphic should be placed with respect to the text. The parameter is a constant from an enum, ContentDisplay, such as ContentDisplay.LEFT, ContentDisplay.RIGHT, ContentDisplay.TOP or ContentDisplay.BOTTOM.
All of these setter methods have corresponding getters, such as getText() and getFont(). I have not listed a setter method for the background color. It is possible to set a background color, but it's cumbersome. (Setting the background color of a control, c, to white looks like this:
c.setBackground(new Background(new BackgroundFill(Color.WHITE,null,null)));
where Background and BackgroundFill are in package javafx.scene.layout.) It is more easily done by setting the CSS style of the control with the setStyle() method. CSS is also useful for setting the border and for setting padding (the amount of empty space left around the content).
A Label is certainly the simplest type of control. It adds almost nothing to the Labeled class. Its purpose is simply to display some unedited text and/or a graphic to the user. The label class has two constructors. The first has one parameter of type String specifying the text for the label. The second adds a parameter of type Node specifying a graphic for the label. For example, assuming that tuxIcon is the ImageView object from the previous subsection,
Label message = new Label("Hello World"); Label linuxAd = new Label("Choose Linux First!", tuxIcon);
The default background of a label is completely transparent, and the label has no border or padding by default. Often I would like to add at least some padding. Here is an example of setting all three properties using CSS:
message.setStyle("-fx-border-color: blue; -fx-border-width: 2px; " + "-fx-background-color: white; -fx-padding: 6px");
You've already seen buttons used in Section 6.1. A Button, like a Label, displays some text and/or a graphic to the user, and the Button class, like the Label class, has two constructors:
Button stopButton = new Button("Stop"); Button linuxButton = new Button("Get Linux", tuxIcon);
When the user clicks a button, an event of type ActionEvent is generated. You can register an event handler for the action with the button's setOnAction method. For example,
stopButton.setOnAction( e -> animator.stop() );
In addition to the methods inherited from Labeled, a button has the instance methods setDisable(boolean) and setToolTip(string) that were mentioned at the beginning of this section. These two methods can be very useful for buttons. The setDisable() and setText() methods are particularly useful for giving the user information about what is going on in the program. A disabled button is better than a button that gives an obnoxious error message such as "Sorry, you can't click on me now!" For example, suppose that we want to use two buttons to start and stop an AnimationTimer, animator. When the animation is running, the start button should be disabled, and when the animation is paused, the stop button should be disabled. The code for this might look like:
Button startButton = new Button("Run Animation"); Button stopButton = new Button("Stop Animation"); stopButton.setDisable(true); // Stop button is initially disabled. startButton.setOnAction( e -> { animator.start(); startButton.setDisable(true); stopButton.setDisable(false); } ); stopButton.setOnAction( e -> { animator.stop(); startButton.setDisable(false); stopButton.setDisable(true); } );
This ensures that the user can't try to start the animation when it is already started or stop it when it is already stopped.
Often, there is a button that triggers some default action in a program. For example, the user might enter data into some text input boxes and then click a "Compute" button to process the data. It would be nice if the user could just press Return when done typing, instead of having to click the button. In JavaFX, you can make a Button, button, into the default button for the window that contains it by calling
button.setDefaultButton(true);
When a window has a default button, then pressing the Return (or Enter) key on the keyboard is equivalent to clicking the default button, unless the key event generated by the Return key is consumed by another component. This can be very convenient for the user.
6.4.3 CheckBox and RadioButton
A CheckBox is a component that has two states: selected or unselected. (Being "selected" means that the checkbox is checked.) The user can change the state of a check box by clicking on it. The state of a checkbox is represented by a boolean value that is true if the box is selected and is false if the box is unselected. A checkbox has a label, which is specified when the box is constructed:
CheckBox showTime = new CheckBox("Show Current Time");
CheckBox is a subclass of Labeled, so a checkbox can also have a graphic, and all the instance methods from the Labeled class can also be used with checkboxes. (There is no constructor that specifies a graphic for the checkbox; any graphic has to be set by calling setGraphic(node).)
Usually, it's the user who sets the state of a CheckBox by clicking on it, but you can also set the state programmatically. The current state of a checkbox is set using its setSelected(boolean) method. For example, if you want the checkbox showTime to be checked, you would say "showTime.setSelected(true)". To uncheck the box, say "showTime.setSelected(false)". You can determine the current state of a checkbox by calling its isSelected() method, which returns a boolean value.
In many cases, you don't need to worry about events from checkboxes. Your program can just check the state whenever it needs to know it by calling the isSelected() method. However, a checkbox does generate an event when its state is changed by the user, and you can detect this event and respond to it if you want something to happen at the moment the state changes. When the state of a checkbox is changed by the user, it generates an event of type ActionEvent. If you want something to happen when the user changes the state, you must register a handler with the checkbox by calling its setOnAction() method. (Note that if you change the state by calling the setSelected() method, no ActionEvent is generated. However, there is another method in the CheckBox class, fire(), which simulates a user click on the checkbox and does generate an ActionEvent.)
It is actually possible for a checkbox to be in a third state, called "indeterminate," although that possibility is turned off by default. See the API documentation for details.
Closely related to checkboxes are radio buttons. Like a checkbox, a radio button can be either selected or not. However, radio buttons are expected to occur in groups, where at most one radio button in a given group can be selected at any given time. Radio button groups let the user select one choice among several alternatives. In JavaFX, a radio button is represented by an object of type RadioButton. When used in isolation, a RadioButton acts just like a CheckBox, and it has the same constructor, methods, and events, including methods inherited from Labeled. Ordinarily, however, a RadioButton is used in a group. A group of radio buttons is represented by an object belonging to the class ToggleGroup. A ToggleGroup is not a component and does not itself have a visible representation on the screen. A ToggleGroup works behind the scenes to organize a group of radio buttons, to ensure that at most one button in the group can be selected at any given time.
To use a group of radio buttons, you must create a RadioButton object for each button in the group, and you must create one object of type ToggleGroup to organize the individual buttons into a group. Each RadioButton must be added individually to the scene graph, so that it will appear on the screen. (A ToggleGroup plays no role in the placement of the buttons on the screen.) Each RadioButton must also be added to the ToggleGroup. You do that by calling the radio button's setToggleGroup(group) instance method. If you want one of the buttons to be selected initially, you can call setSelected(true) for that button. If you don't do this, then none of the buttons will be selected until the user clicks on one of them.
As an example, here is how you could set up a group of radio buttons that can be used to select a color:
RadioButton redRadio, blueRadio, greenRadio, yellowRadio; // Variables to represent the radio buttons. // These might be instance variables, so that // they can be used throughout the program. ToggleGroup colorGroup = new ToggleGroup(); redRadio = new RadioButton("Red"); // Create a button. redRadio.setToggleGroup(colorGroup); // Add it to the ToggleGroup. blueRadio = new RadioButton("Blue"); blueRadio.setToggleGroup(colorGroup); greenRadio = new RadioButton("Green"); greenRadio.setToggleGroup(colorGroup); yellowRadio = new RadioButton("Yellow"); yellowRadio.setToggleGroup(colorGroup); redRadio.setSelected(true); // Make an initial selection.
As an alternative to calling redRadio.setSelected(true), you can use the selectToggle() instance method in the ToggleGroup class to select the radio button:
colorGroup.selectToggle( redRadio );
Just as for checkboxes, it is not always necessary to register listeners for radio buttons. You can test the state of an individual RadioButton by calling its isSelected() method, or you can call the toggle group's getSelectedToggle() method. The return type of this method is Toggle, which is an interface implemented by RadioButton. For example:
Toggle selection = colorGroup.getSelectedToggle(); if (selection == redRadio) { color = Color.RED; } else if (selection == greenRadio){ . . .
Here's what these radio buttons look like, lined up vertically in a container:
6.4.4 TextField and TextArea
The TextField and TextArea classes represent components that contain text that can be edited by the user. A TextField holds a single line of text, while a TextArea can hold multiple lines. It is also possible to set a TextField or TextArea to be read-only so that the user can read the text that it contains but cannot edit the text. Both classes are subclasses of an abstract class, TextInputControl, which defines their common properties.
TextField and TextArea have many methods in common. The instance method setText(text), which takes a parameter of type String, can be used to change the text that is displayed in an input component. The contents of the component can be retrieved by calling its getText() instance method, which returns a value of type String. You can add a String of text onto the end of the text that is already in the component by calling the instance method appendText(text). The text in the setText() and appendText() methods can include "\n" characters to represent line breaks; in a TextField they will be ignored. The instance method setFont(font) can be used to change the font that is used in the text component.
If you want to stop the user from modifying the text, you can call setEditable(false). Call the same method with parameter true to make the input component user-editable again.
The user can only type into a text component when it has the input focus. The user can give the input focus to a text component by clicking it with the mouse, but sometimes it is useful to give the input focus to a text field programmatically. You can do this by calling its requestFocus() method.
A substring of the text in a text component can be "selected." The selected text is highlighted and can be cut or copied from the text component. (The user can right-click in the component to bring up a pop-up menu of editing commands.) TextInputComponent has several instance methods for working with the text selection, but I only mention one of them: selectAll(), which selects the entire string of text in the text component.
For example, when I discover an error in the user's input in a TextField, input, I usually call both input.requestFocus() and input.selectAll(). This helps the user see where the error occurred and lets the user start typing the correction immediately. If the user starts typing, the old text in the input box, which is highlighted, will automatically be deleted.
Both the TextField class and the TextArea class have two constructors. One of the constructors has no parameter and makes an initially empty text input box. The second constructor takes a parameter of type String, specifying the initial content of the box.
A TextField has a preferred number of columns, which by default is 12. This determines the preferred width of the text field, that is, the size that is used if the size is not reset by the program or when the text field is laid out by a container. The preferred number of columns for a TextField, input, can be changed by calling input.setPrefColumnCount(n), where n is a positive integer.
Similarly, a TextArea has both a preferred number of columns and a preferred number of rows, which by default are 40 and 10. The value can be changed using the TextArea instance methods setPrefColumnCount(n) and setPrefRowCount(n).
The TextArea class adds a few useful methods to those inherited from TextInputControl, including some methods for getting and setting the amount by which the text has been scrolled. Most important is setWrapText(wrap), where wrap is of type boolean. This method says what should happen when a line of text is too long to be displayed in the text area. If wrap is true, then any line that is too long will be "wrapped" onto the next line, with the line break occurring between words if possible; if wrap is false, the line will simply extend outside the text area, and the user will have to scroll the text area horizontally to see the entire line. The default value of wrap is false.
Since it might be necessary to scroll a text area to see all the text that it contains, a TextArea comes with scroll bars. The scroll bars are visible only when they are needed because the text cannot fit in the available space.
See the sample program TextInputDemo.java for a short example of using a text field and a text area. Here is the window from that program, after the text has been edited and scrolled down a few lines:
(I pasted the source code of the program into the text area!)
6.4.5 Slider
A Slider provides a way for the user to select an integer value from a range of possible values. The user does this by dragging a "knob" along a bar. A slider can, optionally, be decorated with tick marks and with labels. This picture, from the sample program SliderDemo.java, shows three sliders with different decorations and with different ranges of values:
Here, the second slider is decorated with tick marks, and the third one is decorated with labels. It's possible for a single slider to have both types of decorations.
The most commonly used constructor for Sliders specifies the start and end of the range of values for the slider and its initial value when it first appears on the screen:
public Slider(double minimum, double maximum, double value)
If the parameters are omitted, the values 0, 100, and 0 are used. By default, a slider is horizontal, but you can make it vertical by calling its method setOrientation(Orientation.VERTICAL). Orientation is an enum from package javafx.geometry.
The current value of a Slider can be read at any time with its getValue() method, which returns a value of type double. If you want to change the value, you can do so with the method setValue(val), which takes a parameter of type double. If the specified value is not between the minimum and maximum slider values, it is adjusted to lie in that range.
If you want to respond immediately when the user changes the value of a slider, you can register a listener with the slider. Sliders, unlike other components we have seen, do not generate ActionEvents. Instead, they have an observable property of type Double that represents the value. (See Subsection 6.3.7.) You can access the value property of a Slider, slider, by calling slider.valueProperty(), and you can add a listener to the property that will be called whenever the value changes. For example, in the sample program, I add listeners to the sliders with commands such as
slider1.valueProperty().addListener( e -> sliderValueChanged(slider1) );
The listener will be called whenever the value changes, either because the user is dragging the knob on the slider or because the program calls setValue(). If you want to know whether the user generated the event, call the slider's boolean-valued isValueChanging() method, which returns true if the user is dragging the knob on the slider.
Using tick marks on a slider is a two-step process: Specify the interval between the tick marks, and tell the slider that the tick marks should be displayed. There are actually two types of tick marks, "major" tick marks and "minor" tick marks. You can have one or the other or both. Major tick marks are a bit longer than minor tick marks. The method setMajorTickSpacing(x) indicates that there should be a major tick mark every x units along the slider. The parameter is of type double. (The spacing is in terms of values on the slider, not pixels.) To control the minor tick marks, you can call setMinorTickCount(n). The parameter is an int that specifies how many minor tick marks are placed between consecutive major tick marks. The default value is 4. If you don't want minor tick marks, set the count to zero. Calling these methods is not enough to make the tick marks appear. You also have to call setShowTickMarks(true). For example, the second slider in the sample program was created and configured using the commands:
slider2 = new Slider(); // Use default values (0,100,0) slider2.setMajorTickUnit(25); // space between big tick marks slider2.setMinorTickCount(5); // 5 small tick marks between big tick marks. slider2.setShowTickMarks(true);
Labels on a slider are handled similarly. A label will be placed at every major tick mark, but some labels will be omitted if they would overlap. To see the labels, you need to call setShowTickLabels(true). For example, the third slider in the above illustration was created and configured to show labels with the commands:
slider3 = new Slider(2000,2100,2018); slider3.setMajorTickUnit(50); // tick marks are not shown! slider3.setShowTickLabels(true)
The value of a slider is of type double. You might want to restrict the value to be an integer, or to be some multiple of a given value. What you can do is call slider.setSnapToTicks(true). After the user finishes dragging the slider's knob, the value will be moved to the value at the nearest major or minor tick mark, even if the tick marks are not visible. The value will not be restricted while the user is dragging the knob; the value is just adjusted at the end of the drag. The value set by calling setValue(x) is also not restricted, but there is another method, adjustValue(x), that will set the value to the value at the tick mark nearest to x. For example, if you want a slider to snap to integral values in the range 0 to 10, you could say:
Slider sldr = new Slider(0,10,0); sldr.setMajorTickUnit(1); // major ticks 1 unit apart sldr.setMinorTickCount(0); // no minor tick marks sldr.setSnapToTicks(true);
In the sample program, the third slider has been configured to snap to an integer value at the end of a drag operation.