28.5 Creating Your Own Component


So you've been bitten by the bug. There isn't a component anywhere in the Swing library that fits your needs, and you've decided that it's time to write your own. Unfortunately, you're dreading the prospect of creating one. Maybe you've heard somewhere that it is a complex task, or your jaw is still bouncing on the floor after browsing through some of the Swing component source code.

This section should help dispel those fears. Creating your own component isn't hard just extend the JComponent class with one of your own and away you go! On the other hand, getting it to behave or even display itself correctly can take a bit of patience and fine-tuning. So here is a step-by-step guide to steer you clear of the hidden "gotchas" that lurk in the task of creating a component.

When creating Swing components, it's always a good idea to adhere to the JavaBeans standards. Not only can such components be used programmatically, but they can also be plugged into one of the growing number of GUI-builder tools. Therefore, whenever possible, we try to highlight areas that you can work on to make your components more JavaBeans-friendly.

28.5.1 Getting Started

First things first. If you haven't already, you should read through the JComponent section of Chapter 3. This will help you get a feel for the kinds of features you can expect in a Swing component and which ones you might want to use (or even disable) in your own component. If you are creating a component that is intended as a container,[2] be sure to glance at the overview sections on focus policies and layout managers as well. Remember that you can use any layout manager with a Swing component.

[2] This is sort of confusing. Because of the class hierarchy of JComponent, all classes that extend it are capable of acting as containers. For example, it is legal to add a JProgressBar to a JSlider. Clearly, the slider is not meant to act as a container, but Swing will allow it nevertheless . . . with undefined results.

After you've done that, you're ready to start. Let's go through some steps that will help you flesh out that component idea into working Swing code.

28.5.1.1 You should have a model and a UI delegate

If you really want to develop your idea into a true Swing component, you should adhere to the MVC-based architecture of Swing. This means defining models and UI delegates for each component. Recall that the model is in charge of storing the state information for the component. Models typically implement their own model interface, which outlines the accessors and methods that the model must support. The UI delegate is responsible for painting the component and handling any input events that are generated. The UI-delegate object always extends the ComponentUI class, which is the base class for all UI-delegate objects. Finally, the component class itself extends the abstract JComponent and ties together the model and the delegate.

Figure 28-11 shows the key classes and interfaces involved in creating a Swing component. The shaded boxes indicate items that the programmer must provide. This includes the model, the basic UI delegate and its type class, and an implementation of the component to bundle the model and UI delegate pieces together. Finally, you may need to create your own model interface if a suitable one does not exist.

Figure 28-11. The three parts of a Swing component
figs/swng2.2811.gif

If you wish to support multiple L&Fs with your component, you may consider breaking your UI-delegate class down into an abstract "skeleton," implementing functionality that is independent of L&F (such as those classes found in javax.swing.plaf.basic), as well as functionality specific to each L&F (such as those in the javax.swing.plaf.metal package or the com.sun.java.swing.plaf.motif package). Some common functionality that you might find in the former is the ability to handle various mouse or keyboard events, while painting and sizing the component is typically the domain of the latter. See Chapter 26 for more details on L&Fs.

So now that we know what we have to build, let's continue our discussion with a look at each piece, starting with the model.

28.5.2 Creating a Model

The model of the object is responsible for storing the state data of the component. Models are not hard to build, but they are not necessarily easy to get right either. Here are some important tips to think about when working with models.

28.5.2.1 Reuse or extend existing models whenever possible

Creating a data model from scratch looks trivial, but it typically takes more time and effort than most people think. Remember that good models are abstractions of component state and are often capable of representing more than one type of component. For example, the BoundedRangeModel serves the JSlider, JProgressBar, and JScrollBar components. In addition, models are responsible for storing event listeners, handling synchronization issues, and firing property change events to the components that use them. The ability to come up with useful, general models is one of the hallmarks of the experienced API designer.

A great deal of time was spent in creating the individual models for the Swing components. And because they are central to the functionality of the Swing components, you can be assured that they're well tested. For example, with the jog shuttle example (shown later in this chapter), we decided to reuse the BoundedRangeModel. This model does an excellent job with any component that contains a value within a closed range. Chances are that there might already be a model that meets your needs, and you can either reuse or extend it to your liking.

Table 28-4 summarizes the Swing component models that we cover in this book. This should give you a good feel for whether a certain data model can be reused or extended in your own component. For an example of a component built with an existing model, take a look at Chapter 16.

Table 28-4. Swing models

Model

Chapter

Description

ButtonModel

Chapter 5

Holds the state of a button (like JButton), including its value and whether it is enabled, armed, selected, or pressed; supports "rollover" images

BoundedRangeModel

Chapter 6

Holds an int value that can vary between fixed maximum and minimum limits; used for JSlider, JProgressBar, JScrollBar, and their relatives

ComboBoxModel

Chapter 7

Holds the elements of a list and a single selected element; used for JComboBox

Document

Chapter 20

