VFP学习、开发漫谈 (五)
应网友要求,今天就聊一聊在局域网环境下,如何提高客户机的访问速度。首先,数据要与程序分离,即:数据库(表)存放在服务器上,程序安装在客户端上。无论服务器的性能有多好,终究不如从本地启动程序来得快。但有一个问题需要考虑,这就是程序的自动更新问题。
一个运行中的程序是不可能更新自身的,所以,必须从另一个程序去更新。为此,我用 VFP 开发了一个通用的小程序 Start.exe 和一个参数文件 Install.ini,用它来检测程序文件是否需要更新,更新完成后再启动主程序。在桌面上建立应用程序快捷方式时,应指向 Start.exe 而不是应用程序本身。
Install.ini 文件中保存的是应用程序的更新路径以及应用程序的名称,内容仅两行:
UpdateDir= \\Server1\ERP$\Update
StartExeFile= main.exe
第一行设置程序的更新路径,即要从哪里查找新程序,第二行设置应用程序的文件名。建议将该文件加密,以防止用户修改该文件、查看应用程序的访问路径。为此,我还编写了一个字符串加密函数:
FUNCTION CryptStr(tcStr,tlType) && tcStr:目标字符串,tlType:操作类型 .t.[加密] .f.[解密]
LOCAL i,cRet,cChr
i = ASC(LEFT(tcStr,1)) && 取第一个字符的 Ascii码
IF (i>126 AND tlType) OR (i<=126 AND !tlType) && 若 i>126,说明字符串已加密,否则,说明字符串未加密
cRet = tcStr && 无需加解密,返回原字符串
ELSE
cRet = ''
FOR i = 1 TO LEN(tcStr)
cChr = SUBSTR(tcStr,i,1)
cRet = cRet + CHR(MOD(ASC(cChr)+128,256))
NEXT
ENDIF
RETURN cRet
ENDFUNC
Start.exe 的源代码相对比较简单,如下所示(以下代码仅保留了核心算法,有删减):
#DEFINE CRLF CHR(13)+CHR(10) && 定义回车换行符常量
LOCAL cStr,cUpdateDir,cStartExeFile
SET DEFAULT TO (JUSTPATH(SYS(16))) && 设默认路径为 Start.exe 之路径
IF !FILE('Install.ini') && 检测本地配置文件是否存在
MESSAGEBOX('未发现配置文件“Install.ini”!',16,'提示')
RETURN
ENDIF
cStr = FILETOSTR('Install.ini') && 本地配置文件放入字符串
IF ASC(LEFT(cStr,1)) < 127 && 本地配置文件加密
STRTOFILE(CryptStr(cStr,.t.),'Install.ini')
ENDIF
cStr = CryptStr(cStr,.f.) + CRLF && 解密并添加回车换行符防止末行未回车
cUpdateDir = ADDBS(STREXTRACT(cStr,'UpdateDir=',CRLF)) && 升级路径
cStartExeFile = STREXTRACT(cStr,'StartExeFile=',CRLF) && 主程序
IF FILE(cStartExeFile) && 主程序存在时,检测文件是否需要更新
= ADIR(aFileOld, cStartExeFile) && 获取本地主程序属性到数组 aFileOld[]
= ADIR(aFileNew, cUpdateDir+cStartExeFile) && 获取网络主程序属性到数组 aFileNew[]
IF aFileOld[1,2] # aFileNew[1,2] OR; && 若文件大小或更新时间不同,更新之
aFileOld[1,3] # aFileNew[1,3] OR ;
aFileOld[1,4] # aFileNew[1,4]
COPY FILE (cUpdateDir+'*.*') TO *.*
ENDIF
ELSE
COPY FILE (cUpdateDir+'*.*') TO *.* && 主程序不存在时,更新之
ENDIF
RUN /N &cStartExeFile && 启动主程序
QUIT
提高访问速度的第二个途径是,数据库的优化设计和编程方法。这个命题有点大,具体来说,可从以下几个方面考虑:
1. 数据表的记录越多,访问速度肯定越慢。在设计数据表时,要将当前数据和历史数据分开存放,对当前数据表可以添加、修改和删除,对历史数据仅能查询统计。通过定期将数据移至历史库,形成良性循环。
以我公司的材料明细账为例,每年的记录在10万条左右,每年末,我都将其复制到一个历史数据表中(如:明细账_2013.dbf),然后再将数据从原数据表中清除。为了加快对历史数据的访问,历史数据表应该包括相关表中的字段,如:材料名称、计量单位等,这样在查询时,无需与任何数据表连接,毕竟连接是需要时间的。这是典型的“以空间换速度”。另外,对经常检索的字段添加索引。
2. 对经常查询的字段建立索引标识,就不用我说了,每个用户都会想到。这里我要说的是:最好为每个表都建立一个主索引。假设工资表以“员工编号”为主索引,那么执行“UPDATE 工资表 SET 工资=8000 WHERE 员工编号=287”时,系统仅检索一条记录,若员工编号为普通索引,则系统会检索所有记录。
在设置主索引时,有一个问题需要注意:主索引不允许重复是包含逻辑删除记录的,即:未删除记录的索引值不能和逻辑删除记录的索引值相同。因此在索引条件中应该添加“FOR !DELETE()”,除非字段类型是“Integer(AutoInc)”。
3. 尽可能少用 Set Filter To 或数据环境中表的 Filter 属性,可以改用“条件索引”。因为设置了 Filter 过滤条件后,每移动一下记录指针,都需要判断过滤条件,对系统的响应速度影响很大。而采用条件索引就不存在此问题。
举例来说,物料主文件表有一个“类别”字段,1 表示材料,2 表示自制件。该表被 BOM 表单调用。先为该表建立两个索引标识:材料、自制件。这两个索引的表达式都是“物料代码”,但索引条件不同,分别是“类别=1”、“类别=2”。在表单的数据环境中,将“物料主文件.dbf”表添加两次,分别命名为“材料”和“自制件”,并分别指定索引标识为“材料”、“自制件”。这要比使用过滤条件访问数据快很多。
另外,将满足条件的记录放在游标中(我常使用 Select ... Into Cursor 游标名),也要比设置过滤条件好些。虽然执行查询时费些时间,但移动记录时就没有“停顿”感了。由于查询结果仅是表的一个“快照”不能实时反映新数据,因此,我一般都在表单上放置一个“刷新”按钮,用于重新查询。
4. 在“一对多”数据查询表单中,使用关联要比从子表中手工检索数据快,数据量越大,速度差异越明显。下图是采购计划表单的设计界面和数据环境:
5. 不要在打开表单时,就去执行耗时较长的查询操作,而应在用户单击某个命令按钮时再执行。比如:检验员对入库单进行审核时,如果从数据库中检索需要审核的入库单耗时较长,那么就不要试图在用户打开窗口时就显示这些入库单,而应该在表单上添加一个命令按钮,单击该按钮后才显示。
6. 适当把握记录的显示“时机”,使其“按需显示”。下图是一个材料字典窗口,初始时,仅显示材料的分类树,当单击某个分类节点时,才显示该分类的材料记录(查询材料记录的代码放在oleTree.NodeClick事件中):
7. 速度的快与慢本来就是一种主观上的操作感受,如果在用户等待过程中,能够显示操作进度或提示性文字,就会大大改善用户的使用体验。应该养成一个好的编程习惯:在用户等待过程中给出提示信息。
下面是一个显示提示信息的自定义函数,支持多行,窗口自动居中:
FUNCTION WaitWindow(cString,tnSeconds)
* cString: 要显示的信息,使用CHR(10)换行
* tnSeconds:等待的秒数,若省略则一直显示,直到发出 Wait Clear
#DEFINE CR CHR(13)
LOCAL nScrRows,nScrCols,nRows,nCols,i
nScrRows = SROWS() && 屏幕行数
nScrCols = SCOLS() && 屏幕列数
* 找出显示字符串的行数(nRows)和最大列数(nCols),并将行存入数组
nRows = OCCURS(CR,cString) + 1
cString = CR + cString + CR
nCols = 0
DIMENSION aStr[nRows]
FOR i = 1 TO nRows
aStr[i] = STREXTRACT(cString,CR,CR,i)
nCols = MAX(nCols,LEN(aStr[i]))
NEXT
* 重新生成显示字符串(每行居中)
cString = ''
FOR i = 1 TO nRows
cString = cString + IIF(i=1,'',CR)+PADC(aStr[i],nCols)
NEXT
* 以窗口方式居中显示
DO CASE
CASE EMPTY(tnSeconds)
WAIT cString WINDOW AT (nScrRows-nRows)/2+WLROW('')+4,(nScrCols-nCols)/2+WLCOL('') NOWAIT NOCLEAR
CASE TYPE('tnSeconds') = 'N'
WAIT cString WINDOW AT (nScrRows-nRows)/2+WLROW('')+4,(nScrCols-nCols)/2+WLCOL('') TIMEOUT tnSeconds
OTHERWISE
WAIT cString WINDOW AT (nScrRows-nRows)/2+WLROW('')+4,(nScrCols-nCols)/2+WLCOL('')
ENDCASE
ENDFUNC
8. 对于频繁调用的表单,如:第 6 条中的材料字典窗口,经常被其他表单调用,频繁加载和释放表单会消耗大量的系统资源,因此,可将关闭窗口操作改为隐藏窗口,下次要显示时,将窗口 Show 一下就可以了。
[ 本帖最后由 liuxingang28 于 2014-2-28 13:04 编辑 ]