级别:中级
Adrian Van Emmenis([email protected])
独立顾问
2003 年 4 月
简介
开放源码 Eclipse 项目是 Java 领域中最有趣的新近开发项目之一。Eclipse 把自己描述成“一种通用的工具平台 — 开放的可扩展 IDE,可用于任何用途且没有特殊之处”。关于 Eclipse 的介绍,请参阅 developerWorks 文章“Getting started with the Eclipse Platform”。
它的两个主要组件是名为 SWT 的图形库和与其匹配的名为 JFace 的实用程序框架。在本文中,我将集中讨论这些组件。Eclipse 网站(请参阅本文后面的参考资料)中的 Eclipse 技术概述(Eclipse Technical Overview)对这两个组件的描述如下:
SWT 是一个窗口构件集和图形库,它集成于本机窗口系统但有独立于 OS 的 API。 JFace 是用 SWT 实现的 UI 工具箱,它简化了常见的 UI 编程任务。JFace 在其 API 和实现方面都是独立于窗口系统的,它旨在使用 SWT 而不隐藏它。图 1 演示了 Eclipse、JFace 和 SWT 之间的关系。
图 1. Eclipse Workbench、JFace 和 SWT
已发表的关于 JFace 和 SWT 的大多数文章(到目前为止)就如何在较大型 Eclipse 框架的环境中使用它们进行了讨论。在本文中,我打算采取另一种方法。我将向您演示如何在独立的 Java 程序中使用 JFace 和 SWT。
我选择的示例是文件资源管理器。我们实际上不会实现很多真正的功能,但我们将使用足够多的 GUI,以使您了解如何构建一个功能完整的程序。
安装说明
您可以下载本文中示例的源代码,但要把我的系统设置考虑在内:
随后的所有调整名称和文件分隔符的工作留给您完成,以便程序能在您的系统上正确地运行。
构建/运行指导
您需要将以下 jar 文件置于类路径(class path)上:
C:\eclipse-2.1.0\plugins\org.eclipse.jface_2.1.0\jface.jar
C:\eclipse-2.1.0\plugins\org.eclipse.runtime_2.1.0\runtime.jar
C:\eclipse-2.1.0\plugins\org.eclipse.swt.win32_2.1.0\ws\win32\swt.jar
C:\eclipse-2.1.0\plugins\org.eclipse.ui.workbench_2.1.0\workbench.jar
C:\eclipse-2.1.0\plugins\org.eclipse.core.runtime_2.1.0\runtime.jar
为确保 Java VM 获得您在运行时使用的 GUI 的正确共享库,请结合以下参数运行它:
-Djava.library.path=C:\eclipse-2.1.0\plugins\org.eclipse.swt.win32_2.1.0\os\win32\x86\
最后,请在包含 icons 文件夹的文件夹中运行程序,这样示例就可以找到包含图标的 gif 文件。
Hello, World
让我们从我能想到的最简单的 JFace 程序开始,逐步扩充它,将其构建为最常见的“Hello, World”程序。
import org.eclipse.jface.window.*; import org.eclipse.swt.widgets.*; public class Hello { public static void main(String[] args) { ApplicationWindow w = new ApplicationWindow(null); w.setBlockOnOpen(true); w.open(); Display.getCurrent().dispose(); } }
这里我们创建了一个名为 Hello 的类,其中 main 方法仅仅创建了一个 ApplicationWindow,然后打开它。setBlockOnOpen() 使 open() 阻塞,直到窗口关闭为止。
在窗口已关闭之后,我们获取当前的 Display 并除去它。这会释放在操作系统中用到的资源(以后我将讨论为什么这样做总是良好的习惯做法)。
当您运行该程序时,您会看到类似图 2 的窗口:
图 2. Hello(版本 2)
就是如此。它甚至没有说“Hello, World”。在修正它之前,让我们把话题转到 JFace 窗口。
JFace 应用程序窗口
窗口是顶级窗口(换句话说,由 OS 窗口管理器管理的窗口)的 JFace 类。JFace 窗口实际上不是顶级窗口的 GUI 对象(SWT 已经提供了一个,名为 Shell)。相反,JFace 窗口是助手对象,它知道对应的 SWT Shell 对象,并提供代码来帮助创建/编辑它,以及侦听它的事件等。图 3 演示了您的代码、JFace 和 SWT 之间的关系。
图 3. 您的代码、JFace Window 和 SWT Shell 之间的关系
事实上,这一模型是理解 JFace 如何工作的关键。它并不真的是 SWT 之上的层,而且它没有试图向您隐藏 SWT。相反,JFace 意识到有几种使用 SWT 的常用模式,而且它提供了一些实用程序代码,以帮助您更方便地对这些模式编程。
为了做到这一点,JFace 提供可使用的对象,或提供可将其子类化的类(有时它两者都提供)。
尽管我们仅仅直接使用了一个 ApplicationWindow,但实际上它们被设计为可以子类化也可以加入特定行为。它们有现成的菜单栏、工具栏、供您插入特定于应用程序的内容的区域和状态栏 — 全都是可选的。图 4 用 JFace File Explorer 示例本身演示了这些区域。
图 4. 应用程序窗口的各个部分
让我们改进 Hello,使它成为 ApplicationWindow 的子类。更改的行在清单 2 中突出显示。
清单 2. Hello(版本 2)import org.eclipse.jface.window.*; import org.eclipse.swt.widgets.*; public class Hello extends ApplicationWindow { public Hello() { super(null); } public static void main(String[] args) { Hello w = new Hello(); w.setBlockOnOpen(true); w.open(); Display.getCurrent().dispose(); } }
您编写的构造函数必须调用超类构造函数(如往常一样)。让我们暂时不考虑该构造函数的参数。
运行该程序的结果与前一个程序没有任何不同。缺省情况下,程序不会为我们显示任何装饰性的东西。
我们的程序要创建一个带有文本“Hello, World”的按钮。这个按钮要显示在内容(Contents)区域。要做到这一点,我们必须实现 Control createContents(Composite parent) 方法。
ApplicationWindow 将在所有其它窗口构件已经创建之后但窗口在屏幕上显示之前调用该方法。
参数 parent 是代表内容区域的复合窗口构件。
这里的想法是您创建一个复合窗口构件,将其添加到 parent,然后添加您的窗口构件,并返回您创建的复合窗口构件。图 5 演示了实例层次结构。
图 5. Application Window 的实例层次结构
我们的内容目前非常简单:parent 下的单一按钮,如清单 3 所示。
清单 3. Hello(版本 3)import org.eclipse.jface.window.*; import org.eclipse.swt.*; import org.eclipse.swt.widgets.*; public class Hello extends ApplicationWindow { public Hello() { super(null); } protected Control createContents(Composite parent) { Button b = new Button(parent, SWT.PUSH); b.setText("Hello World"); return b; } public static void main(String[] args) { Hello w = new Hello(); w.setBlockOnOpen(true); w.open(); Display.getCurrent().dispose(); } }
结果是图 6。
图 6. Hello(版本 3)
这就是我们要实现的。我们使用 JFace 创建的第一个“Hello, World”程序:包含单一按钮的窗口。
现在让我们继续讨论文件资源管理器这一话题。首先,我们将创建显示文件夹层次结构的树查看器。
使用 TreeViewer
和 ApplicationWindow 一样,TreeViewer 不是真正的 SWT 窗口构件,它也没有打算向您隐藏 SWT 窗口构件。它使用 SWT 树窗口构件来显示各项,并且还使用许多其它对象来协助它。
不象 ApplicationWindow,JFace TreeViewer 并不旨在被子类化。
这里的想法是 TreeViewer 知道要显示的树的根元素。当然,您必须告诉它那个对象是什么:TreeViewer: void setInput(Object rootElement)
为了开始显示,树查看器向根元素请求子元素并显示它们。然后,当用户展开其中的一个子元素时,树查看器向该节点请求子元素,以此类推。实际上,并不完全是那样。TreeViewer 并不直接使用域对象 — 而是使用另一个名为 ContentProvider 的对象,这个对象才使用域对象,如图 7 所示。
图 7. TreeViewer、ContentProvider 和域对象
当然,您必须实现 ContentProvider。对于 TreeViewer,您的类必须实现 ITreeContentProvider 接口。
实现 TreeContentProvider
有六个方法需要实现。实际上我们不用做全部的工作,只需实现其中的三个就行,因此,本着“即时满意(instant gratification)”的精神,让我们暂时只考虑那几个方法吧。
下面的代码演示了树查看器如何向内容提供程序请求正好位于根元素下的顶级元素:
ITreeContentProvider: public Object[] getElements(Object element)
随后,每当它需要特定元素的子元素时,它就使用以下方法:
ITreeContentProvider: public Object[] getChildren(Object element)
为了知道某个节点是否有子元素(有的话会将小加号放到它旁边),树查看器只需请求该节点的子元素,然后会询问有多少子元素。万一您的代码需要更快捷的方法来做到这一点,则您必须实现另一个方法:
public boolean hasChildren(Object element)
正如您所见,内容提供程序不持有对任何域对象的引用。持有对这些域对象的引用的是树查看器本身,它把这些域对象作为参数传递给内容提供程序中的各个方法。
在我们的例子中,节点是 File 对象。为获取子元素,我们使用 listFiles()。我们必须记得要检查 listFiles() 是否返回 null,然后使其变成空数组。
为了获取顶级元素(正好位于根元素之下),我们只需重用 getChildren() 方法。
getParent() 方法被用来实现 reveal(Object element) 方法,后者使树查看器滚动其 SWT 树窗口构件,以便显示树中特定的节点。问题是:如果此刻实际上并没有显示那个节点,那么应该在哪里显示它?JFace 会寻找其父元素,以及父元素的父元素等等,直到它达到已显示的节点,然后它再次回头寻找,直到目标节点已显示。
hasChildren() 方法只是做了显而易见(未优化)的事情,最后我们有了清单 4 中所示的代码。
清单 4. FileTreeContentProvider(版本 1)import java.io.*; import java.util.*; import org.eclipse.jface.viewers.*; public class FileTreeContentProvider implements ITreeContentProvider { public Object[] getChildren(Object element) { Object[] kids = ((File) element).listFiles(); return kids == null ? new Object[0] : kids; } public Object[] getElements(Object element) { return getChildren(element); } public boolean hasChildren(Object element) { return getChildren(element).length > 0; } public Object getParent(Object element) { return ((File)element).getParent(); } public void dispose() { } public void inputChanged(Viewer viewer, Object old_input, Object new_input) { } }
实现顶级 Explorer 类
我们将采用 Hello, World 程序,更改其名称,然后用 createContents() 方法创建 TreeViewer(而不是创建一个按钮),将其内容提供程序设置为我们的文件树内容提供程序,然后将输入设置到某个文件夹。在这个例子中,我选择的文件夹是 C: 驱动器中的顶级文件夹。
注:需要从 createContents() 返回 SWT 窗口构件。正如前面提到的,JFace Tree Viewer 不是 SWT 窗口构件,因此我们不能将它返回。我们需要从树查看器获取真正的窗口构件。我们通过使用 getTree() 做到这一点。
我们的主窗口类现在看起来与下面相似:
清单 5. Explorer(版本 1)import java.io.*; import org.eclipse.jface.viewers.*; import org.eclipse.jface.window.*; import org.eclipse.swt.*; import org.eclipse.swt.widgets.*; public class Explorer extends ApplicationWindow { public Explorer() { super(null); } protected Control createContents(Composite parent) { TreeViewer tv = new TreeViewer(parent); tv.setContentProvider(new FileTreeContentProvider()); tv.setInput(new File("C:\\")); return tv.getTree(); } public static void main(String[] args) { Explorer w = new Explorer(); w.setBlockOnOpen(true); w.open(); Display.getCurrent().dispose(); } }
运行该程序,您将看到与图 8 相似的结果。
图 8. Explorer(版本 1)
除了样板文件代码,我们只需向 Hello, World 程序添加 9 行代码就可做到这一点。
正如您可能猜想的那样,程序用 File 的 toString() 方法来显示这些文件,这不是我们真正想要的。要改变这一点,我们需要提供一个标签提供程序。
实现标签提供程序
正如有一个内容提供程序对象可用来获取树节点的子元素一样,当需要实际显示这些节点时,树查看器有另一个助手对象:标签提供程序。和前面一样,我们需要设置它:
public void setLabelProvider(IBaseLabelProvider labelProvider)
而且需要实现下面的方法以返回要为每个元素显示的文本:
public String getText(Object element)
如果我们将标签提供程序添加到树查看器图中,就会得到图 9。
图 9. 显示内容提供程序和标签提供程序的树查看器
我们可以实现接口 ILabelProvider,但将缺省实现 LabelProvider 子类化更容易(如果没有显式地设置标签提供程序,则使用的就是这个类)。
我们希望利用 getText() 做的事是返回文件名最后的部分 — 相对文件名而非 toString() 缺省使用的绝对文件名。图 6 演示了代码。
图 6. FileTreeLabelProvider(版本 1)import java.io.*; import org.eclipse.jface.viewers.*; public class FileTreeLabelProvider extends LabelProvider { public String getText(Object element) { return ((File) element).getName(); } }
而且我们必须记得使树查看器使用这个标签提供程序,如清单 7 所示。
清单 7. Explorer(版本 2)import java.io.*; import org.eclipse.jface.viewers.*; import org.eclipse.jface.window.*; import org.eclipse.swt.*; import org.eclipse.swt.widgets.*; public class Explorer extends ApplicationWindow { public Explorer() { super(null); } protected Control createContents(Composite parent) { TreeViewer tv = new TreeViewer(parent); tv.setContentProvider(new FileTreeContentProvider()); tv.setLabelProvider(new FileTreeLabelProvider()); tv.setInput(new File("C:\\")); return tv.getTree(); } public static void main(String[] args) { Explorer w = new Explorer(); w.setBlockOnOpen(true); w.open(); Display.getCurrent().dispose(); } }
这一次运行该程序时,我们会获得更清楚的视觉效果,如图 10 所示。
图 10. Explorer(版本 2)
我们现在要做的是将树查看器移到左边,将一个表查看器放在右边,以显示在树查看器中已选中的文件夹中的文件列表。
使用表查看器
为了处理表,JFace 有一个 TableViewer。和 TreeViewer 一样,它有输入(根对象)、内容提供程序和标签提供程序。它比树查看器简单,因为它不需要处理树。图 11 演示了内容提供程序和标签提供程序。
图 11. 显示内容提供程序和标签提供程序的表查看器
设置输入对象的方法与前面相同:
TableViewer: void setInput(Object rootElement)
实现文件表查看器内容提供程序
让我们考虑内容提供程序。这一次,根元素比树查看器根元素简单。表查看器仅仅期望根对象有许多子元素,因此要实现的唯一有趣方法是获取子元素的方法:
public Object[] getElements(Object rootElement)
要实现的接口是 IStructuredContentProvider。
根对象是一个文件夹;其子元素是该文件夹包含的文件/文件夹。因此我们的文件表内容提供程序类与清单 8 类似。
清单 8. FileTableContentProvider(版本 1)import java.io.*; import org.eclipse.jface.viewers.*; public class FileTableContentProvider implements IStructuredContentProvider { public Object[] getElements(Object element) { Object[] kids = null; kids = ((File) element).listFiles(); return kids == null ? new Object[0] : kids; } public void dispose() { } public void inputChanged(Viewer viewer, Object old_object, Object new_object) { } }
因此我们现在有两个查看器:树查看器和表查看器。为了将它们相邻地安置在一起,我们创建了 SWT SashForm 窗口构件。该窗口构件用一个用户可以调节的边框分隔其子元素。然后,我们将树和表添加到框格表单(sash form)(图 12)。
图 12. 包含树查看器和表查看器的框格表单
接下来要做的是使表查看器查看用户在树查看器中选中的每个文件夹。要做到这一点,我们必须侦听事件。
侦听事件
当用户在树查看器中选中一项时,树查看器发出 SelectionChangedEvent 事件。我们需要侦听该事件,当发出该事件时,需要将表的输入设置为树查看器中当前选中的文件。
为了侦听来自树查看器的选择更改事件,我们使用下面的方法:
public void addSelectionChangedListener(ISelectionChangedListener listener)
当用户选中/取消选中树查看器中的节点时,用下面的方法调用选择更改侦听器:
public void selectionChanged(SelectionChangedEvent event)
为了实现该侦听器类,我们将在主资源管理器窗口中编码一个匿名类。在 selectionChanged() 方法中,我们将需要获得刚选中的对象,并使其成为表查看器的输入。将所有的工作组合在一起,就得到了清单 9。
清单 9. Explorer(版本 3)import java.io.*; import org.eclipse.jface.viewers.*; import org.eclipse.jface.window.*; import org.eclipse.swt.*; import org.eclipse.swt.custom.*; import org.eclipse.swt.widgets.*; public class Explorer extends ApplicationWindow { public Explorer() { super(null); } protected Control createContents(Composite parent) { SashForm sash_form = new SashForm(parent, SWT.HORIZONTAL | SWT.NULL); TreeViewer tv = new TreeViewer(sash_form); tv.setContentProvider(new FileTreeContentProvider()); tv.setLabelProvider(new FileTreeLabelProvider()); tv.setInput(new File("C:\\")); final TableViewer tbv = new TableViewer(sash_form, SWT.BORDER); tbv.setContentProvider(new FileTableContentProvider()); tv.addSelectionChangedListener(new ISelectionChangedListener() { public void selectionChanged(SelectionChangedEvent event) { IStructuredSelection selection = (IStructuredSelection) event.getSelection(); Object selected_file = selection.getFirstElement(); tbv.setInput(selected_file); } }); return sash_form; } public static void main(String[] args) { Explorer w = new Explorer(); w.setBlockOnOpen(true); w.open(); Display.getCurrent().dispose(); } }
如果运行该程序,就会得到类似图 13 的结果。
图 13. Explorer(版本 3)
和树查看器一样,如果不显式地给表查看器设置标签提供程序,它就会使用缺省的标签提供程序。这就是这里发生的情况 — 如果您还记得的话,缺省行为是显示由元素的 toString() 方法返回的字符串,它正好是绝对文件名。
让我们实现自己的表标签提供程序。
实现文件表标签提供程序
现在只需考虑一个方法:
public String getColumnText(Object element, int column)
这里有两个参数:要获取其标签的元素和列索引(从 0 开始)。
该方法的实现相当简单 — 如果我们不考虑列索引参数的话,如清单 10 所示。
清单 10. FileTableLabelProvider(版本 1)import java.io.*; import org.eclipse.jface.viewers.*; import org.eclipse.swt.graphics.*; public class FileTableLabelProvider implements ITableLabelProvider { public String getColumnText(Object obj, int i) { return ((File) obj).getName(); } public void addListener(ILabelProviderListener ilabelproviderlistener) { } public void dispose() { } public boolean isLabelProperty(Object obj, String s) { return false; } public void removeListener(ILabelProviderListener ilabelproviderlistener) { } public Image getColumnImage(Object arg0, int arg1) { return null; } }
要配置表使其包含一列且列首标签为“Name”,必须从表查看器抽取表窗口构件,将表的列这一窗口构件作为表的子元素来创建,并给它设置一些属性,如清单 11 中所示。
清单 11. Explorer(版本 4)import java.io.*; import org.eclipse.jface.viewers.*; import org.eclipse.jface.window.*; import org.eclipse.swt.*; import org.eclipse.swt.custom.*; import org.eclipse.swt.widgets.*; public class Explorer extends ApplicationWindow { public Explorer() { super(null); } protected Control createContents(Composite parent) { SashForm sash_form = new SashForm(parent, SWT.HORIZONTAL | SWT.NULL); TreeViewer tv = new TreeViewer(sash_form); tv.setContentProvider(new FileTreeContentProvider()); tv.setLabelProvider(new FileTreeLabelProvider()); tv.setInput(new File("C:\\")); final TableViewer tbv = new TableViewer(sash_form, SWT.BORDER); tbv.setContentProvider(new FileTableContentProvider()); tbv.setLabelProvider(new FileTableLabelProvider()); TableColumn column = new TableColumn(tbv.getTable(), SWT.LEFT); column.setText("Name"); column.setWidth(200); tbv.getTable().setHeaderVisible(true); tv.addSelectionChangedListener(new ISelectionChangedListener() { public void selectionChanged(SelectionChangedEvent event) { IStructuredSelection selection = (IStructuredSelection) event.getSelection(); Object selected_file = selection.getFirstElement(); tbv.setInput(selected_file); } }); return sash_form; } public static void main(String[] args) { Explorer w = new Explorer(); w.setBlockOnOpen(true); w.open(); Display.getCurrent().dispose(); } }
运行该程序,我们应得到类似图 14 的结果。
图 14. Explorer(版本 4)
结束语
我们在很短的时间里完成了非常多的 JFace 编码工作。我们已经使用了一个应用程序窗口和两个查看器(树和表),并实现了它们的内容和标签提供程序。我们已经使用的 SWT 窗口构件有:Button、SashForm、Table 和 TableColumn,而且实现了一个事件侦听器。
但本文也有一些不完善的地方。我们没有考虑内容/标签提供程序中的一些方法,树查看器既显示文件又显示文件夹,没有显示图标,而且没有涉及菜单栏、工具栏、状态栏或任何弹出菜单。
在下一篇文章中,我们将整理内容/标签提供程序,并对查看器进行排序和过滤。我们将给窗口添加状态栏,给两个查看器都添加图标,并了解 JFace 图像注册表(image registry)。
参考资料
下载本文中示例的源代码。本文地址:http://com.8s8s.com/it/it15789.htm