XUL: 快速开发跨平台富客户端的新途径
?
XUL示例:
下面这个例子展示了如何开发一个简单的 XUL 用户接口:
界面效果图:

?
对应的源码如下:

从上面的代码可以发现 XUL 的编写非常简单,声明 DTD 实体,定义脚本函数,然后就是定义主窗口元素。
<!DOCTYPE window [
<!ENTITY Sample "Add DTD Entity Declearation">
]>
是对XML DTD实体的声明。
?
<window id="findfile-window" title="Find Files" orient="horizontal"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
</window>定义了主窗口<window>元素,所有其他界面元素的定义都包含在这里面。
?
<script> <![CDATA[
function sample(){ //Add your functions
}
]]> </script>
定义了JavaScript脚本函数,负责对窗口事件的响应并执行相应操作。
?
增加其他元素也非常简单,在主窗口上顺序添加即可。
?
3. XUL 关键功能和机制
3.1. XUL 应用程序 Package
XUL 应用程序架构和 Eclipse 的插件(Plugin)结构很相似,可以动态加载组件。XUL 中每个功能组件相对独立,以 Package 的形式存在(这里的 package 一般是 JAR 包以便下载安装,也可以是普通的文件目录)。在 Mozilla 或 Firefox 中,主要的功能组件都存放在"Mozilla&Firefox 安装目录/chrome"下。这个"chorme"目录下有一个"chrome.rdf"和"installed-chrome.txt" 文件,"chrome.rdf"文件记录了所有安装组件(Package)的信息(包括存放路径),"installed-chrome.txt"记录了所有已安装的组件列表和它们的访问类型。因为 XUL 安装程序会将路径信息注册到该"chrome.rdf"文件中,所以当增加新的组件 Package 时,你可以把它放在任何目录下,甚至包括可以加载(mount)到本地文件系统的远程站点。
一个典型的XUL组件Package的目录结构如下:

