GUI 开发可能是令人畏惧的任务。GUI 框架并不是总是拥有良好的文档,需要的代码量可能迅速增长,拖慢开发工作流。特别是支持这些 GUI 框架的拖放工具和 IDE 通常诱使 GUI 软件开发人员创建难以管理和阅读的代码。这可能会进一步混淆业务逻辑和 GUI 描述代码之间的界限,从而使软件维护更加困难。
这就是声明性 UI 语言之所以方便的原因。UI 语言描述 “是什么”,而不是 “该如何”。例如, HTML 描述显示的内容,而不是描述用于呈现内容的呈现函数。声明性语言并不指定 “该如何”,从而省略了控制流。尽管这种省略听起来好像一种限制,但它实际上是一种优点,因为控制流的副作用 — 如修改全局状态(比如变量)或调用其它函数或方法 — 被消除了。选择声明性语言还有利于将 UI 代码和应用程序代码分隔开来。这种分离将来还能提供一些好处,比如明确区分项目和团队角色,这甚至有可能降低业务逻辑和多个视图或视图技术之间的集成成本。
目前正在使用的声明性 XML UI 的例子不在少数。使用 GNOME 桌面环境的 Linux® 和 UNIX® 操作系统有 Glade。Microsoft® Windows® 用户拥有 Extensible Application Markup Language (XAML),该语言支持丰富的功能,包括在 XML 中插入代码。Adobe® Flex® Framework 的 MXML 格式为 Adobe Shockwave (SWF) 播放器描述 GUI 并包含代码插入。参阅 参考资料 中的链接了解更多信息。
Java 技术中的基本声明性 UI 框架的必要组件可能包括:
- 验证:使用 XML Schema
- 一个 DOM:处理具体事宜的自定义 DOM,比如同步 GUI 组件状态和 XML 节点状态
- 持久性: GUI 的编组(marshalling)和解组(unmarshalling)
- 图像数据:存储为 Base64 数据
- Swing 组件:GUI 开发常用的 Swing 组件的表示
下面可以创建声明性 XML 了,创建过程中要谨记上述必要组件。
第一个 XML 格式示例(见 清单 1)展示了一个简单的窗口、一个面板和一个按钮。清单 1 包含基本的必要属性,比如坐标、大小和引用单独内存组件的惟一标识符。
清单 1. 声明性 XML 概念
<?xml version="1.0" encoding="UTF-8"?>
<xui:XUI>
<xui:Window id="window_0" name="Hello World" width="300" height="300" x="426"
y="282" visible="true">
<xui:GridLayout height="1" width="1"></xui:GridLayout>
<xui:Panel id="panel_0" x="0" y="0" name="Hello Panel"
width="1" height="1">
<xui:GridLayout height="1" width="1"></xui:GridLayout>
<xui:Button x="0" y="0" width="1" height="1" id="button_0"
label="Press Me" enabled="true" selected="true" orientation="horizontal"/>
</xui:Panel>
</xui:Window>
<xui:Resource type="java" class="ButtonModel" uri="model.jar"/>
</xui:XUI>
|
这个声明性 XML UI 将把 XML 元素映射到 Java Swing 框架,由于 Swing 可用于所有现有 Java 运行时环境,该框架提供了极大的可移植性。许多 Swing 组件将在 XML 格式中拥有相应的 XML 元素。
这个框架使用一个 XML 模式。XML 模式允许在一个模式实例中使用指定顺序、基数(cardinality)和数据类型。这一点很重要,该框架将要求一个具有指定类型和特定顺序的 XML 元素集。清单 2 展示了一个 XML 模式实例中的初始元素和属性的层次结构。
清单 2. 声明性 XML UI 模式:初始元素
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema elementFormDefault="qualified"
targetNamespace="http://xml.bcit.ca/PurnamaProject/2003/xui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xui="http://xml.bcit.ca/PurnamaProject/2003/xui">
<xs:element name="XUI">
<xs:complexType>
<xs:sequence>
<xs:element minOccurs="0" maxOccurs="128" ref="xui:Window"/>
<xs:element minOccurs="0" maxOccurs="1" ref="xui:Resource"/>
</xs:sequence>
<xs:attribute name="id" type="xs:anyURI" use="required"/>
</xs:complexType>
</xs:element>
<xs:element name="Resource">
<xs:complexType>
<xs:sequence>
</xs:sequence>
<xs:attribute name="uri" type="xs:anyURI" use="required"/>
<xs:attribute name="class" type="xs:token" use="required"/>
<xs:attribute name="type" use="required">
<xs:simpleType>
<xs:restriction base="xs:token">
<xs:enumeration value="java"/>
<xs:enumeration value="groovy"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
</xs:complexType>
</xs:element>
<xs:element name="Window">
<xs:complexType>
<xs:sequence>
<xs:element ref="xui:GridLayout"/>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element ref="xui:BasicDialog"/>
<xs:element ref="xui:OpenFileDialog"/>
<xs:element ref="xui:SaveFileDialog"/>
<xs:element ref="xui:CustomDialog"/>
<xs:element ref="xui:Panel"/>
<xs:element ref="xui:SplitPanel"/>
<xs:element ref="xui:TabbedPanel"/>
</xs:choice>
<xs:element minOccurs="0" maxOccurs="1" ref="xui:MenuBar"/>
</xs:sequence>
<xs:attribute name="id" type="xs:ID" use="required"/>
<xs:attribute name="x" type="xs:short" use="required"/>
<xs:attribute name="y" type="xs:short" use="required"/>
<xs:attribute name="width" type="xs:unsignedShort" use="required"/>
<xs:attribute name="height" type="xs:unsignedShort" use="required"/>
<xs:attribute name="name" type="xs:string" use="required"/>
<xs:attribute name="visible" type="xs:boolean" use="required"/>
</xs:complexType>
</xs:element>
<xs:element name="GridLayout">
<xs:complexType>
<xs:attribute name="width" type="xs:unsignedShort" use="required"/>
<xs:attribute name="height" type="xs:unsignedShort" use="required"/>
</xs:complexType>
</xs:element>
</xs:schema>
|
下面详细检查一下这个模式。首先,根据 XML Recommendation 的建议,XML 声明必须出现在最前面 — 甚至在空格和注释前面。其次,schema 元素包含其他元素:
elementFormDefault="qualified"表明所有元素必须有一个名称空间 — 可以是前缀,也可以是默认名称空间。targetNamespace="http://xml.bcit.ca/PurnamaProject/2003/xui"规定了目标名称空间 URI。- 模式实例使用 W3C XML Schema Recommendation 和其中的所有元素(
xmlns:xs="http://www.w3.org/2001/XMLSchema")。 xmlns:xui="http://xml.bcit.ca/PurnamaProject/2003/xui"识别另一个名称空间和它的对应前缀。
在 XSD 中使用名称空间很重要,这样就能消除名称空间冲突。当来自两个或多个 XML 格式的两个或多个元素的名称相同时,名称空间冲突 就会出现。这种冲突使对它的对应标记集感兴趣的应用程序感到困惑。通过使用名称空间和对应的名称空间前缀,您可以彻底避免这个问题。
再次,根级别(root-level)数据类型元素 XUI 表明:
- 它允许一个由 0 个到 128 个
Window元素组成的序列,序列末尾是一个Resource元素。稍后您将发现,这两个元素将在模式实例中被引用。 - 它有一个
id元素,该元素是必需的且其类型必须为anyURI。
XUI 元素可能包含许多 Window 元素,它也可能没有任何 Window 元素(如果 minOccurs 元素的值为 0)。至于 Resource 元素:
- 它有一个空的内容模型,因为它的
xs:sequence元素为空。 - 它有 3 个属性,它们都是必需的。
- 最后的
type属性创建了一个从 XSD 的已定义类型(token)派生而来的简单类型,其中 restriction 片段是enumeration,允许列举java和groovy的字面文本值。
Resource 元素的目的是向这个 Java 框架提供一个资源(本例中是一个 JAR)的 URI,该资源包含运行时可以加载并绑定的已编译 Java 类。这个资源依赖于将被调用的一个特殊类(class 属性的值),该类主要用于提供一个已公开的类,以便响应从 GUI 生成的所有事件。
Window 元素:
- 包含一个
GridLayout序列,该序列可以包括GridLayout,BasicDialog、OpenFileDialog、SaveFileDialog、CustomDialog、Panel、SplitPane和TabbedPane元素,以及 0 个或一个MenuBar。 - 拥有 7 个属性 — 都是必需的 — 它们使用 XML Schema Recommendation 中的各种已定义数据类型(注意
xs前缀)。
Window 可以包含多个不同的顶级和中级容器。Window 元素引用了一个 GridLayout 元素。GridLayout 指定了由一个单元网格组成的维度,用于容纳组件。GridLayout 提供的布局特性类似于 Java 环境中的 java.awt.GridBagLayout,但没有后者复杂。
如果不深入检查,这个 XML 模式的描述性似乎已经足够了。清单 3 展示了另外几个元素。
清单 3. 声明性 XML UI 模式:更多元素
...
<xs:element name="CustomDialog">
<xs:complexType>
<xs:sequence>
<xs:element ref="xui:GridLayout"/>
<xs:element minOccurs="0" maxOccurs="unbounded" ref="xui:Panel"/>
</xs:sequence>
<xs:attribute name="modal" type="xs:boolean" use="required"/>
<xs:attribute name="idref" type="xs:IDREF" use="optional"/>
<xs:attribute name="name" type="xs:string" use="required"/>
<xs:attribute name="id" type="xs:ID" use="required"/>
<xs:attribute name="x" type="xs:short" use="required"/>
<xs:attribute name="y" type="xs:short" use="required"/>
<xs:attribute name="width" type="xs:unsignedShort" use="required"/>
<xs:attribute name="height" type="xs:unsignedShort" use="required"/>
<xs:attribute name="visible" type="xs:boolean" use="required"/>
</xs:complexType>
</xs:element>
<xs:element name="Panel">
<xs:complexType>
<xs:sequence>
<xs:element maxOccurs="1" minOccurs="1" ref="xui:GridLayout"/>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element minOccurs="0" maxOccurs="unbounded" ref="xui:Button"/>
<xs:element minOccurs="0" maxOccurs="unbounded" ref="xui:Calendar"/>
<xs:element minOccurs="0" maxOccurs="unbounded" ref="xui:CheckBox"/>
<xs:element minOccurs="0" maxOccurs="unbounded" ref="xui:ComboBox"/>
<xs:element minOccurs="0" maxOccurs="unbounded" ref="xui:HypertextPane"/>
<xs:element minOccurs="0" maxOccurs="unbounded" ref="xui:Image"/>
<xs:element minOccurs="0" maxOccurs="unbounded" ref="xui:Label"/>
<xs:element minOccurs="0" maxOccurs="unbounded" ref="xui:List"/>
<xs:element minOccurs="0" maxOccurs="unbounded" ref="xui:PasswordField"/>
<xs:element minOccurs="0" maxOccurs="unbounded" ref="xui:ProgressBar"/>
<xs:element minOccurs="0" maxOccurs="unbounded" ref="xui:RadioButton"/>
<xs:element minOccurs="0" maxOccurs="unbounded" ref="xui:SliderBar"/>
<xs:element minOccurs="0" maxOccurs="unbounded" ref="xui:Table"/>
<xs:element minOccurs="0" maxOccurs="unbounded" ref="xui:TextArea"/>
<xs:element minOccurs="0" maxOccurs="unbounded" ref="xui:TextField"/>
<xs:element minOccurs="0" maxOccurs="unbounded" ref="xui:Tree"/>
</xs:choice>
</xs:sequence>
<xs:attribute name="x" type="xs:unsignedShort" use="required"/>
<xs:attribute name="y" type="xs:unsignedShort" use="required"/>
<xs:attribute name="width" type="xs:unsignedShort" use="required"/>
<xs:attribute name="height" type="xs:unsignedShort" use="required"/>
<xs:attribute name="name" type="xs:string" use="required"/>
<xs:attribute name="id" type="xs:ID" use="required"/>
<xs:attribute name="idref" type="xs:IDREF" use="optional"/>
</xs:complexType>
</xs:element>
<xs:element name="RadioButton">
<xs:complexType>
<xs:sequence>
<xs:element maxOccurs="3" minOccurs="0" ref="xui:Image"/>
</xs:sequence>
<xs:attribute name="label" type="xs:string" use="required"/>
<xs:attribute name="x" type="xs:unsignedShort" use="required"/>
<xs:attribute name="y" type="xs:unsignedShort" use="required"/>
<xs:attribute name="width" type="xs:unsignedShort" use="required"/>
<xs:attribute name="height" type="xs:unsignedShort" use="required"/>
<xs:attribute name="enabled" type="xs:boolean" use="required"/>
<xs:attribute name="selected" type="xs:boolean" use="required"/>
<xs:attribute name="id" type="xs:ID" use="required"/>
<xs:attribute name="orientation" use="required">
<xs:simpleType>
<xs:restriction base="xs:token">
<xs:enumeration value="horizontal"/>
<xs:enumeration value="vertical"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
</xs:complexType>
</xs:element>
...
|
注意,清单中没有存储任何易变的状态信息 — 只保护可能有助于 GUI 组件重建的状态信息。其中一个例子是 CustomDialog 元素的状态信息:
- 对话框中允许的
Panel元素的数量 - 对话框是否是模式对话框(模式对话框直到用户关闭对话框时才捕捉焦点)
- 桌面中的坐标(x 和 y,以像素为单位)
- 大小(宽度和高度,以像素为单位)
- 窗口的可见性
Panel 是一个中级容器,可以包含相当多的原子组件。再看一下 清单 3,Panel 拥有一个 GridLayout,可以选择在 Panel 内不放置原子组件,也可以根据需要放置任意数量的原子组件。Panel 本身具有 x 和 y 坐标。Panel 使用 x 和 y 坐标引用父容器的 GridLayout 中的定位,而不是引用桌面中的像素(就像 CustomDialog 一样)。就像俄罗斯玩偶一样,这种嵌套构造法非常类似于 Swing 的布局规则。了解上述基本知识后,现在可以解决软件实现问题了。
我们首先简要介绍建议的 Java 框架。清单 4 展示了程序员创建应用程序时必须遵循的步骤:
清单 4. Java API 调用概念
try {
// Gain access to a XUI builder through factory
// In this framework the term XUI is going to represent the custom DOM
XUIBuilder builder = XUIBuilderFactory.getInstance().getXUIBuilder(); // (1)
// Validate and parse (unmarshal) the XML document
builder.parse("browser.xml"); // (2)
// Build a custom DOM
XUI xui = builder.getXUIDocument(); // (3)
// Create 1:1 GUI component mapping to custom DOM
xui.visualize(); // (4) (5)
// Create bindings to data model (i.e. JAR file from Resource element)
xui.bind(); // (6)
// Get root node from the XUI document
XUINode root = xui.getRoot();
// Save a copy of the DOM to file (marshal)
xui.marshalXUI("browser-marshalled.xml");
} catch (XUIParseException xpe) {
xpe.printStackTrace();
} catch (XUIBindingException xbe) {
xbe.printStackTrace();
} catch (IOException ioe) {
ioe.printStackTrace();
}
|
清单 4 清晰地定义了功能的分离,允许进一步优化框架的组件。图 1 试图可视化这个工作流。图 1 中的每个圈起来的数字对应 清单 4 中每个加注释的数字,但代码展示了两个额外步骤(检索对 XUI 根节点的引用和将 DOM 编组到文件)。这些步骤是:
图 1 展示了以下步骤:
- 从
BuilderFactory检索一个Builder。 - 在允许检索一个 XUI 文档之前,
Builder首先确保 XML 文档已经被验证和解析。如果解析或验证失败,将出现一个XUIParseException,框架将停止文档加载。 Builder创建 DOM,其中的对象反映读入的 XML 元素。Realizer对象(在内部由XUI对象调用)被实例化并准备执行下一步。- Realizing 是框架根据预先创建的 XML 节点层级(框架引擎的真正核心)创建 GUI 组件层级的地方。
- 使用 Java 环境中的 power of reflection,模型逻辑(应用程序中驱动 UI 的部分)被绑定到刚才实现的 GUI 组件。
图 1. XUI API 用于构建 GUI 的框架流和详细步骤视图
这个共包含 6 个步骤的调用流易于使用,但其中包含大量信息和对象实例化,值得仔细研究。这个框架的核心位于步骤 5 和步骤 6。
在 图 1 中,步骤 5 创建了一个组件模型,支持将这个 XML 节点(现在是内存对象)和一个 GUI 组件组成一对。这种配对需要非常严格地同步以下事件:
- 对于框架读入的每个
XUINode(表示任意 XML 元素的内存对象),必须创建一个XUIComponent来包围XUINode。 - 对于在内存中创建的每个
XUIComponent,必须创建一个 GUI 对等物,比如javax.swing.JFrame。 - 每当一个
XUIComponent实例 — 或它的一个子类型(比如XUIButton)— 被修改时(比如修改大小),XUIComponent必须确保XUINode和 GUI 对等物同时、同等地更新。
通过满足上述要求,这个框架允许程序员将多个 XML 文档读入内存(解组),修改 DOM,然后将更改保存回一个 XML 文档(编组)。程序员甚至可以以编程方式创建几个新的 DOM 并对它们进行编组。
这个框架提供了一个 toString 方法(见 清单 5),以便 XUINode 将自身编组为 XML。根节点可以包含多个子节点,子节点可以包含自己的子节点,依次类推。通过调用根级别节点的 toString 方法,这个框架可以轻松编组整个 XML 文档。名称空间被添加进来,以便每个元素知道自己在层级中的级别(通过 level 变量)。这样,当 toString 方法被调用时,它可以实现缩进,以使这些文档更易于阅读。
清单 5. XUINode
toString 方法实现
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
String namespacePrefix = "";
// insert indenting ... 2 spaces for now.
if(isRoot) {
sb.append(XMLPI + "\n");
sb.append(API_COMMENT + "\n");
} else {
sb.append("\n");
for(int s = 0; s < level; s++) {
sb.append(" ");
}
}
sb.append("<");
// get namespaces for this node
Enumeration keys = nameSpaces.keys();
String names = "";
while(keys.hasMoreElements()) {
String uri = (String)keys.nextElement();
String prefix = (String)nameSpaces.get(uri);
/* if its the xsi namespace (XML Schema Instance),
* ignore it, this isn't part of that namespace but it is
* needed for the XML Schema validator to work. */
if(!(prefix.equals("xsi"))) {
sb.append(prefix + ":");
namespacePrefix = prefix;
}
names += (" " + "xmlns:" + prefix + "=\"" + uri + "\"");
}
if(beginOfNamespace) {
sb.append(name + names);
} else {
sb.append(name);
}
// do attributes if there are any
if(attributes.getLength() > 0) {
int length = attributes.getLength();
for(int i = 0; i < length; i++) {
String attributeValue = attributes.getValue(i);
String attributeQName = attributes.getQName(i);
sb.append(" " + attributeQName + "=\"" + attributeValue + "\"");
}
}
sb.append(">");
sb.append(cdata);
int size = childNodes.size();
for(int i = 0; i < size; i++) {
XUINode e = (XUINode)childNodes.get(i);
sb.append(e.toString());
}
if(size > 0) {
sb.append("\n");
for(int s = 0; s < (level); s++)
sb.append(" ");
}
if(namespacePrefix.length() > 0) {
sb.append("</" + namespacePrefix + ":" + name + ">");
} else {
sb.append("</" + name + ">");
}
return sb.toString();
}
|
另一个值得探讨的部分是容器类型 XUIWindow,它是 XUIComponent 的间接子类型。XUIWindow 实现表示一个 javax.swing.JFrame 组件,因此它必须允许将子元素添加到布局中。清单 6 展示了它的实现。首先必须确保只有某些类型的子元素可以添加到 XUIWindow 中。这样,XUIComponent 的 DOM 节点表示(XUINode)将被检索以便访问该元素的属性。注意,这要求所有 XUIComponent 的构造器初始化这些值。
下面进一步检查以确保以下两点:该组件是一个中级容器(比如 XUIPanel);这个中级容器能够包含在 XUIWindow 的行列网格中。最后,将组件添加到 XUIWindow 要确保以下两点:组件已启用并在布局网格中设置了正确的位置;XUIWindow 的 XUINode(win 变量)获得了一个引用 —— 引用新的子组件的 XUINode,即 addChildNode() 调用。
清单 6. XUIWindow
addComponent 方法实现
public void addComponent(XUIComponent component) throws XUITypeFormatException {
if(component instanceof XUIBasicDialog
|| component instanceof XUIOpenFileDialog
|| component instanceof XUICustomDialog
|| component instanceof XUIMenuBar
|| component instanceof XUIPanel
|| component instanceof XUISplitPanel
|| component instanceof XUITabbedPanel
|| component instanceof XUISaveFileDialog) {
// get the node
XUINode node = component.getNodeRepresentation();
if(!(component instanceof XUIMenuBar)) {
int x = Integer.parseInt(node.getAttributeValue("x"));
int y = Integer.parseInt(node.getAttributeValue("y"));
int width = Integer.parseInt(node.getAttributeValue("width"));
int height = Integer.parseInt(node.getAttributeValue("height"));
// can't add dialogs so need to check for type here.
if(component instanceof XUIBasicDialog
|| component instanceof XUIOpenFileDialog
|| component instanceof XUICustomDialog
|| component instanceof XUISaveFileDialog) ; // nothing
else {
// check to make sure it fits within the grid.
Dimension localGrid = this.getGrid();
if(width > localGrid.getWidth() || height >
localGrid.getHeight()) {
throw new XUITypeFormatException(node.getName()
+ " (id: " + node.getAttributeID()
+ ") must be within this window's grid width and"
+ "height (w: " + localGrid.getWidth()
+ " + h: " + localGrid.getHeight() + ")");
}
Rectangle rect = new Rectangle(y, x, width, height);
component.getPeer().setEnabled(true);
frame.getContentPane().add(component.getPeer(), rect);
// for mapping components to the regions they occupy
childComponentMappings.put(component, rect);
}
component.setComponentLocation(x, y);
} else {
// do specifics for a menubar
frame.setJMenuBar((JMenuBar)component.getPeer());
}
frame.invalidate();
frame.validate();
// add the component's node
int level = win.getLevel();
node.setLevel(++level);
if(win.getParent() == null)
win.addChildNode(node);
} else {
StringBuffer sb = new StringBuffer();
sb.append("Type not supported in XUIWindow. ");
sb.appen("The following types are supported:\n");
for(int i = 0; i < supportedComponents.size(); i++) {
String s = (String)supportedComponents.get(i);
sb.append("- " + s + "\n");
}
throw new XUITypeFormatException(sb.toString());
}
}
|
最后一个需要研究的代码部分是处理运行时绑定。调用 XUI 对象的 bind 方法时,BindingFactory 的一个实例将被调用。
要将模型代码绑定到已构造的 GUI,BindingFactory 的 doBinding 方法(见 清单 7)必须执行以下操作:
- 捕获 URL,无论在本地、Internet 上或相对位置。
- 通过
JarURLConnection类检查 JAR 并使用一个单独的自定义类加载器加载类。 - 从加载的 XML 文档查找一个类,它匹配
Resource元素的class属性的名称。这个类是模型的入口点。 - 使用 Java 反射框架实例化这个充当入口点的类并调用它的
init方法。init方法在概念上类似于一个典型 Java 类的main方法,因为它们都是入口点。 - 如果 JAR 文件包含图像,还需要将图像载入内存。
清单 7.
BindingFactory 的 doBinding 方法
public void doBinding(XUINode resource, XUI xui) throws XUIBindingException,
MalformedURLException, IOException {
if(resource.getAttributeValue("type").equals("java")) {
String className = resource.getAttributeValue("class");
String aURLString = resource.getAttributeValue("uri");
URL url = null;
// get the url ... if it's not a valid URL, then try and grab
// it as a relative URL (i.e. java.io.File). If that fails
// re-throw the exception, it's toast
try {
url = new URL("jar:" + aURLString + "!/");
} catch (MalformedURLException mue) {
String s = "jar:file://" + new File(aURLString)
.getAbsolutePath().replace("\\", "/") + "!/";
url = new URL(s);
if(url == null) {
// it really was malformed after all
throw new
MalformedURLException("Couldn't bind to: "
+ aURLString);
}
}
// get a jar connection
JarURLConnection jarConnection = (JarURLConnection)url.openConnection();
// get the jar file
JarFile jarFile = jarConnection.getJarFile();
// jar files have entries. Cycle through the entries until finding
// the class sought after.
Enumeration entries = jarFile.entries();
// the class that will be the entry point into the model
JarEntry modelClassEntry = null;
Class modelClass = null;
XUIClassLoader xuiLoader =
new XUIClassLoader(this.getClass().getClassLoader());
while(entries.hasMoreElements()) {
JarEntry remoteClass = (JarEntry)entries.nextElement();
// load the classes
if(remoteClass.getName().endsWith(".class")) {
// have to get the second last word between period marks. This
// is because the convention allows for:
// org.purnamaproject.xui.XUI
// that is, the periods can represent packages.
StringTokenizer st =
new StringTokenizer(remoteClass.getName(), ".");
String previousToken = st.nextToken();
String currentToken = "";
String nameOfClassToLoad = previousToken;
while(st.hasMoreTokens()) {
currentToken = st.nextToken();
if(currentToken.equals("class"))
nameOfClassToLoad = previousToken;
else {
nameOfClassToLoad += currentToken;
}
}
// get an output stream (byte based) attach it to the
//inputstream from the jar file based on the jar entry.
ByteArrayOutputStream baos = new ByteArrayOutputStream();
InputStream is = jarFile.getInputStream(remoteClass);
final byte[] bytes = new byte[1024];
int read = 0;
while ((read = is.read(bytes)) >= 0) {
baos.write(bytes, 0, read);
}
Class c = xuiLoader.getXUIClass(nameOfClassToLoad, baos);
// check for the class that has the init method.
if(remoteClass.getName().equals(className + ".class")) {
modelClassEntry = remoteClass;
modelClass = c;
}
} else {
String imageNameLowerCase = remoteClass.getName().toLowerCase();
if(imageNameLowerCase.endsWith(".jpeg")
|| imageNameLowerCase.endsWith(".jpg")
|| imageNameLowerCase.endsWith(".gif")
|| imageNameLowerCase.endsWith(".png")) {
// add resources (images)
XUIResources.getInstance().addResource(remoteClass, jarFile);
}
}
}
// now instantiate the model.
try {
// create a new instance of this class
Object o = modelClass.newInstance();
// get the method called 'init'. This is part of the API
// requirement
Method m = modelClass.getMethod("init", new Class[] {XUI.class});
// at last, call the method up.
m.invoke(o, new Object[] {xui});
} catch(InstantiationException ie) {
ie.printStackTrace();
} catch(IllegalAccessException iae) {
iae.printStackTrace();
} catch(NoSuchMethodException nsm) {
nsm.printStackTrace();
} catch(InvocationTargetException ite) {
System.out.println(ite.getTargetException());
ite.printStackTrace();
}
} else {
throw new XUIBindingException(
"This platform/API requires Java libraries.");
}
}
|
检查这个框架的机制后,现在让我们测试一下这个框架并展示一个应用程序示例。
项目框架(见 下载)包含几个示例,其中 Web 浏览器示例的内容非常详尽。
这个示例是一个相对真实的示例,您可能会打算将它放进一个声明性 XML UI 文档中。查看 清单 8,这个主 Window 指定了 x 和 y 坐标以及一个 id 值。所有元素必须拥有惟一的 ID 值,以便业务逻辑能够引用这些元素。
Window 元素包含以下几个子元素:
Panel:提供主要布局OpenFileDialog:打开新的 Web 页面SaveFileDialog:保存当前查看的 Web 页面CustomDialog:显示一个 yes 或 no 退出对话框CustomDialog:显示 Web 书签MenuBar:显示在Window的顶部,并提供菜单项功能Resource:引用驱动这个 UI 的 Java 模型代码
所含组件(比如 Button)的所有坐标用于表示在网格内的位置。所含组件的全部大小是指每个组件在网格内的宽度和高度的单元数量。元素的定义是高度声明性的,因为它们定义的是属性,而不是关于如何使用和创建那些属性的逻辑。本文的其他几个相关知识点包括:
MenuItem可能有快捷键,比如 Ctrl-X 用于退出应用程序。Window拥有多个对话框,但默认情况下这些对话框不会显示,除非用户调用它们。- 所有容器(比如
Panel)必须拥有布局,而且必须指定布局中的行数和列数。
清单 8. Web 浏览器 XML UI
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated by The Purnama Project XUI API version 0.5 -->
<xui:XUI xmlns:xui="http://xml.bcit.ca/PurnamaProject/2003/xui"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xml.bcit.ca/PurnamaProject/2003/xui ../../xui.xsd"
id="http://xml.bcit.ca/PurnamaProject/examples/XUIWebBrowser">
<xui:Window id="window_0" name="XUI Web Browser" x="200" y="20" width="800"
height="600" visible="true">
<xui:GridLayout width="1" height="1"></xui:GridLayout>
<xui:Panel x="0" y="0" width="1" height="1" id="panel_0" name="main panel"
idref="window_0">
<xui:GridLayout width="8" height="8"></xui:GridLayout>
<xui:HypertextPane x="1" y="0" width="8" height="7" id="hyper_0"
uri="http://www.w3c.org"></xui:HypertextPane>
<xui:Button x="0" y="0" width="1" height="1" id="button_0" label="Back"
enabled="true" orientation="horizontal"></xui:Button>
<xui:Button x="0" y="3" width="1" height="1" id="button_1" label="Home"
enabled="true" orientation="horizontal"></xui:Button>
<xui:Button x="0" y="7" width="1" height="1" id="button_2"
label="Forward" enabled="true" orientation="horizontal"></xui:Button>
</xui:Panel>
<!-- For opening files. Only want to see html files -->
<xui:OpenFileDialog x="10" y="10" width="400" height="300"
id="filedialog_0" idref="window_0" visible="false">
<xui:Filter>html</xui:Filter>
<xui:Filter>htm</xui:Filter>
</xui:OpenFileDialog>
<!-- For saving files. Only want to save html files -->
<xui:SaveFileDialog x="10" y="10" width="400" height="300"
id="savedialog_0" idref="window_0" visible="false">
<xui:Filter>html</xui:Filter>
<xui:Filter>htm</xui:Filter>
</xui:SaveFileDialog>
<!-- Ask the user if they really want to quit -->
<xui:CustomDialog x="200" y="200" width="320" height="160"
id="customdialog_1" idref="window_0" name="Exit Purnama Browser"
modal="true" visible="false">
<xui:GridLayout width="1" height="1"></xui:GridLayout>
<xui:Panel x="0" y="0" width="1" height="1" id="panel_2"
name="Quit Panel" idref="customdialog_0">
<xui:GridLayout width="5" height="4"></xui:GridLayout>
<xui:Label id="label_0" x="1" y="1" width="3" height="1"
justified="center" text="Do you really want to exit?"></xui:Label>
<xui:Button x="2" y="1" width="1" height="1" id="button_3"label="Yes"
enabled="true" orientation="horizontal"></xui:Button>
<xui:Button x="2" y="3" width="1" height="1" id="button_4" label="No"
enabled="true" orientation="horizontal"></xui:Button>
</xui:Panel>
</xui:CustomDialog>
<!-- For displaying the bookmarks -->
<xui:CustomDialog x="100" y="100" width="300" height="300"
id="customdialog_0" idref="window_0" name="Bookmarks" modal="false"
visible="false">
<xui:GridLayout width="1" height="1"></xui:GridLayout>
<xui:Panel x="0" y="0" width="1" height="1" id="panel_1"
name="bookmarks panel" idref="customdialog_0">
<xui:GridLayout width="1" height="1"></xui:GridLayout>
<xui:List x="0" y="0" width="1" height="1" id="list_0" enabled="true"
itemSelected="0" scrolling="vertical">
<xui:ListItem>http://www.w3c.org</xui:ListItem>
<xui:ListItem>http://www.agentcities.org</xui:ListItem>
<xui:ListItem>http://www.apache.org</xui:ListItem>
<xui:ListItem>http://www.gnu.org</xui:ListItem>
</xui:List>
</xui:Panel>
</xui:CustomDialog>
<!-- The menu bar with pop-up menu items too -->
<xui:MenuBar id="menuBar_0" idref="window_0">
<xui:Menu id="menu_0" idref="menuBar_0" enabled="true"
isPopupMenu="false" isSubMenu="false" label="File">
<xui:MenuItem id="mi_1" idref="menu_0" enabled="true" label="Open URL"
checked="false">
<xui:Shortcut keyCode="F" keyModifier1="ALT"></xui:Shortcut>
</xui:MenuItem>
<xui:MenuItem id="mi_0" idref="menu_0" enabled="true" label="Save"
checked="false">
<xui:Shortcut keyCode="F" keyModifier1="ALT"></xui:Shortcut>
</xui:MenuItem>
<xui:MenuItem id="mi_2" idref="menu_0" enabled="true" label="Exit"
checked="false">
<xui:Shortcut keyCode="X" keyModifier1="CTRL"></xui:Shortcut>
</xui:MenuItem>
</xui:Menu>
<xui:Menu id="menu_1" idref="menuBar_0" enabled="true"
isPopupMenu="false" isSubMenu="false" label="Bookmarks">
<xui:MenuItem id="mi_3" idref="menu_1" enabled="true"
label="Add Bookmark" checked="false">
<xui:Shortcut keyCode="D" keyModifier1="CTRL"></xui:Shortcut>
</xui:MenuItem>
<xui:MenuItem id="mi_4" idref="menu_0" enabled="true"
label="Manage Bookmarks" checked="false">
<xui:Shortcut keyCode="M" keyModifier1="CTRL"></xui:Shortcut>
</xui:MenuItem>
</xui:Menu>
<xui:Menu id="menu_2" idref="hyper_0" enabled="true" isPopupMenu="true"
isSubMenu="false" label="">
<xui:MenuItem id="mi_5" idref="menu_2" enabled="true"
label="Save As ..." checked="false"></xui:MenuItem>
<xui:MenuItem id="mi_6" idref="menu_2" enabled="true" label="Previous"
checked="false"></xui:MenuItem>
<xui:MenuItem id="mi_7" idref="menu_2" enabled="true" label="Next"
checked="false"></xui:MenuItem>
<xui:MenuItem id="mi_8" idref="menu_2" enabled="true" label="Home"
checked="false"></xui:MenuItem>
<xui:MenuItem id="mi_9" idref="menu_2" enabled="true" label="Bookmark"
checked="false"></xui:MenuItem>
</xui:Menu>
</xui:MenuBar>
</xui:Window>
<!-- The library (model) code that drives the user interface -->
<xui:Resource type="java" class="BrowserModel" uri="BrowserModel.jar"/>
</xui:XUI>
|
当然,如果没有用户交互,这个 UI 没有任何价值。下面就介绍如何进行用户交互。
在 清单 8 中,Resource 元素包含一个类名,该类充当进入应用程序模型的入口点。清单中给出的类名是 BrowserModel,因此,在 Java 端,已编译的类名必须与之匹配。类名包括名称空间,本例中为默认名称空间。
因此,任何类都可以充当进入应用程序的模型部分的入口点,只要它的名称与 Resource 元素的 class 属性值相同。要让用户交互在运行时能够正确连接,实现类必须遵循以下几个规则:
- 使一个方法带有以下签名:
public void init(XUI document)。 - 实现适当的事件处理接口以监听事件(比如用于
XUIButton实现的ActionModel)。 - 使用 XML 元素的
id值引用 GUI 组件(这可以通过使用XUI中的几个方法实现)。 - 将自身作为监听器添加到适当的组件。这个框架中的所有事件生成组件(比如
XUIButton类实现)都实现XUIEventSource,因此都生成 UI 事件。
在 清单 9 中,BrowserModel 类在 init 方法中执行自己的初始化。初始化过程包括通过 id 值引用组件,创建包含 Web URL 书签的菜单项,通过 addEventListener 方法将自身作为一个监听器添加到多个组件。BrowserModel 可以将自身添加为一个监听器,因为它是一个 XUIModel(ActionModel 是 XUIModel 的子类型)。还有一点值得一提:XUIComponentFactory 类提供多种创建 XUI 组件的方法。
清单 9. 部分代码清单:初始化
...
import org.purnamaproject.xui.binding.ActionModel;
...
public class BrowserModel implements ActionModel, TextModel, WindowModel,
ListActionModel {
...
private XUI xui;
...
public void init(XUI document) {
xui = document;
...
bookmarksList = (XUIList)xui.getXUIComponent("list_0");
homeButton = (XUIButton)xui.getXUIComponent("button_1");
...
List bookmarks = bookmarksList.getItems();
for(int i = 0; i < bookmarks.size(); i++) {
String url = (String)bookmarks.get(i);
XUIMenuItem aMenuItem = XUIComponentFactory.makeMenuItem(url);
bookmarksMenu.addMenuItem(aMenuItem);
linkModel.addSource(aMenuItem);
aMenuItem.addEventListener(linkModel);
}
...
homeButton.addEventListener(this);
...
}
...
}
|
清单 10 显示了针对各种组件的事件处理代码,例如:
openMenuItem将导致一个fileDialog出现(一个模式对话框将打开一个本地存储的 Web 网页)。homeButton和popuphomeMenuItem(在窗口中通过单击右键访问)都会调用doHome方法,该方法将浏览器导向HypertextPane元素(参见 清单 8)的uri属性值。fileDialog将加载一个新文件,然后递增index变量,应用程序的queue使用该变量跟踪此前访问过的 Web 页面。
清单 10. 部分代码清单:事件处理
public void action(XUIComponent component)
{
if(component == openMenuItem) {
fileDialog.setVisible(true);
} else if(component == homeButton || component == popuphomeMenuItem) {
doHome();
} else if(component == prevButton || component == popupprevMenuItem) {
doPrevious();
} else if(component == nextButton || component == popupnextMenuItem) {
doNext();
} else if(component == fileDialog) {
if(fileDialog.getSelectedFile() !=null)
hyperTextPane.setURL(fileDialog.getSelectedFileAsURL());
index++;
if(index != queue.size()) {
nextButton.setEnabled(false);
popupnextMenuItem.setEnabled(false);
for(int i = index; i < queue.size(); i++) {
queue.remove(i);
}
}
queue.add(hyperTextPane.getURL());
prevButton.setEnabled(true);
popupprevMenuItem.setEnabled(true);
} else if(component == saveDialog) {
try {
FileOutputStream fos = new FileOutputStream(saveDialog.getSelectedFile());
hyperTextPane.getDocument().writeTo(fos);
} catch (FileNotFoundException fnfe) {
fnfe.printStackTrace();
} catch (IOException ioe) {
ioe.printStackTrace();
}
} else if(component == popupsaveasMenuItem || component == saveMenuItem) {
saveDialog.setVisible(true);
} else if(component == popupbookmarkMenuItem || component == bookmarkMenuItem) {
doBookmark(hyperTextPane.getURL());
} else if(component == notDontExit) {
exitDialog.setVisible(false);
browserWindow.setVisible(true);
} else if(component == yesExit) {
System.exit(0);
} else if(component == exitMenuItem) {
exitDialog.setVisible(true);
} else if(component == manageBookmarksMenuItem) {
bookmarksDialog.setVisible(true);
}
}
|
图 2 中最后的应用程序展示了一个基本的 Web 浏览器,它允许显示本地页面、基于 Web 的页面和此前访问过的 Web 页面,并提供书签管理功能。
图 2. Web 浏览器的屏幕截图
本文的 下载 部分包含其他几个示例应用程序。
尽管这个解决方案很精彩,但这种方法是相当理想化的:框架中的安全问题被忽略了。回想一下这个 API 是如何随意地从任何 URI 加载 JAR 文件的。回顾一下 清单 8 中的 Resource 元素。其类型实际上是 anyURI,这意味着本地文件、网络上的文件和 Internet 上的文件。一个应用程序应该信任来自任何地方的业务逻辑吗?显然,您需要考虑某种安全模型以限制不可信资源的加载。解决这个问题的一种方法是通过引用一个查找表来限制 URI。另一种更干净的方法是使用数字证书。
最后要注意一点,在这个声明性 XML UI 格式中可以加载其他 XML 格式。由于需要使用名称空间,这个 XML 模式支持这个功能。作为一个示例,您可以在 XML 文档中嵌入一个单独的 XML 格式来表示可伸缩的矢量图形。
本文介绍了声明性 XML UI 语言的定义和外观,一个配套 Java 框架和一个示例应用程序 — 一个 Web 浏览器。最后,本文提出了潜在的安全问题和其他应该关注的问题。
创建声明性 XML UI 并不是什么新技术,它是一个日渐成熟和应用广泛的软件开发领域。这种技术的好处之一是有助于促进软件重用和模块化。
| 描述 | 名字 | 大小 | 下载方法 |
|---|---|---|---|
| 本文的 XUI XSD 和 Java API1 | xui.zip | 30KB | HTTP |
注意:
- 这个 .zip 文件包含本文引用和使用的所有源代码,包括 Apache Ant 构建文件、XML 模式、示例和第三方库(JARs)。
学习
- XML Schema Part 0: Primer:这个 W3C 入门教程是了解 XML 模式的一个好的起点。
- XML Schema Part1: Structures:了解 XML 模式中的数据结构。
- XML Schema Part 2: Datatypes:了解 XML 中定义的数据类型。
- Swing 教程:Sun 公司的这个教程详细介绍如何在 Swing 中为应用程序和 applets 开发 GUI。
- Document Object Model (DOM) on W3C:在这个与 DOM 相关的材料概述中了解关于 W3C Document Object Model 的更多信息。
- Base64 数据(Wikipedia):阅读 Base64 编码的详细解释。
- XAML:了解关于 XAML 的更多信息。
- MXML 格式:了解关于这种 Adobe 格式的更多信息。
- 使用 XML Schema 定义元素的基本知识(Ashvin Radiya 和 Vibha Dixit,developerWorks,2000 年 8 月):阅读这个很不错的 XML 模式简介,尝试使用 XML 模式代替 DTD 定义 XML 文档的结构。
- Tip: Work with schemas and namespaces(Brett McLaughlin,developerWorks,2002 年 9 月):揭秘 XML 模式中的名称空间,包括多个名称空间。
- 实现 Castor 数据绑定,第 2 部分: 编组和解组 XML
(Brett D. McLaughlin, Sr.,developerWorks,2007 年 12 月):阅读这篇介绍如何使用 Castor 编组和解组的好文章。
- IBM XML 认证:了解如何才能成为一名 IBM 认证的 XML
和相关技术的开发人员。
- XML 技术库:访问 developerWorks XML 专区
,获得广泛的技术文章和技巧、教程、标准和 IBM 红皮书。
- developerWorks 技术活动 和 网络广播:随时关
注这些活动中的技术。
-
技术书店:浏览关于这个主题和其他
技术主题的图书。
- developerWorks
播客:收听面向软件开发人员的有趣访谈和讨论。
获得产品和技术
- Glade:了解和下载 Glade,这是一个 RAD 工具,支持针对 GTK+ 工具包和 GNOME 桌面环境的快速、简便的 UI 部署。
- 下载 IBM 产品评估版
或 在线试用
IBM SOA Sandbox,并开始使用来自 DB2®、Lotus®、Rational®、Tivoli® 和
WebSphere® 的应用程序开发工具和中间件产品。
讨论
- XML 专区讨论论坛:参与任何一个与 XML 相关的讨论。
-
参与 developerWorks 博
客 并加入 developerWorks 社区。

Arron Ferguson 担任了 12 年大学讲师,在 British Columbia Institute of Technology 讲授软件工程。他的经验和兴趣包括 Java 技术、XML、Web 技术、2D 和 3D 动画、数字媒体创作。他还是一位自由技术编辑和评论家,出版了一本图书:Creating Content Management Systems in Java(Charles River Media,2006 年)。Arron 还热衷于支持 Linux 和其他开源技术。