梦想空间's profile梦想空间PhotosBlogListsMore ![]() | Help |
|
|
July 18 VFP编程技巧(收藏)
如何在程序的开始检测权限和根据权限操作
利用DOS下的内部或外部命令来完成VFP无法达到的功能
July 12 在VFP中如何调用其它程序(收藏)豆三兄的
在VFP中我们可以用run来调用由VFP自身生成的.exe文件。命令格式: 当调用非VFP自身生成的.exe文件时,需要加上绝对路径和参数。 在这种情况下,虽然可以通过加上参数使程序程序能够流畅运行,但还是要指定绝对路径,而在实际开发时我们常常无法确定所需要启动的应用程序的文件名及其绝对路径。比如,在上例中,我们无法确定用户的word到底安装在什么地方,如果用户把word安装到了别的地方,上述命令便会出错。再如,一个.gif文件,有的用户喜欢用ACD See来打开,有的用户喜欢用IE来打开,有的用户喜欢用豪杰来打开,…… DECLARE INTEGER ShellExecute IN shell32.DLL INTEGER HWND,STRING lpszOP,STRING lpszFile,STRING lpszParams,STRING lpszDir,INTEGER fsshowcmd 可以用任意的文件名来替换上文中的 c:\mlx.doc 。如果指定的文件名Windows无法找到相应的关连程序,这时程序将不会做出反应。有关的参数可以自行偿试进行修改,以达到最佳效果。 ------------------------------------------------------------- ShellExecute
在 VFP 中的定义 Visual FoxPro 应用示例 * 打开默认的浏览器并定位到天堂论坛 * 打开默认的邮件阅读器来发一封信给天堂版主 * 打印文本文件 "c:\mytextfile.txt" 用过《网络蚂蚁》的朋友都知道,在帮助菜单的对话框里作者留下了他的电子邮件地址,单击该邮件地址我们便能给作者发送电子邮件。这种功能看起来有些神秘,实际上只要利用Windows API的ShellExecute函数,便可轻松地实现该功能。 ShellExecute是用来打开特定格式的文件(如WORD文件、EXCEL表格)的函数。该函数存放在Shell32.DLL动态链接库中,通过查阅MSDN,我们能够得到下述帮助: HINSTANCE ShellExecute(HWND hwnd,LPCTSTR lpOperation,LPCTSTR lpFile,LPCTSTR lpParameters,LPCTSTR lpDirectory,INT nShowCmd); 其调用参数的含义如下: hwnd:指明打开文件的窗口句柄。 lpOperation:指明操作类型,分别是"open"(打开)、"print"(打印)、"explore"(浏览)。 lpFile:欲打开文件的文件名。这里应该将文件的含义向更深层次理解。文件可以是本地文件,也可以是远程文件;文件的类型可以是文本文件,也可以是多媒体文件。 lpParameters:打开文件时所传递的参数。特别适合打开EXE文件。 lpDirectory:文件所在路径。 nShowCmd:打开文件时窗口的状态。0表示隐藏,1正常方式、2最小化方式、3最大化方式。 如果该函数能够成功打开文件,则该函数的返回值大于32。该函数的返回值能够为你调试程序提供很多有用的信息,具体情况见MSDN内帮助文件。 好,有了上述认识之后,我们便能在VFP的程序开发中利用ShellExecute函数来实现发送电子邮件的功能。新建一表单ABOUTME,在该表单上添加下列控件(见表1)。 需要说明的是,在Label1中的MouseIcon设为一手型光标,其所对应的图标文件在系统内可以随处找到(利用查找文件功能),本文用的是Windows自带h_move.cur文件。针对Label11的各类设置都是为了将该标签打扮得更象"超文本"。 添加完上述控件之后,接下来便是编写控件的CLICK事件代码了。对Label1和Label2的CLICK事件设定相同,其他代码为: *进行声明操作: DECLARE INTEGER ShellExecute IN shell32.DLL INTEGER HWND,STRING,STRING lpszFile,STRING,STRING,INTEGER *向kingdom@126.com邮箱内发送一封主题为"你好"的电子邮件。 在Label1的CLICK事件里,添加以下代码: ShellExecute(0,"open","mailto:kingdom@126.com?subject=你好",0,0,1) 在Label2的CLICK事件里,将上述语句改为: ShellExecute(0,"open","http://NationalTax.home.Chinaren.com",0,0,1) *以最大化窗口方式打开IE,进入作者主页。 确认按钮的CLICK事件代码很简单:thisform.release 运行上述表单,当鼠标移动到电子邮件或是作者主页区域时,鼠标便会变成一只手,点一下即可进行相应的操作:单击Label1,启动Outlook Express;单击Label2,启动IE,表单真有些像一个小型的IE!其实,在VFP的程序开发中,一些看似不可能的事件只要稍微用到一些Windows API,所有的事件一下子都变得简单多了。"山重水复疑无路,柳暗花明又一村",这可能就是编程的乐趣所在吧 Windows API简介: Application Program Interface,即应用程序编程接口,它是Windows提供给程序员的一系列函数。这些API函数同一般的函数相似,同样具有输入输出参数,并编译到一个独立的文件中,该文件被称为动态链接库。运用API函数可以实现许多复杂和有趣的功能,如重新启动计算机、跟踪当前激活窗体、收发E-mail等。
如何在一个类中用编程的形式中如一个方法程序(收藏)表单运行时用BINDEVENT()来绑定事件,属性或方法从VFP自己的对象到其它VFP对象
*--------------------------------
列子1:
在一个表单中有n个命令按钮,我想随便单击其中任何一个按钮,此表单关闭,进入另一个表单。
不想一个按钮一个按钮的添加代码,而想用动态方式将对象的CLICK事件与原有或新建的事件或方法绑定。 ----------------------------------------------------------------------------------
可以为表单新建一个方法,比如MyMethod,然后写入诸如下面的代码:
THISForm.Release DO FORM 表单名 在表单的Init事件中加入以下代码:
FOR i = 1 TO THIS.ControlCount IF UPPER(THIS.Controls[i].BaseClass) == "COMMANDBUTTON" = BINDEVENT(THIS.Controls[i], "Click", THIS, "MyMethod") ENDIF ENDFOR 这样应该就可以了。但是,需要注意的是,只有VFP7以上版本才有BINDEVENT()函数。
也完全可以把按钮做成类,然后再向表单中添加若干基于该类的按钮。 不过如果如你所说,按钮是不可视的,那应该怎么都没用了。 列子2: ---------------------------------------------------------------------------------- 动态加载一个控件,如按钮,并给按钮的Click事件绑定事件: 1、建一个过程文件(如MyProc.PRG),内容如下
DEFINE CLASS myhandler AS Session PROCEDURE CmdClick MESSAGEBOX('ok',64,'') &&此处改为相应代码 RETURN ENDPROC ENDDEFINE 2、表单的INIT事件:
SET PROCEDURE TO MyProc.prg
3、表单上动态加载控件按钮的CLICK事件: THISFORM.ADDOBJECT('Cmd_Test','CommandButton')
THISFORM.Cmd_Test.CAPTION='Cmd_Test' THISFORM.Cmd_Test.TOP=100 THISFORM.Cmd_Test.LEFT=100 THISFORM.Cmd_Test.HEIGHT=25 PUBLIC oHandler oHandler=NEWOBJECT("myhandler") BINDEVENT(THISFORM.Cmd_Test,"Click",oHandler,"CmdClick") THISFORM.Cmd_Test.VISIBLE=.T. ----------------------------------------------------------
例子3:
*BINDEVENT() 函数。下面是一个简单示例。 PUBLIC oform1
oform1=NEWOBJECT("form1")
oform1.SHOW RETURN DEFINE CLASS form1 AS FORM TOP = 24 LEFT = 158 DOCREATE = .T. CAPTION = "Bindevent 示例" NAME = "Form1" PROCEDURE _click
THISFORM.text1.VALUE=THISFORM.ACTIVECONTROL.NAME &&此处可改为表单文件名 ENDPROC PROCEDURE INIT
LOCAL i,j,cname THISFORM.LOCKSCREEN=.T. FOR i=1 TO 3 FOR j=1 TO 3 cname='cmd'+STR(i,1)+STR(j,1) THISFORM.ADDOBJECT(cname,'commandbutton') WITH THISFORM.&cname. .LEFT=j*100-60 .TOP=i*40 .CAPTION='按钮 &cname.' .HEIGHT=25 .WIDTH=80 .VISIBLE=.T. ENDWITH BINDEVENT(THISFORM.&cname.,'click',THISFORM,'_CLICK') ENDFOR ENDFOR THISFORM.ADDOBJECT('text1','textbox') WITH THISFORM.text1 .LEFT=80 .TOP=160 .HEIGHT=25 .WIDTH=200 .VISIBLE=.T. ENDWITH THISFORM.LOCKSCREEN=.F. ENDPROC ENDDEFINE
*注意一点:Bindevent引用的代码中慎用“This”引用控件,它指向的还是原方法所在对象。如上例中,如果方法中出现“This”,则,它指的是表单,而不是按钮。
---------------------------------------------------------------
例子4: 下列代码将Visual FoxPro主窗口的MouseMove事件绑定到自定义类MyHandler的MyMouseMove方法,绑定后,当在主窗口中移动鼠标时,将显示鼠标的坐标位置。
PUBLIC oHandler &&注意,请将保存对象的变量设置为全局变量
oHandler=NEWOBJECT("MyHandler")
*!* 将_SCREEN.MouseMove绑定到oHandler.MyMouseMove
=BINDEVENT(_SCREEN,"MouseMove",oHandler,"MyMouseMove")
DEFINE CLASS MyHandler AS Custom
PROCEDURE MyMouseMove
*!* 要保证该方法与_SCREEN.MouseMove有同样的参数设置
LPARAMETERS nButton, nShift, nXCoord, nYCoord
WAIT WINDOW "鼠标坐标:"+STR(nXCoord)+"|"+STR(nYCoord) NOWAIT
ENDPROC
ENDDEFINE June 16 在VFP中实现可靠的随机密码和多用户权限控制 (收藏) 在应用系统中,经常使用口令实现对系统操作权限的控制,常规的方法是在进入系统时提示操作者输入一个字符串口令。这种口令的设置方法多种多样,有的是将固定口令密码写在程序里,系统开始要求操作者输入该密码,正确方可进入,这种方法的缺点是密码不能改变,且全系统只有一个。还有一种方法是将操作者输入的密码通过加密,转换为加密伪码存储在数据库中,但这种方法的密码和伪码仍有显式的对应关系,容易被破解。本文介绍一种在VFP中用随机伪码存库的方法实现简单可靠的系统加密,并用此方法实现一个应用系统的多用户权限控制,这种方法的特点是实现简单,加密可靠,不易破解,可将一个1至7位的用户密码转换为20位的随机伪码,且每次重新设定密码时所产生的伪码都不相同,通过变换生成的20位伪码没有任何规律性,即使从数据库中擦除伪码也无法进入系统,从而实现了可靠的密码权限控制。
---- 一、可靠的随机伪码存库
---- 从用户密码到存库的随机伪码之间的变换由两个函数完成,一个是加密函数,一个是解密函数。加密函数的思想是对用户密码(真码)进行复杂化、隐蔽化处理,也就是将真码淹没在20位伪码中,加密函数如下:
FUNCTION MAZH1
PARAMETERS ZMZ ZMZ=VAL(ZMZ) N1=RAND()*10^9 IF N1 N1=N1+10^9 ENDI N1=INT(N1) C1=STR(N1+ZMZ)+STR(N1) C2=SUBS(C1,5,20)+SUBS(C1,1,4) P1='' P2='' FOR II=1 TO 10 P1=P1+SUBS(C2,2*II-1,1) P2=P2+SUBS(C2,2*II,1) ENDFOR WMZ=P1+P2 RETURN WMZ ---- 若真码为:1234567,则伪码为:64915302152868193982,无论真码是一位还是相同多位,伪码总是具有同样的不确定性和复杂性,所以若想通过简化真码来分析伪码是不可能的。 ---- 解码函数是将数据库中存放的伪码转换成原用户密码,其代码如下:
FUNCTION MAZH2
PARAMETERS WMZ PP='' FOR II=1 TO 10 PP=PP+SUBS(WMZ,II,1)+SUBS(WMZ,II+10,1) ENDFOR DD=SUBS(PP,17,4)+SUBS(PP,1,16) M1=SUBS(DD,1,10) M2=SUBS(DD,11,10) ZMZ=INT(VAL(M1)-VAL(M2)) RETURN ZMZ ---- 由于提交的系统全是编辑的,非法者是无法得到密码转换函数中的信息的,所以解密方法是不易被发现的。 ---- 二、多用户权限控制的实现 ---- 对一个包含多种业务操作的应用系统,由于业务的要求,常需要限制不同操作者的业务操作范围,在VFP中,可以建立一个权限数据表,表中 有多个字段,分别用于存储了每个操作者的代号、姓名、口令以及是否可以操作业务模块的伪标识码,见下表: 工号 姓名 系 统 口 令 业务1操作标识码 业务2操作标识码 …
01 刘君红 71510714108669886598 75913 759130063600536 02817028179076390563 …
02 何晚平 17615822156435449957 8341 3834133074230642 56314563144262342423 …
03 刘棋东 16710714110199886591 044 18701188588160484 83413834133074230642 …
04 赵民 58613143131858393185 4611702 8171696390565 48710144103584210444 …
05 王雪菲 80614563147782342425 52412 171122488399485 53210299114872812320 … … …
---- 系统的主表单上的多个业务模块由多个按钮来实现启动,那么在系统主表单的[确定]按钮的CLICK EVENT方法程序中添加一些判断代码,就可以实现系统的口令和操作权限控制。输入工号和口令后,按[确定],CLICK EVENT中的代码首先根据输入的工号,对权限数据表中对应记录的口令伪码进行解码,得出的真码与输入口令一致放可进入系统,口令测试通过后,再通过对每个业务所对应的伪标识码字段内容进行解码,确定该业务模块是否允许操作,进而将该模块的启动按钮的ENABLED属性设置为.T.或.F.。为了在系统一启动就打开权限数据表,要将权限表添加到主表单的数据环境中。主表单的一个按钮可以启动“权限维护”表单,该表单可以完成对每个工号的口令和操作权限设置。一般只有系统管理员被赋予“权限维护”的权限,不同工号的操作员在进入系统后可以自行修改自己的密码,而管理员无法知道操作员的密码,只能进行擦除,这一点更加提高了操作员密码的安全性。
---- 以上表单和程序在金长城PII/400机上用Visual Foxpro5.0调试通过 对DBF表进行加密或解密 (收藏)*前提:被操作的表如果已经打开,一定要先关闭
*--------------------------------------- DBF_JM('temp.dbf',1) &&加密 DBF_JM('temp.dbf',0) &&解密 FUNCTION DBF_JM PARAMETERS filename,jm IF AT('.',filename)=0 filename=filename+'.dbf' &&处理文件名 ENDIF IF jm=1 &&加密 handle=FOPEN(filename,2) &&打开文件 keybite=FREAD(handle,1) &&读表头第一个字节 =FSEEK(handle,0) &&指针移回第一个字节 =FWRITE(handle,CHR(ASC(keybite)+2)) &&用比原来高2的ASCII字符改写 =FCLOSE(handle) &&关闭文件 ELSE &&解密 handle=FOPEN(filename,2) keybite=FREAD(handle,1) =FSEEK(handle,0) =FWRITE(handle,CHR(ASC(keybite)-2)) &&用比原来低2的ASCII字符改写 =FCLOSE(handle) ENDIF ENDFUNC *--------------------------------------- FUNCTION DBF_JieMi &&解密 PARAMETERS lfile lfn=ALLTRIM(lfile) lh=FOPEN(lfn,12) =FSEEK(lh,0,0) =FSEEK(lh,8) lk1=FREAD(lh,1) lk2=FREAD(lh,1) =FSEEK(lh,0,0) =FSEEK(lh,8) =FWRITE(lh,CHR(ASC(lk1)-2),1) =FWRITE(lh,CHR(ASC(lk2)-2),1) =FCLOSE(lh) RETURN ENDFUNC FUNCTION DBF_JiaMi &&加密 PARAMETERS lfile lfn=ALLTRIM(lfile) lh=FOPEN(lfn,12) =FSEEK(lh,0,0) =FSEEK(lh,8) lk1=FREAD(lh,1) lk2=FREAD(lh,1) =FSEEK(lh,0,0) =FSEEK(lh,8) =FWRITE(lh,CHR(ASC(lk1)+2),1) =FWRITE(lh,CHR(ASC(lk2)+2),1) =FCLOSE(lh) RETURN ENDFUNC sql详解 (收藏)Select 用途: 从指定表中取出指定的列的数据
语法: SELECT column_name(s) FROM table_name
解释: 从数据库中选取资料列,并允许从一或多个资料表中,选取一或多个资料列或资料行。SELECT 陈述式的完整语法相当复杂,但主要子句可摘要为:
SELECT select_list [ INTO new_table ] FROM table_source [ WHERE search_condition ] [ GROUP BY group_by_expression ] [ HAVING search_condition ] [ ORDER BY order_expression [ ASC | DESC ] ]
例: “Persons” 表中的数据有 LastName FirstName Address City Hansen Ola Timoteivn 10 Sandnes Svendson Tove Borgvn 23 Sandnes Pettersen Kari Storgt 20 Stavanger 选出字段名” LastName”、” FirstName” 的数据 SELECT LastName,FirstName FROM Persons 返回结果: LastName FirstName Hansen Ola Svendson Tove Pettersen Kari 选出所有字段的数据 SELECT * FROM Persons 返回结果: LastName FirstName Address City Hansen Ola Timoteivn 10 Sandnes Svendson Tove Borgvn 23 Sandnes Pettersen Kari Storgt 20 Stavanger Where 用途: 被用来规定一种选择查询的标准 语法: SELECT column FROM table WHERE column condition &#118;alue 下面的操作符能被使用在WHERE中: =,<>,>,<,>=,<=,BETWEEN,LIKE 注意: 在某些SQL的版本中不等号< >能被写作为!= 解释: SELECT语句返回WHERE子句中条件为true的数据 例: 从” Persons”表中选出生活在” Sandnes” 的人 SELECT * FROM Persons WHERE City='Sandnes' Persons 表中的数据有: LastName FirstName Address City Year Hansen Ola Timoteivn 10 Sandnes 1951 Svendson Tove Borgvn 23 Sandnes 1978 Svendson Stale Kaivn 18 Sandnes 1980 Pettersen Kari Storgt 20 Stavanger 1960 返回结果: LastName FirstName Address City Year Hansen Ola Timoteivn 10 Sandnes 1951 Svendson Tove Borgvn 23 Sandnes 1978 Svendson Stale Kaivn 18 Sandnes 1980 And & Or 用途: 在WHERE子句中AND和OR被用来连接两个或者更多的条件 解释: AND在结合两个布尔表达式时,只有在两个表达式都为 TRUE 时才传回 TRUE OR在结合两个布尔表达式时,只要其中一个条件为 TRUE 时,OR便传回 TRUE 例: Persons 表中的原始数据: LastName FirstName Address City Hansen Ola Timoteivn 10 Sandnes Svendson Tove Borgvn 23 Sandnes Svendson Stephen Kaivn 18 Sandnes 用AND运算子来查找Persons 表中FirstName为”Tove”而且LastName为” Svendson”的数据 SELECT * FROM Persons WHERE FirstName='Tove' AND LastName='Svendson' 返回结果: LastName FirstName Address City Svendson Tove Borgvn 23 Sandnes 用OR运算子来查找Persons 表中FirstName为”Tove”或者LastName为” Svendson”的数据 SELECT * FROM Persons WHERE firstname='Tove' OR lastname='Svendson' 返回结果: LastName FirstName Address City Svendson Tove Borgvn 23 Sandnes Svendson Stephen Kaivn 18 Sandnes 你也能结合AND和OR (使用括号形成复杂的表达式),如: SELECT * FROM Persons WHERE (FirstName='Tove' OR FirstName='Stephen') AND LastName='Svendson' 返回结果: LastName FirstName Address City Svendson Tove Borgvn 23 Sandnes Svendson Stephen Kaivn 18 Sandnes Between…And 用途: 指定需返回数据的范围 语法: SELECT column_name FROM table_name WHERE column_name BETWEEN &#118;alue1 AND &#118;alue2 例: “Persons”表中的原始数据 LastName FirstName Address City Hansen Ola Timoteivn 10 Sandnes Nordmann Anna Neset 18 Sandnes Pettersen Kari Storgt 20 Stavanger Svendson Tove Borgvn 23 Sandnes 用BETWEEN…AND返回LastName为从”Hansen”到”Pettersen”的数据: SELECT * FROM Persons WHERE LastName BETWEEN 'Hansen' AND 'Pettersen' 返回结果: LastName FirstName Address City Hansen Ola Timoteivn 10 Sandnes Nordmann Anna Neset 18 Sandnes Pettersen Kari Storgt 20 Stavanger 为了显示指定范围之外的数据,也可以用NOT操作符: SELECT * FROM Persons WHERE LastName NOT BETWEEN 'Hansen' AND 'Pettersen' 返回结果: LastName FirstName Address City Svendson Tove Borgvn 23 Sandnes Distinct 用途: DISTINCT关键字被用作返回唯一的值 语法: SELECT DISTINCT column-name(s) FROM table-name 解释: 当column-name(s)中存在重复的值时,返回结果仅留下一个 例: “Orders”表中的原始数据 Company OrderNumber Sega 3412 W3Schools 2312 Trio 4678 W3Schools 6798 用DISTINCT关键字返回Company字段中唯一的值: SELECT DISTINCT Company FROM Orders 返回结果: Company Sega W3Schools Trio Order by 用途: 指定结果集的排序 语法: SELECT column-name(s) FROM table-name ORDER BY { order_by_expression [ ASC | DESC ] } 解释: 指定结果集的排序,可以按照ASC(递增方式排序,从最低值到最高值)或者DESC(递减方式排序,从最高值到最低值)的方式进行排序,默认的方式是ASC 例: “Orders”表中的原始数据: Company OrderNumber Sega 3412 ABC Shop 5678 W3Schools 2312 W3Schools 6798 按照Company字段的升序方式返回结果集: SELECT Company, OrderNumber FROM Orders ORDER BY Company 返回结果: Company OrderNumber ABC Shop 5678 Sega 3412 W3Schools 6798 W3Schools 2312 按照Company字段的降序方式返回结果集: SELECT Company, OrderNumber FROM Orders ORDER BY Company DESC 返回结果: Company OrderNumber W3Schools 6798 W3Schools 2312 Sega 3412 ABC Shop 5678 Group by 用途: 对结果集进行分组,常与汇总函数一起使用。 语法: SELECT column,SUM(column) FROM table GROUP BY column 例: “Sales”表中的原始数据: Company Amount W3Schools 5500 IBM 4500 W3Schools 7100 按照Company字段进行分组,求出每个Company的Amout的合计: SELECT Company,SUM(Amount) FROM Sales GROUP BY Company 返回结果: Company SUM(Amount) W3Schools 12600 IBM 4500 Having 用途: 指定群组或汇总的搜寻条件。 语法: SELECT column,SUM(column) FROM table GROUP BY column HAVING SUM(column) condition &#118;alue 解释: HAVING 通常与 GROUP BY 子句同时使用。不使用 GROUP BY 时,HAVING 则与 WHERE 子句功能相似。 例: “Sales”表中的原始数据: Company Amount W3Schools 5500 IBM 4500 W3Schools 7100 按照Company字段进行分组,求出每个Company的Amout的合计在10000以上的数据: SELECT Company,SUM(Amount) FROM Sales GROUP BY Company HAVING SUM(Amount)>10000 返回结果: Company SUM(Amount) W3Schools 12600 Join 用途: 当你要从两个或者以上的表中选取结果集时,你就会用到JOIN。 例: “Employees”表中的数据如下,(其中ID为主键): ID Name 01 Hansen, Ola 02 Svendson, Tove 03 Svendson, Stephen 04 Pettersen, Kari “Orders”表中的数据如下: ID Product 01 Printer 03 Table 03 Chair 用Employees的ID和Orders的ID相关联选取数据: SELECT Employees.Name, Orders.Product FROM Employees, Orders WHERE Employees.ID = Orders.ID 返回结果: Name Product Hansen, Ola Printer Svendson, Stephen Table Svendson, Stephen Chair 或者你也可以用JOIN关键字来完成上面的操作: SELECT Employees.Name, Orders.Product FROM Employees INNER JOIN Orders ON Employees.ID = Orders.ID INNER JOIN的语法: SELECT field1, field2, field3 FROM first_table INNER JOIN second_table ON first_table.keyfield = second_table.foreign_keyfield 解释: INNER JOIN返回的结果集是两个表中所有相匹配的数据。 LEFT JOIN的语法: SELECT field1, field2, field3 FROM first_table LEFT JOIN second_table ON first_table.keyfield = second_table.foreign_keyfield 用”Employees”表去左外联结”Orders”表去找出相关数据: SELECT Employees.Name, Orders.Product FROM Employees LEFT JOIN Orders ON Employees.ID = Orders.ID 返回结果: Name Product Hansen, Ola Printer Svendson, Tove Svendson, Stephen Table Svendson, Stephen Chair Pettersen, Kari 解释: LEFT JOIN返回”first_table”中所有的行尽管在” second_table”中没有相匹配的数据。 RIGHT JOIN的语法: SELECT field1, field2, field3 FROM first_table RIGHT JOIN second_table ON first_table.keyfield = second_table.foreign_keyfield 用”Employees”表去右外联结”Orders”表去找出相关数据: SELECT Employees.Name, Orders.Product FROM Employees RIGHT JOIN Orders ON Employees.ID = Orders.ID 返回结果: Name Product Hansen, Ola Printer Svendson, Stephen Table Svendson, Stephen Chair 解释: RIGHT JOIN返回” second_table”中所有的行尽管在”first_table”中没有相匹配的数据。 Alias 用途: 可用在表、结果集或者列上,为它们取一个逻辑名称 语法: 给列取别名: SELECT column AS column_alias FROM table 给表取别名: SELECT column FROM table AS table_alias 例: “Persons”表中的原始数据: LastName FirstName Address City Hansen Ola Timoteivn 10 Sandnes Svendson Tove Borgvn 23 Sandnes Pettersen Kari Storgt 20 Stavanger 运行下面的SQL: SELECT LastName AS Family, FirstName AS Name FROM Persons 返回结果: Family Name Hansen Ola Svendson Tove Pettersen Kari 运行下面的SQL: SELECT LastName, FirstName FROM Persons AS Employees 返回结果: Employees中的数据有: LastName FirstName Hansen Ola Svendson Tove Pettersen Kari Insert Into 用途: 在表中插入新行 语法: 插入一行数据 INSERT INTO table_name &#118;alueS (&#118;alue1, &#118;alue2,....) 插入一行数据在指定的字段上 INSERT INTO table_name (column1, column2,...) &#118;alueS (&#118;alue1, &#118;alue2,....) 例: “Persons”表中的原始数据: LastName FirstName Address City Pettersen Kari Storgt 20 Stavanger 运行下面的SQL插入一行数据: INSERT INTO Persons &#118;alueS ('Hetland', 'Camilla', 'Hagabakka 24', 'Sandnes') 插入后”Persons”表中的数据为: LastName FirstName Address City Pettersen Kari Storgt 20 Stavanger Hetland Camilla Hagabakka 24 Sandnes 运行下面的SQL插入一行数据在指定的字段上: INSERT INTO Persons (LastName, Address) &#118;alueS ('Rasmussen', 'Storgt 67') 插入后”Persons”表中的数据为: LastName FirstName Address City Pettersen Kari Storgt 20 Stavanger Hetland Camilla Hagabakka 24 Sandnes Rasmussen Storgt 67 Update 用途: 更新表中原有数据 语法: UPDATE table_name SET column_name = new_&#118;alue WHERE column_name = some_&#118;alue 例: “Person”表中的原始数据: LastName FirstName Address City Nilsen Fred Kirkegt 56 Stavanger Rasmussen Storgt 67 运行下面的SQL将Person表中LastName字段为”Rasmussen”的FirstName更新为”Nina”: UPDATE Person SET FirstName = 'Nina' WHERE LastName = 'Rasmussen' 更新后”Person”表中的数据为: LastName FirstName Address City Nilsen Fred Kirkegt 56 Stavanger Rasmussen Nina Storgt 67 同样的,用UPDATE语句也可以同时更新多个字段: UPDATE Person SET Address = 'Stien 12', City = 'Stavanger' WHERE LastName = 'Rasmussen' 更新后”Person”表中的数据为: LastName FirstName Address City Nilsen Fred Kirkegt 56 Stavanger Rasmussen Nina Stien 12 Stavanger Delete 用途: 删除表中的数据 语法: DELETE FROM table_name WHERE column_name = some_&#118;alue 例: “Person”表中的原始数据: LastName FirstName Address City Nilsen Fred Kirkegt 56 Stavanger Rasmussen Nina Stien 12 Stavanger 删除Person表中LastName为”Rasmussen”的数据: DELETE FROM Person WHERE LastName = 'Rasmussen' 执行删除语句后”Person”表中的数据为: LastName FirstName Address City Nilsen Fred Kirkegt 56 Stavanger Create Table 用途: 建立新的资料表。 语法: CREATE TABLE table_name ( column_name1 data_type, column_name2 data_type, ....... ) 例: 创建一张叫“Person”的表,该表有4个字段LastName, FirstName, Address, Age: CREATE TABLE Person ( LastName varchar, FirstName varchar, Address varchar, Age int ) 如果想指定字段的最大存储长度,你可以这样: CREATE TABLE Person ( LastName varchar(30), FirstName varchar(30), Address varchar(120), Age int(3) ) 下表中列出了在SQL的一些数据类型: Data Type Description integer(size) int(size) smallint(size) tinyint(size) Hold integers only. The maximum number of digits are specified in parenthesis. decimal(size,d) numeric(size,d) Hold numbers with fractions. The maximum number of digits are specified in size. The maximum number of digits to the right of the decimal is specified in d. char(size) Holds a fixed length string (can contain letters, numbers, and special characters). The fixed size is specified in parenthesis. varchar(size) Holds a variable length string (can contain letters, numbers, and special characters). The maximum size is specified in parenthesis. date(yyyymmdd) Holds a date Alter Table 用途: 在已经存在的表中增加后者移除字段 语法: ALTER TABLE table_name ADD column_name datatype ALTER TABLE table_name DROP COLUMN column_name 注意:某些数据库管理系统不允许移除表中的字段 例: “Person”表中的原始数据: LastName FirstName Address Pettersen Kari Storgt 20 在Person表中增加一个名为City的字段: ALTER TABLE Person ADD City varchar(30) 增加后表中数据如下: LastName FirstName Address City Pettersen Kari Storgt 20 移除Person表中原有的Address字段: ALTER TABLE Person DROP COLUMN Address 移除后表中数据如下: LastName FirstName City Pettersen Kari Drop Table 用途: 在数据库中移除一个数据表定义及该数据表中的所有资料、索引、触发程序、条件约束及权限指定。 语法: DROP TABLE table_name Create Database 用途: 建立新的数据库. 语法: CREATE DATABASE database_name Drop Database 用途: 移除原有的数据库 语法: DROP DATABASE database_name 聚集函数 count 用途: 传回选取的结果集中行的数目。 语法: SELECT COUNT(column_name) FROM table_name 例: “Persons”表中原始数据如下: Name Age Hansen, Ola 34 Svendson, Tove 45 Pettersen, Kari 19 选取记录总数: SELECT COUNT(Name) FROM Persons 执行结果: 3 sum 用途: 以表达式传回所有值的总和,或仅 DISTINCT 值。SUM 仅可用于数值资料行。已忽略 Null 值。 语法: SELECT SUM(column_name) FROM table_name 例: “Persons”表中原始数据如下: Name Age Hansen, Ola 34 Svendson, Tove 45 Pettersen, Kari 19 选取”Persons”表中所有人的年龄总和: SELECT SUM(Age) FROM Persons 执行结果: 98 选取”Persons”表中年龄超过20岁的人的年龄总和: SELECT SUM(Age) FROM Persons WHERE Age>20 执行结果: 79 avg 用途: 传回选取的结果集中值的平均值。已忽略 Null 值。 语法: SELECT AVG(column_name) FROM table_name 例: “Persons”表中原始数据如下: Name Age Hansen, Ola 34 Svendson, Tove 45 Pettersen, Kari 19 选取”Persons”表中所有人的平均年龄: SELECT AVG(Age) FROM Persons 执行结果: 32.67 选取”Persons”表中年龄超过20岁的人的平均年龄: SELECT AVG(Age) FROM Persons WHERE Age>20 执行结果: 39.5 max 用途: 传回选取的结果集中值的最大值。已忽略 Null 值。 语法: SELECT MAX(column_name) FROM table_name 例: “Persons”表中原始数据如下: Name Age Hansen, Ola 34 Svendson, Tove 45 Pettersen, Kari 19 选取”Persons”表中的最大年龄: SELECT MAX(Age) FROM Persons 执行结果: 45 min 用途: 传回选取的结果集中值的最小值。已忽略 Null 值。 语法: SELECT MIN(column_name) FROM table_name 例: “Persons”表中原始数据如下: Name Age Hansen, Ola 34 Svendson, Tove 45 Pettersen, Kari 19 选取”Persons”表中的最小年龄: SELECT MIN(Age) FROM Persons 执行结果: 19 算术函数 abs 用途: 传回指定数值表达式 (Numeric Expression) 的绝对正值。 语法: ABS(numeric_expression) 例: ABS(-1.0) ABS(0.0) ABS(1.0) 执行结果: 1.0 0.0 1.0 ceil 用途: 传回大于等于给定数值表达式的最小整数。 语法: CEIL(numeric_expression) 例: CEIL(123.45) CEIL(-123.45) 执行结果: 124.00 -123.00 floor 用途: 传回小于或等于给定数值表达式的最大整数。 语法: FLOOR(numeric_expression) 例: FLOOR(123.45) FLOOR(-123.45) 执行结果: 123.00 -124.00 cos 用途: 在指定表达式中传回指定角度 (以弪度为单位) 的三角余弦值的数学函数。 语法: COS(numeric_expression) 例: COS(14.7 执行结果: -0.599465 cosh 用途: 传回以弧度为单位的角度值,其余弦为指定的 float 表达式,也称为反余弦。 语法: COSH(numeric_expression) 例: COSH(-1) 执行结果: 3.14159 sin 用途: 以近似的数值 (float) 表达式传回给定角度 (以弧度) 之三角正弦函数 (Trigonometric Sine)。 语法: SIN(numeric_expression) 例: SIN(45.175643) 执行结果: 0.929607 sinh 用途: 传回以弪度为单位的角度,其正弦为指定的 float 表达式 (也称为反正弦)。 语法: SINH(numeric_expression) 例: SINH(-1.00) 执行结果: -1.5708 tan 用途: 传回输入表达式的正切函数。 语法: TAN(numeric_expression) 例: TAN(3.14159265358979/2) 执行结果: 1.6331778728383844E+16 tanh 用途: 传回以弪度为单位的角度,其正切为指定的 float 表达式 (也称为反正切)。 语法: TANH(numeric_expression) 例: TANH(-45.01) 执行结果: -1.54858 exp 用途: 传回给定的 float 表达式的指数 (Exponential) 值。 语法: EXP(numeric_expression) 例: EXP(378.615345498) 执行结果: 2.69498e+164 log 用途: 传回给定的 float 表达式之自然对数。 语法: LOG(numeric_expression) 例: LOG(5.175643) 执行结果: 1.64396 power 用途: 传回给定表达式指定乘幂的值。 语法: POWER(numeric_expression,v) 例: POWER(2,6) 执行结果: 64 sign 用途: 传回给定的表达式之正 (+1)、零 (0) 或负 (-1) 号。 语法: SIGN(numeric_expression) 例: SIGN(123) SIGN(0) SIGN(-456) 执行结果: 1 0 -1 sqrt 用途: 传回给定表达式的平方。 语法: SQRT(numeric_expression) 例: SQRT(10) 执行结果: 100 VFP 5.0远程视图使用面面观 (收藏)武汉交通科技大学计算机系 吴业福 袁小玲
摘要:远程视图是Visual Foxpro 5.0 中开发Client/Server 应用系统的基础,本文首先介绍了远程视图有关参数的放置技巧,然后介绍如何去设计、使用远程视图,最后讨论了使用远程视图的四个实际问题。
关键词:远程视图;远程数据;连接;锁定;缓冲。
一、绪论
VFP 是一种较好的客户/ 服务器应用系统开发工具,用VFP 开发C/S 系统的关键是如何访问远程数据( 远程服务器中的Table/View)。可以使用VFP 提供的SQL Pass-Through 函数访问远程数据,但最常用的方法是使用远程视图(Remote View) 去访问远程数据。本文结合本人的使用经验对远程视图的使用技巧加以介绍。
二、远程视图的环境设置
在设计远程视图时,需要对远程视图的有关环境信息进行设置。使用Tools 菜单中的Options.... 选项来设置Remote Data 选项。其选项设置说明如下: *Share Connection: 指出今后设计的远程视图是否使用共享连接。使用共享连接可大大减少数据库服务器中客户访问许可数,但会影响客户机的访问速度。 *SQL Updates/Criteria: 指出对后台数据进行update 操作的条件。
它有四种可能:
①Key Field only
②Key and Updatable Fields
③Key and Modified Fields
④Key and Time Stamp。 一般选择③较合适。
*SQL Updates/Method: 指出对后台数据的更新方法。 它有两种选择:①SQL Delete then Insert ②SQL update。 一般选择②比较合适。 *Connection Defaults: 主要用于SQL Pass-Through 函数访问远程数据,对于远程视图则不需要放置。
三、远程视图的设计
有两种方法设计远程视图,一是使用View Designer;二是使用SQL 语句。用View Designer 可设计较为简单的视图,而使用SQL 的Create Remote View 语句则可进行复杂视图的设计。如果试图去用View Designer 观察或修改( 用Create 创建的视图) 则极有可能被破坏它( 例如:若创建视图的Select 语句中含有exists 子句, 则一定会被破坏!)
1. 用Create 命令设计
例如:假定连接名为WYFconnect,两个远程表为SealList 和ShipList. Create 命令可如下:
CREATE SQL VIEW ViewTest REMOTE CONNECTION WYFconnect AS SELECT SealList.* FROM SealList, ShipList WHERE SealList IS NOT NULL AND SealList AND 箱号NOT IN (SELECT 箱号FROM ShipList WHERE ShipList. 箱号IS NOT NULL AND ShipList. 船名=SealList. 船名 使用该方法可以在程序中动态地构造满足不同条件的远程视图(View Designer 不能构造带参数的远程视图)。例如:设m.ShipName 为一可变内存变量,其创建语句为: CREATE SQL VIEW ViewTest REMOTE CONNECTION WYFconnect AS SELECT * FROM SealList WHERE 船名=m.ShipName. 2. 用View Designer 设计
启动View Designer 选择连接名后出现设计图, 在其上部可以加入远程服务器中的表或视图,在其下部进行参数/ 条件设置: *Select Criteria: 设置远程数据值上的记录必须满足的条件,类似于下SQL Select 语句中的Where 子句。在这里可以构造复杂条件,但条件中使用的函数和语法规则必须是后台数据库支持的,例如:不能使用DTOC( 日期)=‘90/01/01’,而必须使用Convert(char(10), 日期)=‘90/01/01’, 因为后台数据库不支持DTOC() 函数。 *Fields: 选择结果集中的字段,在这里要强调的是Function/Expression 的使用,使用它可构造出与源表字段不同的字段,如:T. 船名+‘’+T. 中文AS 船舶。实践证明Function/Expression 中不能使用双引号,而只能使用单引号。 *Order by 和Group by: 对结果分组、分类。要求符合后台数据库所支持的SQL 语法。 *Update Criteria: 设置视图的有关替换参数, 具体如下:
①要选中Send SQL Update
②SQL where 一般选择Key and Modify Fields
③Update Using 一般选择SQL Update
④务请选择关键字段和可更改字段( 在钥匙符号下面和铅笔符号下面加“√”),否则将无法正确完成Insert/update 功能。
四、远程视图的使用
1. 缓冲与锁定设置 对于远程视图,建议使用缓冲机制来提高远程视图的工作效率和使用的方便性。首先在Option 菜单中设置Data 组件,需设置下列参数: ①选择Open Exclusive 以让库共享打开,库中的远程视图则可共享使用; ②Locking And Buffering 设置:选中Automatic File locking 和Multiple Record Locks,并将Buffering 设置成Fields Buffering,加锁方式设置为“记录级乐观锁定”。在具体设计Form 时,对数据环境中的每一个远程视图光标可重新根据需要来设置锁定和缓冲机制。如果不重新设置,它取Form 的缺省设置。
2. 数据的写入与前台刷新 对于采用了Buffering 技术的远程视图,当修改/ 删除/ 增加了记录时,需用TableUpdate(.T.,.T.) 函数来确保数据写到后台数据库中;若要取消对缓冲区中视图数据的修改,需用TableRevert(.T.) 函数。例如: Select View1 Append Blank Begin Transaction if TableUpdate(.T.,.T.) EndTranseaction else RollBack =TableRevert( ·T ·) endif
如果该远程视图与别的表/ 远程视图发生级联修改/ 删除/ 增加关系,则需使用事务机制,以确保数据的完整性。
如果对应视图的后台表/ 视图数据发生变化时,使用下述方法让前台客户机中的数据与实际后台数据一致:
①Select 〈远程视图名〉 USE〈远程视图名〉&& 再次打开即可
②Select 〈远程视图名〉 =Requery() && 刷新缓冲区值
五、远程视图使用中碰到的若干问题
1. 如何在一个远程视图中多次使用同一远程数据源?
依靠别名。例如:Select A. 船名,A. 航次,B. 姓名as 操作员,C. 姓名as 仓库员from Shipname A, worker B, worker C where A. 操作员代码=B.Code and A. 仓库员代码=C.Code
2. 设计远程视图时Select 语句的语法应符合VFP 或后台数据库?
后台数据库支持的SQL 语法。例如:Create SQL View AAA Remote Conection BB As Select A.* From Shipname A Where Convert(Char(10), 进港日期)=‘1998/05/30’
3. 远程视图能否使用Pack 和Recall 语句?
不能使用pack 语句,例如: use View1; delete for 〈条件〉 && 加删除标志 pack && 错误,必须使用TableUpdate () 函数 对于Recall 命令,如果已执行了TableUpdate(),则执行无效; 若未发Tableupdate(), 则可用Recall 来取消删除标记。
4. 本地视图的数据源有远程视图,如何刷新本地视图?
例如:有两个远程视图Rview1 Rview2, 创建本地视图的命令为:
Create SQL view As Select A.* B.* from Rview1 A,R view2 B where A. 箱号=B. 箱号
刷新view 之前必须先刷新Rview1 和Rview2: =requery (“Rview1”) =Requery (“Rview2”) =Refresh (“view”)
六、小结
远程视图是VFP 访问远程数据服务器中数据的有利武器,有了前面的介绍,就可得心应手地设计C/S 应用系统了。
参考文献 [1 ]E.sander 等著visual FoxPro3.0 实用指南机械工业出版社1996 The Skills of Using Remote View in Visual Foxpro 5.0 Wuyefu and Yuanxiaoling Dept. Computer, Wuhan Transaction University , WuHan430063 ABSTRACT: In the paper, it is introduced how to choice option parameters and to use remote view in Visual Foxpro 5.0. In the end, the solution to some problems is also discussed. KEYWORDS: Remote view, Remote Data, Connection, Lock, buffering 关于报表打印的几点技巧 (收藏) VFP所提供的报表设计器虽说功能强大,并能提供所见所得的报表预览,但我总觉得没有DOS下直接用代码编制的打印程序来得方便和自由,虽然DOS没有预览功能。也正因为如此,我很少谈及VFP 的打印问题,为了和大家交流,这次也谈谈VFP的报表打印问题,希望以此和大家共同探讨。 一、部分与打印有关的系统变量 VFP本身为我们提供了几个与打印有直接关系的系统变量,它们是:(部分) _BOX 是否打印文字边框,.T.=打印 _GETNPD 指定或保存打印机接口驱动程序的文件名。 _PADVANCE 设定打印纸进纸方式,=FORMFEED(默认)整张进纸。 _PAGENO 设定或保存当前的打印页号。 _PBPAGE 设定或返回打印的起始页号。 _PEPAGE 设定或返回打印的终止页号。 _PCOLNO 设定或返回当前打印头的列。 _PLINENO 设定或返回当前打印头的行。 _PCOPIES 设定或返回打印份数。 _PLENGTH 设定或返回打印纸的页长,默认=66行长。 _PPITCH 设定打印机的打印密度。 _PQUALITY 设定打印机的打印质量。 ... ... 这些变量在设计报表程序时,有些是很有用的,故在此列出。 二、一些常用的打印技巧 1.怎样打印指定的页 REPORT FORM XXXX RANGE 2,5 TO PRINTER &&从第2起打至第5页止 2.如何计算总页数,以实现“第?页/总?页” 在打印前根据细节区所打印的记录条数,先进行计算,然后再打印,具体代码: PUBL mPAGE SELE XXX &&xxx=供打印的数据表 XX=10 &&XX=细节区所打印的记录条数 mPAGE=IIF(RECCOUNT()%XX=0,INT(RECCOUNT()/XX),INT(RECCOUNT()/XX)+1) mPAGE就是总页数,这样在需要总页数的地方就可直接引用mPAGE变量了。 3.如何使报表打满一页 如果打印的记录不足一页,“页注脚”会自动上移,影响报表的美观,解决的 办法和上面的差不多,即补足一页中所缺少的记录(补足空白记录): SELE XXX &&xxx=供打印的数据表 XX=10 &&XX=一页细节区所打印的记录条数 mI=RECCOUNT()%XX &&取得缺少的记录条数 FOR I=1 to mI APPEND BLANK ENDF 4.报表在设计时明明可以打印,可一安装到其他机器或重装系统后,就会出现 “XXX 带区太大不能放入页中”等提示,而且无法正常退出(尤其是对自定义纸张 大小的程序),这是为什么呢? 我们用报表设计器设计的打印程序,保存退出后,磁盘上就会出现 .frx和.FRT 文件,我们的所有设计均保存在这两个文件中。在VFP中 .frx相当于.DBF表,.FRT 相当于.FPT备注型文件,我们用USE XXX.frx 可以象打开.DBF文件一样打开.frx文 件,在.frx文件中有个Expr备注型字段名,在这个字段名中有如下内容:其中()内是 我所加的译文 ====================================================================== RIVER=winspool DEVICE=Epson LQ-1600K OUTPUT=LPT1: ORIENTATION=0 PAPERSIZE=256 (纸张大小) PAPERLENGTH=1000 (纸张长度) PAPERWIDTH=1600 (纸张宽度) DEFAULTSOURCE=8 (默认来源) PRINTQUALITY=180 (打印质量) COLOR=2 YRESOLUTION=180 TTOPTION=1 ====================================================================== 从这个Expr备注型字段里可以看出:PAPERSIZE=256 这里的256表示是自定义纸张, 如果: PAPERSIZE=9 为A4、11为A5 具体数据见VFP帮助的Printfo()一节, 而: PAPERLENGTH=1000 (纸张长度) PAPERWIDTH=1600 (纸张宽度) 则分别代表自定义纸张的长度和宽度。 之所以会出现上面提到的问题,是因为系统重 新安装打印机后,WIN系统一般默认的是A4打印纸,与我们设计时保存在.frx文件里的 纸张不符,因而造成这种情况。 那么如何避免出现这个问题呢? 下面是一段检测纸张类型的代码,供您参考: 这段代码必须放在执行report form ... 命令前。 use xxx.frx in 0 ALIAS mPrint &&在空闲工作区以mPrint别名打开xxx.frx文件 x=atcline('PAPERSIZE',mPrint.Expr) &&取得PAPERSIZE在Expr字段中的行 sSIZE=subs(mline(mPrint.Expr,x),11) &&取得设计时保存的纸张类型 mSIZE=allt(str(Prtinfo(2))) &&取得当前打印机默认的纸张类型 x=atcline('PAPERLENGTH',mPrint.Expr) &&取得纸张长度在Expr字段中的行 sLEN=subs(mline(mPrint.Expr,x),13) &&取得纸张长度 x=atcline('PAPERWIDTH',mPrint.Expr) &&取得纸张宽度在Expr字段中的行 sWIDTH=subs(mline(mPrint.Expr,x),12) &&取得纸张宽度 use in 'mPrint' &&关闭xxx.frx文件 if sSIZE=mSIZE &&如果相符,则正常打印 report form xxx.frx to printer else Messagebox('请设定打印机纸张为自定义:长='+sLEN+',宽='+sWIDTH,0+48+0,'提示') report form xxx.frx to printer prompt &&打印前先打开打印机设置对话窗口 endi 5.不让打印的结果显示在屏幕上 report form xxx.frx to printer Noconsole 6.打印或打印预览时,如何使系统打印工具条不出现 系统提供的打印工具条,我们无法检测其各按钮的事件,不能掌握用户当时操 作的情况,那如何不让它出现呢? 首先您得做一个表单(最好是模式表单),用于代替系统的预览窗口(Preview),然后: do form dybd &&打开这个表单 report form xxx.frx windows dybd 这样系统提供的打印工具条就不会出现了。 当然如果自己再做个类似于打印工具条的类,既可掌握按钮事件又美观就更好了, 注:经查VFP3.0可能没有windows子句。 避免VFP中出现“Cannot Quit Visual FoxPro” (收藏) 一、出现“Cannot Quit Visual FoxPro”的原因 第一种原因是:在执行了read event后,read event事件处于活动状态,此时,试图使用系统窗口的关闭按钮退出Visual FoxPro(或Visual FoxPro编写的应用,以下同),则会出现“Cannot Quit Visual FoxPro”。 在Visual FoxPro3.0中,即使read event事件处于活动状态时,也可以调用Quit直接退出。但是,在Visual FoxPro5.0中,当read event事件处于活动状态时调用Quit则不行,就会出现“Cannot Quit Visual FoxPro”。应先执行clear event关闭read event事件,然后才能正常退出。 另一种原因是:在Visual FoxPro5.0中,如果在还有活动表单的情况下,无论使用系统窗口的关闭按钮,还是调用Quit,都会出现“Cannot Quit Visual FoxPro”。这时,要正常退出,必须先关闭活动的表单。 二、避免出现“Cannot Quit Visual FoxPro”的方法 要避免出现“Cannot Quit Visual FoxPro”,在欲退出之前,要先执行clear event。在Visual FoxPro5.0中还要先关闭活动的表单。 在Visual FoxPro3.0中,在程序的开头可以加上 ON SHUTDOWN QUIT 这样,在欲退出Visual FoxPro时,程序自动直接调用Quit退出,避免出现“Cannot Quit Visual FoxPro”。 在Visual FoxPro5.0中,程序的开头可以加上类似以下的程序 ON SHUTDOWN DO MyQuit PROC MyQuit CLEAR Event IF _SCREEN.cFormCount>0 DIME TmpForm[—SCREEN.FormCount] FOR i=1 TO —SCREEN.FormCount TmpForm[i]=—SCREEN.Forms(i) ENDFOR FOR i=1 TO —SCREEN.FormCount TmpForm[i].Release ENDFOR ENDIF QUIT 这样,在欲退出Visual FoxPro时,程序先自动清除read event事件,关闭所有的表单,然后调用Quit正常退出。 VFP9.0中通过FFC类库使用GDI+ (收藏)第一部分 原著: Walter Nicholls 翻译: YAUR 一套新的GDI+ FFC图形类将随着vfp9.0最终版一起发布,不过你可以去VFP的官方网站上(http://msdn.com/vfoxpro)的下载说明中先睹为快。这套GDI+类预计在8月上旬或之后不久就可以下载了,它可以在vfp9.0 beta版中使用,下载网址还是上边提到的那个,在这篇文章中,Walter Nicholls 介绍了本系列(共四部分)中的第一部分:直接在vfp表单上绘图。 在 vfp9中最令人期待的和最让人兴奋的新特点之一就是增强的报表引擎,尽管这个报表引擎有众多令人激动的特点,不过,最让我兴奋的是我们可以通过编写用户插件来修改和扩展报表中类目的外观 图1 有两种方法可以做到这些,其中之一是使用ReportListener类,它提供了一种可以钩住(或译关联监控 hook)报表产生和显示过程的方法。另外一个则是直接访问用来显示报表的GDI+图形介面。 就像一份额外的红利一样,vfp9.0中的对ReportListener类的一些支持改进使得在其他地方也可以使用GDI+,做一个详细的计划,你也可以写出一个单独的可以在表单、报表上绘图、甚至将图片存为档、存入磁片的GDI类。 VFP自带了一套类库的集合,称之为"功能类",就是我们常说的FFC,在9.0中包含了一个类库:_GDIPPLUS.VCX,它给你提供了在vfp应用程式中使用GDI+的捷径。 注:我们期望GDI+类库能够在2004年8月份的早些时候或之后不久就能够在微软的网站中提供下载,大家注意微软的vfp主页中的一些公告(http://msdn.com/vfoxpro) 在这个关于GDI+ FFC类库介绍系列的第一部分,我们提供给你们一个旋风般的旅行,并且也为你们示范如何在vfp的表单上绘图,在第二部分,我将为你们展示如何使用一些技巧来扩展你的报表系统:结合GDI+类库使用ReportListener类。 我将做的让它看起来容易至极 看看前面几页,找找"如何做一个圆形图"这一届,这个图片是一个通过这篇文章而完成的一个最终产品,并且我承诺将会使这个过程看上去很容易,到文章的结尾,你就会发现,这一切是的的确确的非常简单。 紧记这篇文章所描述的是基于 vfp9 beta版的(version 09.00.0000.1720). 所以在写这篇文章的时候,vfp9的一些功能并不是一成不变的,它可能和最终正式版由一些不同,然而,你应该可以成功的用vfp9 BETA版来使用这些发布在这里的范例(文章附带的示例) FFC类库和GDI+ API的关系 我们所指的"GDI+"实际上为程式师提供了好几种形式。在.net世界中,它们有System.Drawing 以及与之相关联的命名空间。C++程式师使用一个几乎差不多的在gdiplus.h中的API定义,然而,通过GDIPLUS.DLL(vfp开发人员也可以利用这些api来进行介面增强)提供的介面实际上是被称之为"GDI+ Flat API"的非可视介面。 为了使GDI+干起活来更加容易,新的VFP9的FFC类库GDIPLUS.VCX提供了一个非常类似于.net系统中 System.Drawing 命名空间的视觉化介面,这就意味着那些最初为.NET和C++程式师所写的书籍、文章和类似于MSDN的参考资料通过使用FFC类库同样可以应用到VFP中,当然一些小的改动该是要做地。 主要的区别在于: . 所有的类名都被冠以"GP"字母的首码 . 一些属性的名字是不同的(例如用PenWidth来代替Width) . 用#define 来定义的常量也会加上"GDIPLUS_" 首码 . 它仅仅是GDI+完整功能集合的一个子集,不过扩展起来是容易地 这些不同中的大部分时决定于开发语言的约束,例如,"Point"和"Width"在vfp中有其他的含义, integers和floats的差别和其他开发语言不同,表1展示了_GDIPLUS.VCX的类定义和.net中相同类的对比关系。 表1 FFC 类 功能描述 .NET 类 GpGraphics 绘图介面(画布)和绘图操作 Graphics GpColor 由红、蓝、绿及alpha组成的颜色 Color GpPoint 2D平面座标 Point, PointF GpSize 尺寸大小 (宽度, 高度). Size, SizeF GpRectangle 位置和尺寸集合 Rectangle, RectangleF GpPen 绘制直线和曲线 Pen GpBrush 数字画笔类的抽象基类 Brush GpSolidBrush 使用纯色填充区域 SolidBrush GpHatchBrush 使用图案填充区域 HatchBrush GpFontFamily 描述一种字体的同类字体 (例如, "Arial"). FontFamily GpFont 用来修是文本的集合,包括大小,字体等 Font GpStringFormat 文本排版、排列资讯,例如:居中 StringFormat GpImage 图片类的基类:包括向量图和点阵图 Image GpBitmap 由2D图元资讯阵列组成的图形 Bitmap 在表单上使用GDI+ 当我开始在vfp中使用GDI+的时候,因为GDI+早已在VFP中使用了,所以我期望我能更合理的使用GDI+功能并且让所做得事情变得更好。看起来似乎我是错的,不过好像不是太错,在使用GDI+功能的时候有些环节你还是要仔细一点,而且你的工作也不得不受到一些制约。 我们将使用GDI+的功能来在表单上直接绘制来取代通常使用的控制项,例如图像和容器控制项,方法就是使用表单事件:Paint(),它是在重绘表单背景和所有表单上的物件的时候被触发的。 保持清洁的表单介面 你应该首先确保你想绘制的表单区域没有被VFP中的其他任何操作或物件来使用,作为开始练习的情况,不要在上面放置其他的控制项(按钮、文本框等),因为它们将导致重绘冲突,我喜欢在表单上放一个隐藏的形状控制项,在绘图即将发生的时候将它显示出来,这不仅提醒我保持其他控制项不来骚扰,也使得我在编写其上绘图的位置和尺寸的代码的时候方便了许多。 如果你想在一个页面或页框中绘图的时候,你必须采用一些机制来确保这些绘图代码只能在相应的页面处于活动的情况下才能执行。 最后,不要忘记了”?”语句或者使用了set talk on导致的输出。你可以用将表单属性AllowOutput 设置为.F.的方法来避免输出。它可以防止程式将你的表单作为活动输出视窗,我经常在我的基类表单中设置这个属性,因为这个设置常常不太容易想起来。 隐屏绘图 为了改善性能,VFP通常使用一个称之为”双缓冲”的技术,在这种模式下,VFP不是直接在视窗中绘制控制项,取代它的是绘制成一个隐屏点阵图,然后在需要的时候复制这个图像来刷新视窗,这种机制的速度远比那种在视窗或视窗中的物件在改变大小、位置等的时候就刷新快的多。 不爽的是,我们却不能使用这种隐屏介面模式,因为在视窗被刷新的时候俄我们对表单介面的所有绘图操作会被删除,而且paint()事件仅仅是在隐屏点阵图被重绘的时候才被触发的。 隐屏绘图 围绕这个工作是属于本篇文章的范围之内的,所以现在应该关闭隐屏点阵图的模式来运行附带的示例,或者已经知道这个限制了。你可以使用SYS(602,0)来禁止使用隐屏点阵图模式,使用SYS(602,0)就可以改回来了。这是一个全局设定,所以我不建议你在你的表单的Init()中或其他的类似全局事件中载入这个设置。 GDI+初始化 在任何GDI+功能被使用的时候,GDI+系统必须被“启动”。VFP可以自动做这些,不过仅仅是在需要的时候,如系统自然也不知道你的想法!所以你可以完全自己来载入GDI+,仅仅一句代码: createobject( "ReportListener" ) 你不用担心GDI+的释放,VFP会在应用程式关闭的时候为你做这个的。 设置一个基础表单 在本文的剩余部分,我们将使用同样的基础表单来干活,这是一个modeless, resizable表单,而且上边还有一个居中隐藏的形状控制项(shpPlaceholder),看图2。我将使用新的锚属性(VFP新增的)来确保当表单尺寸改变的时候这个形状控制项尺寸也能变化。表2展示了一些表单的主要属性和方法,同样在附带的下载档gpintro_blank.scx中这些属性已被搞定。 图2 属性方法 值和表述 Width, Height 350, 250 AllowOutput .F. BorderStyle 3 (Sizable) Caption "Blank form - GDI+ Intro" WindowType 0 (Modeless) Init() * 确保 GDI+ 已经初始化 createobject("ReportListener") shpPlaceholder.Left, .Top, .Width, .Height 25, 25, 300, 200 shpPlaceholder.Anchor 15 (保持和表单所有边距相同) shpPlaceholder.Visible .F. 表2 我假定你已经复制了FFC类库到你的专案根目录中的FFC/目录中,如果不想这样,调整一下相应得路径,现在,我们准备开始使用GDI+。 出发:GpGraphics对象 要想视觉化的来做这些事情,你需要GpGraphics物件,它描绘一个视窗介面、一个报表页或者其他任何可能的物件。GpGraphics物件提供了查找(修改)一个绘图介面的尺寸和解析度,还有绘制介面的方法,表3展示了一些GpGraphics物件非常重要的属性和方法。 说明: 绘制各种形状的轮廓 FillRectangle( oPen, x,y, width,height ) FillRectangle( oPen, oRect ) FillRectangles( oPen, aRects[] ) FillEllipse( oPen, x,y, width,height ) FillEllipse( oPen, oRect) FillClosedCurve( oPen, aPoints[] ) FillPie( oPen, x,y, width,height, start,sweep ) FillPie( oPen, oRect, start, sweep ) FillPolygon( oPen, aPoints[] ) Description: 绘制各种填充图形 DrawImageAt( oImage, x,y ) DrawImageAt( oImage, oPoint ) DrawImageScaled( oImage, x,y,width, height ) DrawImageScaled( oImage, oRect ) DrawImagePortionAt( oImage, oDestPoint, oSrcRect, nSrcUnit ) DrawImagePortionScaled( oImage, oDestRect, oSrcRect, nSrcUnit ) 说明: 绘制图形或半帧像. DrawStringA( cString, oFont, oRect, [oStringFormat], [oBrush] ) DrawStringW( cUnicodeString, oFont, oRect, [oStringFormat], [oBrush] ) 说明:绘制文字. MeasureStringA( cString, oFont, layoutarea, [oStringFormat], [@nChars], [@nLines] MeasureStringW( cUnicodeString, oFont, layoutarea, [oStringFormat], [@nChars], [@nLines] 说明: 计算文本的大小 ResetTransform() TranslateTransform( xOffset, yOffset [,matrixorder] ) RotateTransform( nAngle [,matrixorder] ) ScaleTransform( xScale, yScale [,matrixorder] ) 说明: 改变图形坐标系 Save( @graphicsstate ) 说明: 保存当前图形物件的状态。 Restore( graphicsstate ) 说明: 恢复前一次保存的状态。 表3 在某些情况下,GDI+物件已经存在而且我们需要给它一个简单的VFP介面。例如,VFP9的报表系统内部就使用了GDI+。报表中的每一页都被一个图形物件封装了,通过存储于ReportListener的一个属性里的控制码,在这种情况下,就有一个简单的GpGraphics示例,而且俄I你可以通过这个控制码来访问它(更详细的内容在下篇文章中描述)。 在其他情况下,你必须自己创建一个GDI+图形物件,例如:要绘制一个表单,你必须创建一个基于原始表单视窗的控制码(HWnd)的GpGraphics对象。 local oGr as GpGraphics of ffc/_gdiplus.vcx oGr = newobject('GpGraphics','ffc/_gdiplus.vcx') oGr.CreateFromHWND( Thisform.HWnd ) GpGraphics物件已经在这个表单中进行了适当的设置,它的座标空间为(0,0)位置来代替左上角。如果你没有一个预先搞好的GpGraphics基类物件,例如你要产生一个点阵图并且保存到档,那你可能要做很多细微的工作。 绘制线条和形状 为了展示第一个示例,我将持续快速的介绍一系列类,我们将从绘制一个类似于图3的椭圆形来开始。 图3 要在视窗介面中绘制一些图形,我们需要一个工具,在这一步中,我们使用GpPen物件,它可以绘制线条(这里是椭圆的轮廓线)。我们也需要设置颜色工具,使用GpColor物件,表4和表5中列出了一些非常重要的GpPen和GpColor物件的属性和方法。 属性/方法名 说明 PenColor 画笔颜色(ARGB 值) PenWidth 画笔宽度 PenType 画笔类型 (参考 GDIPLUS_PENTYPE_ 常量). Alignment 画笔对齐方式(参考 GDIPLUS_PENALIGNMENT_ 常量). DashStyle 使用的虚线类型 Create( color [, width] [, unit] ) 用给定的颜色画图 CreateFromBrush( brush [, width] [, unit] ) 创建一个基于刷子的画笔 Init( color ) 设定初始颜色 Init() 产生一个空的画笔物件 (使用时必须调用Create()) 表4:GpPen的属性/方法描述 属性/方法名 说明 Red Red component (0...255). Green Green component (0...255). Blue Blue component (0...255). Alpha Alpha (透明度). 0 =完全透明, 255 =完全不透明 ARGB GDI+ 颜色 (32位整形参数). FoxRGB Visual FoxPro 颜色值,可以用 RGB()或 GETCOLOR() 函数返回.注意: 这个值不包括alpha资讯, 并且要将Alpha属性值设置为完全不透明(255). Set( red,green,blue [,alpha] ) 设置颜色 (如果没有制定Alpha,那默认为 100% 不透明) Init( red,green,blue [,alpha] ) 创建特定的颜色(如果没有制定Alpha,那默认为 100% 不透明) Init( argb ) 创建单一的GDI+ color Init() 创建默认颜色,黑色 表5:GpColor的属性/方法描述 以下代码创建了一个深蓝色的GpColor物件 oLineColor = newobject( ; 'GpColor','ffc/_gdiplus.vcx','', 0,0,100 ) 以下代码创建了一个基于以上颜色的GpPen物件,而且制定宽度为3图元 oPen = newobject('GpPen', 'ffc/_gdiplus.vcx' ) oPen.Create( m.oLineColor, 3 ) 要在表单介面中绘制一个椭圆形,我们使用GpGraphics物件的DrawEllipse方法,以下代码将使用之前创建的画笔物件来绘制这个椭圆,它的位置是由我们之前隐藏的那个占位元形状物件所决定的。 oGr.DrawEllipse( m.oPen ; , Thisform.shpPlaceholder.Left ; , Thisform.shpPlaceholder.Top ; , Thisform.shpPlaceholder.Width ; , Thisform.shpPlaceholder.Height ; ) 将这些代码放置到表单的Paint()事件中,当你运行以后,你就能看到如图3的增强图形了。 关于矩形 在接下来的这个例子中,我们将再次使用同样的座标,为了节省代码,我们将创建一个GpRectangle物件并且将之作为一个单独得参数来引用,这里还是绘制椭圆的代码碎片,这时我们使用GpRectangle物件来定义椭圆区域的范围。 oBounds = newobject( ; 'GpRectangle','ffc/_gdiplus.vcx','' ; , Thisform.shpPlaceholder.Left ; , Thisform.shpPlaceholder.Top ; , Thisform.shpPlaceholder.Width ; , Thisform.shpPlaceholder.Height ; ) oGr.DrawEllipse( m.oPen, m.oBounds ) 填充区域:刷子 现在,让我来用量绿色来填充这个椭圆,为了绘制填充区域,我们需要一个新的工具:刷子。GDI+ FFC类库提供了几个刷子,但是用春色来绘制我们还是要用GpSolidBrush物件。 * 创建一个亮绿色的刷子 local oFillColor as GpColor of ffc/_gdiplus.vcx ; , oBrush as GpSolidBrush of ffc/_gdiplus.vcx oFillColor = newobject( ; 'GpColor','ffc/_gdiplus.vcx','' ; , 0,255,0 ) && green oBrush = newobject( ; 'GpSolidBrush', 'ffc/_gdiplus.vcx', '' ; , m.oFillColor ) 要在荧幕上填充椭圆,我可以使用FillEllipse()方法。 oGr.FillEllipse( m.oBrush, m.oBounds ) 要绘制椭圆的轮廓线,我们必须首先调用FillEllipse()方法,然后使用DrawEllipse()。最后你就能看到如图4所示的结果了。 图4 来个 饼形图 怎么样? 现在你已经有了些基础,来看看怎么画一个 饼形图 吧。下面的代码我用了一个包含2列的阵列作为表单属性,每1列都为饼形图的1个切片提供资料,来看一下伪代码: aSliceData[ nRow, 1 ] = data value aSliceData[ nRow, 2 ] = fill color for the slice (in GDI+ ARGB format) nSliceTotal = total of all the data values Paint()事件会调用这个阵列并计算适当的角度来画这个饼形图。这里有个新概念:DrawPie()和FillPie()方法,他们就象DrawEllipse 和 FillEllipse 一样,用一个 起始角 和一个 扫描角(切片的一个范围),来描述 切片角度。 常式我这是故意弄得简单些,其实你可以很容易地扩展她,比如显示标签或标记值(或许你要把其中一个切片拖出来)。尽管用远端的SQL查询获取切片资料可能不是个好主意,但你也别在Paint()事件中放太多的代码。 下面的是Paint()的关键代码,完整的源码在附带的 gpintro_piechart.scx 里,图5是执行结果。 图5 * Create the drawing objects local oLineColor as GpColor of ffc/_gdiplus.vcx ; , oPen as GpPen of ffc/_gdiplus.vcx ; , oBrush as GpSolidBrush of ffc/_gdiplus.vcx oLineColor = newobject( ; 'GpColor','ffc/_gdiplus.vcx','' ; , 0,0,0 ) && black oPen = newobject( ; 'GpPen', 'ffc/_gdiplus.vcx',''; , m.oLineColor ) && 1-pixel-wide pen oBrush = newobject( ; 'GpSolidBrush', 'ffc/_gdiplus.vcx','' ) oBrush.Create() && don't specify colour yet * Work out from the slice data what the starting * angles should be local nSlices nSlices = alen(This.aSliceData,1) local aAngles[m.nSlices+1], iSlice aAngles[1] = 0 && start at 0 degrees (+ve x axis) for iSlice = 2 TO m.nSlices aAngles[m.iSlice] = aAngles[m.iSlice-1] ; + 360*This.aSliceData[m.iSlice-1,1]/This.nSliceTotal endfor aAngles[m.nSlices+1] = 360 && Stop at full circle * Draw the pie slices for iSlice = 1 to m.nSlices oBrush.BrushColor = This.aSliceData[m.iSlice,2] oGr.FillPie(m.oBrush, m.oBounds ; , aAngles[m.iSlice] ; , aAngles[m.iSlice+1] - aAngles[m.iSlice]) oGr.DrawPie(m.oPen, m.oBounds ; , aAngles[m.iSlice] ; , aAngles[m.iSlice+1] - aAngles[m.iSlice]) endfor 写上文本 最后一步是在饼形图上写上一些文本。因为饼形图上包含了很多不同的颜色,一般样式的文字可能难以阅读,因此我给她带上阴影,让我们在图上写一个“VFP9 is cool!”。这里有个新工具:GpFont类,详见 表6。 属性/方法名 说明 FontName 字体名称,比如:"Arial." Style 比如:粗体、斜体(GDIPLUS_FONTSTYLE_ bits的集合) Size 字大小 Unit 默认是points(1/72英寸) Create( fontname, size [,style [,unit]] ) 指定字体字形建立GDI+的字体物件 Create( GpFontFamily, size [,style [,unit]] ) 指定字体系列和字形建立GDI+的字体物件 Init( fontname/family, size [,style [,unit]] ) 初始化一个空的GpFont并调用Create() GetHeight( GpGraphics ) 获取指定的GpGraphics物件的字体行间距 GetHeightGivenDPI( nDPI ) 用指定的解析度(点/英寸)获取字体行间距 表6:GpFont类 在这个饼形图上写字,我们建立一个GpFont物件,设为 Arial,粗体,32Points高: oFont = newobject('GpFont','ffc/_gdiplus.vcx') oFont.Create( "Arial" ; && font name , 32 ; && size in units below , GDIPLUS_FONTSTYLE_BOLD; && attributes , GDIPLUS_UNIT_POINT ; && units ) 我们还需要一个Brush物件,Brush前面我介绍过,我们必须指定这些字写在哪儿--让我们把她放到图的中间。我们已经有了一个GpRectangle物件定义了边界,因此最后一步就是告诉GDI+如何在这个矩形中排版。那么,欢迎来到 GpStringFormat类(请看表7) * Get a basic string format object, then set properties oStringFormat = newobject( ; 'GpStringFormat','ffc/_gdiplus.vcx') oStringFormat.Create( ) oStringFormat.Alignment ; = GDIPLUS_STRINGALIGNMENT_Center oStringFormat.LineAlignment ; = GDIPLUS_STRINGALIGNMENT_Center 属性/方法名 说明 Alignment 文本的水准对齐方式 LineAlignment 垂直对齐 FormatFlags 请看 gdiplus.h 里的 GDIPLUS_STRINGFORMATFLAGS_ 常量 Trimming 如何TRIM一个字串 HotkeyPrefix Windows "hotkey" 首码 Create( [flags] [, languageID] ) 建一个新的字串格式物件 GetGenericDefault( [lMakeClone] ) 复制一个默认字串格式或获取一个默认字串格式的控制码 GetGenericTypographic( [lMakeClone] ) 复制一个默认字串格式式样或获取一个默认字串格式式样的控制码 表7:GpStringFormat类的一些属性和方法 这个字串我们要画2次:上面是纯色,偏移几点再来个半透明的阴影。这里又要解释一个新概念:颜色值的“alpha”元件。代码如下: * Now draw the text with a drop-shadow. * First, shrink the bounding box by 4 pixels * and move 4 pixels to the right and down oBounds.W = oBounds.W - 4 oBounds.H = oBounds.H - 4 oBounds.X = oBounds.X + 4 oBounds.Y = oBounds.Y + 4 * and draw the shadow in a 66% black oBrush.BrushColor = 0xA8000000 oGr.DrawStringA( This.cOverlayText ; , oFont, oBounds, oStringFormat, oBrush ) * Now move the bounding box back to its original * position, and draw the same string in opaque white oBounds.X = oBounds.X - 4 oBounds.Y = oBounds.Y - 4 oBrush.BrushColor = 0xFFFFFFFF oGr.DrawStringA( This.cOverlayText ; , oFont, oBounds, oStringFormat, oBrush ) 你现在应该可以看到如 图6 所示。注意:要是你RESIZE这个表单,这文本也会跟着伸缩。你还可以用 GpStringFormat物件 的其他属性控制她的伸缩。 图6 结束语 你要注意写文本的方法叫“DrawStringA”而不是“DrawString”,这是因为这个函数有2个版本。DrawStringA为VFP中8位元的字串而设计,DrawStringW为16位元的Unicode字串而设计(这个W是WIDE的意思)。这同样用于ReportListener,在那里你用Unicode往往比用VFP的字串值更好。 建一个象GpGraphics和GpFont这样的GDI+物件是需要代价的(很占CPU资源)。为了提高性能,你可以缓冲这些Paint()事件,但你必须注意有时这些缓冲会失效,比如,表单RESIZE了,或你画的那个绑定框改变了等等。 性能方面还有些小技巧。如果你习惯用ARGB值,你就完全可以不使用GpColor物件--你使用32位元的颜色值就好了。还有个技巧可以充分提高性能:双缓冲,不过这个技巧在另一篇文章里面。 下个月:图表在报表中的应用 这个系列的下个月部分,我要展示如何把图表移到VFP9的报表里面,而且还有很多有趣的技巧。 CA资料-我眼中的 Visual FoxPro 8 (收藏)本文已在《程序员》杂志 2003 第一期发表 鸣谢 经常来 BOE 的朋友都知道,我早就是一个业余的 Foxer 了,随着时间的推移,每一次写东西都是惴惴不安的,真的担心弄出一些误导言论或者是贻笑大方。 每一次完成初稿我都会给一些朋友预览,好听听他们的建议。漫步者、Boby、将来是我 这三位网友就是我的第一批读者,感谢他们一次又一次读那些晦涩的言语、理解不成熟思想。特别是漫步者,几乎成了我的私人秘书,就拿这篇东西来说,正文中所有的小标题都是他加上的。他说,这样能让读者容易看懂…… 成文后,属上的都是我的名字,真是有白占别人劳动的嫌疑,这里向这些朋友表示深深地感谢! 写在开始 那一日,我连夜等待 Visual FoxPro 8下载,直到凌晨2点,实在是熬不下去,这才睡下;4点时分,被闹钟催醒,睡眼朦胧中看到了 Visual FoxPro 8 的下载连接,迷迷糊糊的也知道动作是怎样完成的,反正早上醒来 Visual FoxPro 8 已经在我的硬盘上了。解压、安装,一阵兴奋难以言表;忽又想起,还要上班,于是烧盘、走路……这样,开始了我的 Visual FoxPro8 探索之旅。 原来以为,已经看过大量原版资料(感谢 夜来香、RMH、Fbilo 他们已经翻译这些东西,英文不好的朋友现在也能享用了),我研究Visual FoxPro 只需要少量的精力。嘿嘿,谁知道:我竟然花费了整整10天才看完了我所感兴趣的东西。而且是每天作战到凌晨,家人意见不少,但是他们哪知道我乐在其中呢? 今年10月,微软公司如期发布了 Visual FoxPro 8b 版,这是自 Visual FoxPro 3 以来又一个精彩的版本,一下子吸引住了全球 Foxers 的眼球!下文是我学习、探索 Visual FoxPro 8b的一些感受,愿与读者分享。 (一)结构化异常处理 经常有人问我,你认为 Visual FoxPro 最需要改进的地方在那里。我总是不假思索地回答:异常处理!期盼它能拥有结构化异常处理机制,把异常及其处理限定在最小的范围之内,使代码的封装更优。 Visual FoxPro 8 以前对异常处理还限于全局 On Error 例程和对象的 Error 的事件,它们都有这样一个毛病:一旦出现了异常,程序就必须跳出正在执行的程序模块,执行异常处理例程,这时发生异常前执行的程序模块的上下文已经丢失殆尽,系统很难调整到正常状态或者给调用接口一个明确的答复! Visual FoxPro 8的 Try…Catch…Finally…End Try 结构让人激动不已,它帮助我们方便的抓住“异常”,实时针对各种异常做出反应,缩小异常的影响范围,保护系统的运行状态;也使得程序模块的封装性更好。想想以前应付“异常”而设计的各种方案,一夜之间成了昔日黄花、显得那样苍白无力,真是“沧海变桑田”啊! 仅此一项增强,Visual FoxPro 8就已经光彩夺目,相信任何开发人员都经不起“结构化异常处理”的诱惑,升级到Visual FoxPro 8! (二)CursorAdapter 类 Visual FoxPro 8提供的CursorAdapter 类也是大家所津津乐道的。CursorAdapter 是一个基于松散耦合思想设计的对象化的 Cursor 处理模型。 1、VFP8以前的数据处理 在 Visual FoxPro 8 之前,我这样评价在 Visual FoxPro 里的数据处理:灵活而强健,面向记录处理为主、面向集合处理为辅,采用过程化程序化模型,但可以利用面向对象的 XBase 语言自行封装数据处理对象。 说到Visual FoxPro 处理数据的灵活和强健,归根到底是Visual FoxPro 同时支持 XBase语言和SQL 语言。XBase 语言善于对记录的梳理,这种处理往往是基于“行”的模式;SQL 语句对数据的处理是根据“集合”的概念,按照条件取得“集合”,然后处理。无论采用 XBase 语言处理数据,还是使用 SQL语言,Visual FoxPro 预设提供面向过程的数据处理程序化模型,而不是流行的面向对象的程序化模型。(当然,Visual FoxPro 并不限制开发人员编写自己的数据处理对象。) 那么在 Visual FoxPro 中所谓的“数据”到底是什么呢?是 Cursor!在Visual FoxPro 中整个数据处理是围绕着 Cursor 进行的(而不是 DBF),所以对象化数据处理模型也应该从 Cursor 着手! 2、面向对象的数据处理器 在 Visual FoxPro 设计对象化的 Cursor 是没有太大的意义的,这句话很突兀、让人摸不到头脑! 我们用典型的对象化游标──ADO 组件的 RecordSet 做分析。可以发现RecordSet 中定义了数据集合的来源、数据集合的结构(表结构)、数据的更新回送方式,另外RecordSet还能够对数据集合进行操作,诸如Move、Find、Delete之类的方法。 在 Visual FoxPro 里对象化 Cursor,是不是要照搬 RecordSet 的一套呢?如果由我们来设计这个构架,我们如何取舍呢?我觉得没有必要实现对 Cursor 本身的封装,这样是在自残,我刚才已经说过了:整个 Visual FoxPro 对数据的处理就是对 Cursor 的处理,如果封装了Cursor,就背离了整个体系的构架思路,同时也放弃了 Visual FoxPro 对数据处理的“灵活与强健”的特色。这就是前面提到的“在 Visual FoxPro 设计对象化的 Cursor 是没有太大的意义的”。 对象化 Cursor 没意义,那么我们究竟需要什么呢? 用一个面向对象的 Cursor 的处理(管理)器来管理 Cursor的数据来源、Cursor 的数据结构、Cursor 中数据变动的更新回送;而Cursor 依旧是传统意义上的 Cursor,没有任何改变,依旧可以用 XBase 或者 SQL 语言对直接 Cursor 进行处理──这才是我们需要的。 说到这里,大家会问了,这不就是DBC 中的视图吗?它们在实现的功能上确实有点像,但实质上是有差别的: 首先,视图是 DBC 的对象(组件),利用视图设计的系统对 DBC 的依赖性大,不符合多层体系构架的思路。而我们的对象化 Cursor (处理)管理器与 DBC 无关,完全是程序设计级别上的概念。从多层体系设计的观点看,属于业务逻辑层次。 再者,视图是数据库中的概念,灵活性差、可控制性差;而对象化 Cursor (处理)管理器就不一样了,作为对象,具备了面向对象程序设计的一切好处,很容易定义、改变、维护,更能够继承使用,这都是使用视图所办不到的。 随便就能举出几个视图弱势的例子,有时候数据集合的结构一致,但是数据来源不一样,就必须麻烦的设计几个视图,而使用对象化 Cursor (处理)管理器就只需要改变数据来源属性;又例如,数据来源相同、数据集合结构相同,只不过获取数据的条件不同(Where 子句构造不一样),用视图就必须设计新的视图,而使用对象化 Cursor (处理)管理器只需要改变相关属性即可…… 说到底:视图是数据库的概念,具备的是数据库对象(组件)的特性,而不具备程序化语言的特点,对象化 Cursor (处理)管理器解决的就是这个问题。 3、松散耦合 在视图时代或者是RecordSet时代采用的是紧密耦合的思维,说的直白一点,就是:数据从哪里来就更新到哪里去,程序员并不能在其中做什么干涉,一旦执行了TableUpdate(),就自动更新数据源,没有商量的余地! 松散耦合提供了一种开放的模型,它以 Cursor 为中心,从本质上认识到 Cursor 与资料源的相互关系是:数据采集和变动更新回写。数据采集就是 SQL-Select操作、数据更新回写就是SQL-Insert、SQL-Update、SQL-Delete。松散耦合模型提供 Cursor 数据来源的Select子对象、Cursor 数据添加回写数据源的 Insert子对象、Cursor 数据修改回写数据源的 Update 子对象、Cursor数据删除回写数据源的 Delete 子对象。由于从构架上把Cursor与数据源的各种互动操作分割开来,再辅以事件模型,这就给了开发人员很大的程序化余地! 相比之下,视图虽然也采用类似的思路与数据源互动,但是它把整个互动过程作为一个整体看待,严严实实的包装起来,造成开发人员的参与性差。 4、VFP8的CursorAdapter对象 让我们回过头再看 Visual FoxPro 8 提供的 CursorAdapter 对象,预设情况下支持四种数据来源,分别是:Native(本地资料)、ADO、ODBC、XML。请思考一下,为什么 CursorAdapter 能够支持几种性质完全不同数据来源呢,原因就是采用了松散耦合模型,让开发人员用自己的代码参与资料的获取,使得整个体系的扩展性大大增强,这就是松散耦合的魅力! 概括VFP8中的CursorAdapter,它是:一个基于松散耦合思想设计的对象化的 Cursor 处理模型。CursorAdapter 既保留了 Visual FoxPro 对 Cursor 处理的传统优势,又引入了先进的程序化模型和构架。再次强调它的特色: Cursor 本身没有被对象化,依旧是传统意义上的 Cursor,可以使用一切传统语句处理 Cursor。 Cursor 的管理方式对象化了,可程序化性更好,有利于代码的封装。 采用了松散耦合的设计思维,以 Cursor 为中心、但又分割了 Cursor 与数据源之间的各种操作、再加入事件模型,为开发人员提供了很多参与的机会。 (三)全新的“连接” Visual FoxPro 8在异构数据库程序化的增强,更直接一些就是对 SQL Server支持的改进。我觉得,至少体现在两个方面:一个是通过 CursorAdapter 对象提供一种比远程视图更加灵活、更容易程序化的 Cursor 管理对象;另一个是在原有的基础上革新了“连接”的概念,使得“连接”处理更加独立、“连接”管理更加方便、“连接”更容易被共享! 前面我们已经对CursorAdapter作了分析,接下来就让我们看看“连接”的变化吧! 1、DBC 中的“连接”对象和“连接句柄” 以前我介绍 Visual FoxPro 里的“连接”时,经常强调这样两个概念:DBC 中的“连接”对象(组件)和“连接句柄”。 DBC 中的“连接”对象(组件)只是一种定义,描述怎样通过 ODBC 连接到数据源;“连接句柄”是实例化的“连接”对象(组件)。我们可以通过USE远程视图或者使用 SQLCONNECT() 打开“连接”对象,得到“连接句柄”。 当然这里还有两个更加直接的方式得到“连接句柄”,就是在SQLSTRINGCONNECT()中直接使用连接字符串;还有就是SQLCONNECT()中直接引用操作系统(ODBC.INI)中的DSN 定义。这里需要注意的是,这两种方式跳过了 DBC 的“连接”对象(组件),使得Visual FoxPro 不容易管理它们。也许这个问题在 Visual FoxPro 8 以前还不很明显,但 Visual FoxPro 8 以后应该慎用它们了。 Visual FoxPro 8 以前,“连接句柄”并不独立,主要是“远程视图”自己管理“连接句柄”,其它“远程视图”或者 SPT需要共享“连接句柄”是非常麻烦的,“远程视图”之间共享“连接句柄”,需要定义所有参与共享的“远程视图”的ShareConnection属性为.T.;SPT 要取得“远程视图”的“连接句柄”必须在USE“远程视图”以后,使用 CURSORGETPROP() 函数取得“连接句柄”。除了麻烦以外,还有不能实现的功能,如先期已经存在的“连接句柄”不能被“远程视图”共享。这一切都是因为“远程视图”太过“自主”所致! 2、Statement Handle Vs. “连接句柄” 为了解决“远程视图”的“独断专行”,Visual FoxPro 8 提出了“远程视图”设计时与运行时分离的思路,引入了一个新的关于“连接”的概念── Statement Handle,同时 Visual FoxPro 8 保留了“连接句柄”概念。 从 Visual FoxPro 8 开始开发者直接处理的任何Handle 都是 Statement Handle,而不是“连接句柄”了!进一步讲,“远程视图”使用的是 Statement Handle、SPT使用的也是 Statement Handle。也就是说,我们在 DBC 里建立、设计“远程视图”使用的连接与“远程视图”运行时的连接可以没有任何关系,我们可以在 USE 语句中实时指定“远程视图”将要使用的Statement Handle。这就是 Visual FoxPro 8 针对 C/S 程序化中“连接”的一个改进。 Statement Handle 与“连接句柄”的关系。我以为: ? “连接句柄”是 Statement Handle 的基础,Statement Handle 是“连接句柄”的衍生; ? 真正与数据源连接的是“连接句柄”,而不是Statement Handle;开发人员直接操作的是Statement Handle,而不是“连接句柄”; ? 某一条“连接句柄”可以衍生出一条或者多条Statement Handle,而某一条 Statement Handle只可能对应到一条确定的“连接句柄”; ? 新建一条连接的实例,将(可能)产生一条新的“连接句柄”和一条新的Statement Handle,这条Statement Handle对应到这条“连接句柄”; ? 新建一条连接的实例,如果当前Visual FoxPro 系统中已经存在一条符合以下两个要求的“连接句柄”,这两个要求分别是允许被共享,并且“连接”语句的定义一致(可以认为来源于 DBC 的同一个“连接”对象),这时不会生成一条新的“连接句柄”(不会再与数据源建立一条新的 ODBC 通道),而是直接由已经存在的“连接句柄”衍生出一条新的Statement Handle,这条Statement Handle共享原先那条“连接句柄”。 ? 当一条“连接句柄”衍生的 Statement Handle 全部被释放了以后,“连接句柄”才会释放。 ? 能够衍生出多条Statement Handle的“连接句柄”一定是根据 DBC中“连接”对象(组件)所创建的。使用连接字符串(SQLSTRINGCONNECT())或者直接引用操作系统 DSN 创建的“连接句柄”只能衍生出惟一条Statement Handle。这是因为,Visual FoxPro 判断不同 Statement Handle是否“同源”的依据是 DBC 的“连接”对象(组件),而不是 ADO.NET那样根据连接字符串定义区分。 从上面的表述中,我们发现:因为“连接句柄”与 Statement Handle 的关系是“一对多”的,如果拿 SQL Server 作为资料源,我们可以建立一条“连接句柄”、根据它衍生多条Statement Handle,实际上连接到 SQL Server 的只有一条连接,而应用程序中却可以有多条Statement Handle,这就是一种新概念的“连接共享”! 3、一个实例 让我们通过下面的例子来具体了解一下Statement Handle与“连接句柄”。设DBC里有一个“连接对象”(组件),名为 “CD1”,连接到SQL Server 数据库。 Con1=SQLCONNECT("CD1",.T.) 连接成功,返回 Con1=1。Con1 表示 Statement Handle ,以后就可以通过这个Con1 打开“远程视图”或者执行 SPT 命令;由于此时系统中并没有存在“连接句柄”,所以同时产生一条“连接句柄”,需要获取“连接句柄”可以通过: ODBC1=SQLGETPROP(Con1,"ODBChdbc") 我这里ODBC1=49551184。这个ODBC1并不需要开发人员关心,也不需要通过程序维护。 接着,我们就根据 ODBC1衍生其它的Statement Handle: Con2=SQLCONNECT("CD1",.T.) 返回 Con2=2。Con2 表示 Statement Handle,以后就可以通过这个Con2 打开“远程视图”或者执行 SPT 命令;我们获取 Con2 的“连接句柄”看一看: ODBC2=SQLGETPROP(Con2,"ODBChdbc") 我这里ODBC2=49551184。说明,ODBC1和 ODBC2 是一个东西,衍生了两条Statement Handle。虽然,程序化中,我们可以使用 Con1或者 Con2,但是真正与 SQL Server 的连接只有一条,就是“连接句柄”。 接着,我们需要关闭“连接句柄”衍生的所有的Statement Handle,才能真正断开Visual FoxPro 与 SQL Server 之间的连接。 SQLDISCONNECT(Con1) SQLDISCONNECT(Con2) 通过这个简单的例子,也许有利于我们来理解新的“连接”。虽然,Visual FoxPro 8在“连接”这一块动了大手术,但是这对于旧的系统影响并不是很大,因为程序化接口并没有变化,Visual FoxPro 只是在更底层的地方加了一层概念。话要说回来,如果要真正用好这个新特性,还是应该改变一下程序化思路。 4、 “连接”体系更改的原因 也许,我们还会有这样的疑问:Visual FoxPro 8为什么要加入 Statement Handle 这样的概念,按照以前的做法直接使用“连接句柄”在各处传递,不是也能够实现“连接共享”吗? 笔者认为这是面向对象化程序化的需要、多层开发的需要。整个构架系统的思路就是更好的面向对象化程序化、更好的多层开发,所以才有了结构化错误处理、CursorAdapter和此处的 Statement Handle。 上文,我使用了“衍生”一词,从词义理解一定是用“连接句柄”显式生成新的Statement Handle,但是从实例代码中大家看到了,产生 Con1和 Con2 的代码是一样的。从理论上讲,就是:Visual FoxPro 能够自行管理 “连接句柄”,开发人员只需要也只应该处理相应的 Statement Handle,而并不需要关心由同一“连接句柄”衍生出的其它Statement Handle。这样对于模块化开发是很有好处的,同时也解决了“共享连接”问题。 关于“连接”的话题谈的差不多了,不过有一个问题是需要注意的:一个“连接句柄”可以衍生多个Statement Handle,在开发中,如果有一个 Statement Handle进入了事务处理状态,这时所有相关的Statement Handle也进入了事务处理状态,这因为 SQL Server 的事务是根据真实的连接区分的——由于某一条Statement Handle 的要求,使得“连接句柄”进入了事务状态,理所当然的该条“连接句柄”衍生的所有Statement Handle也一起进入了同一个事务处理。(当然,不是由已经进入事务状态的“连接句柄”衍生的Statement Handle,不会进入事务状态。) 终于完成了我在 Visual FoxPro 8 中的“首航”,细细揣摩一些 Visual FoxPro 8的新特征,我深深体会到一个指导思想贯穿着整个 Visual FoxPro 8的构架,这个思想就是:支持更完美的对象封装。由此推断:用 Visual FoxPro 8 进行对象化程序化、多层开发一定比以前更加容易、更加灵活! 本文只涉及到 Visual FoxPro 8 的三个新特性,事实上还有更多令人愉悦的东西没能讲到,希望以后能有机会与大家共同探讨!大家可以到: 三思同志 发表于2005-04-23 7:56 AM IP: 218.15.22.*
强大的报表设计器 Visual FoxPro 9.0 的报表设计器(1) Micorsoft公司对新推出的Visual Foxpro 9报表设计器作了显著地改进,同时又与老版本的Visual Foxpro保持了向后兼容性,新版本的报表设计器是一个新旧版本的混合体。 在本文中,你将了解报表设计器对新的数据环境、报表保护、用户界面、对象布局与数据分组功能的增强。最后,你将了解Visual FoxPro 9报表设计器的一个最有用的增强功能:多条明细区带(multiple detail bands)。 报表设计被一个新增的“Xbase报表设计器”的工具代替。它提供了一些新的对话框,并且比以前的版本更方便使用。它还提供了一些旧版本报表设计中所没有的新特色。你可以通过改变一个名为“_REPORTBUILDER”的系统属性来决定使用哪种报表设计器,如下所示: *--如果要使用新版本的报表设计器 _REPORTBUILDER = HOME() + 'ReportBuilder.app' *--如果要使用旧版本的报表设计器_REPORTBUILDER = '' 报表输出引擎:与报表设计器一样,你可以控制是否选用新版本的报表输出引擎。但与报表设计器不同的是Visual FoxPro 9默认报表输出引擎为旧版本方式。主要是因为在新版本的输出引擎中使用了GDI+库,而老版本的输出引擎使用的是GDI库,使用老版本的输出引擎就可以让应用程序可以在不用版本的windows上显示出同样的输出效果。你可以用如下命令来切换你的输出引擎: *--使用新版本的输出引擎 SET REPORTBEHAVIOR 90 *--使用旧版本的输出引擎 SET REPORTBEHAVIOR 80 在下文中我们假定使用的是新版本的报表设计器和输出引擎。 数据环境(DE) Visual FoxPro 9的报表设计器能与让多个报表共享同一个数据环境。数据环境能够以类的方式保存,并在需要的时候被报表载入。这为那些需要制定通用报表数据环境的应用程序提供了方便。 要将数据环境保存为一个类,首先你要为报表定义一个数据环境,然后激活数据环境窗口,并在“File”主菜单中单击“Save As Class...”选项。 这样系统会弹出一个新的对话框(参见图1)。在这种情况下,Save单选按钮组中只有“DataEnvironment”处于允许状态。 图1. 使用“Save As Class”对话框指定要保存的类名以及所在的类库,并将指定报表的数据环境保存在这个类中。 Visual FoxPro 9.0 的报表设计器(2) 载入数据环境 除了能为报表定义数据环境以外,Visual FoxPro 9还能让你将某个报表的数据环境类载入到报表中去。“Report”菜单中的“Load Data Environment...”选项可以让你选择到底载入哪个数据环境。 通过报表设计器载入数据环境 如果要为一个新报表载入数据环境,那源数据环境的所有代码和成员变量都会复制到新报表中。这表明当你改变原来报表的数据环境后,并不会对新报表的数据环境产生任何影响。 图2显示了当你从“Report”主菜单中单击了“Load Data Environment...”选项后弹出的属性对话框。你可以在里面选择从哪个报表中复制源数据环境。 图2. 单击”Data Environment“选项卡,从中选择你要从哪个报表中复制数据环境 在上图中,单击“Copy from another report file”单选按钮,然后单击“Select...”按钮,这样会弹出一个打开对话框,你可以从中选择从哪个报表中复制。如果你选中了一个报表,那系统弹出一个确认框。 假如我们要将某个报表的数据环境复制到当前报表中去,Visual FoxPro 9会警告你将覆盖当前报表的数据环境,你必须选择“是”才能继续进行复制。这个提示功能可以防止由于你的误操作而将当前报表的数据环境覆盖掉。如果你选择“否”的话,那复制就会取消,如果选择的是“是”,那就会真正进行复制操作,并且当操作完成后,系统会出现另一个对话框,提示你操作完成。 现在数据环境已经复制成功了,你可以操控新的数据环境。但你要始终记得原报表数据环境的改变并不会对新的数据环境有任何影响。 从一个类中载入数据环境 当要从一个类中载入数据环境时,你必须要为新报表的数据环境写一些额外的代码,使得它能够动态地绑定源数据环境,并且初始化它的一个实例。这意味着如果从类中载入数据环境时,对源数据环境做的所有改动会影响到所有使用它的报表。 你同样可以用图2所示的报表属性对话框来完成这个效果,先单击“Link to a visual DE class”单选按钮,然后从系统弹出的打开对话框中选择你要载入的类库以及类名,当你点击确定按钮后,当前报表的数据环境将得到更新,并且系统会给出相应提示信息。 其实Visual FoxPro自动为数据环境的如下5个方法中加入了一些代码:Init()、BeforeOpentables()、AfterCloseTables()、Destroy()和Error()。有些方法中加入的代码非常简单,仅仅是一个DODEFAULT()命令,这个命令不执行任何操作。其原因是BindEvents()方法必须保证数据环境的这5个方法中的代码行数超过一行才能执行。你可以手动查看这些自动生成的代码,但我强烈建议你别去动这些代码。 Visual FoxPro 9.0 的报表设计器(3) 保护 如果要在Visual FoxPro 9使用报表设计器或者标签设计器,你可以为一个或多个的布局对象设置保护。这种特性可以让你的用户只能对报表进行有限的修改。 你可以为布局对象设置5种保护模式,域对象有着另外的保护选项。带区(Band)有两种保护模式可供你选择。并且你也可对报表本身设置不同的保护方式。 保护一个对象 要在报表设计器中为一个布局对象设置保护,通过激活此对象的属性对话框即可操作,你可通过用右键点击此对象,并在弹出的快捷菜单中选择相应的菜单项,或者直接双击此对象。图3显示了一个布局对象属性对话框的保护页,你可以为布局对象设置如下5种保护方式: • 对象不能被移动或改变大小。它使得用户不能在设计器中移动此布局对象,并且用户不能改变此对象的大小。 • 对象不能被修改。它使得用户不能修改此布局对象的属性。 • 对象不能被删除。 它使得用户不能删除此对象。 • 对象不能被选中。用户不可以选择此对象,当对象处于这种保护方式下时,用户不能移动它或改变它的大小,同样也不能修改或删除它。 • 对象在设计器中不可见。它使得此对象在报表设计中不可见,当对象处于这种保护方式下时,用户不能移动它或改变它的大小,同样也不能修改或删除它。 图3. 布局对象属性对话框中的保护页 这个对话框还有一个名为“Design-time caption”的输入项,它只对域对象有效。你可以在其中输入域对象的名称,这样在报表设计器中就不会显示域的表达示名称,而是显示你输入的名称。当域的表达式非常冗长时,这种显示方式可以使得报表设计器的用户界面更加友好。 保护一个带区(Band) 在报表设计器中要保护一个带区的话,请先激活此带区的属性对话框。你可以通过选择“Report”菜单中的“Edit Bands...”菜单项来打开这个属性对话框,也可以直接双击带区的灰色条。图4显示了一个带区属性对话框的保护页,你可以选择以下两种保护模式: • 带区不可修改。这可以防止用户修改带区的属性。 • 带区不可改变大小。这可以防止用户改变带区的大小。 图4. 带区属性对话框中的保护页 保护报表本身 要为一个报表设定保护方式,要先激活此报表的属性对话框。你可以通过选择“Report”菜单中的“Properties”菜单项来打开这个属性框,也可以右键单击此报表来弹出这样一个菜单。图5显示了一个报表属性对话框的保护页 图5. 报表属性对话框中的保护页 这个对话框的上半部分可让你禁止用户使用属性对话框中的某些属性页。当你选择了相应的检查框后,报表属性对话框中的某些属性页会变得不可用。但“Protection”检查框总是保持选择状态而且不允许你对它进行改动。另外由于“Ruler/Grid”属性页无法保护,因此“Ruler/Grid”检查框也总处于禁止状态,这两个检查框之所以显示是为了保持属性页与检查框的一致性。 属性对话框的下半部分可以禁止用户使用某些菜单项。当你选择了相应的检查框后,相应的菜单会变得不可用。 在命令中设置保护标志 如果要通过命令方式来调用报表设计器或标签设计器中的保护方式,则应该使用PROTECTED关键字,如下所示: CREATE REPORT MyReport PROTECTED MODIFY REPORT MyReport PROTECTED CREATE LABEL MyLabel PROTECTED MODIFY LABEL MyLabel PROTECTED 如果没有指定PROTECTED关键字,报表设计器不会为任何布局对象加上保护 Visual FoxPro 9.0 的报表设计器(4) 增强的用户界面(UI) 报表设计的用户界面作了很多改进,使得用户能更方便、更直观地设计出报表。菜单项也作了很大的调整,上下文菜单做了改进,报表设计器的工具栏中增加了一些新选项。表达式构造对话框(Expression Builder )和表达式构造选项对话框(Express Builder Options)都有了新的特性,此外报表设计器对其它一些用户界面作了细微的改进。 菜单 菜单加入了一些新项目,一些原有的菜单项被更名使得它们表达的意思更清晰,此外一些常用的菜单项被重复以便用户更方便地访问到它,具体如下: • “File”菜单下增加了“Save As Class...”菜单项。 • 报表设计器的工具栏加入了一个名为“水平线(horizontal lines)”的控件,它用来把Grid Lines和Show Position这两个控件与其它的控件分割开来。 • “Report”菜单增加了“重贴标签(relabled)”、“新建(new)”以及“打印预览(Print Preview)”等菜单项。 快捷菜单 对现有的快捷菜单添加了一些新条目,使得菜单条目与相应的对话框能保持更好一致性。 • “全局(Global)”快捷菜单增加了一个名为“重贴标签(relabled)”的菜单项。 • 通过右键单击任意带区(band)的灰色栏可以弹出名为“带区(Band)”的快捷菜单。 • 通过右键单击任意布局对象可以弹出名为“布局对象(Layout Object)”的快捷菜单。 工具栏 如图6所示,报表设计器的工具栏增加了两个新的控件:页面设置控件和字体控件: 图6. 报表设计器增加的两个新控件 表达式构造对话框 表达式构造对话框为“表达式域(Expression for Field)”输入框提供了一个更大的输入空间,允许用户输入更多的报表表达式。 如果你将_REPORTBUILDER这个系统属性设为空的话,那表达式构造对话框会指定本地行为,只有在数据环境中定义的数据表才能够显示在对话框中的列表中。那些没有在数据环境中定义的数据表则不会在列表中显示。 如果将_REPORTBUILDER系统属性为ReportBuilder.app的话,那表达式构造对话框则会呈现另外一种行为。首先,_GETEXPR中定义的表达式构造器会取代本地的表达式构造器。 表达式构造对话框还有一个下拉组合列表框,你可以从中选择要操作的数据表。只有当前已使用的数据表才会出现在这个列表框中。需要强调的是报表设计器是不会自动打开数据环境中定义的数据表的,因此如果数据表没有被打开的话,那它便不会出现在这个列表框中。 采用这种设计方式事,当用户使用你定义的报表时,你可以很好地控制用户能访问哪些数据表,不能访问哪些数据表。你可能在数据环境中定义了若干个数据表,但你不希望用户能访问所有的数据表,这时你可以打开一些允许用户访问的数据表,而其他未打开的数据表则对用户不可见。 鼠标指针的改进 当报表中对象处于大小可变状态时,鼠标指针会发生相应的改变(参见图7)。 图7. 当一个对象处于大小可变状态时,鼠标指针发生的改变。 多项选择对话框 VIsual FoxPro 9提供了一个多项选择对话框,你可以通过它一次性地设置多个布局对象的Portection和Print属性。它也允许你对单个布局对象的其它属性进行修改。要使用这种功能,先要选定多个布局对象,然后在任意一个对象上面单击右键来弹出这个对话框,如图8所示: 图8. 多项选择对话框 所有被选定的对象出现在对话框的“Selection”属性页的列表中。如果你要选取报表中所有的布局对象的话,用CTRL+A组合键可以对它们进行全选,然后再单击右键即可。 图8中的“Sort by”选项组允许你将布局对象按类型或出现在报表中的位置排序。“Remove from list”按钮可以删除列表中的布局对象。如果你双击列表中的某个对象,那么“Properties”属性页就会激活,并且显示出这些布局对象在报表中的某些属性。如图9所示,你可以一次性更改所有出现在“Selection”属性页中的布局对象的某些属性。 图9. 通过“Properties”属性页来修改布局对象的保护属性以及打印属性。 如果你选中了“Apply these protection settings to the selected objects”检查框,那可以对列表中的布局对象设置保护方式。如果你选中了“Apply this condition to the selected objects upon saving”检查框,那么可以允许打印。你可以根据自已的需要对保护及打印做更进一步的设置,设置完毕后,单击“OK”按钮便可同时改变所选的布局对象的这些属性。 更大的缩放级别 预览窗口有了更大的缩放级别,可以从10%缩放到500%。 布局对象的增强 布局对象也做了一些改进,包含一个操控模板字符的可选项,字符表达式的裁剪模式,以及能指定布局对象的相对位置和绝对位置。 模板字符 域属性对话框增加了对模板字符的一些新支持,它们分别是覆盖(Overlay)和交错(Interleave)。用来支持字符的一些特殊格式。 当你使用覆盖方式时,特殊字符会被当做数据的一部分,并且会覆盖其它的字符。举个例子来说,当你使用一个格式化字符串“999-999”时,而用户实际输入的数据是“123456”,那报表的最终结果将会显示为“123-56”,注意数字“4”被格式化字符中的“-”覆盖了。 当你使用交错方式时,特殊字符会插入到当前数据中。举个例子来说,当你使用一个格式化字符串“999-999”时,而用户实际输入的数据是“123456”,那报表的最终结果将会显示为“123-456”,注意“-”插入到了数字“3”和数字“4”的中间。 Visual FoxPro 9.0 的报表设计器(5) 字符表达式的裁剪模式 在Visual FoxPro 9以前,当域对象中的文本过长时一般都会被自动裁剪。在Visual FoxPro 9中,你可以指定域对象的如下几种裁剪方式: • 缺省裁剪方式。这种方式类似于以前版本的Visual FoxPro的处理方式。 • 裁剪最近的字符。它将多余的字符全部裁掉,直至刚好满足输入域的长度。 • 裁剪最近的单词。它将多余的单词全部裁掉,直至刚好满足输入域的长度。 • 文件裁剪方式。如果输入域中的内容中一个非常长的文件路径,那么中间的路径将会以省略号代替,只保留头尾路径。 大小及位置 现在可以更方便地控制布局对象的大小以及所处的位置。与原版本不同,当在新版本报表中加入一个对象时,此对象的“From page top”、“From left”、“Height”和“Width”属性都会自动设置。在报表设计器中,“From page top”属性是指对象相对于页面顶端的相对位置。此对象上的所有灰色栏的高度也都被计算在内。改变对象的“From page top”属性有可能将对象移到到另一个区带中去。 相对位置:“From page top”属性和“Height”属性共同确定了当前对象是处于绝对位置还是相对位置。当对象的“From page top”属性的取值范围在报表的区域内,并且“Height”属性小于或等于所在区带的高度时对象就处于相对位置。一般来说,只有区带内的对象才需要使用相对位置,而像Page Header和Page Footer这样的对象则不需要使用相对位置。 绝对位置:当对象的“From page top”属性的取值范围在报表的区域以外,并且“Height”属性大于所在区带的高度时对象就处于绝对位置。绝对位置意味着对象在每页报表中都有着一个精确的位置,不会发生偏移。 我们可以利用绝对位置来为报表增加水印效果。将一幅水印图像放在Page Header区带中,并且将它设为“缩放内容,保持形状”模式。将水印图像的“From page top”属性和“From left”属性设为某组值,这组值相当于你的水印图像在报表中的左上角坐标。然后通过改变“Height”和“Width”属性来指出水印的大小,但注意的是不要将图像的尺寸设得过大,以免超过了打印区域的边界。 增强的数据分组功能 Visual FoxPro 9报表设计器对数据分组功能做了一些改进,增大了数据分组的最大数量限制以及对水平栏的改进。 最大数据分组数 最大数据分组数从原来的20个增加到了现在的74个。实际原来的版本也支持最大74个数据分组数,但由于原来的界面只支持20个数据分组的输入,从而导致了这种限制。 水平分栏 在以前的版本中,如果要为一个数据组定义多个水平分栏的话将会浪费很多报表空间。并且第一行第一列与报表的顶端之间有一些空白,数据还会从行1列2开始显示。并且在每个数据组中都会有一个多余的区带,如图10所示。即使数据组的页头区带的高度为0,Visual FoxPro 仍然将保留这些空白,如图11所示。 图10. 当为数据组定义多个水平分栏时,原来版本的Visual FoxPro浪费的报表空间 图11. 即使数据组的页头区带高度为0,原来版本的Visual FoxPro仍将保留这些空白 在Visual FoxPro 9中数据组分栏得到了改进。当报表设计器发现一个新数据组时,它将从第一列开始显示,直至満行。如果不满一行,那剩下的部分将以空格填充。如果还有未打印完的明细记录的话,那这些记录会从下一行开始输出,如图12所示。如果数据组的页头区带的高度为0,则Visual FoxPro 将不会保留任何空白,如图13所示。 图12. 将数据组进行水平分栏时,Visual Foxpro 9将节省更多的空间 图13. 如果数据组的页头区带高度为0时,Visual Foxpro 9不会保留任何空白 Visual FoxPro 9.0 的报表设计器(6) 多条明细区带 这个新增功能其实早在以前就很需要,它是对老版本的一个很大的改进。它能让你在一个父表中为每一条记录处理相对应的子表,这种报表格式的一个典型实例如图14所示。 图14. 这个报表为每个客户报告了其购买保险的详细信息 数据表与关联 要想熟练地使用这一新特色,你必须明白父表是怎样与子表一起协同工作的。我们以图15所展示的报表为例,它使用的数据库环境如下: • 客户表(Customer)是父表,它包含了所有购买保险的客户。 • 家庭成员表(Members)是客户表的子表,它包含了客户的所有家庭成员。 • 交通险表(Vehicles)也是客户表的一个子表,它包含了客户所购买的交通险。 • 家庭险表(Homes)也是客户表的一个子表,它包含了客户所购买的家庭险。 主表 必须有一个表来做为报表的主表,在本例中,客户表正是这样的一个数据表。如果你用数据环境来定义数据表的话,那必须将它的InitialSelectedAlias属性定义为这个数据表。 如果你用代码的方式来定义数据表的话,那要保证当此报表运行时,客户表必须处于当前打开的工作区。 目标别名(Target Alias) 所谓目标别名,是指在报表某个特定的区带中做为主表的那个数据表。在本例中,家庭成员表是明细区带1的主表,交通险表是明细区带2的主表,而家庭险表则是明细区带3的主表。 如果没有为某个明细区带定义主表,那它就会呈现出与老版本的Visual FoxPro一样的行为(每个父表只会处理一个明细区带)。但如果你定义每个父表都为主表的话,那结果会全然不同。Visual FoxPro 将依次处理父表中所有记录,并在每个明细区带中依次将它们输出。 关联 关联在如何控制多条明细区带的输出中起到的重要的角色。Visual FoxPro通过父表与子表之间的关联来进行记录的导向。你可以通过SET RELATION或者SET SKIP来定义这些关联。如果你在数据环境中打开了这些数据表,并且数据库中已经定义了它们之间的关联关系的话,那父表与子表之间将会自动生成关联。 如果你采用代码方式打开数据表的话,那列表1将告诉你如何将图15所示的数据环境建立起来。 列表1.建立图15的数据环境 如下代码展示了如何将父表与子表建立报表关联的例子。 *--打开子表 USE Members IN 0 ORDER CustomerFK USE Vehicles IN 0 ORDER CustomerFK USE Homes IN 0 ORDER CustomerFK *--打开父表 SELECT 0 USE customer ORDER CustomerPK *--为父表和子表建立报表关联 SET RELATION TO CustomerPK INTO Members SET RELATION TO CustomerPK INTO Vehicles ADDITIVE SET RELATION TO CustomerPK INTO Homes ADDITIVE 定义多条明细区带 如果你新建一个报表,那它缺省地为这个报表只设置一个明细区带。通过可选区带对话框可增加额外的明细区带。从“Report”菜单中选择“Optional Bands...”菜单项就可以打开这个对话框。它其实就是原来版本中的“标题/小结(Title/Summary)”对话框。 单击“Add”按钮便可以增加一个新的明细区带,你可以为一个报表定义最多20个明细区带。 定义主表 通过明细对话框你可以为每一个明细区带定义其主表。从“Report”菜单中选择“Edit Bands...”就可以打开这个对话框,或者双击明细区带的灰色栏也可达到同样效果。 主表实际上是一个表达式,你必须将相应的数据表名用引号引起来。如果你定义了某个主表,则相应的明细区带的灰色栏将显示它的名字。 如果你要建立一个多条明细区带形式的报表,请别忘了在字段前面加入数据表名的前缀,格式为“数据表名.字段名”,这样可以防止不同的数据表之间的同名字段冲突。 页头和页尾 多条明细区带的另一个改进就是能为每一个明细区带增加独立的页头和页尾。这与页头和页尾分组有些类似,但仍然有一些不同。当每个父表记录的处理流程如下: • 明细区带1的页头被处理。 • 处理明细区带1中主表相应的所有子表记录。 • 明细区带1的页尾被处理。 • 明细区带2的页头被处理。 • 处理明细区带2中主表相应的所有子表记录。 • 明细区带2的页尾被处理。 • 明细区带3的页头被处理。 • 处理明细区带3中主表相应的所有子表记录。 • 明细区带3的页尾被处理。 • 以此类推...... 要为每一个明细区带增加独立的页头和页尾,你必须在详细信息对话框中的“Detail Header/Footer”的检查框中选中相应的明细区带。也可通过点击明细区带上方的标题栏来对明细区带进行排序。 学海无涯 Visual FoxPro 9报表设计器增加了如此多的特色来帮你建立更好的报表,以至于你不得不花更多的精力来学习它的新功能,但我认为这些学习是非常值得的。新的数据环境允许你在不同的报表中共享它。对报表的各个部分提供了强有力的保护方式,用户界面也做了一番改头换面,让你得到更舒适的开发体验。布局对象与数据分组的增强给你提供了更多的报表操控能力。最后,新推出的多条明细区带的特色能挖掘出报表设计器的更多功能。所有的这些改进可让你创建出非常复杂、功能非常强大的报表。 三思同志 发表于2005-04-23 7:57 AM IP: 218.15.22.*
介绍 CursorAdapter 类 vfp8最激动人心的变化是 CursorAdapter 类,它为不同的数据源提供通用的数据接口。 下面介绍怎样用 CursorAdapter 改变在 VFP 8 中连接数据的方式,包括,native tables, ODBC, OLE DB, XML. 。 CursorAdapter 类是 VFP 8 开发组的最给人印象深刻的成就之一。 它将会改变许多开发者连接各种不同的数据来源的方式。 开发小组在 VFP 存取数据的方式方面作了重要改变,将本地和远程数据连接方式进行了统一。另外,创建 CursorAdapter 类对那些已经熟练使用视图和SPT人来说并不费力。对使用ADO RecordSets 或XML可扩展标示语言文件的人也不费力。 CursorAdapter 类的独特之处在于,它是第一个提供本地游标、ODBC、ADO、XML数据源连接的基类,在一个类里面实现了所有连接功能。换句话说,要将ODBC数据源,ADO RecordSet 或XML可扩展标示语言文件翻译成一个 VFP 游标,CursorAdapter 类完全可以胜任。 你或许会说 CursorAdapter 是较早版本中本地视图和远程视图技术的替代 (注意: VFP 8中仍然保留这些功能). 但是在一些情形中,它也代替了SPT, 以及减少了使用ADO和XML时的代码量,可以直接使用ADO和XML。 CursorAdapter 的最大好处是,需要在同一个程序内连接到多个数据源的时候,它为你提供方便。举例说,如果你的程序大部分数据来自 SQL server,但是同时需要与XML可扩展标示语言连接,CursorAdapter 可以整合这两种情况,使程序取回的数据作为VFP的游标。 另外的一个例子是数据现在被储存在 VFP 表中 , 但是计划要移到一个数据库服务器 , 比如 SQL server或Oracle。 你需要先建立一组 VFP CursorAdapter 类,必要的时候以 SQL server以外的数据库代替这些类。 但是,就象我们能跑之前必须学会走一样,先概览一下 CursorAdapter 类和它的特性。 然后,使用 CursorAdapter 类来设计数据类是比较容易的。 建立第一个 CursorAdapter 类,象其他类一样,学习怎样使用它的最好办法是了解建立过程。第一次建立这些类的时候复杂程度低一些,我们开始用 CursorAdapter 类存取 VFP 本地的数据。 这很象使用本地视图取回 VFP 本地表的数据。 稍后在这一篇文章中,我们将会使用另一个 CursorAdapter 类连接到 SQL server数据库,ODBC和XML。 首先,你有二个方法建立 CursorAdapter 。 你能使用数据环境建立,也可以手动通过程序或类设计器来创建一个经过一个CursorAdapter类 。 这一个例子将会使用数据环境建立;较迟的例子将会手动建立. 如果你不熟悉 VFP 8 变化后的数据环境, 你可能认为在设计环境下创建的 CursorAdapter 只能在表单中使用,不能用于类。 然而, VFP 8 中已经改善了设计环境,因此不需要在表单中就可以创建。 用creat class命令创建一个新的数据环境类。 确定从下拉列表中选择based on数据环境类(dataenvironment) 。类名为Tests,所属类库名为tests.vcx. 创建的类在类设计器中出现后,右键单击Data Environment ,选择builder,起动数据环境建立向导。 在数据源类型项下,注意可选的选项。 因为第一个例子将会连接到本地的 VFP 表,选择native。 选择完以后, 使用‘省略号’按钮选择 Northwind 数据库(我这里是gzdata数据库)。 (默认位置是 c:\ program files\microsoft visual foxpro 8\ sample\northwind\ northwind.dbc) 下一步,点cursors 页, 它初始值是空的。 在列表框中,选择new按钮用 CursorAdapter 设计器创建一个新的 CursorAdapter 类。 首先,你应该看看Properties页,这里提供选项来选择类的名字和由类产生的游标的别名。 确定提供一个不同于表名字的别名,避免产生混乱。 在这里,使用 caCustomer 做类名, cCustomer 作为别名。 如果想让这个类用和数据环境一样的数据源,应该选择 "Use DataEnvironment data source" 。 注意你可以为 CursorAdapter 设置不同的数据源, 允许你在不同的类之间整合数据源.( 例如一个类使用ODBC数据源,另一个类使用XML数据源) 要定义CursorAdapter 如何返回来自数据源的数据,使用设计器中的Data Access 页。 按build按钮激活一个对话框,可以选择游标包含的字段。 在这个例子中,选择Customers表, 然后选择Customers.*。 点击向右的箭头移动选择项, 然后点ok。 这为你建立下列SQL语句: select CUSTOMERS.* from CUSTOMERS 如果你想添加过滤器,连接, 或其他条件到查询,你可以在列表框中直接键入。 如果你想建立带参数的查询,有一些选项,在本文后面介绍。 现在, 让我们添加WHERE子句: select CUSTOMERS.* from CUSTOMERS where companyname like 'C%' 这里可以看出源表和游标的不同,因为只有少数记录符合WHERE子句。 在第二个编辑框(schema)中已经为为你创建了字段列表。通常在继续以前花几分钟看看字段顺序是否符合你的习惯是有好处的 在这一页的下半部分有数据包设置对话框(data fetching),用来设置怎样处理远程数据包,当用vfp作为数据源的时候,这里的设置不发挥作用。(如上图)这里我们保留默认设置,稍后再讲述具体细节。在本页的底部附近是缓冲模式设定, 允许你设置任何被关联的表单的缓冲模式。有两个选项:开放式行缓冲和开放式表缓冲。 通常,你使用开放的表缓冲模式,除非你有特殊要求使用行缓冲模式。在这个例子中设置为开放式的表缓冲。最后,“break on error”控制CursorAdapter类怎样来处理错误。默认设置是类(class)自行捕获错误,并且允许你用aerror()函数捕获这些错误。选定这个设置,CursorAdapter类内部不管发生什么错误,vfp都会出现错误信息。也就是说,你需要使用ON ERROR命令或者‘类’的ERROR事件来排除不需要报错的情况。通常情况下不选这项设置,以便程序能处理任何发生的例外情况。 最后一页 (auto update) 配置如何更新源表。在通常情况下,选择“自动更新(auto-update)”和“更新所有字段(update all fields)”。 这将使cursoradaper类对游标(cursor)中数据的任何改变自动建立适当的更新、插入、删除机制。然而,你必须选择游标中的主关键字段,以便这些机制(更新、删除、插入)唯一的识别源表中的记录。在本例中,CustomerID是关键字段。因此,需要在其前面大上‘对号’。其他的设置暂时保留默认值。具体设置办法在本文后面讲述。设置完cursoradaper后,点击“ok”按钮,回到数据环境设置。此时,你应该在左边列表框中能看到caCustomer类, 在右边看到细节。如果你想更改这个类,你可以随时用数据环境‘builder’更改,选择需要更改的CursorAdapter 类,然后点击builder按钮。 存取 VFP 数据 此时,你可以测试数据环境,看看是否能取回在 CursorAdapter 的指令中筛选的数据。 使用命令窗户,例示 DE 类而且唤起 OpenTables 方法: lo = NewObject("deTest","Tests.vcx") ? lo.OpenTables() BROWSE 一个特殊情况是,CursorAdapter的游标连接到其它对象,如果你毁坏了指向CursorAdapter类的对象,会丢失游标和其中的记录。这就是说,你必须确保CursorAdapter对象参数在你打算存取的关联游标的范围内 编辑 VFP 数据 现在, 让我们看看是否能够更新游标,并且把更新准确地发送到源表。在命令窗口测试以下命令: REPLACE contactname WITH 'My Name Here' ?TABLEUPDATE() SELECT customers BROWSE 浏览customers别名,你会发现修改过的记录已经更新到源表中。 如果你在发送replace命令之前没有移动记录指针,客户ID中 'CACTU' 所在的记录被修改。 不管你修改哪条记录, 这证明 CursorAdapter 是能够被更新,而且更新能够准确地被发送到源表。 让我们打开你刚刚测试的数据环境类,这不只是一个练习—它是一个很棒的方法学习当你决定在数据环境以外建立自己的类的时候,该如何正确地配置一个 CursorAdapter 类。 虽然数据环境有一些属性改变和一个方法, 我们实际上对那些改变不感兴趣。看一下下拉列表框中的属性列表,选择 caCustomer 类,看看建立 CursorAdapter 类的时候需要进行的设置。 表 1 概述被builder和每个 PEM 所做改变。 所有的属性包含在 "see Init" ,通过INIT方法中的代码来设置这些属性。 那一段代码显示在list 1中。 在builder设置完以后,看看一些属性是怎样设置的,这是最好的学习方法。你可以在这里或者通过builder改变设置值。然而,如果在这里改变设置值,你需要冒破坏一些功能的风险,因为你在这里改变的属性有可能在builder里不会发生改变。 不管怎样,你能在 Init() 代码中看到 SelectCmd 属性是如何叙述的, 同样在 KeyFieldList , UpdatableFieldList 和 UpdateNameList中也可以看到。 特别注意 UpdateNameList 属性—这个属性列出游标的每一个字段以及源表中对应的字段(带表名称)。 当从头创建你自己的 CursorAdapter 类的时候,你可能想在这个列表中省略表名字。 然而,如果不使用精确的格式,你的更新将会失败, 但是没有错误提示。 在后面讲不通过builder建立一个类的时候我将再说这一点。 前面我说过 CursorAdapter使用本地的数据源的时候 , 本质上是一个替代了本地视图。如果你曾经用过本地视图,你可以发现类似的地方: 生成一个SQL select语句,定义哪些字段将被更新,设置关键字段,剩下的事情让 VFP 来做。 一旦游标中取回数据,可以使用 TableUpdate() 发送更新到源表,而且 VFP 自动地建立必要的update,insert,delete语句。 还是以上面的例子, 取消cCustomer 别名中contact字段被替换的数据。通过TableUpdate , VFP 自动地产生 (并提交)下列更新命令尝试更新: UPDATE customers ; SET CONTACTNAME=ccustomer.contactname ; WHERE ; CUSTOMERID=OLDVAL('customerid','ccustomer'); AND ; CONTACTNAME=OLDVAL('contactname','ccustomer') VFP 能够根据CursorAdapter的KeyFieldList属性和部分UpdateNameList属性中的值产生where子句。它通过子句记录那些已经被改变或添加的记录,保证你不会更新那些别人已经更新过的记录。注意,这是因为我们在它的WhereType 属性中保留默认值 "key fields and any modified fields." 错误处理 明显的,当用 CursorAdapter 更新数据时候 ,并不能完全如你所愿。你知道,多种原因可以导致TableUpdate 更新失败, 例如更新冲突或记录加锁失败。你必须对 CursorAdapter 类做特别处理来发现这些问题吗? 答案是,"视情况而定 ." 我们来建立一个简单的更新问题:给CursorAdapter尝试更新的一条记录加锁。如果类设计器仍然是开着的,关掉它。然后, 像前面做过的一样,用 NewObject 函数启动detest类,而且启动 OpenTables 方法。浏览游标以便你能看到数据,但是先不要改变任何东西。 现在打开 VFP 8 的第二个实例,这样就能够加锁记录。 在命令窗口中运行下列语句加锁你将尝试更新的记录: OPEN DATABASE (HOME(2)+"Northwind\northwind.dbc") USE customers LOCATE FOR customerid = 'CACTU' ?RLOCK() 你应该能返回.T.,表明记录确实被 VFP 的这一个实例加锁。 回到 VFP 的第一个实例,在命令窗口中运行下面的代码: REPLACE contactname WITH 'updated' SET REPROCESS TO 2 SECONDS ?TABLEUPDATE() 在这种情况, TableUpdate 返回.F.,表现记录锁阻止更新成功。 如果你用 AERROR() 捕获错误而且显示错误结果, 你会看到错误信息 "记录没加锁"。 这意味着如果你对设置缓冲的表直接操作而不是一个游标,你能用同样的办法处理错误。 不幸地,不是所有的预期错误都会这样表现出来。需要特别注意的是更新冲突,比如一个使用者企图覆盖其他人所作的修改。想看看这种行为的结果,在当前的 VFP 实例中(CursorAdapter 正在使用)运行下面的代码: ?TABLEREVERT(.T.) REPLACE contactname WITH 'client 1' 现在转变在到第二个例证并且发行下列指令: CLOSE DATABASES all OPEN DATABASE (HOME(2) + "Northwind\northwind.dbc") USE customers LOCATE FOR customerid = 'CACTU' REPLACE contactname WITH 'client 2' BROWSE 返回第一个实例, 尝试用 TableUpdate 发送更新: ?TABLEUPDATE() 在这种情况, TableUpdate 错误地返回.T.,使你相信更新成功了! 然而, 事实上并没有成功,而且这可以被CursorAdapter的CursorRefresh() 方法证明, 用下列代码: ?TABLEREVERT(.T.) ?lo.caCustomer.CursorRefresh() CursorRefresh 方法告诉 CursorAdapter 再运行 SelectCmd 里的语句,并且取回来自源表的最新数据。 对ContactName 字段的测试说明 CursorAdapter 根本没更新字段值! 解决这一个问题的最简单的方法要利用 CursorAdapter 的 AfterUpdate 方法。这一个方法在 TableUpdate 发送保存每条记录的请求后被执行。注意这个‘方法’只对当前记录有效。 如果记录是新的,或者记录已经被删除,会激活AfterInsert 或 AfterDelete 方法。 AfterUpdate 方法捕获一些参数, 包括最初的字段状态,是否被强制更改, 和作为更新的命令。 最后一个参数 , lResult,是我们这部分主题最重要的,因为它告诉我们更新过程是否成功。 使用的另一个重要角色解决更新冲突问题的是系统变量 _tally, 它告诉我们最后一次操作影响了多少记录。 因此,如果 lResult 放回成功的, 但是 _tally是零,那么说明没有记录被更新,那么你可以判定这种情况是一个更新冲突。 简单说,解决这一个问题的简单方法是把下列代码加入 CursorAdapter 类的 AfterUpdate 方法: LPARAMETERS cFldState, lForce, nUpdateType, cUpdateInsertCmd, cDeleteCmd, lResult IF lResult AND _TALLY = 0 THEN ERROR 1585 && update conflict ENDIF 有趣的是你在屏幕上看不到错误信息;错误信息被 TableUpdate “限制住了”,这就迫使你使用 AError 函数分析更新失败的原因。出现这样的现象是因为 BreakOnError 属性保留了默认值 ,也就是错误不引起程序中断。如果你把这个属性设为“ture",那么"更新冲突" 错误信息就会出现,如果作了定义,你的ON ERROR句柄会被运行。 这一个更新冲突问题是为 CursorAdapter设计的,因为 VFP 8 没有自动发现这个问题方法。因此,当不用本地数据源的时候,这个代码( 或相似的代码)会用在你的CursorAdapter类设计中。 CursorAdapter with ODBC 现在你已经有了一定的基础, 让我们继续看看当后台数据库用 SQL server的时候怎样进行设置。我们将从使用 VFP 通过ODBC连接 SQL server上的 Northwind 数据库开始。同样, 让我们从头建立 CursorAdapter 以便可以看见类需要设置的各个方面。 首先,用下列指令在类设计器中建立一个新类: CREATE CLASS caODBC OF tests as CursorAdapter 这里需要设置的最重要的属性是 DataSourceType 属性。 因为我们想通过ODBC连接 SQL server,将这一属性设置为ODBC。当使用这种连接方式的时候 , DataSource 属性需要有一个确切的连接句柄,可以用 SQLConnect 或 SQLConnectString 函数建立。 在任一情况,这些功能应该在 CursorAdapter 类的 Init 方法中使用下列代码: LOCAL lcConnStr, lnConn ** string assumes trusted connection (integrated security) lcConnStr = "Driver=SQL Server;Server=(local);DATABASE=Northwind" lnConn = SQLSTRINGCONNECT(lcConnStr) IF lnConn > 0 THEN THIS.DATASOURCE = lnConn ELSE ** unable to connect ENDIF 连接字符串假定你使用可信赖的关联到 SQL server;如果你使用安全的连接到 SQL server,把 "uid"= 和 "pwd"= 串加入连接句柄指定使用者名称和密码。 任何一种情况下,返回的是连接柄 , 或错误发生时的否定值。 这一个连接柄被指定为 DataSource 属性以便 CursorAdapter 知道该到哪里声明。 下一个步骤是建立 SelectCmd 以便 CursorAdapter 知道需要从数据源取回什么数据。还有一个最好的放置地方是 Init 方法, 因为属性表限制字符串的长度。 把下列代码加入 Init 方法中,设定 DataSource 属性代码之后, 取回公司名字中以 "C" 开头的客户列表: This.SelectCmd = "SELECT " + ; "CustomerID, CompanyName, ContactName, " + ; "Address, City, Region, Country " + ; "FROM Customers WHERE CompanyName LIKE 'C%'" 然后,你应该告诉 CursorAdapter 通过调用CursorFill 方法填充被关联的游标。 你可以省略这一个调用,并且手动从类之外调用, 或放在 Init 方法中,以便它能够自动填充游标。在Init 方法中设置 SelectCmd 之后,可以用 This.CursorFill()调用。 最后,你应该在类的Destroy方法中加上一些代码,以便在对象从内存中释放以后减少到服务器的连接。 没有这些代码的话,每一个新的实例将会产生一个到服务器的新连接, 并且不会释放它: IF this.DataSource > 0 THEN SQLDISCONNECT(this.DataSource) ENDIF 藉由这些变化,你拥有一个实用的 CursorAdapter 类,它能够生成一个只读的游标。在允许更新之前,测试一下类,确保可用,并能准确取回数据。用下列代码测试: lo = NEWOBJECT("caODBC","tests") BROWSE 注意,你不需要像数据环境那样调用 OpenTables 方法。这是因为你把 CursorFill 方法直接加入了 Init 方法,使类在实例化之后自动地填充游标。 Updating ODBC Data(更新ODBC数据) 为了使这个类可更新,你必须正确地给Tables, KeyFieldList , UpdatableFieldList 和 UpdateNameList 属性赋值。 并且设定 AllowInsert , AllowUpdate 和 AllowDelete 属性为true, 确定自动更新特征被激活。 再一次强调,最好的办法是在Init 方法中用代码设置这些属性。 Init 方法一些代码在list2 中。 在关闭类设计器之前,你可能想将 BufferModeOverride 属性换成 "5",“开放的表缓冲”以便移动记录指针的时候 , 自动更新不发生。 测试 CursorAdapter 的更新能力,把它作为一个实例,浏览游标,作一个更改, 然后提交 TableUpdate。 确定所作的更改被响应,调用 CursorAdapter 的 CursorRefresh 方法,再浏览一次。 处理ODBC错误(Handling ODBC Errors) 象使用本地 CursorAdapter一样 ,大多数的错误处理是按照 "传统的"方法—测试 TableUpdate 返回的值,如果失败,使用 AError 来判断原因。 不幸的是,更新冲突的检测也是ODBC连接类型 CursorAdapter 的一个问题。 本地 CursorAdapter 错误的解决办法是在 AfterUpdate 方法中设置错误处理代码, 但这种方法对ODBC型 CursorAdapter 是无效的,因为更新失败的时候,我们不是要处理VFP错误,而是ODBC错误。 因此,最好答案是使用一个存储过程,或在更新句柄中增加一些代码发送到服务器。 回想一下,为本地的 CursorAdapter 更新错误的解决办法是检查 _TALLY,看看是否有记录被更新。为ODBC连接的解决办法与本地CursorAdapter是相似的,但是我们不能使用 _TALLY,因为在远程数据检测上它是不可靠的。 我们可以使用SQL server的@@Rowcount 系统函数来判定记录是否被更新。 如果你要写一个 T- SQL 批量更新语句来更新一笔记录,你应该象下面这样写: --@custID and @oldContact set by earlier code or parameters UPDATE customers SET ContactName = @newContact WHERE CustomerID = @custID AND ContactName = @oldContact IF @@ROWCOUNT = 0 RAISERROR('Update failed.',16,1) RaisError T- SQL 函数使 VFP 接收一个ODBC错误 (1526 号), 在第一个参数里写错误信息.(另外二个参数指出严重和错误的状态) 在这种情况下, 当@@Rowcount=0的时候RaisError 被调用, 表明早先的 T- SQL 语句没影响任何的记录。 所有讨论的这一些表明你可以使用 CursorAdapter 的 BeforeUpdate 方法来描述发送到服务器用来更新的语句。 BeforeUpdate 方法接受五个参数, 有趣的是最后的二个 (cUpdateInsertCmd 和 cDeleteCmd) 建立了关联(注:原文the last two (cUpdateInsertCmd and cDeleteCmd) are interesting in that they are passed by reference. )。 在它们被送去数据源之前 , 这允许你修改指令。 在我们的例子中,我们使用这一个方法增加一个对@@Rowcount的测试然后调用 RaisError 。把下列代码写入 BeforeUpdate : LPARAMETERS cFldState, lForce, nUpdateType, ; cUpdateInsertCmd, cDeleteCmd IF nUpdateType = 1 THEN cUpdateInsertCmd = cUpdateInsertCmd + ; " if @@ROWCOUNT = 0 "+ ; "RAISERROR('Update Failed due to update " + ; "conflict.',16,1)" ENDIF 现在,为发送到后台数据库的每一行记录,用这段代码测试行是否被更新。 如果没更新, VFP 将会接收到错误,TableUpdate 将会失败,而且 AError 将会显示1526号错误,并显示预设的错误信息。 存在二个的问题。 首先,这是特别为 SQL server设置的;对其他的ODBC数据源 ( 比如ORACLE) ,这一段代码无效。 其次,这个错误信息是指一类错误,总是产生相同的 VFP 错误号, 而且在 VFP 中建立正确的错误处理句柄有一点困难。这一个问题可以通过在SQL SERVER服务器上建立通用的错误信息,每条信息对应着自己的唯一错误号码。 另外一个更好的解决方法是使用存储过程来执行更新,代替用VFP建立一个查询。 当然,采用存储过程的不利因素是不能使用VFP 的自动更新句柄来更新数据。 参数化设置 Parameterization 通过学习,你已经知道如何为 CursorAdapter 类的方法、属性添加命令。 基本上,在类中使用的每个事件(event)都有一组before和after方法, 比如 BeforeUpdate 和 AfterUpdate 。然而,没有 BeforeSelect 或 AfterSelect-替代它们叫做 BeforeCursorFill 和 AfterCursorFill,因为游标是用 SelectCmd 的结果来填充的。 BeforeCursorFill 方法接受三个参数, 而且返回 Boolean 值。 第一个参数 , lUseCursorSchema, 叙述 CursorSchema 属性是否控制游标的结构。 第二个参数 , lNoDataOnLoad,与视图的 NODATA 子句类似,只取回表结构,但是没有从数据源取回数据。 对于现在的讨论 , 第三参数 , cSelectCmd,是最有意思的。 我们已经提到过它 (象 BeforeUpdate 的 cUpdateInsertCmd 参数) ,先使用 SelectCmd 的当前设置。然而,如果你改变这一个参数的值,它不改变 SelectCmd 属性的值;取而代之的是,它的值改为被传给数据源的语句 。 举例来说, 假如你已经把 CursorAdapter 对象的 SelectCmd 设定为下列代码: SELECT CustomerID, CompanyName, ContactName, City, Region, Country FROM Customers 在呼叫 CursorAdapter 的 CursorFill 方法之前, BeforeCursorFill 方法的 cSelectCmd 参数会包含这个语句。 假如你在这一个方法中用下列代码: cSelectCmd = cSelectCmd + ; " WHERE CompanyName LIKE '" + ; this.cCompanyName + "%'" 这就导致实际的select命令总是包含如上面代码描述的where子句和 this.cCompanyName(自定义属性) 的当前值.因为它不改变 SelectCmd 的初始值, 你不必用任何特别的代码来确定在select命令中是否包含两个where子句。 参数设置第二部分 如果你以前用过视图,或 SQL pass through,那么你可能熟悉在参数前使用 "?" 字符。 这一个用法也适用于 CursorAdapter 。 下面的代码例子告诉你如何在 CursorAdapter 的 SelectCmd 属性中使用参数: This.SelectCmd = "SELECT * FROM Customers " + ; " WHERE CompanyName like ?lcMyVar " lcMyVar = 'C%' This.CursorFill() 最要紧的是确定变量 "lcMyVar" 在 CursorFill 方法被请求之前被定义。否则的话,VFP会提示找不到值,使用者会无所适从。 你可以使用 CursorAdapter 的属性作为参数,用来替代本地的变量。这样做有这样的优点:只要对象存在,属性也会存在,而且你可以通过使用access/assign方法来确定分配的值符合规则。 使用存储过程 在上面说过, 使用储存过程是一个突破错误处理限制的好办法。让我们逐步探究在ODBC CursorAdapter中使用存储过程的方法。这样我们就能感觉出手动处理 CursorAdapter 类的更新错误是多么复杂。 这一段是关于通过调用数据源的存储过程来代替自动执行的update、insert和delete命令。 这意味着你必须处理 UpdateCmd , InsertCmd 和 DeleteCmd 属性, 而且假设 SQL server上的 Northwind 数据库已经建立存储过程来提供这些功能。 例子, 让我们看一看一个简单存储过程的完整代码,你可以用它更新 Northwind 数据库的customer表的 ContactName 字段: --T-SQL code, not VFP CREATE PROCEDURE UpdateCustomerContact ( @CustomerID nchar (5), @ContactName nvarchar (30), @oldContact nvarchar (30) ) AS IF @CustomeriD IS NULL RAISERROR('CustomerID is a required parameter',16,1) ELSE UPDATE Customers SET ContactName = @contactName WHERE CustomerID = @customerID AND ContactName = @oldContact 为了节约空间,这一个存储过程没有包含全部的错误处理代码。 不管这个,已经有足够的代码来说明怎样在 CursorAdapter 类中执行更新。 幸运地, 建立 UpdateCustomerContact 存储过程作为更新命令,可以取代 BeforeUpdate 方法,用下面的代码: LPARAMETERS cFldState, lForce, nUpdateType, ; cUpdateInsertCmd, cDeleteCmd cUpdateInsertCmd = ; "EXECUTE UpdateCustomerContact '" + ; EVALUATE(this.Alias+".CustomerID") + "','" +; ALLTRIM(EVALUATE(this.Alias+'.ContactName'))+ ; "','" + ; OLDVAL('contactname',this.Alias)+"'" 这里,代码放在 cUpdateInsertCmd 参数里,覆盖了默认的update命令。我使用了evaluate函数,是为了cursor的名字是动态的,说明cursor的名字可以很容易被改变,但是代码不变。还有,我使用了OLDVAL 函数取回 ContactName 字段被修改前的值。如果在存储过程的where子句里需要旧的数据,那么这个函数是必需的。这有点像自动产生update的情况。 记住,在记录被实际更新之前 ,通过TableUpdate自动呼叫 BeforeUpdate 方法。 因此, 无论UpdateCmd当前的值是什么,这个方法程序(指UpdateCmd)被禁用,而且总是使用存储过程。 注意,你也可以使用已经讨论过的参数设置方法,使用 BeforeUpdate 方法。 这就要求你为 CursorAdapter 提供 UpdateCmd设置,但是, 不要在参数里用固定的代码,要用变量或者属性,并在它们前面加上“?”。 在这里需要注意的重要的一点是 cUpdateInsertCmd( 或对象的 UpdateCmd属性) 不能返回任何值。 进一步说,如果你从存储过程返回一个值,它就没有地方 "去" ,那么这个值就会永远丢失。因此,在存储过程中添加一个RaisError呼叫,以便在更新过程中发生任何错误( 例如不正确的参数或一个更新冲突) 的时候,你的代码能够作出反应,这样做是非常重要的。你可以通过测试 TableUpdate 返回值来捕获错误 ,或者用 AError 方法, 然后分析错误。 相似的代码也应该写进 BeforeInsert 和 BeforeDelete 方法,以便它们也调用存储过程,而不是调用设置好的“查询”。 为了节约空间,我将留下代码当做 "读者的练习." CursorAdapter with OLE DB 我们的下一个任务是看看如何通过 CursorAdapter 类使用OLE DB(对象连接与嵌入), 并且把它同我们用过的native和ODBC作一比较。OLE DB技术比ODBC有更多的处理能力, 而且能够连接的数据源类型比ODBC多。 CursorAdapter 通过嵌入ADO对象使用OLE DB,这是OLE DB技术标准的 COM 封装。 VFP 会自动地把ADO记录集 转换成一个 VFP 游标, 以及处理更新, 正如在早先的例子中讲到的一样。 第一件要做的事,当然还是建立一个新的 CursorAdapter 类,我们用代码建立一个。 开始建立一个新的程序,取名字 caADO.prg, 把下列代码添加进去: PUBLIC goCAADO as CursorAdapter goCAADO = CREATEOBJECT('caADO') BROWSE DEFINE CLASS caADO AS CursorAdapter oConn = NULL oRS = NULL Alias = "cCustADO" DataSourceType = "ADO" SelectCmd = "SELECT " + ; "CustomerID, CompanyName, ContactName, "+; "ContactTitle, Address, City, Country "+; "FROM Customers WHERE Customerid LIKE 'C%'" FUNCTION Init() This.DataSource = this.oRS This.oRS.ActiveConnection = this.oConn This.CursorFill() ENDFUNC ENDDEFINE 在这段代码中,我们将 DataSourceType 设为ADO而且把我们对Customers的查询加入 SelectCmd 。 当 DataSourceType 是ADO的时候, DataSource 属性必须包含有效的 RecordSet 或命令对象, 这依赖于你如何使用 CursorAdapter。 如果你不使用参数化查询 (就象前面例子中讲到的用 "?")那么你可以用记录集( RecordSet );否则,你必须使用命令对象,因为ADO已经代替了参数选择。 你的查询中的任何参数在命令对象中自动地被处理。 在这种情况下,我们使用 RecordSet 对象 , 但是应该注意我们必须提供一个“连接”对象。 在这两种情形中,我使用Access方法建立了关于这些对象的参考。 list3 中是Access方法的代码。 两个Access方法首先检查对象是否已经被建立。 如果没有,那么就继续创建对象。在使用 RecordSet 的情形,你只需要建立对象,剩下的由 CursorAdapter 来做。 用“连接”对象的情况下, 你必须提供连接字符串(连接句柄)并且打开连接,因为CursorAdapter 不为你打开连接。这是因为“连接”不是 CursorAdapter 的一个属性, 而是 RecordSet 对象的一个属性。 用OLE DB更新 如果不另外设定几个属性,这么简单的 CursorAdapter 是没有更新能力的。 把下面的代码放在定义类(define)的代码中,在init方法之前运行,将会允许自动更新: KeyFieldList = "CustomerID" UpdatableFieldList = ; "CompanyName, ContactName, ContactTitle, "+ ; "Address, City, Country" UpdateNameList = ; "CustomerID Customers.CustomerID, " + ; "CompanyName Customers.CompanyName, " + ; "ContactName Customers.ContactName, "+; "ContactTitle Customers.ContactTitle, " + ; "Address Customers.Address, "+; "City Customers.City, Country Customers.Country" Tables = "Customers" 然而, RecordSet 会建立它的默认的 CursorLocation 和 CursorType 属性。 如果不改变这些属性设置, RecordSet 最初是只读的,因此,你需要按以下方法修正 oRS_Access 方法: FUNCTION oRS_Access() as ADODB.RecordSet LOCAL loRS as ADODB.RecordSet IF VARTYPE(this.oRS)<>"O" THEN this.oRS = NULL loRS = NEWOBJECT("ADODB.Recordset") IF VARTYPE(loRS)="O" THEN loRS.CursorType= 3 && adOpenStatic loRS.CursorLocation = 3 && adUseClient loRS.LockType= 3 && adLockOptimistic this.oRS = loRS ENDIF ENDIF RETURN this.oRS ENDFUNC 为 RecordSet 加上这些代码以后, CursorAdapter 就能处理自动更新了。 CursorAdapter with XML 最后, 让我们建立使用XML作为数据源的 CursorAdapter。 这一节很有趣,因为XML文本不像通常的数据源。 同样,当数据源设置为XML的时候, CursorAdapter 不会自动建立 SQL 更新, 插入或删除命令。因此,这种类型的 CursorAdapter 需要大量的代码接收和更新数据。 在这一个例子中,我将使用 SQL server 2000 的 SQLXML 提供XML文本。 同时,因为SQLXML支持通过XML更新,我们将花一点时间写必要的代码来执行更新。 假设你已经配置了SQLXML,允许HTTP数据存取Northwind 数据库,而且允许用 UpdateGrams 对数据库进行更新。 在这里,我在 IIS 上面设置使用一个被称为 "nwind" 的虚拟目录存放 HTTP 数据。 因此,我的全部例子将会包含这个网址: http://localhost/nwind,经由 IIS 存取 SQLXML。 让我们开始建立一个新的程序,程序名字为caXML.prg,用下列代码(创建一个类): PUBLIC oCAXML as CursorAdapter SET MULTILOCKS ON && need for table buffering oCAXML = CREATEOBJECT('xcXML') BROWSE NOWAIT DEFINE CLASS xcXML AS CursorAdapter DataSourceType = "XML" Alias = "xmlCursor" UpdateCmdDataSourceType = "XML" InsertCmdDataSourceType = "XML" DeleteCmdDataSourceType = "XML" BufferModeOverride = 5 *custom properties oXMLHTTP = NULL oXMLDOM = NULL cServer = "localhost" cVDir = "nwind" ENDDEFINE 不同于通常的 DataSourceType 和Alias(别名)属性设置,这是我们第一次看见 xxxCmdDataSourceType 属性。 因为这是一个XML类型的 CursorAdapter,如果你想让它用来更新源数据,这些属性就必须进行设置 。推荐在这个类中使用 oXMLHTTP 和 oXMLDOM 属性,将会在下面做详细说明。 接收XML数据 在考虑 CursorAdapter 的更新能力之前,我们先研究怎样从SQLXML 服务器接收文本。 首先,一个简单的select命令不能工作,我们必须建立通用的 SelectCmd 。 这在 Init 方法中很容易做到,同时,在INIT中调用 CursorFill 方法,如以下代码: FUNCTION INIT() as Boolean LOCAL llRetVal, lcMsg, laErr[1] this.SelectCmd = "this.GetXml()" llRetVal = THIS.CursorFill() IF NOT llRetVal THEN AERROR(laErr) lcMsg = "Cursor was not filled!" IF NOT EMPTY(laErr[2]) THEN lcMsg = lcMsg + CHR(13) + laErr[2] ENDIF MESSAGEBOX(lcMsg,16,"XMLCursorAdapter Test") ENDIF RETURN llRetVal ENDFUNC 这一段代码建立 SelectCmd 作为本地用的方法,代替 SQL 命令。 以前的例子中没有这样做过,对任何 CursorAdapter 类,不管什么类型,都是合法的。然而,当你使用一个本地的方法作为 SelectCmd 的时候,你必须也提供代码用来更新,插入和删除, 因为如果不是一条SQL语句的话, VFP 是不会自动处理的。 当我们在 Init() 中使用 CursorFill 的时候,GetXML 方法被调用。 数据源设定为XML的时候,GetXML 方法必须返回只包含一个表的最终文本。 如果它包含多个表,你会得到料想不到的结果。 GetXML 方法在list4 中列示。 GetXML开始于提交一个 MSXML2.XMLHTTP COM 对象。 这个对象处理所有 HTTP 通信,包括发送请求到服务器并且返回结果。 你可以看到,oXMLHTTP 实例对象是被设计好的Access方法控制的,这样设计是为了防止连续地创建和破坏 COM 服务器。 然后,你可以看到我们的典型的SELECT描述, 除了LIKE子句有点稍稍不同以外。 HTTP 需要我们当十六位百分比字符显示到达25%的时候退出显示(指进度显示), 在 SQL server接收查询之前 , 这个值将会变成单一的百分比符号。 然后,代码用具体的查询要求来设置URL,并且通过HTTP发送URL到SQL SERVER服务器。SQL SERVER接收到这个查询,并且处理它,以XML方式返回值。这是因为我们已经在“查询”中包含了FOR XML子句。在这个例子中,XML的根元素叫做 "results" 。你从查询字符串中可以看出。这是按照你的习惯配置的。 此时, lcRetXML 包含一个来自 SQL server的XML数据流。 既然 GetXML 方法被 VFP 作为 SelectCmd 调用,你可以从 GetXML 方法中返回这个变量的内容,而且 VFP 将会把数据流转换成一个 VFP 游标。 你可以通过执行 caXML 程序测试。 会出现一个包含返回的XML文本内容的浏览窗户。 使用调试工具一步一步查看 GetXML 方法,可以看到在被转换到一个 VFP 游标并释放之前 , lcRetXML 变量的值是XML文本。 更新XML数据 下一个步骤是决定该如何使这个游标可更新,以便所作的更改能被发送回 SQLXML 服务器。 SQLXML 能使用一个特别的XML文本,即 UpdateGram, 利用它直接把更新发送到数据库。 在 VFP7 中,可以用 XMLUpdateGram 函数建立这个文本。 用VFP 8 的 CursorAdapter , UpdateGram 属性会自动建立。 第一个步骤是设置 updatable 属性并且建立一个更新指令。 在类定义的开始设置这些属性,并在CursorAdapter的init事件中增加一行代码,提供调用更新命令的方法。 KeyFieldList = 'customerid' Tables = 'customers' UpdatableFieldList = ; "companyname, contactname, contacttitle, "+; "address, city, country " UpdateNameList= ; "customerid customers.customerid, " + ; "companyname customers.companyname, " + ; "contactname customers.contactname, " + ; "contacttitle customers.contacttitle, " + ; "address customers.address, " + ; "city customers.city, country customers.country" FUNCTION INIT() as Boolean LOCAL llRetVal, lcMsg, laErr[1] this.UpdateCmd = "this.UpdateXML()" this.SelectCmd = "this.GetXML()" ** balance of code skipped... 注意,我们已经在init方法中替换了属性表中的 UpdateCmd 和 SelectCmd 设置,实际上最终结果是一样的。 不管怎样, 这一段代码的第一部份现在看起来很熟悉,在这里我们设置了 KeyFieldList ,table, UpdatableFieldList 和 UpdateNameList 属性。 没有这些属性设置,就不可能创建 UpdateGram 。 然后,我们建立 UpdateXML 方法作为 CursorAdapter 的 UpdateCmd 。 没有参数传递给 UpdateXML 方法,所以,所有决定更新的工作必须这个方法里处理。还有,因为XML 类型的 CursorAdapter 没有默认的更新机制, 你必须写代码把更新传递给XML数据源。 在这一段代码中,使用 XMLHTTP 对象传递更新到服务器。 通过LoadXML方法加载 UpdateGram 属性的内容到 XMLDOM( 在ACCESS方法中) 之内,打开到服务器的连接,设定请求的内容为XML, 然后发送 XMLDOM。所有的结果通过 XMLHTTP 对象的 ResponseText 属性返回,接着装载到 XMLDOM 中,并且分析错误信息。 如果没有提示错误,更新已经成功,而且过程结束。然而,如果有错误, 就会产生符合语法的错误信息文本,并且包含在一个自定义的ERROR函数中,这样 TableUpdate 函数就能找到更新失败的原因。 如果没有这些代码, 即使有问题的时候,TableUpdate 总是返回成功信息。 测试一下这段代码,运行 caXML 程序,在游标中更改一个字段, 然后在命令窗口中发出 TableUpdate。 如果 TableUpdate 成功, 你应该能在服务器上看到更新结果。如果 TableUpdate 失败,你需要用 AError 函数接收 SQL server产生的错误信息。 如果你对 UpdateGram 的内容感到好奇, 你可以一步步查看类的 UpdateXML 方法,并且查看 UpdateGram 属性的内容。 然而,如果你不到有关数据变化的方法中 (如 UpdateCmd , InsertCmd 或 DeleteCmd 属性内容) ,你也不能完全弄明白 UpdateGram 属性的内容。 list6显示当ContactName字段的客户ID被更改为‘CACTU' 后UpdateGram的内容。 正如你看到的,SQLXML 能够读取这个文本并且能够很容易地建立一个Update-SQL 语句, 然后发送到 SQL server。 updg:sync(同步更新)元素使更新立即发生;因此,如果你有多个表需要更新,你可以把它们关联起来放到一个 UpdateGram中, 确定把他们封装进这个元素,执行一次就可以全部更新。 结束语 在这一篇文章中,我们涉及了许多方面,讲了新的 CursorAdapter 类的四种表现形式。 你已经学到了如何通过 DataEnvironment 、 CursorAdapter buileder、类设计器、和PRG建立 CursorAdapter 。 你也已经了解了建立本地、ODBC、OLE DB或XML型 CursorAdapter 类的基本方法, 并且怎样使这些类可更新。 下一个步骤是考虑如何把这些类应用到你的程序中。 对我来说,觉得 CursorAdapter 应用到任何程序的 UI 层中效果很好,同时应用于需要大量过程代码的许多商业开发。CursorAdapter并不是一个最好的选择对象用于层来传递数据。因为它把所有的来自数据源的数据转化成一个不便于携带的游标。 然而,在一个使用 CursorAdapter 类进行商业开发的方案中,它能接收来自数据源的数据, 然后用标准的 VFP 命令和函数处理数据,这是因为它产生的是一个 VFP 游标。 数据可以被转换为更一个较适当的类型,例如XML。 CursorAdapter 的另一个有利条件是通用的 OOP 接口,不管需要存取的数据是什么类型。 甚至用XML,需要大量代码来使类可更新,我们仍然可以用使用 CursorFill取回数据,用 TableUpdate 更新数据, 而且用 AError 返回错误, 像使用其他类型的 CursorAdapter一样。 基于一个事先的考虑或者计划,你想建立一个可以重复使用的CursorAdapter类。 这些类可以在程序之间重复使用或者在同一程序内部混合使用,来形成一种你的程序处理数据的标准方法。 VFP+SQL编程方法讨论与经验分享 (收藏)http://community.csdn.net/Expert/topic/4126/4126818.xml?temp=6.805056E-02
以下是我平时对SQL的使用点滴,欢迎各位指点! ************************************************************** SET MULTILOCKS ON LOCAL lcSqlConnectStr IF TYPE("_gnSqlConnectHandle") = "N" AND _gnSqlConnectHandle > 0 ************************************************ FUNCTION disVFPSql() *************************************** *-- 检查连接
IF SELECT(tcCurName) > 0
********************************************************************** IF TYPE("tcCurName")<>'C' OR ; SELECT (tcCurName) lnFldCnt = AFIELDS(laFlds) DO WHILE lnNextModifyRec <>0 CASE RECNO()<0 &&新增 OTHERWISE lnSqlReturn = sqlDo(lcSqlString) *-- 调用SQL设置 PROCEDURE load PROCEDURE init PROCEDURE txtPwd.Keypress 一、采用ado方式,可能代码会很多,但是会很灵活,而且可以让前台没有dbc文件了,如果采用远程视图方式,就不会写这么多代码了,但是必须要定义DBC(数据库)用于保存远程视图及连接等,前一种方式,楼主已经写的比较通俗简明了,我对后一种简单补充一下。
|
|
|