SWT 和 JFace 系列的第 3 部分扩展了我们所学的关于使用 Java™ 技术、Eclipse 以及 SWT 和 JFace 库创建应用程序的知识。这一期将向您展示如何使用表树(tabular tree)、画布、样式文本、滑块、微调控制项(spinner)、刻度和其他控件,以及如何使用一些堆栈布局。
内容
在本系列的前两期中(请参阅 参考资料),我介绍了 Eclipse、Eclipse Standard Widget Toolkit (SWT) 和 JFace GUI 工具包,以构造 Eclipse 和单独的富 GUI(rich GUI)。还介绍了一些基本 GUI 控件和容器类型。然后展示了如何将这些控件组合到简单的工作应用程序中。我详细介绍了如何在一个菜单系统中提供这些应用程序。最后,通过创建一个使 GUI 开发变得更容易的方法的库,我演示了如何遵循最佳实践。
在这一期中,我将继续查看 org.eclipse.swt.custom
和 org.eclipse.swt.widgets
包中的各种小部件。除非另外注明,否则我所讨论的控件都位于 widgets
包中。
在 第 2 部分 中,我介绍了 Tree 和 Table 控件。SWT 支持这些控件的一个混合版本,在 custom
包中,这些控件被称为 TableTree。在 Eclipse V3.1 中,Tree 控件被增强为 TableTree 的一个功能替代,TableTree 则遭到反对。图 1 展示了一个表格式(TableTree 仿真模式)的示例 Tree。正如您可以看到的,树中的每一个项都被划分到列中。“第 2 部分”中展示了如何创建这些表和树,而创建一个表 Tree 实质上就是将这两项任务组合在一起,在这里我就不再重复它们。本文中包含的 Tree 示例将演示如何创建表 Tree。创建 TableTree 的代码与创建 Tree 的代码非常相似,因此,如果需要支持 Eclipse 的以前版本,那么可以使用 TableTree 控件。
本文的其余部分将展示如何使用许多新的 SWT 控件。我将在一个称为 TabFolder1App 的单个应用程序的上下文中做这一介绍。
回页首
本系列包含一些描述使用 Standard Widget Toolkit (SWT) 和 JFace 库来开发应用程序的基础文章,这些库是与基本的 Eclipse 软件开发工具包一起提供的。这一系列集中描述了如何使用 SWT 和 JFace 开发独立的应用程序。不过,您所学的大部分知识都可以应用到在 Eclipse 工作台上使用 SWT 当中。
我们将从具有有限功能的一些简单的 GUI 开始,然后朝着有用的应用程序扩展。我们将介绍大多数标准的、定制的 SWT 小部件和许多 JFace 特性。在我们的讨论中,至少包含一个使用该技术的例子。
本系列假定您熟悉 Java 编程语言和基于 Java 技术的开发,并且对 Java AWT 或 Swing GUI 工具包有一定的了解。
TabFolders 是一个创建使用有限数量空间的复杂 GUI 的简便方法。一个选项卡文件夹(tab folder)被分成一个或多个选项卡(tab),其中每个选项卡都是它本身的一个完整 GUI。一次只显示一个选项卡。在 custom
包中,CTabFolder 是 TabFolder 的增强版,它看起来更好一些,并且可以支持选项卡的关闭。
必须将 TabFolders 和 CTabFolders 定义为以下两个相互排斥的样式之一:
TOP —— 将选项卡放置在顶部。
BOTTOM —— 将选项卡放置在底部。
CTabFolder 支持其他一些可选样式:
FLAT —— 为文件夹提供一个扁平的外观。
BORDER —— 在控件的周围显示边界。
CLOSE —— 允许选项卡关闭(显示一个 Close 按钮)。
与包含一些项的 Trees 和 Tables 类似,TabFolders 也包含一些定义选项卡的 TabItems(或者 CTabItems)。TabFolders 还包含多个控件(通常是 Composites),每个控件都定义了选项卡的一个内容。TabItem.setControl
方法将该控件与相关的选项卡连接起来。
图 2 展示了一个示例 TabFolder,而图 3 展示了一个使用 CTabFolder 的类似 GUI。注意,选定的 Canvas 选项卡在 CTabFolder 上有一个 Close (X) 按钮。
与 第 2 部分 中介绍的方法一致,我将使用 protected
服务方法(位于超类 BasicApplication 中)来创建控件。清单 1 详细介绍了这些方法中的第一种方法,并展示了如何创建 TabFolders;也存在用于创建 CTabFolders 的类似代码。
protected TabFolder createTabFolder(Composite parent, int style) { return new TabFolder(parent, style); } protected TabItem createTabItem(TabFolder parent, int style, String text, Image icon, Control ctl) { TabItem ti = new TabItem(parent, style); if (text != null) { ti.setText(text); } if (icon != null) { ti.setImage(icon); } if (ctl != null) { ti.setControl(ctl); } return ti; } protected TabItem createTabItem(TabFolder parent, String text, Image icon, Control ctl) { return createTabItem(parent, SWT.NONE, text, icon, ctl); }
回页首
Canvas 是最基本的控件类型之一,可以用它来创建定制控件或绘图。图 2 和 图 3 展示了使用 Canvas 来绘制由重叠的矩形和椭圆形组成的图片的一个例子。在这幅绘画中,一些图片被填充,而其他一些则没有被填充。颜色、大小和位置的分配是随意的。
清单 2 展示了用于创建 Canvas 的代码。要实际地在 Canvas 上进行绘图,必须向该 Canvas 添加一个 PaintListener。每当 Canvas 需要重新绘制其客户机区域的任何部分时,都需要调用其 paintControl
方法。有两种绘制风格:
直接绘制 —— 很简单,但内容在整个重绘期间是不稳定的。
在进行绘制之前构建一个模型,然后再根据此模型进行重新绘制 —— 比较复杂,但很稳定。这通常是首选方法。
protected Canvas createCanvas(Composite parent, int style, PaintListener pl) { Canvas c = new Canvas(parent, style); if (pl != null) { c.addPaintListener(pl); } return c; } protected Canvas createCanvas(Composite parent, PaintListener pl) { return createCanvas(parent, SWT.NONE, pl); }
作为绘制风格 2 的一个例子,可以考虑一下清单 3 中定义的简单模型:
abstract protected class PaintItem { public Color color; public void paint(GC gc) { gc.setForeground(color); gc.setBackground(color); } } abstract protected class BaseRectItem extends PaintItem { public boolean fill; public Rectangle extent; } protected class ElipseItem extends BaseRectItem { public void paint(GC gc) { super.paint(gc); if (fill) { gc.fillOval(extent.x, extent.y, extent.width, extent.height); } else { gc.drawOval(extent.x, extent.y, extent.width, extent.height); } } } protected class RectangleItem extends BaseRectItem { public void paint(GC gc) { super.paint(gc); if (fill) { gc.fillRectangle(extent.x, extent.y, extent.width, extent.height); } else { gc.drawRectangle(extent.x, extent.y, extent.width, extent.height); } } }
这些绘制项都由 清单 4 中显示的 PaintListener 绘制。paintControl
方法是随在其上进行绘制的图形上下文(org.eclipse.swt.graphics
包中的 GC)一起提供的。您可以使用 GC 绘制文本和许多形状。此代码将重用通过 Display 类可用的标准系统颜色。由 Canvas 决定是否使用某种背景色填充其区域。gcObjects
集合包含所有需要绘制的 PaintItem 实例。数组 colorIds
是一个到选定的系统颜色的映射。
… new PaintListener() { public void paintControl(PaintEvent e) { GC gc = e.gc; gc.setBackground(canvas.getDisplay(). getSystemColor(colorIds[0])); Point cext = canvas.getSize(); gc.fillRectangle(0, 0, cext.x, cext.y); for (Iterator i = gcObjects.iterator(); i.hasNext();) { PaintItem pi = (PaintItem)i.next(); pi.paint(gc); } } }… protected static int[] colorIds = { SWT.COLOR_WHITE, SWT.COLOR_BLUE, SWT.COLOR_CYAN, SWT.COLOR_GRAY, SWT.COLOR_GREEN, SWT.COLOR_MAGENTA, SWT.COLOR_RED, SWT.COLOR_YELLOW, SWT.COLOR_BLACK };
清单 5 中显示了一些代码,这些代码先清除绘画,然后创建由一组矩形和椭圆组成的绘画。通过 GUI 上的按钮可以激活此代码。
public void doClear() { gcObjects.clear(); canvas.redraw(); } public void doDraw() { gcObjects.clear(); Display display = drawButton.getDisplay(); // create a bunch of objects for (int i = 0; i < 50; i++) { if (i % 2 == 0) { RectangleItem ri = new RectangleItem(); ri.extent = new Rectangle(nextInt(500), nextInt(250), nextInt(500), nextInt(250)); ri.color = display. getSystemColor(colorIds[nextInt(colorIds.length)]); ri.fill = i % 3 == 0; gcObjects.add(ri); } else { ElipseItem ei = new ElipseItem(); ei.extent = new Rectangle(nextInt(500), nextInt(250), nextInt(500), nextInt(250)); ei.color = display. getSystemColor(colorIds[nextInt(colorIds.length)]); ei.fill = i % 5 == 0; gcObjects.add(ei); } } canvas.redraw(); }
回页首
SWT 支持几种输入离散值的方法。Scale 允许在(通常很小的)整数范围内挑选一个值。Slider 允许使用类似滚动条的方法在(可能很大的)整数范围内挑选一个值。Spinner 允许挑选(通过向前/撤退按钮)或键入一个(可能为小数的)数字。注意,Spinner 是 Eclipse V3.1 中的一个新特性。ProgressBar 类似于一个只输出的 Slider,因为可以用它来展示增量活动(进度)。
通常,这些控件允许您提供最小值、最大值和初始值。除了 ProgressBars 之外,这些控件还支持增量值和页面增量值,Sliders 还支持 thumb 宽度。图 4 展示了一个 GUI,它在控件组内包含一个 Slider、一个 Spinner 和一个 Scale,在这些控件的下方是一个 ProgressBar。紧贴在进度条上的是一个(居中的)Label,它展示了进度条的值。
必须将所有这些控件定义为以下两种相互排斥的样式之一:
HORIZONTAL —— 水平地布置控件。
VERTICAL —— 垂直地布置控件。
Spinners 支持其他一些可选样式:
WRAP —— 从高值向低值换行排列。
READ_ONLY —— 不允许键入输入值。
ProgressBars 支持其他一些可选样式:
SMOOTH —— 更新不是在截然不同的步骤中进行的。
INDETERMINATE —— 没有预先确定步骤数的范围;进度条只是在时间上重复。
要创建这些控件,可以使用清单 6-9 中所示的代码。正如在 第 2 部分 中所描述的,将通过 registerCallback
方法,使用 Java 反射将SelectionListeners 添加到这些控件中。每当控件的值发生更改时,都会调用此侦听器。
protected Slider createSlider(Composite parent, int style, int min, int current, int max, int inc, int pageinc, int thumb, String callback) { Slider s = new Slider(parent, style); if (min >= 0) { s.setMinimum(min); } if (max >= 0) { s.setMaximum(max); } if (current >= 0) { s.setSelection(current); } if (inc >= 1) { s.setIncrement(inc); } if (pageinc >= 1) { s.setPageIncrement(pageinc); } if (thumb >= 1) { s.setThumb(thumb); } if (callback != null) { registerCallback(s, this, callback); } return s; } protected Slider createVSlider(Composite parent, int min, int current, int max, int inc, int pageinc, int thumb, String callback) { return createSlider(parent, SWT.VERTICAL, min, current, max, inc, pageinc, thumb, callback); } protected Slider createHSlider(Composite parent, int min, int current, int max, int inc, int pageinc, int thumb, String callback) { return createSlider(parent, SWT.HORIZONTAL, min, current, max, inc, pageinc, thumb, callback); }
protected Spinner createSpinner(Composite parent, int style, int min, int current, int max, int inc, int pageinc, String callback) { Spinner s = new Spinner(parent, style); if (min >= 0) { s.setMinimum(min); } if (max >= 0) { s.setMaximum(max); } if (current >= 0) { s.setSelection(current); } if (inc >= 1) { s.setIncrement(inc); } if (pageinc >= 1) { s.setPageIncrement(pageinc); } if (callback != null) { registerCallback(s, this, callback); } return s; }
protected Scale createScale(Composite parent, int style, int min, int current, int max, int inc, int pageinc) { Scale s = new Scale(parent, style); if (min >= 0) { s.setMinimum(min); } if (max >= 0) { s.setMaximum(max); } if (current >= 0) { s.setSelection(current); } if (inc >= 1) { s.setIncrement(inc); } if (pageinc >= 1) { s.setPageIncrement(pageinc); } return s; } protected Scale createVScale(Composite parent, int min, int current, int max, int inc, int pageinc) { return createScale(parent, SWT.VERTICAL, min, current, max, inc, pageinc); } protected Scale createHScale(Composite parent, int min, int current, int max, int inc, int pageinc) { return createScale(parent, SWT.HORIZONTAL, min, current, max, inc, pageinc); }
protected ProgressBar createProgressBar(Composite parent, int style, int min, int current, int max) { ProgressBar pb = new ProgressBar(parent, style); if (min >= 0) { pb.setMinimum(min); } if (max >= 0) { pb.setMaximum(max); } if (current >= 0) { pb.setSelection(current); } return pb; } protected ProgressBar createVProgressBar(Composite parent, int min, int current, int max) { return createProgressBar(parent, SWT.VERTICAL, min, current, max); } protected ProgressBar createHProgressBar(Composite parent, int min, int current, int max) { return createProgressBar(parent, SWT.HORIZONTAL, min, current, max); }
您可以查询或设置这些控件的当前值。考虑一下清单 10 中定义的线程,该线程将更新 图 4 中的标签、进度条和滑块。此线程在选中“Automatic Update”按钮(即代码中的 modeButton)时启动。
protected class BarUpdater extends Thread { protected int delay; protected Display display; public BarUpdater(Display display) { this.display = display; } public void run() { try { while (true) { try { if (!display.isDisposed()) { display.syncExec(new Runnable() { public void run() { if (!modeButton.isDisposed() && !scale.isDisposed()) { delay = modeButton.getSelection() ? scale.getSelection() : -1; } } }); if (delay >= 0) { Thread.sleep(delay); if (!display.isDisposed()) { display.syncExec(new Runnable() { public void run() { if (!bar.isDisposed()) { int v = bar.getSelection() + 1; if (v > bar.getMaximum()) { v = bar.getMinimum(); } bar.setSelection(v); if (!slider.isDisposed()) { slider.setSelection(v); } if (!valueLabel.isDisposed()) { valueLabel.setText( Integer.toString(v)); } } } }); } } } Thread.sleep(100); } catch (InterruptedException ie) { } } } catch (Exception e) { e.printStackTrace(); } } }
注意,此代码小心地进行检查,看各种控件在使用之前是否已经就绪。在异步 GUI 操作中,这很关键。还会注意到,所有 GUI 访问都是在一个syncExec
(或其同类 asyncExec
)方法中进行的。每当在与创建 GUI 所在的线程不同的线程上访问 GUI 时,都需要这样做。
回页首
正如在 第 1 部分 中描述的那样,SWT 通过 Text 控件支持纯文本的输入和显示。对于更高级的文本表示形式,需要定义字体和颜色,因此可以使用 custom
包中的 StyledText 控件。StyledText 是可由许多 Eclipse 编辑器使用的控件。请考虑一下图 5 中所示的样式文本的示例。该文本包含不同的颜色和字体修饰,比如下划线、删除线、粗体和斜体。注意,删除线和下划线只在 Eclipse V3.1 上受到支持。
必须将 StyledText 定义为以下两种相互排斥的样式之一:
MULTI —— 显示多个行。
SINGLE —— 显示单个行。
StyledText 支持其他一些可选样式:
WRAP —— 从控件的右边换行。
READ_ONLY —— 不允许键入输入值。
清单 11 显示了用于创建 StyledText 的代码。清单 12 使用简单的类似 XML 的语言展示了它的用法,以定义具有这些属性的文本的范围。
protected StyledText createStyledText(Composite parent, int style) { return new StyledText(parent, style); }
styledText = createStyledText(body, SWT.MULTI | SWT.WRAP | SWT.FULL_SELECTION); styledText.addMouseListener(new MouseAdapter() { public void mouseDown(MouseEvent e) { if (e.button == 3) { processPopup(); } }}); TextContent tc = new TextContent(body.getDisplay()); tc.setContent(dummyContent); styledText.setText(tc.toPlainText()); styledText.setStyleRanges(tc.getStyleRanges()); : protected static final String dummyContent = "Just plain text!\n" + "Now is the time for <b>all</b> good men " + "to come to the aid of their country\n" + "<red><i>To <b>be</b></i> or <i>not to <b>be</b></i>?</red> " + "<blue><u>That</u> is the <so>question</so></blue>\n" + "That's all folks!\n";
StyledText 的例子使用了一个 helper 类 TextContent,以确定具有特殊属性的文本的范围。这个类包含在示例代码中,支持对文档进行分析并获得其纯文本内容,以及查找各种范围,其中的纯文本内容具有不同属性或属性组合。它创建了表示这些范围的 SWT 类 StyleRange 的一个数组。StyleRange 有一些字段,这些字段描述了这样的范围,该范围包括前景色和背景色,以及要应用的起始偏移量、长度和属性,比如字体样式(粗体或斜体)、删除线和下划线。
StyledText 所具有的功能比这里展示的还要多。例如,在将文本输入到文档中时,文本没有任何属性,即使是在具有属性的点输入它也是如此。您可以使用 StyledText 的特性来改正这一行为。
回页首
有时,当您希望能够将一个选择列表显示为一个弹出式菜单而不用创建一个弹出式菜单时,可以使用 PopupList 控件做到这一点。图 6 显示了如何使用此控件将选择、剪切、复制和粘贴功能添加到 图 5 的文本编辑器中。
清单 13 显示了 processPopup
方法的内容。注意,安置弹出式菜单的代码与它所涉及的控件(即样式文本)有关。
PopupList popup = createPopupList(shell, 50, popupItems); popup.select(popupItems[0]); Point p = styledText.getLocation(); p = shell.getDisplay().map(styledText, null, p.x, p.y); String choice = popup.open(new Rectangle( p.x + 100, p.y - 100, 100, 200)); if (choice != null) { if (popupItems[0].equals(choice)) { styledText.selectAll(); } else if (popupItems[1].equals(choice)) { styledText.cut(); } else if (popupItems[2].equals(choice)) { styledText.copy(); } else if (popupItems[3].equals(choice)) { styledText.paste(); } } : protected static final String[] popupItems = { "Select All", "Cut", "Copy", "Paste" };
回页首
在前两篇文章中,我讨论了随 SWT 一起提供的几个布局管理器,其中包括 FillLayout、GridLayout 和 FormLayout。custom
包提供了StackLayout,可以用它在一次只显示一个 GUI 的 TabFolder(有些类似于没有选项卡的 TabFolder)的顶部放置多个 GUI。考虑一下 图 7 和图 8,它们显示了一个显示带编号标签的堆栈布局的两种状态。通过“>>”按钮可以让堆栈前进,而通过“<<”按钮则可以让堆栈后退。图 8 显示了按下“>>”按钮 4 次后的布局。
此堆栈由清单 14 中的代码创建。
StackLayout stackLayout = new StackLayout(); Composite clabels = createComposite(body, SWT.BORDER, stackLayout); Label[] labels = new Label[5]; : for (int i = 0; i < labels.length; i++) { Label xlabel = new Label(clabels, SWT.CENTER); xlabel.setText("Stack " + i); labels[i] = xlabel; } stackLayout.topControl = labels[0]; : protected Composite createComposite(Composite parent, int style, Layout layout) { Composite c = new Composite(parent, style); if (layout != null) { c.setLayout(layout); } return c; } protected Composite createComposite(Composite parent, Layout layout) { return createComposite(parent, SWT.NONE, layout); }
清单 15 显示了通过“>>”按钮到达下一个堆栈的代码。对于“<<”按钮,代码与此类似。
protected int currentLabel; public void doNext() { ++currentLabel; if (currentLabel >= labels.length) { currentLabel = 0; } stackLayout.topControl = labels[currentLabel]; clabels.layout(); }
回页首
在 SWT 和 JFace 系列的第三期中,我介绍了更多的 SWT 控件,比如用于创建表树的 Tree;用于绘图的 Canvas;用于输入数字值的 Slider、Scale 和 Spinner;用于显示进度的 ProgressBar;用于输入具有某些属性的文本的 StyledText;以及用于简单动态菜单的 PopupList。我还展示了如何使用 StackLayout 创建时间合理的重叠 GUI。本系列的下一期将展示如何使用更多的 SWT 控件。