Holds the content (i.e., text) of a document that might be displayed in an editor; used by the text components

ListModel

Chapter 7

Holds the elements of a list (as in a JList)

ListSelectionModel

Chapter 7

Holds one or more elements selected from a list

SingleSelectionModel

Chapter 14

Holds an index into an array of possible selections; used by JMenuBar and JPopupMenu

TableModel

Chapter 15

Holds a two-dimensional array of data; the basis for JTable

TableColumnModel

Chapter 15

Controls the manipulation of columns in a table

TreeModel

Chapter 17

Holds items that can be displayed with branches; used by JTree

TreeSelectionModel

Chapter 17

Holds one or more elements selected from a tree

28.5.2.2 Decide on properties and create the model interface

This is the fun part. You should decide which properties will be located in the model and how to access them. Properties can be read/write, read-only, or write-only. They can be any type of object or primitive data type. Also, according to the JavaBeans standard, properties can be indexed as well as bound or constrained. For an explanation of these qualifiers, see Table 28-5.

Table 28-5. Property types

Property type

Description

Bound

The property must send a PropertyChangeEvent to all registered listeners when it changes state.

Indexed

The property is an array of objects or values. Accessors must provide an index to determine which element they want to set or retrieve.

Constrained

The property must fire a VetoableChangeEvent to all registered listeners before it changes state. Any one listener is allowed to veto the state change, in which case the original property state is preserved.

As you are probably aware, there are three types of methods that you will commonly use to interact with object properties: "get," "set," and "is." You use the "get" accessor to retrieve an object or primitive property (for readability, boolean properties usually use "is" rather than "get") and the "set" mutator to alter the value. The JavaBeans standard states that property accessors should adhere to the method signatures shown in Table 28-6. The PropertyType in the table should reflect the object or primitive type of the property.

Table 28-6. Method signatures for property accessors

Type

"get" accessor

"set" mutator

"is" accessor

Standard

PropertyType getProperty( )

void setProperty(PropertyType)

boolean isProperty( )

Indexed

PropertyType getProperty(int)

void setProperty(int, PropertyType)

boolean isProperty(int)

The interface for the model should contain only the appropriate accessor methods for each of the properties you decide on. This ensures that the access rights are enforced in whatever implementation of the model is provided. You should also include methods to add, remove, and enumerate the relevant ChangeEvent or PropertyChangeEvent listeners from your model. It is not necessary to include a method to actually fire off a change event. Although this behavior is implicitly part of the implementing classes, it has no place in the interface itself. The easy way to provide it is discussed below.

Here is the interface for the SimpleModel class we develop later:

//  SimpleModelInteface.java // import javax.swing.*; import javax.swing.event.*; public interface SimpleModelInterface  {     public int getValue( );      public void setValue(int v);     public boolean isActivated( );     public void setActivated(boolean b);     public void addChangeListener(ChangeListener l);      public void removeChangeListener(ChangeListener l);     public ChangeListener[] getChangeListeners( ); }
28.5.2.3 Send events when bound properties change

This is critical. Components and UI-delegate objects are intrinsically linked and must know immediately when any bound or constrained property in the model has changed state. For example, with the ButtonModel, if a button is disabled, the component needs to know immediately that the button cannot be pressed. In addition, the UI delegate needs to know immediately that the button should be grayed on the screen. Both objects can be notified by firing an event that describes the change in state of the bound model property.

Depending on how many changes you intend to send at any given time, you can use either a PropertyChangeEvent or a ChangeEvent to signal a change to a model property. PropertyChangeEvent is the more informative event that is used with JavaBeans components; it describes the source object of the change as well as the name of the property and its old and new state. ChangeEvent, on the other hand, merely flags that a change has taken place. The only data bundled with a ChangeEvent object is the source object. The former is more descriptive and does not require the listener to request the current property state from the model. The latter is more useful if many change events can be fired in a short amount of time and if the recipients typically need to look at the overall model state anyway. (It would be shortsighted to think that your code was more efficient because you saved the effort of building a detailed PropertyChangeEvent if each recipient of the event then had to look up that specific information!)

As we mentioned earlier, this means that you must also have addPropertyChange-Listener( ) and removePropertyChangeListener( ) methods or addChangeListener( ) and removeChangeListener( ) methods to maintain a list of event subscribers, depending on the type of change event you intend to support. These method signatures are typically given in the model interface. You'll also need a protected method that fires the change events to all registered listeners; this is provided in the model. For new models it makes sense to define the accessor method to return the list of registered listeners (getChangeListeners( ) in our example above) as part of the interface. This has not been done for existing Swing model interfaces because it would have broken backward compatibility with third-party implementations of those interfaces written before the accessors were recognized as a standard and useful part of the models.

28.5.2.4 Reuse the EventListenerList class

If you look closely through the otherwise mundane javax.swing.event package, you might find a surprise waiting for you: the EventListenerList class. This handy class allows you to maintain a list of generic event listeners that you can retrieve at any time. In order to use it, simply declare an EventListenerList object in your model and have each of your event subscription methods call methods associated with the EventListenerList:

