扩展 Visual Editor 的控件和属性
Visual Editor for Java 是基于 eclipse 的 GUI 可视化编辑插件,该插件以 Java 代码为中心,实现了所见即所得的用户界面编辑方式,广泛支持目前的 SWT、AWT、Swing 等控件包。本文通过一个完整的对 Visual Editor 的扩展示例,向读者展示扩展 Visual Editor 的控件和属性的方法。
Visual Editor for Java 实现了基于 Java 源代码的所见即所得的用户界面开发模式。当 Java 源代码打开后,Visual Editor 将对其进行解析,寻找可以识别的控件,并将其展现至图形化的编辑器中,其外观和运行时完全一致。支持用户由 Palette 引发的控件拖拽的操作,支持对控件属性的编辑,同时实时的生成相应的代码。
如上图截图所示,Visual Editor 的编辑区主要由五部分构成:
- 图形编辑区,所见即所得的反映用户界面的情况。
- Java代码区,为图形区用户界面对应的 Java 源码,与图形区同步。
- Palette 区,提供了所有已扩展的,供用户选用的 Visual Beans,用户可以拖拽控件至图形区定制界面。若 Palette 里没有列出的控件同时该控件也在 Class Path 中的情况,可以选用按钮进行对控件的手动选择。
- 属性视图,显示当前选中的控件可编辑的所有信息。该视图中的属性更改,直接同步于 Java 代码区的代码,同时,UI相关的更改也会反映至图形编辑区。对于比较简单的类型的属性,比如字符串类型或布尔类型,可以直接在视图中更改,对于复杂的类型,比如字体或颜色等,则可以扩展 Property Editor 对其进行编辑。
- Java Beans视图,展示目前定制了的所有控件的层次关系。在该视图进行的选择或右键点击等操作,实现的效果与图形编辑区相同,选择操作与另外的几个区同步。
Visual Editor 各部分的交互情况,可以由下图表示,该图中初始化动作,就是通过对 Java 代码的解析,将其内容反应至属性视图或者属性编辑器中,这是代码生成的逆过程。
Palette 中提供了常用的 UI 控件的实现,包括 Swing、AWT、SWT 等的支持。如果用户希望根据自身需求,定制所需的控件,又希望能够通过 Visual Editor 进行识别和编辑,则需要对 Visual Editor 进行相应的扩展。这就是本文讲述的内容。
本文希望对 Visual Editor 进行扩展,加入符合特定业务逻辑的控件,该控件的需求如下:
- 为 SWT 控件,是 Label 和 Text 两个控件的组合,其中 Label 要显示 Text 控件的提示信息,而用户可以在 Text 中输入相应内容。下图是用户名的例子:
- 可以对 Text 控件的输入长度进行控制,比如最大输入 8 个字符,则输入 8 个字符后将无法继续输入。
- 可以选择当输入超出设定时,是否发出 Beep 声音提醒。
在该实例过程中,本文将说明如何:
- 扩展一个 Java Build Path 的 Library。
- 扩展 Palette 中的控件。
- 扩展属性视图显示定制控件信息。
- 触发属性编辑器来编辑控件属性。
- 将代码与属性编辑器相互同步。
TextItem 是该控件的具体实现,读者可以参阅本文附件的源码,需要提及的是:
- TextItem 继承于 SWT 的 Composite 控件,在 Composite 中包括 Label 和 Text 两个 SWT 控件,Label 用以显示 Title,Text 用以用户输入。
- 采用 TextItemModel 定义该控件的属性,该 Model 是一个 Java Bean,由 Title 和 Max Length 两个属性构成。抽象出一个模型类主要为了增加该控件的复杂度,并展示如何通过 Property Editor 编辑该模型类。设定 TextItemModel 的代码段:
this.model = model;
titleLabel.setText(model.getTitle());
this.pack();
}
- 通过名为 beep 的布尔变量定义是否需要 beep 提醒。
- 添加 Verify Listener 检测用户输入是否超出最大输入字符的限制,该部分代码如下:
public void verifyText(VerifyEvent e) ...{
String startStr = ((Text) e.widget).getText().substring(0, e.start);
String endStr = ((Text) e.widget).getText().substring(e.end);
String str = startStr + e.text + endStr;
if ((model != null) && (str.length() > model.getMaxLength())) ...{
e.doit = false;
if (beep)
Display.getCurrent().beep();
}
}
} );
BeanInfo 类实现了 java.beans.BeanInfo 接口,它被 Visual Editor 用来描述控件在属性视图中的行为。因为 BeanInfo 类仅在设计用户界面时被用到,因此运行时是不需要该类的。Visual Editor 定义了一些规则,用以将 BeanInfo 类与其描述的控件相联系起来,正如下文所述。
控件可以继承其父类或其他控件的属性行为,由 java.beans.Introspector 来获取相应控件的 BeanInfo 描述,并作为 getAdditionalBeanInfo() 方法的返回值,就可以将其他控件的属性行为继承至本控件,如下面代码所示:
try...{
return new BeanInfo[]...{Introspector.getBeanInfo(Control.class)};
} catch (IntrospectionException e)...{
return new BeanInfo[0];
}
}
java.beans.PropertyDescriptor 用以描述控件的属性,该属性应该在控件中存在一对公开的(public 的) Get 方法和 Set 方法进行访问。例如,setName(String name) 和 getName() 方法,对应的 property 的名字就是“name”,注意第一个字母是小写。对 PropertyDescriptor 还有很多可选的设定,比如设定该 property 是否显示,在属性视图中显示的名称以及该属性的描述等。
以本控件的 Beep 属性为例,该属性是一个布尔型属性,对应文本框是否有超出输入长度的声音提示。描述该属性的 PropertyDescriptor 如下:
清单4:描述 Beep 属性的 PropertyDescriptor
PropertyDescriptor pd;
try ...{
pd = new PropertyDescriptor("beep", TextItem.class);
pd.setDisplayName("TextItem notifier");
pd.setShortDescription("Beep when the length exceed specified.");
pd.setValue("enumerationValues", new Object[] ...{
"No Beep", Boolean.FALSE, "developerworks.ve.example.textitem.TextItem.NO_BEEP",
"Beep", Boolean.TRUE, "developerworks.ve.example.textitem.TextItem.BEEP"});
} catch (IntrospectionException e) ...{
pd = null;
}
return pd;
}
PropertyDescriptor 的构造函数传入的参数分别为属性名称和控件类,通过这两个信息就可以反射获取到控件类中某一属性特定的内容。该属性显示在属性视图中的效果如下图所示:
对应代码来看:
- setDisplayName() 用以定义该属性在属性视图中显示的名称,如上图中所示“TextItem notifier”;
- setShortDescription() 用以设置对该属性的文字描述,该描述会在选中该属性时显示在状态栏的左方,如上图所示“Beep when the length exceed specified.”;
- setValue() 设置该属性对应的值。因为本属性是布尔型,所以希望做出下拉列表框的样式,所以 setValue() 方法的 key 设置为了枚举类型“enumerationValues”,后续的六个参数每三个为一组,表达下拉列表框中的一个选项。它们的意思分别是显示的字符串、实际对应的值以及与生成代码对应的初始化字符串。以前三个参数而言,“No Beep”是显示的字符串,其对应的值是 false,即调用该控件的 setBeep(false),而在代码编辑区生成的代码行如下:
textitem.setBeep(developerworks.ve.example.textitem.TextItem.NO_BEEP);
在 BeanInfo 类中,体现对属性描述的方法是 getPropertyDescriptors(),它返回上文所述的 PropertyDescriptor 的一个数组,每个 PropertyDescriptor 都包含了对该控件的某个属性的具体描述。
对 TextItem 的 TextItemModel 属性来说,它是一个定制的属性类,而不是简单的数据类型或者字符串类型,对于它,用户可能希望拥有独立的编辑器对其进行编辑,实现的效果大概像下面的样子:
该属性编辑对话框是 TitleAreaDialog 的一个实现,实现过程略去不谈,如何在属性视图中激活该对话框?
属性视图中每个属性都是 key 和 value 的集合,对于 Value 的设置,可以是简单的文本框,比如字符串型,整数型等,也可以是下拉列表框,如布尔型等。与此同时,也可以实现为弹出对话框的编辑框。该种类的编辑框可以是 JFace 的 DialogCellEditor 的实现子类,并继承 Visual Editor 的 INeedData 接口。DialogCellEditor 是抽象类,它的唯一的抽象方法就是 openDialogBox(),用以打开上图所示的对话框。
定制化的编辑框,如何建立与控件相应属性的连接?这需要借助 Visual Editor 的 override 机制。BeanInfo 提供了 override 的技术,用以扩展属性的编辑行为。Visual Editor 通过 override 文件来扩展出丰富的可定制化的编辑器。
向 Visual Editor 扩展 override 文件,需要对“org.eclipse.jem.beaninfo.registrations”扩展点进行扩展,使其能够识别扩展的 Override 文件的位置。本例中的扩展如下:
清单 5:org.eclipse.jem.beaninfo.registrations 扩展点
point ="org.eclipse.jem.beaninfo.registrations" >
< registration
container ="developerworks.ve.example.textitem" >
< override
package ="developerworks.ve.example.textitem"
path ="overrides/developerworks/ve/example/textitem" >
</ override >
</ registration >
</ extension >
其中,path 指向保存 override 文件的路径。对应于 TextItem 而言,其 override 文件为 TextItem.override,该文件的目的就是由定制话的编辑行为覆盖掉原有属性的编辑行为。本例中的 TextItem.override 如下:
< xmi:XMI xmi:version ="2.0"
xmlns:xmi ="http://www.omg.org/XMI"
xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance"
xmlns:org.eclipse.ve.internal.cde.decorators =
"http:///org/eclipse/ve/internal/cde/decorators.ecore"
xmlns:ecore ="http://www.eclipse.org/emf/2002/Ecore"
xmlns:event ="event.xmi" >
< event:Add featureName ="eStructuralFeatures" >
< addedEObjects xsi:type ="ecore:EReference" name ="model" unsettable ="true" >
< eAnnotations xsi:type ="org.eclipse.ve.internal.cde.decorators:BasePropertyDecorator"
cellEditorClassname =
"developerworks.ve.example.textitem/
developerworks.ve.example.textitem.TextItemModelEditor" />
</ addedEObjects >
</ event:Add >
</ xmi:XMI >
该文件中值得注意的几个地方地方:
- name=“model”,建立了与名为 model 的属性的连接,即可以通过 setModel() 与 getModel() 访问该属性,这与前面编写的控件行为一致。
- cellEditorClassname 一项,指向了定制化后的编辑器类,该属性的值,分割线“/”前为该编辑器所在插件的 id,分割线后为该编辑器的类路径。
通过上述几步操作,即可以用定制化的编辑器行为覆盖掉 BeanInfo 中缺省的编辑器行为,实现了对属性编辑的自由扩展。
属性编辑器与 Java 代码有同步的需求,即可以将已经生成的 Java 代码进行解析,将解析的结果赋给打开后的属性编辑器显示,同时又需要对用户在属性编辑器中输入的信息进行解析,并生成 Java 代码。这两个是互逆的过程。
Visual Editor 的核心部分,是描述被编辑控件的EMF模型,该模型描述了控件实例,控件之间的各种关系,以及可对控件进行编辑操作的各种属性等内容。该模型的实例实现了 org.eclipse.jem.internal.instantiation.base.IJavaInstance 接口。也就是所,上述的解析 Java 代码的过程,可以转化为解析 IJavaInstance 对象的过程;而生成 Java 代码的过程,也就是生成 IJavaInstance 对象的过程。
如何解析 IJavaInstance 对象?对于本例来讲,下面是解析的一个片段,可以看到,对 IJavaObjectInstance 对象的解析,就是对其主体构造函数的解析,因为 set 和 get 方法对该属性操作,会创建一个该属性对象对应的实例,该创建就是通过构造函数。解析出该构造函数,就可以还原该对象的内容。getArguments() 方法,就是获取该构造函数传入的参数,由这些参数,最终还原出该对象。
清单 7:解析 IJavaObjectInstance 对象
PTExpression exp = ((ParseTreeAllocation) allocation).getExpression();
PTClassInstanceCreation instance = ((PTClassInstanceCreation) exp);
PTStringLiteral stringLiteral = (PTStringLiteral) instance.getArguments().get( 0 );
String title = stringLiteral.getLiteralValue();
PTNumberLiteral numberLiteral = (PTNumberLiteral) instance.getArguments().get( 1 );
int max = Integer.parseInt(numberLiteral.getToken());
model = new TextItemModel(title, max);
如何生成 Java 代码?即如何生成 IJavaObjectInstance 的实例?还是要回到构造函数上来,通过初始化字符串,传入正确类型的参数,生成相应的构造函数语句,就能够创建出 Visual Editor 可识别的 IJavaObjectInstance 实例,从而转化为代码编辑区中的 Java 代码。本例中解析的代码如下:
" developerworks.ve.example.textitem.TextItemModel " ;
private IJavaInstance createJavaObject(TextItemModel model) ... {
return BeanUtilities.createJavaObject(TEXTITEMMODELCLASSPATH,
JavaEditDomainHelper.getResourceSet(editDomain),
getJavaAllocation(model));
}
private JavaAllocation getJavaAllocation(TextItemModel model) ... {
String initString = "new " + TEXTITEMMODELCLASSPATH +
"(" + """ + model.getTitle() + "", " + model.getMaxLength() + ")";
return BeanPropertyDescriptorAdapter.createAllocation(initString, editDomain);
}
到目前为止,围绕该控件的编码工作已经完成:拥有了定制化的控件,描述该控件的 BeanInfo,以及对该控件进行编辑的编辑器。下一步要做的就是将该控件集成到 Visual Editor 的编辑器中,供用户使用了。
如何扩展该控件至 Palette?Visual Editor 的 Palette 由 EMF 的模型来描述,EMF 可以序列化为 XMI,而插件可以定制它们自己的XMI文件,描述控件的种类,归属于哪些控件组,以及图标和实现类等信息。对于本例中的 TextItem 来讲,编写了 palette.xmi 文件来描述该控件,使其被 Palette 所识别,对应于 TextItem,是<children></children>中的内容,分别描述了该控件由哪种图标表示,实现类是哪个,在 Palette 中显示的名称,以及创建后的实例,默认采用的名称。
< org .eclipse.ve.internal.cde.palette:Drawer xmi:version ="2.0"
xmlns:xmi ="http://www.omg.org/XMI" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ecore ="http://www.eclipse.org/emf/2002/Ecore"
xmlns:org.eclipse.ve.internal.cde.palette =
"http:///org/eclipse/ve/internal/cde/palette.ecore"
xmlns:org.eclipse.ve.internal.cde.utility =
"http:///org/eclipse/ve/internal/cde/utility.ecore" >
< entryLabel xsi:type ="org.eclipse.ve.internal.cde.utility:ConstantString"
string ="Custom Widgets" />
< children xsi:type ="org.eclipse.ve.internal.cde.palette:EMFCreationToolEntry"
icon16Name ="platform:/plugin/developerworks.ve.example.textitem/icons/sample.gif"
creationClassURI ="java:/developerworks.ve.example.textitem#TextItem" >
< entryLabel xsi:type ="org.eclipse.ve.internal.cde.utility:ConstantString"
string ="TextItem" />
< keyedValues xsi:type ="ecore:EStringToStringMapEntry"
key ="org.eclipse.ve.internal.cde.core.nameincomposition" value ="textitem" />
</ children >
</ org.eclipse.ve.internal.cde.palette:Drawer >
该 palette.xmi 文件,需要 Visual Editor 知道其路径,因此需要在对“org.eclipse.ve.java.core.contributors”扩展点做如下扩展,使其能够读取到扩展的定义文件。
清单10:“org.eclipse.ve.java.core.contributors”扩展点
point ="org.eclipse.ve.java.core.contributors" >
< palette
container ="developerworks.ve.example.textitem"
categories ="palette.xmi" />
</ extension >
经过上述定制,控件已经显示于 Palette 中,拖拽控件至图形编辑区已经可以预览该控件和生成相应的代码,生成控件的 Java 代码为 new TextItem(parent, SWT.NONE)。也就是说,控件生成传入构造函数的参数,第二项 style 默认为 SWT.NONE。如果希望该控件构造函数传入的参数 style 为 SWT.BORDER 或其他样式,如何定制呢?
Visual Editor 提供了这种定制的支持,同样依赖于 XMI 文件,实现对构造函数默认参数的覆盖。本例中,BorderTextItem 是 TextItem 的另一种使用,不同点仅在于生成代码中,构造函数传入参数不同。BorderTextItem 传入的 style 参数为 SWT.BORDER,并将该样式赋给了该控件的文本框。覆盖该构造函数参数的 XMI 文件如下:
< xmi:XMI xmi:version ="2.0" xmlns:xmi ="http://www.omg.org/XMI"
xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ecore ="http://www.eclipse.org/emf/2002/Ecore"
xmlns:cdm ="http:///org/eclipse/ve/internal/cdm.ecore"
xmlns:utility ="http:///org/eclipse/ve/internal/cde/utility.ecore"
xmlns:org.eclipse.jem.internal.instantiation =
b"http:///org/eclipse/jem/internal/instantiation.ecore"
xmlns:java ="java.xmi"
xmlns:textitem ="java:/developerworks.ve.example.textitem" >
< textitem:TextItem xmi:id ="BorderTextItem_1" >
< allocation xsi:type ="org.eclipse.jem.internal.instantiation:ParseTreeAllocation" >
< expression xsi:type ="org.eclipse.jem.internal.instantiation:PTClassInstanceCreation"
type ="developerworks.ve.example.textitem.TextItem" >
< arguments xsi:type ="org.eclipse.jem.internal.instantiation:PTName"
name ="{parentComposite}" />
< arguments xsi:type ="org.eclipse.jem.internal.instantiation:PTFieldAccess"
field ="BORDER" >
< receiver xsi:type ="org.eclipse.jem.internal.instantiation:PTName"
name ="org.eclipse.swt.SWT" />
</ arguments >
</ expression >
</ allocation >
</ textitem:TextItem >
< cdm:AnnotationEMF annotates ="BorderTextItem_1" >
< keyedValues xmi:type ="ecore:EStringToStringMapEntry"
key ="org.eclipse.ve.internal.cde.core.nameincomposition" value ="bordertextitem" />
</ cdm:AnnotationEMF >
</ xmi:XMI >
本文件中,<textitem:TextItem> </textitem:TextItem>标记符内的内容就是该控件的定义。
清单 12:palette.xmi 文件中 BorderTextItem 定义
icon16Name ="platform:/plugin/developerworks.ve.example.textitem/icons/sample.gif"
icon32Name ="" id =""
prototypeURI ="platform:/plugin/developerworks.ve.example.textitem/
palette/BorderTextItem.xmi#BorderTextItem_1" >
< entryLabel xsi:type ="org.eclipse.ve.internal.cde.utility:ConstantString"
string ="BorderTextItem" />
</ children >
prototyleURI 指向了定义该控件的位置,即 developerworks.ve.example.textitem 插件下的 palette 文件夹内的 BorderTextItem.xmi 文件,对应该文件中的 BorderTextItem_1 创建项。由此建立了 Palette 与构造函数的 XMI 文件的联系。
用户如何将定制化的控件加到他们的构建路径里?和 JRE System Library 一样,如果我们的控件打包为 textitem.jar,他们可能希望有下图演示的方便的构建方式:
如何定制 ClassPath 容器?
首先区分一下,本文前述的 Java 类,哪些需要在 runtime 时使用,哪些仅仅是 Visual Editor 使用:
- TextItem 控件类,是控件的具体实现,TextItemModel 类,是该控件类的模型类,这两个类是 runtime 类,在用户的应用程序中会采用。
- TextItemBeanInfo 类,是描述 TextItem 属性编辑行为的类,而 TextItemModelEditor 类,是对应于 TextItemModel 的属性编辑器,这两个类属于在 Visual Editor 下使用的类,用户并不关心他们内部的实现。
所以,打包 runtime 用的 jar 包,仅需将 TextItem 类和 TextItemModel 类打包为 textitem.jar,这是创建 ClassPath 容器的第一步。而 TextItemBeanInfo 和 TextItemModelEditor 为插件扩展才会用到的类,则可以另外打包为 textitembeaninfo.jar,放入导出的插件中,并对扩展点 “org.eclipse.jem.beaninfo.registrations”进行扩展,令 Visual Editor 能够找到扩展的 BeanInfo 类和属性编辑器类:
< searchpath package ="developerworks.ve.example.textitem" />
</ beaninfo >
之后就是将 textitem.jar 做成 Class Library 并加入到 Class Path 容器中了。该步需要对 eclipse 的 jdt 的一些扩展点做扩展,扩展如下,可以参阅 eclipse 的帮助文件中扩展点的说明了解各扩展点的含义,此处不再详述:
清单 14:扩展 Classpath Container Page 扩展点
< classpathContainerPage name ="Custom Widgets"
class ="org.eclipse.ve.internal.java.wizard.RegisteredClasspathContainerWizardPage"
id ="developerworks.ve.example.textitem" >
</ classpathContainerPage >
</ extension >
< extension point ="org.eclipse.jdt.core.classpathContainerInitializer" >
< classpathContainerInitializer
class ="org.eclipse.ve.internal.java.core.RegisteredClasspathContainerInitializer"
id ="developerworks.ve.example.textitem" >
</ classpathContainerInitializer >
</ extension >
< extension point ="org.eclipse.ve.java.core.registrations" >
< registration
container ="developerworks.ve.example.textitem"
description ="Custom Widgets" >
< library runtime ="textitem.jar" />
</ registration >
</ extension >
至此,定制控件的插件已经完成。将该插件打包放入 eclipse 的 plugin 文件夹下,重启 eclipse,就可以激活该插件,在 Visual Editor 中使用定制后的控件了。下面从使用者的角度贯穿一下如何使用该定制控件,读者可以根据每个步骤,思考在本文中是如何实现该步骤的功能的:
Step1:创建 Java 的 Project,将名为 Custom Widgets 的 ClassPath 容器以及 SWT 的类容器加入项目的构建路径:
Step2: 创建 Visual Class,可以在 Style 中选中 SWT 的 Composite 或 Shell 作为该 Visual Class 的基类面板,这里选择为 Composite:
Step3:由 Palette 中拖拽 TextItem 或 BorderTextItem 至图形编辑区的 Composite 上,代码编辑区可以相应生成代码。
Step4:在属性视图中编辑该控件的属性“TextItem notifier”设置为 BEEP,由 TextItem model 的编辑器打开编辑对话框,输入 Title为“User Name: ”,最大长度为 8。
Step5:运行该 Java 文件,预览一下成果,运行效果应该如下图所示,当在文本框中输入的用户名字符数超过 8 个,则会有 Beep 的声音提示:
本文向您阐述了如何扩展 Visual Editor,加入定制化的控件供用户使用,以及相关的很多问题和解决方案,供您参考。通过这种定制,用户可以很容易的所见即所得的将定制后的控件放到他们希望的地方,并能够图形化的编辑该控件的各种属性,使得开发 Java 用户界面的工作变得简单和可视化。
本文附件中包含了该插件和其源码,读者可以进行体验和参考。请尽量阅读该插件的完整代码,这有助于您了解本文的内容。
描述 | 名字 | 大小 | 下载方法 |
---|---|---|---|
样本代码 | textitem.zip | 36KB | HTTP |
关于下载方法的信息 |
- DeveloperWorks文章:在 WebSphere Studio Visual Editor for Java 中扩展选用板。
- Eclipse.org 文章:Extending The Visual Editor: Enabling support for a custom widget。
- Visual Editor 项目站点:http://www.eclipse.org/vep。