http://www.ibm.com/developerworks/cn/opensource/os-cn-swtmulti/index.html?ca=drs-cn-1022
SWT 是开发 Java Rich UI 的重要技术,但在很多实践中,SWT 定义的组件无法满足特定工程的需求。本文以支持多选的 Combo 为例,介绍了一个制作自定义 SWT 组件的全过程,该过程包括了开发一个自定义组件的需求搜集,设计,和实现。
问题的提出
SWT 是目前主流的 Java UI 开发工具库,也是 eclipse workbench 和 JFace 的基础。然而,在有些工程实践中,SWT 自带的组件无法满足某些特定的业务需求。解决这类问题我们有各种办法。比如自己扩展出自定义的组件,或者购买第三方组件直接使用,也可以通过一些特定的接口来集成其他类型的组件。在很多情况下,我们会选择第一种办法,也就是自己动手制作组件。
SWT Combo 是每个开发人员都非常常用的一个组件,但是它有一个明显的缺陷,那就是它不能支持同时选择下拉列表中的多个项目。如果碰到这种需求,通常的一个妥协的方法是通过一个复选框组来替代。但是这个替代方法占用的空间比较大,而且随着选项数量的变化,对于用户界面的影响也很大。在这篇文章里,笔者通过扩展自定义组件的方法成功得解决了这个问题。如图 1,这个自定义的 SWT 组件能够同时选择下拉列表中的多个项目。
图 1. 支持多选的 SWT Combo
如何扩展
通常而言,制作自定义的 SWT 组件有三种方式,
- 继承已有的 SWT 组件类型,并修改其表现形式;
- 组合多种已有的 SWT 组件来构建新的组件;
- 从操作系统的原生组件上进行制作,之后创建 SWT 组件来封装它。
由于 SWT Combo 本身的特点,本文将采用第二种方法来进行扩展。
搜集需求
由于这个目标组件,也就是支持多选的 SWT Combo,具有一些特定的表现形式,在动手之前,开发人员必须把要实现的每个功能点都了解清楚。以便为实现清晰的设计做好准备。经过仔细研究和分析,该组件应该具备如下的表现形式。
- 整个组件可以由 Text 和 List 两个部分组成
- 文本框受到鼠标动作的时候,下拉列表出现;
- 可以使用 CTRL 键来进行多选;
- 可以使用鼠标的滑动同时选中一个连续的列表;
- 当该下来列表失去焦点的时候,文本框获得文字;
- 文字表现为多个选中项目的文本的连接,用逗号分开;
- 下拉列表弹出的时候,需要高亮显示已被选中的文本。
设计
有了综合出来的需求,我们就可以根据需求来一步步地进行设计。由于我们将使用已有的 SWT 组件来构建新的组件,自然而然我们就会使用 SWT Text 和 SWT List 作为开发的基础。
由于 List 组件需要根据 Text 组件的位置动态的进行显示,所以它需要有一个 SWT Shell 作为容器来显示它。这个动态的 Shell 根据 Text 组件上的事件响应而显示,也根据其他的一些事件而销毁。在 Shell 销毁的时候,它所容纳的 List 组件也会自动销毁。对于 Text 组件,我们把它也封装在一个 Composite 当中,这样客户程序员只需要把这个组件当成一个普通的 SWT 组件来使用即可。
图 2 是该自定义组件的设计图。
图 2. 设计
在上图的设计中,文本框通过鼠标事件和 Float Shell 相联系,来实现需求 (2) 。在 List 上增加事件来实现需求 (3) (4) 。为了实现需求 (5),我们可以为 Float Shell 增加事件,并且实现需求 (6),将被选中的条目反映到 Text 的显示上。
所以,该设计中还必须有变量存储所有候选的 List 的条目 (Items),也应当有一个变量来记载被选中的项目的索引值 (Item Selection) 。
实现
实现步骤 1 – 建立框架
在弄清楚了需求和主要设计之外,我们可以开始着手实现的工作。为了能够让这个自定义组件和其他 SWT 组件一样方便的使用,我们将这个类定义成为 Composite 的子类。然后,我们需要为这个 composite 增加几个基本的变量,包括一个 SWT Text 对象,一个 Shell 对象和一个 SWT List 对象。除了这几个对象之外,还要增加一个 String 类型的数组表示所有可用的候选项目和
一个整形列表来代表被选中的项目的索引。
有了这些信息,我们就可以构造出如下的一个类,作为整个实现的基础。
清单 1. 实现基本的类框架
public class MultipleSelectionCombo extends Composite { Text _text=null; String[] _items=null; int[] _selection=null; Shell _floatShell = null; List _list = null; public MultipleSelectionCombo(Composite parent, String[] items, int[] selection, int style){ super(parent, style); _selection = selection; _items = items; init(); } private void init(){ GridLayout layout = new GridLayout(); layout.marginBottom =0; layout.marginTop = 0; layout.marginLeft =0; layout.marginRight = 0; layout.marginWidth = 0; layout.marginHeight = 0; setLayout(new GridLayout()); _text = new Text(this, SWT.BORDER | SWT.READ_ONLY); _text.setLayoutData(new GridData(GridData.FILL_BOTH)); } }
实现步骤 2 – 显示出 float Shell
在步骤 1 中,我们构造了框架,并且把 text 对象实例化在它父 composite 的之内。但是到目前为止,我们还没有对 Shell 对象和 List 对象做任何实质性的工作。根据需求 (2),我们需要为 text 对象增加事件处理以实例化并显示 shell 对象和 List 对象。代码如清单 2 。
需要注意的是,Shell 对象必须正好显示在 Text 对象的下方,为了得到相对于整个屏幕的绝对坐标,我们使用 toDisplay 这个方法来获得 Text 对象的绝对坐标,然后推算出 Shell 应该在紧贴着 Text 对象下方出现的位置。
另外,因为这个 shell 对象不同于一般的对话框,它没有边框,所以要使用 SWT.NO_TRIM 属性来构建。
清单 2. 显示 shell 和 list
private void init(){ ... ... ... ... _text.addMouseListener(new MouseAdapter() { public void mouseDown(MouseEvent event) { super.mouseDown(event); initFloatShell(); } }); } private void initFloatShell(){ Point p = _text.getParent().toDisplay(_text.getLocation()); Point size = _text.getSize(); Rectangle shellRect = new Rectangle(p.x, p.y + size.y, size.x, 0); _floatShell = new Shell(MultipleSelectionCombo.this.getShell(), SWT.NO_TRIM); GridLayout gl = new GridLayout(); gl.marginBottom = 2; gl.marginTop = 2; gl.marginRight = 2; gl.marginLeft = 2; gl.marginWidth = 0; gl.marginHeight = 0; _floatShell.setLayout(gl); _list = new List(_floatShell, SWT.BORDER | SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL); for (String value : _items) { _list.add(value); } GridData gd = new GridData(GridData.FILL_BOTH); _list.setLayoutData(gd); _floatShell.setSize(shellRect.width, 100); _floatShell.setLocation(shellRect.x, shellRect.y); _floatShell.open(); }
实现步骤 3 – 用 Ctrl 键实现多选
有了步骤 2,Shell 对象以及它内置的 List 对象就能顺利显示出来。之后我们来考虑需求 (3),也就是能够支持基于 Ctrl 的多选。也就是说,当没有按 Ctrl 键的情况下用鼠标在 List 对象上进行项目选择的时候,鼠标弹起的瞬间,Shell 和 List 就应该消失,表示选择已经完成。而当按下 Ctrl 键的情况,鼠标弹起的瞬间,Shell 和 List 应该继续显示,等待着下一个选择。
如下给 List 增加了一个鼠标事件就可以完成这个需求。
清单 3. 实现 list 上的 Ctrl 多选
private void initFloatShell(){ ... ... ... ... _floatShell.setSize(shellRect.width, 100); _floatShell.setLocation(shellRect.x, shellRect.y); _list.addMouseListener(new MouseAdapter() { public void mouseUp(MouseEvent event) { super.mouseUp(event); _selection = _list.getSelectionIndices(); if ((event.stateMask & SWT.CTRL) == 0) { _floatShell.dispose(); } } }); _floatShell.open(); }
同时考虑需求 (4),我们发现这是 List 对象默认的特性,当 List 对象是以 SWT.MULTI 的风格进行构造的话,鼠标以拖动方式进行选择的时候,就会自动选取若干个连续的候选项目。
实现步骤 4 – 当浮动窗口消失的时候
对于需求 (5),我们只需要简单的为 Shell 对象增加一个事件,就可以对它消失的行为进行控制。如下所示,当 Shell 消失的时候,_selection 对象就能够获得 List 对象中被选择中的项目索引。
清单 4. shell 的消失行为
private void initFloatShell(){ ... ... ... ... _floatShell.addShellListener(new ShellAdapter() { public void shellDeactivated(ShellEvent arg0) { if (_floatShell != null && !_floatShell.isDisposed()) { _selection = _list.getSelectionIndices(); _floatShell.dispose(); } } }); _floatShell.open(); }
实现步骤 5 – 文本框显示正确的选择
在以上四个步骤都完成之后,我们需要考虑需求 (5) 和 (6),也就是将被选中的项目反映到 Text 对象上去。这个本质上就是把 _selection 和 _items 结合起来产生一个字符串,并且设置到 Text 对象中作为当前的显示。
清单 5. 文本框内容显示
private void initFloatShell(){ ... ... ... ... _list.addMouseListener(new MouseAdapter() { public void mouseUp(MouseEvent event) { super.mouseUp(event); _selection = _list.getSelectionIndices(); if ((event.stateMask & SWT.CTRL) == 0) { _floatShell.dispose(); displayText(); } } }); ... ... ... ... _floatShell.addShellListener(new ShellAdapter() { public void shellDeactivated(ShellEvent arg0) { if (_floatShell != null && !_floatShell.isDisposed()) { _selection = _list.getSelectionIndices(); displayText(); _floatShell.dispose(); } } }); _floatShell.open(); } private void displayText(){ if(_selection!=null && _selection.length>0){ StringBuffer sb = new StringBuffer(); for(int i=0;i<_selection.length;i++){ if(i>0)sb.append(","); sb.append(_items[_selection[i]]); } _text.setText(sb.toString()); }else{ _text.setText(""); } }
实现步骤 6 – 高亮显示已经选择的项目
目前我们只剩下最后的一个步骤,那就是当 list 显示出来的时候,它要反映出当前已经选择的项目。这个也很好办,只要通过 List 对象的 setSelection 方法就能很容易的做到。
清单 6. list 高亮选择已被选中的项目
private void initFloatShell(){ ... ... ... ... for (String value : _items) { _list.add(value); } _list.setSelection(_selection); ... ... ... ... }
使用该组件
通过我们一系列的工作,MultipleSelectionCombo 已经能够和一个普通的 SWT 组件一样的被使用了。如下是一个简单的使用例子。
清单 7. 使用案例
MultipleSelectionCombo combo=new MultipleSelectionCombo(shell, new String[]{"item 1","item 2","item 3"}, new int[]{0,1}, SWT.NONE); combo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
总结
定制 SWT 组件是一件非常有趣的事情,开发人员可以通过自己的代码实现出不同的表现形式来满足实际工作的需要。本文通过一个实际的例子深入浅出的讲述了一种定制 SWT 组件的全过程。包括从需求的考虑,基本的设计到按步骤的实现过程。
通过这篇文章,笔者转达出来的想法包括
- 定制 SWT 组件本身并不是一个很困难的事情,正相反的是,有些时候,还非常的容易。
- 在确定开始做之前,必须要思考它特有的表现形式,这为创建出一个合适的设计会打下良好的基础。
- 在设计和实现过程中,要能够灵活运用 SWT 的事件和监听器来创造出特有的表现形式。
- 注意创建出来的资源,必须有销毁的工作,否则代码可能会出现性能方面的问题。