级别: 初级 Adrian Van Emmenis, 独立顾问
2003 年 6 月 09 日
在本文中,A. O. Van Emmenis 继续研究在第 1 部分中着手构建的示例。他将完善内容提供程序和标签提供程序,并演示如何在 JFace 查看器中使用排序和过滤。他将演示如何给窗口添加状态行,给两个查看器添加图标,并讨论如何通过使用 JFace 图像注册表来节省系统资源。
安装说明 在本系列的 第 1 部分中,我着手构建了一个示例,该示例将 JFace 应用程序窗口子类化并且使用树查看器和表查看器来显示文件夹和文件。这次,我们将完成上个示例的收尾工作,然后给窗口添加一条状态行。我们要做的较大更改是给两个查看器添加图标,并了解 JFace 图像注册表。最后,我们将对查看器进行排序和过滤,以获得一个更加真实的“文件资源管理器”样式。 安装说明 如果要下载本文中 示例的源代码,请注意我的系统设置:
- Windows 2000
- Eclipse,稳定构建版 M3(2002 年 11 月 15 日)
- Eclipse 安装在 C:/eclipse-2.1.0 中
请您自行完成随后所有调整名称和文件分隔符的工作,以便程序能在您的系统上正确地运行。 构建/运行指示信息 请确保以下 jar 文件位于类路径上:
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 的正确共享库,请使用以下参数运行 Java VM: -Djava.library.path=C:/eclipse-2.1.0/plugins/org.eclipse.swt.win32_2.1.0/os/win32/x86/ 最后,请从包含 icons文件夹的文件夹中运行这些程序,以便示例能找到包含图标的 gif 文件。
继续使用第 1 部分的示例 上一篇文章结束时,我们的资源管理器应用程序如图 1 所示: 图 1. 资源管理器(V4) 我们在最左边窗格中使用树查看器显示文件夹和文件。当在左边窗格中选中某个文件夹时,它所包含的文件就显示在最右边窗格的表查看器中。让我们通过设置窗口标题来着手完善这个示例。
设置窗口标题 这是 JFace 不试图向您隐藏 SWT 的又一个例子。必须获取底层 SWT Shell 窗口小部件并设置其标题。 我们使用 getShell() 向 JFace 窗口求取其 SWT shell,然后使用 setText() 设置 shell 的标题,于是,在资源管理器 createContents() 方法中,我们将使用如下语句: getShell().setText("JFace File Explorer"); 我们在表视图中有了一列。让我们添加另一个以字节为单位显示文件大小的列。我们将使该列中的文本向右对齐。它的代码与第一列类似,所以我不在这里显示。 既然有了两列,那就把表查看器的选择样式设置为 FULL ,表示选中某行后,整行都将被突出显示。 new TableViewer(sash_form, SWT.BORDER | SWT.FULL_SELECTION);
|
状态行 状态行实际上是由类 StatusLineManager 照管的 SWT 组合控件,该类可以包含其它控件。 通常这些控件都是显示状态信息的只读文本控件,但也可请求在那里显示临时进展监视器。 状态行知道要显示标准消息,并且有时要显示错误消息。 ApplicationWindow 方法 setMessage(String) 实际上只是 getStatusLineManager().setMessage(String) 的快捷方式。 | |
给应用程序窗口添加状态行 状态行是应用程序窗口的另一个可选组件。必须通过使用 addStatusLine() ,在窗口创建 SWT 窗口小部件之前要求窗口创建状态行。 让我们用状态行显示在表视图中选中了多少项。每当表视图中的选择发生改变时,我们就更新状态行并显示有多少项被选中,如清单 1 所示: 清单 1. 资源管理器 — 设置状态行中的文本
tbv.addSelectionChangedListener(new ISelectionChangedListener()
{
public void selectionChanged(SelectionChangedEvent event)
{
IStructuredSelection selection =
(IStructuredSelection) event.getSelection();
setStatus("Number of items selected is " + selection.size());
}
});
| 缺省情况下,表查看器被设置为单选方式。可以通过在创建表查看器时将多选方式作为样式参数的另一位(以文字表示)添加,将表查看器更改为多选方式。 new TableViewer(sash_form, SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI); 综合以上代码就得到清单 2: 清单 2. 资源管理器(V5)
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);
addStatusLine();
}
protected Control createContents(Composite parent)
{
getShell().setText("JFace File Explorer");
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 | SWT.FULL_SELECTION | SWT.MULTI);
tbv.setContentProvider(new FileTableContentProvider());
tbv.setLabelProvider(new FileTableLabelProvider());
TableColumn column = new TableColumn(tbv.getTable(), SWT.LEFT);
column.setText("Name");
column.setWidth(200);
column = new TableColumn(tbv.getTable(), SWT.RIGHT);
column.setText("Size");
column.setWidth(100);
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);
}
});
tbv.addSelectionChangedListener(new ISelectionChangedListener()
{
public void selectionChanged(SelectionChangedEvent event)
{
IStructuredSelection selection =
(IStructuredSelection) event.getSelection();
setStatus("Number of items selected is " + selection.size());
}
});
return sash_form;
}
public static void main(String[] args)
{
Explorer w = new Explorer();
w.setBlockOnOpen(true);
w.open();
Display.getCurrent().dispose();
}
}
| 运行该程序的结果如图 2 所示: 图 2. 资源管理器(V5) 如您所见,size 列目前不正确 — 我们马上就修正它。
图标和图像 在 SWT 中,用来表示图标的对象是 Image (有关类 Image 的详细信息,请参阅本文后面的 参考资料)。当直接创建可显示图像的 SWT 窗口小部件时,可使用 setImage(Image) 设置它。 关于图像有一个问题:它们是受限资源。图像是比较重量级的对象,具有对外部 OS 资源的引用(图 3)。在某些操作系统上,对任一时间内可拥有图像的数量有严格限制,因此使用它们时必须注意这一限制。 图 3. 操作系统中的图像描述符、图像和资源 最终,当应用程序退出时,会释放 Image 所用的资源 — 但即使那样,在某些操作系统上仍可能有问题:共享库造成资源被继续占用。出于各种原因,不能指望 Java 垃圾收集器会替您清理它们。有关标准窗口小部件工具箱(Standard Widget Toolkit)的完整说明,请参阅 参考资料。 有个好消息:图像 可以在窗口小部件之间共享,而且 SWT 为 Image 提供了 dispose() 方法,它可以释放图像所用的资源。 因为窗口小部件可以共享图像,所以 SWT 做出了这样的设计决定:当窗口小部件不再存在时(也就是说,在关闭窗口时),SWT 将不会自动清除这些图像。然而,JFace 会给予您一些帮助,它告诉您大型 UI 对象(如窗口和查看器)何时可以清除它们的图像,然后由您负责清除已创建的任何图像。 实际上,绝大多数应用程序都从文件或数据库获取其图标,这些文件或数据库包含一些标准格式(如 gif 或 jpeg)的数据。为此,JFace 提供了 ImageDescriptor (请参阅 参考资料),这是一个轻量级对象,它不存储图像本身,而是可以按需要创建特定的图像。这个类有许多可以从不同来源构造图像的子类。 在本例中,我们将图标存储在位于 icons 文件夹的 .gif 文件中,因此我们将在 ImageDescriptor 中使用这一工厂(factory)方法:
public static ImageDescriptor createFromURL(URL url) TableViewer 获取元素图标的方式和它获取元素文本的方式大体相同。它请求标签提供程序。我们必须做的就是从 ITableLabelProvider 实现该方法: public Image getColumnImage(Object element, int columnIndex) 为了便于我们管理图像的共享和清除,我们将使用 ImageRegistry (请参阅 参考资料)。
JFace 图像注册表 管理一组共享且昂贵的资源是经典的软件工程问题。JFace 提供经典的解决方案:一个能高速缓存图像和图像描述符的中央共享注册表。 其思想是:您的代码获取它需要的图像描述符,然后将其添加到图像注册表,并使用键为每个图像描述符建立索引。当希望获得图像时,就用它的键从注册表中访存。 当清除顶级显示(Display)时,图像注册表将负责清除它的图像。如果需要更频繁地清除图像,则可能要创建数个图像注册表,并按需要直接清除图像。要获得更多的详细信息,请参阅 Eclipse 网站上关于使用图像的文章(请参阅 参考资料)以获得链接。 我们的小示例将只使用少数几个图标,因此我们将使用单个图像注册表。让我们先看看表查看器。 给文件表查看器添加图标 要从名为 icons/file.gif 的文件创建图像描述符,可使用以下语句: image_descriptor = ImageDescriptor.createFromURL(new URL("icons/file.gif")); 获得图像描述符之后,可将它存储在图像注册表中。在本例中,我们使用字符串“file”作为键: image_registry.put("file", image_descriptor); 然后,要再次获取该图像,可使用以下语句: image = image_registry.get("file"); URL 的构造函数抛出一个已检查异常(checked exception)。我们要使用的所有 URL 都将是硬编码的,因此我们将把 URL 的创建包装在某个工具代码中,以将已检查异常转换为运行时异常。 另外,我们希望在代码中共享这些图像,因此需要集中创建图像注册表,并使它可被全局访问。 现在该创建一个实用程序类了,如清单 3 所示: 清单 3. Util(V1)
import java.net.*;
import org.eclipse.jface.resource.*;
public class Util
{
private static ImageRegistry image_registry;
public static URL newURL(String url_name)
{
try
{
return new URL(url_name);
}
catch (MalformedURLException e)
{
throw new RuntimeException("Malformed URL " + url_name, e);
}
}
public static ImageRegistry getImageRegistry()
{
if (image_registry == null)
{
image_registry = new ImageRegistry();
image_registry.put(
"folder",
ImageDescriptor.createFromURL(newURL("file:icons/folder.gif")));
image_registry.put(
"file",
ImageDescriptor.createFromURL(newURL("file:icons/file.gif")));
}
return image_registry;
}
}
| getImageRegistry() 方法简单地创建了一个图像注册表并添加了两个图像描述符。我们现在需要更改 FileTableLabelProvider 以根据元素是文件还是文件夹来返回正确的图像,如清单 4 所示: 清单 4. FileTableLabelProvider(V2)
import java.io.*;
import org.eclipse.jface.viewers.*;
import org.eclipse.swt.graphics.*;
public class FileTableLabelProvider implements ITableLabelProvider
{
public String getColumnText(Object element, int column_index)
{
if (column_index == 0)
{
return ((File) element).getName();
}
if (column_index == 1)
{
return "" + ((File) element).length();
}
return "";
}
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 element, int column_index)
{
if (column_index != 0)
{
return null;
}
if (((File) element).isDirectory())
{
return Util.getImageRegistry().get("folder");
}
else
{
return Util.getImageRegistry().get("file");
}
}
}
| 由于现在表有两列(我们在前面添加了大小(size)列),所以还必须调整 getColumnText(Object,int) 以使程序正确工作。 运行该程序的结果如图 4 所示: 图 4. 资源管理器(V6) 程序开始变得十分漂亮了 — 至少在表查看器中是这样。让我们升级 FileTreeLabelProvider 以使用图像,如清单 5 所示: 清单 5. FileTreeLabelProvider(V2)
import java.io.*;
import org.eclipse.jface.viewers.*;
import org.eclipse.swt.graphics.*;
public class FileTreeLabelProvider extends LabelProvider
{
public String getText(Object element)
{
return ((File) element).getName();
}
public Image getImage(Object element)
{
if (((File) element).isDirectory())
{
return Util.getImageRegistry().get("folder");
}
else
{
return Util.getImageRegistry().get("file");
}
}
}
| 运行该程序的结果如图 5 所示: 图 5. 资源管理器(V7) 现在,可以很容易地看出文件和文件夹之间的区别,我们还可以看到:在表视图中,缺省排序算法按字母顺序给各项排序 — 但将文件夹和文件混在一起。您可能还注意到:在树视图中,我们同时看到了文件夹 和文件。我们现在来修正这个问题。
排序和过滤 要在查看器中对项进行排序,我们使用 ViewerSorter 。 ViewerSorter (请参阅 参考资料)是旨在被子类化的类。查看器使用查看器排序程序以便采取两个步骤对它的元素排序。首先,它询问元素的 类别(category)。这返回一个整数,然后它把元素按其 类别号的升序归到各类别组: public int category(Object element) 接着,在每个类别中,它使用 ViewerSorter 的 compare() 方法进行排序。该方法类似于标准 Java 类 Comparator 中的 compare() 方法: public int compare(Viewer viewer, Object element1, Object element2) 缺省情况下,使用标签提供程序返回的字符串并忽略大小写进行排序。 我们在表查看器中只实现 category 方法,如清单 6 所示: 清单 6. FileSorter(V1)
import java.io.*;
import org.eclipse.jface.viewers.*;
public class FileSorter extends ViewerSorter
{
public int category(Object element)
{
return ((File) element).isDirectory() ? 0 : 1;
}
}
| 这将先对文件夹排序,再对文件排序。 在树查看器中,我们希望只显示文件夹。我们通过使用 ViewerFilter (请参阅 参考资料)做到这一点。和排序程序一样,过滤器也旨在被子类化。我们需要实现 select() 方法,它检查元素,如果将要显示该元素,则返回 true。在本例中,我们希望只允许文件夹通过过滤器,所以有清单 7 中的 AllowOnlyFoldersFilter() 方法: 清单 7. AllowOnlyFoldersFilter(V1)
import java.io.*;
import org.eclipse.jface.viewers.*;
public class AllowOnlyFoldersFilter extends ViewerFilter
{
public boolean select(Viewer viewer, Object parent, Object element)
{
return ((File) element).isDirectory();
}
}
| 注:我们过滤的元素是第 3 个参数。现在给出了查看器和父元素 — 以便在需要时访问它们。 现在,我们只需将这些类的实例附加到查看器,如清单 8 所示: 清单 8. 资源管理器(V8)
import java.io.*;
import org.eclipse.jface.action.*;
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
{
...
protected Control createContents(Composite parent)
{
getShell().setText("JFace File Explorer");
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://"));
tv.addFilter(new AllowOnlyFoldersFilter());
final TableViewer tbv =
new TableViewer(sash_form, SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI);
tbv.setContentProvider(new FileTableContentProvider());
tbv.setLabelProvider(new FileTableLabelProvider());
tbv.setSorter(new FileSorter());
...
}
| 注:正如这些方法的名称所表明的那样,一个查看器可以同时有多个过滤器,但只能有一个排序程序。 现在运行资源管理器所得的结果如图 6 所示: 图 6. 资源管理器(V8)
结束语 好了,这些图标无疑使程序更漂亮了一些。我们有了窗口标题,它告诉我们看到的是什么,还有一个新的漂亮的状态行,它告诉我们选中了多少项,再通过过滤和排序将文件和文件夹整齐地分隔开,这个程序开始象一个真正的文件资源管理器了。 它可能看起来漂亮了一点,但现在还做不了什么。在本系列(由 3 部分组成)的最后一篇文章中,我们将通过添加菜单和操作来弥补这一缺陷。我们将了解如何创建菜单栏、工具栏和弹出菜单。我们还将开发一些示例,这些示例中的菜单会使用一些巧妙的 JFace 实用程序来启动程序并访问系统剪贴板,而且我们还将演示如何使用侦听器使菜单项对上下文敏感。
参考资料
关于作者
| | | A. O. Van Emmenis 是一位独立顾问,专门从事 Java/J2EE 培训和咨询工作,他在英国的剑桥工作。Van 已经在软件行业工作了约 20 年左右。他最初与对象打交道是在 CAD 行业使用 Smalltalk 的时候,他现在在工作中主要使用 Java。他对敏捷(Agile)方法和 GUI 设计特别感兴趣。您可以通过 van@vanemmenis.com与 Van 联系。 | |