本文来源:https://netbeans.apache.org/tutorials/60/nbm-nodesapi2.html
本教程显示了如何利用NetBeans中的Nodes API的某些功能。它显示了如何执行以下操作:
-
用图标装饰节点
-
使用HTML标记来增强节点的显示方式
-
创建要在属性表中显示的属性
-
提供来自节点的Action
本教程作为《Netbeans选择管理教程》的后续教程 ,该教程涵盖了如何使用Lookup在Netbeans窗口中管理,以及岩石如何在管理选择中使用Nodes API。
作为本教程的基础,本教程使用在第一个教程中创建并在第二个教程中进一步增强的源代码。如果您尚未完成这些教程,建议先进行。
创建一个Node子类
在上个教程所属,Node表示对象。这意味着它们本身不是数据模型,而是它们是基础数据模型的表示层。
在NetBeans IDE的“项目”或“文件”窗口中,可以看到在基础数据模型是磁盘上的文件的情况下使用的Node。在IDE的“服务”窗口中,可以看到在基础对象是NetBeans运行时环境的可配置方面(例如可用的应用程序服务器和数据库)的情况下使用它们。
作为表示层,Node向其建模的对象添加了对人类友好的属性。重要的是:
-
显示名称 -可读的,用户友好的显示名称
-
描述 -易于理解的描述,通常显示为工具提示
-
图标 —一些字形,以图形方式指示所显示对象的类型以及可能的状态
-
Action-右键单击节点时出现在上下文菜单上的Action,可由用户调用
在前面的教程中,您MyChildren
通过调用以下类来创建Node。
new AbstractNode (new MyChildren(), Lookups.singleton(obj));
然后调用setDisplayName(obj.toString())提供一个基本的显示名。要使节点更具用户友好性,还需要做很多工作。首先,你需要创建一个Node子类:
1、在MyEditor项目中,右键org.myorg.myeditor的package,新建 java class
2、命名为MyNode,继承MyNode 并修改构造函数
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");
}
3、打开同个package下的MyEditor文件,注释和添加一行
public MyEditor() {
initComponents();
associateLookup(ExplorerUtils.createLookup(mgr, getActionMap()));
// mgr.setRootContext(new AbstractNode(new MyChildren()));
// setDisplayName("My Editor");
mgr.setRootContext(new MyNode());
}
4、现在对Children对象进行类似的更改。MyChildren
在编辑器中打开该类,并createNodes
如下更改其方法:
protected Node[] createNodes(Object o) {
APIObject obj = (APIObject) o;
return new Node[] { new MyNode(obj) };
}
用HTML增强显示名称
该代码现在可以运行,但是到目前为止,您所要做的只是移动逻辑。它将完全像以前一样。您现在唯一的(非用户可见的)区别是使用Node子类,而不是仅使用AbstractNode。
您要做的第一件事是提供增强的显示名称。Nodes和Explorer API支持有限的HTML子集,您可以使用它们来增强Node的标签在Explorer UI组件中的显示方式。支持以下标记:
-
字体颜色-不支持字体大小和字体设置,但使用标准html语法支持颜色
-
字体样式标签-b,i,u和s标签-粗体,斜体,下划线,删除线
-
SGML实体的有限子集:“,<,&,‘,’,“,”,–,—,≠,≤,≥,©,®,&trade ;和 
扩展APIObject示例,并确定奇数APIObjects
应以蓝色文本显示。
@Override
public String getHtmlDisplayName() {
APIObject obj = getLookup().lookup(APIObject.class);
//if (obj!=null && obj.getIndex() % 2 != 0) {
if (obj != null && obj.getIndex() % 2 != 0) {
return "<font color='0000FF'>APIObject " + obj.getIndex() + "</font>";
} else {
return null;
}
}
教程中使用了“&” 不知所云,各位看客知道啥意思欢迎留言。
2、上面的代码完成的工作是:绘制时,显示节点的Explorer组件getHtmlDisplayName()
首先调用。如果返回非空值,它将使用接收到的HTML字符串和快速,轻量级的HTML渲染器进行渲染。如果为null,则它将退回到所返回的值getDisplayName()
。所以这样一来,任何MyNode
其APIObject
还没有能被2整除的指数将有一个非空的HTML显示名称。
再次运行,如下图
有两方面的原因getDisplayName()
,并getHtmlDisplayName()
为不同的方法:首先,它是一个优化; 第二,正如您将在后面看到的那样,它可以将HTML字符串组合在一起,而无需剥离<html>标记。
您可以进一步增强它-在上一教程中,日期已包含在HTML字符串中,并且已在此处将其删除。因此,让您的HTML字符串更复杂一点,并为所有节点提供HTML显示名称。
1、修改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可以在各种外观下运行,并且不能保证您的硬编码颜色不会与节点所出现的树或其他UI组件的背景颜色相同或非常接近。
NetBeans HTML渲染器对HTML规范进行了次要扩展,从而可以通过传递UIManager键来查找颜色。Swing使用的外观提供UIManager,用于管理给定外观使用的颜色和字体的名称-值映射。大多数(但不是全部)外观通过调用来找到用于不同GUI元素的颜色UIManager.getColor(String)
,其中字符串键是一些商定的值。因此,通过使用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;
}
}
2、再次运行
您会在上面注意到,您摆脱了蓝色,改用普通的旧黑色。使用UIManager.getColor("textText")
保证的价值,可以使我们的文本在任何外观和感觉下始终可读,这是有价值的;另外,应在用户界面中谨慎使用颜色,以避免愤怒的水果沙拉效果,如果您确实想在UI中使用更狂野的颜色,那么最好的选择是找到始终能获得所需颜色的UIManager键/值对,或者创建ModuleInstall类,并从UIManager中得到颜色派生色。或者,如果您知道外观颜色的主题,请根据外观对其进行硬编码(if ("aqua".equals(UIManager.getLookAndFeel().getID())…
).
提供图标
明智地使用图标还可以增强用户界面。因此,提供16x16像素的图标是改善UI外观的另一种方法。使用图标的一个警告是,不要试图通过图标传达太多信息,因为那里没有很多像素可以使用。适用于图标和显示名称的第二个警告是,永远不要只使用颜色来区分节点 -世界上有很多人是色盲的。
提供图标非常简单-您只需加载图像并进行设置。您需要具有GIF或PNG文件才能使用。如果您没有一个容易使用的产品,则可以使用以下一种产品:
1、将上面图像或者其他16x16 PNG或GIF复制到MyEditor该类相同的package中
2、将以下方法添加到MyNode类中
public Image getIcon (int type) {
return Utilities.loadImage ("org/myorg/myeditor/icon.png");
}
请注意,图标大小和样式可能会有所不同-传递给int的可能值getIcon()
是on上的常量java.beans.BeanInfo
,例如BeanInfo.ICON_COLOR_16x16
。另外,虽然可以使用标准JDK ImageIO.read()
加载图像,但Utilities.loadImage()
可以对其进行更优化,具有更好的缓存行为并支持图像商标。
3、现在运行代码,该图标用于某些节点,而不是用于其他节点。这样做的原因是,未展开和展开的图标通常使用不同的图标。解决此问题所需要的重新另外一个方法,getOpenedIcon展开节点图标
public Image getOpenedIcon(int i) {
return getIcon (i);
}
4、现在运行套件,所有节点具有正确的图标
Action和Node
您将处理Node的下一个方面是Action,节点有一个弹出式菜单,其中可以包含用户可以对该节点调用的操作。swing的任何子类。操作可以由节点提供,并将显示在其弹出菜单中。此外,还有一个关于presenters的概念,稍后会讲到。
1、在MyNode类中,重写getActions()方法
public Action[] getActions (boolean popup) {
return new Action[] { new MyAction() };
}
2、在MyNode创建内部类MyAction
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);
}
}
3、再次运行套件,请注意,右键点击节点时,显示一个菜单项
当选择菜单项时,将会调用该Action
Presenters(演讲者)
当然,有时您可能想提供一个子菜单或复选框菜单项或JMenuItem以外的其他组件来显示在弹出菜单中。这很简单:
1、MyAction实现Presenter.Popup,修复导入
private class MyAction extends AbstractAction implements Presenter.Popup {
2、实现抽象方法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;
}
3、再次运行,请注意,现在具有以下条件
结果并不太令人兴奋-您现在有了一个名为“Submenu”的子菜单,其中有两个相同的菜单项。但同样,您应该对这里可能发生的情况有一个想法-如果您想返回一个JCheckBoxMenuItem
或其他种类的菜单项,则可以这样做。
注意:您还可以使用Presenter.Menu提供不同的组件来显示主菜单中的任何操作,但是某些版本的Mac OS-X(用于Macintosh)在将随机Swing组件嵌入菜单项中时根本无法正常播放。为了安全起见,请不要在主菜单中使用JMenu,JMenuItem及其子类。 |
Properties和Property Sheet
在本教程中,您将涉及的最后一个主题是属性。你可能知道NetBeans IDE包含一个“属性表”,它可以显示一个节点的“属性”。“属性”的确切含义取决于节点是如何实现的。属性本质上是名称-值对,具有Java类型,哪些分组在集和显示在属性表-可写属性可以通过属性编辑器编辑(见java.beans.PropertyEditor
有关属性编辑器的一般信息)。
因此,从一开始,Node的内建思想是,节点可以拥有可以在属性表上查看和可选地编辑的属性。为此添加支持非常容易。在Node API中有一个便利的类' Sheet,它表示一个节点的全部属性集。您可以向它添加表的实例。表示“属性集”,在属性表中以属性组的形式出现。
1、覆盖MyNode.createSheet(),修复导入
@Override
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;
}
2、运行套件
3、File > Open Editor, Window -> IDE Tools > Properties
4、点击不同的节点,并注意属性表的更新,就想MyViewer组件一样
上面的代码使用了一个非常方便的类:PropertySupport.Reflection
,可以简单地将其传递给对象,类型以及getter和setter方法名称,它将创建一个Property对象,该对象可以读取(并可选地写入)该对象的该属性。问题。因此,您可以使用PropertySupport.Reflection
一种简单的方法将一个Property
对象连接到的getIndex()
方法APIObject
。
如果您想要Property
对象用于基础模型对象上的几乎所有getter / setter方法,则可能要使用或subclass BeanNode
,这是该对象的完整实现Node
,可以给它一个随机对象,并将尝试为其创建所有必要的属性(并听取更改)通过反射(可以BeanInfo
通过为节点所代表的对象的类创建一个来控制它们的精确显示方式 )。
注意:设置name
属性非常重要。属性对象根据名称测试其相等性。如果您正在向中添加一些属性Sheet.Set
而它们似乎正在消失,则很可能未设置其名称-因此将一个属性与另一个HashSet
具有相同(空)名称的属性放在a中会导致后来添加的属性替换先前添加的属性。
读写属性
为了进一步理解这个概念,您真正需要的是一个读/写属性。因此,下一步是添加一些其他支持APIObject
以使Date
属性可设置。
1、打开org.myorg.myapi.APIObject,将final从声明的date字段行中删除
2、添加以下方法
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));
}
}
3、APIObject中调用fire方法
public void setDate(Date d) {
Date oldDate = date;
date = d;
fire("date", oldDate, date);
}
4、在MyNode.createSheet()中,更改dateProp声明方式,让它可读可写
Property dateProp = new PropertySupport.Reflection(obj, Date.class, "date");
现在,您没有指定显式的getter和setter方法,而是提供了属性名和PropertySupport。反射会为我们找到getter和setter方法(实际上,它还会自动找到addPropertyChangeListener()方法)。
5、重新运行模块套件,注意,现在可以在MyEditor中选择MyNode实例,并实际编辑日期值,如下所示:
重新启动IDE后,结果将保持不变。(不过未试验成功,有可能是本地资源回收问题(每次启动数据值重置,注释了getPersistenceType方法也不起作用),不过不影响下面操作)
但是,此代码中仍然存在一个错误:更改Date属性时,还应该更新节点的显示名称。因此,您将对进行其他更改,MyNode
并让其监听上的属性更改APIObject
。
1、让MyNode实现PropertyChangeListener,修复导入,实现抽象方法
public class MyNode extends AbstractNode implements PropertyChangeListener {
2、将以下行添加到带参数的构造函数中
obj.addPropertyChangeListener(WeakListeners.propertyChange(this, obj));
注意,这里使用的是org.openide.util.WeakListeners上的实用方法。这是一种避免内存泄漏的技术——一个APIObject只会弱引用它的MyNode,所以如果节点的父节点被折叠,节点可以被垃圾回收。如果该节点仍然在APIObject拥有的监听器列表中被引用,这将是内存泄漏。在您的例子中,节点实际上拥有APIObject,因此这并不是一个可怕的情况—但是在实际编程中,数据模型中的对象(比如磁盘上的文件)可能比节点显示给用户的对象的寿命长得多。当你向一个对象添加一个侦听器,而你从来没有显式地删除它,最好使用“weaklistener”—否则你可能会造成内存泄漏,这将是一个相当头疼的问题。但是,如果实例化一个单独的侦听器类,请确保从附加的代码中保留对它的强引用——否则几乎在添加它时就会被垃圾收集。
3、最后,实现propertyChange()
方法:
@Override
public void propertyChange(PropertyChangeEvent evt) {
if ("date".equals(evt.getPropertyName())) {
this.fireDisplayNameChange(null, getDisplayName());
}
}
4、再次运行模块套件,MyNode
在MyEditor
窗口中选择并更改其Date
属性-请注意,Node
现在已正确更新的显示名称,如下所示,其中2020-09-02都反映在节点和属性表中
属性分组
您可能已经在运行NetBeans IDE的表单编辑器Matisse时注意到,属性表顶部有一组按钮,用于在属性集组之间进行切换。
Property
有方法
getValue()
和
setValue()
一样,
PropertySet
(他们都继承了这个从
java.beans.FeatureDescriptor
)。在某些情况下,可以使用这些方法,以在给定
Property
或
PropertySet
属性表或某些类型的属性编辑器之间传递临时的“hints” (例如,将默认的filechooser目录传递给的编辑器
java.io.File
)。这就是您可以为一个或多个“ PropertySet”指定组名(显示在按钮上)的技术。在实际编码中,这应该是本地化字符串,而不是如下所示的硬编码字符串:
@Override
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;
}
3、再次运行模块套件
![](https://i-blog.csdnimg.cn/blog_migrate/66d6759fba5e8669ef3bbc9df0972b6e.png)
一般属性表警告
如果您使用的是NetBeans 3.6或更早版本,您可能会注意到,较旧版本的NetBeans大量使用属性表作为UI的核心元素,而今天这种情况并不普遍。原因很简单:基于属性表的UI并非十分友好。这并不意味着不要使用属性表,而要明智地使用它。如果您可以选择为自定义程序提供漂亮的GUI,请这样做-您的用户将感谢您。
概念回顾
本教程试图获得以下想法:
-
节点是表示层
-
节点的显示名称可以使用HTML的有限子集进行自定义
-
节点具有图标,您可以为创建的节点提供自定义图标
-
节点有动作;实现Presenter.Popup的Action可以提供自己的组件以显示在弹出菜单中;使用Presenter.Menu的主菜单项和使用Presenter.Toolbar的工具栏项也是如此
-
节点具有属性,这些属性可以显示在属性表上
下一步
现在,您已开始深入研究如何从NetBeans的属性表中获取更多信息。在 接下来的教程中,你将介绍如何编写自定义编辑器,并提供了属性表使用自定义的行内编辑器。