Swing must be the easiest framework ever devised for building complicated graphical user interfaces. Swing still requires an understanding of certain complexities common to all event-driven systems.
Half a dozen times now I have seen programmers create a graphical component with unreasonably sluggish behavior. All proved to be variations of the same problem, with the same solution.
A user resized or scrolled a window, fiddled with knobs, or did something that caused a graphical view to be redrawn over and over again. Redraw events were generated faster than the redraw could execute, so the redraw lagged behind the user's activity. These views always contained custom graphical content, not standard Swing widgets. Redraws were handled by the programmer, by filling polygons, calculating rasters, and so on. A typical first implementation redraws the entire view from scratch each time.
Essentially this is a real-time programming problem, where events that arrive too late must be ignored. Swing cannot safely ignore events as a default policy. Only the programmer knows how events affect state. Only the programmer can anticipate how much time redraws might take. (X-Windows must also handle this problem explicitly. I have seen X-based software deliberately change redraw policies on machines with different performance.)
One solution is for the programmer's redraw event listener to block further redraws until the current one finishes. After completion, only the latest redraw is executed and the rest are discarded. A further refinement is for every redraw to pause for so many milliseconds. If another redraw event arrives right away, the first can be ignored. If not, the redraw continues. (See sample code below.)
Another solution is for the programmer to draw to a large off-screen buffer, so that the redraw event listener just copies part of a bitmap. Copying is fast, but not all redraws can be done this way (say 3D rotation). Swing does use default double buffering to avoid flashing images as they draw. But the default buffering cannot help with changes in image dimensions.
When you create a new graphical component,
extend a new class from JPanel
, not from
Canvas
. Override
paintComponent(Graphics g)
not
paint(Graphics g)
. The default
paint(Graphics g)
implemented in
JPanel
will perform double buffering and
call your paintComponent(Graphics g)
when
necessary. The first line of your
paintComponent(Graphics g)
should call
super.paintComponent(g)
so that JPanel
can repaint the background color. You can
then cast g
to a Graphic2D
. More
details are available from
http://java.sun.com/products/jfc/tsc/articles/painting/index.html
Try JComponent.setDebugGraphicsOptions(int
debugOptions)
to learn more about how your
component is drawn.
If you are uncertain where your events are
coming from, try
java.awt.Toolkit.getDefaultToolkit().addAWTEventListener
(AWTEventListener listener, long
eventMask)
or override
Component.processEvent(AWTEvent)
, and
delegate to super.processEvent(AWTEvent)
.
Intermediate users sometimes think they have discovered an unreasonable number of bugs. Components freeze or do not refresh properly. Events seem to be lost. A documented method appears to have no effect. Workarounds only make the problems less likely to occur. Most likely, these programmers have created multiple threads without proper attention to the "Swing event-dispatching thread."
I first recommend Appendix B "Swing Components and Multi-threading" of Kim Topley's "Core Java Foundation Classes" (Prentice Hall). The Swing development team also explains this issue clearly at http://java.sun.com/products/jfc/tsc/articles/threads/threads1.html :
Once a Swing component has been realized, all code that might affect or depend on the state of that component should be executed in the event-dispatching thread.
A component is "realized" as soon as event listeners may be called.
The programmer usually constructs and assembles GUI components in a single thread. Action listeners (callbacks or event handlers) are always called by Swing in the separate event-dispatching thread (EDT). The EDT is like an X-event loop where all GUI activity maintains a well-determined order. Only one GUI event can occur at a time, without asynchronous overlap.
Sometimes a GUI event initiates
time-consuming work. If an event listener
does not return, then all GUI components will
freeze, block further events, and not refresh
when uncovered. Time-consuming listeners
should spawn a new worker thread and return
quickly. When done, the worker thread may
need to update the GUI again. To reenter the
EDT, the worker must instantiate a
Runnable
object, then call
SwingUtilities.invokeLater()
or
SwingUtilities.invokeAndWait().
Be careful after adding event-listeners. If your initialization code changes selections in a table or combo-box, then listeners will be called. Add event listeners as late as possible to avoid confusion.
You may have a method that can be called from
inside or outside the EDT. Check
SwingUtilities.isDispatchThread()
before
changing the GUI from this method.
See more relevant articles at http://java.sun.com/products/jfc/tsc/articles/index.html .
Here are convenience methods to implement strategies in the previous sections.
public class RealTimeSwing { /** Run this Runnable in the Swing Event Dispatching Thread, and return when done with execution. This method can be called whether or not the current thread is in the Swing thread. @param runnable This is the code to be executed in the Swing thread. */ public static void invokeNow(Runnable runnable) { if (runnable == null) return; try { if (SwingUtilities.isEventDispatchThread()) {runnable.run();} else {SwingUtilities.invokeAndWait(runnable);} } catch (InterruptedException ie) { LOG.warning("Swing thread interrupted"); Thread.currentThread().interrupt(); } catch (java.lang.reflect.InvocationTargetException ite) { ite.printStackTrace(); throw new IllegalStateException (ite.getMessage()); } } /** Use this method for handling events in realtime, when the events may be generated more quickly than the the handler can complete. Call this method from the appropriate Listener. Executes Runnables inside and outside the Swing Thread. Returns immediately. If this method is called again with the same id after less than the specified pause, then the first call will be ignored. The most recent call with a given id will not be allowed to start until previous call finishes. When previous call finishes, only the most recent call with same id will run. Others will be discarded. @param id This string uniquely identifies this task. If this method is called again with the same id within the specified number of milliseconds, then the first call will not be executed. If this id is already executing, then it will wait until either the previous execution finishes, or until a later call with the same id. @param milliseconds Wait this long in a separate thread before executing Runnables. You can safely set this to zero. Set the time less than the expected time to execute the Runnables. If you set to 0, then a series of calls will be executed at least twice. If you set to greater than 0, then the first call may be ignored if followed quickly by another call. @param worker This Runnable will be executed first outside the Swing thread. Set to null to skip this step. @param refresher This Runnable will be executed inside the Swing thread after the worker has completed. Activity that uses or changes the state of Swing widgets should be included here. Set to null to skip this step. */ public static void invokeOnce(String id, final long milliseconds, final Runnable worker, final Runnable refresher) { synchronized (s_timestamps) { if (!s_timestamps.containsKey(id)) { // call once for each id s_timestamps.put(id,new Latest()); } } final Latest latest = s_timestamps.get(id); final long time = System.currentTimeMillis(); latest.time = time; (new Thread("Invoke once "+id) {public void run() { if (milliseconds > 0) { try {Thread.sleep(milliseconds);} catch (InterruptedException e) {return;} } synchronized (latest.running) { // can't start until previous finishes if (latest.time != time) return; // only most recent gets to run if (worker != null) worker.run(); // outside Swing thread if (refresher != null) invokeNow(refresher); // inside Swing thread } }}).start(); } private static Maps_timestamps = new HashMap (); private static class Latest { /** Last time for this event */ public volatile long time=0; /** for synchronization */ public final Object running = new Object(); } } /* Copyright (c) Bill Harlan, 1999 All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - Neither the names of contributors, nor the names of their employers may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
Bill Harlan, 1999
Return to parent directory.