本教程介绍如何编写提供“选定对象”的组件以及可随全局选择的更改而进行自我更新的组件。
“选择”是所有非平凡应用程序的一个重要概念。NetBeans有两个关于选择的基本概念—获得焦点的TopComponent
的Lookup
以及获得焦点的Top
Component
的activated Node
(s)。本教程将只介绍选择的查找部分—在未来教程中将介绍更高级的内容。
使用选择可以执行诸如上下文敏感之类的操作(根据所显示的内容启用或禁用的操作),并且可以使IDE中的每个调色板窗口(如Property Sheet或Navigator组件)显示所选内容的某个方面。
基本上,每个TopComponent
都有一个对象包,该包可以封装操作,并且其他代码能够查询它。这个对象包就是它的Lookup—实际上就是一个Map,其中的键就是类对象,而值就是扩展或实现键-类的对象。该方法的一大优点就是能够使用该机制对提供某些对象的组件以及使用这些对象的组件进行解耦—以便它们可以在单独的模块中实现,或者可以为旧对象提供新的编辑器,而系统的其余部分将继续透明地工作。
若要下载完整的示例,请访问http://plugins.netbeans.org/PluginPortal/faces/PluginDetailPage.jsp?pluginid=3146。
创建模块套件和项目
本教程的示例将包含三个模块,均包含在一个模块套件中,如下图所示。
首先创建模块套件以包含所有三个模块:
1. 选择File > New Project (Ctrl-Shift-N)。在Categories下,选择NetBeans Plug-in Modules。在Projects下,选择Module Suite Project并单击Next。
2. 在Name和Location面板中,在Project Name中键入SelectionSuite。将Project Location更改为计算机上的任意目录。单击Finish。
3. 再次选择File > New Project (Ctrl-Shift-N)。在Categories下,选择NetBeans Plug-in Modules。在Projects下,选择Module Project并单击Next。
4. 在Name和Location面板中,在Project Name中键入MyAPI。向导中的默认值用于在您刚刚创建套件的目录下创建该模块,此处可以保留。单击Next。
5. 在Basic Module Configuration面板中,将Code Name Base中的 yourorg替换为myorg,以便代码名称库为org.myorg.myapi。向默认的Module Display Name中添加空格,以便它更改为My API。保留本地化包和XML层的位置,以便它们将存储在名称为org/myorg/myapi的包中。单击Finish。
6. 现在您将创建两个以上的模块—按照上面的相同步骤,但使用名称"MyEditor"和"MyViewer"。随着进一步操作,您将了解创建三个模块的原因。
创建API并设置依赖性
此处您将要执行的操作就是创建一个普通的API类。在现实世界中,这样一个API可能代表一些文件或通过编程方式建模的某些其他种类的数据。出于本教程的目的,只需让简单对象拥有几个属性就够了。
1.
右键单击org.myorg.myapi
包并选择New > Java Class,如下所示:
2.
将该类命名为APIObject
。
3. 将默认代码替换为以下内容:
public final class APIObject {
private final Date date = new Date();
private static int count = 0;
private final int index;
public APIObject() {
index = count++;
}
public Date getDate() {
return date;
}
public int getIndex() {
return index;
}
public String toString() {
return index + " - " + date;
}
}
这将是该模块包含的全部代码。正如您所见,每次创建APIObject
的新实例时,都会使计数器增加—以便APIObject
的每个实例都用一些独特的属性。
4.
下一步是让API模块导出org.myorg.myapi
包以便其他包可以看到其中的类。右键单击My API项目并选择Properties。
5.
在Project Properties对话框的API Versioning页面中,在Public Packages列表中选中org.myorg.api
的复选框,如下所示。
6.
现在您需要在模块之间设置一些依赖性。其他两个模块My Editor和My Viewer将使用APIObject
类,以便它们中的每个模块需要声明它们依赖于API模块。对于每个其他模块项目,右键单击项目节点并选择 Properties。
7.
在每个Project Properties对话框的Libraries页面中,单击Add Dependency按钮。在弹出的对话框中,键入APIObject
—应该有惟一一个匹配,那就是您的API模块。选择该模块并单击OK以添加依赖性。
创建Viewer Component
现在您将创建一个单独组件,该组件将跟踪全局选择中是否存在可用的APIObject
(即,获得焦点的TopComponent
是否
在其Lookup中拥有一个APIObject
)。如果存在一个APIObject
,它将显示有关它的一些数据。
对于此类事项的一个常见用例就是创建主/详细信息视图。
“单独组件”就是类似于NetBeans IDE中的Projects窗口、Property Sheet或Navigator等组件—系统中永远存在的一个惟一组件。Window Component向导将自动生成创建此类单独组件所需的所有代码—您只需使用表单设计器或编写代码来提供单独组件的内容。
- 右键单击
org.myorg.myviewer
包
并选择New > Other。 - 在显示的对话框中,选择Module Development > Window Component并单击Next(或按Enter)。
- 在向导的“Basic Settings”页面上,选择
navigator
作为放置您的查看器组件的位置,并且选中在启动时打开该组件的复选框,如下所示:
- 单击Next进入向导的“Name, Icon and Location”页面。
- 在下面的页面中,将类命名为
MyViewer
并单击Finish(或按Enter)。
现在您拥有一个TopComponent
骨架
—称为MyViewerTopComponent
的单独组件。单击MyViewerTopComponent
的编辑器选项卡
—应该可以看到表单编辑器。向该组件中添加两个标签,它们将显示所选择的APIObject
(如果有的话)
的一些信息。
1. 将两个JLabels从Palette拖动到表单,一个标签位于另一个标签的下面。
如上面所示更改第一个标签的文本,以便默认情况下它显示“[nothing selected]”。
2. 单击编辑器工具栏中的Source按钮以切换到代码编辑器
3.
修改该类的签名,以便MyViewerTopComponent
实现LookupListener
:
public class MyViewerTopComponent extends TopComponent implements LookupListener {
4.
右键单击编辑器并选择Fix Imports,以便导入LookupListener
。
5. 将签名行中放置插入符号(^),如下所示。一个灯泡形状的图案将出现在编辑器边缘。按Alt-Enter,然后当出现带有文本“Implement All Abstract Methods”的弹出框时再次按Enter。这样会将LookupListener方法添加到您的类中。
6.
您现在拥有一个实现LookupListener
的类。现在它需要侦听某些事项。本例中,有一个方便的全局Lookup对象,该对象只是代理获得焦点的组件的Lookup—可以通过调用Utilities.a
ctionsGlobalContext()
来获得。因此,您可以只侦听这个全局选择查找,而不必跟踪您自己获得焦点的组件,这样,焦点发生更改时就会激发相应的更改。编辑源代码,以便它包含以下方法,如下所示:
private Lookup.Result result = null;
public void componentOpened() {
Lookup.Template tpl = new Lookup.Template (APIObject.class);
result = Utilities.actionsGlobalContext().lookup(tpl);
result.addLookupListener (this);
}
public void componentClosed() {
result.removeLookupListener (this);
result = null;
}
public void resultChanged(LookupEvent lookupEvent) {
Lookup.Result r = (Lookup.Result) lookupEvent.getSource();
Collection c = r.allInstances();
if (!c.isEmpty()) {
APIObject o = (APIObject) c.iterator().next();
jLabel1.setText (Integer.toString(o.getIndex()));
jLabel2.setText (o.getDate().toString());
} else {
jLabel1.setText("[no selection]");
jLabel2.setText ("");
}
}
只要在窗口系统中显示组件
,
就调用
componentOpened()
;只要用户单击其选项卡上的X按钮关闭组件,就调用componentClosed()
。因此当显示组件时,您希望对它跟踪选择—即上面代码所执行的操作。
resultChanged()
方法是LookupListener
的实现。只要选定的APIObject
更改,它就会更新您放在表单上的两个JLabel
。
创建Editor Component
现在您需要某些事项来实际提供APIObject
的实例,以便使用该代码。幸运的是,这非常简单。
这次您将创建另一个TopComponent
,它在编辑器区域打开并从其Lookup
提供APIObject
的一个实例。您可以再次使用Window Component模板,但该模板设计用于创建单独组件,而不是很多组件。因此您将只需创建一个没有模板的TopComponent
子类,以及一个将打开其他子类的操作。
- 您将需要向My Editor模板中添加三个依赖性以便它能够查找您将使用的类。右键单击My Editor项目并选择Properties。在Project Properties对话框的Library页面上,单击Add Dependency按钮,并键入
TopComponent
。该对话框将自动建议设置对Window System API的依赖性。对Lookups
(Utilities API)执行相同的操作。 - 右键单击My Editor项目中的
org.myorg.myeditor
包并选择New > JPanel Form。 - 将它命名为“MyEditor”,并完成该向导。
- 当打开表单编辑器时,将两个JTextField拖动到表单上,一个在另一个的上面。在属性页上,将每个JTextField的Editable属性(复选框)设置为
false
。 - 单击编辑器工具栏中的Source按钮以切换到代码编辑器。
- 更改
MyEditor
的签名以展开TopComponent
而不是javax.swing.JPanel
:
public class MyEditor extends TopComponent {
- 将以下代码添加到
MyEditor
的构造函数
中:
APIObject obj = new APIObject();
associateLookup (Lookups.singleton (obj));
jTextField1.setText ("APIObject #" + obj.getIndex());
jTextField2.setText ("Created:" + obj.getDate());
setDisplayName ("MyEditor " + obj.getIndex());
右键单击编辑器并选择Fix Imports。
行associateLookup (Lookups.singleton (obj));
将创建一个只包含一个对象的Lookup—APIObject
的新实例
—并将该Lookup
指定为由MyEditor.getLookup()
返回的内容。尽管这是一个虚构的示例,但是您可以想像APIObject
如何代表一个文件、数据库中的一个条目或者您希望编辑或查看的其他任何内容。您还可以设想一个组件,该组件允许您选择或编辑APIObject
的
多个惟一实例—这将是下一部教程的主题。
为了使您的编辑器组件变得有趣一些(尽管它实际上并不编辑任何内容),您将文本字段的值设置为APIObject
中的值,以便有内容可显示。
打开Editor Components
现在您需要一种在编辑器区域打开MyEditor
组件的方法,以便显示某些内容。若要通过选择执行有意义的操作,您需要多个编辑器,以便对多个APIObject
跟踪。由于您希望有多个编辑器,因此需要主菜单具有一个简单操作,该操作将创建并在窗口系统中打开MyEditor
的另一个实例
(相对于Window Component模板为我们创建的内容,即始终查找单独组件的操作,如IDE中的Navigator或Property Sheet组件)。
- 右键单击
org.myorg.myeditor
包
并选择New > Other。 - 在对话框中选择Module Development > Action并单击Next。
- 接受默认值(“always enabled”)并再次按Next。
- 在GUI Registration页上,通过再次按Next接受默认值(这将使您的操作出现在File菜单顶部)。
- 在向导的最后一页上,将该操作命名为
OpenEditorAction
并将其显示名称设置为“Open Editor”。 - 按Finish以生成该操作类。
- 现在代码编辑器应该通过一个名为
OpenEditorAction
的类打开,该类创建CallableSystemAction
的子类(javax.sw
ing.Action
的
NetBeans子类,javax.swing.Action
允许您将上下文敏感的帮助与某个操作相关联
)
。向其performAction()
方法中添加以下代码:
{0><}0{>MyEditor editor = new MyEditor();
editor.open();
editor.requestActive();
以上代码将只创建MyEditor
的一个新实例
(
这又会创建APIObject
的一个新实例并将该实例放在其Lookup
中
)
并在窗口系统中打开该实例。
运行代码
现在您准备运行该教程。只需右键单击拥有三个模块的模块套件SelectionSuite
并从弹出菜单中选择Run。当IDE打开时,只需选择File > Open Editor—调用您的操作。反复执行这些操作,以便打开多个编辑器组件。还应该打开单独的MyViewer
窗口。请注意,在单击不同选项卡时MyViewer
窗口的内容如何随之改变,如下所示:
如果您单击Projects窗口,则会注意到文本更改为“[No Selection]”,如下所示:
如果您没有看到MyViewer
窗口,那么可能是因为没有在向导中选中表示在系统启动时打开的复选框—只需转到Window菜单并选择Open MyViewer Window以显示它。
那么,重点是什么?
您可能想了解这次练习的意义—只是向您显示我们能够处理选择—这很重要!关键是将代码分成了三个模块:My Viewer模块不知道有关My Editor模块(每个模块都可以独自运行)的任何内容。它们仅共享对My API的常见依赖性。这非常重要—它意味着两件事情:1. 可以单独开发和发布My Viewer和My Editor,2. 想提供不同于My Editor的另一种编辑器的任何模块都可以这样做,并且只要替代编辑器从其Lookup中提供APIObject
的一个实例,查看器组件就会完美地配合工作。
为了实际描绘该操作的价值,假设APIObject
是更复杂的内容,比如MyEditor
是一个图像编辑器,并且APIObject
代表正在编辑的图像。此处可以实现的强大功能是您能够将MyEditor
替换为基于SVG矢量的编辑器(假设),并且查看器组件(假设显示当前编辑图像的属性)将与此新的编辑器透明工作。这就是工作模型,因此您可以向针对Java文件的NetBeans IDE添加新的工具,这些工具将在不同版本的NetBeans中工作,您可以让Java文件拥有另一个编辑器(如表单编辑器),并且当使用表单编辑器时,针对Java文件的所有组件和操作仍将正常工作。
这就是NetBeans处理Java和其他源文件的方式—这种情况下,可以从编辑器的Lookup中获得的内容为DataObject,组件(如Navigator和Property Sheet)只是查看获得焦点的TopComponent
的
可用对象。
这种方法的另一个要点是人们经常将现有应用程序迁移到NetBeans平台。这种情况下,属于数据模型的对象可能就是现有的工作代码,为了使该代码集成到NetBeans中,不应该对代码进行更改。通过将数据模型的API保留在单独的模块中,可以保持NetBeans集成与核心业务逻辑分离。
动态更改选定对象
为了更清晰地证明这个方法的强大功能,您还需要采取一个步骤,向编辑器组件添加一个按钮,使用新的APIObject
动态替换已有的APIObject
。
- 在表单编辑器中打开
MyEditor
(单击编辑器工具栏中的Design工具栏按钮),然后将一个JButton
拖动到其中。 - 将JButton的文本属性设置为“Replace”。
- 右键单击
JButton
,然后选择Events > Action > actionPerformed。这将打开代码编辑器,并且事件处理程序方法中有插入符号。 - 在类定义的前面,您将添加最后一个字段:
public class MyEditor extends TopComponent {
private final InstanceContent content = new InstanceContent();
InstanceContent是一个类,该类允许我们动态修改Lookup的内容(具体指AbstractLookup
的一个实例)。
- 将以前添加到构造函数的所有行复制到剪贴板,然后将其从构造函数中删除,但以“associateLookup...”开头的行除外。该构造函数行应该更改为以下形式:
associateLookup (new AbstractLookup (content));
- 您将在JButton的操作处理程序中使用放置在剪贴板中的行—因此您应该在第一次初始化该组件时运行一次该代码。向构造函数添加以下行,位于上面的行之后:
jButton1ActionPerformed (null);
- 通过粘贴剪贴板内容并将以下行添加到末尾修改事件处理程序方法,使它如下面所示:
private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {
APIObject obj = new APIObject();
jTextField1.setText ("APIObject #" + obj.getIndex());
jTextField2.setText ("Created:" + obj.getDate());
setDisplayName ("MyEditor " + obj.getIndex());
content.set(Collections.singleton (obj), null);
}
- 右键单击编辑器并选择Fix Imports。
您现在准备再次运行该套件。再次右键单击SelectionSuite并选择Run。注意,现在当您单击Replace按钮时,所有组件包括MyViewer
实例等所有
内容是如何进行更新的。
提供多个对象
这当然适合于解耦,是不是从组件中提供这一个对象,类似于拥有一个只包含一个键和一个值的Map
?
答案是肯定的。当您从多个API中提供多个对象时,该技术就会变得更强大。
例如,在NetBeans中提供上下文敏感的操作是非常常见的。一个恰当的例子就是构成NetBeans的Actions API的内置SaveAction
。该操作实际所执行的内容是,只侦听在全局上下文上是否存在名为SaveCookie
的内容—方法与您
的查看器窗口侦听APIObject
一样。
如果出现一个SaveCookie
(通常当文件的内容被修改但尚未保存时编辑器会向其查找中添加一个SaveCookie
),则该操作变为激活状态,因此
Save工具栏按钮和菜单项也变为激活状态。当调用Save操作时,它调用SaveCookie.save()
,从而使SaveCookie
消失,因此Save操作变为禁用,直到出现一个新的SaveCookie
为止。
您可能已经注意到,上下文敏感是New Action向导的一个选项。该向导当前生成的操作实际使用的方法在Lookup
之前执行
;执行此类上下文敏感操作的基于Lookup的方法如developer FAQ中所述。
因此实际的模式是从您的组件的Lookup
中提供多个对象
—对于正在编辑的对象,不同的辅助组件和不同的操作所感兴趣的方面也不一样。可以将这些方面清晰地划分为辅助组件和操作可以依赖和侦听的接口。
值得注意的其他事项
尽管与本教程主题不直接相关,但是值得注意的是,如果您打开三个MyEditor
实例,然后关闭并重新启动NetBeans,您将会看到重新启动后将神奇地出现三个MyEditor
实例。默认情况下,您的编辑器在关闭时已序列化到磁盘上并在重新启动时还原。
如果您不希望出现此行为,则有两个其他选择。在MyEditor
上覆盖以下方法以使编辑器不会在重新启动时重新打开:
public int getPersistenceType() {
return PERSISTENCE_NEVER;
}
如果您希望持久保存打开的组件,而丢弃已经关闭的那些组件,则返回PERSISTENCE_ONLY_OPENED
。默认设置(出于向后兼容的原因)为PERSISTENCE_ALWAYS
,该设置并不适合于编辑器样式的组件—这意味着即使已经关闭的编辑器也会永远保留并在重新启动时重新加载。
但是请注意,已序列化到磁盘中的部分内容是您的组件在主窗口中的位置。因此单独的TopComponent
(如属性页或我们的查看器组件)应该使用PERSISTENCE_ALWAYS
—否则如果用户关闭它们后,下次打开时它们将出现在编辑器区域中,而不是出现在应该出现的位置。
进行清理
默认情况下,模块模板假设您想使用layer.xml
文件安装对象。如果是My API模块,则实际上并不使用该文件。因此要稍微缩短启动时间,应执行下面的方法:
- 展开My API项目的Important Files节点
- 双击Module Manifest节点
- 从Manifest中删除以下行:
OpenIDE-Module-Layer:org/myorg/myapi/layer.xml
- 然后删除
org.myorg.myapi
中相应的
layer.xml
文件
下一步
到目前为止,您已经注意到某些组件具有更加粒度化的选择逻辑,甚至涉及多个选择。在下一部教程中,将介绍如何使用Nodes API处理这种情况。