本文纯属金山词霸产物,如果看不懂请参考英文原文
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组件的主要部分如下:
本文将配图展示创建一个自定义组件,类似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,也可以被应用程序所使用。
待续...