EventListenerList theList = new EventListenerList( ); public void addChangeListener(ChangeListener l) {     theList.add(ChangeListener.class, l); } public void removeChangeListener(ChangeListener l) {     theList.remove(ChangeListener.class, l); }

When you need to retrieve the listener list to fire a ChangeEvent, you can do so with the following code:

ChangeEvent theEvent; protected void fireStateChanged( ) {     Object[] list = theList.getListenerList( );     for (int index = list.length-2; index >= 0; index -= 2) {         if (list[index]==ChangeListener.class) {             if (theEvent == null)                 theEvent = new ChangeEvent(this);             ((ChangeListener)list[index+1]).stateChanged(theEvent);         }     } }

There are a couple curious features about this code worth explaining. EventListenerList is intended to store all listener registrations for a particular component. Because objects might be registered more than once as listeners for different kinds of events (through different interfaces), the list needs to keep track of both the interface under which the listener was registered and the reference to the listener itself. It does this by storing pairs of entries in the array. Even-numbered subscripts contain the interface under which a listener has been registered, and the following subscript contains the reference to the listener itself hence, the loop that checks for the proper listener class before invoking the method on the associated listener. Because the even subscripts are known to contain specific interface references, a fast equality check (==) can be used instead of the more expensive instanceof operator.

Changing this method to handle the more robust PropertyChangeEvent is simply a matter of adding three parameters to the method signature, which are used in instantiating the event object.

28.5.2.5 Don't put component properties in the model

Model properties define the state data of a component type; component properties typically define unique characteristics of a specific component including the display format. Be sure not to confuse the two. While accessors for model properties are available at the component level, they are delegated to corresponding methods in the model. Clients of the component can either call the component's accessors for model properties or use the model itself. Display properties, on the other hand, exist only at the component level. Hence, it is important that component properties stay in the component and don't creep into the model.

An example might better explain the differences. The minimum, maximum, and value properties apply to all bounded-range components, including JScrollBar and JProgressBar. Furthermore, these properties relate to data values, not display. For these reasons, they make sense as properties of the data model that all bounded-range components use. Major and minor tick marks, as well as labels, are specific to the JSlider object and how it displays itself. They serve better as component properties.

28.5.2.6 Implement the model

