本文纯属金山词霸产物,如果看不懂请参考英文原文
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,也可以被应用程序所使用。
待续...