文章有点长,写得比较详细,有兴趣与耐心开发JSF组件的就往下看吧,下面将详细介绍一个标准JSF组件的制作过程,并且后面将使用QFaces将它升级为Ajax方式的组件(如果要升级为Ajax组件,请先安装QFaces增强框架).如果你发现有任何问题或错漏,请给予批评指正,相关的完整代码在QFaces的Demo示例中,可以自行下载查看:
QFaces相关完整示例下载
现在先来看一下一个JSF标准组件所需要用到的一些文件:
1.UIComponent
― 组件的主类,用于组件渲染,状态的保存及一些行为的处理等.
2.UIComponentTag
- 组件的jsp标签处理类,主要进行值绑定及方法绑定等.
3.tld
- 标签库描述符文件,主要用于注册组件的可用属性等.
4.faces-config.xml
- 配置文件,主要用于注册你的组件.
注意的是,我们这里所介绍的环境是JSF1.2的JSP视图的组件, 如果你使用了facelets视图技术,则可以省去UIComponentTag及tld文件,制作组件的方法基本相同,但注册及方法绑定稍有不同.
现在我们来一步一步创建一个组件吧,暂且把这个组件的名字取为:Hello
步骤1.创建UIComponent类 – HtmlHello.java
public class HtmlHello extends UIComponentBase implements Ajax{
@Override
public String getFamily() {
return null;
}
public void ajaxInvokeOnPost(Map<String, String> params, FacesContext fc) {
throw new UnsupportedOperationException("Not supported yet.");
}
public void ajaxInvokeOnGet(Map<String, String> params, FacesContext fc) {
// 后面我们将实现这个方法
}
}
为了简单起见,我们继承了比较基础的UIComponentBase 同时实现QFaces中的Ajax接口,Ajax接口需要实现两个方法:ajaxInvokeOnPost 与 ajaxInvokeOnPost, 分别处理post与get请求,可以选择实现,后面我们将实现ajaxInvokeOnGet这个方法,因为这对性能很好,如果你只需要一个标准JSF组件,可以不实现这个接口.在这里我们需要先进行一些其它方面的工作:渲染组件
为了渲染组件,我们需要覆盖encodeBegin方法:
public class HtmlHello extends UIComponentBase implements Ajax{
@Override
public String getFamily() {
return null;
}
public void ajaxInvokeOnPost(Map<String, String> params, FacesContext fc) {
throw new UnsupportedOperationException("Not supported yet.");
}
public void ajaxInvokeOnGet(Map<String, String> params, FacesContext fc) {
// 我们将实现这个方法
}
@Override
public void encodeBegin(FacesContext fc) throws IOException {
String clientId = this.getClientId(fc); // 获得组件的客户端ID
ResponseWriter rw = fc.getResponseWriter();
rw.startElement("input", this);
rw.writeAttribute("id", clientId, null);
rw.writeAttribute("name", clientId, null);
rw.writeAttribute("type", "text", null);
rw.endElement("input");
}
}
你可以看到我们实现的encodeBegin方法,实际上只是在客户端渲染一个input而已,如果渲染正常,应该在html代码中输出像这样的东西:
<input
type=”text” id=”…” name=”…” />
接着我们先确保这个组件能在页面中正常渲染并显示,所以我们先注册一下这个组件,并看一下效果,后面再实现组件的Ajax功能.
步骤2.创建UIComponentTag类 – HtmlHelloTag.java
public class HtmlHelloTag extends UIComponentELTag{
@Override
public String getComponentType() {
return "demo.component.HtmlHello";
}
@Override
public String getRendererType() {
return null;
}
}
这一步中我们只是创建了一个java类并扩展UIComponentELTag, 覆盖了getComponentType, 这个方法会告诉JSP使用哪一个主类来处理页面标签.类似于一个桥梁作用,后面会看到这是如何联系到我们的主类HtmlHello.java的.
getRendererType方法只返回null, 因为我们的组件主类HtmlHello会自行渲染.所以不需要其它渲染类.
好了,HtmlHelloTag.java就这样简单,我们暂时不需要其他属性(id与rendered属性默认已经有了,我们只要在tld文件中登记一下就可以),接着我们来将这个HtmlHelloTag注册到tld文件中.
步骤3.创建TLD标签库描述符文件 – MyComponent.tld
<?xml version="1.0" encoding="UTF-8"?>
<taglib xsi:schemaLocation="
http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-jsptaglibrary_2_1.xsd"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
version="2.1">
<description>在这里登记标签库的名称,版本,以及页面引用时指定的uri</description>
<display-name>MyComponents</display-name>
<short-name>mc</short-name>
<tlib-version>1.0</tlib-version>
<uri>http://mycomponents/demo</uri>
<tag>
<name>hello</name>
<tag-class>
demo.component.HtmlHelloTag
</tag-class>
<attribute>
<name>id</name>
</attribute>
<attribute>
<name>rendered</name>
<deferred-value>
<type>java.lang.Boolean</type>
</deferred-value>
</attribute>
</tag>
</taglib>
MyComponent.tld这个文件的名称并没有什么特殊的或需要约定的规则,把它直接放在WEB-INF文件夹下就可以了,JSF会自己去找到它.
你完全可以直接复制这个文件来进行修改,这里指定了组件库的名称,版本,引用时的uri,需要注意的是uri中指定的http://mycomponents/demo可以自己指定,但要与JSP页面引用时一致如:<%@taglib
prefix="mc" uri="http://mycomponents/demo"%>
另外,你可以在<tag>中看到我们将HtmlHelloTag注册到了这个tld描述文件,<tag-class>中我们指定了标签处理类的完全限定类名,<name>中我们给这个标签指定了一个名称:hello, 后面我们可以这样使用这个标签组件:<mc:hello />,同时我们把id与rendered属性登记了上去,这是两个基本属性,每个扩展自UIComponentELTag的类都会处理,所以这里登记一下就可以了。
处理完UIComponentTag与tld文件之后,我们只要再一步将自己的Component主类注册到faces-config.xml文件中就可以了。
步骤4.将组件注册到faces-config.xml
<faces-config version="1.2"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_1_2.xsd">
<component>
<component-type>demo.component.HtmlHello</component-type>
<component-class>
demo.component.HtmlHello
</component-class>
</component>
</faces-config>
在faces-config.xml文件中,我们注册了前面创建的组件HtmlHello.
<component-class>中指定了这个组件的主类的完全限定类名,<component-type>指定了这个组件的类型,实际上你可以使用其它自定的component-type,但是这个名称必须与HtmlHelloTag.java中的方法:getComponentType所返回的名称是一致的:
public String
getComponentType() {
return "demo.component.HtmlHello";
}
HtmlHelloTag.java就是这样联系到这个组件的主类的。
提示:你可以自己新创建一个faces-config.xml文件或使用JSF自己的faces-config.xml来注册你的新组件,如果另外创建一个faces-config.xml,那么你需要在web.xml中登记这个文件的路径。
步骤5.先测试一下这个组件
接着,我们先创建一个jsp页面HelloDemo.jsp来测试一下新创建的组件,
然后引入我们自定义的标签库,uri需要与我们在tld中定义的相同。
<%@taglib prefix="mc"
uri="http://mycomponents/demo"%>
这时当我们打入“<m”的时候jsp已经有了新定义组件的一些提示,下面看一下组件是否可以正常显示,以下是HelloDemo.jsp的代码:
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<%@taglib prefix="f" uri="http://java.sun.com/jsf/core"%>
<%@taglib prefix="h" uri="http://java.sun.com/jsf/html"%>
<%@taglib prefix="mc" uri="http://mycomponents/demo"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<f:view>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Demo</title>
</head>
<body>
<h2>Hello Demo</h2>
<mc:hello />
<%@ include file="../back.jsp" %>
</body>
</html>
</f:view>
可以看到组件已经可以正常的显示了, 简单的显示了一个input框,虽然确实非常简单,并且几乎没有一点作用。不过这基本上就是一个标准JSF组件的创建过程,知道了原理之后我们就可以扩展出更多更高级的功能了。如果你只需要一个标准组件,那么到这一步就可以了.
接下来我们将使用QFaces给组件增加Ajax的功能,提高用户体验。
步骤6.Ajax扩展 - 创建javascript文件 HtmlHello.js
先创建一个javascript文件:HtmlHello.js 为了方便起见,在这里将它创建在了component包中,与组件的主类等放在一起,这也有利于以后将组件打包成jar,方便部署.
接着开始编写这个js脚本,敲入如下的ajax请求代码,后面你会知道组件是如何触发这个脚本的.
function qfaces_demo_hello(clientId, componentClass) {
var obj = QFaces.getObj(clientId); // 获取QFacesObject对象
var hello = QFaces.getComponent(clientId); // 获取组件
obj.put("componentClass", componentClass); // 这个参数是组件get请求必要的
obj.put("myname", hello.value); // 这个参数是我们自己要进行传递的参数
var process = function () { // 定义回调处理函数
alert(obj.request.responseText);
}
obj.setProcess(process); // 设置回调处理函数
obj.get(); // 发送请求(get方式,必须传递参数componentClass)
}
一些解释:
QFaces是我自定义的一个javascript类,可以方便处理ajax请求,已经简化掉了很多几乎没有作用,或经常重复多余的代码.这个类是如何引入的,后面会在组件的渲染中介绍.(你可以直接解开QFaces的jar文件,从中找到这个js进行一些修改或许对你有用)
clientId组件的客户端id, 查看html源码,可以看到jsf组件的客户端id都像这样 xxx:xxx:xxx
componentClass 组件的java类的完全限定类名.
后面在组件的渲染中,我们将传递这两个值给这个js函数.下面是QFaces及QFacesObject类的解释:
1.
QFaces.getObj(clientId):可以获取一个处理这次请求的js对象QFacesObject,为了避免当页面同时存在多个hello组件时发生全局的XMLHttpRequest冲突,所以传递了组件唯一的clientId, 并且在整个请求过程中,你仍然可以通过clientId在其它方法中获得这个QFacesObject,它是全局的,里面包含了一些有用的东西,如XMLHttpRequest对象,下面是QFacesObject的一些有用的东西:
2.
put()方法:可以很方便的设置传递参数,经过改造看起来很像java Map类的put方法.
3.
setProcess(function):设置回调时的处理函数,在这里我们直接将Ajax带回来的数据alert出来.
4.
get() 与 post()方法:这两个方法分别表示了get请求与post请求.但是有一些不同,使用get方法必需要传递参数”componentClass”即组件的完全限定类名.而post方法必需要传递”clientId”即组件的客户端id.
因为get方法中框架使用了反射机制处理请求,所以需要知道是哪一个组件类发起的请求,并调用相应的处理方法(ajaxInvokeOnGet),但是Post方法却是通过clientId来查找组件,并调用相应的处理方法(ajaxInvokeOnPost).
Post方法性能较差,因为他会传递整个组件树的状态信息,并进行恢复视图等处理.在我的多次测试中,这个阶段几乎至少要花掉约20毫秒的时间,如果页面视图足够复杂可能花费的时间更多,但是Get方法不需要这个阶段,它不需要跑任何生命周期,就好像你写servlet或Filter来处理这个请求一样.
但是在某些时候post方法非常有用,他可以访问到整个组件树中的所有组件.QFaces.jar包中的validator组件使用的就是这个方式,它需要访问目标组件的value,validator,convert等属性,
尽管post方式性能稍差,但是QFaces中还是帮你省下了JSF生命周期中的后面5个阶段.所以性能还是有所保障的.
5.
QFaces.getComponent(“id”) 获取页面组件,为什么不使用document.getElementById(“id”)?
有时候你可能会发现某些浏览器下无法正常通过id获取页面组件,所以定义了这个方法,更好的兼容多浏览器.
步骤7.Ajax扩展 - 让组件UIComponent触发js
现在开始升级一下前面创建的组件主类HtmlHello.java,让它能触发js调用,并进行ajax请求.
public class HtmlHello extends UIComponentBase implements Ajax{
@Override
public String getFamily() {
return null;
}
public void ajaxInvokeOnPost(Map<String, String> params, FacesContext fc) {
throw new UnsupportedOperationException("Not supported yet.");
}
public void ajaxInvokeOnGet(Map<String, String> params, FacesContext fc) {
// 我们将实现这个方法
}
@Override
public void encodeBegin(FacesContext fc) throws IOException {
QFaces.loadRequired(); // 装载必要的资源如QFaces.js
QFaces.loadJavascript("demo/component/HtmlHello.js", this); // 装载自定义的js
String componentClass = this.getClass().getName(); // 获取组件的完全限定类名
String clientId = this.getClientId(fc); // 获得组件的客户端ID
// 组织js函数用于组件的onblur - javascript:qfaces_demo_hello("…", "…");
StringBuilder onblur = new StringBuilder("javascript:qfaces_demo_hello(");
onblur.append("'").append(clientId).append("'");
onblur.append(",'").append(componentClass).append("'");
onblur.append(")");
ResponseWriter rw = fc.getResponseWriter();
rw.startElement("input", this);
rw.writeAttribute("id", clientId, null);
rw.writeAttribute("name", clientId, null);
rw.writeAttribute("type", "text", null);
rw.writeAttribute("onblur", onblur, null);
rw.endElement("input");
}
}
你可以看到升级的encodeBegin方法在渲染组件之前装载了QFaces必要的js资源及我们自定义的HtmlHello.js资源(注意资源路径是"/"而非"."符号), 并且给组件增加了一个onblur属性, 这个就是调用我们新定义的js函数的属性了:qfaces_demo_hello(clientId,componentClass)
你可能会担心多个组件同时在页面的时候会导致重复加载js资源的问题,不过这个问题QFaces已经作了处理.
现在先来测试一下组件是否可以正常的触发这个js函数吧,稍微修改一下:qfaces_demo_hello:
function qfaces_demo_hello(clientId, componentClass) {
alert("clientId=" + clientId + "\ncomponentClass=" + componentClass);
// var obj = QFaces.getObj(clientId);
// var hello = QFaces.getComponent(clientId);
// obj.put("componentClass", componentClass);
// obj.put("myname", hello.value);
// var process = function () {
// alert(obj.request.responseText);
// }
// obj.setProcess(process);
// obj.get();
}
可以看到正常的触发了这个函数,显示了组件传递给js的clientId及componentClass
现在重新修改回正常的js请求,并进行下一步Ajax请求的最终应答处理:
function qfaces_demo_hello(clientId, componentClass) {
var obj = QFaces.getObj(clientId);
var hello = QFaces.getComponent(clientId);
obj.put("componentClass", componentClass);
obj.put("myname", hello.value);
var process = function () {
alert(obj.request.responseText);
}
obj.setProcess(process);
obj.get();
}
步骤8.Ajax扩展 - 让组件UIComponent响应Ajax请求
现在我们开始处理Ajax请求的响应,回到组件主类HtmlHello.java来实现最初的方法ajaxInvokeOnGet, 下面是响应的方法实现:
public void
ajaxInvokeOnGet(Map<String, String> params, FacesContext fc) {
String myname =
params.get("myname");
String message = "Hello, welcome to use QFaces! " + myname;
QFaces.ResponseHTML(fc, message);
}
你可以看到这个方法很简单,取回了我们通过js发送过来的参数myname的值,并组织新值message,然后使用QFaces的ResponseHTML方法直接响应这个字符串.当然你也可以从FacesContext中获得response对象进行自定义响应.现在看一下最终效果截图:
在输入了一些信息之后,焦点离开组件,弹出了我们最终想要的效果!这已经是一个完整的带有Ajax功能的组件了.但它的复用性并不好,因为我们硬编码了Ajax响应.接下来的"进一步升级"会让组件支持BackingBean方法绑定.使它可以进行重用.如果你有兴趣就继续往下看.
3. 进一步升级 - 创建一个可重用的Ajax组件
现在先简单回顾一下上面整个组件的制作过程,及处理流程:
制作过程:
1.创建组件主类HtmlHello.java ->
2.创建标签属性处理类HtmlHelloTag.java ->
3.创建tld标签描述符定义标签库,并将HtmlHelloTag.java注册到其中 ->
4.创建faces-config.xml(可以直接使用jsf自带的),并将组件注册到其中. ->
5.测试组件可以正常渲染并运行 ->
6.(升级)创建js文件HtmlHello.js ->
7.(升级)让组件能装载js并触发js的ajax请求 ->
8.(升级)让组件响应Ajax请求
处理流程:
1.JSP页面发现组件标签<mc:hello /> ->
2.JSF根据tld中定义的Tag类HtmlHelloTag.java来设置相应标签属性的值 ->
3.HtmlHelloTag.java根据getComponentType的返回值确定组件的主处理类的类型 ->
4.HtmlHelloTag.java将相应的属性和值绑定等赋给组件主类 ->
5.组件主类HtmlHello.java开始渲染自己(1.加载QFaces.js,
HtmlHello.js 2.渲染一个input)
->
6.页面逞现,input输入信息并离开焦点后触发js
: qfaces_demo_hello进行Ajax请求 ->
7.QFaces根据请求的参数componentClass确定相应的处理类HtmlHello.java
8.QFaces调用HtmlHello.java的ajaxInvokeOnGet进行Ajax的响应.
现在开始升级方法绑定,为了节省篇幅,后面的叙述及部骤可能不会太过详细,但代码仍是完整的.我们将使组件可以绑定一个这样的方法:
java.lang.String
showMessage(java.lang.String)
这个方法将获取用户输入组件的值,同时用Ajax返回一些提示或验证信息,使用方法像这样
<mc:hello
showMessage=”#{DemoBean.showMessage}” />
1.
升级MyComponent.tld
<?xml version="1.0" encoding="UTF-8"?>
<taglib xsi:schemaLocation="
http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-jsptaglibrary_2_1.xsd"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
version="2.1">
<description>在这里登记标签库的名称,版本,以及页面引用时指定的uri</description>
<display-name>MyComponents</display-name>
<short-name>mc</short-name>
<tlib-version>1.0</tlib-version>
<uri>http://mycomponents/demo</uri>
<tag>
<name>hello</name>
<tag-class>
demo.component.HtmlHelloTag
</tag-class>
<attribute>
<name>id</name>
</attribute>
<attribute>
<name>rendered</name>
<deferred-value>
<type>java.lang.Boolean</type>
</deferred-value>
</attribute>
<attribute>
<name>showMessage</name>
<deferred-method>
<method-signature>
java.lang.String showMessage(java.lang.String)
</method-signature>
</deferred-method>
</attribute>
</tag>
</taglib>
现在我们在tld文件中注册了一个新的属性showMessage,他绑定了一个这样的方法:
java.lang.String
showMessage(java.lang.String)
2.
升级组件主类HtmlHello.java的渲染
public class HtmlHello extends UIComponentBase implements Ajax{
@Override
public String getFamily() {
return null;
}
public void ajaxInvokeOnPost(Map<String, String> params, FacesContext fc) {
throw new UnsupportedOperationException("Not supported yet.");
}
public void ajaxInvokeOnGet(Map<String, String> params, FacesContext fc) {
String myname = params.get("myname");
String message = "Hello, welcome to use QFaces! " + myname;
QFaces.ResponseHTML(fc, message);
}
@Override
public void encodeBegin(FacesContext fc) throws IOException {
QFaces.loadRequired();
QFaces.loadJavascript("demo/component/HtmlHello.js", this);
String componentClass = this.getClass().getName();
String clientId = this.getClientId(fc);
// 取得方法绑定的表达式
String messageExp = (this.showMessage != null ?
this.showMessage.getExpressionString() : null);
// 组织js函数用于组件的onblur:javascript:qfaces_demo_hello("", "", "");
StringBuilder onblur = new StringBuilder("javascript:qfaces_demo_hello(");
onblur.append("'").append(clientId).append("'");
onblur.append(",'").append(componentClass).append("'");
onblur.append(",'").append(messageExp).append("'"); // 增加传递的参数
onblur.append(")");
ResponseWriter rw = fc.getResponseWriter();
rw.startElement("input", this);
rw.writeAttribute("id", clientId, null);
rw.writeAttribute("name", clientId, null);
rw.writeAttribute("type", "text", null);
rw.writeAttribute("onblur", onblur, null);
rw.endElement("input");
}
private MethodExpression showMessage;
public void setShowMessage(MethodExpression showMessage) {
this.showMessage = showMessage;
}
}
3.
升级 HtmlHelloTag.java
public class HtmlHelloTag extends UIComponentELTag{
@Override
public String getComponentType() {
return "demo.component.HtmlHello";
}
@Override
public String getRendererType() {
return null;
}
@Override
protected void setProperties(UIComponent ui) {
super.setProperties(ui);
if (ui instanceof demo.component.HtmlHello) { // 设置组件的方法绑定
((HtmlHello) ui).setShowMessage(showMessage);
}
}
@Override
public void release() {
super.release();
this.showMessage = null; // 释放资源
}
private MethodExpression showMessage;
public void setShowMessage(MethodExpression showMessage) {
this.showMessage = showMessage;
}
}
4.
升级 HtmlHello.js
function qfaces_demo_hello(clientId, componentClass, messageExp) {
var obj = QFaces.getObj(clientId);
var hello = QFaces.getComponent(clientId);
obj.put("componentClass", componentClass);
obj.put("myname", hello.value);
obj.put("messageExp", messageExp); // 传递方法绑定的表达式
var process = function () {
alert(obj.request.responseText);
}
obj.setProcess(process);
obj.get();
}
5.
升级组件主类的响应处理
现在升级一下主类的ajaxInvokeOnGet使它能处理方法绑定.
public void ajaxInvokeOnGet(Map<String, String> params, FacesContext fc) {
String myname = params.get("myname");
String messageExp = params.get("messageExp"); // 取回方法绑定的表达式
MethodExpression me = QFaces.createMethodExpression(messageExp,
String.class, new Class[]{String.class}); // 重建这个方法绑定
String result = (String) me.invoke(fc.getELContext(), new Object[]{myname}); // 调用
QFaces.ResponseHTML(fc, result); // 输出响应
}
6.
新建BackingBean – DemoBean.java
现在新建一个backingBean
– DemoBean.java 并增加一个方法进行测试
public class DemoBean {
public String showMessage(String myname) {
String message = "你好, 很高兴见到你! " + myname;
return message;
}
}
7.
最终测试
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<%@taglib prefix="f" uri="http://java.sun.com/jsf/core"%>
<%@taglib prefix="h" uri="http://java.sun.com/jsf/html"%>
<%@taglib prefix="mc" uri="http://mycomponents/demo"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<f:view>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Demo</title>
</head>
<body>
<h2>Hello Demo</h2>
<mc:hello showMessage="#{DemoBean.showMessage}" />
<%@ include file="../back.jsp" %>
</body>
</html>
</f:view>
现在输入信息并onblur之后你可以看到更好的响应输出了,组件支持了方法绑定,重用程度变得更好!
好了就写到这里吧.更多的值绑定,状态保存,validator...等等.还是需要自己更深入的了解JSF,当能够自如的了解并制作组件这后,我相信你应该也会觉得它确实很好用的. 实际上这也并没有什么高深的知识,任何复杂的问题都是由日常生活中非常简单的道理构成的.只要知道了原理,再深入研究,一切都会变得很简单.这与其它行业不同,比如重工业,你知道了某些原理,没有一些资金支持,购买硬件设备供研究的话也是白搭,
但编程却不同,几乎只要一台电脑就什么都有可能,呵呵! 这也是编程诱人的地方.
这篇文章仅供学jsf组件制作的朋友参考,如果你已经是高手,或者你发现有什么不对的地方欢迎拍砖,批评指正,共同交流学习!QQ: 31703299
QFaces相关完整示例下载
- huliqing@huliqing.name
- http://www.huliqing.name