资源描述
SVG 和 XForms: 呈现定制内容
使用 SVG 1.2 新的基于 XML 的扩展机制
Antoine Quint (antoine.quint@fuchsia-)), SVG 顾问与研究科学家, Fuchsia Design
简介: 第一个可伸缩向量图形(Scalable Vector Graphics,SVG 1.0)规范奠基了 XML 表示二维交互式图像和动画的标准。从那以后,W3C SVG 工作组一直致力于改进它的特性,使 SVG 更容易在 Web 和桌面应用程序开发中使用。SVG 1.2 一个很有前途的特性是呈现定制内容(Rendering Custom Content, RCC)——它提供了清晰的以 XML 为中心的扩展机制,在一个 SVG 文档中混合与匹配不同的 XML 名称空间。本文在介绍 RCC 机制的同时将引导您创建一个简单的按钮部件。
在过去的两年中,SVG 团体已经研究出了新的方法,利用 SVG 的 XML 基础在单个 SVG 文档中混合与匹配多个名称空间。 在关注的主要焦点中,用户界面工作成为在 SVG 文档中内嵌使用定制 XML 标记的基本用例。 呈现定制内容(RCC)——在最新的 SVG 1.2 草案中推介的一项新技术——提供了一种新的框架,允许自定义 XML 语法作为格式良好的扩展无缝集成到 SVG 文档中。在本系列文章中的 基础知识一文中曾经提到,由于提供了混合与匹配这两类标记的基础,RCC 在 SVG 和 XForm 的集成中起着重要的作用。本文中将介绍 RCC 的基础,探讨如何实现一个用户界面部件(按钮),并提供一个更丰富的 UI 应用程序的演示。
RCC 基础
在开始用 RCC 编写这个简单的按钮部件之前,我将回顾一下基本知识和新推出的 RCC 元素家族。
将 XML 绑定到图形
使用 RCC,您可以规定给出的外部名称空间元素在 SVG 文档中的行为。下面这些是最基本的 SVG 代码,在开始用 RCC 编程时您一定会用到:
清单 1. 设置 RCC 环境
<extensionsDefs namespace="http://ns.example.org/MyCustomNamespace/">
<elementDef name="myCustomElement">
<prototype>
<!-- SVG markup specifying rendering here -->
</prototype>
</elementDef>
</extensionsDefs>
清单 1 中引入了两个新的 SVG 1.2 元素。 <extensionDefs> 是一个顶层元素,您将要包括的所有给定名称空间中的元素定义都将放在这个元素中。名称空间在明确的 namespace 属性中定义,该属性直接以 URI 作为属性值。仅仅是为了避免造成更大的混乱,这个属性与 XML 的 xmlns 属性并不完全相似——这里的名称空间永远是 SVG —— namespace 属性在这里只是表明所描述的定制元素属于哪个名称空间。从概念上讲, <extensionDefs> 帮您定义了一个库。现在,您需要定义在这个名称空间库中有哪些东西。要注册一个元素,您需要使用 <elementDef> 元素,并用 name 属性指定元素的名称。
基本上,RCC 是一种绑定机制,把非 SVG 的 XML 元素匹配到 SVG 树中。但是,RCC 并不是为各种绑定而设计的,相反,这种机制主要用于应该有某种交互式呈现的定制 XML 标记的绑定,从而映射到 SVG 片段。正如我在第一篇“基础知识”文章中提到的那样,SVG 片段在 RCC 世界中实际上是一个影像树。我无法用言语表达影像树所带来的种种不可思议的神奇作用,但是我将在以后再讨论它,因为它对于理解这种特性非常重要。要在预设的影像树中链接自定义的元素,您需要使用新的 <prototype> 元素。该元素的内容被自动复制到绑定时定义的每个元素实例的影像树中。
绑定可以在文档生命期中的两个不同阶段完成。第一次是在加载文档的时候:SVG 实现在一开始检查文档中已有的 <extensionDefs>元素,并确定哪些外部元素是已知的,然后在解析树的其他部分时每当遇到这些已知的元素就触发 RCC 绑定机制。绑定也可以发生在这些已知元素中的某一个被添加到树中的时候,比如通过 insertChild() DOM 调用增加元素。这使得整个绑定机制是 活动的,这是该过程与 XSLT 使用模板将 XML 元素绑定到另一个 XML 输出之间的一个重要区别,后者本质上是一次性的批处理操作。
影像树
现在绑定机制已经清楚了,非常重要的一点是要完全理解影像树在 RCC 上下文中的重要性和使用方法。我在以前的文章中曾经提到,影像树对于呈现定制元素的巨大好处是它保留了一个健康的、井井有条的文档树。使用 RCC 编写组件,比如用户界面部件,很可能涉及到一些巧妙的脚本使用抽象标记的 SVG 表示。通过影像树,这些代码逻辑可以依靠那些不容易被外部代码访问的 SVG 子树——尽管在需要时仍然能够访问。除了前述为 RCC 支持而添加的元素之外,SVG 1.2 增加了由所有定制元素实现的 DOM 接口SVGShadowable 。使用这个接口,您可以通过调用诸如 myCustomElement.shadowTree 这样的调用获得对定制元素影像树的访问。
RCC 和 DOM 事件
因为总是能够通过 DOM 访问定制元素的影像树,在对组件编程时您可以使用 活动绑定。只要监听定制元素能够触发的合适 DOM 事件,您就会不断地得到报告定制元素正在做什么。RCC 绑定机制提供了两个新的专用于绑定的事件:
· SVGBindBegin 对应绑定的起始阶段,即定制元素已经被解析但还没有建立和呈现为影像树的时候。
· SVGBindEnd 通知绑定阶段已经完成,即您的元素已经被绑定和呈现。
如果您希望定制元素仍然是活动的,您只能依靠 DOM 的突变事件,如 DOMAttrModified 用于检测属性的变化, DOMSubTreeModified则用于能够对其子树变化作出响应的定制容器。
模块性
我简要地提一下使用 <extensionDefs> 定义库的概念。这个概念也是非常合理的,因为事实上您不必为可能用到的每个文档都重新定义您的扩展。您可以使用 xlink:href 属性指明您的库定义的位置。因此,您可以像下面这样定义和使用基于 RCC 的 XForms 实现扩展:
清单 2. 跨越多个文件的 RCC 扩展体系结构
XFormsImplementation.svg
<svg version="1.2">
<extensionDefs id="xforms" namespace="http://www.w3.org/2002/xforms">
<!-- implementation -->
</extensionDefs>
</svg>
SomeFileUsingSVGandXFormsWithYourSpiffyRCCImplementation.svg
<svg version="1.2">
<extensionDefs xlink:href="XFormsImplementation.svg#xforms" />
<!-- some code that can use both XForms and SVG elements -->
</svg>
通过这个简单而优雅的库结构,几乎任何人都能够创建一个 RCC 库放在网络上供其他人使用。这样就简化了使用 RCC 开发 SVG 应用程序,并促进社区推动的工作在不远的将来能够提供免费的库。另外值得一提的是, <prototype> 元素的内容不仅可以是老的 SVG 图形元素,还可以包括 RCC 定义的定制元素。因此又引入了 RCC,这种递归形式非常适合创建重用并以其他组件为基础的组件。把这些结合在一起就形成了一个可以使用的非常不错的框架。现在您可以准备做一些具体的工作了。
回页首
使用 RCC 编程: <ui:button>
在简介中我曾经提到,RCC 最常见的用例是创建 UI 部件。要创建一个好的 UI 部件,即使非常简单的部件,也需要大量细致的工作。在此,我将说明如何创建一个非常简单的按钮(包括图形和编程两个方面)。
标记
在 RCC 结构中,标记实际上起着所要实现目标的蓝图的作用。如果您头脑中有一个希望用 SVG 和 RCC 机制设计的组件,您要做的第一件事可能就是在纸上勾勒出这种想法,并想象出标记应该是什么样子。这里要涉及的是一个简单的按钮,上面带有文本标签。简而言之,我要建立一个拥有几何与位置属性和文字标签的简单元素,把它作为一个子节点:
<ui:button x="10" y="40" width="150">ui:button</ui:button>
…… 它看起来是这个样子:
图 1. 显示的 <ui:button> 元素
我已经有了 <button> 元素。如果我是一个好的编程人员,应该提供匹配的模式来描述它。但是我只关心 SVG 方面,而不想告诉您在设计自己的组件时如何做出正确的决策。我的 <button> 元素不是一个 SVG 元素,因此必须存在于不同的名称空间中。您可能已经注意到,我在上面的代码片段中使用的 ui 前缀,这个前缀在 SVG 文档的根元素 <svg> 中定义:
清单 3. 在文档中声明名称空间
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:ui="http://xmlns.fuchsia-
xmlns:ev="http://www.w3.org/2001/xml-events"
version="1.2">
现在,我的 ui 名称空间已经定义了,后面如果需要实际放置按钮的不同实例时,就可以在文件中安全地使用 <ui:button> 元素了。不必费心访问名称空间的 URI,它什么地方也去不了。这里我对您的紧迫而精细的工作再建议一种更好的方法,希望您能使用 RDDL 文件或者类似的东西提供关于您的组件的某些信息。我还定义了另外两个名称空间和前缀,当然就是 SVG 名称空间(设置成默认名称空间)和 XML Events 名称空间(前缀是 ev )。顺便说一下,XML Events 建议现在是 SVG 1.2 的一部分,因此您同时拥有了设计和命名环境。这样就可以进入下一阶段,还是涉入到标记。这一次是组件定义的内容:
清单 4. 外部元素实现的第一步
<extensionDefs namespace="http://xmlns.fuchsia-
<elementDef name="button">
<prototype>
<svg id="root" width="0" height="26">
<rect id="rect" style="stroke: black; fill: lightgray;"
x=".5" y=".5" rx="5" ry="5" width="0" height="25" />
<text x="50%" y="65%"
style="pointer-events: none; text-anchor: middle;">
<refContent />
</text>
</svg>
</prototype>
<script ev:event="SVGBindEnd" type="text/ecmascript">
new SimpleRCCButton(evt.target);
</script>
<script type="text/ecmascript">
<!-- SimpleRCCButton class -->
</script>
</elementDef>
</extensionDefs>
我从包装 <extensionDefs> 元素开始,通知库定义的开始,按照在根元素 <svg> 中定义的一样,描述了以 ui 为前缀的命名空间。接下来是 <elementDef> 元素,初始化 <ui:button> 元素的定义。现在,我可以通知 SVG 实现有一个新的定制元素,我用 <prototype>元素定义了它的外观。如前所述,在这里可以放置需要复制到每个定制元素实例影像树中的任何 SVG 。如您在 图 1中所看到的那样,这个简陋的按钮是一个圆角矩形,中间显示了文字。SVG 强大的图形基础和文字特性使编写按钮的原型非常简单。
您可以使用 <rect> 元素与 rx 和 ry 属性获得圆角的效果。它的 style 属性指定了 CSS 性质:黑色边界的内部填充浅灰色。它有固定的 height (高度)25 和 width (宽度) 0.... 0!?是的,矩形的宽度与 <ui:button> 本身的宽度紧密联系,后面您将看到,程序中将在适当的时候改变矩形的宽度。 x 和 y 属性的值都是 0.5,以便边界线不会被父元素 <svg> 剪掉。接下来是 <text> 元素,它自身的text-anchor 放在字符串的中间,不接受鼠标事件(矩形完全能够捕获这些事件,但这样可以避免用户在点击按钮时,在文字上产生丑陋的视觉效果),并定位在父元素 <svg> 所建立的当前视点的中间。
您可能注意到一种奇怪的现象: <svg> 和 <rect> 上都有一个 id 属性。这些 id 如何在生成的每个影像树中保持唯一性呢?RCC 通过局部作用域的 id 解决了这个问题。当讨论代码时,使用这些 id 的原因就清楚了。您可能还注意到另外一件奇怪的事——<refContent/> 元素。这是 SVG 1.2 为 RCC 支持引入的另一个新元素,它的巧妙之处在于允许直接引用元素实例的 DOM 树。这里<refContent/> 元素用于粘贴影像树中的标签,并随时保持标签的同步。
现在已经定义了图形模板, <script> 元素通过 XML Events 的 event 属性在 ECMAScript 中定义了 SVGBindEnd 事件的处理程序。当元素实例的绑定完成时,包含在脚本中的那一行代码将被调用,它仅仅创建一个新的 SimpleRCCButton 类实例,并把它的构造函数指针作为唯一的参数传递给元素实例(事件的目标)。创建的 ECMAScript 对象负责无法用标记声明完成的一切工作,因此基本上是作为一个控制器。后面的 <script> 元素包含了从实例中摘选出的 SimpleRCCButton 类的所有代码——我将在后面讨论这个过程。现在,标记部分已经完成,应该开始考虑编写一些代码了!
代码
SimpleRCCButton 类的工作很简单:保证按钮能够及时响应变化和用户界面事件。第一项工作是编写这个类的构造函数,并保证能够很容易地引用后面将用到的几个元素:
清单 5. SimpleRCCButton 类构造函数
function SimpleRCCButton (element) {
// keep pointers to useful bits
this.element = element;
var shadow = this.element.shadowTree;
this.root = shadow.getElementById('root');
this.rect = shadow.getElementById('rect');
// initialize the shadow tree
this.init();
};
<ui:button> 的实例被作为参数传递给构造函数,我使用 this.element 保持对它的引用。然后我通过 shadowTree 在 shadow 变量中注册了指向实例元素影像树的指针。 shadowTree 是 SVGShadowable 接口的一个成员,被所有 RCC 定义的定制元素的实例所继承。此外,影像树包含两个需要跟踪的 SVG 元素。利用在 <prototype> 中精心设下的伏笔 id , this.root 保存了父元素 <svg> ,而圆角矩形则由 this.rect 保存。现在已经建立这些必要的引用,可以调用 init() 方法初始化元素的影像树了:
清单 6. SimpleRCCButton 初始化方法
SimpleRCCButton.prototype.init = function () {
// register mutation events on the element instance
this.element.addEventListener('DOMAttrModified', this, false);
// register UI events on the shadow tree's rectangle
this.rect.addEventListener('mousedown', this, false);
this.rect.addEventListener('mouseup', this, false);
this.rect.addEventListener('click', this, false);
// update the position and width of the button
this.fixButtonPosition();
this.fixButtonWidth();
};
第一项任务是在实例元素上为 DOMAttrModified (它对应属性值的变化)的变化事件注册一个事件监听器,以便跟踪通过 DOM 对按钮所作的修改。接下来要求对影像树的矩形监听各种不同的鼠标事件( mousedown 、 mouseup 和 click ),后面将看到如何响应这些事件。这一次全部使用 DOM Events EventTarget 接口中基本的 addEventListener() 方法。然后需要第一次固定按钮的位置和大小,其中涉及到使用 fixButtonPosition 和 fixButtonWidth 方法来调整影像树:
清单 7. SimpleRCCButton 类的刷新方法
SimpleRCCButton.prototype.fixButtonPosition = function () {
this.root.setAttribute('x', this.element.getAttribute('x'));
this.root.setAttribute('y', this.element.getAttribute('y'));
};
SimpleRCCButton.prototype.fixButtonWidth = function () {
var width = parseInt(this.element.getAttribute('width'));
this.root.setAttribute('width', width + 1);
this.rect.setAttribute('width', width);
};
这两个方法都包括非常简单的 DOM 代码。基本上是把属性值从实例元素中读出,并复制到影像树中的适当位置。对于按钮的位置,设置影像树中 <svg> 元素的 x 和 y 属性。设置按钮的宽度只是稍微复杂一点:复制矩形的宽度并在 <svg> 元素中增加了一个单位,为矩形的边框留下位置。
在我注册事件的监听器时,您可能注意到 this 指的是作为一个参数(第二个)传递的 DOM Events 的 EventListener 接口实例。我这样做是因为 EventListener 接口只规定了一个 handleEvent() 方法,这意味着任何实现该方法的对象都是事件监听器。在这里,每当注册的一个事件被触发时,都会对控制器对象调用 handleEvent() 。现在来看看这个方法是什么样子,如何处理事件使按钮具有交互性:
清单 8. SimpleRCCButton 类的事件处理程序
SimpleRCCButton.prototype.handleEvent = function (evt) {
var type = evt.type;
if (type == 'DOMAttrModified') {
this.fixButtonPosition();
this.fixButtonWidth();
} else if (type == 'mousedown') {
this.rect.style.setProperty('fill', 'pink');
window.captureMouse(evt, this.rect);
} else if (type == 'mouseup') {
this.rect.style.setProperty('fill', 'lightgray');
} else if (type == 'click') {
var event = document.createEvent('ButtonPush');
this.element.dispatchEvent(event);
}
};
该函数显然只接收事件作为唯一的参数。但最重要的是,需要指出接收事件的类型。如果是实例元素属性的一个突变事件,我需要同时调用两个方法相应的修改按钮的外观。每次同时调用两个方法有点笨拙。我本来可以花点时间在调用所有的刷新方法之前检查哪个属性被修改了,但是出于简化的原因,我还是选择现在这种方法,并相信您在编写自己的代码时会有更好的理解。接下来是处理鼠标事件。
因为我实现的是一个下压按钮,最好是能够在鼠标点到按钮图像上的时候,让用户有压下按钮的感受。因此,当影像树的矩形接收到mousedown 事件时,我把 CSS 的 fill 属性改成稍微亮一点的颜色——比如粉红色。然后我捕获了鼠标,使所有的事件从现在开始都由矩形捕获,这是一种传统的 UI 技巧,以便在用户激活按钮的过程中禁止出现其他的鼠标交互。因此当鼠标按钮被压下然后释放时( mouseup 事件),我必须保证把矩形的 fill 属性恢复原来的值。
现在我已经说明了如何解决鼠标与按钮交互的视觉效果。但是下压按钮的整个目标是在它被激活时触发一个动作。我决定让定制元素在激活时触发一个定制的动作,这样它就不仅仅从响应鼠标事件和 DOM 变化的意义上讲是一个 SVG 元素,还可以用它自己的语义触发事件。创建和分派定制的事件只需要两个 DOM 调用就能实现。第一个是通过对我的 DOM document 调用 createEvent() 方法创建事件,并赋给它一个自定义的类型—— ButtonPush 。剩下的就是通过在定制元素中调用 dispatchEvent 方法实际触发该事件,该方法定义在极其有用的 EventTarget DOM Events 接口中。
回页首
结果
现在,我们的下压按钮实现已经完成了,我可以创建一个非常简单的应用程序,其内部构造(而不是 RCC 定义和其他需要的 SVG 标记)如下所示:
清单 9. 扩展元素的使用
<ui:button x="10" y="10" width="300">
<ui:button>
<script ev:event="ButtonPush">
var button = evt.target;
var width = parseInt(button.getAttribute('width'));
button.setAttribute('width', width - 10);
</script>
</ui:button>
<ui:button x="10" y="40" width="150">
Another <ui:button>
<script ev:event="ButtonPush">
var button = evt.target;
var width = parseInt(button.getAttribute('width'));
button.setAttribute('width', width + 10);
</script>
</ui:button>
这里使用了定制元素 <ui:button> 的两个实例,每个都有自己单独的影像树、控制器对象、交互式处理、状态,等等。它们共有的唯一一点就是基于同一个原型——基于 RCC 的定义。这个例子非常简单:它有一个大按钮,每次点击时宽度逐渐缩小,还有一个小按钮每次点击时宽度逐渐增加。如您所见,通过使用与 RCC 定义中幕后监听 SVG 事件相同的 XML Events 机制,我可以监听按钮的定制事件。打开 参考资料中 demos.zip 压缩包中的文件,您将看到:
图 2. 两个独立的按钮
在同一个压缩包中,您还会看到一个更高级的基于 RCC 的示例应用程序。这个应用程序是为了说明更高级的 RCC 定制元素的应用。我还有一个更强大的可以换皮肤的下压按钮和一个可设置风格的组合框,它们利用了前面定义的下压按钮,并强调了 RCC 的可重入性。尽管出于简洁起见,本文没有没有详细地讨论定制元素如何实现更换皮肤或者 CSS 样式,现在您完全可以看看这些文件并领会实现的技巧。图 3 所示的是该应用程序的截屏图片,模仿了 Windows XP 部件的观感,当,这里是 100% 的 SVG 而没有任何过程性的 GDI 调用:
图 3. 可设置风格的组合框应用程序
本文详细探讨了 RCC 的内部工作机理,引导您创建了一个简单部件的实现,并提供了理解更复杂应用程序的基础知识。下一篇文章中,我将讨论与 SVG 有关的内容,探讨如何实现 XForms 和 SVG 的结合。在本系列的最后一部分中,我将介绍如何把已有的基于 RCC 的 UI 部件包装到一些更高级的 XForms 控件中,并用 XForms 重新改写可设置风格的组合框。下次再见!
参考资料
· 您可以参阅本文在 developerWorks 全球站点上的 英文原文.
· 从 这里下载本文的实例文档。
· 追根溯源——直接在 W3C 上阅读 XForms 1.0和 SVG 1.2规范。
· 关于 SVG 的背景知识,请参阅 developerWorks教程 “ 可伸缩向量图形介绍” (2002 年 2 月) 和 “ 交互式动态可伸缩向量图形”(2003 年 6 月)。
· 了解 XForms 的更多背景知识,请参阅 developerWorks教程“理解 XForms”(2004 年 1 月)。
· 利用 DOM Level 3 Events,创建一个定制类型的专用事件。
· 回顾另一个相关的 W3C 规范 XML Events,该规范与 DOM Events 息息相关。
· 下载最新的 Adobe SVG Viewer 6 预发布版,看看这些例子,并尝试 RCC 和 SVG 1.2 的其他优势。
· 在 developerWorks 中国网站 XML 专区能找到更多的 XML 资料。
· 了解如何才能成为 IBM 认证的 XML 及相关技术的开发人员。
关于作者
Antoine Quint 是一名独立的 SVG 顾问和研究科学家,他以特约专家的身份参与到 W3C SVG Working Group 中。他还喜欢教学工作,喜欢在风景优美的地方出席会议,以及做一些外购工作。所有这些都是以 SVG 为导向的。在巴黎的家中,他与一只名叫 Stig Elmer 的猫生活在一起。Antoine 还写过一本非常延迟的书,那是他与老友 Robin Berjon 一起工作的成果。
展开阅读全文