如何编写自定义Swing组件(上)

本文纯属金山词霸产物,如果看不懂请参考英文原文

How to Write a Custom Swing Component


如何编写自定义Swing组件
by Kirill Grouchnikov
02/22/2007

当提到比较awt组件和swing组件的区别时, 首先被提到的就是Swing 是轻量级的(lightweight).确切的说其按钮、框架和菜单都没有使用本地化控制(native controls).所有组件包括渲染和事件处理都是靠纯java控制的。这给我们提供了很多方法去创建真正与平台无关的组件,而创建一个在所有平台上外观一致的自定义组件并非一件简单的事,这篇文章将演示如何创建自定义组件的过程并高亮显示重点、步骤和易犯的错误。

基础部分
Swing architecture overview这篇文章提供了非常优秀的swing结构和开发的高级概述(high-level overview)。虽然创建组件要遵循一些规则会略微有点麻烦,不过最终代码会更容易理解。它遵循”不重复发明轮子”的原则。最初你会想要把所有的东西都集中到一个类里,包括扩展API,模型处理(状态和通知),事务处理,布局和绘制。但是按照MVC (model-view-controller)结构将其划分为多个类可以让你的组件代码更容易理解,并且从长远来说更加容易扩展。
所有核心Swing组件的主要部分如下:

  • 组件(component)类本身,他负责提供创建、修改和查询组件状态的API
  • 模型接口和和模型接口的默认实现,它负责处理组件的业务逻辑和组件改变通知
  • UI delegate 负责处理组件布局,事件处理(鼠标和键盘事件)以及组件的绘制。

本文将配图展示创建一个自定义组件,类似WINDOWS Vista Explorer 中新的 view slider (如图1)。这个组件按看上去很像一个滑标嵌入一个pop-up menu。但他和常规的JSlider又有所不同,首先,它会含有关联标签(labels)和图标(icon)的选项(control points),其次,若range是相邻的,(如Small Icons和Medium Icons),能够动态的修改图标大小,若range是非关联的(如Tiles-Details),滑块只能滑动到这些选项上,不能滑动到这些选项之间的位置。


 
图1:Windows Vista OS 中的 View slider


组件类:UI Delegate 装配 
自定义组件的第一个类就是组件本身的API,这个API足够简单并且委托大部分业务逻辑给模型(参考下一章),除此之外,为了设置合适的UI delegate,你需要增加一个样板(boilerplate)(详细介绍请参考  Enhancing Swing Applications 一文),最终,你的代码应该是类似这样的:

    private static final String uiClassID = "FlexiSliderUI"; 

    public void setUI(FlexiSliderUI ui) { 
        super.setUI(ui); 
    } 

    public void updateUI() { 
        if (UIManager.get(getUIClassID()) != null) { 
            setUI((FlexiSliderUI) UIManager.getUI(this)); 
        } else { 
            setUI(new BasicFlexiSliderUI()); 
        } 
    } 

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

    public String getUIClassID() { 
        return uiClassID; 
    } 

 
这里需要注意的一点是:你需要提供一个可靠的UI delegate,如果当前安装的look and feel 没有提供特殊的UI delegate时,这个UI delegate将处理组件的绘制,布局和事件处理。


模型接口
这可能是这个组件最重要的接口了。它将从业务层面表现的你的组件功能。模型接口不要包含任何和界面绘制相关的方法(像setFont或getPreferredSize)。我们的组件将遵循LinearGradientPaint API并且定义模型为一些range序列: 

   public static class Range { 
        private boolean isDiscrete; 

        private double weight; 

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

模型中设置和查询range的API

    public void setRanges(Range... range); 

    public int getRangeCount(); 

    public Range getRange(int rangeIndex); 

 另外,模型还提供set和get当前值的API,这个Value类包含有Range对象

    public static class Value { 
        public Range range; 

        public double rangeFraction; 

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

 
这个模型还提供当前值对象的get和set方法:
模型接口的最后一部分为增加/移除变化监听器(ChangeListeners)的方法,他遵循swing核心组件的model接口风格(参考BoundedRangeModel);

    void addChangeListener(ChangeListener x); 

    void removeChangeListener(ChangeListener x); 

 

模型实现
模型的实现类非常简单,参考DefaultBoundedRangeModel,变化监听器(ChangeListeners)使用EventListenerList来保存。当模型值被改变时将触发ChangeEvent:

    protected void fireStateChanged() { 
        ChangeEvent event = new ChangeEvent(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); 
            } 
        } 
    } 

 以上为swing 核心组件源代码,我们从后向前检索所有listener.提取出stateChanged方法实现来执行。相关方法非常简单,检查值是否有效,并且复制slider ranges数组(之所以这样做是为了让那些恶意程序代码不能直接作用于model)

模型的单元测试
对UI组件进行单元测试是非常困难的,而模型必须被彻底地测试—记住,和损失模型的尾零(trailing zero)相比,损失一个像素并无大碍,使用一个简单的测试来确认你的默认模型实现的正确性:
(译注:尾零部分没有翻译,因为不熟,原文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)

    public void testSetValue2() { 
        FlexiRangeModel model = new DefaultFlexiRangeModel(); 
        FlexiRangeModel.Range range0 = new FlexiRangeModel.Range(true, 0.0); 
        model.setRanges(range0); 

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

组件类:API
回到组件类来,我们增加缺少的构造函数和模型的get方法.一些和 UI相关联的设置(如图标和标签文字)保存在组件类本身中(不属于模型)。组件的构造器类似LinearGradientPaint,当接收到null或者不一致的参数时将抛出异常

    public JFlexiSlider(Range[] ranges, Icon[] controlPointIcons, 
            String[] controlPointTexts) throws NullPointerException, 
            IllegalArgumentException 

这个实现非常简单,首先,检查3个数组非空并且数组长度符合要求,然后,创建DefaultFlexiRangeModel对象并且设置它的ranges.复制图标和文字数组,最后调用updateUI方法安装并且初始化look-and-feel delegate.这个类增加的API如下:

    public int getControlPointCount(); 
    
    public Icon getControlPointIcon(int controlPointIndex); 
    
    public String getControlPointText(int controlPointIndex); 

    public FlexiRangeModel getModel(); 

    public FlexiRangeModel.Value getValue(); 

    public void setValue(FlexiRangeModel.Value value); 

 
注意最后两个方法其实就是调用了模型中的相应方法,只是为了方便而已。前三个方法主要是提供给UI delegate,也可以被应用程序所使用。

 

待续...

 

你可能感兴趣的:(UI,windows,swing,单元测试,金山)