本教程演示了如何使用NetBeans Nodes API的一些特性。包括以下内容:
l 使用图标装饰Nodes
l 使用HTML标记增强Node的显示效果
l 在属性表中创建显示属性
l 提供Node的Action
入门
本教程是NetBeans选择管理教程的延续,该教程讲述了如何使用Lookup
来管理
NetBeans窗口系统中的选择,其后续教程演示了在管理选择过程中如何使用Nodes API。
要下载完整的示例,单击此处。
本教程使用在第一部教程中创建并在第二部教程中改进过的代码作为基础。如果您没有学习过这些教程,建议首先学习这些教程。
创建Node子类
正如在前一部教程中提到的一样,Node属于表示对象。这意味着,它们本身不是一个数据模型 —— 而是底层数据模型 的一个表示层。在NetBeans IDE的Projects或Files窗口中,可以看到在使用Node时底层数据模型为磁盘上的文件。在IDE的Services窗口中,可以看到,当底层数据是NetBeans运行时环境的可配置内容时使用Node
,比如可用的应用服务器和数据库。
作为表示层,Node
向它们模拟的对象添加人类友好的属性。基本属性包括:
l 显示名称 —— 一个人类可读的、用户友好的显示名称
l 描述 —— 人类可读的、用户友好的描述,通常显示为一个工具提示
l 图标 —— 一些图形化表示显示的对象及其可能状态的符号
l 动作 —— 右键单击节点时出现在上下文菜单中的动作,可以由用户调用
在上一部教程中,使用MyChildren
类创建
Node
,方法是首先调用
new AbstractNode (new MyChildren(), Lookups.singleton(obj));
然后调用setDisplayName(obj.toString())
,
从而提供一个基本的显示名称。
还可以使用很多方法使Node
更具用户友好性。
首先需要创建一个Node
子类:
- 在My Editor项目中,右键单击
org.myorg.myeditor
包并选择New > Java Class。 - 打开向导后,将类命名为“MyNode”并按下Enter键或单击Finish。
- 将类的签名和构造函数更改为:
public class MyNode extends AbstractNode {
public MyNode(APIObject obj) {
super (new MyChildren(), Lookups.singleton(obj));
setDisplayName ( "APIObject " + obj.getIndex());
}
public MyNode() {
super (new MyChildren());
setDisplayName ("Root");
}
- 在代码编辑器中打开同一个包的
MyEditor
。 - 将构造函数中的以下行:
mgr.setRootContext(new AbstractNode(new MyChildren()));
setDisplayName ("My Editor");
替换为以下代码:
mgr.setRootContext(new MyNode());
- 现在对Children对象进行类似更改。在编辑器中打开
MyChildren
类并将其createNodes
方法更改为:
protected Node[] createNodes(Object o) {
APIObject obj = (APIObject) o;
return new Node[] { new MyNode(obj) };
}
使用HTML增强显示名称
现在代码可以运行了,但是目前为止您做的所有工作都只与逻辑有关。 一切和原来一样。惟一的区别(用户看不到)是,您现在使用了Node子类,而不只是AbstractNode。
要做的第一件事是提供一个增强的显示名称。Node和Explorer API支持HTML的一个有限子集,可以使用这个子集增强Node
标签在
Explorer UI组件中的显示效果。支持以下标记:
l 字体颜色 —— 使用标准html语法不支持字体大小和设置,但支持字体颜色
l 字体样式标记 —— b、i、u和s标记分别表示粗体、斜体、下划线和删除线
l SGML实体的一个有限子集:"、<、&、‘、’、“、”、–、—、≠、≤、≥、©、®、™和
因为APIObject
不包含特殊数据
,
它只包含一个整数和一个创建日期
,
所以您需要扩展这个示例
,
并决定将奇数编号的
APIObject
显示为蓝色文本。
- 向
MyNode
添加以下方法
:
public String getHtmlDisplayName() {
APIObject obj = getLookup().lookup (APIObject.class);
if (obj!=null && obj.getIndex() % 2 != 0) {
return "<font color='0000FF'>APIObject " + obj.getIndex() + "</font>";
} else {
return null;
}
}
- 上面的代码完成的任务如下:当绘图时,显示节点的Explorer组件首先调用
getHtmlDisplayName()
。如果组件获得非空的返回值,那么它将用收到的HTML字符串和一个快速的、轻量型HTML呈现器来呈现节点。如果返回值为空,那么它将回滚到由getDisplayName()
返回的值。
通过这种方法,任何MyNode
,只要其APIObject
的索引不能被2整除,那么这个MyNode
将具有一个非空的HTML显示名称。
再次运行套件,您会看到如下内容:
getDisplayName()
和
getHtmlDisplayName()
是两个独立的方法
,
这有两个原因
:
首先,这是最优化设置;其次,您将会看到,它可以将HTML字符串连接起来,而不需要移除<html>标记。
可以进一步增强这一点 —— 在上一部教程中,日期包含在HTML字符串中,此处已经将日期移除了。现在让HTML字符串稍微复杂一点,为所有节点提供HTML显示名称。
- 修改
getHtmlDisplayName()
方法如下:
public String getHtmlDisplayName() {
APIObject obj = getLookup().lookup (APIObject.class);
if (obj != null) {
return "<font color='#0000FF'>APIObject " + obj.getIndex() + "</font>" +
"<font color='AAAAAA'><i>" + obj.getDate() + "</i></font>";
} else {
return null;
}
}
- 再次运行套件,现在您将看到以下内容:
可以稍作改动来改善显示效果:当前在HTML中使用的是硬编码的颜色。 虽然NetBeans可以有不同的外观,但是不能保证硬编码的颜色与树或显示Node的其他UI组件的背景色不同或差别较大。
NetBeans HTML呈现器对HTML规范进行了扩展,它可以通过传递UIManager键来查找颜色。Swing使用的外观提供了一个UIManager,它管理给定外观使用的颜色和字体的名称-值映射。大多数(但不是全部)外观通过调用UIManager.getColor(String)
来查找用于不同
GUI
元素的颜色
,
其中字符串键是某个一致的值。
因此,使用来自UIManager的值,可以保证总是生成可读的文本。将使用的两个键是“textText”和“controlShadow”,前者返回文本的默认颜色(通常为黑色,除非使用带有黑色背景主题的外观), 后者提供一个与默认控制背景颜色相对的颜色,但差别不是太大。
- 修改
getHtmlDisplayName()
方法如下:
public String getHtmlDisplayName() {
APIObject obj = getLookup().lookup (APIObject.class);
if (obj != null) {
return "<font color='!textText'>APIObject " + obj.getIndex() + "</font>" +
"<font color='!controlShadow'><i>" + obj.getDate() + "</i></font>";
} else {
return null;
}
}
- 再次运行套件,您应该看到如下结果:
您将会注意到原来的蓝色现在变成了黑色。使用UIManager.getColor("textText")
的值可以保证文本在任何外观下都是可读的
,
这非常有用
;
另外
,
在用户界面中应该谨慎使用颜色
,
以避免搞得像
水果色拉一样。如果想在UI中使用更加个性的颜色,最好找到一个始终满足要求的UIManager键/值对,或者创建一个ModuleInstall类并从UIManager中导出需要的颜色,如果知道外观的颜色主题,那么可以根据每个外观对其进行硬编码(if ("aqua".equals(UIManager.getLookAndFeel().getID())
……)
。
提供图标
使用图标是一个明智的选择,它可以增强用户界面的效果。因此,提供16x16像素的图标是另一种改进UI外观的方法。使用图标的一个缺陷是,不能通过图标传递较多的信息 —— 没有太多的像素。另一个缺陷是(显示名称也有同样的缺陷)不能只使用颜色来区分节点 —— 世界上有许多色盲。
提供一个图标非常简单 —— 只需载入一个图像并进行设置。需要使用GIF或PNG文件。如果没有这种格式的文件,可以使用下面的文件:
- 将上面的图像,或另一个16x16 PNG或GIF文件复制到
MyEditor
类所在的包中。
- 向
MyNode
类添加以下方法:
public Image getIcon (int type) {
return Utilities.loadImage ("org/myorg/myeditor/icon.png");
}
注意,可能有不同的图标大小和样式 —— 传递给getIcon()
的可能的英寸值是java.beans.BeanInfo
上的一个常量,比如 BeanInfo.ICON_COLOR_16x16
。另外,尽管可以使用标准JDK ImageIO.read()
装载图像
,
但是
Utilities.loadImage()
更理想,因为它具有更好的缓存行为,而且支持商标图像。
- 如果现在运行代码,您将会注意到:图标只被应用到部分节点,而没有应用到其他节点上。这是因为默认情况下为展开的
Node
和未展开的Node
使用不同的图标。要避免这一点,只需覆盖另一个方法。
将以下方法添加到MyNode
:
public Image getOpenedIcon(int i) {
return getIcon (i);
}
- 现在,如果运行代码,所有节点将拥有同样的图标,如下图所示。
操作和节点
将要处理的
Node
的另一个方面是操作。一个Node
拥有一个弹出菜单,其中包含用户可以调用的针对此Node
的操作。javax.swing.Action
的任何子类都可由一个Node
提供,而且都会显示在弹出菜单中。此外,还涉及到一个呈现器 的概念,稍后将会讨论。
首先,创建一个可以用于节点的简单操作:
- 将
MyNode
的getActions()
方法重写为:
public Action[] getActions (boolean popup) {
return new Action[] { new MyAction() };
}
- 现在,创建
MyAction
类作为MyNode
的内部类:
private class MyAction extends AbstractAction {
public MyAction () {
putValue (NAME, "Do Something");
}
public void actionPerformed(ActionEvent e) {
APIObject obj = getLookup().lookup (APIObject.class);
JOptionPane.showMessageDialog(null, "Hello from " + obj);
}
}
- 再次运行套件,并注意当右键单击一个节点时,会显示一个菜单项:
当选择菜单项时,将调用该操作:
呈现器
当然,有时候想在弹出菜单中显示一个子菜单、复选框菜单项或者其他某个组件,而不是一个JMenuItem。这很容易实现:
- 添加
MyAction
的签名,以实现Presenter.Popup
:
private class MyAction extends AbstractAction implements Presenter.Popup {
- 按Ctrl-Shift-I修正导入的内容。
- 当在空白处出现灯泡状图案时,将插入符号放在
MyAction
的类签名行中并按下Alt-Enter键,然后接受提示“Implement All Abstract Methods”。 - 实现新创建的方法
getPopupPresenter()
,如下所示:
public JMenuItem getPopupPresenter() {
JMenu result = new JMenu("Submenu"); //remember JMenu is a subclass of JMenuItem
result.add (new JMenuItem(this));
result.add (new JMenuItem(this));
return result;
}
- 再次运行套件,显示结果如下:
结果太令人兴奋了 —— 现在有了一个叫做“Submenu”的子菜单,它包含两个相同的菜单项。但是,您应该举一反三 —— 如果想返回一个JCheckBoxMenuItem
或其他类型的菜单项,也可以使用此方法。
警告:也可以使用Presenter.Menu来提供一个不同的组件,以显示主菜单的其他任何操作,但是 某些Macintosh Mac OS-X版本不能很好地处理嵌入到菜单项中的随机Swing组件。为安全起见,不要在主菜单中使用除JMenu、JMenuItem子类以外的其他组件。
属性和属性表
本教程将要讨论的最后一个主题是属性。您可能知道NetBeans IDE包含一个“属性表”,这个表可以显示Node
的“属性”。
“属性”的确切含义依赖于实现Node
的方式。
属性其实就是拥有一个Java类型的名称-值对,这些名称-值对经过分组并在属性表中显示——其中可写的属性可以通过其属性编辑器 来编辑(参见java.beans.PropertyEditor
,获得关于属性编辑器的大致信息)。
因此,Node
表达的其实是这样一种思想
,
一个节点可以有多个属性
,
可以在属性表上查看这些属性
,
以及
(
可选
)
编辑这些属性。
很容易实现这一点。在Nodes API中有一个很方便的类Sheet
,它表示一个Node的完整属性集。可以向其添加Sheet.Set
实例,该实例表示“属性集”,属性集作为一组属性出现在属性表中。
- 将
MyNode.createSheet()
重写为:
protected Sheet createSheet() {
Sheet sheet = Sheet.createDefault();
Sheet.Set set = Sheet.createPropertiesSet();
APIObject obj = getLookup().lookup(APIObject.class);
try {
Property indexProp = new PropertySupport.Reflection(obj, Integer.class, "getIndex", null);
Property dateProp = new PropertySupport.Reflection(obj, Date.class, "getDate", null);
indexProp.setName("index");
dateProp.setName("date");
set.put(indexProp);
set.put(dateProp);
} catch (NoSuchMethodException ex) {
ErrorManager.getDefault();
}
sheet.put(set);
return sheet;
}
- 按Ctrl-Shift-I键修正导入的内容。
- 右键单击模块套件,并选择Run利用安装的套件模块启动NetBean的一个副本。
- 使用File > Open Editor显示编辑器。
- 选择Window > Properties显示NetBeans属性表。
- 单击编辑器窗口并在不同节点间移动选择,注意属性表正在更新,就像
MyViewer
组件所做的一样
,
如下图所示
:
上面的代码利用了一个非常方便的类:PropertySupport.Reflection
,
可以通过此类传递一个对象、类型、
getter
和
setter
方法名称
,
它将创建一个
Property对象,该对象可以读取(也可以写入)当前对象的属性。因此我们使用PropertySupport.Reflection
将一个
Property
对象连接到
APIObject
的
getIndex()
方法。
如果希望将Property
对象用于一个底层模型对象上几乎所有的
getters/setters方法
,
那么您也许需要使用子类
BeanNode
,
这个子类是一个能够赋给一个随机对象的
Node
的完整实现。
而且需要通过反射的方式为这个子类创建所有必须的属性(并监听更改)(可以为该节点所表示的对象的类创建一个BeanInfo
,用以控制表示属性的精确程度)。
警告:设置属性的name
非常重要。
属性对象根据名称测试它们是否相等。如果向Sheet.Set
添加了一些属性,并且这些属性可能会消失,很可能是因为没有设置其名称
——
因此,如果向
HashSet
添加一个与另一个属性同名(空)的属性,则后添加的属性将会取代先添加的属性。
读-写属性
要进一步掌握此概念,还需要一个读/写属性。因此下一步将要添加对APIObject
的一些支持,以设置
Date
属性。
- 在代码编辑器中打开
org.myorg.myapi.APIObject
。 - 从声明
date
字段的行移除final
关键字 - 向
APIObject
添加以下
setter和属性更改支持方法:
private List listeners = Collections.synchronizedList(new LinkedList());
public void addPropertyChangeListener (PropertyChangeListener pcl) {
listeners.add (pcl);
}
public void removePropertyChangeListener (PropertyChangeListener pcl) {
listeners.remove (pcl);
}
private void fire (String propertyName, Object old, Object nue) {
//Passing 0 below on purpose, so you only synchronize for one atomic call:
PropertyChangeListener[] pcls = (PropertyChangeListener[]) listeners.toArray(new PropertyChangeListener[0]);
for (int i = 0; i < pcls.length; i++) {
pcls[i].propertyChange(new PropertyChangeEvent (this, propertyName, old, nue));
}
}
- 现在,在APIObject中,调用上面的fire方法:
public void setDate(Date d) {
Date oldDate = date;
date = d;
fire("date", oldDate, date);
}
- 在
MyNode.createSheet()
中,更改dateProp
的声明方式,使其既可写又可读:
Property dateProp = new PropertySupport.Reflection(obj, Date.class, "date");
现在,不用指定显式的getter和setter,只需提供属性名称,然后PropertySupport.Reflection
将为我们找到所需的getter和setter方法(并且实际上,它也会自动找到addPropertyChangeListener()
方法)。
- 重新运行模块套件,而且请注意,您现在可以在
MyEditor
中选择一个MyNode
实例并实际地编写日期值,如下所示:
注意:结果会持续到重新启动IDE的时候。
然而,此代码中仍然存在bug:当更改Date属性时,也应该更新代码的显示名称。因此,您将对MyNode
进行一次或多次更改,并让其监听
APIObject
上的属性更改。
- 修改
MyNode
的签名,从而让其实现java.beans.PropertyChangeListener
:
public class MyNode extends AbstractNode implements PropertyChangeListener {
- 按Ctrl-Shift-I修正导入的内容。
- 将插入符放在签名行中,接受提示“Implement All Abstract Methods”。
- 向带有参数
APIObjec
的构造函数添加以下行:
obj.addPropertyChangeListener(WeakListeners.propertyChange(this, obj));
注意,在此处您在org.openide.util.WeakListeners
. 上使用了一个实用方法。这是一种避免内存泄漏的技术 —— APIObject
将只弱引用其MyNode
,因此如果Node
的父节点是折叠的,Node
可能被垃圾收集。如果Node
一直
被APIObject
的侦听器列表引用,那么这就是一种内存泄漏。在本文的例子中,Node
实际上拥有APIObject
,因此这并不是一个严重的问题 —— 但是在实际的程序中,数据模型(比如磁盘上的文件)中的对象可能长期处于活动状态,比Node
显示给用户的时间长的多。如果要向从未显式删除过的对象添加一个侦听器,最好使用WeakListeners
,否则将可能引起内存泄漏,这在以后变成一个很头疼的问题。如果实例化一个单独的侦听器类,请确保从连接到它的代码中对其进行强引用 —— 否则,只要它被添加,就会被垃圾收集。
- 最后,实现
propertyChange()
方法:
public void propertyChange(PropertyChangeEvent evt) {
if ("date".equals(evt.getPropertyName())) {
this.fireDisplayNameChange(null, getDisplayName());
}
}
- 再次运行模块套件,在
MyEditor
窗口中选择一个MyNode
,并更改其Date
属性 —— 注意,Node
的显示名称现在已被正确更新,如下图所示,其中2009年同时反映到节点和属性表中。
分组属性集
您也许注意到,当运行Matisse(NetBeans IDE的表单编辑器)时,在属性表顶部有一个按钮集,可用来在各组属性集之间转换。
通常,建议只在拥有大量属性时才这样做,而不应该为获得易用性而 使用大量属性。然而,如果觉得需要将属性集进行分组,这很容易实现。
Property
拥有getValue()
和setValue()
方法,与PropertySet
一样
(
它们都从
java.beans.FeatureDescriptor
继承了这两种方法)。在某些情形中可以使用这些方法在给定的Property
或
PropertySet
与属性表或某些类型的属性编辑器之间传递临时的“提示”(例如,将默认的
filechooser目录传递给一个java.io.File
编辑器)。
而且通过这种技术可以为一个和多个PropertySet
指定一个分组名称(用于在按钮上显示)。
在实际的编码中,这可能是一个本地化字符串,而不是硬编码字符串,如下所示:
1.
在代码编辑器中打开MyNode
2.
修改createSheet()
方法如下(其中蓝色的行是修改或添加的行):
protected Sheet createSheet() {
Sheet sheet = Sheet.createDefault();
Sheet.Set set = sheet.createPropertiesSet();
Sheet.Set set2 = sheet.createPropertiesSet();
set2.setDisplayName("Other");
set2.setName("other");
APIObject obj = getLookup().lookup (APIObject.class);
try {
Property indexProp = new PropertySupport.Reflection(obj, Integer.class, "getIndex", null);
Property dateProp = new PropertySupport.Reflection(obj, Date.class, "date");
indexProp.setName("index");
dateProp.setName ("date");
set.put (indexProp);
set2.put (dateProp);
set2.setValue("tabName", "Other Tab");
} catch (NoSuchMethodException ex) {
ErrorManager.getDefault();
}
sheet.put(set);
sheet.put(set2);
return sheet;
}
- 再次运行套件,并请注意,现在在属性表顶部有一些按钮,每个按钮下有一个属性,如下图所示:
属性表注意事项
如果使用过NetBeans 3.6或更旧的版本,您可能会注意到,较旧的NetBeans版本大量使用属性表,并将其作为UI的核心元素,而现在属性表没有这么流行了。原因很简单:基于属性表的UI的用户友好性不是很好。这并不是说不要使用属性表,而是要明智地使用。如果可以提供一个具有出色GUI的定制器,那么将它贡献出来吧 —— 用户将会感谢您。
如果一个对象拥有大量属性,那么尝试将最可能的设置组合封装成一些总体设置。例如,考虑如何设置一个管理Java类导入的工具?您可以使用整数设置可以导入通配符的包的使用次数,也可以设置在导入一个完全限定类名称之前该名称的使用次数,以及其他数值。或者询问自己,用户试图做什么?在这种情况下,要么删除导入语句,要么删除完全限定名称。因此,像low noise、medium noise和high noise这样的设置,其中“noise”表示编辑的源文件中完全限定类/包名称的数量,这种设置就很不错而且更容易使用。如果使用的方法能使用户更加轻松,那就采用这种方法。
概念回顾
本教程探讨了以下思想:
l 节点是一个表示层
l 可以使用有限的HTML子集定制Node的显示名称
l 节点拥有图标,而且可以为创建的节点定制图标
l 节点拥有操作;实现Presenter.Popup的操作可以提供其自身的组件并显示在弹出菜单中;同样也可以使用Presenter.Menu将其组件显示在主菜单项中, 使用Presenter.Toolbar显示在工具栏项中
l 节点拥有属性,这些属性可以显示在属性表中。
下一步
您现在已经了解如何将属性表的更多优点应用到NetBeans中。在下一部教程中,您将了解如何编写定制编辑器,以及定制用于属性表的内联编辑器。