1. Adding Interactivity to the Renderer
A renderer is a collection of algorithms that take a Scene data structure
as their input and produce a FrameBuffer data structure as their output.
After the algorithms build the FrameBuffer object, we would like to see
the image that it represents. With the previous renderer, client programs
that used the renderer would store the FrameBuffer data in an image file.
Then the client program's user could open the image file with a separate
image viewing program. But, by the time the user opens the image file, the
renderer's client program will have probably terminated. That means that
the client program cannot respond to any kind of user feedback. The user
cannot interact with the renderer while the renderer is running.
The previous renderer is an "offline" renderer. After the rendering
pipeline fills a FrameBuffer with pixel data, the client program must
save the FrameBuffer as an image file in the file system (this is the
"offline" part; the pixel data needs to be stored outside of the client
program). The client program can continue to modify the Scene data
structure, re-renderer the modified scene into the FrameBuffer, and then
save another image file. But the client program cannot update the Scene
based on input from the program's user, who does not get to see the image
files until later. An offline renderer is good for creating the frames of
an animation since we do not expect to interact with an animation.
We want to create a system that allows user interaction with the renderer.
We want a system that allows a client program to display a FrameBuffer
on the computer's screen while the client program is running. This will
allow the client program to interactively respond to user input. The client
program can react to a user's mouse click or keyboard input by modifying the
Scene data structure, re-rendering the Scene into the FrameBuffer, and
then updating the screen display with the FrameBuffer's new contents.
We want to create an "online" (interactive) version of the renderer. There
are two distinct parts to an online renderer. First, we need a way to transfer
pixel data from a FrameBuffer object to a computer's display screen. Second,
we need a way to communicate user inputs from the computer's devices (mouse,
keyboard, etc.) to a client program.
With the first part in place, as soon as a client program has the rendering
pipeline fill a FrameBuffer with the pixel data that represents a Scene,
the client program can immediately have that FrameBuffer displayed on the
computer's screen for the user to see. With the second part in place, if the
user interacts with the program in any way, by moving the mouse, by clicking
on the displayed image, by using the keyboard to send a command to the
program, then the program can use that information to update the Scene,
re-render it into the FrameBuffer, and initiate an update of the display
screen.
Once we have those two pieces in place, by implementing a cycle of
- 1) display the
FrameBufferto the user, - 2) use user input to update the
Scene, - 3) re-render the
Sceneinto theFrameBuffer, - 4) got to step 1,
a program that uses this renderer becomes interactive.
This document describes the two parts of this interactive system.
A new class in the renderer.framebuffer package, the FrameBufferPanel
class, implements the first part.
We use the Java language's build in GUI framework to implement the second part. The Java language has an extensive, and sophisticated, library of GUI components and event handlers. We will briefly outline the basics of Java event-driven GUI programming.
1.1 Renderer source code
The Java source code for this renderer is publicly available as a zip file.
Here is a link to the renderer's source code.
Download and unzip the source code to any convenient location in your computer's file system. The renderer does not have any dependencies other than the Java 11 (or later) JDK. Once you have downloaded and unzipped the distribution, you are ready to compile the renderer and run the renderer's (interactive) example programs.
2. The FrameBufferPanel class
With an offline renderer client, after a FrameBuffer object is filled with
pixel data the client program saves the FrameBuffer data to an image file
in the file system and then a separate image viewer program is used to open
the image file, retrieve the pixel data from the storage device, and copy
the pixel data into a screen window owned by the image viewer program.
To create an online renderer client, we need a way to copy pixel data directly
from a FrameBuffer object into a screen window that is owned by the client
program.
This renderer adds a file, FrameBufferPanel.java, to the renderer.framebuffer
package. A FrameBufferPanel object is the link between a FrameBuffer
object and the Java GUI system. A FrameBufferPanel is a subclass of the
JPanel class from Java's Swing GUI library. A JPanel is a GUI component
that can act as a drawing surface. We can "draw" the pixel information
contained in a FrameBuffer into the graphics context of a JPanel.
Since a JPanel is a GUI component, it can be added to any GUI container.
We can use an instance of FrameBufferPanel as part of a Java GUI along
side of any other Java GUI components, like buttons, sliders, checkboxes,
and menus. We can also use event handlers to tie those GUI components to
the 3D scene we are displaying in the FrameBufferPanel.
The main difficulty in designing the FrameBufferPanel class is that we
want the transfer of pixel data from a FrameBuffer object to a JPanel
object to be as fast as possible. To get the speed that we need we use
Java's MemoryImageSource class. Also, we designed the pixel_buffer
array in the FrameBuffer class to be in the exact same data format
required by Java's MemoryImageSource. This way, the data in a
FrameBuffer object can be transferred by a MemoryImageSource object
directly to the JPanel object.
- https://docs.oracle.com/en/java/javase/21/docs/api/java.desktop/javax/swing/JPanel.html
- https://docs.oracle.com/en/java/javase/21/docs/api/java.desktop/java/awt/image/MemoryImageSource.html
In a later section of this document we will explain a bit more about how
the FrameBufferPanel class uses a JPanel to paint the pixels from a
FrameBuffer on the computer screen.
The class FrameBufferPanel is to our renderer much as the GLUT library
is to the OpenGL renderer. The OpenGL renderer only knows how to compute
a framebuffer full of pixel data. OpenGL does not have the ability to
display pixel data in a screen window. The GLUT library (or any one of
many other similar libraries) takes the pixel data off of OpenGL's hands
and displays the data in a graphics window. So GLUT acts as an interface
between the OpenGL renderer and the operating system's GUI, which displays
the pixel data and handles user events. (What is a bit weird about the
OpenGL case is that OpenGL computes all of the pixel data in the graphics
card, so all the pixel data is right where it needs to be, and it never
leaves the graphics card, but the OpenGL library itself does not have a
way to make that pixel data appear on the screen. That must be done by
some other library.)
Here is another interesting analogy for our FrameBufferPanel class.
Google's Chrome web browser is built on top of a renderer (but it is
not a 3D-graphics renderer, it is an HTML renderer). This HTML renderer
is called Blink. You could say that Chrome is to Blink much as our
FrameBufferPanel class is to our 3D renderer (or as GLUT is to the OpenGL
renderer). In each case, the renderer computes values for all the pixels
within a framebuffer but the renderer does not by itself know how to
display these pixels nor how to handle user events. So the renderer
needs an interface between itself and the operating system's GUI. So
FrameBufferPanel is an interface between our renderer and the
Java GUI, Chrome is an interface between the Blink renderer and the
operating system's GUI, and GLUT is an interface between the OpenGL
renderer and the operating system's GUI.
- https://www.chromium.org/blink/
- https://en.wikipedia.org/wiki/Blink_(browser_engine)
- https://en.wikipedia.org/wiki/Web_browser_engine
- https://www.opengl.org/resources/libraries/glut/
- https://en.wikipedia.org/wiki/OpenGL_Utility_Toolkit
3. GUI Programming
The FrameBufferPanel class lets us use our renderer with the Java GUI
framework. The FrameBufferPanel class makes a FrameBuffer object look,
to the Java GUI framework, like a JPanel component. In this section we
will explain what we mean by a GUI component and how we allow a user to
interact with the FrameBufferPanel.
GUI programming can be divided into two aspects, the appearance and the behavior of a GUI. The appearance of a GUI is a special case of 2D graphics programming. The behavior of a GUI is an example of event-driven programming.
- https://en.wikipedia.org/wiki/Graphical_user_interface
- https://en.wikipedia.org/wiki/Event-driven_programming
Below we will first describe how to code the appearance of a GUI. Then we will describe how to code a GUI's behavior.
Here are a few introductory references to Java GUI programming. Most of these references are chapters from introductory Java textbooks, so they are basic references that assume you have never written event-driven GUI programs. Please use these references to supplement the discussion of Java GUI programming given below.
- Chapter 14, Graphical User Interfaces from Building Java Programs
- Chapter 6, Intro to GUI Programming (PDF) from Intro to Programming Using Java
- Chapter 4.4, A Graphical User Interface (GUI) (PDF) from Java, Java, Java
- Chapter 13, Graphical User Interfaces (PDF) from Java, Java, Java
- GUI Programming.pdf by Ken Slonneger
- Chapter 13, Window Interfaces using Swing
- Java Programming Tutorial, Programming Graphical User Interface
- Trail: Creating a GUI With Swing from The Java Tutorials
4. Java's GUI Framework
Java's GUI system is made up of a large number of classes, most of them in
two packages (and their sub-packages). The two packages are java.awt and
javax.swing. The java.awt package was written first. The javax.swing
package came later. Some classes in javax.swing are meant to replace
classes fromjava.awt. Most classes in javax.swing supplement the
classes in java.awt.
- https://docs.oracle.com/en/java/javase/21/docs/api/java.desktop/java/awt/package-summary.html
- https://docs.oracle.com/en/java/javase/21/docs/api/java.desktop/javax/swing/package-summary.html
A good way to understand the Java GUI framework is to divide up (most) of its classes into five categories.
Here is an outline of the five main parts of the Java GUI framework.
I) Components
II) Containers
III) Layout Managers
IV) Events
V) Event Listeners
I) Components II) Containers
1) Label, JLabel 1) Window, JWindow
2) Button, JButton 2) Frame, JFrame
3) Checkbox, JCheckBox 3) Panel, JPanel
4) ComboBox, JComboBox 4) Dialog, JDialog
5) List, JList 5) Box
6) TextField, JTextField
7) TextArea, JTextArea III) Layout Managers
8) ScrollBar, JScrollBar 1) BorderLayout
9) JRadioButton 2) FlowLayout
10) JSlider 3) GridLayout
11) JScrollPane 4) BoxLayout
12) JToolBar 5) CardLayout
13) JTabbedPane 6) GridBagLayout
14) Choice 7) BoxLayout
15) Canvas
16) menus
IV) Events V) Event Listener Interfaces
1) ActionEvent 1) ActionListener (1 method)
a) button 2) ItemListener (1 method)
b) combo box 3) AdjustmentListener (1 method)
c) list 4) ChangeListener (1 method)
d) radio button 5) TextListener (1 method)
e) text field 6) FocusListener (2 methods)
f) menu item 7) KeyListener (3 methods)
g) timer 8) MouseListener (5 methods)
2) ItemEvent MouseMotionListener (2 methods)
a) check box 9) MouseWheelListener (1 method)
b) combo box 10) ComponentListener (4 methods)
c) list 11) WindowListener (7 methods)
d) choices WindowFocusListener (2 methods)
e) check box menu item WindowStateListener (1 method)
f) radio button menu item
3) AdjustmentEvent
a) scroll bar
4) ChangeEvent
a) slider
5) TextEvent
a) text field
b) text area
c) text pane
6) FocusEvent
a) all components
7) KeyEvent
a) all components
8) MouseEvent
a) all components
9) MouseWheelEvent
a) all components
10) ComponentEvent
a) all components
11) WindowEvent
a) windows (including frames and dialogs)
- https://docs.oracle.com/en/java/javase/21/docs/api/java.desktop/java/awt/Component.html
- https://docs.oracle.com/en/java/javase/21/docs/api/java.desktop/java/awt/Container.html
- https://docs.oracle.com/en/java/javase/21/docs/api/java.desktop/java/awt/LayoutManager.html
- https://docs.oracle.com/en/java/javase/21/docs/api/java.desktop/java/awt/AWTEvent.html
- https://docs.oracle.com/en/java/javase/21/docs/api/java.desktop/java/awt/event/AWTEventListener.html
The components, containers, and layout managers determine the appearance of a GUI.
The events and event listeners determine the behavior of a GUI.
Components are the building blocks of a GUI's appearance. Things like buttons, check boxes, drop down lists, text boxes, menus, etc. These are the elements of a GUI that the user interacts with. Here is a visual summary of the most common Java GUI components.
The word "Component" is used by the Java GUI system but other GUI systems have other names for components. For example, in the Microsoft Windows GUI system components are called "Controls". In the Python GUI system components are called "Widgets". In the HTML GUI system they are called "Elements" (more specifically, "interactive elements").
- https://web.mit.edu/6.005/www/sp14/psets/ps4/java-6-tutorial/components.html
- https://learn.microsoft.com/en-us/windows/win32/controls/individual-control-info
- https://en.wikipedia.org/wiki/Graphical_widget
- https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input
- https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button
A Container is used to group components together in a GUI. A typical GUI can usually be divided into logical parts. The GUI components within each part will be grouped into a container. Look at whatever GUI program you are using to read this document. Try to find the logical groups of components in that GUI. Can you see the probable outlines of their containers? (Look for toolbars, menu bars, side panels, button groups, etc.)
A Layout Manager is responsible for automatically positioning components within a container. In most GUI systems, the programmer is not responsible for the exact position of every component in a screen window. The programmer places components in a container and gives the container a layout manager. When the program runs, the layout manager assigns each component its exact location in the screen window. If the GUI's user resizes the screen window, then the layout manager does the work of figuring out a new location for each component. This scheme makes GUI programming easier for the programmer and the results more reliable for the user.
Here is a visual summary of the most common Java layout managers.
An Event is a Java object that represents a user's interaction with some
component in the GUI. If we click on a button in the GUI, that button click
is represented by an ActionEvent object. If we click on a check box in
the GUI, that click is represented by an ItemEvent object. An event object
stores information about the GUI event (which button (or check box) was
clicked, when the click occurred, etc.). Every event object originates
in the operating system, which delivers the object to the Java Virtual
Machine (JVM), which then delivers the object to an event listener method
in the Java GUI program.
Event Listeners are the building blocks of a GUI's behavior. An event listener is a method that we write as part of our GUI program. Our event listener method will be called by the JVM when the JVM determines that the method is responsible for handling a particular event object. Notice that while we write the event listener methods (they are part of our code), our code never calls the event listeners. They are only called by the JVM. This is sometimes referred to as "Inversion of Control" (IoC). We write the methods but we relinquish the calling of those methods to some other code, in this case the JVM.
5. Creating A GUI
First we will look at code that creates Java GUIs without any event handlers. We will emphasize the code that gets a GUI up on a screen and adds components to the GUI. In later sections we will see how to make those components do something.
For code examples of creating Java GUIs, download the following zip file.
An effective way to understand the Java code that builds a GUI is to execute the code using the Java Shell program. The Java Shell (jshell.exe) lets us run one line of Java code at a time. This gives us a way to experiment with individual lines of GUI code, to see the visual effect each line of code has on the GUI.
- https://dev.java/learn/jshell-tool/
- https://docs.oracle.com/en/java/javase/25/jshell/introduction-jshell.html
- https://www.oracle.com/a/ocom/docs/corporate/java-magazine-jul-aug-2017.pdf#page=29
Open a command-prompt window and on the command-line type the following command.
> jshell
You should see a response that looks something like this.
| Welcome to JShell -- Version 11.0.7
| For an introduction type: /help intro
jshell>
At the jshell prompt, type the following five lines of Java code.
jshell> import java.awt.*
jshell> import javax.swing.*
jshell> var jf = new JFrame("Hello")
jshell> jf.setSize(300, 300)
jshell> jf.setVisible(true)
You should see a Java GUI window appear near the upper left-hand corner of your computer screen. Now type the following three additional lines of code, one line at a time. Notice the visual effect that each line has on the window.
jshell> js.setTitle("Interesting!")
jshell> jf.getContentPane().setBackground(Color.green)
jshell> jf.setLocation(0, 400)
We can make the window disappear and then reappear.
jshell> jf.setVisible(false)
jshell> jf.setVisible(true)
Let us build up a simple GUI with a few components. The JFrame window
has a BorderLayout object as its layout manager. But BorderLayout
is not the easiest layout manager to use. So let us replace the default
Borderlayout object with a FlowLayout object.
jshell> jf.setLayout(new FlowLayout())
Now try the following lines of code, one line at a time. Be sure to notice the visual effect (or lack of an effect) of each line.
jshell> var label = new JLabel("Notice Me")
jshell> jf.add(label)
jshell> jf.pack()
jshell> var button = new JButton("Press Me")
jshell> jf.add(button)
jshell> jf.pack()
Notice that creating a GUI component object, like a JButton, does not make
that component part of the GUI. When we use the new operator to create a
component object, we are creating a Java object in the Java heap. That does
not (yet) have any effect on the screen's GUI. Even when we add the component
to the JFrame object, the component does not (yet) appear on the screen
(though it is now part of the GUI). We need to tell the GUI to "rebuild"
itself in order to see the new component as part of the visual GUI.
Now try the following three slightly more complex lines of code. You can
copy-and-paste these lines into your jshell prompt, one line at a time.
jshell> jf.add(new JLabel(UIManager.getIcon("FileView.directoryIcon")))
jshell> jf.add(new JButton(UIManager.getIcon("FileView.fileIcon")))
jshell> jf.pack()
At this point our GUI has four components in it. Try resizing the JFrame
window. In particular, make the window less wide. Notice how the components
rearrange themselves under the direction of the FlowLayout manager.
To remind ourselves what code we have typed into the jshell prompt, use
the following JShell command.
jshell> /list
Let us give the JFrame a different layout manager and see what difference
it makes.
jshell> jf.setLayout(new GridLayout(2, 2))
jshell> jf.pack()
Be sure to resize the window and see how it now behaves.
Let's try one more layout manager.
jshell> jf.getContentPane().setLayout(new BoxLayout(jf.getContentPane(), BoxLayout.Y_AXIS))
jshell> jf.pack()
Resize the window. Try changing Y_AXIS to X_AXIS and re-executing those
lines of code. How does this compare to the flow layout?
Let's return to the flow layout.
jshell> jf.setLayout(new FlowLayout())
jshell> jf.pack()
Let us now add a container to our GUI.
jshell> var jp = new JPanel()
jshell> jf.add(jp)
jshell> jf.pack()
Let's put three buttons in the JPanel container.
jshell> jp.add(new JButton("Button 1"))
jshell> jp.add(new JButton("Button 2"))
jshell> jf.pack()
jshell> jp.add(new JButton("Button 3"))
jshell> jf.pack()
Notice that the three buttons act as a rigid group. They flow as a single unit (and they do not seem to want to flow within their group).
Try changing the layout manager of the JPanel container.
jshell> jp.setLayout(new GridLayout(2,2))
jshell> jf.pack()
Review the code that has been executed so far.
jshell> /list
The most interesting idea in this code is that we have constructed a tree data structure, a GUI "scene graph".
JFrame
/ \
/ \
FlowLayout List<Component>
/ / | \ \
JLabel / | \ \
JButton | \ \
JLabel \ \
JButton \
JPanel
/ \
/ \
GridLayout List<Component>
/ | \
/ | \
JButton JButton JButton
Every GUI you see on a screen is represented in the GUI program's memory
as a tree (scene graph) data structure. The root of a GUI scene graph must
be a "top level container", either a JFrame, a JDialog, or a JWindow.
These are the kinds of windows that you can have floating around your
computer screen. Every container, including a top level one, contains a
reference to a LayoutManager object and a List<Component> object. The
members of that list are the components that visually show up inside the
container's window. While the program is running the LayoutManager
object decides how those components are placed inside the container's
window. A JPanel container is also a component object, so we can nest
JPanel objects inside a top level window. This allows us to extend our
tree to any depth, since the List<Component> for a JPanel can contain
more JPanel objects. (But you cannot let a JPanel be the root of a GUI
scene graph; a JPanel cannot exist on your computer screen by itself.)
Each JPanel groups the components within its list and its LayoutManager
object decides how to position those components within the extent of the
JPanel.
Here is a diagram of the inheritance tree for components and containers. (Notice how trees keep appearing, over and over again, in Computer Science.)
To see more complex examples of building a GUI, look at the code in the example folder.
In particular, notice the files in the JavaGui/gui-experiments folder that
have the filename extension .jsh. These files are Java shell scripts
(or "Java scripts", which are not to be confused with JavaScript). A Java
shell script is a file meant to be run by the JShell program.
To run a Java shell script, open a command-prompt window in the folder
that contains the script file (for example the guiExperiment3.jsh file
in the JavaGui/gui-experiments folder`) and on the command-line type
the a command like the following.
> jshell guiExperiment3.jsh
This causes JShell to execute every line of code in the script file. What
is really useful is that when JShell gets to the end of the script's code,
JShell does not quit, it stops and gives us a prompt from which we can
continue entering more Java code. To see what code was executed by the
script, start with the JShell list command.
jshell> /list
Then try adding this line of code.
jshell> jp.setBackground(Color.yellow)
Putting lines of Java code in a JShell script file, executing the script,
and then experimenting with the code using the jshell prompt, is a
sophisticated and effective way to learn about the Java language. It
works especially well with GUI code.
6. Java 2D Graphics
The JPanel class plays two roles in Java's GUI system. As we have seen,
it is a container for holding and organizing other components. But JPanel
is also a "drawing surface". Java has a library of methods that let you
draw two-dimensional geometric shapes on a JPanel. This section explains
how to get started with this 2D graphics API.
Open a command-prompt window and on the command-line start a JShell session.
> jshell
When the jshell prompt appears, copy-and-paste the following block of
code into the prompt. JShell allows you to copy several lines of code at
a time. It will execute each line of code and then give you another prompt.
import java.awt.*
import javax.swing.*
var jf = new JFrame("2D Graphics")
jf.setLayout(new FlowLayout())
jf.setSize(300, 300)
var jp = new JPanel()
jp.setPreferredSize(new Dimension(200, 200))
jp.setBackground(Color.green)
jf.add(jp)
jf.setVisible(true)
Graphics g = jp.getGraphics()
The last line of code created a "graphics context", called g, that we
can use for drawing geometric shapes. A graphics context is essentially
a viewport into the framebuffer that holds the pixel data for the GUI
window.
At the jshell prompt, type the following lines of Java code, one line
at a time so that you can see what each line adds to the GUI. These lines
of code update the pixel data in the graphics context (the viewport).
jshell> g.setColor(Color.red)
jshell> g.fillRect(10, 20, 60, 30)
jshell> g.setColor(Color.blue)
jshell> g.fillRect(100, 10, 30, 80)
jshell> g.setColor(Color.yellow)
jshell> g.drawRect(120, 120, 50, 50)
jshell> g.setColor(Color.black)
jshell> g.drawLine(0,0, 200,200)
jshell> g.setColor(Color.magenta)
jshell> g.drawString("This is graphical.", 20, 180)
jshell> g.drawOval(150, 10, 30, 80)
jshell> g.fillOval(50, 100, 50, 50)
jshell> UIManager.getIcon("FileView.computerIcon").paintIcon(jp, g, 30, 60)
Use your mouse to slightly change the size of the JFrame window. Notice
that all the graphical shapes disappear. This is because the JFrame and
its JPanel "repaint" themselves when you change the size of their window.
When the JPanel object repaints itself, it has no way to remember the code
that we used to paint on the JPanel. Our code was not in any way part of
that JPanel object. Our code just used a reference to the graphics context
from theJPanel object. When the JPanel object repainted itself, it used
its background color to redraw all of its pixels. The background color, along
with the size, is something we set inside of the JPanel object.
Use your mouse to make the JFrame window very wide, so that the JPanel
moves to the right of where it was.. Then copy-and-paste the following
block of code into the jshell prompt to repaint the graphical shapes.
g.setColor(Color.red)
g.fillRect(10, 20, 60, 30)
g.setColor(Color.blue)
g.fillRect(100, 10, 30, 80)
g.setColor(Color.yellow)
g.drawRect(120, 120, 50, 50)
g.setColor(Color.black)
g.drawLine(0,0, 200,200)
g.setColor(Color.magenta)
g.drawString("This is graphical.", 20, 180)
g.drawOval(150, 10, 30, 80)
g.fillOval(50, 100, 50, 50)
UIManager.getIcon("FileView.computerIcon").paintIcon(jp, g, 30, 60)
Notice that the graphical shapes are not drawn inside of the JPanel!
This is because the "graphics context" g is old and out of date (it
represents the graphics context of where the JPanel was). After the
JPanel repositions and repaints itself, we need to get a new reference
to the graphics context.
jshell> g = jp.getGraphics()
Do not resize the window yet. After you get the new graphics context,
copy-and-paste the above block of code back into your jshell prompt.
This time the graphical elements should be drawn inside of the JPanel
(and the misplaced graphical shapes should still be in the JFrame).
Use your mouse to move the JFrame window around your computer screen
(but do not change the window's size). The graphical shapes should all
move with the window because moving a window does not cause the window
to "repaint" itself.
Having our artwork erased every time we even slightly resizes our graphics window (or minimize the window) is very inconvenient. We fix this by using a really interesting concept. We will create our own GUI component, one that knows how to draw itself with the graphical shapes that we want.
We create our own GUI component by defining a subclass of JPanel. The
JPanel class contains a lot of code that makes it into a GUI component.
We want to reuse as much of that code as we can and modify just enough of
the JPanel class so that our subclass of JPanel paints itself the way
we want it to. (We will leave all the other behaviors of JPanel alone,
though if you know what you are doing, you can modify other JPanel
behaviors in the subclass.)
The minimum amount of code that we need to write for our new GUI component
is a constructor and a paintComponent() method. The paintComponent()
method is called by the JVM whenever a GUI component needs to "repaint"
itself. The code we put in paintComponent() is what gives our GUI
component its own distinct look.
Copy and past this class definition into your jshell prompt.
class MyComponent extends JPanel {
public MyComponent(){ // constructor
this.setPreferredSize(new Dimension(200, 200));
this.setBackground(Color.green);
}
@Override public void paintComponent(Graphics g){
super.paintComponent(g);
g.setColor(Color.red);
g.fillRect(10, 20, 60, 30);
g.setColor(Color.blue);
g.fillRect(100, 10, 30, 80);
g.setColor(Color.yellow);
g.drawRect(120, 120, 50, 50);
g.setColor(Color.black);
g.drawLine(0,0, 200,200);
g.setColor(Color.magenta);
g.drawString("This is graphical.", 20, 180);
g.drawOval(150, 10, 30, 80);
g.fillOval(50, 100, 50, 50);
}
}
At your jshell prompt enter these two lines of code.
jshell> jf.add(new MyComponent())
jshell> jf.pack()
Enter those two lines of code again.
jshell> jf.add(new MyComponent())
jshell> jf.pack()
We can create as many instances as we want of our new GUI component.
This idea of creating a new GUI component that knows how to paint itself
is the main idea behind how the FrameBufferPanel class manages to paint
the pixels from a FrameBuffer object into a JPanel object.
Exercise: Modify the MyComponent class so that the size and background
color are constructor parameters.
Notice that MyComponent is not very interactive. It always draws the same
shapes and, other than moving it around, it does not respond to any user
interactions. We want to start looking at Java events so that we can use
them to make our GUI components (and our 3D renderer) more interactive.
There are more Java 2D examples in the folder JavaGui/graphics-experiments.
Here are a few references for using Java's 2D graphics API.
- https://math.hws.edu/javanotes-swing/c6/s2.html
- https://math.hws.edu/eck/cs124/downloads/javanotes9-swing-linked.pdf#page=289
- https://math.hws.edu/graphicsbook/c2/s5.html
- https://math.hws.edu/eck/cs424/downloads/graphicsbook-linked.pdf#page=56
- https://docs.oracle.com/javase/tutorial/2d/index.html
- https://docs.oracle.com/en/java/javase/21/docs/api/java.desktop/java/awt/Graphics.html
- https://docs.oracle.com/en/java/javase/21/docs/api/java.desktop/java/awt/Graphics2D.html
7. Java's Event Handling Model
We need to explain the relationship between three of the categories defined above, components, events, and event listeners. Roughly, a component is an object that a user interacts with, an event listener is a method that handles those interactions, and an event is a message sent by a component to an event handler when the user interacts with the component.
If an event is a message sent by a component to an event handler, then who is the messenger? It turns out that there are two messengers, the operating system (OS) and the Java Virtual Machine (JVM). Every event object originates in the OS. The OS passes them along to the JVM. The JVM calls the appropriate event handler method and passes the event object as a parameter to the method.
For code examples that use Java events, download the following zip file.
Let's look at a simple code example.
Open a command-prompt window and on the command-line start a JShell session.
> jshell
When the jshell prompt appears, copy-and-paste the following block of code
into the prompt. JShell allows you to copy several lines of code at a time.
It will execute each line of code and then give you another prompt.
import java.awt.*
import javax.swing.*
var jf = new JFrame("GUI with Events")
jf.setLayout(new FlowLayout())
var jb = new JButton("Press Me")
jf.add(jb)
jf.pack()
jf.setSize(200, 100)
jf.setVisible(true)
jb.addActionListener(e -> System.out.println(e + "\n"))
You should see a Java GUI window appear near the upper left-hand corner of
your computer screen and the window should contain a button. Every time you
press the button, you should see an output printed in the console window.
The output you see is the toString() of an event object (called e in this
code) that was delivered by the JVM to an event handler method (which, in
this code, is a lambda expression).
The last line of code is new to us. That line of code added an event handler
to the GUI code. Let's analyze it in detail. The variable jb refers to a
JButton object, which is a GUI component that can be the source of event
objects. When a JButton is pressed the JVM creates an ActionEvnet object.
That ActionEvent object needs to be delivered (by the JVM) to a method that
can handle it. The lambda expression
e -> System.out.println(e + "\n")
is a method that can handle an event object (called e). The
addActionListener() method tells the JButton object to remember this
method (the lambda expression) as the method the button's event objects should
be delivered to.
When we click on the JButton, the JVM creates an appropriate ActionEvent
object. Since the JVM knows which pixel we clicked on (the OS told it), the
JVM knows which button was clicked. The JVM asks that button to look up which
method is registered as its event handler. Then the JVM calls that method and
passes the event object as the method's parameter (the variable e). The
method prints the ActionEvent object on the console window.
The above code used a very compact notation, a lambda expression, to represent the event handler method. This compact notation is very nice to use when we are allowed to use it. But Java does not always allow this notation. Let us look at a more verbose notation for event handlers. This notation is always allowed.
Copy and past the following code into the jshell prompt. This code adds
a second button with its own event handler.
import java.awt.event.*
var jb2 = new JButton("Button 2")
jf.add(jb2)
jf.pack()
class ButtonHandler implements ActionListener {
@Override public void actionPerformed(ActionEvent e) {
System.out.println(e + "\n");
}
}
var buttonHandler = new ButtonHandler()
jb2.addActionListener(buttonHandler)
Like jb, jb2 is a GUI component that can be the source of event objects.
So jb2 needs an event handler method. We define a class ButtonHandler
which implements an interface, ActionListener. This tells Java that instances
of this class are capable of handling ActionEvent objects. That's because the
ActionListener interface requires a single method that takes an ActionEvent
object as its parameter. We create an instance of our ButtonHandler class.
That instance object holds just one thing, a method that is capable of
handling ActionEvent objects. The addActionListener() method tells 'jb2'
to remember this object as the carrier of the method that is responsible for
its events.
We just saw a compact and a verbose syntax for event handlers. Java also has an in-between notation, the "anonymous inner class" syntax.
Copy and past the following code into the jshell prompt. This code adds
a third button with its own event handler.
var jb3 = new JButton("Button 3")
jf.add(jb3)
jf.pack()
jb3.addActionListener(new ActionListener(){
@Override public void actionPerformed(ActionEvent e) {
System.out.println(e + "\n");
}
})
Here is a fourth JButton whose event handler uses the most compact notation.
var jb4 = new JButton("Button 4")
jf.add(jb4)
jf.pack()
jb.addActionListener(e -> System.out.println(e + "\n"))
Carefully compare the syntax of the last three button examples.
Here is an example of a GUI component that uses a different kind of event
object. A JCheckBox causes ItemEvent objects to be created when it is
checked. Notice that we tell the JCheckBox object to remember an
ItemListener (instead of an ActionListener).
var cb = new JCheckBox("Check Me");
jf.add(cb)
jf.pack()
jf.setSize(250, 150)
cb.addItemListener(e -> System.out.println(e + "\n"))
An interesting thing with JCheckBox is that we can activate it
with a line of code. Type this line into the jshell prompt.
cb.setSelected(true)
Now type this line.
cb.setSelected(false)
These lines change the visual appearance of the checkbox and they also
trigger an event. We can call setSelected() on a JButton but it
doesn't do anything (try it). We can activate JButton with the
doClick() method (try it). What does doClick() do to a JCheckBox?
Here is a JCheckBox handler that uses the anonymous inner class syntax.
Notice how we can now see explicit mentions of ItemListener, an
itemStateChanged method, and an ItenEvent parameter.
var cb2 = new JCheckBox("CheckBox 2")
jf.add(cb2)
jf.pack()
cb2.addItemListener(new ItemListener(){
@Override public void itemStateChanged(ItemEvent e) {
System.out.println(e + "\n");
}
})
Here is a picture that helps illustrate the relationships between a GUI component, an event handler, and an event. The picture shows how a component object, an event handler object, and an event object are related to each other in the Java heap.
GUI component object
+----------------------+ List<EventListener>
| | object
| List<EventListener>--|-------->+------+
| | | | EventListener object
| | +------+ +----------------------+
| addListener( ){...} | | -----|-------->| |
| | +------+ | handleEvent(Event e) |
+----------------------+ | | | { |
An object that initiates +------+ | ... |
events and receives Event | } |
objects from the JVM. | |
+----------------------+
An object that implements
Event object the EventListener interface
+-------------------+ and handles event objects
| Created by JVM | delivered by the JVM.
| using information |
| from the OS about |
| an event |
+-------------------+
Notice that a component holds a reference to a List of event listeners.
That means that a component can have multiple event listeners assigned to
it (which is actually a common situation). In your JShell session, try
adding a second event listener to one of the components in the GUI so
that when you click on that component, both of its event handlers respond.
Exercise: Start a new JShell session and create a GUI with two buttons that share a single event listener object. When you click either button, the same event listener method gets called.
Notice that a single component can have multiple event listeners and a single event listener can service multiple components. Try creating a GUI example that demonstrates both of these concepts.
8. Event-driven GUI Programs
So far we have take a very limited view of event-driven GUI programming. Our event handlers have done nothing useful. Usually, the GUI of an event-driven program should show some visual change after the user interacts with some component.
We need to add to our examples a GUI object that needs to be updated when the user interacts with one of our components. We need to add a fourth kind of object to our examples. The fourth kind of object, a "GUI object", is kind of a vague reference to something on the screen that the user can see and that changes appearance due to some event from some component in the GUI.
GUI component object
+----------------------+ List<EventListener>
| | object
| List<EventListener>--|-------->+------+
| | | | EventListener object
| | +------+ +----------------------+
| addListener( ){...} | | -----|-------->| |
| | +------+ | handleEvent(Event e) |
+----------------------+ | | | { |
An object that initiates +------+ | ... |
events and receives Event | } |
objects from the JVM. | |
+----------------------+
An object that implements
Event object the EventListener interface
+-------------------+ and handles event objects
| Created by JVM | delivered by the JVM.
| using information |
| from the OS about |
| an event | GUI object
+-------------------+ +------------------+
| |
| paintComponent() |
| { |
| ... |
| { |
| |
+------------------+
GUI object that needs
to be repainted after
an event occurs.
One specific kind of "GUI object" that we can use is an instance of Java's
JPanel class. The JPanel class has a method paintComponent() that we
call to tell the panel to update its drawing surface. That is the method
shown in the picture above inside of the "GUI object". In other words, we
can use events to take a component, like the MyComponent that we wrote
earlier, and make it interactive.
Open a command-prompt window and on the command-line start a JShell session.
> jshell
Copy-and-paste the following block of code into your jshell prompt.
import java.awt.*
import javax.swing.*
import java.awt.event.*
int colorNumber = 0
var jf = new JFrame("Simple GUI Example 1")
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)
jf.setLayout(new FlowLayout())
var jb = new JButton("Change Color")
jf.add(jb)
var buttonHandler = new ActionListener(){
@Override public void actionPerformed(ActionEvent e){
colorNumber = (colorNumber + 1) % 4;
System.out.println("The color number is = " + colorNumber);
}
}
jb.addActionListener(buttonHandler)
jf.pack()
jf.setVisible(true)
Click on the button in the GUI several times. Notice that the GUI prints
the value of the colorNumber variable in the console window and it
cycles the value between 0 and 3. We want to turn this program into an
interactive GUI program that uses the colorNumber variable to change
the appearance of the GUI.
We need to add to this example a "GUI object" that we can update on each
button click. The following block of code uses an anonymous inner class
to define a subclass of JPanel and override the paintComponent()
method. This new kind of JPanel uses the colorNumber variable to set
its background color. We also need to modify the button handler so that
after it updates the value of colorNumber, it tells the "GUI object"
that it needs to repaint itself.
Copy-and-paste this block of code into you jshell prompt.
Color[] color = {Color.red, Color.green, Color.blue, Color.yellow};
var jp = new JPanel(){ // The GUI object.
@Override protected void paintComponent(Graphics g){
super.paintComponent(g);
this.setBackground(color[colorNumber]);
}
}
jp.setPreferredSize(new Dimension(200, 100))
jf.add(jp)
jf.pack()
var buttonHandler2 = new ActionListener(){
@Override public void actionPerformed(ActionEvent e){
colorNumber = (colorNumber + 1) % color.length;
System.out.println("The color number is = " + colorNumber);
jp.repaint(); // Redraw the GUI object.
}
}
jb.addActionListener(buttonHandler2)
The JButten now has two event handlers, but it should only have one.
Here is how we can remove the event handler that is no longer needed.
Type the following line of code into the JShell prompt.
jb.removeActionListener(buttonHandler)
Notice one very important detail. Even though we override the
paintComponent() method in the JPanel class, we do not call it to
tell the component to repaint itself. Instead, we call another method
in the component, the repaint() method. This seems odd, and it is a
common mistake to call paintComponent() instead of repaint(). But
calling paintComponent() does not work! And calling it can make a
GUI program buggy and act in strange ways. When we call repaint()
that tells the JVM to please call 'paintComponent()for us. The JVM
will callpaintComponent()`, but it calls it at the correct time and,
more importantly, with the correct graphics context.
Now we have a very simple, interactive, event-driven, GUI program. Here is what the complete program looks like. Look for the places in the code where each of the four kinds of objects from the above picture get instantiated, the component object that is the source of events, the event handler object, the event object, and the "GUI object".
import java.awt.*
import javax.swing.*
import java.awt.event.*
int colorNumber = 0
Color[] color = {Color.red, Color.green, Color.blue, Color.yellow}
var jf = new JFrame("Simple GUI Example 1")
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)
jf.setLayout(new FlowLayout())
var jp = new JPanel(){ // The GUI object.
@Override protected void paintComponent(Graphics g){
super.paintComponent(g);
this.setBackground(color[colorNumber]);
}
}
jp.setPreferredSize(new Dimension(200, 100))
jf.add(jp)
var jb = new JButton("Change Color");
jf.add(jb);
var buttonHandler = new ActionListener(){
@Override public void actionPerformed(ActionEvent e){
colorNumber = (colorNumber + 1) % color.length;
System.out.println("The color number is = " + colorNumber);
jp.repaint(); // Redraw the GUI object.
}
}
jb.addActionListener(buttonHandler)
jf.pack()
jf.setVisible(true)
Let's build a second simple example of an event-driven GUI program.
Start a new JShell session and copy-and-paste this block of code into
the jshell prompt.
import java.awt.*
import javax.swing.*
import java.awt.event.*
var jf = new JFrame("Simple GUI Example 2")
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)
jf.addMouseListener(new MouseAdapter(){
@Override public void mouseClicked(MouseEvent e){
System.out.println("Mouse click: (x, y) = ("
+ e.getX() + ", " + e.getY() + ")");
}
})
jf.setSize(400, 400)
jf.setVisible(true)
This program prints on the console window the coordinates of any mouse clicks
that are inside the JFrame window. Play with the program a bit. Notice where
the x-coordinate is small and where it is large. Same for the y-coordinate.
Change the size of the window and see how this changes the range of x and
y coordinate values. Notice that you cannot click on a pixel with coordinate
near to (0, 0) (why do you think that is?).
Let's modify this program so that it keeps track of the last five mouse clicks. Give the program two lists, one list to keep track of x-coordinates and the other list for the y-coordinates. When the lengths of the lists get to six, then we remove the an item from each list so we keep just the last five mouse coordinates.
Copy-and-paste the following block of code into your jshell prompt.
var xs = new ArrayList<Integer>()
var ys = new ArrayList<Integer>()
jf.addMouseListener(new MouseAdapter(){
@Override public void mouseClicked(MouseEvent e){
xs.add(e.getX()); // Add at the end.
ys.add(e.getY());
if (6 == xs.size()){
xs.remove(0); // Remove from the front.
ys.remove(0);
}
System.out.print("[ ");
for (int i = 0; i < xs.size(); ++i) {
System.out.printf("(%d, %d) ", xs.get(i), ys.get(i));
}
System.out.println("]");
}
})
jf.pack()
jf.setVisible(true)
Again, play with this program a bit. Notice how at the beginning the
lists are shorter that five, then they grow to length five, and then
stay at length five. Also notice that the JFrame now has two mouse
listeners. The first one prints the coordinate of the last mouse click.
The second mouse listener prints the list of the last five mouse clicks.
Let's turn this program into an interactive GUI program that draws a disk
on the screen wherever one of the last five mouse clicks was. We need to
give this program a "GUI object" that we can update the visual appearance
of. We will once again use a subclass of JPanel for the "GUI object".
Copy-and-paste the following block of code into your jshell prompt.
var jp = new JPanel(){ // The GUI object.
@Override protected void paintComponent(Graphics g){
super.paintComponent(g);
final Rectangle r = g.getClipBounds();
g.clearRect(r.x, r.y, r.width, r.height);
for (int i = 0; i < xs.size(); ++i){
g.fillOval(xs.get(i), ys.get(i), 10, 10);
}
}
}
jp.setPreferredSize(new Dimension(400, 400))
jf.add(jp)
jp.addMouseListener(new MouseAdapter(){
@Override public void mouseClicked(MouseEvent e){
xs.add(e.getX());
ys.add(e.getY());
if (6 == xs.size()){
xs.remove(0);
ys.remove(0);
}
jp.repaint(); // Redraw the GUI object.
}
})
jf.pack()
The paintComponent() method uses the lists of x and y coordinates to
draw small ovals. Before drawing the ovals the method clears the graphics
context of the old ovals.
We need to update the mouse listener so that it tells the JPanel when to
repaint itself (instead of writing to the console window). One important
detail is that we are switching the mouse listener from the JFrame to the
new JPanel The old JFrame mouse listeners are still part of the program.
Do they have any effect on the program?
Here is what the complete program looks like. Look for the places in the code where each of the four kinds of objects from the above picture get instantiated, the component object that is the source of events, the event handler object, the event object, and the "GUI object".
import java.awt.*
import javax.swing.*
import java.awt.event.*
var xs = new ArrayList<Integer>()
var ys = new ArrayList<Integer>()
var jf = new JFrame("Track Last 5 Mouse Clicks")
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)
var jp = new JPanel(){ // The GUI object.
@Override protected void paintComponent(Graphics g){
super.paintComponent(g);
final Rectangle r = g.getClipBounds();
g.clearRect(r.x, r.y, r.width, r.height);
for (int i = 0; i < xs.size(); ++i){
g.fillOval(xs.get(i), ys.get(i), 10, 10);
}
}
}
jp.setPreferredSize(new Dimension(400, 400))
jf.add(jp)
jp.addMouseListener(new MouseAdapter(){
@Override public void mouseClicked(MouseEvent e){
System.out.println("Mouse click: (x, y) = ("
+ e.getX() + ", " + e.getY() + ")");
xs.add(e.getX());
ys.add(e.getY());
if (6 == xs.size()){
xs.remove(0);
ys.remove(0);
}
jp.repaint(); // Redraw the GUI object.
}
})
jf.pack()
jf.setVisible(true)
Let's do a third example.
Start a new JShell session and copy-and-paste this block of code into
the jshell prompt.
import java.awt.*
import javax.swing.*
import javax.swing.event.*
int sliderValue = 50
var jf = new JFrame("Simple GUI Example 3");
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.setLayout(new FlowLayout());
var js = new JSlider(JSlider.HORIZONTAL)
jf.add(js)
var sliderHandler = new ChangeListener(){
@Override public void stateChanged(ChangeEvent e){
final JSlider slider = (JSlider)e.getSource();
sliderValue = slider.getValue();
System.out.println("Slider value = " + sliderValue);
}
}
js.addChangeListener(sliderHandler)
jf.pack()
jf.setSize(300, 200)
jf.setVisible(true)
This program has a JSlider sitting in a JFrame. When you slide the
slider, its current value is printed to the console window. Notice that
this (default) slider has integer values between 0 and 100.
Exercise: Try using the your jshell prompt to add a second JSlider to
the JFrame but with the second one constructed using JSlider.VERTICAL.
Have the second slider share its event listener with the first slider.
We want to use the slider value to change the visual appearance of a
"GUI object". We will once again use a subclass of JPanel as our
"GUI object".
Let us make the slider move a dot back and forth across the window.
Copy-and-paste the following block of code into your jshell prompt.
var jp = new JPanel(){ // The GUI object.
@Override public void paintComponent(Graphics g){
super.paintComponent(g);
final Rectangle r = g.getClipBounds();
g.clearRect(r.x, r.y, r.width, r.height);
g.fillOval((int)((sliderValue/100.0) * r.width) - 10, (r.height/2) - 10, 20, 20);
}
};
jp.setPreferredSize(new Dimension(200, 100))
jf.add(jp)
var js = new JSlider(JSlider.HORIZONTAL)
jf.add(js)
var sliderHandler = new ChangeListener(){
@Override public void stateChanged(ChangeEvent e){
final JSlider slider = (JSlider)e.getSource();
sliderValue = slider.getValue();
System.out.println("Slider value = " + sliderValue);
jp.repaint(); // Redraw the GUI.
}
}
js.addChangeListener(sliderHandler)
The paintComponent() method first clears its graphics context. Then
it uses the slider value as a percentage to determine where across the
graphics context the dot should go. Vertically, the dot is placed in
the center of the graphics context.
Notice that we needed to redefine the event handler so that it calls
the repaint() method on the JPanel.
Here is what the complete program looks like. Look for the places in the code where each of the four kinds of objects from the above picture get instantiated, the component object that is the source of events, the event handler object, the event object, and the "GUI object".
import java.awt.*
import javax.swing.*
import javax.swing.event.*
int sliderValue = 50
var jf = new JFrame("Slider GUI Example")
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)
jf.setLayout(new FlowLayout())
var jp = new JPanel(){ // The GUI object.
@Override public void paintComponent(Graphics g){
super.paintComponent(g);
final Rectangle r = g.getClipBounds();
g.clearRect(r.x, r.y, r.width, r.height);
g.fillOval((int)((sliderValue/100.0) * r.width) - 10, (r.height/2) - 10, 20, 20);
}
};
jp.setPreferredSize(new Dimension(200, 100))
jf.add(jp)
var js = new JSlider(JSlider.HORIZONTAL)
jf.add(js)
var sliderHandler = new ChangeListener(){
@Override public void stateChanged(ChangeEvent e){
final JSlider slider = (JSlider)e.getSource();
sliderValue = slider.getValue();
System.out.println("Slider value = " + sliderValue);
jp.repaint(); // Redraw the GUI.
}
}
js.addChangeListener(sliderHandler)
jf.pack()
jf.setVisible(true)
Exercise: The disk moves horizontally across the middle of the GUI window. Modify the program so that the disk moves from the lower left-hand corner of the window to the upper right-hand corner as you move the slider to the right. When the disk is in a corner, you should only see one quarter of the disk.
Exercise: Add a second slider to the program and have that slider move the disk vertically.
When you look carefully at the last three examples, you will notice that they all have one other ingredient besides the four kinds of objects we talked about (event source object, event handler object, event object, and "GUI object). All three examples have important data that they keep track of and use for updating the "GUI object" (look at each example and find its data). This data is an important idea, and leads to the next topic, the Model-View-Controller design pattern.
9. Model-View-Controler (MVC) Design Pattern
Building event-driven GUIs that do interesting things is not easy. In this section we will discuss a software pattern that is meant to help with the design of complex event-driven GUIs.
As mentioned at the very end of the last section, event-driven GUI programs will always have data that needs to updated whenever an event occurs and which needs to be used to update the appearance of the GUI. We call that data the state of the GUI. Much of the complexity in building complicated GUIs comes from managing the state data.
In the last section we emphasized that an event-driven GUI program needed four kinds of objects. Now we will add a fifth object to our designs, a program state object.
Here is a picture showing the five kinds of objects. When an event handling method is called (by the JVM) to handle an event object, the handler should use the information in the event object to update the program state object. Notice that this means that there must be some kind of connection between the event handler object and the program state object. In addition, the "GUI object" should look to the program state object for all the information that it needs to repaint the GUI. This means that the "GUI object" also needs a way to communicate with the program state object.
GUI component object
+----------------------+ List<EventListener>
| | object
| List<EventListener>--|-------->+------+
| | | | EventListener object
| | +------+ +----------------------+
| addListener( ){...} | | -----|-------->| |
| | +------+ | handleEvent(Event e) |
+----------------------+ | | | { |
An object that initiates +------+ | ... |
events and receives Event | } |
objects from the JVM. | |
+----------------------+
An object that implements
Event object the EventListener interface
+-------------------+ and handles event objects
| Created by JVM | delivered by the JVM.
| using information |
| from the OS about | Program state object
| an event | +----------------------+
+-------------------+ | Information about | GUI object
| the current state | +------------------+
| of the application | | |
| program. This needs | | paintComponent() |
| to be updated when | | { |
| an event occurs. | | ... |
+----------------------+ | { |
| |
+------------------+
GUI object that needs
to be repainted after
an event occurs.
The Model-View-Controller (MVC) pattern takes the above objects and gives them new names and tries to formalize the role that they play in the GUI program.
In MVC, the program state object is called the Model. What we have been calling the "GUI object" is called the View. The event handler object, the event source component, and the event objects are all together called the Controller.
When programmers think about MVC, they think of the following picture which describes the roles played by these objects. The picture represents a user using a GUI program. The user sees the View. At some point the user interacts with some component and causes an event. The Controller handles the event and uses it to update information in the Model. Then the Controller sends a signal to the View to repaint itself. The View queries the Model to find out how it should update its appearance. The user see the resulting change in the appearance of the GUI and may then initiate another interaction with the Controller.
+-----------------+
| |
| Model |
| |
+-----------------+
4. View queries the / \
Model and then / \ 2. Controller updates
repaints itself. / \ the Model.
/ \
+-------------+ +----------------+
| | | |
| View |<-----------------| Controller |
| | 3. Controller | |
+-------------+ notifies +----------------+
\ the View. /
5. User sees \ / 1. User interacts
repainted \ / with the Controller.
View. \ / (The JVM sends an
+--------------+ Event object to the
| | Controller.)
| User |
| |
+--------------+
10. High-level, Low-level, and Focus Events
It is possible to imaging a GUI system that has only one kind of event, mouse events. Whenever the user clicked the mouse, your program would get an event telling it which pixel the user clicked on, and that's it. In such a system, your program would need to figure out, with every mouse click, what kind of component (if any) was under the mouse click, and then do the appropriate action. Such a GUI system would put a tremendous responsibility on every programmer who used it. It would be very tedious and error prone to program with. You would soon wish that the GUI system did some of the work for you and let you know not just where the mouse was clicked, but that the mouse was clicked over a button. Being told that the mouse was clicked "over a button" is called a "high-level event". Just being told where the mouse was clicked is called a "low-level event". All modern GUI systems have both kinds of events.
Let us reconsider the life of a mouse event, from mouse to OS to JVM to some component
When you click the mouse, the OS notices the mouse click "event" since the OS owns all the hardware devices.
The OS knows what pixel the mouse was clicked on. The OS keeps a data structure that lets it know which process owns the window at that pixel. The OS sends a message to that process telling it that there was a mouse click at the given pixel in its window.
In our case, the process that owns the clicked on pixel is the JVM. So the OS tells the JVM that one of its pixels was clicked on. The JVM translates the coordinates of the clicked on pixel from the OS's screen coordinates to the coordinates of the window owned by the JVM. The JVM keeps a data structure that lets it know which GUI component owns each pixel in its window and whether or not that component has an event listener attached to it. If so, the JVM sends a message (with an Event object) to the listener object attached to the GUI component that owns the pixel that the mouse was clicked on. To our Java program, that GUI component seems to be the source of the mouse event, not the mouse itself.
In general, the JVM receives from the OS what we refer to as "low-level events", like "mouse moved", or "mouse button pressed down", or "mouse button released". The JVM will translate these low level events into "higher-level events". For example, when the mouse moves into one of the JVM's windows, the JVM will translate the "mouse move" event into a "mouse entered" event. Similarly, when the mouse moves out of the JVM's window, the JVM creates a "mouse leave" event.
If the component that was clicked on is, say, a JButton, then our Java
program does not actually get a low-level "mouse clicked" event from the
JVM. Instead, the JVM translates that low-level MouseEvent into a
high-level ActionEvent. Most GUI components actually get high-level
events, like ActioEvent, ItemEvent, AdjustmentEvent, or TextEvent.
These high-level events convey more "meaning" to a program than low-level
events like "mouse clicked". However, the JPanel and JFrame containers
are components that only receive low-level events (or lower-level events
like "mouse entered").
When the JVM sends a message (with an Event object) to the clicked on component, the JVM doesn't actually send the message to the GUI component object. Instead, the JVM sends the message to an event listener object that is registered with the GUI component. This is a "separation of concerns" object-oriented-design choice. The GUI component's responsibility is "appearing" in certain way. We do not want to give the GUI component the added responsibility of "acting" in a certain way. We separate the "behavior" part of the GUI component into a separate object (the event listener) that has the responsibility of implementing the GUI component's actions. This design allows us to do things like make a button change its behavior as a program executes. Also, we can give a button multiple behaviors or we can add/remove behaviors from a button as a program executes. We often say that the GUI component's behavior has been "delegated" to the event listener object (this terminology is used a lot by Microsoft in the C# language).
When you click the mouse on a GUI component that has a registered
MouseListener, one of the methods in that MouseListener object gets
called by the JVM. Notice that the methods in the MouseListener object
are not called by any of our code. If you look at the code for an event
driven program, you will see many methods defined in the code that are
never called from within the code. We write these (event handling)
methods but we never call them. Normally, that is not how we write
software. We tend to write methods in our code that can be called by
other methods in our code. In an event driven GUI program we write
methods that will be called by the "GUI framework". The idea that we
write methods but we don't call them (they get called by someone else)
is referred to as "inversion of control" (IoC). When our GUI program is
running, it is the Java GUI framework that is really in control, not our
code. After our code has instantiated all the necessary objects described
above (in the MVC pattern) our code terminates and leaves the Java GUI
framework in control. (If you look at any of our GUI program, you will see
that the main() method returns after it has instantiated all the needed
objects, and yet, our program does not terminate). The Java GUI framework
waits for events (from the operating system) and then calls our event
listener methods as needed. When our event listener methods return, they
are returning to the Java GUI framework, not to any code that we wrote.
We say that control has been "inverted" from our program to the Java GUI
framework. Another term used to describe the methods in listener objects
is "callback functions". This is an older term used in the C language.
The idea is that we provide these functions to the GUI framework so that
it can "call back" on these functions as events happen.
As was mentioned in the last paragraph, when the main() method of a Java
GUI program returns, the program does not terminate. Instead, control shifts
to the Java GUI framework. We can be more specific about this shift of
control. All Java code runs in some thread. When you launch a Java program,
the JVM starts a thread to run your main() method. The thread that runs the
main() method is often called the "main thread". Unless you launch more
threads, the main thread will be your program's only thread. When main()
returns, the JVM terminates the main thread and then your program terminates.
But in a Java GUI program, as soon as you make some GUI component visible,
the JVM launches a new thread, the "Event Dispatch Thread" (EDT). This
thread is how the JVM waits for events from the operating system and it is
this thread that calls your event handling methods. So after the main()
method returns and terminates, the EDT continues to run and receive events
from the OS. So the "inversion of control" happens on the Event Dispatch
Thread.
10.1 Focus Events
Consider a GUI program that has a JFrame that contains a JPanel that
contains JButon. Suppose that the user's mouse passes over the JButton.
Which of these components should be sent the low-level MouseEvent? One
reasonable answer is "all of them". Another reasonable answer is "none of
them" (instead, the JButton should be sent a high-levelActionEventif
theJbutton` is clicked on). Questions like this are common in GUI systems
and they do not have fixed, uniform answers. The answers vary from system
to system.
A focus event represents a change in who receives low level events. When the focus switches from one window (or component) to another, low level events like mouse and keyboard events change from being delivered from the first window to the second window. Both the window that loses focus and the window that gains focus is sent a focus event object.