上一部教程介绍了在NetBeans中处理组件级选择的基本知识—如何从TopComponent
的Lookup
中提供对象,以及如何编写对获得焦点的组件的Lookup
敏感的其他组件。
本教程侧重于Nodes API,它可以比组件级选择执行更加粒度化的视图和选择。当然,您可以编写一个组件,该组件可以根据需要读取和写入其自身的Lookup,并且这样可以提供更加粒度化的选择逻辑。但是Nodes API可以轻松执行该操作,并且与自己执行该操作相比,它提供了很多优势。
第一个优点是Nodes API提供一个表示层—采用某种方式编辑的数据模型和向用户公开该数据模型的UI组件之间的层。该层非常有用并且功能强大,因为可以采用多种方式或使用多个UI演示同一种模型。
第二个优点是Explorer API—模块org.openide.explorer
提供一组丰富的组件—树、列表、三个表以及更多—这可以呈现Node及其子节点。
Node
是一个普通的层次结构对象—一个Node
具有:
l Children—其下面的层次结构中的节点,可以显示在树中
l Actions—一个操作数组,可以显示在弹出菜单中
l Display Name—一个人类可读的局部的显示名称,可以显示在UI组件中
l Icon—可以显示在UI组件中的图标
Node
可以激活以上任何一項内容的更改,并且资源管理器UI组件将自动更新自身。这并不意味着上一部教程没有用—相反,它解释了Nodes API可以工作的原因。org.openide.nodes.Node
具有一个getLookup()
方法
。实际上当您更改IDE中Projects选项卡的选择时发生了一些事情,例如Projects选项卡是TopComponent。它代理树中当前选择的对象的Lookup
—就像Utilities.actionsGlobalContext()
Lookup
代理获得焦点的组件一样,并且当焦点改变时激活更改。
使用Explorer API中的组件,很容易创建您自己的Node
树视图,并且使用非常少的代码在您自己的组件中拥有此类型的代理。查看器类型组件(如上一部教程中的MyViewer
组件)不用执行其他任何特殊的操作便能够响应Explorer组件中的选择更改—当选择更改时将自动通知它们。
入门
本教程中的代码将延续上一部教程中的代码—假设您熟悉该代码以及其执行的操作。若要下载完整的示例,请访问http://plugins.netbeans.org/PluginPortal/faces/PluginDetailPage.jsp?pluginid=3146。
创建资源管理器视图
您将做的第一件事是对您的MyEditor
编辑器组件进行一些实际修改。 首先从编辑器中打开它。
- 首先,显示My Editor项目的属性对话框。在Libraries选项卡上,单击add按钮,然后在对话框中键入“BeanTreeView”。当您看到列出了Explorer & Property Sheet API之后,单击OK,如下所示。这将在Explorer API模块上添加一个依赖性,以便您可以使用其中的类。
- 下一步,删除操作处理程序方法的主体部分:
private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {
}
并从构造函数中删除对它的调用。这样,当您删除与其关联的按钮时,处理程序方法也将被删除。
- 切换到表单设计器,选择所有组件并删除它们。
- 在Component Inspector中,右键单击
TopComponent
节点,然后选择Set Layout > BorderLayout,如下所示:
- 单击Component Palette窗口中的
JScrollPane
按钮并将滚动窗格拖动到表单上—它将占据整个表单。此处的关键是所有Explorer UI组件都是JScrollPane
的子类—因此您可以只更改实例化代码即可创建一个资源管理器视图。 - 选择您的
JscrollPane
,右键单击它,然后选择Customize Code。通过添加new BeanTreeView()
来自定义Code Customizer中的初始行,如下所示:
BeanTreeView
是Explorer API中的一个组件—在Node
及其子节点之上的一个基本的基于
JTree
的视图,其中具有内置的弹出菜单处理、搜索以及更多操作。
- 切换到代码编辑器并按下Ctrl-Shift-I以导入BeanTreeView,因为需要添加导入语句,如下所示:
- 下一步是为您的树提供显示的内容。Explorer UI组件的工作类似于以下内容:当添加到某个容器时,它们搜索该容器及其祖先,直到它们找到实现
ExplorerManager.Provider
的容器为止。因此不要将该节点设置为直接在组件上查看—应该将其设置为在组件的管理器上查看。这样便可以拥有多个视图,主/详细信息视图以及由单个管理器管理的所有此类视图。按照如下方式添加MyEditor的签名:
public class MyEditor extends TopComponent implements ExplorerManager.Provider {
然后按下Ctrl-Shift-I修复导入。保留签名行中的插入符号,一个灯泡状的图案将出现在边缘。按下Alt-Enter并接受“Implement all abstract methods”提示。这将添加一个方法getExplorerManager()
。按照如下方式实现该方法:
private final ExplorerManager mgr = new ExplorerManager();
public ExplorerManager getExplorerManager() {
return mgr;
}
- 现在,由于目标是一个可以显示多个
APIObject
s的组件,因此您需要一个或两个Node
以显示在您的组件中。每个组件都将拥有自己的APIObject
实例。因此,您现在将添加代码来为您的树视图创建一个根节点。向构造函数中添加以下行:
mgr.setRootContext(new AbstractNode(new MyChildren()));
上面的代码为MyEditor
的子组件的所有资源管理器视图设置根节点。
- 如果您尝试Fix Imports,则可能会看到错误对话框,它告诉您
AbstractNode
或MyChildren
都无法解析。若要解析AbstractNode
,您需要在Nodes API模块上添加一个依赖性。右键单击My Editor项目,然后转到Libraries页面并单击Add Dependency。在Add对话框中键入“AbstractNode”,并且选中列表中的“Nodes API”项目时单击OK或按Enter键。 - 现在回到源编辑器中,按Ctrl-Shift-I键以执行Fix Imports。将通知您
MyChildren
无法解析。好了—您可以编写它了。
实现Node和Node子节点
您将注意到上面使用的类名为AbstractNode
。尽管名称有所暗示,但它并不是抽象类!它是org.openide.nodes.Node
的一种有用实现,可以节省很多时间和精力—而不是您自己实现Node,您可以只创建AbstractNode并将一个可为其提供子节点的Children
对象传递给它,然后根据需要设置它的图标和显示名称。因此这是使Node
对象表示某些内容的简单方法,无需为Node
创建任何子类。
下一步是实现MyChildren
,以便初始节点下面具有子节点。
- 右键单击My Editor项目中的
org.myorg.myeditor
包并从弹出菜单中选择New > Java Class。 - 在New Java Class向导中,将该类命名为“MyChildren”,然后单击Finish或按Enter键来创建该类。
- 修改该类的签名以便展开
Children.Keys
:
class MyChildren extends Children.Keys {
- 按Ctrl-Shift-I键执行Fix Imports
- 将插入符号放置到类签名行中。当空白处出现灯泡状图案时,按Alt-Enter键,然后再次按Enter键以接受“Implement all Abstract Methods”提示。这将添加一个
createNodes (Object key)
方法—这是您将创建节点的位置,这些节点将是您的根节点的子节点。 - 但是首先,您需要覆盖一个方法—
addNotify
。由于使用的是Swing组件中的addNotify()
模式,因此Children.Keys.addNotify()
将在
第一次关注这个Children对象时得到调用—即第一次询问它的子节点。因此您可以延迟创建子Node,直到用户实际上已经在视图中展开了父节点并且需要查看子节点为止。将插入符号放置在源文件中的某个位置并按Alt-Insert键。然后选择“Override Method...”。在出现的对话框中,展开“Children”,选择addNotify()
方法,然后单击OK或按Enter键。 - 实现
addNotify()
方法如下所示:
protected void addNotify() {
APIObject[] objs = new APIObject[5];
for (int i = 0; i < objs.length; i++) {
objs[i] = new APIObject();
}
setKeys (objs);
}
正如您从名称Children.Keys
中所猜测的一样,您的父类所执行的操作就是获取一个数组或关键对象的Collection
,并充当它们的Node
s的工厂。因此,您在addNotify()
中调用setKeys()
,因为addNotify()
告诉您某些操作将询问子节点。对于数组中的每个元素或您传递给setKeys()
的集合,都将调用一次createNodes()
(请注意,这意味着如果必要的话可以让多个节点代表一个对象)。
- 现在您需要实现为所有这些节点实际创建Node对象的代码。按照如下代码实现
createNodes()
:
protected Node[] createNodes(Object o) {
APIObject obj = (APIObject) o;
AbstractNode result = new AbstractNode (new MyChildren(), Lookups.singleton(obj));
result.setDisplayName (obj.toString());
return new Node[] { result };
}
- 按Ctrl-Shift-I键执行Fix Imports。
- 最后一步是安装一些管道代码,这些代码会将资源管理器连接到TopComponent的查找。首先,从类定义的前面删除以下行
private final InstanceContent content = new InstanceContent();
—您将使用一个实用工具将选定的Node
的Lookup
连接到您的组件的Lookup
。
- 修改
MyEditor
的构造函数使它类似以下内容:
public MyEditor() {
initComponents();
associateLookup (ExplorerUtils.createLookup(mgr, getActionMap()));
mgr.setRootContext(new AbstractNode(new MyChildren()));
setDisplayName ("My Editor");
}
运行教程
您可能已经注意到,由于向创建的每个AbstractNode
传递了
MyChildren
的一个新实例,因此最后您将看到一个具有无限深度的APIObject
树
—每个Node
将有五个子Node
,每个子Node
都拥有其自己的APIObject
。
现在您可以准备运行了,因此右键单击SelectionSuite
并选择Clean and Build,然后再次右键单击并从弹出菜单中选择Run。当NetBeans启动时,使用File菜单上的Open Editor操作打开MyEditor
的一个实例。
注意,当您单击和/或展开不同的节点时,查看器和属性页都会进行更新以显示属于每个节点的APIObject
,如下所示:
探究资源管理器
既然您已经拥有了上述代码,那么现在来研究一下NetBeans中其他可用的组件,这些组件还可以呈现一个Node
及其子节点。通过在表单编辑器中打开MyEditor
并更改Custom Creation Code属性以使用另一个组件即可实现。对于其中一些组件,您需要将JScrollPane
替换为不同类型的组件(看上去很简单,只需从表单编辑器中删除JScrollPane
,然后向构造函数中添加代码add (new BeanTreeView(), BorderLayout.CENTER)
即可)
。
其中一些选项如下:
l ListView—显示JList中的节点(您可以设置它如何加深Node层次结构)
l TreeTableView—一个树表,该表最左侧的列是一个树。
l ChoiceView—Node及其子节点的组合框视图
l
MenuView—一个JButton
,它弹出一个Node及其子节点菜单。
l IconView—一个以相同间隔的图标显示Node子节点的组件,与Windows资源管理器不同
处理多项选择
您可能已经注意到Node的基本树视图BeanTreeView
,它允许您一次选择多个Node。因此,可能需要修改您的查看器组件以显示有关所有选定节点的信息:
- 在编辑器中,打开My Viewer项目中的
org.myorg.myviewer.MyViewerTopComponent
。 - 用以下代码替换
resultChanged()
侦听器方法:
public void resultChanged(LookupEvent lookupEvent) {
Lookup.Result r = (Lookup.Result) lookupEvent.getSource();
Collection c = r.allInstances();
if (!c.isEmpty()) {
StringBuffer text1 = new StringBuffer();
StringBuffer text2 = new StringBuffer();
for (Iterator i = c.iterator(); i.hasNext();) {
APIObject o = (APIObject) i.next();
text1.append (o.getIndex());
text2.append (o.getDate().toString());
if (i.hasNext()) {
text1.append (',');
text2.append (',');
}
}
jLabel1.setText (text1.toString());
jLabel2.setText (text2.toString());
} else {
jLabel1.setText("[no selection]");
jLabel2.setText ("");
}
}
因此,您可以看到ExplorerUtils
创建的Lookup
不仅代理所选
Node
的
Lookup
,
而且它还正确代理多个Node
的
Lookup
。
概念回顾
回顾此处涉及的几个概念:
l
Lookup
就像Map
,它的键是类,值为这些类的实例。对于将Lookup
视为对象出入的位置也非常有用,您可以订阅特定类型对象的到达和离开通知。
l
Utilities.actionsGlobalContext()
是一个Lookup
,它代理TopComponent
当前具有键盘焦点的Lookup
,并且当焦点移动到另一个组件时激活更改。
l
Node
是可以通过Explorer API在树、列表或其他组件中显示的表示对象。每个Node
都拥有自己的Lookup
。
l
就像Utilities.actionsGlobalContext
代理TopComponent的Lookup
一样(因此您可以只要求查找结果并且侦听其中的更改,而不必亲自跟踪焦点更改),因此由ExplorerUtils.createLookup(ExplorerManager, ActionMap)
创建的Lookup
将创建一个Lookup
,该Lookup
自动代理在Explorer组件中选定的Node
的
Lookup
。
下一步
因此,现在您已经拥有一个显示Node
的视图,这些Node
显示一些底层模型对象(本例中为APIObject
)。在下一部教程中,将介绍如何使用操作、属性以及更多彩色显示名称来增强已经创建的Node。