Subpages: 1. JComponent properties, size, and positioning
2. Event handling and dispatching
5. AppContext service
6. Inside Timers & the TimerQueue
7. JavaBeans architecture
8. Fonts, Colors, Graphics and text
9. Using the Graphics clipping area
10. Graphics debugging
11. Painting and validation
12. Focus Management
13. Keyboard input, KeyStrokes, and Actions
2.11 Painting and validation
At the heart of JComponent's painting and validation mechanism lies a service class called RepaintManager. It is the RepaintManager that is responsible for sending painting and validation requests to the AWT system event queue for dispatching. To summarize, it does this by intercepting repaint() and revalidate() requests, coalescing any requests where possible, wrapping them in Runnable objects, and sending them to invokeLater(). There are a few issues we have encountered in this chapter that deserve more attention here before we actually discuss details of the painting and validation processes.
2.11.1 Double buffering
We've mentioned double-buffering, how to disable it in the RepaintManager, and how to specify the double-buffering of individual components with JComponent's setDoubleBuffered() method. But how does it work?
Double buffering is the technique of painting into an off-screen image rather than painting directly to a visible component. In the end, the resulting image is painted to the screen (which occurs relatively quickly). Using awt components, developers were required to implement their own double-buffering to reduce flashing. It was clear that double-buffering should be a built-in feature because of its widespread use. Thus, it is not much of a surprise to find this feature in all Swing components.
Behind the scenes, double-buffering consists of creating an Image and retrieving its Graphics object to use in all painting methods. If the component being repainted has children, this Graphics object will be passed down to them to use for painting, and so on. So if we are using double-buffering for a component, all its children will also be using double-buffering (whether or not they have double-buffering enabled or not) because they will be rendering into the same Graphics object. Note that there is only one off-screen image per RepaintManager, and there is normally only one RepaintManager instance per applet or application (RepaintManager is a service class that registers a shared instance of itself with AppContext--see section 2.5).
As we discuss in chapter 3, JRootPane is the top-level Swing component in any window (this includes JInternalFrame -- although it isn't really a window). By enabling double-buffering on JRootPane, all of its children will also be painted using double-buffering. As we saw in the last section, RepaintManager also provides global control over all component double-buffering. So another way to guarantee that all components will use double-buffering is to call:
2.11.2 Optimized drawing
We didn't really discuss the fact that components can overlap each other in Swing yet, but they can. JLayeredPane, for example, is a container allowing any number of components to overlap each other. Repainting such a container is much more complex than repainting a container we know does not allow overlapping, mainly because of the ability for components to be transparent.
What does it mean for a component to be transparent? Technically this means its isOpaque() method returns false. We can set this property by calling setOpaque(). What opacity means, in this context, is that a component will paint every pixel within its bounds. If this is set to false, we are not guaranteed that this will happen. This is generally set to true, but we will see that when it is set to false it increases the workload of the whole painting mechanism. Unless we are constructing a component that must not fill its entire rectangular region (as we will do in chapter 5 with custom polygonal buttons), we should always leave this set to true, as it is by default for most components. (This value is normally set by a component's UI delegate.)
JComponent's isOptimizedDrawingEnabled() method is overriden to return true for almost all JComponent subclasses except: JLayeredPane, JViewport, and JDesktopPane (a subclass of JLayeredpane). Basically, calling this method is equivalent to asking a component: is it possible that any of your child components can overlap each other? If it is then there is a lot more repainting work to do to take into account that fact that any number of components, from virtually anywhere in our container hierarchy, can overlap each other. Additionaly, since components can be transparent, components layered completely behind others may still show through. Such components are not necessarily siblings (i.e. in the same container) because we could conceivably have several non-opaque containers layered one on top of another. In situations like this, we must do a whole lot of 'tree walking' to figure out which components need to be refreshed. If isOptimizedDrawingEnabled() is overriden to return true, then we assume we do not have to consider any situations like this. Thus, painting becomes more efficient, or 'optimized.'
2.11.3 Root validation
A revalidate() request is generated when a component needs to be laid out again. When a request is received from a certail component there must be some way of determining whether laying that component out will affect anything else/ JComponent's isValidateRoot() method returns false for most components. Basically, calling this method is equivalent to asking: if I lay your contents out again, can you guarantee that none of your parents or siblings will be adversely affected (i.e. need to be laid out again)? By default only JRootPane, JScrollPane, and JTextField return true. This seems surprizing at first, but apparently it is true that these components are the only Swing components whose contents can be successfully laid out, in any situation (assuming no heavyweight components), without affecting parents or siblings. No matter how big we make anything inside a JRootPane, JScrollPane, or JTextField, they will not change size or location unless some outside influence comes into play (i.e. a sibling or parent). To help convince you of this, try adding a multi-line text component to a container without placing it in a scroll pane. You may notice that creating new lines will change its size (depending on the layout). The point is not that it rarely happens or that it can be prevented, but that it can happen. This is the type of thing that isValidateRoot() is supposed to warn us about. So where is this method used?
A component or its parent is normally revalidated when a property value changes and that component's size, location, or internal layout has been affected. By recursively calling isValidateRoot() on a Swing component's parent until we obtain true, we will end with the closest ancestor of that component that guarantees us its validation will not affect its siblings or parents. We will see that RepaintManager relies on this method for dispatching validation requests.
As we know there is usually only one instance of a service class in use per applet or application. So unless we specifically create our own instance of RepaintManager, which we will almost never need to do, all repainting is managed by the shared instance which is registered with AppContext. We normally retreive it using RepaintManager's static currentManager() method:
myRepaintManager = RepaintManager.currentManager(null);
This method takes a Component as its parameter. However, it doesn't matter what we pass it. In fact the component passed to this method is not used anywhere inside the method at all (see the RepaintManager.java source code), so a value of null can safely be used here. (This definition exists for sub-classes to use if they want to work with more than one RepaintManager, possibly on a per-component basis.)
RepaintManager exists for two purposes: to provide efficient revalidation and repainting. It intercepts all repaint() and revalidate() requests. This class also handles all double-buffering in Swing and maintains a single Image used for this purpose. This Image's maximum size is, by default, the size of the screen. However, we can set its size manually using RepaintManager's setDoubleBufferMaximumSize() method. (All other RepaintManager functionality will be discussed throughout this section where applicable.)
RepaintManager maintains a Vector of components that need to be validated. Whenever a revalidate request is intercepted, the source component is sent to the addInvalidComponent() method and its "validateRoot" property is checked using isValidateRoot(). This occurs recursively on that component's parent until isValidateRoot() returns true. The resulting component, if any, is then checked for visibility. If any one of its parent containers is not visible there is no reason to validate it. Otherwise RepaintManager 'walks down its tree' until it reaches the 'root' component, a Window or Applet. RepaintManager then checks the invalid components Vector and if the component isn't already there it is added. After being successfully added, RepaintManager then passes the 'root' to the SystemEventQueueUtilities' queueComponentWorkRequest() method (we saw this class in section 2.3). This method checks to see if there is a ComponentWorkRequest (this is a private static class in SystemEventQueueUtilities that implements Runnable) corresponding to that 'root' already stored in the work requests table. If there isn't one, a new one is created. If there is one already, we just grab a reference to it. Then we synchronize access to that ComponentWorkRequest, place it in the work requests table if it is a new one, and check if it is pending (i.e. it has been added to the AWT system event queue). If it isn't pending, we send it to SwingUtilities.invokeLater(). It is then marked as pending and we leave the synchronized block. When it is finally run from the event-dispatching thread it notifies RepaintManager to execute validateInvalidComponents(), followed by paintDirtyRegions().
The validateInvalidComponents() method basically checks RepaintManager's Vector containing the components in need of validation, and calls validate() on each one. (This method is actually a bit more careful than we describe here, as it synchronizes access to prevent the addition of invalid components while executing).
The paintDirtyRegions() method is much more complicated, and we'll discuss some of its details below. For now, it suffices to say that this method paints all damaged regions of each component maintained by RepaintManager.
JComponent defines two repaint() methods, and the no-argument version of repaint() is inherited from java.awt.Container:
public void repaint(long tm, int x, int y, int width, int height)
public void repaint(Rectangle r)
public repaint() // inherited from java.awt.Container
If you call the no-argument version the whole component is repainted. For small, simple components this is fine. But for larger, more complex components this is certainly not efficient. The other two methods take the bounding region to be repainted (the dirtied region) as parameters. The first method's int parameters correspond to the x-coordinate, y-coordinate, width, and height of that region. The second method takes the same information encapsulated in a Rectange instance. The second repaint() method shown above just sends its traffic to the first. The first method sends the dirtied region's parameters to RepaintManager's addDirtyRegion() method.
RepaintManager maintains a Hashtable of dirty regions. Each component will have at most one dirty region in this table at any time. When a dirty region is added, using addDirtyRegion(), the size of the region and the component are checked. On the off chance that either has a width or height <= 0 the method returns and nothing happens. If it is bigger than 0x0, the source component's visibility is then tested along with each of its ancestors, and, if they are all visible, its 'root' component, a Window or Applet, is located by 'walking down its tree' (similar to what occurs in addInvalidateComponent()). The dirty regions Hashtable is then asked if it already has a dirty region of our component stored. If it does it returns its value, a Rectangle, and the handy SwingUtilities.computeUnion() method is used to combine the new dirty region with the old one. Finally, RepaintManager passes the 'root' to the SystemEventQueueUtilities' queueComponentWorkRequest() method. What happens from here on is identical to what we saw for revalidation (see above).
Now we can talk a bit about the paintDirtyRegions() method we summarized earlier. (Remember that this should only be called from within the event-dispatching thread.) This method starts out by creating a local reference to RepaintManger's dirty regions Hashtable, and redirecting Repaintmanager's dirty regions Hashtable reference to a different, empty one. This is all done in a critical section so that no dirty regions can be added while the swap occurs. The remainder of this method is fairly long and complicated so we'll conclude with a summary of the most significant code (see RepaintManager.java for details).
The paintDirtyRegions() method continues by iterating through an Enumeration of the dirty components, calling RepaintManager's collectDirtyComponents() method for each. This method looks at all the ancestors of the specified dirty component and checks each one for any overlap with its dirty region using the SwingUtilities.computeIntersection() method. In this way each dirty region's bouds are minimized so that only its visible region remains. (Note that collectDirtyComponents() does take transparency into account.) Once this has been done for each dirty component, the paintDirtyRegions() method enters a loop. This loop computes the final intersection of each dirty component and its dirty region. At the end of each iteration paintImmediately() is called on the associated dirty component, which actually paints each minimized dirty region in its correct location (we'll discuss this below). This completes the paintDirtyRegions() method, but we still have the most significant feature of the whole process left to discuss: painting.
JComponent includes an update() method which simply calls paint(). The update() method is never actually used by any Swing components at all, but is provided for backward compatibility. The JComponent paint() method, unlike typical AWT paint() implementations, does not handle all of a component's painting. In fact it very rarely handles any of it directly. The only rendering work JComponent's paint() method is really responsible for is working with clipping areas, translations, and painting pieces of the Image used by RepaintManager for double-buffering. The rest of the work is delegated to several other methods. We will briefly discuss each of these methods and the order in which painting operations occur. But first we need to discuss how paint() is actually invoked.
As we know from our discussion of the repainting process above, RepaintManager is responsible for invoking a method called paintImmediately() on each component to paint its dirty region (remember there is always just one dirty region per component because they are intelligently coalesced by RepaintManager). This method, and the private ones it calls, make an intelligently crafted repainting process even more impressive. It first checks to see if the target component is visible, as it could have been moved, hidden, or disposed since the original request was made. Then it recursively searches the component's non-opaque (using isOpaque()) parents and increases the bounds of the region to repaint accordingly until it reaches an opaque parent:
1. If the parent reached is a JComponent subclass, the private _paintImmediately() method is called and passed the newly computed region. This method queries the isOptimizedDrawing() method, checks whether double-buffering is enabled (if so it uses the off-screen Graphics object associated with RepaintManager's buffered Image), and continues working with isOpaque() to determine the final parent component and bounds to invoke paint() on.
A. If double-buffering is enabled, it calls paintWithBuffer()(another private method). This method works with the off-screen Graphics object and its clipping area to generate many calls to the parent's paint() method (passing it the off-screen Graphics object using a specific clipping area each time). After each call to paint(), it uses the resulting off-screen Graphics object to draw directly to the visible component. (In this specific case, the paint() method will not use any buffers internally because it knows, by checking certain flags we will not discuss, that the buffering is being taken care of elsewhere.)
B. If double-buffering is not enabled, a single call to paint() is made on the parent.
2. If the parent is not a JComponent, the region's bounds are sent to that parent's repaint() method, which will normally invoke the java.awt.Component paint() method. This method will then forward traffic to each of its lightweight children's paint() method. However, before doing this it makes sure that each lightweight child it notifies is not completely covered by the current clipping area of the Graphics object that was passed in.
In all cases we have finally reached JComponent's paint() method!
Inside JComponent's paint() method, if graphics debugging is enabled a DebugGraphics instance will be used for all rendering. Interestingly, a quick look at JComponent's painting code shows heavy use of a class called SwingGraphics. This isn't in the API docs because its package private. It appears to be a very slick class for handling custom translations, clipping area management, and a Stack of Graphics objects used for caching, recyclability, and undo-type operations. SwingGraphics actually acts as a wrapper for all Graphics instances used during the painting process. It can only be instantiated by passing it an existing Graphics object. This functionality is made even more explicit, by the fact that it implements an interface called GraphicsWrapper, which is also package private.
The paint() method checks whether double-buffering is enabled and whether this method was called by paintWithBuffer() (see above):
1. If paint() was called from paintWithBuffer() or if double-buffering is not enabled, paint() checks whether the clipping area of the current Graphics object is completely obscured by any child components. If it isn't, paintComponent(), paintBorder(), and paintChildren() are called in that order. If it is completely obscured, then only paintChildren() needs to be called. (We will see what these three methods do shortly.)
2. If double-buffering is enabled and this method was not called from paintWithBuffer(), it will use the off-screen Graphics object associated with RepaintManager's buffered Image throughout the remainder of this method. Then it will check whether the clipping area of the current Graphics object is completely obscured by any child components. If it isn't, paintComponent(), paintBorder(), and paintChildren() will be called in that order. If it is completely obscured, only paintChildren() needs to be called.
A. The paintComponent() method checks if the component has a UI delegate installed. If it doesn't it just exits. If it does, it simply calls update() on that UI delegate and then exits. The update() method of a UI delegate is normally responsible for painting a component's background, if it is opaque, and then calling paint(). A UI delegate's paint() method is what actually paints the corresponding component's content. (We will see how to customize UI delegates extensively throughout this text.)
B. The paintBorder() method simply paints the component's border if it has one.
C. The paintChildren() method is a bit more involved. To summarize, it searches through all child components and determines whether paint() should be invoked on them using the current Graphics clipping area, the isOpaque() method, and the isOptimizedDrawingEnabled() method. The paint() method called on each child will essentially start that child's painting process from part 2 above, and this process will repeat until either no more children exist or need to be painted.
The bottom line: When building or extending lightweight Swing components it is normally expected that if we want to do any painting within the component itself (vs. in the UI delegate where it normally should be done) we will override the paintComponent() method and immediately call super.paintComponent(). In this way the UI delegate will be given a chance to render the component first. Overriding the paint() method, or any of the other methods mentioned above should rarely be necessary, and it is always good practice to avoid doing so.