当前页面: 开发资料首页 → Eclipse 专题 → 使用 XML: Eclipse 中的布局、属性和首选项
摘要: XM 最初就是在 developerWorks 的“使用 XML”专栏中开发的,它是一个简单的使用 XML 和 XSL 的发布框架。在这一部分中,Benoit Marchal 进一步讨论了用户界面考虑事项,其中包括如何为 XSL 发布插件管理 Eclipse 中的属性和首选项。
在几篇专栏文章之前,我就开始构建用于 XM 的用户界面。我选择了在 Eclipse 上构建用户界面,Eclipse 是 IBM 赠予开放源码社区的一个集成开发环境(IDE)。它旨在作为一个基本的框架,工具开发人员可以通过插件来扩展它。我在这个系列文章中的目标是编写这样一个用于 XSL 的插件。
Eclipse 最令人感兴趣的方面之一是,通过将同一类中最佳的插件组合起来,您就有可能(并且方便地)定制自己的工作环境。例如,在 XM 和 XML 编辑器之间存在明显的相互协作(请参阅 参考资料中的“Black Sun”站点)。 而且,正如本专栏文章所展示的,如果目前不支持您所选的工具,则可以不费力地添加一个插件来支持该工具。
用户界面考虑事项
用户界面始终是插件要考虑的问题。毕竟,插件旨在提供比 XM 长期以来所具有的原始命令行界面更为图形化的界面。
我之所以选择使用 Eclipse 是因为它在用户界面上具备的优势。Eclipse 有它自己的 GUI 库 — 标准窗口小部件库(standard widget library,SWT)— 它提供了一种本机的 Swing 替代。我广泛地使用过基于 Swing 的产品,总是发现它们不如本机应用程序。不知何故,就是觉得本机控件的 Swing 仿真不是很好。SWT 使用平台的控件,结果要好得多。
不过,令人吃惊的是,在插件中几乎没有代码来管理用户界面。插件大多注册不可见的服务,譬如在上一篇专栏文章“使用 Eclipse 和 XM 构建项目”(请参阅 参考资料以获取这篇文章的链接)中所讨论的构建器。这篇专栏文章将讨论一些首选项和属性,这使我有机会来研究 SWT。
我鼓励您下载这个项目的代码(请参阅 参考资料),并在您阅读本文时查看它。
SWT 布局
SWT 具有您期望从现代库中获得的所有控件,譬如输入域、列表和按钮。但它也有树、工具栏、表以及其它控件。只要可能,SWT 就使用本机控件,所以最后的结果是,Java 应用程序和本机应用程序实际上没有区别。
AWT、Swing 和 SWT 具有许多相似之处。它们的库的主要区别在于其实现(Java 平台与本机平台)和命名约定。对于 SWT,控件是 窗口小部件。窗口小部件类似于 JDK 中的组件。在屏幕上,窗口小部件被安排在 组合(composite)(面板和窗口等)中。SWT 组合类似于 JDK 中的容器。
在本专栏文章中,我不想把过多时间花在 SWT 的一些细节之上。只要说窗口小部件在
org.eclipse.swt.widgets
包中就足够了。其中的类名是自解释的,所以您浏览该文档应该没有问题。
相反,我将把注意力放在布局上,它不同于 AWT 和 Swing 中的布局。清单 1 摘自
XMProjectPropertiesPage
类。我将在
首选项和属性中详细讨论该类,但现在让我们把注意力放在
buildGUI()
方法上。
private Control buildUI(Composite parent)
{
Composite composite = new Composite(parent,SWT.NULL);
RowLayout rowLayout = new RowLayout();
rowLayout.type = SWT.VERTICAL;
rowLayout.wrap = false;
composite.setLayout(rowLayout);
hasNature = new Button(composite,SWT.CHECK);
hasNature.setText(Resources.getString("eclipse.hasnature"));
hasNature.addSelectionListener(new SelectionListener()
{
public void widgetDefaultSelected(SelectionEvent e)
{
group.setEnabled(hasNature.getSelection());
}
public void widgetSelected(SelectionEvent e)
{
group.setEnabled(hasNature.getSelection());
}
});
group = new Group(composite,SWT.NONE);
group.setText(Resources.getString("eclipse.properties"));
GridLayout gridLayout = new GridLayout();
gridLayout.numColumns = 2;
group.setLayout(gridLayout);
Label label = new Label(group,SWT.RIGHT);
label.setText(Resources.getString("eclipse.src"));
srcText = new Text(group,SWT.SINGLE);
GridData gridData = new GridData();
gridData.widthHint = 150;
srcText.setLayoutData(gridData);
label = new Label(group,SWT.RIGHT);
label.setText(Resources.getString("eclipse.rules"));
rulesText = new Text(group,SWT.LEFT);
gridData = new GridData();
gridData.horizontalAlignment = GridData.FILL;
gridData.grabExcessHorizontalSpace = true;
rulesText.setLayoutData(gridData);
label = new Label(group,SWT.RIGHT);
label.setText(Resources.getString("eclipse.publish"));
publishText = new Text(group,SWT.LEFT);
gridData = new GridData();
publishText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL
| GridData.GRAB_HORIZONTAL));
isBuild = new Button(group,SWT.CHECK);
isBuild.setText(Resources.getString("eclipse.build"));
gridData = new GridData();
gridData.horizontalSpan = 2;
isBuild.setLayoutData(gridData);
return composite;
}
</td></tr></table>类似于 AWT 和 Swing,SWT 使用 布局来控制组合中窗口小部件的大小和位置。SWT 布局几乎与 JDK 布局一样,但它们使用不同的类 — 同样,概念类似,但实现不同。
SWT 在
org.eclipse.swt.layout
包中提出了四种布局:
FillLayout
:以竖直列或水平行方式呈现窗口小部件。它类似于 JDK 中的
BoxLayout
。
RowLayout
:以一行或多行来呈现窗口小部件。更确切地讲,它可使窗口小部件在行尾换行。
RowLayout
类似于 JDK 中的
FlowLayout
。
GridLayout
:功能更强大的布局。它类似于 JDK 中的
GridBagLayout
。
GridLayout
以网格方式来呈现窗口小部件,并提供了许多选项来控制间距、边界和对齐等因素。窗口小部件可以跨网格中的几个单元。
FormLayout
:SWT 独有的布局(至少我不知道 JDK 有等同的布局方式)。
FormLayout
是一个特殊的网格,它将数据输入域及其标签组合在一起。
清单 1
使用
RowLayout
和
GridLayout
。
图 1 显示了清单 1 所创建的表单。它包含一个复选框和一个由四个其它窗口小部件所组成的组。
RowLayout
将复选框和组对齐。这里不能选择用
FillLayout
,因为复选框和组的大小不一样(
FillLayout
要求其中所有的窗口小部件的大小都一样)。
初始化
RowLayout
与设置
RowLayout
的几个属性一样容易:
type
用于在竖直列方向和水平行方向进行切换,而
wrap
控制布局是否能创建多个行。然后,用
setLayout()
方法将该布局分配给组合。
组(group)是在窗口小部件周围绘制一个边框的组合。在
清单 1 中,组使用了
GridLayout
类。为了初始化
GridLayout
,您只要设置列数。注意:
GridBagLayout
通过常量(如
REMAINDER
和
RELATIVE
)间接地指定行数。
要分别指定每个单元的属性,
GridLayout
使用了
GridData
类。
GridData
类似于 JDK 中的
GridBagConstraints
,但属性名称不同。注意 SWT 和 JDK 之间的主要差别:使用
SWT,您可以在窗口小部件上设置
GridData
(用
setLayoutData()
方法),而不是在布局上设置。
清单 1
展示了 SWT 与 JDK 之间的另一个区别:组合没有容器中的
add()
方法。相反,窗口小部件可以在其构造函数中引用它们的父代。
虽然这两个 API 存在不同,但如果您已经使用过 AWT 或 Swing,那么学习 SWT 对您来说很容易。
首选项和属性
首选项和属性是紧密相关的。这两者都控制环境中的配置和选项。首选项用于配置选项,而配置选项应用于每个项目中的环境。一个典型示例是用颜色来突出显示编辑器中的语法。
用 Eclipse 的术语来讲,属性附属于资源(请参阅 参考资料以获取“Creating a project”的链接,这篇文章讨论了 Eclipse 资源)。例如,属性可以控制如何构建一个给定的项目。
XM 插件将需要首选项和属性。我们的项目已经使用属性来设置源、发布和规则目录,以及控制是构建项目还是制作项目。这些属性源自老的命令行界面。在“Creating a project”中,我介绍了一个向导,由它来初始化属性,但到目前为止,还没有一个关于界面的小窍门来修改这些属性。
自从我开始使用项目性质(nature)(请参阅 参考资料以获取以前文章的链接)以来,我一直在搜索一种选项来将 XM 性质分配给任何项目。最终,我得出结论,性质可以被视为特殊的属性。
至于首选项,控制台需要一个机制来过滤消息。当处理大项目时,重新构建会生成众多消息。由于难以区分错误和进展消息,因此我想给用户一个选项来控制显示哪些消息。
项目属性
让我们从项目属性开始。实现涉及两个类:
ProjectProperties
和
XMProjectPropertiesPage
。
ProjectProperties
是一个实用程序类。Eclipse 不需要这个类,但我发现它十分方便。
ProjectProperties
负责保存和读取由 Eclipse 提供的
IProject
的属性。通过将该代码单独封装在一个类中,更易于确保在整个插件中以一致的方式对待属性(特别是缺省值)。(请参阅
参考资料以获取关于下载这个类的代码的信息。)
XMProjectPropertiesPage
是 Eclipse 特有的类。它将页面添加到导航器的属性对话框,您可以对导航器中的资源单击鼠标右键来访问该对话框。
XMProjectPropertiesPage
被附加到
org.eclipse.ui.propertyPages
扩展点。
清单 2
摘自 plugin.xml,这个文档是声明该扩展的插件清单。要特别注意
objectClass
属性,它指定了对哪个对象应用该属性。导航器所识别的任何对象都可能是该域的候选对象,从一般的
IResource
到更为具体的元素(如
IProject
或
IFolder
)。为了更为具体,使用弹出菜单所引入的
标记。
</td></tr></table>在
清单
2 中,
objectClass
被设置为
IProject
对象。这意味着,如果用户单击项目,就能看到属性页。然而,文件和目录将不显示该属性页。
XMProjectPropertiesPage
继承了
PropertyPage
,并覆盖
createContents()
,如
清单
3 所示。
createContents()
方法通过调用
buildUI()
(我在前面的
SWT
布局中介绍过)并在
ProjectProperties
对象的帮助下读取属性来初始化属性页。
public Control createContents(Composite parent)
{
Control control = buildUI(parent);
try
{
IAdaptable adaptable = getElement();
if(adaptable instanceof IProject)
{
properties = new ProjectProperties((IProject)adaptable);
readProperties();
}
}
catch(CoreException x)
{
ErrorDialog.openError(
PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(),
Resources.getString("eclipse.dialogtitle"),
Resources.getString("eclipse.propertyerror"),
PluginTools.makeStatus(x));
}
return control;
}
</td></tr></table>
PropertyPage
还定义了当用户按
OK、
Cancel、
Apply 或
Default 按钮时的回调方法。
XMProjectPropertiesPage
覆盖了其中一些方法以保存新的属性值。
作为属性的项目性质
严格地讲,项目性质 不是属性。正如我 以前所讨论的,Eclipse 使用项目性质来确定用哪个插件来管理项目。项目性质的功能之一是控制菜单项、构建器以及其它事物。属性页还包括表示性质的复选框。
我之所以将项目视为属性是因为我想给用户一个机会以将 XM 添加到任何已有的项目中。其基本原理是 Eclipse 可以让您处理与所熟悉的环境完全不同的项目和语言。您可以在早上编写 Java 代码,在下午早些时候编写 PHP 代码,然后在下午晚些时候用 XM 对网站进行更新。
在现实世界中,一个项目常涉及两个或多个这样的工具。例如,在 Pineapplesoft,我们的大多数网站都采用了 XSL(通过 XM)和 PHP。一个项目有双重性质,这似乎很合乎逻辑。从用户的角度来看,在一个项目上启用 XM 性质和设置项目属性是同时发生的,因此,这似乎比在同一屏幕上提供所有这些选项要更合乎逻辑。
当测试该特性时,我认识到我没有正确地实现项目的性质。事实证明,您必须将项目性质声明为一个扩展点。这个扩展点必须实现
IProjectNature
接口,并提供一些方法来配置该项目和取消该项目的配置。
这个扩展点在
XMProjectNature
类中,如
清单 4 所示。当性质被添加到项目时,该框架调用
configure()
方法。当除去性质时,它调用
deconfigure()
方法。
configure()
方法应该确保该性质正常工作,这涉及到注册合适的构建器。
deconfigure()
方法执行与此相反的工作。
configure()
中的代码最初是在项目向导中出现;我只不过必须将它移到了新的地方。
package org.ananas.xm.eclipse;
import org.eclipse.core.runtime.*;
import org.eclipse.core.resources.*;
public class XMProjectNature
implements IProjectNature, PluginConstants
{
private IProject project;
public void configure()
throws CoreException
{
IProjectDescription description = project.getDescription();
if(!hasBuildSpec(description.getBuildSpec()))
{
ICommand[] old = description.getBuildSpec(),
specs = new ICommand[old.length + 1];
System.arraycopy(old,0,specs,0,old.length);
ICommand command = description.newCommand();
command.setBuilderName(BUILDER_ID);
specs[old.length] = command;
description.setBuildSpec(specs);
project.setDescription(description,new NullProgressMonitor());
}
}
public void deconfigure()
throws CoreException
{
IProjectDescription description = project.getDescription();
int count = getBuildSpecCount(description.getBuildSpec());
if(count != 0)
{
ICommand[] old = description.getBuildSpec(),
specs = new ICommand[old.length - count];
int i = 0,
j = 0;
while(i < old.length)
{
if(!old[i].getBuilderName().equals(BUILDER_ID))
specs[i] = old[j++];
i++;
}
description.setBuildSpec(specs);
project.setDescription(description,new NullProgressMonitor());
}
}
public IProject getProject()
{
return project;
}
public void setProject(IProject project)
{
this.project = project;
}
private boolean hasBuildSpec(ICommand[] commands)
{
return getBuildSpecCount(commands) != 0;
}
private int getBuildSpecCount(ICommand[] commands)
{
int count = 0;
for(int i = 0;i < commands.length;i++)
if(commands[i].getBuilderName().equals(BUILDER_ID))
count++;
return count;
}
}
</td></tr></table>清单 5 是插件清单中的声明。
清单 5. 摘自 plugin.xml 的另一段代码 <table bgcolor="#eeeeee" width="100%" cellpadding="5" cellspacing="0" border="1"><tr><td>
</td></tr></table>XM 首选项
首选项类似于属性,但它们与插件相关,而与资源无关。
遗憾的是,Eclipse 存储首选项的方式不同于存储属性的方式。对于首选项,Eclipse 使用首选项存储。必须用每个首选项的缺省值来初始化该存储(并且,事实上,如果首选项的值不是缺省值,则该存储只记录一条首选项)。
该存储在插件类中被初始化,这个类初始化对该插件而言是全局的对象。大多数插件都有一个插件类(如果您使用 Eclipse 向导来生成新的插件,那么向导会自动生成一个),但不知何故,XM 没有这个插件类。
通过将一个类属性添加到清单中的
标记来声明插件类,如下所示:
</td></tr></table>对于这个项目,插件类是
XMPlugin
。
XMPlugin
继承了
AbstractUIPlugin
。当框架装入这个插件时,会创建该插件类的实例,使它有机会初始化全局对象。
实质上插件并不能试图实例化插件类本身。内存中包含的这个插件类的实例从来不应该超过一个。为了使这一点更方便,插件类实现单模式(singleton pattern):它在私有类变量中存储一个自身的引用。通过
getDefault()
方法,可以访问该共享实例。
对我们来说,最有用的方法是
initializeDefaultPreferences()
,它初始化首选项存储中的缺省值,如下面所示:
protected void initializeDefaultPreferences(IPreferenceStore store)
{
store.setDefault(LEVEL_PREFERENCE_NAME,LEVEL_ALL);
}
</td></tr></table>在
XMPreferencesPage
中实现了首选项页(在首选项页上,用户可以更改首选项值),如
清单
6 所示。首选项页类似于属性页,但 Eclipse 提供了
FieldEditorPreferencePage
类,它是一个特殊的类,该类进一步简化了首选项页。
package org.ananas.xm.eclipse;
import org.ananas.xm.*;
import org.eclipse.ui.*;
import org.eclipse.jface.preference.*;
public class XMPreferencesPage
extends FieldEditorPreferencePage
implements PluginConstants, IWorkbenchPreferencePage
{
public XMPreferencesPage()
{
super(GRID);
IPreferenceStore store =
XMPlugin.getDefault().getPreferenceStore();
setPreferenceStore(store);
}
protected void createFieldEditors()
{
RadioGroupFieldEditor levelEditor =
new RadioGroupFieldEditor(
LEVEL_PREFERENCE_NAME,
Resources.getString("eclipse.level"),
1,
new String[][]
{
{ Resources.getString("eclipse.level.all"), LEVEL_ALL },
{ Resources.getString("eclipse.level.info"), LEVEL_INFO },
{ Resources.getString("eclipse.level.warning"), LEVEL_WARNING },
{ Resources.getString("eclipse.level.error"), LEVEL_ERROR },
},
getFieldEditorParent());
addField(levelEditor);
}
public void init(IWorkbench workbench)
{
}
}
</td></tr></table>
FieldEditorPreferencePage
使用域编辑器,而不是常规的窗口小部件。域编辑器只是一个与首选项相关的窗口小部件。只需在
createFieldEditors()
方法中实例化域编辑器,而框架会负责装入和保存首选项。
最后,我更新了控制台以读取首选项,进而相应地显示消息。如 清单 7所示,控制台直接从插件类检索首选项存储。控制台还注册属性更改侦听器。当首选项发生变化(例如,通过首选项页做了更改)时,属性存储就通知侦听器。
IPreferenceStore store = XMPlugin.getDefault().getPreferenceStore();
store.addPropertyChangeListener(new IPropertyChangeListener()
{
public void propertyChange(PropertyChangeEvent e)
{
setLevel((String)e.getNewValue());
}
});
setLevel(store.getString(LEVEL_PREFERENCE_NAME));
</td></tr></table>结束语
我已经在我先前的专栏文章中充满激情地讲解了 Eclipse,但这个环境仍然让我兴奋。它是功能强大且灵活的框架。而且,对于工具开发人员来说,它很容易上手。正如在本专栏文章中多次提到的,可以很 容易地增强 Eclipse。在 Pineapplesoft,我们现在将 Eclipse 和 XM 应用到了所有的 Web 发布项目。
<table border="0" cellpadding="0" cellspacing="0"><tr><td valign="middle">
</td><td valign="top" align="right"></td></tr></table></td></tr></table>
关于作者<table border="0" cellspacing="0" cellpadding="0" width="100%"><tr><td colspan="3"></td></tr><tr align="left" valign="top"><td></td><td></td><td width="100%">
Benoit Marchal 是一位比利时顾问。他是 XML by Example 及其它 XML 书籍的作者。您可以就 XML 项目方面的问题向 Benoit 寻求帮助 — 可以通过 bmarchal@pineapplesoft.com 与他联系。
</td></tr></table>
↑返回目录
前一篇: 将 ActiveX 控件集成到 SWT 应用程序
后一篇: 使用 XML: 创建项目