NetBeans管理教程三:NetBeans Nodes API

本教程演示了如何使用NetBeans Nodes API的一些特性。包括以下内容

l         使用图标装饰Nodes

l         使用HTML标记增强Node的显示效果

l         在属性表中创建显示属性

l         提供NodeAction

入门

本教程是NetBeans选择管理教程的延续该教程讲述了如何使用Lookup来管理NetBeans窗口系统中的选择后续教程演示了在管理选择过程中如何使用Nodes API

要下载完整的示例,单击此处

本教程使用在第一部教程中创建并在第二部教程中改进过的代码作为基础。如果您没有学习过这些教程,建议首先学习这些教程。

创建Node子类

正如在前一部教程中提到的一样Node属于表示对象。这意味着,它们本身不是一个数据模型 —— 而是底层数据模型 的一个表示层。在NetBeans IDEProjectsFiles窗口中可以看到在使用Node时底层数据模型为磁盘上的文件。在IDEServices窗口中,可以看到,当底层数据是NetBeans运行时环境的可配置内容时使用Node,比如可用的应用服务器和数据库。

作为表示层Node向它们模拟的对象添加人类友好的属性。基本属性包括

l         显示名称 —— 一个人类可读的、用户友好的显示名称

l         描述 —— 人类可读的、用户友好的描述通常显示为一个工具提示

l         图标 —— 一些图形化表示显示的对象及其可能状态的符号

l         动作 —— 右键单击节点时出现在上下文菜单中的动作可以由用户调用

在上一部教程中使用MyChildren类创建Node方法是首先调用

new AbstractNode (new MyChildren(), Lookups.singleton(obj))
  
  

然后调用setDisplayName(obj.toString())从而提供一个基本的显示名称。还可以使用很多方法使Node更具用户友好性。首先需要创建一个Node 子类:

  1. My Editor项目右键单击org.myorg.myeditor包并选择New > Java Class
  2. 打开向导后将类命名为MyNode并按下Enter或单击Finish
  3. 将类的签名和构造函数更改为
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");
  
  
    }
  
  
  1. 在代码编辑器中打开同一个包的MyEditor
  2. 将构造函数中的以下行:
mgr.setRootContext(new AbstractNode(new MyChildren()));
  
  
setDisplayName ("My Editor");
  
  

替换为以下代码:

mgr.setRootContext(new MyNode());
  
  
  1. 现在对Children对象进行类似更改。在编辑器中打开MyChildren类并将其createNodes方法更改为
protected Node[] createNodes(Object o) {
  
  
APIObject obj = (APIObject) o;
  
  
return new Node[] { new MyNode(obj) };
  
  
}
  
  

  
  
   
    
  
  

使用HTML增强显示名称

现在代码可以运行了但是目前为止您做的所有工作都只与逻辑有关。 一切和原来一样。惟一的区别(用户看不到)是,您现在使用了Node子类,而不只是AbstractNode

要做的第一件事是提供一个增强的显示名称。NodeExplorer API支持HTML的一个有限子集,可以使用这个子集增强Node标签在Explorer UI组件中的显示效果。支持以下标记

l         字体颜色 —— 使用标准html语法不支持字体大小和设置但支持字体颜色

l         字体样式标记 —— bius标记分别表示粗体、斜体、下划线和删除线

l         SGML实体的一个有限子集"<&‘’“”–—≠≤≥©®™ 

因为APIObject不包含特殊数据它只包含一个整数和一个创建日期所以您需要扩展这个示例并决定将奇数编号的APIObject显示为蓝色文本。

  1. 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;
  
  
    }
  
  
}
  
  
  1. 上面的代码完成的任务如下当绘图时显示节点的Explorer组件首先调用getHtmlDisplayName()如果组件获得非空的返回值,那么它将用收到的HTML字符串和一个快速的、轻量型HTML呈现器来呈现节点。如果返回值为空,那么它将回滚到由getDisplayName()返回的值。通过这种方法任何MyNode只要其APIObject的索引不能被2整除,那么这个MyNode将具有一个非空的HTML显示名称。

再次运行套件您会看到如下内容

getDisplayName() getHtmlDisplayName()是两个独立的方法这有两个原因首先,这是最优化设置;其次您将会看到它可以将HTML字符串连接起来而不需要移除<html>标记。

