前言:本文是在下的在学习ZK官方文档时整理出来的初稿。本来里面有很多的效果图片和图片代码的。奈何博客园中图片不能粘贴上去,所以感兴趣的筒子们就将就吧。内容中,如有不好的地方,欢迎斧正!
ZK框架的分析与应用
1 ZK框架的引入
1.1 概述
ZK是一套以 AJAX/XUL/Java 为基础的网页应用程序开发框架,用于丰富网页应用程序的使用界面。最大的好处是,在设计AJAX网络应用程序时,轻松简便的操作就像设计桌面程序一样。 ZK包含了一个以AJAX为基础、事件驱动(event-driven)、高互动性的引擎,同时还提供了丰富多样、可重复使用的XUL与HTML组件,以及以 XML 为基础的使用界面设计语言 ZK User-interfaces Markup Language (ZUML)。
通过ZK这个框架,软件开发者基本可以脱离美工,由开发人员自己来布局页面,这样能够大大提高软件开发的效率。
1.2 构建一个简单的ZK应用
2 如何组建UI
组建UI即创建我们展示给Client端的View层页面。
2.1 使用基于xml的方式
2.2 使用纯Java代码的方式
2.3 xml与java代码混用的方式
2.4 zk中组建UI时需要了解的xml基础
通常情况下,我们都是使用XML的方式来组建UI,XML文件有严格的语法。如:必需要有结束标签;标签之间的嵌套要合理;等等。
XML文件中的<?……?>表示的是处理命令。它位于xml文件的第一行,它和根元素是一个级别的。
在XML文件中是不能使用特殊字符的,如果我们一定要用那么就要将它替换掉:
特殊字符 |
替换成 |
< |
< |
> |
> |
& |
& |
“ |
" |
‘ |
' |
\t(Tab) |
当属性值中要使用时才需要替换 |
\n |
当属性值中要使用时才需要替换 |
3 ZK页面中的基本概念:桌面、页面、组件
我们通过浏览器去访问ZK应用的时候,浏览器展示给我们的是ZK的一个Desktop,而在一个Desktop中可以包含多个Page。组成一个Page的基本元素就是ZK Component(ZK组件)。
3.1 Desktop、Page和Component之间的关系
桌面(Desktop) |
它是zul页面的集合,即一个Desktop可以包含多个zul页面。 |
页面(Page) |
zul页面是组件的集合。 |
组件(component) |
ZK组件是组成zul页面的最小单位。 组件是一种用户接口(UI),比如一个标签、一个按钮。所有的ZK组件都实现了org.zkoss.zk.ui.Component接口 |
下面我们通过一幅图来看它们之间的关系:
注:使用Include组件包含一个页面时,如果我们设置它的mode(模式)为defer(延迟),那么,将有两个页面在这个Desktop上被创建
3.2 将Component附加到Page上
一个组件只有附加到一个page时才能在client端得到显现。要使一个组件属于一个page的话有两种方式:
1、 将组件添加到一个已经附加到page的组件上。相应的api:Component.appendChild(org.zkoss.zk.ui.Component)、Component.insertBefore(org.zkoss.zk.ui.Component, org.zkoss.zk.ui.Component)、Component.setParent(org.zkoss.zk.ui.Component)
2、 使用Component.setPage(org.zkoss.zk.ui.Page)直接将组件添加到一个page上,这个组件就成为了该页面的根组件。
3.3 销毁Page上的Component
ZK的组件之间是树状结构的,每一组件都只有一个根。
从页面上销毁一个组件可以通过下面两种方式来实现:
1、 组件不是根组件时:Component.setParent(null)
组件是根组件时:Component.setPage(null)
2、 Component.detach()
很显然第二种方式要简便。
销毁一个组件下面的所有组件可以通过如下代码来实现:
Component.getChildren().clear();
它等同于
for (Iterator it = comp.getChildren().iterator(); it.hasNext();) {
it.next();
it.remove();
}
3.4 使用ZK Studio来编辑zul页面
建议使用ZK Studio来编辑zuml页面,它里面有很好的content assist和visual editor功能。我们只需要在eclipse上安装一个ZK Studio插件就可以了。
你也可以直接使用XML Editor来编辑zuml页面,不过最好是在window组件中加上schema,再在eclipse中配置好对应的xsd文件,一样可以实现content assist的功能。
<window xmlns="http://www.zkoss.org/2005/zul"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.zkoss.org/2005/zul
http://www.zkoss.org/2005/zul/zul.xsd">
4 ZK常用基本组件
4.1 输入框组件汇总
在ZK中,输入框被细分成了很多种:textbox、intbox、decimalbox、doublebox、datebox、timebox、spinner、combobox、bandbox
组件的常用属性有(属性是可选的):constraint、disabled、maxlength、readonly
组件名 |
用途 |
|
textbox |
用于输入文本。 例1:<textbox value="Jerry" width="150px" /> 效果图: 例2:<textbox type="password" value="foo" width="150px" /> 效果图: |
|
intbox |
用于输入整数,其他的按键都会被屏蔽。比如输入字母时不会有任何反应 例:<intbox constraint="no negative,no zero" width="150px" value="12345678" /> 效果图: |
|
decimalbox |
用于输入小数,其他的按键都会被屏蔽。 例:<decimalbox format="###.##" value="154.326" width="150px" /> 效果图: |
|
doublebox |
用于输入双精度数,其他的按键都会被屏蔽 |
|
datebox |
用于输入日期,它自带一个日历控件,且可以对日期进行格式化。 例:<datebox id="db" width="150px" format="yyyy/MM/dd" /> 效果图: 未点击时: 点击后: |
|
timebox |
用于输入日期,注意:它的value是Date类型的。 例:
效果图: |
|
spinner |
用于输入整数,其他的按键都会被屏蔽,组件上的箭头用于增加或减小输入框中的值。它的value只能是int类型的整数。step属性用于控制增长的歩长 例:<spinner value="123345" step="2"/> 效果图: |
|
combobox |
下拉框。默认情况下是可以输入值的,设置了disabled=”true”之后,就只能通过鼠标选择而不能输入值。 例:
效果图: 未点击时: 点击后: |
|
bandbox |
一般用作搜索框。可以使用buttonVisible="false"将组件上的按钮隐藏 例:<bandbox value="123" /> 效果图: |
4.1.1 对输入框输入内容的约束和验证
输入框通常都有一个constraint属性,它是用来约束用户输入的,使用constraint属性可以控制输入控件接受什么值。
当失去光标的焦点时,如果不符合约束条件,就会弹出出错信息。
在ZK中提供了两种约束用户输入的方式:第一是使用ZK自定义的约束条件;第二是使用正则表达式。
constraint属性可以使用的值有:no positive、no negative、 no zero、no empty、no future、no past、no today 和一个正则表达式的集合。
no positive、no negative、 no zero适用于intbox。
no future、no past和no today 约束仅适用于datebox。
no empty适用于任何组件。
正则表达式约束仅适用于字符串类型组件,例如textbox、combobox和bandbox。
① ZK自定义的约束条件
constraint中可以使用的约束条件表
约束条件 |
解析 |
no empty |
输入不能为空 |
no future |
不能是以后的时间。用于datebox |
no negtive |
不能输入负数。用于intbox |
no past |
不能是过去的时间。用于datebox |
no positive |
不能是正数。用于intbox |
no today |
不能是今天。用于datebox |
no zero |
不能是0。用于intbox |
between yyyyMMdd and yyyyMMdd |
控制时间在一个范围内。 例:<datebox constraint="between 20071225 and 20071203"/> |
after yyyyMMdd |
例:<datebox constraint="after 20071225"/> |
before yyyyMMdd |
|
end_before end_after after_start after_end |
指定弹出的错误提示框的位置。 例:<textbox constraint="no empty, start_before"/> 效果图: |
② 使用正则表达式
例:<textbox constraint="/.+@.+\.[a-z]+/:邮箱地址不正确" />
效果图:
也可以不指定出错信息,这时会使用默认的出错信息。
例:<textbox constraint="/.+@.+\.[a-z]+/" />
效果图:
如果是在java代码中,我们可以这样指定控件的constraint:
new Textbox().setContraint("/.+@.+\\.[a-z]+/");
4.2 容器组件
容器组件可以用来包含其他组件。
4.2.1 Window组件
Window组件是我们在ZK中使用最多的一个组件。
一个Window组件,就像HTML中的div标签一样,用来将组件组合在一起。
和其他组件不同,Window组件具有如下特点:
◆ Window组件是ID空间宿主。Window组件下的任何组件(包括它自身)都可以通过这个Window组件的id来进行检索。使用:id.getFellow(java.lang.String)
◆ Window组件可以设置成overlapped(重叠)、popup(弹出)、embeded(嵌入)
◆ Window可以作为一个模态窗口(modal dialog)
① Window组件的模式(mode)
zul页面中,使用mode属性来设置Window的模式。
Window组件有5种不同的模式:overlapped、popup、modal、hightlighted、embeded
默认的是embeded模式。
我们可以通过Window.setMode(java.lang.String)来改变Window的模式。
我们也可以通过调用Window.doOverlapped()、Window.doPopup()、Window.doModal()、Window.doHightlighted()、Window.doEmbeded()来改变Window的模式。
例:
<zk> <window id="win" title="Hi!" border="normal" width="200px"> <caption> <toolbarbutton label="Help" /> </caption> <checkbox label="Hello, Wolrd!" /> <button label="CLOSE" onClick='win.setVisible(false)'/> </window> <button label="Overlap" onClick="win.doOverlapped();" /> <button label="Popup" onClick="win.doPopup();" /> <button label="Modal" onClick="win.doModal();" /> <button label="Embed" onClick="win.doEmbedded();" /> <button label="Highlighted" onClick="win.doHighlighted();" /> <space bar="true"/> <button label="SHOW" onClick='win.setVisible(true)'/> </zk> |
效果图:
embeded:这是默认的模式。这种模式下,我们不能改变Window的位置。
overlapped:overlapped模式下的Window与其他的组件是层叠的。用户可以使用鼠标拖拽它。开发人员可以通过Window.setLeft(java.lang.String)和Window.setTop(java.lang.String)来设置它的位置。
popup:popup模式和overlapped模式相似。它们之间不同是:在popup模式下,我们只要点击其他组件的话,这个弹出的Window就会自动消失。
modal:modal与hightlighted模式基本上是相同的。modal模式下,Window之外的组件是不能够操作的(如下图)。
hightlighted:
4.1.2 Window组件的常用属性
window属性表
属性名 |
说明 |
||
border |
取值有:normal、none(默认为none) 例:
|
||
closable |
一旦我们关闭了这个Window,那么这个页面上将不会有这个Window存在,也不能对它进行引用,否则会报错,这是也隐藏最大的不同。我们也可以通过Window.detach()来实现相同的效果。 Window关闭的时候会触发onClose事件。 我们可以重写onClose方法来实现我们自己想要的操作。比如,我们想在点击关闭按钮的时候将Window隐藏,而不是detach 例:
效果图: |
||
contentStyle |
通过这个属性,我们可以为Window定制样式。 例1:
效果图: 例2:
效果图: |
||
position |
我们可以为mode为overlapped、pupup和modal的Window设置位置。 如:<window width="300px" mode="overlapped" position="right,bottom"> 可供选择的position有:center、left、right、top、bottom |
||
sizable |
用于设置Window是否可以改变大小。 当用户改变Window的大小时会触发onSize事件 |
||
title AND caption |
用来设置标题。(注:caption是一个组件,而非属性) 例:
效果图: |
4.2 Grid组件
Grid组件是处理与展现大量数据的组件。与它功能类似的组件有:Grid、Listbox、Tree。
4.2.1 Grid、Listbox、Tree的比较
◆ Grid和Listbox都是用来展现列表数据的。而Tree主要用来展示分层数据。
◆ Listbox和Tree允许用户选中它所展现的一条或多条数据(可以通过ZK提供的方法来获取选中的数据),而Grid则不行。
◆ Grid、Listbox和Tree都支持分页,具体请查阅Paging组件。
4.2.2 Grid概观
相关子组件:grid、columns、colum、rows、row
组件名 |
说明 |
grid |
使用它来定义一个grid |
columns |
用来设置grid的标题头。 |
column |
位于columns里面,用来设置列的相关属性 |
rows |
用来展现数据 |
row |
位于rows里面,用来展现一行数据 |
例1:
<zk> <grid width="500px"> <columns> <column label="Column 1"/> <column label="Column 2"/> <column label="Column 3"/> </columns> <rows> <row> <label value="Item A.1" /> <label value="Item A.2" /> <label value="Item A.3" /> </row> <row> <label value="Item B.1" /> <label value="Item B.2" /> <label value="Item B.3" /> </row> <row> <label value="Item C.1" /> <label value="Item C.2" /> <label value="Item C.3" /> </row> </rows> </grid> </zk> |
效果图:
4.2.3 Grid展现大批量数据
在grid中,我们有两种方式来展现大批量数据:一是使用滚动条;二是使用分页。
① 使用滚动条
只需要为grid的hight属性设置一个值,当数据展现超过这个hight时,滚动条会自动出现。
例:
我们在上面的例子中的grid标签上加个属性height=”100px”,即<grid width="500px" height="100px">。得到的效果图如下:
② 使用分页
在grid中添加属性:mold=”paging” pageSize=”n”即可。当展现的数据量超过n条时,分页控件会自动出现。
(注意:这里的分页只是页面上的分页,而不是后台的分页。有人叫这种分页为假分页)
例:
我们将上面代码中的height去掉,加上属性:mold="paging" pageSize="3"
效果图:
pageSize的默认值为:20
除此指定mold=”paging”之外,我们还可以使用Paging组件来实现分页。详情请参照后续Paging分页的介绍。
4.2.4 使用Paging组件来为Grid分页
使用Paging组件,我们可以自定义的放置paging组件的位置。还可以使用一个Paging组件来控制多个grid,这时需要指定Grid组件的paginal属性为同一个Paging。
例:
<vbox> <paging id="pg" pageSize="3" /> <hbox> <grid width="300px" mold="paging" paginal="${pg}"> <columns> <column label="Left" /> <column label="Right" /> </columns> <rows> <row> <label value="Item 1.1" /> <label value="Item 1.2" /> </row> <row> <label value="Item 2.1" /> <label value="Item 2.2" /> </row> <row> <label value="Item 3.1" /> <label value="Item 3.2" /> </row> <row> <label value="Item 4.1" /> <label value="Item 4.2" /> </row> </rows> </grid> <grid width="300px" mold="paging" paginal="${pg}"> <columns> <column label="Left" /> <column label="Right" /> </columns> <rows> <row> <label value="Item A.1" /> <label value="Item A.2" /> </row> <row> <label value="Item B.1" /> <label value="Item B.2" /> </row> <row> <label value="Item C.1" /> <label value="Item C.2" /> </row> <row> <label value="Item D.1" /> <label value="Item D.2" /> </row> <row> <label value="Item E.1" /> <label value="Item E.2" /> </row> </rows> </grid> </hbox> </vbox> |
效果图:
4.2.5 Paging组件的onPaging事件
当用户点击paging组件的时候,会触发onPaging事件。如果想实现后台自动分页,我们就可以通过这个事件来实现。
例:
<window id="win" title="分页" border="normal" width="400px"> <grid mold="paging" onCreate="init()"> <columns> <column label="编号" /> <column label="名字" /> </columns> <rows id="rowsId"> </rows> </grid> <paging id="myPaging" pageSize="3" onPaging="nextPage(event)"/> <zscript> <![CDATA[ import org.zkoss.zul.event.PagingEvent; class Student{ private int id; private String name; public Student(){} public Student(int id, String name){ this.id = id; this.name = name; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } } List stuList = new ArrayList(); Student stu1 = new Student(1, "张三"); Student stu2 = new Student(2, "李四"); Student stu3 = new Student(3, "王五"); Student stu4 = new Student(4, "朱红"); stuList.add(stu1); stuList.add(stu2); stuList.add(stu3); stuList.add(stu4); final int PAGE_SIZE = myPaging.getPageSize(); public void nextPage(Event event){ PagingEvent pe = (PagingEvent) event; int pgno = pe.getActivePage();//页数(从零计算) int start = pgno * PAGE_SIZE; int end = start + PAGE_SIZE; end = end>stuList.size()?stuList.size():end; System.out.println(start+","+end); rowsId.getChildren().clear();//清空数据 createData2View(start, end); } private void createData2View(int start, int end){//显示数据 for(int i=start; i<end; i++){ Student stu = (Student)stuList.get(i); Row row = new Row(); Label lab1 = new Label(); lab1.setValue(stu.getId()+""); Label lab2 = new Label(); lab2.setValue(stu.getName()); lab1.setParent(row); lab2.setParent(row); rowsId.appendChild(row); } } public void init(){ createData2View(0, 3); myPaging.setTotalSize(stuList.size());//总数据量 } ]]> </zscript> </window> |
效果图:
除了可以在onPaging事件上写函数之外,我们还可以使用下面的代码来为Paging组件添加事件监听器:
pagingId.addEventListener("onPaging", new EventListener() {
public void onEvent(Event event) {
……
……
……
}
};
4.2.6 Grid的排序功能
通过在column上设置属性sort,即可实现排序功能。详情请参见Listbox的排序。
4.2.7 使用grid的Model来动态展现数据
例:
<window title="Live Grid" border="normal" width="150px"> <zscript><![CDATA[ String[] data = new String[10]; for (int j = 0; j < data.length; ++j) { data[j] = "option " + j; } ListModel strset = new SimpleListModel(data); ]]></zscript> <grid height="100px" model="${strset}"> <columns> <column label="options" /> </columns> </grid> </window> |
效果图:
4.2.8 Grid的辅助标题(Auxiliary Headers)
除了使用columns来添加表头以外,我们还可以使用辅助表头auxhead来添加表头。
auxhead支持rowspan和colspan属性,这是column所不支持的。
columns和column只能用在grid组件里面,而auxheader、auxhead可以用在Listbox、Grid和Tree组件里面。
例:
<zk> <grid> <auxhead> <auxheader label="H1'07" colspan="6" /> <auxheader label="H2'07" colspan="6" /> </auxhead> <auxhead> <auxheader label="Q1" colspan="3" /> <auxheader label="Q2" colspan="3" /> <auxheader label="Q3" colspan="3" /> <auxheader label="Q4" colspan="3" /> </auxhead> <columns> <column label="Jan" /> <column label="Feb" /> <column label="Mar" /> <column label="Apr" /> <column label="May" /> <column label="Jun" /> <column label="Jul" /> <column label="Aug" /> <column label="Sep" /> <column label="Oct" /> <column label="Nov" /> <column label="Dec" /> </columns> <rows> <row> <label value="1,000" /> <label value="1,100" /> <label value="1,200" /> <label value="1,300" /> <label value="1,400" /> <label value="1,500" /> <label value="1,600" /> <label value="1,700" /> <label value="1,800" /> <label value="1,900" /> <label value="2,000" /> <label value="2,100" /> </row> </rows> </grid> </zk> |
效果图:
4.2.9 使用span属性来合并单元格
例:
<zk> <grid width="500px"> <columns> <column label="Left" align="left" width="150px"/> <column label="Center" align="center" width="150px"/> <column label="Right" align="right" /> <column label="Column 4" /> </columns> <rows> <row> <label value="Item A.1" /> <label value="Item A.2" /> <label value="Item A.3" /> <label value="Item A.4" /> </row> <row spans="1,2,1"> <label value="Item B.1" /> <label value="Item B.2" /> <label value="Item B.4" /> </row> <row spans="3"> <label value="Item C.1" /> <label value="Item C.4" /> </row> <row spans=",,2"> <label value="Item D.1" /> <label value="Item D.2" /> <label value="Item D.4" /> </row> <row> <div> <label value="Item A.1" /> <button label="BUTTON" /> </div>[微软用户1] <span> <label value="Item A.2" /> <button label="BUTTON" /> </span> <label value="Item A.3" /> <label value="Item A.4" /> </row> </rows> </grid> </zk> |
效果图:
4.2.10 Grid组件的常用属性
属性名 |
说明 |
sizedByContent |
根据内容改变大小。取值:true、false 一般与span=”true”连用。例如: <grid sizedByContent="true" span="true" width="800px"> |
span |
当设置了sizeByContent后,每一列都只会占用它需要的宽度。如果我们想每一列都看起来宽松一点的话,就可以设置span=”true” |
emptyMessage |
当展现的内容为空时,就会显示我们设置的emptyMessage中的内容。 例如:<grid id="test1" emptyMessage="没有相关数据"> |
sizable |
columns组件的属性,设置sizable=”true”后,用户就可以使用鼠标来更改列宽。例如:<columns sizable="true"> |
4.2.11 Grid表头上的弹出菜单:Menuepopup
默认情况下menuepopup被设置成none,我们可以在columns组件上设置menuepopup=”auto”来显示一个默认的弹出菜单。你也可以为组件设置自己定义的弹出菜单。
① 默认的Menuepopup:
例:
<zk> <grid width="300px"> <columns menupopup="auto"> <column label="第一列" /> <column label="第二列" /> </columns> <rows> <row> <label value="Item 1.1" /> <label value="Item 1.2" /> </row> <row> <label value="Item 2.1" /> <label value="Item 2.2" /> </row> <row> <label value="Item 3.1" /> <label value="Item 3.2" /> </row> </rows> </grid> </zk> |
效果图:
鼠标移上点击→
(勾选的列即为显示出来的列)
② 自定义的Menuepopup
例:
<zk> <menupopup id="editPopup"> <menuitem label="Group" image="/imgs/sml1.gif"/> <menuitem label="Sort Ascending" image="/imgs/sml2.gif"/> <menuitem label="Sort Descending" image="/imgs/sml3.gif"/> </menupopup> <grid width="300px"> <columns menupopup="editPopup"> <column label="第一列" /> <column label="第二列" /> </columns> <rows> <row> <label value="Item 1.1" /> <label value="Item 1.2" /> </row> <row> <label value="Item 2.1" /> <label value="Item 2.2" /> </row> <row> <label value="Item 3.1" /> <label value="Item 3.2" /> </row> </rows> </grid> </zk> |
效果图:
4.2.12 Grid中Detail组件的使用
detail组件是用来展示详情部分,主行和详情部分在同一行上。
例:
<grid> <columns> <column hflex="min" /> <column label="productName"/> <column label="Price"/> <column label="Item Location"/> </columns> <rows> <row> <detail open="true"> <hlayout> <image sclass="myimg" width="100px" height="100px" src="/imgs/tea1.gif" /> 这里面可以有其他的组件来展示详细信息。也可以直接使用文字来描述…… </hlayout> </detail> <label value="商品1"/> <label value="$250"/> <label value="美国"/> </row> </rows> </grid> |
效果图:
展开时:
收拢后:
4.2.13 foot组件
用来显示grid的foot。
例:
<grid width="300px"> <columns> <column label="Type" width="50px" /> <column label="Content" /> </columns> <rows> <row> <label value="File:" /> <textbox width="99%" /> </row> <row> <label value="Type:" /> <hbox> <listbox rows="1" mold="select"> <listitem label="Java Files,(*.java)" /> <listitem label="All Files,(*.*)" /> </listbox> <button label="Browse..." /> </hbox> </row> </rows> <foot> <footer>footer1</footer> <footer>footer2</footer> </foot> </grid> |
效果图:
4.2.14 Grid中使用Group组件分组
支持groupfoot。
例:
<zk> <grid width="800px"> <columns sizable="true"> <column label="Brand" /> <column label=" Processor Type " width="150px" /> <column label="Memory (RAM)" width="120px" /> <column label="Price" width="100px" /> <column label="Hard Drive Capacity" width="150px" /> </columns> <rows> <group label="Dell" /> <row> <label style="padding-left:15px" value="Dell E4500 2.2GHz" /> <label value="Intel Core 2 Duo" /> <label value="2GB RAM" /> <label value="$261.00" style="color:green" /> <label value="500GB" /> </row> <row> <label style="padding-left:15px" value="XP-Pro Slim Dell-Inspiron" /> <label value="Intel Core 2 Duo " /> <label value="2GB RAM" /> <label value="$498.93" style="color:green" /> <label value="500GB" /> </row> <row> <label style="padding-left:15px" value="Dell P4 3.2 GHz" /> <label value="Intel Pentium 4" /> <label value="4GB RAM" /> <label value="$377.99" style="color:green" /> <label value="500GB" /> </row> <group label="Compaq" /> <row> <label style="padding-left:15px" value="Compaq SR5113WM" /> <label value="Intel Core Duo" /> <label value="1GB RAM" /> <label value="$279.00" style="color:green" /> <label value="160GB" /> </row> <row> <label style="padding-left:15px" value="Compaq HP XW4200" /> <label value="Intel Pentium 4" /> <label value="4GB RAM" /> <label value="$980" style="color:green" /> <label value="500GB" /> </row> <groupfoot spans="5"> <label value="This a summary about Compaq Desktop PCs" /> </groupfoot> </rows> </grid> </zk> |
效果图:
收起来:
4.3 List组件
相关子组件:listbox、listitem、listcell、listhead、listheader
listbox组件可以用来展示列表数据。用户可以选中列表数据中的某一条记录。
listbox的mold:default、select、paging。
如果我们使用mold=”select”,那么它将被解析成HTML的select标签。
4.3.1 List组件概观
例1:
<window title="listbox demo" border="normal" width="250px"> <listbox id="box"> <listhead sizable="true"> <listheader label="name" sort="auto" /> <listheader label="gender" sort="auto" /> </listhead> <listitem> <listcell label="Mary" /> <listcell label="女" /> </listitem> <listitem> <listcell label="John" /> <listcell label="男" /> </listitem> <listitem> <listcell label="Jane" /> <listcell label="女" /> </listitem> <listfoot> <listfooter> <label value="This is footer1" /> </listfooter> <listfooter> <label value="This is footer2" /> </listfooter> </listfoot> </listbox> </window> |
效果图:
4.3.2 mold为select的lisbox
当listbox的mold属性为select时,可以作为一个下拉框来使用。
例:
<zk> <listbox mold="select" rows="1" width="70px"> <listitem label="Car" /> <listitem label="Taxi" /> <listitem label="Bus" selected="true" /> <listitem label="Train" /> </listbox> </zk> |
效果图: →
4.3.3 listbox的常用属性
属性名 |
说明 |
sizedByContent |
根据内容改变大小。取值:true、false 一般与span=”true”连用。例如: <listbox sizedByContent="true" span="true" width="800px"> |
span |
当设置了sizeByContent后,每一列都只会占用它需要的宽度。如果我们想每一列都看起来宽松一点的话,就可以设置span=”true” |
emptyMessage |
当展现的内容为空时,就会显示我们设置的emptyMessage中的内容。 如:<listbox id="test1" emptyMessage="没有相关数据"> |
sizable |
listhead组件的属性,设置sizable=”true”后,用户就可以使用鼠标来更改列宽。例如:<listhead sizable="true"> |
4.3.4 listbox的分页
同grid组件类似,只要在listbox组件上加上属性mold="paging" pageSize="n"即可。如果不指定pageSize,则会使用默认值20。
4.3.5 listbox的自动排序功能
我们通过listheader上的sort属性来实现listbox的自动排序。默认情况下的设置为sort=”auto”。它能对这一列所展示的字符串进行自动排序。如果我们想自定义的控制,根据指定的字段来排序,可以使用sort="auto(field1[,field2...])"的格式来指定。
① 默认的排序功能
例:
<zk> <listbox width="400px" mold="paging" pageSize="2"> <listhead sizable="true"> <listheader label="序号" sort="auto" hflex="min"/> <listheader label="名字" sort="auto" /> <listheader label="性别" sort="auto" /> </listhead> <listitem> <listcell label="1" /> <listcell label="Mary" /> <listcell label="女" /> </listitem> <listitem> <listcell label="2" /> <listcell label="John" /> <listcell label="男" /> </listitem> <listitem> <listcell label="3" /> <listcell label="Jane" /> <listcell label="女" /> </listitem> </listbox> </zk> |
效果:
② 根据指定的字段来排序,使用sort="auto(field1[,field2...])"的格式
例:
<?init class="org.zkoss.zkplus.databind.AnnotateDataBinderInit"?> <window title="Test auto(FIELD_NAME1,FIELD_NAME2)" border="normal" width="300px"> <zscript> <![CDATA[ class Person { private String firstName; private String lastName; private int age; public Person(String firstName, String lastName, int age) { this.firstName = firstName; this.lastName = lastName; this.age = age; } public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public String getFullName() { return firstName + " " + lastName; } public int getAge() { return age; } } java.util.List persons = new java.util.ArrayList(8); persons.add(new Person("Tom", "Yeh", 43)); persons.add(new Person("Henri", "Chen", 43)); persons.add(new Person("Jim", "Yeh", 39)); ]]> </zscript> <listbox model="@{persons}"> <listhead> <listheader label="Full Name" sort="auto(lastName, firstName)" /> <listheader label="Age" sort="auto(age)" /> </listhead> <listitem self="@{each=person}"> <listcell label="@{person.fullName}" /> <listcell label="@{person.age}" /> </listitem> </listbox> </window> |
效果图:
→
4.4 Tree组件
tree组件可以实现与listbox相似的功能。但是我们通常都是用tree组件来做树状的菜单。这里不做过多介绍。
请看下面的例子:
<zk> <window id="main" width="100%" height="100%" border="none"> <borderlayout> <west size="200px" splittable="true" flex="true" collapsible="true" title="功能菜单"> <tree id="tree" zclass="z-dottree" width="150px" rows="100"> <treechildren> <treeitem> <treerow> <treecell><image src="/imgs/menu.png"/>信息汇总</treecell> </treerow> <treechildren> <treeitem> <treerow> <treecell><image src="/imgs/menu.png"/>基本信息</treecell> </treerow> <treechildren> <treeitem> <treerow> <treecell><image src="/imgs/menuitem.png"/>班级信息</treecell> </treerow> </treeitem> <treeitem> <treerow> <treecell><image src="/imgs/menuitem.png"/>学生信息</treecell> </treerow> </treeitem> <treeitem> <treerow> <treecell><image src="/imgs/menuitem.png"/>我的信息</treecell> </treerow> </treeitem> </treechildren> </treeitem> </treechildren> </treeitem> </treechildren> </tree> </west> </borderlayout> </window> </zk> |
效果图:
4.5 开发中的ZK常用组件
4.5.1 A组件
例1:
<a href="http://www.zkoss.org" label="Visit ZK!" image="/imgs/gear1.gif"> <grid> <rows> <row>What ever content</row> </rows> </grid> </a> <a href="http://www.zkoss.org" label="Visit ZK!"/> <a href="../../grid/paging.zul" label="Visit Grid Demo!"/> |
效果图:
a组件中的路径都是相对路径。如果路径是以斜杠(/)开头,那么则表示是当前的context path(上下文路径)。其他的路径都是相对于文件本身的路径而言的。
a组件可以包含其他的组件,但是它的子组件也要能处理鼠标点击事件。
4.5.2 Button组件
按钮组件有两种类型,button和toolbarbutton,它们的主要功能是类似的,只是其外观显示不同。
在解析时,button组件使用html中的button标记,而toolbarbutton则使用的是html中的a标记。
① button组件的属性
属性名 |
说明 |
|
label |
指定按钮上的文字 |
|
image |
为按钮指定图像 |
|
dir |
用于控制图像和文字哪个显示在前面,默认是图像显示在前面。 若dir="reverse"则文字显示在前面 |
|
orient |
控制布局是横向还是纵向,默认是横向布局。 orient="vertical"则为纵向布局 |
|
onClick |
按钮被点击后进行的响应 |
|
href |
按钮被点击后跳转的页面 |
|
mold |
取值有:default、trendy、os |
|
autodisable |
通过设置这个属性,可以防止重复提交。 例:
效果图: 如果在按钮的id前面加上加号(+),则点击一次后被指定的按钮将变成disabled,需要我们手动变回enabled;如果不加+,则点击时的这次request完成以后,按钮将自动从disabled变成enabled |
② 配置button默认的属性
在zk.xml中做如下设置,则可以使按钮的默认mold变成trendy:
<library-property> <name>org.zkoss.zul.Button.mold</name> <value>trendy</value> </library-property> |
在zk.xml中做如下设置,则可以使按钮的autodisable指向它自身:
<language-addon> <component> <component-name>button</component-name> <extends>button</extends> <property> <property-name>autodisable</property-name> <property-value>self</property-value> </property> </component> </language-addon> |
③ button上面使用图片
例1:
<?page title="button标签"?> <window title="按钮应用" border="normal" width="40%"> <button label="文字left" image="/imgs/sml1.gif" dir="reverse" onClick="view.value=self.label" href="/index.zul"/> <button label="文字right" image="/imgs/sml2.gif" onClick="view.value=self.label"/> <button label="文字below" image="/imgs/sml3.gif" orient="vertical" onClick="view.value=self.label"/> <button label="文字above" image="/imgs/sml4.gif" orient="vertical" dir="reverse" onClick="view.value=self.label"/> <separator/> <label id="view"/> </window> |
效果图:
④ 按钮的onClick与href属性
我们可以通过两种方式为一个按钮指定动作:一是通过onClick来监听;二是使用href来指定一个路径。这两种方式中,href的优先级要高于onClick。如果同时指定两者,则onClick不会被发送。
⑤ 使用html中的button标签
通过指定xml命名空间为native,则ZK解析器会按照html的格式来解析这个命名空间下的组件。(很少用)
例:
(实际应用中,我们可以使用下面的代码跳转到一个jsp页面,或者一个actoin中,等等)
<n:form action="/foo/my_handler" xmlns:n="native"> <textbox/> <button type="submit" label="Submit"/> <button type="reset" label="Reset"/> </n:form> |
4.5.3 验证码Captcha
例:
<vbox> <span>验证码:<textbox id="tb" value="请输入" onFocus='self.setValue("")' onBlur="check()"/></span> <captcha id="cpa" length="5" width="200px" height="50px" /> <zscript><![CDATA[ void check(){ if(!tb.getValue().toLowerCase().equals(cpa.getValue().toLowerCase())){ Messagebox.show("请输入正确的验证码"); tb.setValue("请输入"); } } ]]> </zscript> </vbox> |
效果图:
4.5.4 combobutton组件
combobutton是一种特殊的按钮,它里面可以包含一个popup或者menuepopup做为child
例:
<zk> <combobutton label="popup" image="/imgs/sml3.gif" autodrop="true"> <popup> <vbox> <hbox> Search <textbox /> </hbox> <listbox width="200px"> <listhead> <listheader label="Name" /> <listheader label="Description" /> </listhead> <listitem> <listcell label="John" /> <listcell label="CEO" /> </listitem> <listitem> <listcell label="Joe" /> <listcell label="Engineer" /> </listitem> </listbox> </vbox> </popup> </combobutton> <combobutton label="menu popup" image="/imgs/sml2.gif"> <menupopup> <menuitem label="Index" /> <menu label="Menu"> <menupopup> <menu label="Color Picker" content="#color=#029BCB" /> </menupopup> </menu> </menupopup> </combobutton> </zk> |
autodrop属性用于设置鼠标滑过时是否自动打开弹出框。
效果图:
→ →
4.5.5 Fisheye组件
例:
<zk> <div height="450px"> <checkbox label="Attach icon edge at bottom" onCheck='fsb.attachEdge=self.checked?"bottom":"top"' /> <checkbox label="Vertical orient" onCheck='fsb.orient=self.checked?"vertical":"horizontal"' /> <separator bar="true" /> <fisheyebar id="fsb" style="position:absolute;margin:80px 150px;" attachEdge="top" itemWidth="80" itemHeight="80" itemMaxHeight="160" itemMaxWidth="160"> <fisheye image="/imgs/button04.jpg" label="folder" onClick="alert(self.label)" /> <fisheye image="/imgs/button05.jpg" label="search" onClick="alert(self.label)" /> <fisheye image="/imgs/button06.jpg" label="project" onClick="alert(self.label)" /> <fisheye image="/imgs/button08.jpg" label="Email" onClick="alert(self.label)" /> <fisheye image="/imgs/button21.gif" label="Globe" onClick="alert(self.label)" /> <fisheye image="/imgs/button23.gif" label="telescope" onClick="alert(self.label)" /> </fisheyebar> </div> </zk> |
效果图: →
→
→
4.5.6 HTML组件
如果我们要在页面上直接使用html标签而不让zk的解析器来解析,那么我们就可以使用html组件。配合<![CDATA[]]>一起使用。
注意:html组件中的内容不是这个组件的child。我们可以在里面使用EL表达式
例:
<window id="win" title="Html Demo" border="normal"> <html><![CDATA[ <h4>Hi, ${win.title}</h4> <p>It is the content of the html component.</p> ]]></html> </window> |
效果图:
4.5.7 Iframe组件
iframe组件和HTML中的ifram标签的功能相同。
iframe组件和include组件很相似,但是它们有很大的不同:
◆ include包含进来的子页面是属于当前的页面的,也就是属性当前desktop的一部分。我们可以直接访问include包含进来的组件。包含的动作是在server端,browser对此一无所知。
◆ iframe包含进来的子页面是作为一个单独的页面来加载的。包含的动作发生在browser端。
例:
<window id="win" title="This is an Iframe Demo!"> <iframe style="width:99%; height:400px;border:3px inset;" src="http://www.zkoss.org" /> <iframe style="width:99%; height:400px;border:3px inset;" src="/components/_essentialComponent/006html/html.zul" /> </window> |
4.5.8 Include组件
include组件是一个ID空间宿主。所以不用担心两个include组件中的id会重复。
src属性可以指定为一个servlet、jsp、zul等等。
Include可以包含ZUML页面、静态页面、JSP页面或者一个servlet。
当我们使用Include包含的是一个非ZUML的页面时,那么那个页面中的内容将会被作为Include组件的内容。
当我们使用Include包含一个ZUML页面时,那个被包含的ZUML页面中的组件将会作为Include组件的子组件
例:
<include src="another.zul"/> <include src="another.jsp"/> |
简单的include用法我们就不再多言。我们重点讲一下include关于url的参数传递。
① include中通过路径来传递参数
include.zul页面: <zk> <window title="include demo" border="normal" width="300px"> <zscript> <![CDATA[ class User{ private int id; private String name; private String sex; public User() { } public User(int id, String name, String sex) { this.id = id; this.name = name; this.sex = sex; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } } User user = new User(1, "小张", "女"); ]]> </zscript> Hello, World! <include src="temp.zul?text=5" /> <include src="temp.zul" some="something" another="${user}" /> 使用include时,两种传值的方式 </window> </zk> temp.zul页面: <window title="temp页面" border="normal" width="280px"> <label if="${param.text ne null}" value="1.param.text:${param.text}"/> <separator bar="true"/> <label if="${requestScope.some ne null}" value="2.equestScope.some:${requestScope.some}"/> <separator bar="true"/> <label if="${requestScope.another ne null}" value="3.requestScope.another:${requestScope.another}、${requestScope.another.id}、${requestScope.another.name}、${requestScope.another.sex}"/> <separator bar="true" /> </window> |
效果图:
通过代码的话,我们可以使用:Include.setDynamicProperty(java.lang.String, java.lang.Object)来传递参数。
接收传递过来的参数时,我们可以使用:Execution.getParameter(java.lang.String)或者使用javax.servlet.ServletRequest的接口,也就是说我们可以通过EL表达式来得到传递的参数值。
通过url,我们只能传递string类型的参数。传递复杂数据类型的参数时请参照上面的例子。
② 刷新include包含进来的页面
例:
<window title="include demo" border="normal" width="300px"> <div style="color:red">Hello, World!刷新include进来的页面</div> <include id="inner" src="/components/_essentialComponent/003captcha/captcha.zul?text=5" /> <button label="Load" onClick='inner.src = "/components/_essentialComponent/003captcha/captcha.zul?text=10"'/> <button id="reload1" label="Reload1" onClick="String tmp=inner.src; inner.src=null; inner.src=tmp;"/> <button id="reload2" label="Reload2" onClick="inner.invalidate();"/> </window> |
解析:
1、Load按钮所设置的的路径是恒定不变的。当第二次再点击时,就不会进行刷新了。
2、通过先设置inner.src=null后,再设置它的值,这样就可以完成刷新同一个页面了。例如:Reload1按钮
3、通过inner.invalidate()也可以实现相同的效果
4.5.9 Image组件
例:
<zk> Location: <textbox onChange="updateMap(self.value)"/> Map: <image id="image"/> <zscript><![CDATA[ void updateMap(String location) throws Exception{ if (location.length() > 0) { org.zkoss.image.AImage img = new org.zkoss.image.AImage(location); image.setContent(img); } } ]]> </zscript> <separator/> <image src="/imgs/folder.gif" /> </zk> |
效果图:
Image组件还支持javax.awt.image.RenderedImage产生的图片:
例:
<window title="Test of Live Image"> <image id="img"/> <zscript><![CDATA[ import java.awt.*; import java.awt.image.*; import java.awt.geom.*; int x = 10, y = 10; void draw(int x1, int y1, int x2, int y2) { BufferedImage bi = new BufferedImage(400, 300, BufferedImage.TYPE_INT_RGB); Graphics2D g2d = bi.createGraphics(); Line2D line = new Line2D.Double(x1, y1, x2, y2); g2d.setColor(Color.blue); g2d.setStroke(new BasicStroke(3)); g2d.draw(line); img.setContent(bi); } draw(x, y, x += 10, y += 10); ]]> </zscript> <button label="change" onClick="draw(x, y, x += 10, y += 10)"/> </window> |
4.5.10 Imagemap组件
例:
<window title="Test of Imagemap"> 点击图片显示点击的坐标 <imagemap src="/imgs/map.gif" onClick='alert(event.x + "," + event.y)' /> 点击我们设置的区域,显示对应的区域id <imagemap src="/imgs/map.gif" onClick="alert(event.area)"> <area id="First" coords="0, 0, 100, 100" /> <area id="Second" shape="circle" coords="200, 200, 100" /> </imagemap> </window> |
第一个imagemap的效果图:
4.5.11 Label组件
Label组件是一个特别常用的组件。用法也很简单。我们这里主要讲一下它的3个属性:pre、multiline和maxlength
属性名 |
说明 |
pre |
用来屏蔽多余的空白字符。多余即首尾的空白字符(处理后被去掉),和中间多个空白字符(处理后变成一个)。 |
multiline |
取值:true、false。是否显示多行 |
maxlength |
最多显示几个字符 |
例:
<window id="win" title="demo" border="normal"> <vbox id="result"> <label id="lbl1" pre="true"></label> <separator bar="true" /> <label id="lbl2" multiline="false" /> <separator bar="true" /> <label id="lbl3" maxlength="10" /> <zscript><![CDATA[ lbl1.value = " this thing has spaces.\nnext line."; lbl2.value = " this thing no space.\nnext line."; lbl3.value = " this is more than 10 chars."; ]]></zscript> </vbox> </window> |
效果图:
4.5.12 Menue组件
例:
<window> <menubar id="menubar"> <menu label="File"> <menupopup> <menuitem label="New" onClick="alert(self.label)" /> <menuitem label="Open" onClick="alert(self.label)" /> <menuitem label="Save" onClick="alert(self.label)" /> <menuseparator /> <menuitem label="Exit" onClick="alert(self.label)" /> </menupopup> </menu> <menu label="Help"> <menupopup> <menuitem label="Index" onClick="alert(self.label)" /> <menu label="About"> <menupopup> <menuitem label="About ZK" onClick="alert(self.label)" /> <menuitem label="About Potix" onClick="alert(self.label)" /> </menupopup> </menu> </menupopup> </menu> </menubar> </window> |
效果图:
→ →
5 根据需求和条件手动控制页面的加载和组件的创建
5.1 根据条件来控制组件的创建
ZK中提供了很多根据条件来创建组件的方式。注意他们和visible的区别。visible是控制组件的显示和隐藏,而这里要介绍的是控制组件是否创建。
5.1.1 if和unless属性(适用于所有组件)
基本上所有的zk组件都有if和unless属性,他们用于控制组件是否被创建。
例1:当param.readonly为true时,我们使用label组件,否则使用textbox组件
<label value="${customer.label}" if="${param.readonly == 'true'}"/> <textbox value="${customer.value}" unless="${param.readonly == 'true'}"/> |
例2:只有当a等于1,b不等于2的时候,这个window才会被创建
<window if="${a==1}" unless="${b==2}"> ... </window> |
5.1.2 switch和case(适用于zk标签)
我们一般用switch case组合来控制ZUML页面的创建。第一个匹配成功的条目才会被创建,只要一个匹配成功,就不再做匹配。没有case属性的zk标签是默认的匹配项。
例:
<zk switch="${fruit}"> <zk case="apple"> 当${fruit}值为apple时被创建 </zk> <zk case="${special}"> 当${fruit}值为${special}时被创建 </zk> <zk> 当上面没有一个匹配成功时被创建 </zk> </zk> |
我们也可以为case指定多个值,只要其中的一个能够匹配成功,那么整个表达式就匹配成功,该zk标签中的内容将被创建。
例:
<zk switch="${fruit}"> <zk case="apple, ${special}"> 当${fruit}的值为apple 或 ${special} 时被创建 </zk> </zk> |
我们还可以在case中使用正则表达式:
例:
<zk switch="${fruit}"> <zk case="/ap*.e/"> Evaluate if the regular expression, ap*.e"., matches the switch condition. </zk> </zk> |
5.1.3 choose和when(适用于zk标签)
choose when的功能与switch case差不多,这里不再过多介绍。
例:
<zk choose=""> <zk when="${fruit == 'apple'}"> Evaluated if the when condition is true. </zk> <zk><!-- default --> Evaluated if none of above cases matches. </zk> </zk> |
注:你不需要为choose属性指定值,它的作用仅仅是为了标识choose的结束,即:通过它可以找到choose结束的地方
5.2 根据需求加载页面或组件
一个ZUML文件可以被划分成多个页面,一般情况下,这些页面都是同时被加载进来的。这样,在页面首次被加载的时候将会消耗很多带宽,为了提高性能,我们可以通过fulfill属性来延迟页面的加载,也就是我们说的根据需求来加载。
不仅是页面可以延迟加载,组件也是可以延迟加载的。例如下拉框,我们在点击它的时候才加载里面的数据的话,我们也可以为其设置fulfill属性
例1:当combobox收到了onOpen的事件后再加载里面的数据
<combobox fulfill="onOpen"> <comboitem label="First Option"/> </combobox> |
例2:通过事件触发和组件的id来指定组件的加载
<button id="btn" label="show" onClick="content.visible = true"/> <div id="content" fulfill="btn.onClick"> Any content created automatically when btn is clicked </div> |
不属于同一个id空间的两个组件,我们可以通过路径来指定:
<button id="btn" label="show" onClick="content.visible = true"/> <window id="content" fulfill="../btn.onClick"> Any content created automatically when btn is clicked </window> |
按需加载一个页面:
我们也可以为fulfill属性指定一个页面。这个延迟加载的页面将会作为相应的组件的children。
<zk> <button id="btn" label="Click to Load"/> <div fulfill="btn.onClick=another.zul"/> </zk> |
6 迭代赋值
默认情况下,ZK加载器为zuml页面中的每个XML元素实例化一个组件。如果我们想生成一个组件集的话,我们可以通过forEach来实现。
例:
<listbox> <listitem label="${each}" forEach="Apple, Orange, Strawberry"/> </listbox> |
它等同于
<listbox> <listitem label="Apple"/> <listitem label="Orange"/> <listitem label="Strawberry"/> </listbox> |
也等同于
<zscript><![CDATA[ String[] grades = new String[] {"Best", "Better", "Good"}; ]]></zscript> <listbox> <listitem label="${each}" forEach="${grades}"/> </listitem> |
当ZK加载器通过集合中的项目来迭代的时候,它会更新两个隐含对象:each和forEachStatus。
这种迭代方式依赖于forEach属性的值的类型。
forEach属性的值必须是一个集合类型。大致有以下几种情况:
① java.util.Collection:迭代Collection集合中的每个元素
② java.util.Map:迭代Map中的每一个Map.Entry
③ java.util.Iterator:迭代Iterator中的每个元素
④ java.util.Enumeration:迭代Enumeration中的每个元素
⑤ Object[]、int[]、short[]、byte[]、char[]、float[]、double[]:迭代数组中的每个元素
⑥ null:不会生成任何组件,将会被忽略
看下面这个例子:
<zk> <hlayout> <zscript> String[] classes = new String[] {"College", "Graduate"}; Object[] grades = new Object[] { new String[] {"Best", "Better"}, new String[] {"A++", "A+", "A"} }; </zscript> <listbox width="200px" forEach="${classes}"> <listhead> <listheader label="${each}"/> </listhead> <listitem label="${forEachStatus.previous.each}: ${each}" forEach="${grades[forEachStatus.index]}"/> </listbox> </hlayout> </zk> |
效果图:
将<hlayout></hlayout>去掉后的效果图:
解析:forEach就好比一个for循环,它循环的范围就是forEach所在的标签的开始标签至结束标签处。故,在上例中,listbox处的forEach循环它里面的listhead和listitem组件。我们不难发现,这是一个两重循环。listitem处还有一个forEach,也就是第二重循环。循环第一次开始时,listheader处的each被赋值为”College”。listitem处又有一个forEach,这时each从属于listitem处的forEach,这时我们要引用上一层循环中的each的话,只能通过forEachStatus.previous.each来得到。
注:forEach好比一个for循环,each就好比一个iterator
forEach和each组合在一起可以与下面的代码类比:
while(iterator.hasNext()){//forEach开始处 each = iterator.next();//给each属性赋值 } |
我们可以在zscript中直接使用each和forEachStatus:
<window> <button label="${each}" forEach="apple, orange"> <zscript><![CDATA[ self.parent.appendChild(new Label("" + each)); ]]></zscript> </button> </window> |
效果图:
你不能在事件监听器中获得each和forEachStatus的值,因为当页面加载完成后,each已经不存在了,而这个时候我们再去触发事件想要得到each中的值的话,就不能够实现了。
一个简单的解决办法就是在页面加载的时候就将第一个each的值都用组件的属性存放起来,这样的话,当页面加载完成后,我们就可以通过检索预先存放的属性值来得到我们想要的值了。
例:
<window> <button label="${each}" forEach='"中国","美国"' onClick='alert(self.getAttribute("country"))'> <custom-attributes country="${each}"/> </button> </window> |
注:用户自定义属性,属性名是随便取的,但是标签名一定是custom-attribute
迭代集合中的一部分:通过指定forEachBegin和forEachEnd
<grid> <rows> <row forEach="${foos}" forEachBegin="${param.begin}" forEachEnd="${param.end}"> ${each.name} ${each.title} </row> </rows> </grid> |
7 XML命名空间简介
在ZK中标准的命名空间有3个:client、native、zul
ZK解析器在加载一个页面的时候,首先通过页面的拓展名来确定页面默认的命名空间。
在ZK工程中一般有两种页面:.zul和.zhtml。.zul的页面默认按zul的命名空间来进行解析。.zhtml的页面默认按native的命名空间来解析(即把它当成一个普通的html页面)。
如果我们想在默认的命名空间下使用不同的命名空间,让zk解析器按指定的命名空间来解析的话,我们就可以通过下面的方式来指定页面的命名空间。
① 指定由客户端(前台)来控制组件,而不是后台
<combobox xmlns:w="client" w:onFocus="this.open()" onChange="doOnChange()"/> |
② 使用html的形式来解析组件(在zul页面中)
<n:table xmlns:n="native"> <n:tr> <n:td>Username</n:td> <n:td><textbox/></n:td> </n:tr> <n:tr> <n:td>Password</n:td> <n:td><textbox type="password"/></n:td> </n:tr> </n:table> |
③ 使用zul的形式来解析组件(在zhtml页面中)
<!-- foo.zhtml --> <p> <!-- assumed from the XHTML language --> <u:window xmlns:u="zul"/> <!-- ZK Loader will search the ZUL language instead --> </p> |
8 Zscript的使用
zscript是zuml页面上的页面脚本。
zuml页面中的脚本分两种:1、客户端脚本 2、服务器端脚本
zscript是属于服务器端脚本。<zscript>里面的代码是java代码,它运行在服务器端,所以它能够访问java包,在它里面你也可以定义变量、方法甚至是类,在同一个page下它对EL表达式是可见的。也就是说,EL表达式可以使用<zscript>中的变量、方法、类等。
在zscript中的代码必须是一段有效的xml文本。所以,在用要特殊字符的时候我们需要转义。为了避免转义,我们通常使用CDATA来包围这段代码。<![CDATA[代码]]>
书写zscript的方式大致有三种:
1、标准格式:
<zscript> <![CDATA[ //代码 ]]> </zscript> |
2、将zscript写在事件监听器上
如下面的例子
<button id="reload1" label="Reload1" onClick="String tmp=inner.src; inner.src=null; inner.src=tmp;"/> |
3、将zscript写在attribute中
如果处理事件处理器的代码比较复杂,我们可以在attribute元素中指定事件的名字,然后编写代码。上面的代码可以写为:
<button id="reload1" label="Reload1"> <attribute name="onClick"><![CDATA[ String tmp=inner.src; inner.src=null; inner.src=tmp; ]]></attribute> </button> |
zscript中的变量对EL表达式是可见的:
例:
<zscript> <![CDATA[ Date now = new Date(); ]]> </zscript> <label value='${now}'/> |
9 在ZUML页面上使用tld文件中指定的函数
在页面上,我们经常会对一些内容进行格式化,然后再输出。通常我们都将这些转换函数写成一个公有的函数。在ZK中我们可以通过以下步骤来实现在页面上调用指定的函数:
1、 用一CommonUtils类将所有的公共函数写在一起:
例:
public class CommonUtils { public static final String TIME_PATTERN_DEFAULT = "yyyy-MM-dd HH:mm:ss"; private static String getPattern(String pattern){ if("".equals(pattern) || null==pattern){ pattern = TIME_PATTERN_DEFAULT; } return pattern; } public static String parseDateToString(Date date, String pattern){ String redate = null; pattern = CommonUtils.getPattern(pattern); SimpleDateFormat sdf = new SimpleDateFormat(pattern); redate = sdf.format(date); return redate; } } |
2、 新建一个tld文件来描述CommonUtils类中的每一个公共函数
<taglib> <function> <name>parseDateToString</name> <function-class>com.cn.developer.common.CommonUtils</function-class> <function-signature> java.lang.String parseDateToString(java.util.Date, java.lang.String) </function-signature> <description> Returns a formatted date. </description> </function> <!-- any number of functions are allowed --> </taglib> |
3、 在页面上引用我们的tld文件
<?taglib uri="/tld/commonFunction.tld" prefix="c"?> <window id="win" title="引用tld文件中的函数" border="normal" width="200px"> <zscript> <![CDATA[ Date now = new Date(); ]]> </zscript> <label value='${c:parseDateToString(now, "yyyy-MM-dd") }'/> </window> |
访问上面的页面的效果图:
10 自定义变量解析器
将EL表达式中特定的名字转换成我们指定的值。
下面举一个例子:
<zk>
<listbox>
<listitem label="${each.name}" forEach="${customers}"/>
</listbox>
</zk>
如果直接访问这个页面,肯定会报错。因为zk无法解析customers。下面的操作将会解析出这个页面。
首先,我们定义一个类,这个类要实现VariableResolver接口:
public class CustomerResolver implements VariableResolver { @Override public Object resolveVariable(String name) throws XelException { if ("customers".equals(name)) return Customer.getAll("*"); return null; //not a recognized variable } } |
在Customer类中加入静态的方法Customer.getAll(java.lang.String)
public class Customer { private int id; private String name; private String sex; public Customer() { } public Customer(int id, String name, String sex) { this.id = id; this.name = name; this.sex = sex; } //略去getter和setter //.......... public static List<Customer> getAll(String str){ List<Customer> list = new ArrayList<Customer>(); list.add(new Customer(1, "张三", "男")); list.add(new Customer(2, "李四", "女")); return list; } } |
resolver.zul:
<?variable-resolver class="com.cn.developer.resolver.CustomerResolver"?> <zk> <listbox> <listitem label="${each.name}" forEach="${customers}"/> </listbox> </zk> |
最后得到的效果:
我们可以将这个Resolver设置成全局的,而不需要每个页面都引用,可以在zk.xml中加入下面的代码:
<listener>
<listener-class> com.cn.developer.resolver.CustomerResolver </listener-class>
</listener>
10 ZK-MVVM开发模式
ZK6.0之前的版本都是采用MVP(Model-View-Presenter)设计模式。从ZK6.0开始就有了很大的不同,采用MVVM(Model-View-ViewModel)设计模式。
10.1 ZK MVP开发模式
我们通过一个例子来剖析MVP的开发模式。
假如我们要实现下面的功能:Client客户端页面上点击一个按钮,然后从Server服务端返回消息”Hello World”
MVP通过下图所示来实现:
作用过程:
①用户在屏幕上点击按钮(一个动作action),一个通信事件就被触发。
②后台的事件监听器(event listener)处理这个事件(event)。
③访问数据库(access data)并返回
④用户界面元素(UI element)被改变,相应的把视觉反馈(visual feedback)呈现给用户
相关的代码如下:
10.2 MVVM开发模式
实现与2.1中相同的功能,MVVM的实现过程如下图:
作用过程:
①用户在屏幕上点击按钮(action)触发一个事件(event)
②绑定器(Binder)感知到相应的事件(corresponding event)被触发
③绑定器(Binder)在ViewModel中找到相应的动作逻辑(action logic)并调用它
④动作逻辑(action logic)从Model层访问数据库(access data),并更新ViewModel中相应的属性
⑤ViewModel通知(notify chnage)绑定器(Binder) 一些属性被改变
⑥每当有属性被改变,绑定器(Binder)就会从ViewModel中加载数据(load data)
⑦绑定器(Binder)改变相应的UI组件来给用户呈现视觉反馈(visual feedback)
很明显,UI设计者必须至少告诉绑定器(Binder)以下几点:
◆ 用到了哪些UI事件(UI event)来触发哪些动作逻辑(action logic)。(这样Binder才知道调用什么方法)
◆ 用到了哪些UI属性(UI attribute)来显示哪些数据。(这样Binder才知道加载什么数据,更新什么数据)
◆ 用到的哪些UI属性(UI attribute)是用来输入到数据库的。(这样Binder才知道要保存哪些属性)
相关代码如下:
在ZK绑定(ZK Bind)中用到了ZK注解(annotation)来完成了很多工作。
结合代码,我们再一次分析程序运行的过程如下:
① 当用户按下了”Show”按钮,onClick event就被触发到了Binder
② ZK Binder通过指定的ZK 注解 @command('showHello')在ViewModel中找到命令的名字(command name)为showHello的方法。
③ Binder在HelloViewModel.java中调用showHello()方法,并且改变message的属性值为”Hello World”。
注意:@command('showHello')是告诉Binder showHello这个方法是一个command方法,而@NotifyChange("message")则是告诉Binder 如果@command('showHello')中的showHello()方法被调用,那么HelloViewModel中的属性message将会被改变。也就是说当这个方法被调用后,页面上(客户端)可以感知到属性message的改变。
④ Binder找到HelloViewModel.java中与组件标签(component label)相关联的属性message的属性值(这是由于页面上message被指定了ZK注解@load(vm.message)的缘故)。因此它通过调用HelloWorldModel.java的message属性的getMessage()方法从vm.message中加载数据,并改变标签label的属性值。最后”Hello World!”就呈现到了客户端用户。
10.3 为什么要使用MVVM的设计模式
有时用户看了上面的页面之后,说想通过弹出窗(pop window)来显示信息message。这样的改变其实很容易,将信息放到一个模态窗口中:
MVP实现:
对于MVP设计模式而言,view层的zul页面修改了之后,Presenter层的java文件必须完全被重写,这时需要注入弹出窗口popwin、弹出窗口中的标签popwin$lbl
MVVM实现:
当客户改变View层的需求的时候,MVVM设计模式的优势就体现出来了。UI设计者可以独立的处理这些改变,而不用去改变HelloViewModel.java文件,因为客户需要的是改变message的呈现方式而不是message本身。
注意:模态窗口的显示或隐藏是通过判断message的值是否为空来控制的。(visible="@load(not empty vm.message)")
11 ZK数据绑定注解
Client端页面上的数据与Server端的ViewModel中的属性的绑定是通过注解来完成的。
如下图所示:
11.1 单向绑定与双向绑定
@save:将它绑定的标签对应的值保存到ViewModel中,
@load:将ViewModel中的对应的属性值加载到前台页面。
@bind:双向绑定,相当于是@save和@bind的一种简写方式。
双向绑定和我们通常实现的ajax功能类似。
11.2 模板绑定
我们可以将一个集合的值赋给model属性,再将model赋给template组件的name属性,这就是我们所说的模板绑定,其实就是将值绑定在template上。
通过模板绑定,我们可以迭代集合中的每一个元素,从而创建多个组件。template组件就好比一个while循环一样。
11.3 表单绑定
11.4 条件绑定
11.5 数据绑定中使用EL表达式
12 ZK开发中使用的注解
12.1 Client端ZUL页面上使用的注解
语法(syntax) |
说明(explanation) |
viewModel="@id(name) @init(expression)" |
用于设置ViewModel。 ◆ 一个拥有apply="org.zkoss.bind.BindComposer"属性的组件可以使用这个注解,如果没有这个注解,那么这个组件将会被设置到一个composer上。 ◆ expression:如果它被赋予一个Class,那么这个Class将创建一个ViewModel实例。 |
comp-attribute="@load(expression)" |
单向绑定。用来加载属性的表达式 ◆ 用户可以在表达式中指定绑定条件,通过参数before和after来设定。例如@load(vm.filter, after='myCommand'),这表示Binder会在执行完command方法myCommand之后加载这个属性 |
comp-attribute="@save(expression)" |
单向绑定。保存组件的属性值到ViewModel属性中的表达式。 ◆ 用户可以在表达式中指定绑定条件,通过参数before和after来设定。例如:@save(vm.filter, before='myCommand'),这表示Binder会在执行command方法myCommand之前保存这个属性到ViewModel |
comp-attribute="@bind(expression)" |
双向绑定。在组件的属性和ViewModel的属性之间实现双向绑定,它不支持条件绑定,也就是说在绑定时不能使用条件。 ◆ 它等同于@load(expression) @save(expression) ◆ 如果组件的属性不支持@save的话,Binder将会自动忽略它 ◆ 通知改变(notify change)的例子:对于表达式vm.filter,如果任意一个通知(notification)说vm或vm.filter改变了,那么这个属性将会被重新加载 ◆ 复杂一点的例子:对于表达式e.f.g.h,如果任何一个通知(notification)说e.f.g.h、e.f.g、e.f或者e被改变了,那么组件的属性将会被重新加载 |
@converter(expression, arg = arg-expression) |
为绑定提供一个转换器conveter。 ◆ 如果expression是一个ZK提供的conveter,那么它将会直接被使用 ◆ 如果expression是一串字符,那么它将会从ViewModel中查找相应的getConveter方法,方法的名字为get+expression返回值为Conveter。参照上面提到conveter的代码。 ◆ 在执行conveter之前,你可以传递多个参数给conveter。这些参数将会在调用conveter方法之前被赋值。 |
comp-event="@command(expression, arg =another-expression)" |
组件的事件绑定,绑定到相应的事件command方法。 ◆ expression必须是ViewModel中的一个command方法的名字(当然,这个方法必须是以@command为注解的) ◆ 事件被触发时,它将会按照ZK Bind Lifecycle来执行这个command方法 |
12.2 Server端ViewMole中使用的注解
语法(syntax) |
说明(explanation) |
@NotifyChange on setter |
它是用于ViewModel类属性的setter方法上的,当调用了属性的setter方法之后,它将会通知(notify)Binder该属性的值被改变了。它是默认开启的,所以,当通知目标(notification target)和属性相同时,在属性的setter方法上你可以省略不写这个注解。 ◆ 我们可以为属性properties指定name,通过@NotifyChange(expression)、@NotifyChange({expression1,expression2,……}),当指定的属性改变时调用这个setter方法 |
@NotifyChange on command method |
它可以用于command方法上,当这个command方法被调用时,通知Binder属性值改变了 |
@NotifyChangeDisabled on setter |
这个注解用在属性的setter方法上可以使setter方法默认开启的@NotifyChange关闭 |
@Command('commanName') |
定义一个command方法。commandName是可选的,如果不指定的话,command方法的名字默认就是它所注解的方法的名字。 |
13 构建第一个MVVM页面
我将用一个查询的例子来展示你怎样利用ZK Bind在ZK 6中使用MVVM设计模式。请假设你正在创建一个查询页面,查询页面上显示的item清单是通过一个字符串条件检索出来的,选中的item会显示它的详细信息。
按照MVVM的设计理念,我们应该抛开视觉效果,首先设计ViewModel。一个ViewModel应该不依赖View层,但是要考虑到与View层交互的数据(data)和行为方法(action)。在这个例子中我们需要一个字符串作为检索条件,一个ListModelList<Item>来保存查询结果,一个doSearch()方法来执行查询命令(command)。此外,我们还需要一个域来保存当前选中的item。
我们可以看出,ViewModel是独立于View层的,这就意味着其他View也可以重复使用这个ViewModel。
通过在一个属性的getter方法上加上@NotifyChange,Binder设置了属性之后,Binder将会被通知属性已经被改变,被绑定属性的组件将会被重新加载。你也可以为command方法加上注解@NotifyChange,这样Binder执行了这个方法后,方法里面如果有指定属性被改变,那么与属性绑定的组件将会被重新加载。定义一个command,我们需要为方法加上Java注解@command,方法的名字就是command默认的名字。
通过为filter的setter方法setFilter()加上@NotifyChange,当Binder保存用户输入的filter值的时候,Binder将会被@NotifyChange通知来重新加载与filter属性相关的ViewModel。我们为doSearch() command方法加上了注解@NotifyChange({"items","selected"}),这是因为每次根据当前filter的值执行完查询后,我们都将创建一个新的items集合,并且需要将选中的属性item(selected)设置为空。当doSearch() command方法被调用后,Binder需要能够感知到这两个属性的改变。
注解@NotifyChange能够通知Binder什么时候自动加载View,和加载什么数据。
注意:
① @NotifyChange是默认激活的,因此你可以忽略它。然而,你想要改变通知的目标,那么你就必须要为不同的属性名加上@NotifyChange注解。
② 如果你想让一个属性默认的@NotifyChange失效,那么你就必须要为该属性的setter方法加上@NotifyChangeDisabled注解。
@id(name) @init(expression)是用于为Binder指定一个ViewModel,其中name是ViewModel的名字,expression是一个EL2.0表达式,用于指定绑定的类。
将filter值绑定到textbox上:
在上面的代码中,我们将vm.filter绑定到了textbox的value属性和button的disabled属性。而且,我们在disabled属性上使用了@load(expression),因为它只需要加载ViewModel中的filter属性的值。当我们在页面上修改textbox中的值的时候,vm.filter的值将会立即改变,相应的,按钮是否可点击状态也会立即改变,这取决于vm.filter是否为空。
将查询结果绑定到listbox上:
在ZK6中有一个新的功能叫template。当我们绑定一个集合的时候,使用它简直是绝配。
下面我们将用listbox和template来显示查询结果:
@load(expressoin)也可以被应用到listbox组件的model属性上。我们需要提供一个名为model(在这里template的name必须为model,与listbox中的model对应)的template来绑定到这个listbox组件的model上,用这个template来显示model中的每一个item。在这个template中,我们还需要给这个template的var属性设置一个值(例如示例代码中的item),这样我们就能用它来表示template中的数据元素了。
绑定当前选中的item:
当我们绑定listbox的时候,我们也将listbox中当前选中的item绑定到了ViewModel中的selected属性上。你不必担心当前选中的项目的数据类型不匹配,因为Binder会将它自动转换成Item(vm.items中存放的元素的类型就是Item,参照上面的代码)。
在上面的代码中我们将groupbox的visible属性与表达式not empty vm.selected绑定,因此,只有当用户选择了一个item之后,这个groupbox才会可见。
在这里,@converter又一次被用到了,但是和之前一次有所不同。现在这个converter来自于ViewModel。当然,我们需要在ViewModel中定义一个名为getTotalPriceConverter的方法,并且返回值的类型为Conveter。
将button的动作绑定到一个command方法:
现在我们需要在点击button按钮的时候执行一个ViewModel中的方法。为了实现这个功能,我们需要在button的onClick事件上添加一个@Command注解。
在组件的事件(event)属性上使用@command(expression)代表将这个事件绑定到ViewModel中的一个command方法上。
它的规则如下:
① 表达式expression必须是一个String字符串
② String字符串必须是ViewModel中的一个command方法的名字
③ 在ViewModel中必须有一个含有注解@Command('commandName')的可执行方法,如果不指定commandName,那么Binder就使用方法的名字作为command方法默认的名字。
实现的最终效果:
14 ZK6 MVVM结合Spring
尽管我们在ZVVM设计模式中使用Spring,但我们并没有把ViewModel设置成一个Spring的bean。主要有以下两个原因:
①在Spring中没有哪一种bean的范围(scope)是能够与ViewModel的生命周期(lifecycle)准确匹配的
②如果我们将ViewModel设置成一个Spring的bean,那么将会有两个变量引用同一个ViewModel,一个是@id(zul页面上指定的ViewModel的id),一个是Spring的bean的名字。这两个变量会很容易让开发者迷惑。
因此,我们就用最原始的方式来使用ViewModel,和激发Spring的变量解析器(variable resolver)
看下面的例子:
由于ViewModel不是Spring的bean,所以我们不能通过Spring的autowire(自动注入)来检索OrderService对象,但是,我们可以用ZK专门的注解@WireVariable来达到相同的效果。
将Conveter设计成一个单例bean:
有时我们想要用到一个Converter来完成类型或格式转换,但是在ZK自带的Conveter里面找不到合适的,这时我们通常是在相应的ViewModel中写一个getXXConveter的方法来返回一个Conveter(参见前面的例子)。 这样的Conveter没有重用性,为了提高组件的重用性,我们可以将我们需要用到的Conveter放到一个独立的类中,并把它们设置成一个bean,这样我们就可以在zul页面上直接使用这个conveter而不必在ViewModel中添加getXXConveter的方法。
代码如下:
由于Conveter不依赖于其他的bean,而且是无状态的,所以我们可以将它声明成单例bean。上面代码中的Conveter都需要接收一个format的参数作为格式化的Date的格式。
页面上使用:
同时,我们也可以将Validator设计成一个单例bean:
Validator和Conveter类似,我们可以将ViewModel中的Validator全部提取出来放到一个单独的类中。
这个验证器验证的域(shippDate)是基于另一个域(creationDate)的,并且将验证消息存入验证消息持有器中。
页面上使用:
这样设置之后,我们就不必在zul页面上使用”vm.”前缀来调用它们,因为它们可以被ZK的DelegatiingVariableResolver检索。任何一个zul页面都可以通过它们的bean name来得到相应的validator。
任何一个可以重复使用的元素都可以被分离开来,放到不同的包中。
[微软用户1]使用div组件(或span组件)将两个组件包含后,它们只占用了一个单元格的位置。