IBM®
跳转到主要内容
    中国 [选择]    使用条款
 
 
Select a scope: Search for:    
    首页    产品    服务与解决方案     支持与下载    个性化服务    
跳转到主要内容

developerWorks 中国  >  XML | Java technology | Web development  >

创建一种声明性 XML UI 语言

用 Java 语言构建一个 UI 和配套框架

developerWorks
文档选项

未显示需要 JavaScript 的文档选项

样例代码

英文原文

英文原文


级别: 中级

Arron Ferguson, 大学讲师, IBM

2009 年 10 月 15 日

用编程代码编写 GUI 常常导致混乱的设计,这反过来导致业务逻辑和 UI 代码之间的混淆。本文探讨如何创建带有配套 Java™ 框架的声明性 XML UI 标记集合,以便在运行时解析、构造并最终将已声明的 GUI 组件绑定到业务逻辑。

简介

GUI 开发可能是令人畏惧的任务。GUI 框架并不是总是拥有良好的文档,需要的代码量可能迅速增长,拖慢开发工作流。特别是支持这些 GUI 框架的拖放工具和 IDE 通常诱使 GUI 软件开发人员创建难以管理和阅读的代码。这可能会进一步混淆业务逻辑和 GUI 描述代码之间的界限,从而使软件维护更加困难。

常用缩略词
  • API:应用程序编程接口
  • DOM:文档对象模型
  • GUI:图形用户界面
  • HTML:超文本标记语言
  • IDE:集成开发环境
  • JAR:Java 压缩文档
  • UI:用户界面
  • URI:统一资源标识符
  • XML:可扩展标记语言
  • XSD:XML 模式信息集模型
  • W3C:万维网联盟

这就是声明性 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

第一个 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 模式

这个声明性 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,允许列举 javagroovy 的字面文本值。

Resource 元素的目的是向这个 Java 框架提供一个资源(本例中是一个 JAR)的 URI,该资源包含运行时可以加载并绑定的已编译 Java 类。这个资源依赖于将被调用的一个特殊类(class 属性的值),该类主要用于提供一个已公开的类,以便响应从 GUI 生成的所有事件。

Window 元素:

  • 包含一个 GridLayout 序列,该序列可以包括 GridLayoutBasicDialogOpenFileDialogSaveFileDialogCustomDialogPanelSplitPaneTabbedPane 元素,以及 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 元素的数量
  • 对话框是否是模式对话框(模式对话框直到用户关闭对话框时才捕捉焦点)
  • 桌面中的坐标(xy,以像素为单位)
  • 大小(宽度和高度,以像素为单位)
  • 窗口的可见性

Panel 是一个中级容器,可以包含相当多的原子组件。再看一下 清单 3Panel 拥有一个 GridLayout,可以选择在 Panel 内不放置原子组件,也可以根据需要放置任意数量的原子组件。Panel 本身具有 xy 坐标。Panel 使用 xy 坐标引用父容器的 GridLayout 中的定位,而不是引用桌面中的像素(就像 CustomDialog 一样)。就像俄罗斯玩偶一样,这种嵌套构造法非常类似于 Swing 的布局规则。了解上述基本知识后,现在可以解决软件实现问题了。





回页首


支持的 Java 框架

我们首先简要介绍建议的 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 编组到文件)。这些步骤是:

文档对象模型(Document Object Model)是什么?

文档对象模型(简称 DOM)是 XML 元素到内存对象(由 API 提供)的映射,这种映射允许程序员将 XML 数据读入内存对象(称为解组),操作数据,然后将数据写回到 XML(称为编组)。最著名的常用 DOM 是 W3C DOM。

图 1 展示了以下步骤:

  1. BuilderFactory 检索一个 Builder
  2. 在允许检索一个 XUI 文档之前,Builder 首先确保 XML 文档已经被验证和解析。如果解析或验证失败,将出现一个 XUIParseException,框架将停止文档加载。
  3. Builder 创建 DOM,其中的对象反映读入的 XML 元素。
  4. Realizer 对象(在内部由 XUI 对象调用)被实例化并准备执行下一步。
  5. Realizing 是框架根据预先创建的 XML 节点层级(框架引擎的真正核心)创建 GUI 组件层级的地方。
  6. 使用 Java 环境中的 power of reflection,模型逻辑(应用程序中驱动 UI 的部分)被绑定到刚才实现的 GUI 组件。

图 1. XUI API 用于构建 GUI 的框架流和详细步骤视图
XUI API 用于构建 GUI 的框架流和详细步骤视图

这个共包含 6 个步骤的调用流易于使用,但其中包含大量信息和对象实例化,值得仔细研究。这个框架的核心位于步骤 5 和步骤 6

GUI 组件 & XML 节点构成

图 1 中,步骤 5 创建了一个组件模型,支持将这个 XML 节点(现在是内存对象)和一个 GUI 组件组成一对。这种配对需要非常严格地同步以下事件:

  • 对于框架读入的每个 XUINode(表示任意 XML 元素的内存对象),必须创建一个 XUIComponent 来包围 XUINode
  • 对于在内存中创建的每个 XUIComponent,必须创建一个 GUI 对等物,比如 javax.swing.JFrame
  • 每当一个 XUIComponent 实例 — 或它的一个子类型(比如 XUIButton)— 被修改时(比如修改大小),XUIComponent 必须确保 XUINode 和 GUI 对等物同时、同等地更新。

