`
abian
  • 浏览: 9729 次
社区版块
存档分类
最新评论

How to Write a Custom Swing Component

阅读更多

How to Write a Custom Swing Component

February 22, 2007
 
 

{cs.r.title}



When you hear comparisons between AWT and Swing components, one
of the first points mentioned is that Swing is lightweight.
What this essentially means is that there are no real native
controls "behind" Swing buttons, internal frames, and menus.
Everything is controlled in pure Java, including rendering and
event handling. While this provides a much more flexible way to
create truly platform-independent components, the task of creating
a custom Swing component that has a consistent look across all
platforms and look-and-feels is not an easy one. This article walks
through the process of creating a new Swing component of medium
complexity and highlights the important points, steps, and pitfalls
along the way.

Basic Building Blocks

The Swing
architecture overview
 provides an excellent high-level overview
of the architectural decisions that were made during the
development of Swing. Although it will take slightly more work to
create a new component following the rules outlined in this
article, the resulting code will be much easier to maintain, since
it will adhere to the core Swing principles without reinventing
the wheel. At the first look you might be tempted to throw
everything together in one single class that will provide the
external API, the model handling (state and notifications), event
handling, layout, and painting. All these, however, belong in
separate classes that follow the modified MVC
(model-view-controller) architecture that will make your component
codebase much easier to maintain and extend in the long run.

The main building blocks of all core Swing components are:

  • The component class itself, which provides an API for creating,
    changing, and querying the component basic state.
  • The model interface and the model default implementation(s)
    that handle the component business logic and change
    notifications.
  • The UI delegate that handles component layout, event handling
    (mouse and keyboard), and painting.

This article will illustrate the process of creating a custom
component that is based on the new view slider from Microsoft
Windows Vista OS Explorer (see Figure 1). While this component
looks very much like a slider embedded in a pop-up menu, it has new
features that are not available on a regular JSlider.
First, it has control points that have associated icons and labels.
In addition, while some ranges are contiguous (like Small Icons-Medium Icons) and allow continuous resizing of the file
icons, other ranges are discrete (like Tiles-Details).
When the value is in one of these ranges, the slider thumb can be
only at control points, and not inside the range.

View slider in Microsoft Windows Vista OS
Figure 1. View slider in Microsoft Windows Vista OS

The Component Class: UI Delegate Plumbing

The first class for a custom component is the component API
itself. The API should be as simple as possible and delegate most
of the business logic to the model (see the next section). In addition
to the API, you should add the boilerplate code for setting the
proper UI delegate (described in detail in the "
Enhancing Swing Applications
" article). At the barest level,
this code should look like this:

    privatestaticfinalString uiClassID ="FlexiSliderUI";

    publicvoid setUI(FlexiSliderUI ui){
        super.setUI(ui);
    }

    publicvoid updateUI(){
        if(UIManager.get(getUIClassID())!=null){
            setUI((FlexiSliderUI)UIManager.getUI(this));
        }else{
            setUI(newBasicFlexiSliderUI());
        }
    }

    publicFlexiSliderUI getUI(){
        return(FlexiSliderUI) ui;
    }

    publicString getUIClassID(){
        return uiClassID;
    }

It is very important to provide a fallback UI delegate that will
handle the component painting, layout, and event handling when the
currently installed look and feel doesn't provide a special UI
delegate.

The Model Interface

This is, perhaps, the most important class for a custom
component. It represents the business side of your component. The
model interface should not contain any painting-related
methods (such as setFont or
getPreferredSize). For our specific component, we
follow the 
LinearGradientPaint
 API and define the model as a sequence of
ranges:

    publicstaticclassRange{
        privateboolean isDiscrete;

        privatedouble weight;

        publicRange(boolean isDiscrete,double weight){
            this.isDiscrete = isDiscrete;
            this.weight = weight;
        }
        
        ...
    }

The model API to set and query the model ranges:

    publicvoid setRanges(Range... range);

    publicint getRangeCount();

    publicRange getRange(int rangeIndex);

In addition, the model should provide the API to set and get the
current value. The Value class points inside a
Range:

    publicstaticclassValue{
        publicRange range;

        publicdouble rangeFraction;

        publicValue(Range range,double rangeFraction){
            this.range = range;
            this.rangeFraction = rangeFraction;
        }
        
        ...
    }

And the model API provides the getter and setter for the current
value:

    publicValue getValue();

    publicvoid setValue(Value value);

The last piece of model interface contains methods for adding
and removing ChangeListeners. This follows the model
interfaces from core Swing components (see 
BoundedRangeModel
):

    void addChangeListener(ChangeListener x);

    void removeChangeListener(ChangeListener x);

The Model Implementation

The model implementation is pretty straightforward and follows
the DefaultBoundedRangeModel. The change listeners are stored as an
EventListenerList. The change to the model value results in a ChangeEvent being fired:

    protectedvoid fireStateChanged(){
        ChangeEventevent=newChangeEvent(this);
        Object[] listeners = listenerList.getListenerList();
        for(int i = listeners.length -2; i >=0; i -=2){
            if(listeners[i]==ChangeListener.class){
                ((ChangeListener) listeners[i +1]).stateChanged(event);
            }
        }
    }

Note that as with all core Swing components, we iterate over the
listener list from the end. This way, if a listener decides
to remove itself inside the stateChanged
implementation, we'll still iterate over all registered listeners
exactly once. The implementation of the value-related methods is
very straightforward with checking of the validity of the passed value
and defensive copying of the slider ranges array (so that changes
in malicious application code won't affect the model).

The Model Unit Tests

While unit testing the UI components can be difficult, the model
should be thoroughly tested--remember that the loss of a pixel is
nothing compared to the loss of a trailing zero in the model,
especially if that zero multiplies an automatic payment by 10. Be
sure to test your default model implementation with such simple
tests as:

    publicvoid testSetValue2(){
        FlexiRangeModel model =newDefaultFlexiRangeModel();
        FlexiRangeModel.Range range0 =newFlexiRangeModel.Range(true,0.0);
        model.setRanges(range0);

        try{
            // should fail since range0 is discrete
            FlexiRangeModel.Value value =newFlexiRangeModel.Value(range0,0.5);
            model.setValue(value);
        }catch(IllegalArgumentException iae){
            return;
        }
        assertTrue(false);
    }

The Component Class: API

Going back to the component class, we can add the missing APIs
for creation of the control itself and getting its model. In
addition, some of the UI-related configuration (such as icons and
label text) is stored in the component class itself (this does
not
 belong in the model). The component constructor follows
that of 
LinearGradientPaint
, throwing exceptions on null
or non-matching parameters:

    publicJFlexiSlider(Range[] ranges,Icon[] controlPointIcons,
            String[] controlPointTexts)throwsNullPointerException,
            IllegalArgumentException

The implementation is quite straightforward. First, it checks
that the arrays are not null and are of matching
lengths. Then, it creates a DefaultFlexiRangeModel and
sets its ranges. Then, it defensively copies the icons and text
arrays, and finally calls the updateUI method that
installs and initializes the look-and-feel delegate. The additional
APIs that are implemented in this class are:

    publicint getControlPointCount();
    
    publicIcon getControlPointIcon(int controlPointIndex);
    
    publicString getControlPointText(int controlPointIndex);

    publicFlexiRangeModel getModel();

    publicFlexiRangeModel.Value getValue();

    publicvoid setValue(FlexiRangeModel.Value value);

Note that the last two are simply syntactic sugar and pass the
calls to the underlying model. These are provided as convenience
methods only. The first three methods are mainly for the UI
delegate, but can be used by the application code as well.

UI Delegate

If the model interface is the most important part of writing a
custom control, the UI delegate is in most cases the most
difficult. The main question is: how do you write painting logic
that will produce consistent results under all (existing and
future) target look and feels? Sometimes, this will be impossible
to do without writing custom UI delegates for each one of the
target look and feels (as is done for many components in the
SwingX project). However,
in some cases you'll find that you can emulate the visual part of
your custom component by combining smaller building blocks that
reuse existing core components. In the latter case, the UI
delegates of the core components will take care of platform/LAF-specific settings such as colors, fonts, and anti-aliasing.

The "
Enhancing Swing Applications
" article describes the boilerplate
code that you should put in the basic implementation of the UI delegate
so that it can be easily extended by custom look and feels. Start
off by creating the install* and
uninstall* methods, even if you're not planning to use
them; a third-party look and feel may decide to add some extra
functionality on top of your basic functionality (for example,
adding mouse-wheel scrolling of the slider).

Now, in our specific example, we can see that the target custom
component contains a slider and a collection of labels (with icons
and text). Since every JComponent is also a
Container, we can easily emulate the visual appearance
of the target component by adding a JSlider and
JLabels (one for each control point) in our
installComponents (don't forget to remove them in the
uninstallComponents). By reusing core Swing
components, we ensure that the visual appearance of our custom
component under both core and third-party LAFs will be consistent
with the rest of the application.

Since we are adding sub-components, we'll need to implement a
custom LayoutManager that will position them on
creation and resizing. This is a fairly straightforward (and a little
tedious) task: the discrete ranges result in labels being placed
right next to each other, while contiguous ranges take the extra
vertical space according to their relative weight. The slider
itself takes the entire vertical space and thus is aligned with the
first and the last control points.

Note that the alternative implementation (when there is no
possible way to reuse existing components) would be much more
difficult and perhaps next to impossible to do with a single UI
delegate. For example, some core LAFs use native APIs to draw the
respective controls (such as slider track and slider thumb), while
some third-party LAFs may not respect the UIManager
settings and provide hard-coded colors and custom theming APIs.

Going back to our implementation (which uses
JSlider), we are facing an interesting problem: a core
slider can be either discrete (snapToTicks or not). The snapping behavior is controlled
inside a mouse motion listener installed in the
BasicSliderUI delegate. What can we do? One option
would be to remove this listener and install our own, while another
would be to provide a custom implementation of
BoundedRangeModel that changes the value when it's set
in the discrete range. The first approach is not the best one--you can't rely on what is done in the SliderUI
delegate of the specific (core or third-party) LAF, as the specific
implementation may not call the super code at all. The
second approach is much better, but we have decided to implement
yet another approach for the reason described below.

Our implementation treats the sub-component slider as a cell
renderer
, just as with lists, trees, and tables. The slider is
used only for rendering and does not get any events at all (see

CellRendererPane
 for more details). This allows us to benefit
from LAF-consistent painting and providing custom handling
of mouse events. In our specific case, if the user clicks the mouse
outside the slider thumb, instead of block scrolling towards the
mouse click, the matching range value is set directly. This is why
we did not use the second approach outlined above: our custom mouse
listener translates the mouse click correctly for both contiguous
and discrete ranges and sets the value. Since this is the only
listener installed on the component (the slider is "rubber stamp"
only), we can be sure that no other listener (unless explicitly set
in a third-party UI delegate) will interfere with our code.

The resulting layout is shown in Figure 2. The blue outlines
represent the bounds of the control point labels, while the red
outline represents the bounds of the cell renderer pane:

Component layout
Figure 2. Component layout

Since we are using the cell renderer pane, we need to override
the paint method and paint the actual slider. Note
that we don't paint the control point labels explicitly since they
are "real" children of our component. In addition, note that the
slider painting is done in a separate protected
method. This allows third-party LAFs to replace the slider painting
without changing the entire painting logic.

    @Override
    publicvoid paint(Graphics g,JComponent c){
        super.paint(g, c);
        this.paintSlider(g);
    }

    protectedvoid paintSlider(Graphics g){
        Rectangle sliderBounds = sliderRendererPane.getBounds();
        this.sliderRendererPane.paintComponent(g,this.slider,
                this.flexiSlider, sliderBounds.x, sliderBounds.y,
                sliderBounds.width, sliderBounds.height,true);
    }

Test Application

Now that we have a fully functioning custom slider, it's time to
test it. The test application creates a slider with few discrete
and contiguous ranges and registers a change listener on this
slider. On a change event, we compute the scale size for painting an
icon (the icon is converted from the Tango Desktop
Project
 icons using the SVG-to-Java2D converter described in
the "
Transcoding SVG to Pure Java2D code
" entry). Figure 3 shows the
application under different icon sizes:

Custom slider with different values selected
Figure 3. Custom slider with different values selected

Figure 4 shows the same slider under different look and feels.
From left to right, the LAFs are: Windows (core), Metal (core),
Motif (core), Liquid (third party), and Napkin (third party). As you can
see, the new component provides an appearance consistent with the set
LAF:

Custom slider under different look and feels
Figure 4. Custom slider under different look and feels

Conclusion

Where to go now? Read the code for core Swing components,
download and study the code for open source components such as
SwingX or Flamingo, and start hacking away
on that dream component of yours.

Resources

 

 width="1" height="1" border="0" alt=" " />
Kirill Grouchnikov has been writing software since he was in junior high school, and after finishing his BSc in computer science, he happily continues doing it for a living. His main fields of interest are desktop applications, imaging algorithms, and advanced UI technologies.
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics