One of the driving forces behind the development of object-oriented programming was the need to create graphical user interfaces (GUIs). It is not surprising, then, that the OO programming model really shines when building GUIs.
In fact, the standard WIMP model of graphical user interfaces (Windows, Icons, Menus, Pointer) was developed at Xerox PARC during the 1970's in and along with one of the first object-oriented programming languages, Smalltalk. GUIs influenced the language design and vice versa. The WIMP model first saw widespread deployment by Apple with the first Macintosh computer and was soon adopted by Microsoft Windows as well.
In this approach to graphical user interface design, the user interface is represented internally as a tree structure, in which the various user interface components (sometimes called widgets) are nodes in the tree, and the parent–child relationship corresponds to containment. In fact, the HTML markup language follows this same approach to defining a user interface.
We will be using JavaFX, a modern object-oriented GUI library. In such a library, there are two interacting hierarchies: first, the containment hierarchy corresponding to the tree of UI components, and second, the class hierarchy of the components themselves. Unlike in HTML, the class hierarchy is extensible, allowing developers to use inheritance to design new components or to customize the behavior of existing components.
JavaFX manages the user interface as a scene graph, which is actually a tree of nodes. At the root of the scene graph is some node; the node is registered with a Scene object that is in turn registered with a Stage, which corresponds to a top-level window in the application.
A scene graph must be constructed. This can be done either by either
All of the non-leaf nodes in the
tree are a subclass of Parent
(typically a subclass of Pane
).
To add a child using code:
Parent p; Node n; p.getChildren().add(n);
The getChildren()
method returns a collection of children that is
tied to the actual children of the parent node. Thus, adding a new node to the
collection causes the parent node to acquire a new child. The collection can
be used in various other ways, however; for example, it can be used to
iterate over the children or to listen to its contents to find out when the set
of children changes.
A scene graph can also be created in advance using the JavaFX SceneBuilder application. SceneBuilder saves the scene is saved in XML format in an .fxml file, which can be loaded by the application to create the entire scene graph at once.
Some JavaFX nodes just display themselves (e.g., Rectangle
). Some nodes do something active
(e.g., Button
, TextField
). Other nodes are just there to control the layout of other
nodes in the window. Examples of these are the various subclasses of Pane
. Pane
places
all its children in the upper left corner. The classes HBox
and VBox
lay out their children in a horizontal row or vertical column, respectively. StackPane
stacks all its children on top of each other, centered. GridPane
, FlowPane
, and BorderPane
lay out children in fancier ways.
There are several ways to control the appearance of the scene. Some nodes have handles that allow them to be minimized, maximized, or resized. There are also various controls such as sliders or knobs to control shape, size, color, or other aspect of the appearance. Nodes can also be added, removed, moved, or transformed. With JavaFX, when such a change is made and the scene needs to be redrawn, the visible components of the scene redraw themselves in the right order.
A useful feature of JavaFX is that nodes can be styled using style sheets (CSS), similar to how HTML
pages are styled (e.g., p.setStyle("-fx-background-color: #ffff00");
). The style sheet
can also be created in advance and loaded from an external file. Style sheets provide an easy way to
rapidly experiment with different looks for an application without recompiling the application.
It is also possible to draw onto a Canvas
using a
GraphicsContext
object. One can draw shapes such as rectangles,
circles, ellipses, or curves. Canvases give complete control over rendering
anything within their borders, but the program has the responsibility for the
rendering. One difference from Swing, JavaFX's predecessor, is that
exposed pixels are redrawn automatically.
JavaFX applications are typically event driven. This is a somewhat different
programming paradigm from the usual imperative style. Its motto is: "Don't call us, we'll call you."
When some hardware event takes place such as a mouse click or key press, an Event
object is generated. An Event
object is simply a Java object like any other
Java object. It contains information about the source and type of the event: button click,
mouse movement, keystroke, etc. The event handler of any listener that has registered to
receive the event is called, and the event object is passed to the handler.
In that way an application can receive and respond to events such as
mouse clicks and button pushes.
Unlike simple Java programs, a JavaFX application typically has multiple concurrent threads:
a main thread, a application thread, and a rendering thread, as well as various background worker threads. The application thread's sole task
is to loop continuously, listening for the occurrence of hardware-generated events such as mouse
clicks. For each such occurrence, it constructs an Event
object describing the
event and the GUI component in which it occurred, then calls the event handler of each registered
listener with that Event
object. The rendering thread's task is to continuously rerender the display.
Event handlers should not contain long-running computations, because this will
tie up the application thread, preventing events from being handled. This will
appear to cause the GUI to freeze and not respond to events.
Fortunately, there is a mechanism for scheduling an update of the GUI on the application
thread later using the method Platform.runLater(Runnable r). The run()
method of the Runnable
object r
is called at the earliest available opportunity by the application thread.
Programs can define event handlers to handle different types of input events. Handlers must be registered to receive events with a GUI component on which the event can occur. There are several ways to specify a handler: classes, inner classes, or lambda expressions. All are basically syntactic sugar for the same thing, but convenient. Here is an example with a lambda expression:
Button b = ...; b.setOnAction(e -> print("button clicked"));
This is essentially the Observer pattern, but for many events, there can be
only one registered handler. Lambda expressions are really just syntactic sugar
for inner classes, which are anonymous classes implementing a specified
interface. In this case, the method setOnAction
expects to
receive an object implementing the interface EventHandler<ActionEvent>
,
where EventHandler
is defined as follows:
EventHandler.java
It is not necessary to use a lambda expression to implement an event handler. The lambda expression is syntactic sugar for the following code defining an inner class:
desugared_lambda.javaIt is not even necessary to use an inner class; an ordinary class can be used to implement the desired interface, as in the following code:
desugared_lambda.java
In fact, even inner classes are syntactic sugar for top-level classes. Because they are just syntactic sugar, they come with some limitations. In particular, inner classes and lambda expressions can only refer to variables defined outside their scope if those variables are final. The reason for this limitation is that their objects may outlive the external variables they refer to. For that reason, they make a copy of any such external variable.