通过满足上述要求,这个框架允许程序员将多个 XML 文档读入内存(解组),修改 DOM,然后将更改保存回一个 XML 文档(编组)。程序员甚至可以以编程方式创建几个新的 DOM 并对它们进行编组。

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 要确保以下两点:组件已启用并在布局网格中设置了正确的位置;XUIWindowXUINodewin 变量)获得了一个引用 —— 引用新的子组件的 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,BindingFactorydoBinding 方法(见 清单 7)必须执行以下操作:

  • 捕获 URL,无论在本地、Internet 上或相对位置。
  • 通过 JarURLConnection 类检查 JAR 并使用一个单独的自定义类加载器加载类。
  • 从加载的 XML 文档查找一个类,它匹配 Resource 元素的 class 属性的名称。这个类是模型的入口点。
  • 使用 Java 反射框架实例化这个充当入口点的类并调用它的 init 方法。init 方法在概念上类似于一个典型 Java 类的 main 方法,因为它们都是入口点。
  • 如果 JAR 文件包含图像,还需要将图像载入内存。

清单 7. BindingFactorydoBinding 方法
				
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 浏览器示例的内容非常详尽。

Web 浏览器 XML UI 文档

这个示例是一个相对真实的示例,您可能会打算将它放进一个声明性 XML UI 文档中。查看 清单 8,这个主 Window 指定了 xy 坐标以及一个 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 没有任何价值。下面就介绍如何进行用户交互。

Web 浏览器 Java 代码模型逻辑

清单 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 可以将自身添加为一个监听器,因为它是一个 XUIModelActionModelXUIModel 的子类型)。还有一点值得一提: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 网页)。
  • homeButtonpopuphomeMenuItem(在窗口中通过单击右键访问)都会调用 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 浏览器的屏幕截图
显示本地页面、基于 Web 的页面和此前访问过的 Web 页面的 Web 浏览器和书签管理器的屏幕截图

本文的 下载 部分包含其他几个示例应用程序。





回页首


问题和挑战

尽管这个解决方案很精彩,但这种方法是相当理想化的:框架中的安全问题被忽略了。回想一下这个 API 是如何随意地从任何 URI 加载 JAR 文件的。回顾一下 清单 8 中的 Resource 元素。其类型实际上是 anyURI,这意味着本地文件、网络上的文件和 Internet 上的文件。一个应用程序应该信任来自任何地方的业务逻辑吗?显然,您需要考虑某种安全模型以限制不可信资源的加载。解决这个问题的一种方法是通过引用一个查找表来限制 URI。另一种更干净的方法是使用数字证书。

最后要注意一点,在这个声明性 XML UI 格式中可以加载其他 XML 格式。由于需要使用名称空间,这个 XML 模式支持这个功能。作为一个示例,您可以在 XML 文档中嵌入一个单独的 XML 格式来表示可伸缩的矢量图形。





回页首


结束语

本文介绍了声明性 XML UI 语言的定义和外观,一个配套 Java 框架和一个示例应用程序 — 一个 Web 浏览器。最后,本文提出了潜在的安全问题和其他应该关注的问题。

创建声明性 XML UI 并不是什么新技术,它是一个日渐成熟和应用广泛的软件开发领域。这种技术的好处之一是有助于促进软件重用和模块化。






回页首


下载

描述名字大小下载方法
本文的 XUI XSD 和 Java API1xui.zip30KBHTTP
关于下载方法的信息

注意:

  1. 这个 .zip 文件包含本文引用和使用的所有源代码,包括 Apache Ant 构建文件、XML 模式、示例和第三方库(JARs)。


参考资料

学习

获得产品和技术
  • Glade:了解和下载 Glade,这是一个 RAD 工具,支持针对 GTK+ 工具包和 GNOME 桌面环境的快速、简便的 UI 部署。

  • 下载 IBM 产品评估版在线试用 IBM SOA Sandbox,并开始使用来自 DB2®、Lotus®、Rational®、Tivoli® 和 WebSphere® 的应用程序开发工具和中间件产品。


讨论


关于作者

Arron Ferguson 的照片

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 和其他开源技术。




对本文的评价










回页首


IBM、IBM 徽标、ibm.com、DB2、developerWorks、Lotus、Rational、Tivoli 和 WebSphere 是 International Business Machines 公司在美国和/或其他国家的商标或注册商标。这些和其他 IBM 商标词汇在内容中首次出现时用适当的符号(® 或 ™)加以标记,表示这些美国注册商标或普通法商标在发布本信息时归 IBM 所有。这些商标还可能是其他国家的注册商标或普通法商标。请查阅最新的 IBM 商标 列表。 Adobe、Adobe 徽标、PostScript 和 PostScript 徽标是 Adobe Systems 公司在美国和/或其他国家的商标或注册商标。 Java 和所有基于 Java 的商标是 Sun Microsystems, Inc. 在美国和/或其他国家的商标。 Microsoft、Windows 和 Windows 徽标是 Microsoft Corporation 在美国和/或其他国家的商标。 Linux 是 Linus Torvalds 在美国和/或其他国家的商标。 UNIX 是 Open Group 在美国和其他国家的注册商标。 其他公司、产品或服务的名称可能是其他公司的商标或服务标志。 其他公司、产品或服务的名称可能是其他公司的商标或服务标志。

IBM 公司保留在 developerWorks 网站上发表的内容的著作权。未经IBM公司或原始作者的书面明确许可,请勿转载。如果您希望转载,请通过 提交转载请求表单 联系我们的编辑团队。
    关于 IBM 隐私条约 联系 IBM 使用条款