Finally, complete the model by implementing the model interface. This example shows a model that can be used as a reference. (We don't use this model for the JogShuttle component we develop later.)

//  SimpleModel.java // import javax.swing.*; import javax.swing.event.*; public class SimpleModel implements SimpleModelInterface {     protected transient ChangeEvent changeEvent = null;     protected EventListenerList listenerList = new EventListenerList( );     private int value = 0;     private boolean activated = false;     public SimpleModel( ) { }     public SimpleModel(int v) { value = v; }     public SimpleModel(boolean b) { activated = b; }     public SimpleModel(int v, boolean b) {          value = v;         activated = b;     }     public int getValue( ) { return value; }     public synchronized void setValue(int v) {        if (v != value) {             value = v;            fireChange( );         }     }     public boolean isActivated( ) { return activated; }     public synchronized void setActivated(boolean b) {        if (b != activated) {            activated = b;            fireChange( );         }     }     public void addChangeListener(ChangeListener l) {         listenerList.add(ChangeListener.class, l);     }          public void removeChangeListener(ChangeListener l) {         listenerList.remove(ChangeListener.class, l);     }     public ChangeListener[] getChangeListeners( ) {         return (ChangeListener[])listenerList.getListeners(ChangeListener.class);     }     protected void fireChange( )      {         Object[] listeners = listenerList.getListenerList( );         for (int i = listeners.length - 2; i >= 0; i -=2 ) {             if (listeners[i] == ChangeListener.class) {                 if (changeEvent == null) {                     changeEvent = new ChangeEvent(this);                 }                 ((ChangeListener)listeners[i+1]).stateChanged(changeEvent);             }                   }     }        public String toString( )  {         String modelString = "value=" + getValue( ) + ", " +             "activated=" + isActivated( );         return getClass( ).getName( ) + "[" + modelString + "]";     } } 

28.5.3 The UI Delegate

Once you've found or created the right model, you need to create a UI delegate for your component. Because your UI will likely be implemented by different classes in different L&Fs, you first create a simple abstract superclass that defines the type of the UI delegate. As noted below, this class is often empty.

28.5.3.1 Create an abstract type class

Start by creating an abstract superclass to be shared by all L&F implementations of your new UI. This extends the abstract javax.swing.plaf.ComponentUI class. If there are any new methods that should be provided by all implementations of the UI, define them as abstract in this superclass. (This requires subclasses to implement them.) In simple cases, there are no such methods needed; if you look at the Swing source code, you'll see that many of the UI delegate superclasses are completely empty. They extend ComponentUI and thus require subclasses to flesh out its abstract methods, but add nothing else. The new class, even if empty, does define a new type for use in parameter and variable declarations. Remember, Java's all about type safety!

One good thing to put in the "empty" superclass is the string constant that identifies this kind of UI for the UIManager: the UI class ID. This way, the programmers of components and delegates won't have to try to remember exactly how this string was spelled or capitalized, reducing the opportunity for mistakes.

//  JogShuttleUI.java // import javax.swing.plaf.*; public abstract class JogShuttleUI extends ComponentUI {     public static final String UI_CLASS_ID = "JogShuttleUI"; }

In our example, the standard implementation of the UI is placed in a file called BasicJogShuttleUI.java.

28.5.3.2 You must implement a paint method

In the UI-delegate object (BasicJogShuttleUI in our example), the paint( ) method is responsible for performing the actual rendering of the component. The paint( ) method takes two parameters: a reference to the component needing to be drawn and a Graphics context with which it can draw the component:

public void paint(Graphics g, JComponent c)

You can use the Graphics object to access any drawing utilities needed to render the component. In addition, the reference to the JComponent object lets you obtain the current model and display properties as well as any other utility methods that JComponent (or your specific component type) provides.

Remember that in the paint( ) method, you are working in component coordinate space and not in container coordinate space. This means that you will be interested in the c.getHeight( ) and c.getWidth( ) values, which are the maximum height and width of space that has been allocated to you by the container's layout manager. You will not be interested in the c.getX( ) or c.getY( ) values. The latter values give the position of the component in the container's coordinate space, not the component's. Within the component coordinate space, the upper-left corner of the drawing area that you will use is (0,0).

One more caveat: remember that Swing components can take borders or insets. The size of these, however, is included in the width and height reported by the component. It's always a good idea to subtract the border or insets from the total space before drawing a component. You can adjust your perspective by implementing the following code:

public void paint(Graphics g, JComponent c) {     // We don't want to paint inside of the insets or borders, so subtract them.     Insets insets = c.getInsets( );  // Takes border into account if needed     g.translate(insets.left, insets.top);     int width = c.getWidth( )-insets.left-insets.right;     int height = c.getHeight( )-insets.top-insets.bottom;     // Do our actual painting here . . .      // Restore state for the rest of the objects that need painting.     g.translate(-insets.left, -insets.top); }
28.5.3.3 Be able to resize yourself

Your component should always be able to draw itself correctly, based on its current size as dictated by the layout manager minus any insets. Recall that when the component is validated in the container's layout manager, the manager attempts to retrieve the preferred or minimum size of the component. If nothing has been set by overriding getMinimumSize( ) or getPreferredSize( ), the layout manager assigns the component an arbitrary size, based on the constraints of the layout. (Of course, even if you report preferences, they might need to be ignored due to lack of available space.)

Drawing a component (or parts of it) using a static size is asking for trouble. You should not hardcode specific widths of shapes, unless they are always one or two pixels when the component is resized. Remember that the "left" size of the insets may be different than the "right," and the "top" different from the "bottom." Above all, you should be aware that components can sometimes be called upon to fit into seemingly impossible sizes, especially during application development. While the programmer will quickly understand that this is not what's intended, the more gracefully the component can handle this, the better.

As you are creating your component, place it in various layout managers and try resizing it. Try giving the component ridiculously low (or high) widths and heights. You may not be able to make it look pretty, but be sure not to throw any unnecessary runtime exceptions to derail your user's application!

28.5.4 Creating the Component Itself

There are three final steps to creating a fully functioning component: deciding on properties, registering your class as a listener for relevant events, and sending events when the component's properties change.

28.5.4.1 Deciding on properties

When creating the component, you need to decide which data properties you want to make available to the programmer. Public properties usually include several from the data model and others that can be used to configure the component's display. All of these properties need to have one or more public methods that programmers can use to obtain or alter their values. (See the previous discussion on model properties for details about creating properties and naming methods.)

There are several properties that you almost always want to provide, to fit well in the Swing MVC architecture. They are shown in Table 28-7.

Table 28-7. Commonly exported properties

Property

Description

model

Data model for the component

UI

UI delegate

UIClassID

Read-only class ID string of the UI delegate; used by the UIManager

Unless there is a really good reason, you should always try to keep the fields that store the properties of your components private to enforce the use of accessors even for subclasses. To see why, we should take a look at Swing buttons and the AbstractButton class. With this class, there are a few restrictions that we should mention:

  • Only buttons that are enabled can be armed. In this case, the mutator named setArmed( ) first checks the button's enabled property to see if it is true. If it isn't, the button cannot be armed.

  • You can't press the button with the setPressed( ) method unless it is armed. Therefore, the setPressed( ) method checks the isArmed( ) method to see if it returns true. If not, the button cannot be pressed.

Both of these cases demonstrate examples of a conditional mutator. In other words, if you call a mutator to set a property, there are isolated cases in which it might not succeed. It also demonstrates why you should try to avoid properties that are protected. In this case, prerequisites were needed for the mutator method to succeed and the property to be set to a specific state. If a subclass is allowed, override the accessors and ignore any prerequisites on that property; the component state could become unsynchronized, and the results could be unpredictable. Even if you know right now that there are no such cases in your own component, you should buy some insurance and plan for the future with this more JavaBeans-friendly approach.

28.5.4.2 Listening to your models

The model is an essential part of any component. If it is sending events, you need to react. Essentially, this means that you should add your component as a listener for any events that the model fires (typically, ChangeEvent or PropertyChangeEvent objects). This can be done through the addChangeListener( ) or the addPropertyChangeListener( ) method of the model.

It's always a good idea to add your component class as a listener to the model in the setModel( ) method. When doing so, be sure to also unregister yourself with any previous model to which you might have been listening. Finally, make sure a call to setModel( ) happens during the initialization phase of your component to make sure that everything is set up correctly.

When a change in the model occurs, you should probably repaint your component to reflect the new state. If you are not interested in performing extra tasks when a change event is sent to you, nor in propagating model change events to outside objects, you can probably get away with just calling repaint( ). In such cases, you may even choose not to listen to the event at all, letting the UI delegate handle it for you.

28.5.4.3 Sending events when bound properties change

Other components in the outside world may be interested in knowing when properties inside your component have changed. You should decide which of your properties make sense as bound or constrained.

You can use the firePropertyChangeEvent( ) method of JComponent to fire off PropertyChangeEvent objects to all registered listeners. The JComponent class contains overloaded versions of these methods for all primitive data types (int, long, boolean, etc.), as well as for the Object class, which pretty much covers everything else. JComponent also contains addPropertyChangeListener( ) and removePropertyChangeListener( ) methods, as well as a selection of getPropertyChangeListeners( ) methods. There is a similar set of methods for vetoable property changes (to support constrained properties). This all means you don't have to worry about maintaining your own event listener list; it's taken care of for you.

28.5.5 Some Final Questions

Finally, before writing that component and placing it in the Swing libraries, here are some questions that you can ask yourself while customizing your component. If the answer to any of these is "Yes," then follow the instructions provided.

Do you want the component to avoid getting focus at all, or through traversal?

Focus traversal refers to the action of pressing Tab or Shift-Tab to cycle the focus onto a component. If you want your component to avoid accepting the focus at all, set its focusable property to false. If you want it to be skipped in the focus cycle, provide a custom focus traversal policy as described at the beginning of this chapter.

Do you want your component to maintain its own focus cycle?

In order to do this, override the isFocusCycleRoot( ) method of JComponent and return true. This specifies that the component traverses repeatedly through itself and each of its children, but does not leave the component tree unless focus is explicitly moved up or down the tree.

public boolean isFocusCycleRoot( ) {return true;}
Do you want to prevent a border from being placed around your component?

You can override the getBorder( ) method to return null:

public Border getBorder( ) {return null;}
Are you always opaque?

Your component should correctly report its opaqueness with the isOpaque( ) method. If your component always fills in every pixel of its assigned area, override this method and return true, as follows:

public boolean isOpaque( ) {return true;}

28.5.6 The Jog Shuttle: a Simple Swing Component

Here is an example of a component that mimics a jog shuttle, which is a control found on fancier VCRs and television remote controls, and on professional film and video editing equipment. It's a dial that can be turned through multiple revolutions; turning clockwise increases the dial's value, and turning counterclockwise decreases it. The shuttle has a fixed minimum and maximum value; the range (the difference between the minimum and maximum) may be more than a single turn of the dial, though it defaults to one turn. This sounds like a job for the BoundedRangeModel, and we reuse this model rather than develop our own. However, we have created our own delegate that handles mouse events and is capable of moving the jog shuttle when the mouse is dragged over it.

28.5.6.1 The component

Here is the code for the component portion, JogShuttle. This class extends JComponent, relying on another class, JogShuttleUI, to display itself. JogShuttle implements ChangeListener so it can receive ChangeEvent notifications from the model.

//  JogShuttle.java // import java.awt.*; import java.awt.event.*; import javax.swing.*; import javax.swing.event.*; import javax.swing.border.*; public class JogShuttle extends JComponent implements ChangeListener {     private BoundedRangeModel model;     // The dialInsets property tells how far the dial is inset from the sunken     // border.     private Insets dialInsets = new Insets(3, 3, 3, 3);     // The valuePerRevolution property tells how many units the dial takes to make a     // complete revolution.     private int valuePerRevolution;     // Constructors     public JogShuttle( ) {         init(new DefaultBoundedRangeModel( ));     }     public JogShuttle(BoundedRangeModel m) {         init(m);     }     public JogShuttle(int min, int max, int value) {         init(new DefaultBoundedRangeModel(value, 1, min, max));     }           protected void init(BoundedRangeModel m) {         setModel(m);         valuePerRevolution = m.getMaximum( ) - m.getMinimum( );         setMinimumSize(new Dimension(80, 80));         setPreferredSize(new Dimension(80, 80));         updateUI( );     }     public void setUI(JogShuttleUI ui) {super.setUI(ui);}     public void updateUI( ) {        setUI((JogShuttleUI)UIManager.getUI(this));        invalidate( );     }     public String getUIClassID( ) {         return JogShuttleUI.UI_CLASS_ID;     }     public void setModel(BoundedRangeModel m) {         BoundedRangeModel old = model;         if (old != null)             old.removeChangeListener(this);         if (m == null)             model = new DefaultBoundedRangeModel( );         else             model = m;         model.addChangeListener(this);         firePropertyChange("model", old, model);     }     public BoundedRangeModel getModel( ) {         return model;     }     // Methods     public void resetToMinimum( ) {model.setValue(model.getMinimum( ));}     public void resetToMaximum( ) {model.setValue(model.getMaximum( ));}       public void stateChanged(ChangeEvent e) {repaint( );}     // Accessors and mutators     public int getMinimum( ) {return model.getMinimum( );}     public void setMinimum(int m) {         int old = getMinimum( );         if (m != old) {             model.setMinimum(m);             firePropertyChange("minimum", old, m);         }     }     public int getMaximum( ) {return model.getMaximum( );}     public void setMaximum(int m) {         int old = getMaximum( );         if (m != old) {             model.setMaximum(m);             firePropertyChange("maximum", old, m);         }     }     public int getValue( ) {return model.getValue( );}     public void setValue(int v) {         int old = getValue( );         if (v != old) {             model.setValue(v);             firePropertyChange("value", old, v);         }     }     // Display-specific properties     public int getValuePerRevolution( ) {return valuePerRevolution;}     public void setValuePerRevolution(int v) {         int old = getValuePerRevolution( );         if (v != old) {             valuePerRevolution = v;             firePropertyChange("valuePerRevolution", old, v);         }         repaint( );     }     public void setDialInsets(Insets i) {dialInsets = i;}     public void setDialInsets(int top, int left, int bottom, int right) {         dialInsets = new Insets(top, left, bottom, right);     }     public Insets getDialInsets( ) {return dialInsets;} }

The component itself is very simple. It provides several constructors, offering the programmer different ways to set up the data model, which is an instance of BoundedRangeModel. You can set the minimum, maximum, and initial values for the jog shuttle in the constructor, or provide them afterwards using the mutator methods setMinimum( ), setMaximum( ), and setValue( ). Regardless of which constructor you call, most of the work is done by the init( ) method, which registers the JogShuttle as a listener for the model's ChangeEvent notifications, sets its minimum and preferred sizes, and calls updateUI( ) to install the appropriate user interface.

Most of the JogShuttle code consists of methods that support various properties. Accessors for the model properties, like getMinimum( ), simply delegate to the equivalent accessor in the model itself. Other properties, like valuePerRevolution, are display-specific and are maintained directly by the JogShuttle class. (The amount the value changes when the shuttle turns through one revolution has a lot to do with how you display the shuttle and how you interpret mouse events, but nothing to do with the actual data that the component represents.) The user interface object (which we'll discuss below) queries these properties to find out how to paint itself.

Of course, the JogShuttle needs to inform the outside world of changes to its state a component that never tells anyone that something has changed isn't very useful. To keep the outside world informed, we have made several of our properties bound properties. The bound properties include minimum, maximum, and value, plus a few of the others. JComponent handles event-listener registration for us. The mutator methods for the bound properties fire PropertyChangeEvent notifications to any event listeners. Of course, if some undisciplined code makes changes to the model directly, these notifications won't occur, so that code had better be listening to the model directly, too. To resolve this issue would require adding code to JogShuttle to receive events from the model and pass them on to its own listeners; it wouldn't be enough to just eliminate public access to the getModel( ) method because there's a constructor that takes a model as input, and there is no way to prevent other code from keeping references to that model.

One other method worth looking at is stateChanged( ). This method is called whenever the model issues a ChangeEvent, meaning that one of the model properties has changed. All we do upon receiving this notification is call repaint( ) , which lets the repaint manager schedule a call to the user interface's paint( ) method, redrawing the shuttle. This may seem roundabout. The UI delegate handles some mouse events, figures out how to change the shuttle's value, and informs the model of the change; in turn, the model generates a change event, and the component receives the event and calls the repaint manager to tell the component's UI delegate to redraw itself. It is important to notice that this roundabout path guarantees that everyone is properly informed of the component's state, that everyone can perform their assigned task, and furthermore, that the repaint operation is scheduled by the repaint manager, so it occurs on the right thread and won't interfere with other event processing.

28.5.6.2 The UI delegate

BasicJogShuttleUI is our "delegate" or "user interface" class, and therefore extends JogShuttleUI and consequently ComponentUI. It is responsible for painting the shuttle and interpreting the user's mouse actions, and so implements the MouseListener and MouseMotionListener interfaces. Although this requires a fair amount of code, it is a fundamentally simple class:

//  BasicJogShuttleUI.java // import java.awt.*; import java.awt.event.*; import javax.swing.*; import javax.swing.plaf.*; import javax.swing.border.*; public class BasicJogShuttleUI extends JogShuttleUI     implements MouseListener, MouseMotionListener {     private static final int KNOB_DISPLACEMENT = 3;     private static final int FINGER_SLOT_DISPLACEMENT = 15;     private double lastAngle;  // Used to track mouse drags     public static ComponentUI createUI(JComponent c) {         return new BasicJogShuttleUI( );     }     public void installUI(JComponent c) {         JogShuttle shuttle = (JogShuttle)c;         shuttle.addMouseListener(this);         shuttle.addMouseMotionListener(this);     }     public void uninstallUI(JComponent c) {         JogShuttle shuttle = (JogShuttle)c;         shuttle.removeMouseListener(this);         shuttle.removeMouseMotionListener(this);     }     public void paint(Graphics g, JComponent c) {         // We don't want to paint inside the insets or borders.         Insets insets = c.getInsets( );         g.translate(insets.left, insets.top);         int width = c.getWidth( ) - insets.left - insets.right;         int height = c.getHeight( ) - insets.top - insets.bottom;           // Draw the outside circle.         g.setColor(c.getForeground( ));         g.fillOval(0, 0, width, height);                  Insets d = ((JogShuttle)c).getDialInsets( );         int value = ((JogShuttle)c).getValue( );         int valuePerRevolution = ((JogShuttle)c).getValuePerRevolution( );           // Draw the edge of the dial.          g.setColor(Color.darkGray);         g.fillOval(d.left, d.top, width-(d.right*2), height-(d.bottom*2));          // Draw the inside of the dial.         g.setColor(Color.gray);         g.fillOval(d.left + KNOB_DISPLACEMENT,                    d.top + KNOB_DISPLACEMENT,                    width - (d.right + d.left) - KNOB_DISPLACEMENT * 2,                    height - (d.bottom + d.top) - KNOB_DISPLACEMENT * 2);          // Draw the finger slot.         drawFingerSlot(g, c, value, width, height, valuePerRevolution,                 FINGER_SLOT_DISPLACEMENT - 1,                 (double)(width/2) - d.right - FINGER_SLOT_DISPLACEMENT,                 (double)(height/2) - d.bottom - FINGER_SLOT_DISPLACEMENT);         g.translate(-insets.left, -insets.top);     }     private void drawFingerSlot(Graphics g, JComponent c, int value,         int width, int height, int valuePerRevolution, int size,         double xradius, double yradius) {              int currentPosition = value % valuePerRevolution;         // Obtain the current angle in radians.         double angle = ((double)currentPosition / valuePerRevolution) *                          java.lang.Math.PI * 2;         // Obtain the x and y coordinates of the finger slot, with the minimum value         // at twelve o'clock.         angle -= (java.lang.Math.PI / 2);         int xPosition = (int) (xradius * java.lang.Math.sin(angle));         int yPosition = (int) (yradius * java.lang.Math.cos(angle));         xPosition = (width / 2) - xPosition;         yPosition = (height / 2) + yPosition;         // Draw the finger slot with a crescent shadow on the top left.         g.setColor(Color.darkGray);          g.fillOval(xPosition-(size/2), yPosition-(size/2), size, size);          g.setColor(Color.lightGray);          g.fillOval(xPosition-(size/2) + 1, yPosition - (size/2) + 1,                    size - 1, size - 1);      }     // Figure out angle at which a mouse event occurred with respect to the     // center of the component for intuitive dial dragging.     protected double calculateAngle(MouseEvent e) {         int x = e.getX( ) - ((JComponent)e.getSource( )).getWidth( ) / 2;         int y = -e.getY( ) + ((JComponent)e.getSource( )).getHeight( ) / 2;         if (x == 0) {  // Handle case where math would blow up.             if (y == 0) {                 return lastAngle;   // Can't tell...             }             if (y > 0) {                 return Math.PI / 2;             }             return -Math.PI / 2;         }         return Math.atan((double)y / (double)x);     }     public void mousePressed(MouseEvent e) { lastAngle = calculateAngle(e); }     public void mouseReleased(MouseEvent e) { }     public void mouseClicked(MouseEvent e) { }     public void mouseEntered(MouseEvent e) { }     public void mouseExited(MouseEvent e) { }     // Figure out the change in angle over which the user has dragged,     // expressed as a fraction of a revolution.     public double angleDragged(MouseEvent e) {         double newAngle = calculateAngle(e);         double change = (lastAngle - newAngle) / Math.PI;         if (Math.abs(change) > 0.5) {  // Handle crossing origin.             if (change < 0.0) {                 change += 1.0;             } else {                 change -= 1.0;             }         }         lastAngle = newAngle;         return change;     }     public void mouseDragged(MouseEvent e) {         JogShuttle theShuttle = (JogShuttle)e.getComponent( );         theShuttle.setValue(theShuttle.getValue( ) +             (int)(angleDragged(e) * theShuttle.getValuePerRevolution( )));     }     public void mouseMoved(MouseEvent e) { } } 

BasicJogShuttleUI starts by overriding several methods of ComponentUI. createUI( ) is a simple static method that returns a new instance of our UI object. installUI( ) registers our UI object as a listener for mouse events from its component (the JogShuttle). uninstallUI( ) does the opposite: it unregisters the UI as a listener for mouse events.

Most of the code is in the paint( ) method and its helper, drawFingerSlot( ), and the event handlers that translate mouse gestures into a sense of the angle through which the user has tried to turn the dial. paint( ) draws the jog shuttle on the screen. Its second argument, c, is the component that we're drawing in this case, an instance of JogShuttle. The paint( ) method is careful to ask the shuttle for all the information it needs without making any assumptions about what it might find. (In turn, JogShuttle delegates many of these requests to the model.)

We handle input using mousePressed( ) to store the angle (relative to the center of the dial) at which a drag starts. Then mouseDragged( ) figures out the angle of the new mouse position and what fraction of a revolution this move represents. It then calls setValue( ) to inform the shuttle (and hence, the model) of its new value.

28.5.7 A Toy Using the Shuttle

Here is a short application that demonstrates the JogShuttle component. We've mimicked a simple toy that lets you doodle on the screen by manipulating two dials. This example also demonstrates how easy it is to work with JComponent.

//  Sketch.java // import java.awt.*; import java.awt.event.*; import java.beans.*; import java.util.*; import javax.swing.*; import javax.swing.border.*; public class Sketch extends JPanel     implements PropertyChangeListener, ActionListener {     JogShuttle shuttle1;     JogShuttle shuttle2;     JPanel board;     JButton clear;     int lastX, lastY;  // Keep track of the last point we drew.     public Sketch( ) {         super(true);           setLayout(new BorderLayout( ));         board = new JPanel(true);         board.setPreferredSize(new Dimension(300, 300));         board.setBorder(new LineBorder(Color.black, 5));         clear = new JButton("Clear Drawing Area");         clear.addActionListener(this);         shuttle1 = new JogShuttle(0, 300, 150);         lastX = shuttle1.getValue( );         shuttle2 = new JogShuttle(0, 300, 150);         lastY = shuttle2.getValue( );         shuttle1.setValuePerRevolution(100);         shuttle2.setValuePerRevolution(100);         shuttle1.addPropertyChangeListener(this);         shuttle2.addPropertyChangeListener(this);         shuttle1.setBorder(new BevelBorder(BevelBorder.RAISED));         shuttle2.setBorder(new BevelBorder(BevelBorder.RAISED));         add(board, BorderLayout.NORTH);         add(shuttle1, BorderLayout.WEST);         add(clear, BorderLayout.CENTER);         add(shuttle2, BorderLayout.EAST);     }     public void propertyChange(PropertyChangeEvent e) {         if (e.getPropertyName( ) == "value") {             Graphics g = board.getGraphics( );             g.setColor(getForeground( ));             g.drawLine(lastX, lastY,                        shuttle1.getValue( ), shuttle2.getValue( ));             lastX = shuttle1.getValue( );             lastY = shuttle2.getValue( );         }     }     public void actionPerformed(ActionEvent e) {         // The button must have been pressed.         Insets insets = board.getInsets( );         Graphics g = board.getGraphics( );         g.setColor(board.getBackground( ));         g.fillRect(insets.left, insets.top,                    board.getWidth( )-insets.left-insets.right,                    board.getHeight( )-insets.top-insets.bottom);     }     public static void main(String[] args) {         UIManager.put(JogShuttleUI.UI_CLASS_ID, "BasicJogShuttleUI");         Sketch s = new Sketch( );         JFrame frame = new JFrame("Sample Sketch Application");         frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);         frame.setContentPane(s);         frame.pack( );         frame.setVisible(true);     } }

There's really nothing surprising (except that you might suddenly wish you had two mice when you run the program). The main( ) method calls UIManager.put( ) to tell the interface manager about the existence of our new user interface. Whenever a component asks for a UI with a class ID "JogShuttleID", the UI manager looks for a class named BasicJogShuttleUI, creates an instance of that class, and uses that class to provide the user interface. Having registered the JogShuttleUI, we can then create our shuttles, register ourselves as a property change listener, and place the shuttles in a JPanel with a BorderLayout. Our property change method simply checks which property changed; if the value property changed, we read the current value of both shuttles, interpret them as a pair of coordinates, and draw a line from the previous location to the new point. Figure 28-12 shows what our toy looks like.

Figure 28-12. The Sketch application with two jog shuttles
figs/swng2.2812.gif


Java Swing
Graphic Java 2: Mastering the Jfc, By Geary, 3Rd Edition, Volume 2: Swing
ISBN: 0130796670
EAN: 2147483647
Year: 2001
Pages: 289
Authors: David Geary

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net