Event-Driven Programs
An Introduction with Examples in Java |
Prof. David Bernstein |
Computer Science Department |
bernstdh@jmu.edu |
Using an Event Queue
Event Listening
Event Bubbling
contentPane.setLayout(null);
label.setBounds(50,50,500,100); contentPane.add(label);
button.setBounds(450,300,100,50); contentPane.add(button);
JFrame
main()
import java.util.*; import javax.swing.*; /** * A simple example of a GUI application * * Note: This example contains a common mistake made by beginning * programmers. Specifically, it manipulates GUI elements * outside of the event dispatch thread * * @author Prof. David Bernstein, James Madison University * @version 1.0 */ public class BadRandomMessageApplication { // The pseudo-random number generator private static Random rng = new Random(); // The messages private static final String[] MESSAGES = { "What a great example!", "This class is great.", "I can't wait to do the programming assignments.", "I wish lectures lasted for 5 hours.", "I've never had a better Professor." }; /** * The entry point of the application * * @param args The command-line arguments */ public static void main(String[] args) throws Exception { JFrame window; JLabel label; JPanel contentPane; String s; // Select a message at random s = createRandomMessage(); // Construct the "window" window = new JFrame(); window.setSize(600,400); window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // Get the container for all content contentPane = (JPanel)window.getContentPane(); contentPane.setLayout(null); // Add a component to the container label = new JLabel(s, SwingConstants.CENTER); label.setBounds(50,50,500,100); contentPane.add(label); // Make the "window" visible window.setVisible(true); } /** * "Create" a message at random * * @return The message */ private static String createRandomMessage() { return MESSAGES[rng.nextInt(MESSAGES.length)]; } }
import java.util.*; import javax.swing.*; /** * A simple example of a GUI application * * @author Prof. David Bernstein, James Madison University * @version 1.0 */ public class BadRandomMessageSwingApplication implements Runnable { // Attributes private JLabel label; // The pseudo-random number generator private static Random rng = new Random(); // The messages private static final String[] MESSAGES = { "What a great example!", "This class is great.", "I can't wait to do the programming assignments.", "I wish lectures lasted for 5 hours.", "I've never had a better Professor." }; /** * The entry point of the application * * @param args The command-line arguments */ public static void main(String[] args) throws Exception { SwingUtilities.invokeAndWait( new BadRandomMessageSwingApplication()); } /** * "Create" a message at random * * @return The message */ private static String createRandomMessage() { return MESSAGES[rng.nextInt(MESSAGES.length)]; } /** * The code to be executed in the event dispatch thread * (required by Runnable) */ public void run() { JFrame window; JPanel contentPane; String s; // Select a message at random s = createRandomMessage(); // Construct the "window" window = new JFrame(); window.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); window.setSize(600,400); // Get the container for all content contentPane = (JPanel)window.getContentPane(); contentPane.setLayout(null); // Add a component to the container label = new JLabel(s, SwingConstants.CENTER); label.setBounds(50,50,500,100); contentPane.add(label); // Make the "window" visible window.setVisible(true); } }
import java.awt.event.*; import java.util.*; import javax.swing.*; /** * A simple example of a GUI application that responds * to events (in this case, events generated by a button) * * @author Prof. David Bernstein, James Madison University * @version 1.0 */ public class BadInteractiveRandomMessageSwingApplication implements ActionListener, Runnable { // Attributes private JLabel label; // The pseudo-random number generator private static Random rng = new Random(); // String "constants" private static final String CHANGE = "Change"; // The messages private static final String[] MESSAGES = { "What a great example!", "This class is great.", "I can't wait to do the programming assignments.", "I wish lectures lasted for 5 hours.", "I've never had a better Professor." }; /** * The entry point of the application * * @param args The command-line arguments */ public static void main(String[] args) throws Exception { SwingUtilities.invokeAndWait( new BadInteractiveRandomMessageSwingApplication()); } /** * Handle actionPerformed messages * (required by ActionListener) * * @param event The ActionEvent that generated the message */ public void actionPerformed(ActionEvent event) { String actionCommand; actionCommand = event.getActionCommand(); if (actionCommand.equals(CHANGE)) { label.setText(createRandomMessage()); } } /** * "Create" a message at random * * @return The message */ private static String createRandomMessage() { return MESSAGES[rng.nextInt(MESSAGES.length)]; } /** * The code to be executed in the event dispatch thread * (required by Runnable) */ public void run() { JButton button; JFrame window; JPanel contentPane; String s; // Select a message at random s = createRandomMessage(); // Construct the "window" window = new JFrame(); window.setSize(600,400); window.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); // Get the container for all content contentPane = (JPanel)window.getContentPane(); contentPane.setLayout(null); // Add the message component to the container label = new JLabel(s, SwingConstants.CENTER); label.setBounds(50,50,500,100); contentPane.add(label); // Add the button to the container button = new JButton(CHANGE); button.setBounds(450,300,100,50); contentPane.add(button); button.addActionListener(this); // Make the "window" visible window.setVisible(true); } }
import java.util.*; import javax.swing.*; /** * A simple example of a GUI JApplet * * Note: This example contains a subtle mistake. Specifically, it * manipulates GUI elements in the init() method which is * NOT called in the event dispatch thread. * * @author Prof. David Bernstein, James Madison University * @version 1.0 */ public class BadRandomMessageJApplet extends JApplet { // Attributes private JLabel label; // The pseudo-random number generator private static Random rng = new Random(); // The messages private static final String[] MESSAGES = { "What a great example!", "This class is great.", "I can't wait to do the programming assignments.", "I wish lectures lasted for 5 hours.", "I've never had a better Professor." }; /** * Default Constructor */ public BadRandomMessageJApplet() { super(); } /** * "Create" a message at random * * @return The message */ private static String createRandomMessage() { return MESSAGES[rng.nextInt(MESSAGES.length)]; } /** * Called to indicate that this JApplet has been loaded */ public void init() { JPanel contentPane; String s; // Select a message at random s = createRandomMessage(); // Get the container for all content contentPane = (JPanel)getContentPane(); contentPane.setLayout(null); // Add a component to the container label = new JLabel(s, SwingConstants.CENTER); label.setBounds(50,50,500,100); contentPane.add(label); } }
import java.awt.event.*; import java.util.*; import javax.swing.*; /** * A simple example of a GUI JApplet that responds * to events (in this case, events generated by a button) * * Note: This example contains a subtle mistake. Specifically, it * manipulates GUI elements in the init() method which is * NOT called in the event dispatch thread. * * @author Prof. David Bernstein, James Madison University * @version 1.0 */ public class BadInteractiveRandomMessageJApplet extends JApplet implements ActionListener { // Attributes private JLabel label; // The pseudo-random number generator private static Random rng = new Random(); // String "constants" private static final String CHANGE = "Change"; // The messages private static final String[] MESSAGES = { "What a great example!", "This class is great.", "I can't wait to do the programming assignments.", "I wish lectures lasted for 5 hours.", "I've never had a better Professor." }; /** * Default Constructor */ public BadInteractiveRandomMessageJApplet() { super(); } /** * Handle actionPerformed messages * (required by ActionListener) * * @param event The ActionEvent that generated the message */ public void actionPerformed(ActionEvent event) { String actionCommand; actionCommand = event.getActionCommand(); if (actionCommand.equals(CHANGE)) { label.setText(createRandomMessage()); } } /** * "Create" a message at random * * @return The message */ private static String createRandomMessage() { return MESSAGES[rng.nextInt(MESSAGES.length)]; } /** * Called to indicate that this JApplet has been loaded */ public void init() { JButton button; JPanel contentPane; String s; // Select a message at random s = createRandomMessage(); // Get the container for all content contentPane = (JPanel)getContentPane(); contentPane.setLayout(null); // Add a component to the container label = new JLabel(s, SwingConstants.CENTER); label.setBounds(50,50,500,100); contentPane.add(label); // Add the button to the container button = new JButton(CHANGE); button.setBounds(450,300,100,50); button.addActionListener(this); contentPane.add(button); } }
JApplication
class that mimics the programming interface and
life-cycle of the JApplet
class
JApplication
Class/** * The actual entry point into this JApplication * (required by Runnable) * * This method is called in the event dispatch thread. */ public final void run() { constructMainWindow(); init(); mainWindow.setVisible(true); }
mainWindow = new JFrame(); mainWindow.setTitle("James Madison University"); mainWindow.setResizable(false); contentPane = (JPanel)mainWindow.getContentPane(); contentPane.setLayout(null); contentPane.setDoubleBuffered(false);
/** * This method is called just before the main window * is first made visible */ public abstract void init();
JApplication
import java.util.*; import javax.swing.*; import app.JApplication; public class BadRandomMessageJApplication extends JApplication { // Attributes private JLabel label; // The pseudo-random number generator private static Random rng = new Random(); // The messages private static final String[] MESSAGES = { "What a great example!", "This class is great.", "I can't wait to do the programming assignments.", "I wish lectures lasted for 5 hours.", "I've never had a better Professor." }; /** * The entry point of the application * * @param args The command-line arguments */ public static void main(String[] args) throws Exception { SwingUtilities.invokeAndWait( new BadRandomMessageJApplication(600,400)); } /** * Explicit Value Constructor */ public BadRandomMessageJApplication(int width, int height) { super(width, height); } /** * "Create" a message at random * * @return The message */ private static String createRandomMessage() { return MESSAGES[rng.nextInt(MESSAGES.length)]; } /** * Called to indicate that this JApplication has been loaded */ public void init() { JPanel contentPane; String s; // Select a message at random s = createRandomMessage(); // Get the container for all content contentPane = (JPanel)getContentPane(); contentPane.setLayout(null); // Add a component to the container label = new JLabel(s,SwingConstants.CENTER); label.setBounds(50,50,500,100); contentPane.add(label); } }
JApplet
and JApplication
with the same functionality they will have an enormous
amount of duplicate codeRootPaneContainer
for a JApplet
has access to the start-up parameters whereas the
RootPaneContainer
for a JApplication
(which is a JFrame
) does not.
package app; import javax.swing.*; /** * Requirements of a RootPaneContainer in a MultimediaApp * * @author Prof. David Bernstein, James Madison University * @version 1.0 */ public interface MultimediaRootPaneContainer extends RootPaneContainer { /** * Returns the value of the "named" parameter * * @param name The name/index of the parameter */ public abstract String getParameter(String name); }
Correcting the Remaining Problems with the Decorator Pattern
package app; import javax.swing.*; /** * The requirements of a multimedia Applet/application * * @author Prof. David Bernstein, James Madison University * @version 1.0 */ public interface MultimediaApp { /** * Called to indicate that this MultimediaApp should destroy any * resources that it has allocated */ public abstract void destroy(); /** * Called to indicate that this MultimediaApp has been loaded */ public abstract void init(); /** * Set the MultimediaRootPaneContainer for the MultimediaApp * * In most cases, the MultimediaRootPaneContainer will be either * a MultimediaApplication or a MultimediaApplet * * @param container The RootPaneContainer for this MultimediaApp */ public abstract void setMultimediaRootPaneContainer( MultimediaRootPaneContainer container); /** * Called to indicate that this MultimediaApp has been started */ public abstract void start(); /** * Called to indicate that this MultimediaApp has been stopped */ public abstract void stop(); }
MultimediaApplet
Classpackage app; import java.awt.*; import javax.swing.*; /** * A MultimediaApplet is a JApplet that delegates all calls * to "transition" methods to a MultimediaApp. The calls * to the MultimediaApp object's "transition" methods will * be made in the event dispatch thread. * * Note: The browser/appletviewer is not supposed to call the * "transition" methods in the event dispatch thread. This class * checks to make sure [since SwingUtilities.invokeAndWait() must not * be called in the event dispatch thread]. While there is a small * performance penalty incurred as a result, the "transition" methods * are not called frequently enough for this to matter. * * @author Prof. David Bernstein, James Madison University * @version 1.0 */ public abstract class MultimediaApplet extends JApplet implements MultimediaRootPaneContainer { private MultimediaApp app; /** * Explicit Value Constructor * * @param app The MultimediaApp to delegate to */ public MultimediaApplet(MultimediaApp app) { super(); this.app = app; setLayout(null); app.setMultimediaRootPaneContainer(this); } /** * Called to indicate that this Applet should destroy any * resources that it has allocated */ public void destroy() { if (SwingUtilities.isEventDispatchThread()) app.destroy(); else { try {SwingUtilities.invokeAndWait(new DestroyRunnable());} catch (Exception e) {} } } /** * Get a reference to the MultimediaApp that is * being delegated to * * @return The MultimediaApp */ protected MultimediaApp getMultimediaApp() { return app; } /** * Called to indicate that this Applet has been * loaded */ public void init() { if (SwingUtilities.isEventDispatchThread()) app.init(); else { try {SwingUtilities.invokeAndWait(new InitRunnable());} catch (Exception e) {} } } /** * Called to indicate that this Applet has been * started */ public void start() { if (SwingUtilities.isEventDispatchThread()) app.start(); else { try {SwingUtilities.invokeAndWait(new StartRunnable());} catch (Exception e) {} } } /** * Called to indicate that this Applet has been * stopped */ public void stop() { if (SwingUtilities.isEventDispatchThread()) app.stop(); else { try {SwingUtilities.invokeAndWait(new StopRunnable());} catch (Exception e) {} } } // --Inner Classes-- /** * A Runnable that delegates to the MultimediaApp object's * destroy() method */ private class DestroyRunnable implements Runnable { public void run() { app.destroy(); } } /** * A Runnable that delegates to the MultimediaApp object's * init() method */ private class InitRunnable implements Runnable { public void run() { app.init(); } } /** * A Runnable that delegates to the MultimediaApp object's * start() method */ private class StartRunnable implements Runnable { public void run() { app.start(); } } /** * A Runnable that delegates to the MultimediaApp object's * stop() method */ private class StopRunnable implements Runnable { public void run() { app.stop(); } } }
package app; import java.util.*; import javax.swing.*; /** * A simple example of a MultimediaApp * * @author Prof. David Bernstein, James Madison University * @version 1.0 */ public class RandomMessageApp extends AbstractMultimediaApp { // Attributes private JLabel label; // The pseudo-random number generator private static Random rng = new Random(); // The messages private static final String[] MESSAGES = { "What a great example!", "This class is great.", "I can't wait to do the programming assignments.", "I wish lectures lasted for 5 hours.", "I've never had a better Professor." }; /** * "Create" a message at random * * @return The message */ private static String createRandomMessage() { return MESSAGES[rng.nextInt(MESSAGES.length)]; } /** * Called to indicate that this MultimediaApp has been loaded */ public void init() { JPanel contentPane; String s; // Select a message at random s = createRandomMessage(); // Get the container for all content contentPane = (JPanel)rootPaneContainer.getContentPane(); contentPane.setLayout(null); // Add a component to the container label = new JLabel(s, SwingConstants.CENTER); label.setBounds(50,50,500,100); contentPane.add(label); } }
package app; /** * A simple example of a MultimediaApplet * * @author Prof. David Bernstein, James Madison University * @version 1.0 */ public class RandomMessageMultimediaApplet extends MultimediaApplet { /** * Default Constructor */ public RandomMessageMultimediaApplet() { super(new RandomMessageApp()); } }
package app; import java.util.*; import javax.swing.*; /** * A simple example of a MultimediaApplication * * @author Prof. David Bernstein, James Madison University * @version 1.0 */ public class RandomMessageMultimediaApplication extends MultimediaApplication { /** * The entry-point of the application * * @param args The command-line arguments */ public static void main(String[] args) throws Exception { SwingUtilities.invokeAndWait( new RandomMessageMultimediaApplication(args, 600, 400)); } /** * Explicit Value Constructor * * @param args The command-line aguments * @param width The width of the content (in pixels) * @param height The height of the content (in pixels) */ public RandomMessageMultimediaApplication(String[] args, int width, int height) { super(args, new RandomMessageApp(), width, height); } }
mainWindow.setDefaultCloseOperation( JFrame.DO_NOTHING_ON_CLOSE); mainWindow.addWindowListener(this); }
/** * Handle windowOpened messages -- the first time the window is * made visible (required by WindowListener) * * @param event The WindowEvent that generated the message */ public void windowOpened(WindowEvent event) { resize(); start(); }
/** * Handle windowDeiconified messages -- when the window is maximized * (required by WindowListener) * * @param event The WindowEvent that generated the message */ public void windowDeiconified(WindowEvent event) { start(); }
/** * Handle windowIconified messages -- when the window is minimized * (required by WindowListener) * * @param event The WindowEvent that generated the message */ public void windowIconified(WindowEvent event) { stop(); }
/** * Handle windowClosing messages * (required by WindowListener) * * @param event The WindowEvent that generated the message */ public void windowClosing(WindowEvent event) { exit(); }
/** * Exist this JApplication (after prompting the user * for a verification) */ private void exit() { int response; response = JOptionPane.showConfirmDialog(mainWindow, "Exit this application?", "Exit?", JOptionPane.YES_NO_OPTION); if (response == JOptionPane.YES_OPTION) { mainWindow.setVisible(false); stop(); mainWindow.dispose(); } }
/** * Handle windowClosed messages -- when the window is disposed * (required by WindowListener) * * @param event The WindowEvent that generated the message */ public void windowClosed(WindowEvent event) { destroy(); System.exit(0); }
The Final Design
package io; import java.io.*; import java.net.*; import java.util.*; /** * A ResourceFinder is used to find a "resource" either in * the .jar file (containing the ResourceFinder class) * or in the local file system * * @author Prof. David Bernstein, James Madison University * @version 1.0 */ public class ResourceFinder { private static final ResourceFinder instance = new ResourceFinder(); /** * Create an instance of a ResourceFinder * * @return The instance */ public static ResourceFinder createInstance() { return instance; } /** * Find a resource * * @return The InputStream of the resource (or null) */ public InputStream findInputStream(String name) { Class c; InputStream is; // Get the Class for this class c = this.getClass(); // Get a URL for the resource is = c.getResourceAsStream(name); return is; } /** * Find a resource * * @return The URL of the resource (or null) */ public URL findURL(String name) { Class c; URL url; // Get the Class for this class c = this.getClass(); // Get a URL for the resource url = c.getResource(name); return url; } /** * Load a list of resource names from a list (e.g., file) * * Note: This method does not return an array of InputStream * objects to conserver resources. * * @param listName The name of the list * @return The resource names */ public String[] loadResourceNames(String listName) { ArrayList<String> buffer; BufferedReader in; InputStream is; String line; String[] names; names = null; is = findInputStream(listName); if (is != null) { try { in = new BufferedReader(new InputStreamReader(is)); buffer = new ArrayList<String>(); while ((line=in.readLine()) != null) { buffer.add(line); } names = new String[buffer.size()]; buffer.toArray(names); } catch (IOException ioe) { // Can't read the list } } return names; } }
Design of a Simple Metronome
package event; /** * The requirements of an object that listens to a Metronome * * @author Prof. David Bernstein, James Madison University * @version 1.0 */ public interface MetronomeListener { /** * Handle a Metronome tick * * @param millis The number of milliseconds since the Metronome started */ public abstract void handleTick(int millis); }
/** * Add a MetronomeListener * * @param ml The MetronomeListener to add */ public synchronized void addListener(MetronomeListener ml) { listeners.add(ml); copyListeners(); } /** * Copy the collection of listeners * * The copy is used in the event dispatch thread to avoid * concurrent modification problems. * * This method is called infrequently (and, in general, * before events are dispatched) so it does not cause * any real efficiency problems. */ private void copyListeners() { copy = new MetronomeListener[listeners.size()]; listeners.toArray(copy); } /** * Remove a MetronomeListener * * @param ml The MetronomeListener to remove */ public synchronized void removeListener(MetronomeListener ml) { listeners.remove(ml); copyListeners(); }
/** * Notify observers in the GUI/event-dispatch thread. * * Note: Listeners are notified in the REVERSE order * in which they are added. */ private synchronized void notifyListeners() { // Setup the state of the MetronomeTickDispatcher dispatcher.setup(copy, time); // Cause the run() method of the dispatcher to be // called in the GUI/event-dispatch thread EventQueue.invokeLater(dispatcher); }
/** * A MetronomeTickDispatcher is used by a Metronome to * inform listeners of a tick. * * This class implements Runnable because its run() * method will be called in the GUI/event-dispatch thread. */ private class MetronomeTickDispatcher implements Runnable { private MetronomeListener[] listeners; private int time; /** * Code to be executed in the event-dispatch thread * (required by Runnable) * * Specifically, notify the listeners */ public void run() { int n; n = listeners.length; for (int i=n-1; i>=0; i--) { if (listeners[i] != null) listeners[i].handleTick(time); } } /** * Setup this MetronomeTickDispatcher for the next * dispatch * * @param listeners The collection of MetronomeListener objects * @param time The (relative) time of the tick */ public void setup(MetronomeListener[] listeners, int time) { this.listeners = listeners; this.time = time; } }
/** * Start this Timer */ public void start() { if (timerThread == null) { keepRunning = true; timerThread = new Thread(this); timerThread.start(); } }
/** * The code that is executed in the timer thread * (required by Runnable) */ public void run() { int currentDelay; long currentTick, drift; currentDelay = delay; if (adjusting) lastTick = System.currentTimeMillis(); while (keepRunning) { try { timerThread.sleep(currentDelay); time += currentDelay; if (adjusting) { // Compensate for any drift currentTick = System.currentTimeMillis(); drift = (currentTick - lastTick) - currentDelay; currentDelay = (int)Math.max(0, delay-drift); lastTick = currentTick; } notifyListeners(); } catch (InterruptedException ie) { // stop() was called } } timerThread = null; }
package app; import java.util.*; import java.awt.event.*; import javax.swing.*; import app.AbstractMultimediaApp; import event.*; /** * A simple MultimediaApp that uses a Metronome * * @author Prof. David Bernstein, James Madison University * @version 1.0 */ public class StopWatchApp extends AbstractMultimediaApp implements ActionListener, MetronomeListener { private boolean running; private JLabel label; private Metronome metronome; private static final String START = "Start"; private static final String STOP = "Stop"; /** * Handle actionPerformed messages * (required by ActionListener) * * @param event The ActionEvent that generated the message */ public void actionPerformed(ActionEvent event) { String actionCommand; actionCommand = event.getActionCommand(); if (actionCommand.equals(START)) { label.setText("0"); metronome.reset(); metronome.start(); running = true; } else if (actionCommand.equals(STOP)) { metronome.stop(); running = false; } } /** * Respond to handleTick messages * (required by MetronomeListener) * * @param millis The time */ public void handleTick(int millis) { label.setText(""+millis/1000); } /** * Called to indicate that this MultimediaApp has been loaded */ public void init() { JButton start, stop; JPanel contentPane; running = false; contentPane = (JPanel)rootPaneContainer.getContentPane(); contentPane.setLayout(null); label = new JLabel("0"); label.setBounds(250,100,100,100); contentPane.add(label); start = new JButton(START); start.setBounds(50,300,100,50); start.addActionListener(this); contentPane.add(start); stop = new JButton(STOP); stop.setBounds(450,300,100,50); stop.addActionListener(this); contentPane.add(stop); metronome = new Metronome(1000, true); metronome.addListener(this); } /** * Called to indicate that this MultimediaApp should start * (required by MultimediaApp) */ public void start() { if (running) metronome.start(); } /** * Called to indicate that this MultimediaApp should stop * (required by MultimediaApp) */ public void stop() { if (running) metronome.stop(); } }