本教程是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 "
APIObject " + obj.getIndex() + "";
} else {
return null;
}
}
上面的代码完成的任务如下:当绘图时,显示节点的Explorer组件首先调用getHtmlDisplayName()。如果组件获得非空的返回值,那么它将用收到的HTML字符串和一个快速的、轻量型HTML呈现器来呈现节点。如果返回值为空,那么它将回滚到由getDisplayName()返回的值。通过这种方法,任何MyNode,只要其APIObject的索引不能被2整除,那么这个MyNode将具有一个非空的HTML显示名称。
再次运行套件,您会看到如下内容:
getDisplayName()和 getHtmlDisplayName()是两个独立的方法,这有两个原因:首先,这是最优化设置;其次,您将会看到,它可以将HTML字符串连接起来,而不需要移除标记。
可以进一步增强这一点 —— 在上一部教程中,日期包含在HTML字符串中,此处已经将日期移除了。现在让HTML字符串稍微复杂一点,为所有节点提供HTML显示名称。
修改getHtmlDisplayName()方法如下:
public String getHtmlDisplayName() {
APIObject obj = getLookup().lookup (APIObject.class);
if (obj != null) {
return "
APIObject " + obj.getIndex() + "" +
"
" + obj.getDate() + "";
} 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 "
APIObject " + obj.getIndex() + "" +
"
" + obj.getDate() + "";
} 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,它表示一个NodeNode的完整属性集。可以向其添加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 getter/setter方法,那么您也许需要使用子类BeanNode,这个子类是一个能够赋给一个随机对象的Node的完整实现。 而且需要通过反射的方式为这个子类创建所有必须的属性(并监听更改)(可以为该节点所表示的对象的类创建一个BeanInfo,用以控制表示属性的精确程度)。
警告:设置属性的name非常重要。属性对象根据名称测试它们是否相等。如果向Sheet.Set添加了一些属性,并且这些属性可能会消失,很可能是因为没有设置其名称 —— 因此,如果向HashSet添加一个与另一个属性同名(空)的属性,则后添加的属性将会取代先添加的属性。
更详细内容,请点击博客地址查看全文:http://blog.csdn.net/java060515/archive/2008/01/16/2046943.aspx