15 Javanotes 9.0, Section 6.3 — Basic Events
Section 6.3
Basic Events
Events are central to programming for a graphical
user interface. A GUI program doesn’t have a main() routine that
outlines what will happen when the program is run, in a step-by-step process
from beginning to end. Instead, the program must be prepared to respond to
various kinds of events that can happen at unpredictable times and in an order
that the program doesn’t control. The most basic kinds of events are generated
by the mouse and keyboard. The user can press any key on the keyboard, move the
mouse, or press a button on the mouse. The user can do any of these things at
any time, and the computer has to respond appropriately.
In Java, events are represented by objects. When an event occurs, the system
collects all the information relevant to the event and constructs an object to
contain that information. Different types of events are represented by objects
belonging to different classes. For example, when the user presses one of the
buttons on a mouse, an object belonging to a class called MouseEvent
is constructed. The object contains information such as the target of the event (that is, the component on
which the user clicked), the (x,y) coordinates of the point in the
component where the click occurred, which modifier keys (such as the shift key) are being held down,
and which button on the mouse was pressed.
When the user presses a key on the keyboard, on the other hand, it is
a KeyEvent object that is created, containing information
relevant to an event generated by using the keyboard.
After the event object is constructed, it can be passed as a parameter to a
designated method. That method is called an event handler for the
event. In JavaFX, event handlers are often written as lambda expressions.
By writing an event handler, the programmer says what should happen when the event occurs.
As a Java programmer, you get a fairly high-level view of events. There is a
lot of processing that goes on between the time that the user presses a key or
moves the mouse and the time that a method in your program is called to
respond to the event. Fortunately, you don’t need to know much about that
processing. But you should understand this much: Even though you didn’t
write it, there is a routine running somewhere that executes a loop of the form
while the program is still running:
Wait for the next event to occur
Handle the event
This loop is called an event loop. Every GUI
program has an event loop. In Java, you don’t have to write the loop. It’s part
of “the system.” If you write a GUI program in some other language, you might
have to provide a main routine that runs the event loop.
In this section, we’ll look at handling mouse and key events in Java, and we’ll
cover the framework for handling events in general. We will also see how to
make an animation.
6.3.1 Event Handling
For an event to have any effect, a program must detect the event and react
to it. In order to detect an event, the program must “listen” for it.
Listening for events is something that is done by an event listener,
which contains an event handler method that can respond to the event.
An event listener is defined by an interface that specifies the event handling
methods that it contains. Listeners for different kinds of events are defined by
different interfaces. In most cases, the interface is a functional interface,
defining a single event handler method; in that case, the listener can be given
by a lambda expression.
For many kinds of events in JavaFX, listeners are defined by a functional interface
named EventHandler, which defines the method handle(event).
The parameter to this method, event, is the event object that contains
information about the event. When you provide a definition for handle(),
you write the code that will be executed to handle the event.
(EventHandler is actually a parameterized type, something that
we have not encountered before and will not encounter officially until Section 7.3.
Basically, all this really means is that EventHandler really
defines many different types, with names like EventHandler<MouseEvent>,
EventHandler<KeyEvent>, and EventHandler<ActionEvent>.
The type EventHandler<MouseEvent> defines a handle(event)
method in which the event is of type MouseEvent,
the type EventHandler<KeyEvent> defines a handle(event)
method in which the event is of type KeyEvent, and so
on. Fortunately, you don’t need to understand parameterized types in this chapter; you only
need to know that the event object that you use when handling an event will have the appropriate
type for that event. For example, when handling a mouse event, the
event object is of type MouseEvent.)
Many events in JavaFX are associated with GUI components. For example, when
the user presses a button on the mouse, the associated component is the one
that contains the mouse cursor when the button is pressed. This object is
called the target of the event. In order to respond to the
event, you need to register a listener either with the target of the event
or with some other object that knows about the event. For example, lets look again at
this statement from HelloWorldFX.java, our first GUI program
from Section 6.1:
helloButton.setOnAction( evt -> message.setText("Hello World!") );
Here, helloButton is an object of type Button.
When the user clicks on the button, an event of type ActionEvent
is generated. The target of that event is helloButton.
The method helloButton.setOnAction() registers an event listener that will
receive notification of any ActionEvents from the button.
The listener in this case is defined by a lambda expression. In the lambda
expression, the parameter, evt, is the ActionEvent
object, and the code in the lambda expression is what happens in response to the
event. Most event handling in this chapter will be set up in a similar way.
For key events and some mouse events, it’s not just the event target
that gets a chance to respond to the event. For example, suppose
that you press a mouse button over a Canvas that is
inside a BorderPane that is in turn inside a
Scene. The target of the mouse event is the Canvas
but the BorderPane and the Scene also have
a chance to respond to the event. That is, you can register a mouse event listener on any
or all of these objects to respond to the event. The object that the listener is registered
with is called the source of the event. The event object parameter, evt,
in an event handler method has both a source, given by evt.getSource(),
and a target, given by evt.getTarget(); often they are the same, but they
don’t have to be. Note that the same event can be sent to several handlers.
A handler can “consume” an event, by calling evt.consume(), to stop it
from being sent to any additional handlers. For example, when you are typing in a
text input box, the input box consumes the key events that you generate by typing,
so that the scene doesn’t get a chance to handle them.
(Actually, it’s more complicated than that. For key events and some kinds of mouse
events, the event first travels down through the scene and then through scene graph nodes that contain
the event target; this is called the “event filtering” or “bubble down” phase of event processing.
After reaching the target, the event travels back up through the scene graph and finally to the scene;
this is the “event handling” or “bubble up” phase. The event can be consumed at any point
along the way, and if that happens, the process stops. None of this is used in this chapter,
but for more information, see the documentation for the addEventFilter()
and addEventHandler() methods in the Scene
and Node classes.)
Most of this section is concerned with mouse and key events. It is important to understand
that many GUI programs do not need to deal with such events directly. Instead, you
work with GUI components that are already programmed to handle mouse and key events on
their own. For example, when the user clicks a Button, it is
the button that listens for mouse events and responds to them. When the button
detects that it has been clicked, it generates an ActionEvent.
When you write an application that uses buttons, you program responses to ActionEvents,
not to mouse events. Similarly, when the user types in a text input box, it is the input box
that listens for key events and responds to them. Nevertheless, at base, it’s mouse and
keyboard events that drive the action in a program. It’s useful to understand them—and
you can do some interesting things by processing them directly.
6.3.2 Mouse Events
A mouse event is represented by an object of type MouseEvent.
(In fact, mouse events can actually be generated by other input devices, such as
a trackpad or touch screen; events from these devices are translated by the system
into MouseEvents.) That class, and all of the classes related
to mouse and key events, can be found in
package javafx.scene.input. As the user manipulates the mouse, several
kinds of event are generated. For example, clicking a mouse button generates three
events, a “mouse pressed” event, a “mouse released” event, and a “mouse clicked” event.
Simply moving the mouse generates a sequence of events as the mouse cursor moves from point
to point on the screen. To respond to mouse events on a
component, you can register listeners with that component. You can register a separate
listener for each kind of mouse event on a component c using instance methods
such as c.setOnMousePressed(handler) and c.setOnMouseMoved(handler).
The parameter is a mouse event handler, generally given as a lambda expression. Suppose,
for example, that canvas is a component of type canvas,
and that you would like a method, redraw(), to be called when the user
clicks the canvas. You can make that happen by saying
canvas.setOnMousePressed( evt -> redraw() );
Generally, you would put this statement in the start() method of
an Application, while setting up the GUI for the program.
Mouse clicks on the canvas could be also be handled by the scene or by any
scene graph node that contains the canvas, directly or indirectly, but it is much more usual for the target of
a mouse event to handle the event.
Mouse event types include:
MouseEntered, generated when the mouse cursor moves from outside a component into the component;
MouseExited, generated when the mouse cursor moves out of a component;
MousePressed, generated when the user presses one of the buttons on the mouse;
MouseReleased, generated when the user releases one of the buttons on the mouse;
MouseClicked, generated after a mouse released event if the user pressed and released the mouse button on the same component;
MouseDragged, generated when the user moves the mouse while holding down a mouse button; and
MouseMoved, generated when the user moves the mouse without holding down a button.
The target of a MouseDragged, MouseReleased, or MouseClicked
event is the same component where the mouse button was pressed, even if the mouse has moved outside
of that component. The target of a MousePressed or MouseMoved event is
the component that contains the mouse cursor when the event occurs. And for MouseEntered
and MouseExited, the target is the component that is being entered or exited.
Often, when a mouse event occurs, you want to know the location of the mouse
cursor. This information is available from the MouseEvent
parameter in the event-handling method, which
contains instance methods that return information about the event.
If evt is the parameter, then you can find out
the coordinates of the mouse cursor by calling evt.getX() and
evt.getY(). These methods return values of type double
that give the x and y coordinates where the mouse cursor
was positioned at the time when the event occurred. The
coordinates are expressed in the coordinate system of the source of the
event, where the top left corner of the component is (0,0). (The source is
the component on which the event listener is registered; this is not necessarily
the same as the event target, but it usually is.)
The user can hold down certain modifier keys
while using the mouse. The possible modifier keys include: the Shift key, the
Control key, the Alt key (called the Option key on the Mac), and the Meta
key (called the Command or Apple key on the Mac). Not every computer has a Meta key.
You might want to respond to a mouse event differently when the user
is holding down a modifier key. The boolean-valued instance methods
evt.isShiftDown(), evt.isControlDown(),
evt.isAltDown(), and evt.isMetaDown() can be called to test
whether the modifier keys are pressed.
You might also want to have different responses depending on whether the
user presses the left mouse button, the middle mouse button, or the right mouse
button. For events triggered by a mouse button,
you can determine which button was pressed or released by calling
evt.getButton(), which returns one of the enumerated type constants
MouseButton.PRIMARY, MouseButton.MIDDLE, or MouseButton.SECONDARY.
Generally, the left mouse button is the primary button and the right mouse button is secondary.
For events such as mouseEntered and mouseExited that are not triggered by buttons,
evt.getButton() returns MouseButton.NONE.
The user can hold down several mouse buttons at the same time. If you want to know
which mouse buttons are actually down at the time of an event, you can use the
boolean-valued functions evt.isPrimaryButtonDown(), evt.isMiddleButtonDown(),
and evt.isSecondaryButtonDown().
As a simple example, suppose that when the user clicks a Canvas,
canvas, you would like to draw a red rectangle at the point where the user clicked.
But if the shift key is down, you want to draw a blue oval instead.
An event handler to do that can be defined as:
canvas.setOnMousePressed( evt -> {
GraphicsContext g = canvas.getGraphicsContext2D();
if ( evt.isShiftDown() ) {
g.setFill( Color.BLUE );
g.fillOval( evt.getX() - 30, evt.getY() - 15, 60, 30 )
}
else {
g.setFill( Color.RED );
g.fillRect( evt.getX() - 30, evt.getY() - 15, 60, 30 );
}
} );
To get a better idea of how mouse events work, you should try the sample program
SimpleTrackMouse.java. This program
responds to any of the seven different kinds of mouse events
by displaying the coordinates of the mouse, the type of event, and a list of
the modifier keys and buttons that are down.
You can experiment with the program to see what happens as you do various
things with the mouse. I also encourage you to read the source code.
6.3.3 Dragging
A drag gesture occurs when the user moves the mouse while holding down
one of the buttons on the mouse. It is interesting to look at what a program needs to do in order to respond
to dragging operations. The drag gesture starts when the user presses a mouse button, it continues
while the mouse is dragged, and it ends when the user releases the button. This
means that the programming for the response to one dragging gesture must be
spread out over the three event handlers, one for MousePressed,
one for MouseDragged, and one for MouseReleased!
Furthermore, the MouseDragged handler
can be called many times as the mouse moves. To keep track of what is
going on between one method call and the next, you need to set up some instance
variables. In many applications, for example, in order to process a
MouseDragged event, you need to remember the previous coordinates of
the mouse. You can store this information in two instance variables
prevX and prevY of type double. It can also
be useful to save the starting coordinates, where the original MousePressed event
occurred, in instance variables. And I suggest having a
boolean variable, dragging, which is set to true
while a dragging gesture is being processed. This is necessary because in many applications, not
every MousePressed event starts a dragging operation to which you want to respond.
Also, if the user presses a second mouse button without releasing the first, there will be
two MousePressed events before the MouseReleased event; usually,
you don’t want the second MousePressed to start a new drag operation. The
event-handling methods can use the value of
dragging to check whether a drag operation is actually in progress.
Often, I will write instance methods to handle the events, which in outline
look something like this:
private double startX, startY; // Point where original mouse press occurred.
private double prevX, prevY; // Most recently processed mouse coords.
private boolean dragging; // Set to true when dragging is in progress.
. . . // other instance variables for use in dragging
public void mousePressed(MouseEvent evt) {
if (dragging) {
// The user pressed a second mouse button before releasing the first.
// Ignore the second button press.
return;
}
if ( we-want-to-start-dragging ) {
dragging = true;
startX = evt.getX(); // Remember starting position.
startY = evt.getY();
prevX = startX; // Remember most recent coords.
prevY = startY;
.
. // Other processing.
.
}
}
public void mouseDragged(MouseEvent evt) {
if ( dragging == false ) // First, check if we are
return; // processing a dragging gesture.
int x = evt.getX(); // Current position of Mouse.
int y = evt.getY();
.
. // Process a mouse movement from (prevX, prevY) to (x,y).
.
prevX = x; // Remember the current position for the next call.
prevY = y;
}
public void mouseReleased(MouseEvent evt) {
if ( dragging == false ) // First, check if we are
return; // processing a dragging gesture.
dragging = false; // We are done dragging.
.
. // Other processing and clean-up.
.
}
I will then install event handlers on the relevant component that simply
call these methods:
c.setOnMousePressed( e -> mousePressed(e) ); c.setOnMouseDragged( e -> mouseDragged(e) ); c.setOnMouseReleased( e -> mouseReleased(e) );
Note that the event handlers in these statements simply call another method in
the same class, and that method has the same parameter as the event handler.
That means that it’s possible to write the lambda expressions as method
references (Subsection 4.5.4). The methods that are called are instance
methods in the object “this“, so the method references would
have names like this::mousePressed, and the event handlers
could be installed using
c.setOnMousePressed( this::mousePressed ); c.setOnMouseDragged( this::mouseDragged ); c.setOnMouseReleased( this::mouseReleased );
As an example, let’s look at a typical use of dragging: allowing the user to
sketch a curve by dragging the mouse. This example also shows many other
features of graphics and mouse processing. In the program, you can
draw a curve by dragging the mouse on a large white drawing area, and you can
select a color for
drawing by clicking on one of several colored rectangles to the right of the
drawing area. The complete source code can be found in SimplePaint.java.
Here is a picture of the program window after some drawing has been done:
I will discuss a few aspects of the source code
here, but I encourage you to read it carefully in its entirety. There are
lots of informative comments in the source code.
In this program, all drawing is done on a single canvas that fills the entire
window. The program is designed to work for any reasonable canvas
size, that is, unless the canvas is too small. This means that
coordinates are computed in terms of the actual width and height of the canvas.
(The width and height are obtained by calling canvas.getWidth() and
canvas.getHeight().) This makes things quite a bit harder than they
would be if we assumed some particular fixed size for the canvas. Let’s look at
some of these computations in detail. For example, the large white drawing
area extends from y = 3 to y = height – 3 vertically and
from x = 3 to x = width – 56 horizontally. These numbers
are needed in order to interpret the meaning of a mouse click. They take into
account a gray border around the canvas and the color palette along the right
edge of the canvas. The gray border is 3 pixels wide. The colored rectangles are 50
pixels wide. Together with the 3-pixel border around the canvas and
a 3-pixel divider between the drawing area and the colored
rectangles, this adds up to put the right edge of the drawing area 56
pixels from the right edge of the canvas.
A white square labeled “CLEAR” occupies the region
beneath the colored rectangles on the right edge of the canvas.
Allowing for this region, we can figure out how
much vertical space is available for the seven colored rectangles, and then
divide that space by 7 to get the vertical space available for each rectangle.
This quantity is represented by a variable, colorSpace. Out of this
space, 3 pixels are used as spacing between the rectangles, so the height of
each rectangle is colorSpacing – 3. The top of the N-th
rectangle is located (N*colorSpacing + 3) pixels down from the top of
the canvas, assuming that we count the rectangles starting with zero. This is because there are
N rectangles above the N-th rectangle, each of which uses
colorSpace pixels. The extra 3 is for the border at the top of the
canvas. After all that, we can write down the command for drawing the
N-th rectangle:
g.fillRect(width - 53, N*colorSpace + 3, 50, colorSpace - 3);
That was not easy! But it shows the kind of careful thinking and precision
graphics that are sometimes necessary to get good results.
The mouse in this program is used to do three different things: Select a
color, clear the drawing, and draw a curve. Only the third of these involves
dragging, so not every mouse click will start a dragging operation. The
mousePressed() method has to look at the (x,y) coordinates
where the mouse was clicked and decide how to respond. If the user clicked on
the CLEAR rectangle, the drawing area is cleared by calling a
clearAndDrawPalette() method that redraws the entire canvas.
If the user clicked somewhere in the strip of colored
rectangles, the corresponding color is selected for drawing. This involves computing which color
the user clicked on, which is done by dividing the y coordinate by
colorSpacing. Finally, if the user clicked on the drawing area, a drag
operation is initiated. In this case, a boolean variable, dragging, is set to
true so that the mouseDragged and mouseReleased
methods will know that a curve is being drawn. The code for this follows the
general form given above. The actual drawing of the curve is done in the
mouseDragged() method, which draws a line from the previous location of
the mouse to its current location. Some effort is required to make sure that
the line does not extend beyond the white drawing area of the canvas. This is
not automatic, since as far as the computer is concerned, the border and the
color bar are part of the canvas. If the user drags the mouse outside
the white drawing area while drawing a curve, the mouseDragged() routine
changes the x and y coordinates to make them lie within the
drawing area.
6.3.4 Key Events
In Java, user actions become events in a program, with a GUI component
as the target of the event. When the user presses a button on the mouse,
the component that contains the mouse cursor is the target of the event.
But what about keyboard events? When the user presses a key, what component is
the target of the KeyEvent that is generated?
A GUI uses the idea of input focus to determine
the target of keyboard events. At any given time, just one
interface element on the screen can have the input focus, and that is where
keyboard events are directed. If the interface element happens to be a JavaFX
component, then the information about the keyboard event becomes an object
of type KeyEvent, and it is delivered to any key event handlers that are
listening for KeyEvents associated with that component.
Note that because of the way key events are processed, the Scene
object in the window that contains the focused component also gets a chance
to handle a key event. If there is no other focused component in the window, then
the scene itself will be the target for key events. In my sample programs,
I will usually add key event handlers to the scene object.
A program generally gives some visual feedback to the user about which component
has the input focus. For example, if the component is a text-input box,
the feedback is usually in the form of a blinking text cursor.
Another possible visual clue is to draw a brightly colored border around the edge
of a component when it has the input focus. You might see that on a button that
has focus. When a button has focus, pressing the space bar is equivalent to
clicking the button.
If comp is any component, and you would like it to have the
input focus, you can call comp.requestFocus().
In a typical user interface, the user can choose to give the focus to a component by
clicking on that component with the mouse. And pressing the tab key will often
move the focus from one component to another. This is handled automatically by
the components involved, without any programming on your part.
However, some components do not automatically request the input focus when the user
clicks on them. That includes, for example, a Canvas.
Such a component can still receive the input focus if its requestFocus()
is called. However, you can’t automatically move the focus to that component
with the tab key. To enable that, you can call comp.setFocusTraversable(true).
And you can test whether a component is focused by calling
comp.isFocused().
The focused component is contained in—or sometimes is itself—a window.
That window is said to be the “focused” or “active” window. It is usually the front
window on the screen. In JavaFX a Stage object is a window.
You can call stage.requestFocus() to request that the window be moved to
the front of the screen and become the active window. And you can call
stage.isFocused() to test whether the window is active.
Java makes a careful distinction between the keys that you press and
the characters that you type. There are lots of keys on a keyboard:
letter keys, number keys, modifier keys such as Control and Shift, arrow keys,
page up and page down keys, keypad keys, function keys, and so on. In some cases, such as the shift key,
pressing a key does not type a character. On the other hand, typing a character
sometimes involves pressing several keys. For example, to type an uppercase
“A”, you have to press the Shift key and then press the A key before releasing
the Shift key. On my MacOS computer, I can type an accented e, by
holding down the Option key, pressing the E key, releasing the Option key, and
pressing E again. Only one character was typed, but I had to perform three
key-presses and I had to release a key at the right time.
In JavaFX, there are three types of key event: KeyPressed, which is generated when
the user depresses any key on the keyboard; KeyReleased, which is generated
when the user releases a key that had been pressed; and KeyTyped, which
is generated when the user types a character, possibly using a series of key presses and
key releases. Note that one user action, such as pressing the E
key, can be responsible for two events, a keyPressed event and a
keyTyped event. Typing an upper case “A” can generate two
keyPressed events, two keyReleased events, and one keyTyped
event.
Usually, it is better to think in terms of two separate streams of events,
one consisting of keyPressed and keyReleased events and the
other consisting of keyTyped events. For some applications, you want
to monitor the first stream; for other applications, you want to monitor the
second one. Of course, the information in the keyTyped stream could be
extracted from the keyPressed/keyReleased stream, but it would be
difficult (and also system-dependent to some extent). Some user actions, such
as pressing the Shift key, can only be detected as keyPressed events.
I used to have a computer solitaire game that highlighted every card that could be
moved, when I held down the Shift key. You can do something like that in Java
by highlighting the cards when the Shift key is pressed and removing the highlight
when the Shift key is released.
There is one more complication. When you hold down a key on the
keyboard, that key might auto-repeat. This means
that it will generate multiple KeyPressed events with just one
KeyReleased at the end of the sequence.
It can also generate multiple KeyTyped events. For the most
part, this will not affect your programming, but you should not expect every
KeyPressed event to have a corresponding KeyReleased
event.
Each key on the keyboard has a code that identifies it. In JavaFX,
key codes are represented by enumerated type constants from the enum KeyCode.
When an event handler for a KeyPressed or KeyReleased
event is called, the parameter, evt, contains the code of the key
that was pressed or released. The code can be obtained by calling the function
evt.getCode(). For example, when the user presses the shift key,
this function will return the value KeyCode.SHIFT. You can find all the
codes in the documentation for KeyCode, but names for most keys
are easy to guess. Letter keys have names like KeyCode.A and KeyCode.Q.
The arrow keys are named KeyCode.LEFT, KeyCode.RIGHT, KeyCode.UP,
and KeyCode.DOWN. The space bar is KeyCode.SPACE. And
function keys have names like KeyCode.F7.
In the case of a KeyTyped event, you want to know which character
was typed. This information can be obtained by calling the function
evt.getCharacter(). This function returns a value of type String
that contains the character that was typed.
As a first example, you can check out the sample program
KeyboardEventDemo.java. This program draws a
small square on a canvas. The user can move the square left, right, up,
and down by pressing arrow keys. This is implemented in a method
private void keyPressed( KeyEvent evt )
that is called by an event handler for KeyPressed events. The
handler is installed on the Scene object in the start()
method with the statement
scene.setOnKeyPressed( e -> keyPressed(e) );
In the keyPressed() method, the value of
evt.getCode() is checked. If it’s one of the arrow keys that
was pressed, the canvas is redrawn to show the square in a different position.
The program also installs handlers for KeyReleased and
KeyTyped events in a similar way. To give the KeyTyped
handler something to do, it changes the color of the square when the user types
“r”, “g”, “b”, or “k”. I encourage you to run the program and to read the
entire source code.
6.3.5 AnimationTimer
There is another kind of basic event that I would like to introduce before
turning to a more interesting example; that is, events that are used to drive
an animation. The events in this case happen in the background, and you don’t have
to register a listener to respond to them. However, you do need to write a
method that will be called by the system when the events occur.
A computer animation is just a sequence of still images, presented to the user
one after the other. If the time between images is short, and if the change from one
image to another is not too great, then the user perceives continuous motion.
In JavaFX, you can program an animation using an object of type AnimationTimer
from package javafx.animation. An AnimationTimer, animator,
has a method animator.start() that you can call to start the animation running or
to restart it if it has been paused. It has the method animator.stop() to pause the animation.
It also has a method handle(time), but handle() is not a
method that you call; it’s one that you need to write to say what happens in the animation.
The system will call your handle() method once for each frame of the animation.
Its job is to do whatever is necessary to implement each frame.
The handle() method will be called on the JavaFX application thread,
which means that you can do things like draw on a canvas or manipulate a
GUI component. However, whatever you do should not take very long, since
JavaFX animations are meant to run at at least 60 frames per second, which means
handle() should ideally take less than 1/60 second to run.
AnimationTimer itself is
an abstract class, and handle() is an abstract method. This means that
to make an animation, you need to write a subclass of AnimationTimer
and provide a definition for the handle() method. Suppose, for example, that
you simply want to call a draw() method for each frame. This could be
done as follows, using an anonymous subclass of AnimationTimer
(see Subsection 5.8.3):
AnimationTimer animator = new AnimationTimer() {
public void handle( long time ) {
draw();
}
};
Then, to get the animation started, you would need to call
animator.start(). This could all be done in an application’s
start() method.
The parameter, time, gives the current time, measured as the
number of nanoseconds since some arbitrary time in the past (the same arbitrary
time that is used by System.nanoTime()). You can use time
in the calculations that you do for the frame, as a way of making each frame
different from the next. Another option is to use a frame number that increases
by one each time a frame is drawn, but you should keep in mind that the rate
at which handle() is called can vary, and the time between frames
is not guaranteed to be 1/60 second.
6.3.6 State Machines
We are ready to look at a program that uses animation and key events
to implement a simple game. The program uses an AnimationTimer
to drive the animation, and it uses a number of instance variables to keep track
of the current state of the game. The idea of “state” is an important one.
The information stored in an object’s instance variables is said to
represent the state of that object. When one of
the object’s methods is called, the action taken by the object can depend on
its state. (Or, in the terminology we have been using, the definition of the
method can look at the instance variables to decide what to do.) Furthermore,
the state can change. (That is, the definition of the method can assign new
values to the instance variables.) In computer science, there is the idea of a
state machine, which is just something that has a
state and can change state in response to events or inputs. The response of a
state machine to an event depends on what state it’s in when the event occurs.
An object is a kind of state machine. Sometimes, this point of view can be very useful in
designing classes.
The state machine point of view can be especially useful in the type of
event-oriented programming that is required by graphical user interfaces. When
designing a GUI program, you can ask yourself: What information about state do I
need to keep track of? What events can change the state of the program? How will
my response to a given event depend on the current state? Should the appearance
of the GUI be changed to reflect a change in state? How should
the state be taken into account when drawing the content of a canvas? All this is an
alternative to the top-down, step-wise-refinement style of program design,
which does not apply to the overall design of an event-oriented program.
In the KeyboardEventDemo program, discussed above, the state of the
program is recorded in instance variables such as squareColor,
squareLeft, and squareTop, which record the color and position
of the colored square. These state variables are used in
a draw() method that draws the square on a canvas. Their values are
changed in the key-event-handling methods.
In the rest of this section, we’ll look at another example, where the state
plays an even bigger role. In this example, the user plays a
simple arcade-style game by pressing the arrow keys. The
program is defined in the source code file SubKiller.java.
As usual, it would be a good idea to compile and run the program as well
as read the full source code. Here is a picture:
The entire application window is filled by a canvas.
The program shows a black “submarine” near the bottom of the canvas.
The submarine moves erratically back and forth near the bottom of the window. Near the top,
there is a blue “boat.” You can move this boat back and forth by pressing the
left and right arrow keys. Attached to the boat is a red “bomb” (or “depth charge”). You
can drop the bomb by hitting the down arrow key. The objective is to
blow up the submarine by hitting it with the bomb. If the bomb
falls off the bottom of the screen, you get a new one. If the submarine explodes, a
new sub is created and you get a new bomb. Try it! Make sure to hit the
sub at least once, so you can see the explosion.
Let’s think about how this game can be programmed. First of all, since we
are doing object-oriented programming, I decided to represent the boat, the depth
charge, and the submarine as objects. Each of these objects is defined by a
separate nested class inside the main application class, and each object has its own
state which is represented by the instance variables in the corresponding class.
I use variables boat, bomb, and sub
to refer to the boat, bomb, and submarine objects.
Now, what constitutes the
“state” of the program? That is, what things change from time to time and affect
the appearance or behavior of the program? Of course, the state includes the
positions of the boat, submarine, and bomb, so those objects have instance
variables to store the positions. Anything else, possibly less obvious? Well,
sometimes the bomb is falling, and sometimes it’s not. That is a
difference in state. Since there are two possibilities, I represent this aspect
of the state with a boolean variable in the bomb object,
bomb.isFalling. Sometimes the
submarine is moving left and sometimes it is moving right. The difference is
represented by another boolean variable, sub.isMovingLeft. Sometimes,
the sub is exploding. This is also part of the state, and it is represented
by a boolean variable, sub.isExploding. However, the explosions
require a little more thought. An explosion is something that takes place
over a series of frames. While an explosion is in progress, the sub
looks different in each frame, as the size of the explosion increases. Also,
I need to know when the explosion is over so that I can go back to moving and drawing the
sub as usual. So, I use an integer variable, sub.explosionFrameNumber
to record how many frames have been drawn since the explosion
started; the value of this variable is used only when an explosion is in progress.
How and when do the values of these state variables change? Some of them seem
to change on their own: For example, as the sub moves left and right, the state variables
that specify its position change. Of course, these variables are changing
because of an animation, and that animation is driven by an AnimationTimer. Each time
the animator’s handle() method is called, some of the state variables have to change to
get ready to draw next frame of the animation. The changes are made
in the handle() method before redrawing the canvas. The boat,
bomb, and sub objects each contain an
updateForNextFrame() method that updates the state variables of
the object to get ready for the next frame of the animation. The handle()
method calls these methods with the statements
boat.updateForNewFrame(); bomb.updateForNewFrame(); sub.updateForNewFrame();
There are several state variables that change
in these update methods, in addition to the position of the sub: If the bomb is
falling, then its y-coordinate increases from one frame to the next. If the
bomb hits the sub, then the isExploding variable of the sub
changes to true, and the isFalling variable of the bomb becomes false.
The isFalling variable also becomes false when the bomb falls off the
bottom of the screen. If the sub is exploding, then its explosionFrameNumber
increases by one in each frame, and if it has reached a certain value, the
explosion ends and isExploding is reset to false. At random times,
the sub switches between moving to the left and moving to the right. Its
direction of motion is recorded in the sub’s isMovingLeft variable.
The sub’s updateForNewFrame() method includes the following lines to
change the value of isMovingLeft at random times:
if ( Math.random() < 0.02 ) {
isMovingLeft = ! isMovingLeft;
}
There is a 1 in 50 chance that Math.random() will be less than
0.02, so the statement “isMovingLeft = ! isMovingLeft” is executed
in one out of every fifty frames, on average. The effect of this statement
is to reverse the value of isMovingLeft, from false to true or from
true to false. That is, the direction of motion of the sub is reversed.
In addition to changes in state that take place from one frame to the next, a few state
variables change when the user presses certain keys. In the program, this is checked
in a handler for KeyPressed events.
If the user presses the left or right
arrow key, the position of the boat is changed. If the user presses the down
arrow key, the bomb changes from not-falling to falling. The handler is a long
lambda expression that is registered with the scene in the application’s
start() method:
scene.setOnKeyPressed( evt -> {
// The key listener responds to keyPressed events on the canvas.
// The left- and right-arrow keys move the boat while down-arrow
// releases the bomb.
KeyCode code = evt.getCode(); // Which key was pressed?
if (code == KeyCode.LEFT) {
boat.centerX -= 15;
}
else if (code == KeyCode.RIGHT) {
boat.centerX += 15;
}
else if (code == KeyCode.DOWN) {
if ( bomb.isFalling == false )
bomb.isFalling = true;
}
} );
Note that it’s not necessary to redraw the canvas in this method,
since this canvas shows an animation that is constantly being redrawn
anyway. Any changes in the state will become visible to the user as soon as the
next frame is drawn. At some point in the program, I have to make sure that the
user does not move the boat off the screen. I could have done this in the key
event handler, but I chose to check for this in another routine, in
the boat object, since it seems natural to let the boat object be responsible
for keeping itself on the screen.
I encourage you to read the source code in SubKiller.java.
Although a few points are tricky, you should with some effort be able to read and
understand the entire program. Try to understand the program in terms of state
machines. Note how the state of each of the three objects in the program changes
in response to events from the timer and from the user.
While it’s not at all sophisticated as arcade games go, the
SubKiller game does use some interesting programming. And it
nicely illustrates how to apply state-machine thinking in event-oriented
programming.
6.3.7 Observable Values
There is one more kind of basic event that plays an important role
in JavaFX: events that are generated when an observable value
is modified. There is an example in the SubKiller program.
A Stage, stage, has a property of type
ObservableBooleanValue that tells whether or not stage
is currently the focused window. You can access the property by calling
stage.focusedProperty(). When the value of an ObservableBooleanProperty
changes, an event is generated. You can register a ChangeListener with the property,
containing an event handler method that will be called when the event occurs.
The handler method in this case has three parameters: the observable property that
generated the event, the previous value of the property, and the new value.
For an ObservableBooleanValue, the old and new values are
of type boolean. There are other observable value types, such
as ObservableIntegerValue, ObservableStringValue,
and ObservableObjectValue.
When I first wrote SubKiller, the animation would continue to run
even when the SubKiller window was not the focused window, which I found
annoying when I was trying to work in another window. I decided to pause the animation
when the window loses the input focus and to restart it when the window regains focus.
When the window loses or gains focus, the value of the observable boolean property
stage.focusedProperty() changes. To react to that change, I added a change listener
to the property, which stops the animation when the value of the property changes to false
and starts the animation when the value changes to true. So, I added this
code to the start() method:
stage.focusedProperty().addListener( (obj,oldVal,newVal) -> {
// This listener turns the animation off when this program's
// window does not have the input focus.
if (newVal) { // The window has gained focus.
timer.start();
}
else { // The window has lost focus.
timer.stop();
}
draw(); // Redraw canvas. (Appearance changes depending on focus.)
});
The addListener() method of an observable property registers a
change listener with the property.
Note that the lambda expression for the event handler takes three parameters.
The only one that I need here is newVal which represents the current
value of the stage’s focused property. The draw()
method draws draws some things differently, depending on whether the
stage is focused. It tests for that calling stage.isFocused().
JavaFX GUI components have many observable properties, of various types. For example,
the text on a Button is a property of type
ObservableStringProperty, and the width and the height of a canvas
are values of type ObservableDoubleProperty. We will encounter
more examples in the next section.