可以进一步增强这一点 —— 在上一部教程中日期包含在HTML字符串中此处已经将日期移除了。现在让HTML字符串稍微复杂一点,为所有节点提供HTML显示名称。

  1. 修改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;
  
  
    }
  
  
}
  
  
  1. 再次运行套件现在您将看到以下内容

可以稍作改动来改善显示效果当前在HTML中使用的是硬编码的颜色。 虽然NetBeans可以有不同的外观,但是不能保证硬编码的颜色与树或显示Node的其他UI组件的背景色不同或差别较大。

NetBeans HTML呈现器对HTML规范进行了扩展它可以通过传递UIManager键来查找颜色。Swing使用的外观提供了一个UIManager它管理给定外观使用的颜色和字体的名称-值映射。大多数但不是全部外观通过调用UIManager.getColor(String)来查找用于不同GUI元素的颜色其中字符串键是某个一致的值。因此,使用来自UIManager的值,可以保证总是生成可读的文本。将使用的两个键是“textText”和“controlShadow”,前者返回文本的默认颜色(通常为黑色,除非使用带有黑色背景主题的外观), 后者提供一个与默认控制背景颜色相对的颜色,但差别不是太大。

  1. 修改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;
  
  
    }
  
  
}
  
  
  1. 再次运行套件您应该看到如下结果

您将会注意到原来的蓝色现在变成了黑色。使用UIManager.getColor("textText")的值可以保证文本在任何外观下都是可读的这非常有用另外在用户界面中应该谨慎使用颜色以避免搞得像水果色拉一样。如果想在UI中使用更加个性的颜色,最好找到一个始终满足要求的UIManager/值对,或者创建一个ModuleInstall类并从UIManager导出需要的颜色,如果知道外观的颜色主题,那么可以根据每个外观对其进行硬编码(if ("aqua".equals(UIManager.getLookAndFeel().getID())……)

提供图标

使用图标是一个明智的选择,它可以增强用户界面的效果。因此,提供16x16像素的图标是另一种改进UI外观的方法。使用图标的一个缺陷是,不能通过图标传递较多的信息 —— 没有太多的像素。另一个缺陷是(显示名称也有同样的缺陷)不能只使用颜色来区分节点 —— 世界上有许多色盲。

提供一个图标非常简单 —— 只需载入一个图像并进行设置。需要使用GIFPNG文件。如果没有这种格式的文件,可以使用下面的文件:

  1. 将上面的图像或另一个16x16 PNGGIF文件复制到MyEditor类所在的包中。
  2. 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() 更理想,因为它具有更好的缓存行为而且支持商标图像。

  1. 如果现在运行代码您将会注意到图标只被应用到部分节点而没有应用到其他节点上。这是因为默认情况下为展开的Node和未展开的Node使用不同的图标。要避免这一点,只需覆盖另一个方法。

将以下方法添加到MyNode

public Image getOpenedIcon(int i) {
  
  
return getIcon (i);
  
  
}
  
  
  1. 现在如果运行代码所有节点将拥有同样的图标如下图所示。

操作和节点

将要处理的Node的另一个方面是操作。一个Node拥有一个弹出菜单,其中包含用户可以调用的针对此Node的操作。javax.swing.Action的任何子类都可由一个Node提供而且都会显示在弹出菜单中。此外,还涉及到一个呈现器 的概念,稍后将会讨论。

首先,创建一个可以用于节点的简单操作:

  1. MyNode getActions()方法重写为:
public Action[] getActions (boolean popup) {
  
  
return new Action[] { new MyAction() };
  
  
}
  
  
  1. 现在创建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);
  
  
    }
  
  
} 
  
  
  1. 再次运行套件并注意当右键单击一个节点时会显示一个菜单项

当选择菜单项时将调用该操作

呈现器

当然有时候想在弹出菜单中显示一个子菜单、复选框菜单项或者其他某个组件而不是一个JMenuItem。这很容易实现:

  1. 添加MyAction的签名以实现Presenter.Popup
private class MyAction extends AbstractAction implements Presenter.Popup {
  
  
  1. Ctrl-Shift-I修正导入的内容。
  2. 当在空白处出现灯泡状图案时将插入符号放在MyAction的类签名行中并按下Alt-Enter键,然后接受提示Implement All Abstract Methods
  3. 实现新创建的方法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;
  
  
}
  
  
  1. 再次运行套件显示结果如下