Content 目录
包含一些以".xul"为后缀的文件,它们定义了 XUL 界面窗口元素。另外还有定义了处理界面事件响应以及负责和其它系统交互的 JavaScript 脚本文件,都是以".js"为文件后缀。需要注意的是 Content 目录下可以有多个 XUL 文件,但定义主窗口的 XUL 文件名必须和组件Package 名相同。
Skin 目录
负责界面外观的显示细节,比如色彩,图片等。主要包含了 CSS 文件和图片。这样就可以独立地修改界面外观而不影响其功能。
Locales 目录
存放和全球化相关的翻译信息。每个 locale 会有一个独立的文件目录,例如"en_US"(美国), "fr_FR"(法国)等。这些子目录下则存放了和各自 locale 相关的 DTD 和 property 文件。DTD 文件包含了 XML 实体声明,这些实体的值用其对应国家的语言表示,被 Content 目录下的 XUL 文件引用并显示在界面上。例如多国语言的快捷键和菜单的内容等。Property 文件和 DTD 类似,不同的是它被脚本所使用。当新增加一个语言时,只需添加新的 locale 子目录即可,无需任何额外的修改。
另外需要注意的是这三个目录下都有一个"contents.rdf"文件,这是一个必须的配置文件,它定义了每个子目录例如 Content 子目录所属的应用程序 Package,相对路径以及作者和版本号等信息。Mozilla&Firefox 就是用这些 contents.rdf 文件中的信息来安装、注册并运行 XUL 应用程序。
3.2. Chrome URL
Chrome URL 是 XUL 或者说 Mozilla&Firefox 的访问和注册机制,所有在本地 Mozilla&Firefox 注册安装的应用程序,都是用这种方式访问的。以 Chrome URL 方式运行的应用程序能获得广泛的访问权限。另一个优点是用 Chrome URL 访问时,Mozilla 会自动去上文提到的"chrome.rdf", "cintents.rdf"文件中寻找相关信息并将数据正确地返回。Chrome URL 本身是和物理路径无关的,因此可以把新应用程序安装在任何目录下。下面是一个具体的 Chrome URL 例子:
Chrome URL 语法是:chrome://<package name>/<part>/<file name>
这里的<package name>是应用程序package名称,<part>是指要访问的子目录,例如上文提到过的"Content","Skin", "Locale",<file name>是对应子目录下的文件名。可以看到 Chrome URL 不包含任何物理路径相关的信息,因为在安装新的应用程序时,所有信息(包括安装路径)都注册到"chrome.rdf"和"installed- chrome.txt"文件中了。
以下是访问 Mozilla 自带组件的 Chrome URL 例子:
chrome://messenger/content/messenger.xul
chrome://navigator/skin/navigator.css?
chrome://messenger/locale/messenger.dtd
另一种格式是例如:
chrome://navigator/content/?
chrome://navigator/Skin/?
chrome://navigator/Locale
当应用程序 Package 下的每个"Content","Skin"和"Locale"子目录中都有一个和 Package名称相同的文件时(例如"navigator.xul"," navigator.css"," navigator.dtd"),就可以用这种方式访问,这样用户就只需知道应用程序 Package 的名字即可。
3.3. XPCOM (Cross-platform Component Object Model)
到目前为止,我们已经知道 XUL 能够构建各种复杂的用户接口,并用 JavaScript 负责窗口的事件响应。但如果我们想实现更复杂的功能,比如要求 XUL 应用程序和独立运行的 Mail Server 交互,负责发送和收取信件,JavaScript 便显得捉襟见肘了,XPCOM 便是扩展 XUL 功能的途径。
XPCOM 作为一个对象模型,也具备了基本的面向对象理念。其中最重要的基本单位便是接口(Interface)和组件(Component)概念。每个接口(Interface)声明了它所具备的功能和属性,组件(Component)可以继承多个接口,并实现这些接口中的所有功能。这样做的目的是在公用接口中清楚地声明一套 API,由组件(Component)根据需要加以组合和实现,组件实现的改变不会对接口有任何地影响。
XUL本身已经实现了很多组件(Component),XUL 组件列表上有详细的说明,我们可以在Mozilla&Firefox 平台上直接使用这些功能。下面是一个使用 XPCOM 的例子:
例如需要对本地文件进行操作,
首先获得已经实现了文件操作功能的组件:var localFile =
Components.classes["@mozilla.org/file/local;1"].createInstance(Components.interfaces.nsILocalFile);
请注意,这里的 Components本身也是一个组件,它负责其它组件的生成,查询和管理。"@mozilla.org/file/local;1" 其实是 Mozilla 根据组件各自功能进行分类的规则,从某种意义上也可以称为命名空间。Components.interfaces.nsILocalFile 是我们所需要的具备文件操作功能的接口,最后我们通过 createInstangce() 函数得到了组件引用 localFile。
接下来可以用这个组件实现我们所需要的功能:function deletFile(filePath){localFile =
ponents.classes["@mozilla.org/file/local;1"].createInstance(Components.interfaces.nsILocalFile);
?
//定位文件或目录路径,filePath例如"/mozilla/ToRemove"
localFile. initWithPath(filePath);
?
//删除该目录所有 (true) 内容
localFile.remove(true);
此外我们还可以开发并添加新的功能组件(Component),由于篇幅限制,先不作介绍,读者可以参见?XPCOM。
3.4. XPI (Cross-platform Installation)
XPI (跨平台安装程序) 是一种非常灵活方便的下载安装机制,只需将开发好的应用程序 Package和 XPI 特定安装脚本(install.js)这两个文件压缩到一个以".xpi"为后缀的文档中,这样一个简单的 XPI 安装程序就完成了(注意:最好使用 Winzip 进行压缩,其他压缩工具如 Winrar可能导致下载时无法读取 XPI 包)。
下面是具体的步骤(假设要安装的应用程序名称是"top"):
完成 install.js 安装脚本:
//初始化,三个参数分别是应用程序名称,应用程序索引,版本号
initInstall("top", "top", "0.1");//找到 Mozilla 安装目录下的"chrome"目录。
var folder = getFolder("chrome");//设置应用程序安装目录即 Mozilla 安装目录下的 Chrome
setPackageFolder(folder);
//指定将安装的应用程序,如果应用程序是目录结构则调用 addDirectory() 方法,
//如果已打成 JAR 包的则调用 addFile() 方法,
addDirectory("top");//注册应用程序 top 的 Chrome URL,第一个参数是 Chrome 注册类别(Content, Skin ,Locale)
//第二个参数指明 contents.rdf 文件所在的目录,contents.rdf 文件包含了 Chrome 注册信息
//第三个参数是安装完毕后 contents.rdf 文件所在目录
registerChrome(Install.CONTENT|Install.DELAYED_CHROME, getFolder(folder, "content"));
//如果还有 Skin 和 Locale 目录,还要注册 Chrome Skin 和 Locale
// registerChrome(Install.SKIN | Install.DELAYED_CHROME, getFolder(folder, "skin"));
// registerChrome(Install.LOCALE | Install.DELAYED_CHROME, getFolder(folder, "locale"));
//开始安装注册应用程序,如果发现以上步骤有错误还可以取消并退出安装程序。//getLastError() 方法返回最近出现的错误信息。
if (0 == getLastError()){performInstall();
}else{cancelInstall();
更新提供下载服务的 Web 页面首先在对应的 Web 页面上增加一个连接:
<a href="Install new XUL application!" onclick="install()"> Install new XUL application!</a>
然后为该页面增加两个 Javascript 方法,内容如下:
function install( ){ //设置参数-应用程序名称(top)和安装程序名称(top.xpi)
var xpi={'top':'top.xpi'};//启动安装程序,并将作为参数传入,callback 是安装结束回调的方法名
InstallTrigger.install(xpi, callBack);
}
function callBack (name, result)
{//安装完成后 XPI 回调该方法。if (result == 0)
alert("Installation complete, please restart your browser.");else
alert("Installation failed, Error - "+result);}
?
XPI 的在线安装注册可以概括为:
- 编写 install.js 安装脚本,将应用程序和 install.js 压缩成以 .xpi 为后缀的包。
- 更新提供下载服务的 Web 页面,包括增加新的下载链接和脚本函数(如上文所述)。
- 用户在线点击下载,XPI 安装程序启动,解压 .xpi 安装包,执行 install.js
- 新应用程序安装完毕,所有信息注册到本地 Mozilla&Firefox Chrome 中。
- 用户重新启动 Mozilla&Firefox, 在本地运行新安装的程序。
?
4. XUL 用户接口的全球化
作为用户接口的设计语言,支持全球化是一个非常重要的性能。而 XUL 本身的特点也很好的支持了全球化。XUL 中最方便的支持全球化的途径便是利用 XML 实体引用机制—TD Entities)。
我们都知道 XML 的实体机制的最大特点便是,"一处修改,多处呈现"。也就是说只需要在定义实体的地方修改其内容而不影响使用实体处的代码。这就很好的支持了全球化,只要我们将接口上显示的部分用 XML 实体抽取,统一地翻译成多种语言。不同版本的应用程序引用对应国家的实体,这就实现了接口的多语言化。
下面是一个典型的支持多语言的 XUL 应用:
英语版本的实体声明:
<?xml version="1.0" encoding="UTF-8" ?>
<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
<?xml-stylesheet href="./CalculateVolumetricWeight.css" type="text/css"?>
<!DOCTYPE window [
<!ENTITY dp1 "Sometimes, large items with a light overall weight can be
charged according to the space they take up on aircraft.">
<!ENTITY dp2 "In these cases, Volumetric Weight,or dimensional (Dim) weight,
is used to calculate the shipment cost.">
<!ENTITY dp3 "It is recommended that you calculate the Volumetric Weight for
every shipment that you send, and then compare">
<!ENTITY dp4 "this to its actual weight. The greater weight of the two is
used to work out the price that we charge you.">
<!ENTITY dp5 "International Volumetric Weights are calculated using the formula below:">
<!ENTITY dp6cm "Length * Width * Height in centimeters / 6000 = Volumetric Weight">
<!ENTITY dp6inch "Length * Width * Height in inches / 166 = Volumetric Weight">
<!ENTITY dp7 "Alternatively, please feel free to use our quick calculator below.">
<!ENTITY title "Volumetric Weight Calculator">
<!ENTITY UnitOfMeasure "Unit of Measure">
<!ENTITY Cmskgs "Cms/kgs">
<!ENTITY InchesPounds "Inches/Pounds">
<!ENTITY Length "Length">
<!ENTITY Width "Width">
<!ENTITY Height "Height">
<!ENTITY VolumetricWeight "Volumetric Weight">
<!ENTITY Calculate "Calculate">
<!ENTITY Reset "Reset">
<!ENTITY errorLength "Invalid length format. Only positive integers are allowed.
Please validate your input and try again.">
<!ENTITY errorWidth "Invalid width format. Only positive integers are allowed.
Please validate your input and try again.">
<!ENTITY errorHeight "Invalid height format. Only positive integers are allowed.
Please validate your input and try again.">
<!ENTITY cm "cm">
<!ENTITY kg "kg">
<!ENTITY Inch "Inch">
<!ENTITY Pound "Pound">
]>中文版本的实体声明:
<?xml version="1.0" encoding="GBK" ?>
<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
<?xml-stylesheet href="./CalculateVolumetricWeight.css" type="text/css"?>
<!DOCTYPE window [
<!ENTITY dp1 "总重量较轻的大件物品可以根据其在飞机上占据的空间付费,在这个情况下,
体积重量将作为货物运费的参考依据。">
<!ENTITY dp2 "建议您计算每件发送货物的体积重量,并和实际数据比较。较大的体积重量
将作为您付费的依据。">
<!ENTITY dp5 "国际标体积准重量计算公式如下:">
<!ENTITY dp6cm "长度 * 宽度 * 高度 (单位厘米) / 6000 = 重量">
<!ENTITY dp6inch "长度 * 宽度 * 高度 (单位英寸)/ 166 = 重量">
<!ENTITY dp7 "或者您可以使用下面的计算器实现快速计算。">
<!ENTITY title "体积重量计算器">
<!ENTITY UnitOfMeasure "度量单位">
<!ENTITY Cmskgs "厘米/千克">
<!ENTITY InchesPounds "英寸/磅">
<!ENTITY Length "长">
<!ENTITY Width "宽">
<!ENTITY Height "高">
<!ENTITY VolumetricWeight "体积重量">
<!ENTITY Calculate "计算">
<!ENTITY Reset "重置">
<!ENTITY errorLength "长度错误! 只允许正数, 请检查并重新输入!">
<!ENTITY errorWidth "宽度错误! 只允许正数, 请检查并重新输入!">
<!ENTITY errorHeight "高度错误! 只允许正数, 请检查并重新输入!">
<!ENTITY cm "厘米">
<!ENTITY kg "千克">
<!ENTITY Inch "英寸">
<!ENTITY Pound "磅">
]>两中语言版本中实体引用代码是相同的,例如:
<row flex="1" id="row2">
<label value="&Length;" />
<textbox id="length" maxlength="6" />
<label id="unit1" value="&cm;" />
</row>
<row flex="1" id="row3">
<label value="&Width;" />
<textbox id="width" maxlength="6" />
<label id="unit2" value="&cm;" />
</row>
?
5. 用 Web Service 实现 XUL 应用程序和服务器的集成
XUL 的主要任务是构建用户接口,负责界面的显示和事件响应。为了增加其扩展性,XUL 提供了一系列和其他系统的集成方法(包括 3.4 介绍的在线安装),这里介绍一种比较典型而且比较流行的途径 - Web Service。
XUL Web Service 调用的核心是 SOAP(Simple Object Access Protocol)。SOAP 是一种基于 XML的 Web Service 通讯协议。XUL 本身实现了 Web Service 相关的一系列组件(Components),具体 API 请参考?XUL Web Services。Mozilla1.0 以后的版本都包含了基于 SOAP 的 Web Service 操作组件,可以在 JavaScript 中直接使用(但必须得到安全访问权限)。
请看下面的简单样例:
将一个简单的学生学号查询方法发布成 Web Service(RPC/Encode方式),函数如下:
margin-top: 20px; margin-right: 0px; margin-bottom: 10px; margin-left: 0px; font-size: 8pt; wid