核心提示:Intraweb一直是Delphi快速开发web应用的首选工具,但自带的控件较少,样式比较难看,TMS与IW倒是可用,可是要收费,对于我们这些习惯用免费的用户来说,想找个破解也比较费劲。EasyUI是基于JQuery开发的框架,内置的控件完全可以满足我们开发一般web程序的需求,而且是免费的,用起来...
Intraweb一直是Delphi快速开发web应用的首选工具,但自带的控件较少,样式比较难看,TMS与IW倒是可用,可是要收费,对于我们这些习
惯用免费的用户来说,想找个破解也比较费劲。EasyUI是基于JQuery开发的框架,内置的控件完全可以满足我们开发一般web程序的需求,而且是免
费的,用起来也心安理得。下面我就IW如何结合EasyUI开发程序谈谈自己的一些心得,与大家交流一下。主要有以下几种方法:一、使用模板
在IWForm内使用模板引入做好的html文件,结合IW自身的控件进行操控。这种方法虽说比较方便,但模板也有自身的缺点,内部不支持中文引用是 一大Bug,目前IW都没有要解决的迹像。如果一定要用模板,也有方法规避,即汉字全部用网页转义“&#”加汉字的十进制编码。模板的使用有很多 文章可供参考,也不是本章的重点,不做具体讲解。
二、MVC设计模式
IW使用MVC方式结合EasyUI设计程序,是本文的重点。我们知道IW与HTML静态页面的交互,可以通过javascript接口来实现,可以 使用AddToInitProc('alert("欢迎")')这样的语句,也可以在控件的JavascriptEvent属性内添加js语句。本文介绍 的方法完全将界面与数据处理分开,一律使用EasyUI来实现界面(完全不用IW的可视控件,数据库控件还是需要的),数据处理交给IW后台做。我们以开 发一个简单的应用程序来一步步实现这些功能,同时会使用一定篇幅介绍EasyUI部分控件的使用(本文必须要有一定的javascript基础)。
第一步:实现登陆界面。
首先引入以下文件,后面其它页面也一样这样引用,直接贴代码:
- <span style="white-space:pre"> </span><link rel="stylesheet" type="text/css" href="easyui/themes/default/easyui.css">
- <link rel="stylesheet" type="text/css" href="easyui/themes/icon.css">
- <script type="text/javascript" src="easyui/jquery.min.js"></script>
- <script type="text/javascript" src="easyui/jquery.easyui.min.js"></script>
- <script type="text/javascript" src="easyui/locale/easyui-lang-zh_CN.js"></script>
- <form id="ff" class="easyui-form" method="post" data-options="novalidate:true">
- <!--form提供了各种方法来操作执行表单字段,比如:ajax提交, load, clear等等。当提交表单的时候可以调用validate方法检查表单是否有效。
- “data-options”控件的各种属性,form有以下属性:
- 属性名 类型 描述 默认值
- novalidate boolean 定义是否验证表单的字段,true:验证,false:不验证。 false
- ajax boolean 定义是否使用ajax提交表单,true:使用,false:不使用。 true
- queryParams object 当表单被提交到服务器的时候增加的额外参数列表。 {}
- url string 提交表单动作的URL地址 null
- -->
- <table cellpadding="5">
- <tr>
- <td>用户名:</td>
- <td><input class="easyui-textbox" type="text" name="username" data-options="required:true" style="width:150px"/></td>
- <!--
- TextBox(文本框)是一个增强的输入字段组件, 它允许用户非常简单的创建一组表单。它是一个用于构建其他组合控件的基础组件,如:combo,databox、spinner等
- required:true表示文本框不能为空,下同。
- -->
- </tr>
- <tr>
- <td>密 码:</td>
- <td><input class="easyui-textbox" type="password" name="passw" data-options="required:true" style="width:150px"/></td>
- </tr>
- </table>
- </form>
- <div style="width:216px;padding:5px 0px;height:30px">
- <a href="javascript:void(0)" class="easyui-linkbutton" onclick="submitForm()" style="width:80px;float:left">登陆</a>
- <!--easyui-linkbutton按钮组件,使用超链接按钮创建,提示:不要将它改为button类别,IE9以下浏览器会不正常,submitForm()提交数据,clearForm()清空数据-->
- <a href="javascript:void(0)" class="easyui-linkbutton" onclick="clearForm()" style="width:80px;float:right">取消</a>
- </div>
- </div>
- </div>
- function submitForm(){
- $('#ff').form('submit',{//这是EasyUI的Form自带功能,就是提交数据
- url:'Login.php', //需要把数据提交到的页面
- onSubmit:function(){//验证数据是否为空,如果为空就返回。
- return $(this).form('enableValidation').form('validate');
- },
- success: function(data){
- //提交成功后的回调函数,data就是返回的数据
- if(parseInt(data)==1)
- //我们在这里返回1和0,1表示成功登陆,在后台实现
- {
- window.location='main.html';
- //登陆成功后,跳转到主程序
- }
- else
- {
- $.messager.alert('错误','用户名或密码错误!','error');
- /*EasyUI消息提示框,就是alert的改进用法,显示警告窗口。
- 参数(依次调用):
- title:在头部面板显示的标题文本。
- msg:显示的消息文本。
- icon:显示的图标图像。可用值有:error,question,info,warning。
- fn: 在窗口关闭的时候触发该回调函数。 */
- $('#ff').form('clear');//清空数据,下同。
- }
- }
- });
- }
- function clearForm(){
- $('#ff').form('clear');
- }
登陆界面基本完成,数据需要提交到“Login.php”这个页面,按一般的做的法,新建一个IWForm,使用模板加载文件,本文用另一种思路,也是本文的关键:
用delphi新建一个Unit,命名Login单元,加入IW工程。
直接贴出代码(参考万一博客):
- {新建Login 单元, 从 TContentBase 继承实现一个 TLogin 类}
- unit Login;
- interface
- uses Classes, IW.Content.Base, System.SysUtils,HTTPApp, IWApplication, IW.HTTP.Request, IW.HTTP.Reply, IWMimeTypes;
- type
- TLogin = class(TContentBase)
- protected
- function Execute(aRequest: THttpRequest; aReply: THttpReply; const aPathname: string; aSession: TIWApplication; aParams: TStrings): Boolean; override;
- public
- constructor Create; override;
- end;
- implementation
- uses ServerController,UserSessionUnit;
- { TLogin }
- constructor TLogin.Create;
- begin
- inherited;
- mFileMustExist := False;
- end;
- function TLogin.Execute(aRequest: THttpRequest; aReply: THttpReply; const aPathname: string; aSession: TIWApplication; aParams: TStrings): Boolean;
- begin
- aReply.ContentType := MIME_HTML;
- aReply.WriteString('这里就是返回到客户端的数据');
- Result := True;
- end;
- end.
- {在 IWServerControllerBase.OnConfig 映射login.php}
- uses
- IWInit, IWGlobal, IW.Content.Handlers, Login;
- procedure TIWServerController.IWServerControllerBaseConfig(Sender: TObject);
- begin
- THandlers.Add('', 'login.php', TLogin.Create);
- //添加虚拟文件名,映射到服务器
- end;
客户端通过Form提交用户名和密码到“Login.php”,“Login.php”是通过服务器添加的一个虚拟文件,映射到从 TContentBase继承实现的TLogin类,用THttpRequest接收提交的数据,并进行处理,用 THttpReply.writestring写入返回客户端数据。这样登陆过程前台与后台代码均完成。
第二步:实现主界面
我们开发的是一个商品信息管理程序,主界面用EasyUI的Layout实现自适应浏览器(记得引入相关js和css):
- <div data-options="region:'north',border:false" style="height:60px;background:#B3DFDA;padding:0px 10px;text-align:center">
- <h3>商品信息管理系统</h3>
- </div>
- <div data-options="region:'west',split:true,title:'商品分类'" style="width:200px;padding:10px;">
- </div>
- <!--<div data-options="region:'east',split:true,collapsed:true,title:'East'" style="width:100px;padding:10px;">east region</div>-->
- <div data-options="region:'south',border:false" style="height:50px;background:#A9FACD;padding:10px;">
- 京ICP证000000号
- </div>
- <div data-options="region:'center',title:'商品简要信息'">
- </div>
很好理解,即左西右东,上北下南加上中央的布局,右边不需要,我把它注释掉。
页面设计思路是这样的,左边放一个Tree,用来显示商品分类,中央放GridData,用来显示商品信息列表,通过两个控件实现删除、添加、修改功能。west这个DIV内加入Tree:
- <ul id="easyui_tt" class="easyui-tree"
- data-options="
- animate:true,//动画
- lines:true,//显示树线
- url:'Treedata.php',//上面有解释,需要提交的页面
- method:'post',//提交方式Post,再强调一下必须用Post
- onClick: function(node){//鼠标单击事件
- QueryByID(node.id);//通过node.id来查询数据,讲DataGrid时再说
- },
- onContextMenu: function(e, node){//右键菜单
- e.preventDefault();//必须用的
- $(this).tree('select', node.target);//选择的Node
- $('#mm').menu('show', {//EasyUI的菜单,非常简单
- left: e.pageX,//弹出菜单的位置
- top: e.pageY
- });
- }">
- </ul>
id:节点ID,对加载远程数据很重要。
text:显示节点文本。
state:节点状态,'open' 或 'closed',默认:'open'。
如果为'closed'的时候,将不自动展开该节点。
checked:表示该节点是否被选中。
attributes: 被添加到节点的自定义属性。
children: 一个节点数组声明了若干节点
Tree的节点是通过url提交请求到服务器接收返回数据加载的,形成
树的数据是JSon格式,我们可以分析一下:
- [{
- "id": 1,//对应node的ID,其他也是一一对应的
- "text": "Node 1",
- "state": "closed",
- "children": [{ //子node
- "id": 11,
- "text": "Node 11"
- },{
- "id": 12,
- "text": "Node 12"
- }]
- },{
- "id": 2,
- "text": "Node 2",
- "state": "closed" //不展开节点
- }]}
不管通过什么语言动态实现Tree,都是非常麻烦的一件事,EasyUI的例子只能实现两层树。从数据库读取Tree数据,在数据库设计的时候有一个技 巧,不知道大家是怎样处理的,我这里说一个我的方法:树的上下级之间用代码表示,2位数字代表根,4位数字代表下一级,依此类推,代码不能用纯数字,这样 不好排序,我在数字前加个字母,这样通过“select*from Tree order by id”就可以把上下级排列在一起,而不是按代码大小排序。数据库就不多讲了,不在本文的范围,大家看一下我的源码里的数据库就知道了。建树代码如下(本想 用JSon,无奈学不到家,只能用字符串拼接):
- function BuildTree:string;
- var
- i,j,old_ln,new_ln:Integer;
- id,s,title,ft:string;
- begin
- ft:='{"id":"%s","text":"%s"},';//Json格式
- with UserSession.FDQuery1 do
- begin
- Open('select*from Tree order by id'); //按id排序可以将父子节点正好罗列在一起
- s:='[';
- old_ln:=0;//初始化开始节点ID的长度
- for i := 0 to RecordCount-1 do
- begin
- id:=Fields.Fields[0].AsString;
- title:=Fields.Fields[1].AsString;
- new_ln:=id.Length-3;//新节点ID的长度,减去3除去了根节点的长度,方便计算
- //通过比较与上一节点ID的长度来判断节点的上下级关系
- if (new_ln=old_ln) then//与上一节点同等级
- s:=s+Format(ft,[id,title]);
- if new_ln>old_ln then //上一节点为父节点
- begin
- s:=s.Substring(0,s.Length-2);
- s:=s+Format(',"state":"closed","children":['+ft,[id,title]);
- end;
- if (new_ln<old_ln) then //上一节点为子节点
- begin
- s:=s.Substring(0,s.Length-1);
- for j :=1 to (old_ln-new_ln) div 2 do
- s:=s+']}';
- s:=s+Format(','+ft,[id,title]);
- end;
- Next;
- old_ln:=new_ln;//将当前节点ID长度赋予旧节点
- end;
- end;
- s:=s.Substring(0,s.Length-1);
- for i := 1 to new_ln div 2 do //结束时需要判断是否为子节点,有几层
- s:=s+']}';
- result:=s+']';
- end;
- onContextMenu: function(e, node){
- e.preventDefault();
- $(this).tree('select', node.target);
- $('#mm').menu('show', {
- left: e.pageX,
- top: e.pageY
- });
- <div id="mm" class="easyui-menu" style="width:120px;">
- <div onclick="addnode()" data-options="iconCls:'icon-add'">添加</div>
- <div onclick="removeit()" data-options="iconCls:'icon-remove'">删除</div>
- </div>
提示:EasyUI很多情况下只需要引用$('')类似的标识就可以将其他控件加进去。实现addnode()、removeit()以及其他功能:
- <span style="white-space:pre"> </span>function appendn(r){//添加节点
- var t = $('#easyui_tt');
- var node =t.tree('getSelected');
- var pii=node.id;
- $.ajax({
- type : "post",
- url : "Treedata.php",
- data : {Action:'Add',ID:pii,Title:r},
- async : false,//这里必须用同步
- success : function(data){
- pii=data;
- }
- });
- t.tree('append', {
- parent: (node?node.target:null),
- data: [{id:pii,text:r}]
- });
- }
- function removeit(){//删除节点
- var node = $('#easyui_tt').tree('getSelected');
- var pii=node.id;
- $.post('Treedata.php',{Action:'Del',ID:pii});
- $('#easyui_tt').tree('remove', node.target);
- }
- function collapse(){//树折叠
- var node = $('#easyui_tt').tree('getSelected');
- $('#easyui_tt').tree('collapse',node.target);
- }
- function expand(){//树展开
- var node = $('#easyui_tt').tree('getSelected');
- $('#easyui_tt').tree('expand',node.target);
- }
- function addnode(){//弹出添加节点对话框,用消息框
- $.messager.prompt('添加', '请输入需要添加的名称:', function(r){
- if (r){
- appendn(r);
- }
- });
- }
- function TestPost(){
- var mydata="TestMYPost测试一下";
- executeAjaxEvent("&data="+mydata, null, "DoCallBack1", false, null, false);
- //中文在IE下乱码
- }
- $.post(GURLBase+"callback?",
- {callback:"DoCallBack1",data:"测试一下可以吗-----?"},
- function(data){processAjaxResponse(data);},"xml");//必须是xml格式
- }//效果是一样的,也需要注册回调函数
- function TestPost(){
- var mydata=escape("TestMYPost测试一下");
- executeAjaxEvent("&data="+mydata, null, "DoCallBack1", false, null, false);//中文在IE下乱码,需要escape
- }
服务器如何处理数据,登陆界面已经详解,基本类似,添加删除也不再列代码,大家可以直接看我的源码,用delphi实现真的很简易。
Tree讲完,我们接着讲DataGrid:
center这个DIV内加入:
- <table class="easyui-datagrid" style="width:100%;height:400px"
- data-options="singleSelect:true,collapsible:true,fitColumns:true,url:'GridData.php',
- method:'post',pageSize:10,pagination:true,onDblClickRow:onDClickRow"
- <!--
- 相同的属性不再说明,
- singleSelect选择单行
- collapsible定义是否显示可折叠按钮,EasyUI大部分控件继承自panel,一般可折叠
- fitColumns列宽自适应
- pageSize分页时每页显示的行数
- pagination是否分布
- -->
- toolbar="#dg_tb"//工具栏,EasyUI可以这种方式嵌入其他控件
- id="easyui_tb">
- <thead><!--头部-->
- <tr>
- <th data-options="field:'codeID',width:80,halign:'center',editor:'text'">商品编号</th>
- <!--field对应数据库字段
- halign标题居中
- editor:'text'编辑样式为文本框
- align:'center'整列居中
- -->
- <th data-options="field:'p_name',width:100,halign:'center',editor:'text'">名称</th>
- <th data-options="field:'p_type',width:80,halign:'center',align:'center',editor:'text'">型号</th>
- <th data-options="field:'p_tid',width:80,halign:'center',align:'center',editor:'text'">类别</th>
- <th data-options="field:'p_pinpai',halign:'center',width:250,editor:'text'">品牌</th>
- <th data-options="field:'p_price',halign:'center',width:60,align:'center',editor:'text'">价格</th>
- <th data-options="field:'p_discount',halign:'center',width:60,align:'center',editor:'text'">折扣</th>
- </tr>
- </thead>
- </table>
- <div id="dg_tb" style="padding:3px"><!--工具栏-->
- <span>商品编号</span>
- <input id="codeID" class="easyui-numberbox" style="line-height:22px;border:1px solid #ccc">
- <span>商品名称</span>
- <input id="p_name" class="easyui-textbox" style="line-height:22px;border:1px solid #ccc">
- <a href="#" class="easyui-linkbutton" plain="true" onclick="doSearch()">查询</a>
- <a href="javascript:void(0)" class="easyui-linkbutton" data-options="iconCls:'icon-add',plain:true" onclick="appendr();">添加</a>
- <a href="javascript:void(0)" class="easyui-linkbutton" data-options="iconCls:'icon-remove',plain:true" onclick="remover()">删除</a>
- <a href="javascript:void(0)" class="easyui-linkbutton" data-options="iconCls:'icon-save',plain:true" onclick="acceptr()">修改</a>
- <a href="javascript:void(0)" class="easyui-linkbutton" data-options="iconCls:'icon-undo',plain:true" onclick="rejectr()">撤消</a>
- </div>
- function QueryByID(id){//以节点ID查询
- var tb=$('#easyui_tb');
- tb.datagrid({queryParams:{Action:'Q_ID',ID:id}});
- /*queryParams是DataGrid提交数据时的参数,
- 也可以直接:tb.datagrid('load',:{Action:'Q_ID',ID:id}});
- 但是在测试中发现,这样提交后数据为空时,表仍然显示有数据,
- 也许是Bug,也许是我不会用。load即是post数据到服务器,同时
- 接收返回数据,GridData全部封闭好了。
- */
- tb.datagrid('load');
- }
以ID查询数据在服务器端这样实现:
- function QueryData(config:string):string;
- var
- arrjson:JSONArray;
- ajson:JSONObject;
- i,j:integer;
- begin
- arrjson:=JSONArray.Create;
- ajson:=JSONObject.Create;
- with UserSession.FDQuery1 do
- begin
- Open('select*from product where '+config);
- for I :=0 to RecordCount-1 do
- begin
- for j := 0 to Fields.Count-1 do
- ajson.Put(Fields.Fields[j].DisplayName,Fields.Fields[j].AsString);
- //形成'{aaa:"BBB",ccc:"DDDD"}'这样的字符串,不需要拼接字符串了。
- arrjson.AddJSON(ajson.ToString(4));
- //字面上理解就是JSon数组,即[{},{}];
- ajson.Clear;
- //清除ajson内的数据,不然ajson会不停put数据,类似js的push用法
- Next;
- end;
- end;
- Result:='{"total":'+i.ToString+',"rows":'+arrjson.ToString(4)+'}';
- //ToString(4)表示以4个空格缩进,不这样使用json数据会被转义
- //datagrid数据多出的total是分页时用到的,表示总行数,rows表示当前显示页面
- //如果不分页,可以直接: Result:=arrjson.ToString(4)';
- arrjson.Free;
- ajson.Free;
- end;
- function doSearch(){//查询功能
- $('#easyui_tb').datagrid('load',{
- Action:'Q_DN',
- id: $('#codeID').val(),
- p_name: $('#p_name').val()
- });}//load参数即可,上面有讲解
- var editIndex = undefined;
- var ExecType='';
- function endEditing(){//结束编辑
- if (editIndex == undefined){return true}
- if ($('#easyui_tb').datagrid('validateRow', editIndex)){
- var ed = $('#easyui_tb').datagrid('getEditor', {index:editIndex,field:'codeID'});
- $('#easyui_tb').datagrid('endEdit', editIndex);
- editIndex = undefined;
- return true;
- } else {
- return false;
- }
- }
- function onDClickRow(index){//双击编辑整行数据
- if (editIndex != index){
- if (endEditing()){
- var tt=$('#easyui_tb').datagrid('selectRow', index);
- var EditID=tt.datagrid('getSelected')['codeID'];//选择行的codeID值
- tt.datagrid('beginEdit', index);
- editIndex = index;
- ExecType='update,'+EditID;//提交到服务器update
- } else {
- $('#easyui_tb').datagrid('selectRow', editIndex);
- //数据库必须依靠主键为标志来更新。
- }
- }
- }
- function appendr(){//添加
- if (endEditing()){
- $('#easyui_tb').datagrid('appendRow',{p_discount:'1.0'});
- editIndex = $('#easyui_tb').datagrid('getRows').length-1;
- $('#easyui_tb').datagrid('selectRow', editIndex)
- .datagrid('beginEdit', editIndex);
- //添加一行空行
- ExecType='insert into,';//提交到服务器insert
- }
- }
- function remover(){//删除
- var tt=$('#easyui_tb').datagrid('getSelected');//找到选择行
- if (tt==undefined) return;
- //没有选择就退出
- var Delindex=$('#easyui_tb').datagrid('getRowIndex',tt);
- //选择行的行号
- var DelID=tt['codeID'];//主键,用于删除
- $.messager.confirm('删除','您确认想要删除记录吗?',
- function(r){ if (r){
- $('#easyui_tb').datagrid('deleteRow', Delindex);
- $.post('GridData.php',{Action:'delete',id:DelID});
- //提交delete
- }});
- editIndex = undefined;//这个本程序没用上,是单击时用的
- }
- function acceptr(){//修改编辑的数据,添加或编辑后,需要修改数据,提交到服务器
- //不修改只是客户端更新,服务器端数据库没变
- if (endEditing()){
- $('#easyui_tb').datagrid('acceptChanges');
- if (ExecType=='') return;
- var selrow=$('#easyui_tb').datagrid('getSelected');
- var row=new Array();
- if(selrow!=undefined)
- row.push(selrow['codeID'],selrow['p_name'],selrow['p_type'],selrow['p_tid'],
- selrow['p_pinpai'],selrow['p_price'],selrow['p_discount']);
- //push就是将数据压入数组
- var param=ExecType.split(',');//分割字符串为数组,delphi的用法类似
- if(param[0]=='update')
- {
- $.post('GridData.php',{Action:param[0],id:param[1],Rowdata:row.toString()},function(data){alert(data)});
- //update时要提交动作、codeID和更新后的数据,
- //post的参数function(data){alert(data)就是服务器返回数据。
- }
- if(param[0]=='insert into')
- {
- $.post('GridData.php',{Action:param[0],Rowdata:row.toString()},function(data){alert(data)});
- //insert时要提交动作和插入后的数据
- }
- ExecType='';
- }
- }
- function rejectr(){//取消
- $('#easyui_tb').datagrid('rejectChanges');
- ExecType='';
- editIndex = undefined;
- }
- function getChanges(){
- var rows = $('#easyui_tb').datagrid('getChanges');
- return rows;
- }
function Exec_SQL(act,id,row:string):string;
源码自己去看,很简单,就是操作数据库。本文基本完成,最后讲一下这种 方式未完成的功能:直接使用http://xxx.xxx.xxx/main.html可以不用登陆就能进入主界面,显然不是我们所期望的,可以在主界面 加入验证登陆的功能,也很简单,可以在页面加载之前$.post提交验证信息到login.php,里面代码已经写了,只是客户端没有添加。还有第三大 点。
三、动态加载
动态加载简单说就是,客户端还是单独做出来,不用放在wwwroot下面,引用js和css时需要在路径前面多加一个“/”即可。仍然从TContentBase 继承实现一个类,在函数中这样实现 :
- function TMyIndex.Execute(aRequest: THttpRequest; aReply: THttpReply; const aPathname: string; aSession: TIWApplication; aParams: TStrings): Boolean;
- var
- ss:Tstrings;
- begin
- aReply.ContentType := MIME_HTML;
- ss:=TstringList.create;
- ss.loadformfile('做好静态页面');
- aReply.WriteString(ss.text);
- ss.free;
- Result := True;
- end;
本文所需要工具:delphiXE7+Intraweb 14.0.38
源码 见 http://bbs.2ccc.com/topic.asp?topicid=479014