结果太令人兴奋了 —— 现在有了一个叫做Submenu的子菜单它包含两个相同的菜单项。但是,您应该举一反三 —— 如果想返回一个JCheckBoxMenuItem或其他类型的菜单项,也可以使用此方法。

警告也可以使用Presenter.Menu来提供一个不同的组件以显示主菜单的其他任何操作但是 某些Macintosh Mac OS-X版本不能很好地处理嵌入到菜单项中的随机Swing组件。为安全起见,不要在主菜单中使用除JMenuJMenuItem子类以外的其他组件。

属性和属性表

本教程将要讨论的最后一个主题是属性。您可能知道NetBeans IDE包含一个“属性表”,这个表可以显示Node的“属性”。“属性”的确切含义依赖于实现Node的方式。属性其实就是拥有一个Java类型的名称-值对,这些名称-值对经过分组并在属性表中显示——其中可写的属性可以通过其属性编辑器 来编辑(参见java.beans.PropertyEditor,获得关于属性编辑器的大致信息)。

因此Node表达的其实是这样一种思想一个节点可以有多个属性可以在属性表上查看这些属性以及可选编辑这些属性。很容易实现这一点。在Nodes API中有一个很方便的类Sheet,它表示一个NodeNode的完整属性集。可以向其添加Sheet.Set实例该实例表示属性集”,属性集作为一组属性出现在属性表中。

  1. 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;
  
  

  
  
   
    
  
  
}
  
  
  1. Ctrl-Shift-I修正导入的内容
  2. 右键单击模块套件并选择Run利用安装的套件模块启动NetBean的一个副本。
  3. 使用File > Open Editor显示编辑器。
  4. 选择Window > Properties显示NetBeans属性表。
  5. 单击编辑器窗口并在不同节点间移动选择注意属性表正在更新就像MyViewer组件所做的一样如下图所示

上面的代码利用了一个非常方便的类PropertySupport.Reflection可以通过此类传递一个对象、类型、gettersetter方法名称它将创建一个Property对象该对象可以读取(也可以写入当前对象的属性。因此我们使用PropertySupport.Reflection将一个Property对象连接到APIObjectgetIndex()方法。

如果希望将Property对象用于一个底层模型对象上几乎所有的getters/setters getter/setter方法那么您也许需要使用子类BeanNode这个子类是一个能够赋给一个随机对象的Node的完整实现。 而且需要通过反射的方式为这个子类创建所有必须的属性(并监听更改)(可以为该节点所表示的对象的类创建一个BeanInfo,用以控制表示属性的精确程度)。

警告设置属性的name非常重要。属性对象根据名称测试它们是否相等。如果向Sheet.Set添加了一些属性,并且这些属性可能会消失,很可能是因为没有设置其名称 —— 因此,如果向HashSet添加一个与另一个属性同名(空)的属性,则后添加的属性将会取代先添加的属性。

-写属性

要进一步掌握此概念还需要一个读/写属性。因此下一步将要添加对APIObject的一些支持,以设置Date属性。

  1. 在代码编辑器中打开org.myorg.myapi.APIObject
  2. 从声明date字段的行移除final关键字
  3. APIObject添加以下settersetter和属性更改支持方法:
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));
  
  
    }
  
  
}
  
  
  1. 现在APIObject调用上面的fire方法
public void setDate(Date d) {
  
  
Date oldDate = date;
  
  
date = d;
  
  
fire("date", oldDate, date);
  
  
 }
  
  
  1. MyNode.createSheet()更改dateProp的声明方式使其既可写又可读
Property dateProp = new PropertySupport.Reflection(obj, Date.class, "date");
  
  

现在,不用指定显式的gettersetter只需提供属性名称gettersetter然后PropertySupport.Reflection将为我们找到所需的gettersettergettersetter方法并且实际上它也会自动找到addPropertyChangeListener()方法

  1. 重新运行模块套件而且请注意您现在可以在MyEditor中选择一个MyNode实例并实际地编写日期值如下所示

注意结果会持续到重新启动IDE的时候

然而此代码中仍然存在bug当更改Date属性时也应该更新代码的显示名称。因此,您将对MyNode进行一次或多次更改,并让其监听APIObject上的属性更改。

  1. 修改MyNode的签名从而让其实现java.beans.PropertyChangeListener
public class MyNode extends AbstractNode implements PropertyChangeListener {
  
  
  1. Ctrl-Shift-I修正导入的内容
  2. 将插入符放在签名行中接受提示Implement All Abstract Methods
  3. 向带有参数APIObjec的构造函数添加以下行
obj.addPropertyChangeListener(WeakListeners.propertyChange(this, obj));
  
  

注意在此处您在org.openide.util.WeakListeners. 上使用了一个实用方法。这是一种避免内存泄漏的技术 —— APIObject 将只弱引用其MyNode因此如果Node的父节点是折叠的Node 可能被垃圾收集。如果Node一直APIObject的侦听器列表引用,那么这就是一种内存泄漏。在本文的例子中,Node实际上拥有APIObject,因此这并不是一个严重的问题 —— 但是在实际的程序中,数据模型(比如磁盘上的文件)中的对象可能长期处于活动状态,比Node显示给用户的时间长的多。如果要向从未显式删除过的对象添加一个侦听器,最好使用WeakListeners,否则将可能引起内存泄漏,这在以后变成一个很头疼的问题。如果实例化一个单独的侦听器类,请确保从连接到它的代码中对其进行强引用 —— 否则,只要它被添加,就会被垃圾收集。

  1. 最后,实现propertyChange()方法:
public void propertyChange(PropertyChangeEvent evt) {
  
  
if ("date".equals(evt.getPropertyName())) {
  
  
this.fireDisplayNameChange(null, getDisplayName());
  
  
    }
  
  
}
  
  
  1. 再次运行模块套件MyEditor窗口中选择一个MyNode并更改其Date属性 —— 注意Node的显示名称现在已被正确更新如下图所示其中2009同时反映到节点和属性表中。

分组属性集

您也许注意到当运行MatisseNetBeans IDE的表单编辑器在属性表顶部有一个按钮集,可用来在各组属性集之间转换。

通常建议只在拥有大量属性时才这样做而不应该为获得易用性 使用大量属性。然而,如果觉得需要将属性集进行分组,这很容易实现。

Property 拥有getValue()setValue()方法PropertySet一样它们都从java.beans.FeatureDescriptor继承了这两种方法。在某些情形中可以使用这些方法在给定的PropertyPropertySet与属性表或某些类型的属性编辑器之间传递临时的“提示”(例如,将默认的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;
  
  
        
  
  
    }
  
  
  1. 再次运行套件并请注意现在在属性表顶部有一些按钮每个按钮下有一个属性如下图所示

属性表注意事项

如果使用过NetBeans 3.6或更旧的版本您可能会注意到较旧的NetBeans版本大量使用属性表并将其作为UI的核心元素而现在属性表没有这么流行了。原因很简单:基于属性表的UI的用户友好性不是很好。这并不是说不要使用属性表,而是要明智地使用。如果可以提供一个具有出色GUI的定制器,那么将它贡献出来吧 —— 用户将会感谢您。

如果一个对象拥有大量属性那么尝试将最可能的设置组合封装成一些总体设置。例如,考虑如何设置一个管理Java类导入的工具?您可以使用整数设置可以导入通配符的包的使用次数,也可以设置在导入一个完全限定类名称之前该名称的使用次数,以及其他数值。或者询问自己,用户试图做什么?在这种情况下,要么删除导入语句,要么删除完全限定名称。因此,像low noisemedium noisehigh noise这样的设置,其中“noise”表示编辑的源文件中完全限定类/包名称的数量,这种设置就很不错而且更容易使用。如果使用的方法能使用户更加轻松,那就采用这种方法。

概念回顾

本教程探讨了以下思想:

l         节点是一个表示层

l         可以使用有限的HTML子集定制Node的显示名称

l         节点拥有图标而且可以为创建的节点定制图标

l         节点拥有操作实现Presenter.Popup的操作可以提供其自身的组件并显示在弹出菜单中同样也可以使用Presenter.Menu将其组件显示在主菜单项中, 使用Presenter.Toolbar显示在工具栏项中

l         节点拥有属性这些属性可以显示在属性表中。

下一步

您现在已经了解如何将属性表的更多优点应用到NetBeans中。下一部教程中,您将了解如何编写定制编辑器,以及定制用于属性表的内联编辑器。

 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值