| 建华 的个人资料ekiagz照片日志网络 | 帮助 |
|
8月20日 利用SQLServer的全局临时表防止用户重复登录在我们研发商务软件的时候,常常会碰到这样的一个问题:怎样防止用户重复登录我们的系统?特别是对于银行或是财务部门,更是要限制用户以其工号身份多次登入。www.c hinaitpower.coml0XCFz
可能会有人说在用户信息表中加一字段判断用户工号登录的状态,登录后写1,退出时写0,且登录时判断其标志位是否为1,如是则不让该用户工号登录。但是这样那势必会带来新的问题:如发生象断电之类不可预知的现象,系统是非正常退出,无法将标志位置为0,那么下次以该用户工号登录则不可登入,这该怎么办呢?www.c hinaitpower.coml0XCFz 或许我们能够换一下思路:有什么东西是在connection断开后能够被系统自动回收的呢?对了,SQL Server的临时表具备这个特性!但是我们这里的这种情况不能用局部临时表,因为局部临时表对于每一个connection来说都是个单独的对象,因此只能用全局临时表来达到我们的目的。www.c hinaitpower.coml0XCFz 好了,情况已明朗话了,我们能够写一个象下面这样简单的存储过程:www.c hinaitpower.coml0XCFz www.c hinaitpower.coml0XCFz create procedure gp_findtemptable -- 2001/10/26 21:36 zhuzhichao in nanjingwww.c hinaitpower.coml0XCFz /* 寻找以操作员工号命名的全局临时表www.c hinaitpower.coml0XCFz * 如无则将out参数置为0并创建该表,如有则将out参数置为1 www.c hinaitpower.coml0XCFz * 在connection断开连接后,全局临时表会被SQL Server自动回收www.c hinaitpower.coml0XCFz * 如发生断电之类的意外,全局临时表虽然还存在于tempdb中,但是已失去活性www.c hinaitpower.coml0XCFz * 用object_id函数去判断时会认为其不存在.www.c hinaitpower.coml0XCFz */www.c hinaitpower.coml0XCFz @v_userid varchar(6), -- 操作员工号www.c hinaitpower.coml0XCFz @i_out int out -- 输出参数 0:没有登录 1:已登录www.c hinaitpower.coml0XCFz aswww.c hinaitpower.coml0XCFz declare @v_sql varchar(100)www.c hinaitpower.coml0XCFz if object_id('tempdb.dbo.##'+@v_userid) is nullwww.c hinaitpower.coml0XCFz beginwww.c hinaitpower.coml0XCFz set @v_sql = 'create table ##'+@v_userid+'(userid varchar(6))'www.c hinaitpower.coml0XCFz exec (@v_sql)www.c hinaitpower.coml0XCFz set @i_out = 0www.c hinaitpower.coml0XCFz endwww.c hinaitpower.coml0XCFz elsewww.c hinaitpower.coml0XCFz set @i_out = 1www.c hinaitpower.coml0XCFz www.c hinaitpower.coml0XCFz 在这个过程中,我们看到假如以用户工号命名的全局临时表不存在时过程会去创建一张并把out参数置为0,假如已存在则将out参数置为1。www.c hinaitpower.coml0XCFz 这样,我们在我们的应用程式中调用该过程时,假如取得的out参数为1时,我们能够毫不客气地跳出一个message告诉用户说”对不起,此工号正被使用!”www.c hinaitpower.coml0XCFz (测试环境:服务器:winnt server 4.0 SQL Server7.0 工作站:winnt workstation)www.c hinaitpower.coml0XCFz 以上内容由 华夏名网 收集整理,如转载请注明原文出处,并保留这一部分内容。 4月22日 项目管理项目管理之我见(原创) chendiy @ sohu.com http://www.chinablog.com/user2/4009/archives/2005/projectManager.shtml 项目为什么会失败?为什么发布日期会一拖延再拖? 项目失败只有一个原因:就是项目经理不合格。除非这个项目经理在项目开始阶段就已经提出来了这个项目会失败,或者是完全属于项目之外不可抗拒的原因导致失败。 看到这儿项目经理也许不服,您请继续… 新增需求的原因导致失败?客户会让你新增100个需求而要你二天交货吗?必然是分析设计阶段没有充分考虑好可扩展性和新增需求导致现在不可控制而失败的! 程序员人力不足导致?人都没有到位,怎么会失败,多少人做多少人的事,多少人做多少人的计划,不会有失败。 程序员技能不够?项目经理是如何面试的?怎么在项目失败了才发现是程序员技能不够?有问题早提出来嘛。 测试人员没有做好?少来了,测试人员只是加了一道保障证明。程序很多流程都通过不了,程序还属于开发调试阶段,与测试人员有什么关系? 我在国内参加个大大小小好些项目,有日本外包,华为外包的,公司自主研发的。发现有这样一个概念很多项目经理都没有搞清楚:什么叫开发阶段?我认为开发阶段最多只能包括单元测试这一部分。综合测试绝对不能属于开发阶段了,也就是说不能到了最后验货阶段还有程序流程走不通,程序随便正常操作都会失败。程序随便正常操作都出现好多bug属于开发还没有完成,绝对还没有过单元测试阶段,离综合测试和验货阶段还早着呢。说明白点,还属于代码code阶段。 不懂程序设计的项目经理,往往不注重code人员,其实这是一个严重的错误。软件的质量来源于什么?由谁来保证?有的项目经理说是由测试人员来保证,就算测试人员的测试用例写得很详细,把需求中的每一个功能点都测试到了,那最后就没有问题了吗?当然不是,很多逻辑上的东西要程序员来保证不出问题的,而测试人员只是起一个验证的作用,问题不应该由测试人员来发现,而应该由开发人员来发现。也就是说,我们尽量不要让测试人员来发现问题。如果第一次测试有至少25%以上的用例通不过,那说明质量监控出了问题。这样的版本根本就不应该拿出来进行测试。由此分析,软件的质量是由程序员来保证的,而不是测试人员。 细细说来! 一个项目的成败与否,与项目的各个阶段皆有关系:需求都不清楚,开发起来 肯定是南辕北辙;分析设计不够好,会让编写的难以维护,随着新增需求的增多, 会导致整个系统混乱不可控制;编码不好,整个系统不稳定是必然的,Bug也是抓不尽的;测试不做好,系统是没有保证的,少了哪个环节都不行。 项目管理,我认为重点主要放在 项目计划(plan),进度监控(),质量监控(quality),风险预测这三个方面。不要说项目的失败是因为新需求引起的,一个没有新增需求和风险的项目是不存在的,承认这一点之后,我们就不会有很多怨言了。 以下从上面提到的几方面进行详述: 项目计划:没有项目计划,那失败还有什么话好说?大家都知道凡事预则立,不预则废。项目计划一定要包括这几方面的内容:各阶段里程碑时间点,各个里程碑的输出结果,风险预测,意外应对。计划一定要提前于交货时间计划,风险意外是否留下时间和应对处理方案? 进度监控:对每个阶段把握好,每个阶段要完成的任务一定要完成,如果完不成,是什么原因导致的?我们的应对策略是什么?我们要信任别人,但是不要忘记锁门。同样的,别人说完成了,你不能就认为别人完成了,要看到结果才能证明完成了。有的项目经理说,我也进度监控啦,他说完成了就完成了,谁想到没有完成?到底是程序员不诚实还是项目没有管理好?你没有锁好门,能怨别人偷你东西吗?还有一种情况就是不懂如何锁门――根本就不知道这一阶段的输出结果是什么?当然进度监控就是一句空话了。 质量监控:也应该是分阶段进行的,每一个阶段的质量监控内容有所不同。 需求分析阶段的质量监控就是完整而又正确的理解用户需求,需求是否清楚可懂,写用例的测试人员是否明白需求? 分析设计阶段的质量监控就是设计是否完全满足需求?这个设计方案是否满足以后新功能的扩展?以及是否有考虑到新功能的意外和设备环境,运行平台的变化? 编码阶段的质量监控就是变量命名是否规范?代码是否可读?是否有详细的注释?是否有重复代码?要知道重复代码是必然会造成系统不稳定,bug成群的。可变部分的代码和不可变部分的代码是否分离。要知道上面讲的每一部分如果没有做好,都会导致后期的产品出现大量问题。代码阶段还有一个重要的工作就是做code review代码公开评审,你自己发现不了的问题别人也许就看得见。 单元测试阶段的质量监控任务就是单元测试代码是否测试通过?代码覆盖是否完全?单元测试报告提交情况如何?单元测试用例有没有做好? 综合测试阶段质量监控任务当然就是看用例是否完全?是否全部真正执行?测试报告有没有写好? 回归测试当然得看以前测试的Bug是否还在,如果还在,当然是无条件打回去重新开发。 测试阶段最主要的监控就是看用例是否真正执行,是否有安全性测试?破坏性测试?异常测试,压力测试? 项目管理表格略. 以上的每个阶段要完成了才能进行下一阶段,否则会造成混乱出现问题的。想并行进行节约时间反而浪费了时间! 4月12日 Oracle 一个好用的SQL语句对于数型结构的表,采用一个递归的SQL语句查出某个节点下所有的子节点
select * from spcpart
start with partid=2
connnect by prior partid=parentid 通过Web Service让Delphi/Visual Basic程序访问EJBhttp://www-128.ibm.com/developerworks/cn/webservices/ws-ejbacess/index.html 1月18日 SQL Excel Access 数据转换Transact-SQL语句进行导入导出:
1.在SQL SERVER里查询access数据: SELECT * FROM OpenDataSource( \\\\\\\\\\\\\\\'Microsoft.Jet.OLEDB.4.0\\\\\\\\\\\\\\\',\\\\\\\\\\\\\\\'Data Source="c:\\\\\\\\\\\\\\\\DB.mdb";User ID=Admin;Password=\\\\\\\\\\\\\\\')……表名 2.将access导入SQL server在SQL SERVER 里运行:
SELECT * INTO newtable FROM OPENDATASOURCE (\\\\\\\\\\\\\\\'Microsoft.Jet.OLEDB.4.0\\\\\\\\\\\\\\\',\\\\\\\\\\\\\\\'Data Source="c:\\\\\\\\\\\\\\\\DB.mdb";User ID=Admin;Password=\\\\\\\\\\\\\\\' )……表名
3.将SQL SERVER表里的数据插入到Access表中在SQL SERVER 里运行:
insert into OpenDataSource( \\\\\\\\\\\\\\\'Microsoft.Jet.OLEDB.4.0\\\\\\\\\\\\\\\',\\\\\\\\\\\\\\\'Data Source=" c:\\\\\\\\\\\\\\\\DB.mdb";User ID=Admin;Password=\\\\\\\\\\\\\\\')……表名 (列名1,列名2) select 列名1,列名2 from sql表
实例:insert into OPENROWSET(\\\\\\\\\\\\\\\'Microsoft.Jet.OLEDB.4.0\\\\\\\\\\\\\\\',\\\\\\\\\\\\\\\'C:\\\\\\\\\\\\\\\\db.mdb\\\\\\\\\\\\\\\';\\\\\\\\\\\\\\\'admin\\\\\\\\\\\\\\\';\\\\\\\\\\\\\\\'\\\\\\\\\\\\\\\', Test) select id,name from Test INSERT INTO OPENROWSET(\\\\\\\\\\\\\\\'Microsoft.Jet.OLEDB.4.0\\\\\\\\\\\\\\\', \\\\\\\\\\\\\\\'c:\\\\\\\\\\\\\\\\trade.mdb\\\\\\\\\\\\\\\'; \\\\\\\\\\\\\\\'admin\\\\\\\\\\\\\\\'; \\\\\\\\\\\\\\\'\\\\\\\\\\\\\\\', 表名) SELECT * FROM sqltablename 二、SQL SERVER 和EXCEL的数据导入导出 1、在SQL SERVER里查询Excel数据:
SELECT * FROM OpenDataSource( \\\\\\\\\\\\\\\'Microsoft.Jet.OLEDB.4.0\\\\\\\\\\\\\\\',\\\\\\\\\\\\\\\'Data Source="c:\\\\\\\\\\\\\\\\book1.xls";User ID=Admin;Password=;Extended properties=Excel 5.0\\\\\\\\\\\\\\\')……[Sheet1$]
下面是个查询的示例,它通过用于 Jet 的 OLE DB 提供程序查询 Excel 电子表格。 SELECT * FROM OpenDataSource ( \\\\\\\\\\\\\\\'Microsoft.Jet.OLEDB.4.0\\\\\\\\\\\\\\\',\\\\\\\\\\\\\\\'Data Source="c:\\\\\\\\\\\\\\\\Finance\\\\\\\\\\\\\\\\account.xls";User ID=Admin;Password=;Extended properties=Excel 5.0\\\\\\\\\\\\\\\')……xactions 2、将Excel的数据导入SQL server :
SELECT * into newtable FROM OpenDataSource( \\\\\\\\\\\\\\\'Microsoft.Jet.OLEDB.4.0\\\\\\\\\\\\\\\',\\\\\\\\\\\\\\\'Data Source="c:\\\\\\\\\\\\\\\\book1.xls";User ID=Admin;Password=;Extended properties=Excel 5.0\\\\\\\\\\\\\\\')……[Sheet1$]
实例:SELECT * into newtable FROM OpenDataSource( \\\\\\\\\\\\\\\'Microsoft.Jet.OLEDB.4.0\\\\\\\\\\\\\\\',\\\\\\\\\\\\\\\'Data Source="c:\\\\\\\\\\\\\\\\Finance\\\\\\\\\\\\\\\\account.xls";User ID=Admin;Password=;Extended properties=Excel 5.0\\\\\\\\\\\\\\\')……xactions 3、将SQL SERVER中查询到的数据导成一个Excel文件T-SQL代码:
EXEC master……xp_cmdshell \\\\\\\\\\\\\\\'bcp 库名。dbo.表名out c:\\\\\\\\\\\\\\\\Temp.xls -c -q -S"servername" -U"sa" -P""\\\\\\\\\\\\\\\'参数:S 是SQL服务器名;U是用户;P是密码说明:还可以导出文本文件等多种格式
实例:EXEC master……xp_cmdshell \\\\\\\\\\\\\\\'bcp saletesttmp.dbo.CusAccount out c:\\\\\\\\\\\\\\\\temp1.xls -c -q -S"pmserver" -U"sa" -P"sa"\\\\\\\\\\\\\\\' EXEC master……xp_cmdshell \\\\\\\\\\\\\\\'bcp "SELECT au_fname, au_lname FROM pubs……authors ORDER BY au_lname" queryout C:\\\\\\\\\\\\\\\\ authors.xls -c -Sservername -Usa -Ppassword\\\\\\\\\\\\\\\'
在VB6中应用ADO导出EXCEL文件代码:Dim cn As New ADODB.Connection cn.open "Driver={SQL Server};Server=WEBSVR;DataBase=WebMis;UID=sa;WD=123;" cn.execute "master……xp_cmdshell \\\\\\\\\\\\\\\'bcp "SELECT col1, col2 FROM 库名。dbo.表名" queryout E:\\\\\\\\\\\\\\\\DT.xls -c -Sservername -Usa -Ppassword\\\\\\\\\\\\\\\'"
4、在SQL SERVER里往Excel插入数据:
insert into OpenDataSource( \\\\\\\\\\\\\\\'Microsoft.Jet.OLEDB.4.0\\\\\\\\\\\\\\\',\\\\\\\\\\\\\\\'Data Source="c:\\\\\\\\\\\\\\\\Temp.xls";User ID=Admin;Password=;Extended properties=Excel 5.0\\\\\\\\\\\\\\\')……table1 (A1,A2,A3) values (1,2,3)
T-SQL代码:INSERT INTO OPENDATASOURCE(\\\\\\\\\\\\\\\'Microsoft.JET.OLEDB.4.0\\\\\\\\\\\\\\\',\\\\\\\\\\\\\\\'Extended Properties=Excel 8.0;Data source=C:\\\\\\\\\\\\\\\\training\\\\\\\\\\\\\\\\inventur.xls\\\\\\\\\\\\\\\')……[Filiale1$] (bestand, produkt) VALUES (20, \\\\\\\\\\\\\\\'Test\\\\\\\\\\\\\\\') 总结:利用以上语句,我们可以方便地将SQL SERVER、ACCESS和EXCEL电子表格软件中的数据进行转换,为我们提供了极大方便! 12月5日 Delphi的TWebBrowser编程综述 Delphi3开始有了TWebBrowser构件,不过那时是以ActiveX控件的形式出现的,而且需要自己引入,在其后的4.0和5.0中,它就在封装好shdocvw.dll之后作为Internet构件组之一出现在构件面板上了。常常听到有人骂Delphi的帮助做得极差,这次的TWebBrowser又是Microsoft的东东,自然不会好到哪里去,虽说MSDN上什么都有,可是内容太过庞杂,如果没有入口点更是件烦人的事,查找起来给人的感觉大概可以用一句话来形容:非常复杂、复杂非常。 这里有平时我自己用TWebBrowser做程序的一些心得和上网收集到的部分例子和资料,整理了一下,希望能给有兴趣用TWebBrowser编程的朋友带来些帮助。 1、初始化和终止化(Initialization & Finalization) 大家在执行TWebBrowser的某个方法以进行期望的操作,如ExecWB等的时候可能都碰到过“试图激活未注册的丢失目标”或“OLE对象未注册”等错误,或者并没有出错但是得不到希望的结果,比如不能将选中的网页内容复制到剪贴板等。以前用它编程的时候,我发现ExecWB有时侯起作用但有时侯又不行,在Delphi生成的缺省工程主窗口上加入TWebBrowser,运行时并不会出现“OLE对象未注册”的错误。同样是一个偶然的机会,我才知道OLE对象需要初始化和终止化(懂得的东东实在太少了)。 我用我的前一篇文章《Delphi程序窗口动画&正常排列平铺的解决》所说的方法编程,运行时出了上面所说的错误,我便猜想应该有OleInitialize之类的语句,于是,找到并加上了下面几句话,终于搞定!究其原因,我想大概是由于TWebBrowser是一个嵌入的OLE对象而不算是用Delphi编写的VCL吧。 initialization OleInitialize(nil); finalization try OleUninitialize; except end; 这几句话放在主窗口所有语句之后,“end.”之前。 ----------------------------------------------------------------------------------- 2、EmptyParam 在Delphi 5中TWebBrowser的Navigate方法被多次重载: procedure Navigate(const URL: WideString); overload; procedure Navigate(const URL: WideString; var Flags: OleVariant); overload; procedure Navigate(const URL: WideString; var Flags: OleVariant; var TargetFrameName: OleVariant); overload; procedure Navigate(const URL: WideString; var Flags: OleVariant; var TargetFrameName: OleVariant; var PostData: OleVariant); overload; procedure Navigate(const URL: WideString; var Flags: OleVariant; var TargetFrameName: OleVariant; var PostData: OleVariant; var Headers: OleVariant); overload; 而在实际应用中,使用后几种方法调用时,由于我们很少用到后面几个参数,但函数声明又要求是变量参数,一般的做法如下: var t:OleVariant; begin webbrowser1.Navigate(edit1.text,t,t,t,t); end; 需要定义变量t(还有很多地方要用到它),很麻烦。其实我们可以用EmptyParam来代替(EmptyParam是一个公用的Variant空变量,不要对它赋值),只需一句话就可以了: webbrowser1.Navigate(edit1.text,EmptyParam,EmptyParam,EmptyParam,EmptyParam); 虽然长一点,但比每次都定义变量方便得多。当然,也可以使用第一种方式。 webbrowser1.Navigate(edit1.text) ----------------------------------------------------------------------------------- 3、命令操作 常用的命令操作用ExecWB方法即可完成,ExecWB同样多次被重载: procedure ExecWB(cmdID: OLECMDID; cmdexecopt: OLECMDEXECOPT); overload; procedure ExecWB(cmdID: OLECMDID; cmdexecopt: OLECMDEXECOPT; var pvaIn: OleVariant); overload; procedure ExecWB(cmdID: rOLECMDID; cmdexecopt: OLECMDEXECOPT; var pvaIn: OleVariant; var pvaOut: OleVariant); overload; 打开: 弹出“打开Internet地址”对话框,CommandID为OLECMDID_OPEN(若浏览器版本为IE5.0, 则此命令不可用)。 另存为:调用“另存为”对话框。 ExecWB(OLECMDID_SAVEAS,OLECMDEXECOPT_DODEFAULT, EmptyParam, EmptyParam); 打印、打印预览和页面设置: 调用“打印”、“打印预览”和“页面设置”对话框(IE5.5及以上版本才支持打 印预览,故实现应该检查此命令是否可用)。 ExecWB(OLECMDID_PRINT, OLECMDEXECOPT_DODEFAULT, EmptyParam, EmptyParam); if QueryStatusWB(OLECMDID_PRINTPREVIEW)=3 then ExecWB(OLECMDID_PRINTPREVIEW, OLECMDEXECOPT_DODEFAULT, EmptyParam,EmptyParam); ExecWB(OLECMDID_PAGESETUP, OLECMDEXECOPT_DODEFAULT, EmptyParam, EmptyParam); 剪切、复制、粘贴、全选: 功能无须多说,需要注意的是:剪切和粘贴不仅对编辑框文字,而且对网页上的非编 辑框文字同样有效,用得好的话,也许可以做出功能特殊的东东。获得其命令使能状 态和执行命令的方法有两种(以复制为例,剪切、粘贴和全选分别将各自的关键字替 换即可,分别为CUT,PASTE和SELECTALL): A、用TWebBrowser的QueryStatusWB方法。 if(QueryStatusWB(OLECMDID_COPY)=OLECMDF_ENABLED) or OLECMDF_SUPPORTED) then ExecWB(OLECMDID_COPY, OLECMDEXECOPT_DODEFAULT, EmptyParam, EmptyParam); B、用IHTMLDocument2的QueryCommandEnabled方法。 var Doc: IHTMLDocument2; begin Doc :=WebBrowser1.Document as IHTMLDocument2; if Doc.QueryCommandEnabled('Copy') then Doc.ExecCommand('Copy',false,EmptyParam); end; 查找: 参考第九条“查找”功能。 ----------------------------------------------------------------------------------- 4、字体大小 类似“字体”菜单上的从“最大”到“最小”五项(对应整数0~4,Largest等假设为五个菜单项的名字,Tag 属性分别设为0~4)。 A、读取当前页面字体大小。 var t: OleVariant; Begin WebBrowser1.ExecWB(OLECMDID_ZOOM, OLECMDEXECOPT_DONTPROMPTUSER, EmptyParam,t); case t of 4: Largest.Checked :=true; 3: Larger.Checked :=true; 2: Middle.Checked :=true; 1: Small.Checked :=true; 0: Smallest.Checked :=true; end; end; B、设置页面字体大小。 Largest.Checked :=false; Larger.Checked :=false; Middle.Checked :=false; Small.Checked :=false; Smallest.Checked :=false; TMenuItem(Sender).Checked :=true; t :=TMenuItem(Sender).Tag; WebBrowser1.ExecWB(OLECMDID_ZOOM, OLECMDEXECOPT_DONTPROMPTUSER, t,t); ----------------------------------------------------------------------------------- 5、添加到收藏夹和整理收藏夹 const CLSID_ShellUIHelper: TGUID = '{64AB4BB7-111E-11D1-8F79-00C04FC2FBE1}'; var p:procedure(Handle: THandle; Path: PChar); stdcall; procedure TForm1.OrganizeFavorite(Sender: Tobject); var H: HWnd; begin H := LoadLibrary(PChar('shdocvw.dll')); if H <> 0 then begin p := GetProcAddress(H, PChar('DoOrganizeFavDlg')); if Assigned(p) then p(Application.Handle, PChar(FavFolder)); end; FreeLibrary(h); end; procedure TForm1.AddFavorite(Sender: TObject); var ShellUIHelper: ISHellUIHelper; url, title: Olevariant; begin Title := Webbrowser1.LocationName; Url := Webbrowser1.LocationUrl; if Url <> '' then begin ShellUIHelper := CreateComObject(CLSID_SHELLUIHELPER) as IShellUIHelper; ShellUIHelper.AddFavorite(url, title); end; end; 用上面的通过ISHellUIHelper接口来打开“添加到收藏夹”对话框的方法比较简单,但是有个缺陷,就是打开的窗口不是模式窗口,而是独立于应用程序的。可以想象,如果使用与OrganizeFavorite过程同样的方法来打开对话框,由于可以指定父窗口的句柄,自然可以实现模式窗口(效果与在资源管理器和IE中打开“添加到收藏夹”对话框相同)。问题显然是这样的,上面两个过程的作者当时只知道shdocvw.dll中DoOrganizeFavDlg的原型而不知道DoAddToFavDlg的原型,所以只好用ISHellUIHelper接口来实现(或许是他不够严谨,认为是否是模式窗口无所谓?)。 下面的过程就告诉你DoAddToFavDlg的函数原型。需要注意的是,这样打开的对话框并不执行“添加到收藏夹”的操作,它只是告诉应用程序用户是否选择了“确定”,同时在DoAddToFavDlg的第二个参数中返回用户希望放置Internet快捷方式的路径,建立.Url文件的工作由应用程序自己来完成。 procedure TForm1.AddFavorite(IE: TEmbeddedWB); procedure CreateUrl(AUrlPath, AUrl: PChar); var URLfile: TIniFile; begin URLfile := TIniFile.Create(String(AUrlPath)); RLfile.WriteString('InternetShortcut', 'URL', String(AUrl)); RLfile.Free; end; var AddFav: function(Handle: THandle; UrlPath: PChar; UrlPathSize: Cardinal; Title: PChar; TitleSize: Cardinal; FavIDLIST: pItemIDList): Bool; stdcall; FDoc: IHTMLDocument2; UrlPath, url, title: array[0..MAX_PATH] of char; H: HWnd; pidl: pItemIDList; FRetOK: Bool; begin FDoc := IHTMLDocument2(IE.Document); if FDoc = nil then exit; StrPCopy(Title, FDoc.Get_title); StrPCopy(url, FDoc.Get_url); if Url <> '' then begin H := LoadLibrary(PChar('shdocvw.dll')); if H <> 0 then begin SHGetSpecialFolderLocation(0, CSIDL_FAVORITES, pidl); AddFav := GetProcAddress(H, PChar('DoAddToFavDlg')); if Assigned(AddFav) then FRetOK :=AddFav(Handle, UrlPath, Sizeof(UrlPath), Title, Sizeof(Title), pidl) end; FreeLibrary(h); if FRetOK then CreateUrl(UrlPath, Url); end end; ----------------------------------------------------------------------------------- 6、使WebBrowser获得焦点 TWebBrowser非常特殊,它从TWinControl继承来的SetFocus方法并不能使得它所包含的文档获得焦点,从而不能立即使用Internet Explorer本身具有得快捷键,解决方法如下:< procedure TForm1.SetFocusToDoc; begin if WebBrowser1.Document <> nil then with WebBrowser1.Application as IOleobject do DoVerb(OLEIVERB_UIACTIVATE, nil, WebBrowser1, 0, Handle, GetClientRect); end; 除此之外,我还找到一种更简单的方法,这里一并列出: if WebBrowser1.Document <> nil then IHTMLWindow2(IHTMLDocument2(WebBrowser1.Document).ParentWindow).focus 刚找到了更简单的方法,也许是最简单的: if WebBrowser1.Document <> nil then IHTMLWindow4(WebBrowser1.Document).focus 还有,需要判断文档是否获得焦点这样来做: if IHTMLWindow4(WebBrowser1.Document).hasfocus then ----------------------------------------------------------------------------------- 7、点击“提交”按钮 如同程序里每个窗体上有一个“缺省”按钮一样,Web页面上的每个Form也有一个“缺省”按钮——即属性为“Submit”的按钮,当用户按下回车键时就相当于鼠标单击了“Submit”。但是TWebBrowser似乎并不响应回车键,并且,即使把包含TWebBrowser的窗体的KeyPreview设为True,在窗体的KeyPress事件里还是不能截获用户向TWebBrowser发出的按键。 我的解决办法是用ApplicatinEvents构件或者自己编写TApplication对象的OnMessage事件,在其中判断消息类型,对键盘消息做出响应。至于点击“提交”按钮,可以通过分析网页源代码的方法来实现,不过我找到了更为简单快捷的方法,有两种,第一种是我自己想出来的,另一种是别人写的代码,这里都提供给大家,以做参考。 A、用SendKeys函数向WebBrowser发送回车键 在Delphi 5光盘上的Info\Extras\SendKeys目录下有一个SndKey32.pas文件,其中包含了两个函数SendKeys和AppActivate,我们可以用SendKeys函数来向WebBrowser发送回车键,我现在用的就是这个方法,使用很简单,在WebBrowser获得焦点的情况下(不要求WebBrowser所包含的文档获得焦点),用一条语句即可: Sendkeys('~',true);// press RETURN key SendKeys函数的详细参数说明等,均包含在SndKey32.pas文件中。 B、在OnMessage事件中将接受到的键盘消息传递给WebBrowser。 procedure TForm1.ApplicationEvents1Message(var Msg: TMsg; var Handled: Boolean); {fixes the malfunction of some keys within webbrowser control} const StdKeys = [VK_TAB, VK_RETURN]; { standard keys } ExtKeys = [VK_DELETE, VK_BACK, VK_LEFT, VK_RIGHT]; { extended keys } fExtended = $01000000; { extended key flag } begin Handled := False; with Msg do if ((Message >= WM_KEYFIRST) and (Message <= WM_KEYLAST)) and ((wParam in StdKeys) or {$IFDEF VER120}(GetKeyState(VK_CONTROL) < 0) or {$ENDIF} (wParam in ExtKeys) and ((lParam and fExtended) = fExtended)) then try if IsChild(Handle, hWnd) then { handles all browser related messages } begin with {$IFDEF VER120}Application_{$ELSE}Application{$ENDIF} as IOleInPlaceActiveObject do Handled := TranslateAccelerator(Msg) = S_OK; if not Handled then begin Handled := True; TranslateMessage(Msg); DispatchMessage(Msg); end; end; except end; end; // MessageHandler (此方法来自EmbeddedWB.pas) ----------------------------------------------------------------------------------- 8、直接从TWebBrowser得到网页源码及Html 下面先介绍一种极其简单的得到TWebBrowser正在访问的网页源码的方法。一般方法是利用TWebBrowser控件中的Document对象提供的IPersistStreamInit接口来实现,具体就是:先检查WebBrowser.Document对象是否有效,无效则退出;然后取得IPersistStreamInit接口,接着取得HTML源码的大小,分配全局堆内存块,建立流,再将HTML文本写到流中。程序虽然不算复杂,但是有更简单的方法,所以实现代码不再给出。其实基本上所有IE的功能TWebBrowser都应该有较为简单的方法来实现,获取网页源码也是一样。下面的代码将网页源码显示在Memo1中。 Memo1.Lines.Add(IHtmlDocument2(WebBrowser1.Document).Body.OuterHtml); 同时,在用TWebBrowser浏览HTML文件的时候要将其保存为文本文件就很简单了,不需要任何的语法解析工具,因为TWebBrowser也完成了,如下: Memo1.Lines.Add(IHtmlDocument2(WebBrowser1.Document).Body.OuterText); ----------------------------------------------------------------------------------- 9、“查找”功能 查找对话框可以在文档获得焦点的时候通过按键Ctrl-F来调出,程序中则调用IOleCommandTarget对象的成员函数Exec执行OLECMDID_FIND操作来调用,下面给出的方法是如何在程序中用代码来做出文字选择,即你可以自己设计查找对话框。 var Doc: IHtmlDocument2; TxtRange: IHtmlTxtRange; begin Doc :=WebBrowser1.Document as IHtmlDocument2; Doc.SelectAll; //此处为简写,选择全部文档的方法请参见第三条命令操作 //这句话尤为重要,因为IHtmlTxtRange对象的方法能够操作的前提是 //Document已经有一个文字选择区域。由于接着执行下面的语句,所以不会 //看到文档全选的过程。 TxtRange :=Doc.Selection.CreateRange as IHtmlTxtRange; TxtRange.FindText('Text to be searched',0.0); TxtRange.Select; end; 还有,从Txt.Get_text可以得到当前选中的文字内容,某些时候是有用的。 ----------------------------------------------------------------------------------- 10、提取网页中所有链接 这个方法来自大富翁论坛hopfield朋友的对一个问题的回答,我本想自己试验,但总是没成功。 var doc:IHTMLDocument2; all:IHTMLElementCollection; len,i:integer; item:OleVariant; begin doc:=WebBrowser1 .Document as IHTMLDocument2; all:=doc.Get_links; //doc.Links亦可 len:=all.length; for i:=0 to len-1 do begin item:=all.item(i,varempty); //EmpryParam亦可 memo1.lines.add(item.href); end; end; ----------------------------------------------------------------------------------- 11、设置TWebBrowser的编码 为什么我总是错过很多机会?其实早就该想到的,但是一念之差,便即天壤之别。当时我要是肯再多考虑一下,多试验一下,这就不会排到第11条了。下面给出一个函数,搞定,难以想象的简单。 procedure SetCharSet(AWebBrowser: TWebBrowser; ACharSet: String); var RefreshLevel: OleVariant; Begin IHTMLDocument2(AWebBrowser.Document).Set_CharSet(ACharSet); RefreshLevel :=7; //这个7应该从注册表来,帮助有Bug。 AWebBrowser.Refresh2(RefreshLevel); End; 作者Blog:http://blog.csdn.net/dusy/ 10月27日 项目解释项目管理分九个知识领域,分别是成本管理、质量管理、时间管理、范围管理、人力资源管理、沟通管理、风险管理、采购管理和整体管理。 其中时间,质量和成本管理构成了三角形 大家在纸上画一个三角形 在各个边上标上时间、质量、成本(等边三角形) 任何一方的移动必定带动其他的变形,如果时间缩短,怎么样?就是我们常说的“献礼工程”,同时必定会影响质量和成本。问大家一个问题,这个三角形中间是什么东东? 对,是范围管理,也就是我们说的项目范围。这也就是我们常说的项目“项目管理三角形” 下面介绍一下项目管理的“项目管理三角形“ 项目三角形中的成本,主要来自于所需资源的成本,自然也包括人力资源的成本。这个相信很好理解。 为了缩短项目时间,就需要增加项目成本(资源)或减少项目范围; 为了节约项目成本(资源),可以减少项目范围或延长项目时间; 如果需求变化导致增加项目范围,就需要增加项目成本(资源)或延长项目时间 通过“项目管理三角形“我们了解了项目成本、时间,质量和范围的简单定义。 我们说一个项目经理有多少时间是用来做沟通的工作的? 应该不少于75%的时间是用来沟通的,所以项目管理将项目沟通管理单独列了出来。 所有这些领域都有一个主线就是项目的整体管理来统一的。 由于时间的限制我们不详细讨论其他的知识领域,因为今天是入门的,哈哈 另外项目管理除了九个知识领域,还应该了解5个过程组 5个过程组就是:启动,计划,执行,控制,收尾。 这5个过程组贯穿于每个知识领域的始终,你们了解吗? 举个例子字来说: 某人(比喻)好不容易找了个女朋友,为了增进进一步的距离,他想来个欧亚8日游,于是他把自己多年的积蓄——3万元,一次性投入。 但在旅游过程中,他的MM看上了另外一个帅哥,于是人财两空,说明什么问题? 说明他的项目启动的时候就出现了问题,没有很好的做市场调研,结果过程就没有办法控制。 根据PMI的解释,接单之后项目自然转入启动阶段 于是他刻苦的工作,终于又攒了3万,这次他不和美女旅游了,考虑到自己的费用,他请这个姑娘看了场电影。 于是他带这个这个姑娘看了——《第一滴血》 看的那叫爽,姑娘看的也很爽,看看完后她觉得这个家伙有暴力倾向,于是又分手。说明什么问题? 对,没有进行有效的需求调查,也就是在计划的时候没有明确的需求定义。 于是他下次的时候知道了姑娘爱看歌舞剧,于是他就请一个靓女看了《天鹅湖》,可是以外有发生了—— 进去后发现座位不在一起,等他们把位子换到一起的时候歌舞剧结束了,这说明什么? 对,说明没有很好的执行,起码在执行过程中没有进行有效的监督。 其他的过程不一一解释,我在这里强调的是收尾的重要性。 我们往往非常注重合同性收尾,却总是忽略管理性收尾。什么是管理性收尾呢? 某人同志吸取了所有的经验教训,终于领了结婚证,还应该干些什么呢? 对了,还应该把所有的经验教训总结一下,以书面的形式汇报给老妈,并张贴于门后。 然后在中堂挂一幅对联:欲谈恋爱者需先阅读门后之——《恋爱指南》 以后凡是自己的兄弟姐妹要谈恋爱的,必须先参阅门后的恋爱指南。 这样能起到什么效果呢,对,以后他们的恋爱项目操作至少能停留在这个水平。 这个过程怎样来保证呢,对,还需要我们的QA人员,也就是他的妈妈负责质量控制。 家规一条,不参阅者或不照此操作者不许谈恋爱! 大公司一般有质量管理部门(QA),QA的成员基本上都是由非常有经验的PM转型过来的老狐狸,是老总接班人的有力争夺者:) 这也是我们说一个失败的项目会培养一批优秀的项目经理的原因。 哪个门后的《恋爱指南》我们称之为文档,文档重要吗?我们说在电信科技处的同志们说重要,为什么因为他们管这个,但对于我们呢? 大家拿起你身边的一只笔,告诉我他多长? 有的说10厘米,有的说10。0987厘米。 我们说他的估算很精确,但不准确!! 这是我如果拿一只笔告诉你正好10厘米,然后和你的笔比对你是不是就比较容易得出测算? 这说明文档是非常重要的,有的人认为文档是最无聊的,项目结束后做个总结不就是了吗。 错,文档的整理应该贯穿于项目管理的始终。 文档的管理是对项目进行良好的跟踪和监控的一个手段,简单的讲就是根据你的项目计划进行你的文档管理。 一般档案分类主线是:立项、计划、执行、结束4大类;然后在每大类中,再根据任务或者团组分类管理,根据哪个需要根据你项目复杂程度和管理习惯,总之原则是方便你对整个项目进度的追踪。 以上我们讲了项目管理的九个知识领域,五大过程组,还有“项目管理三角形“,下面我们讲PMBOK。 PMBOK是项目管理圣经,也就是Project management body of knowledge,项目管理知识体系指南 它是美国项目管理协会(PMI)的核心指导出版物 但它象一本字典,往往你看到第三页会睡着:) 在此简单介绍美国项目管理协会(PMI)和国际项目管理协会(IPMA) 美国项目管理协会只有PMP一个证书,而IPMA有四级,你可以一毕业就可以考试,这个我们后面详细的讲。 下面讲几个名词,如果你掌握了,一和人讲项目管理你就抛出来,一定没有人敢小看你。 他们是WBS、甘特图、基准(BASELINE)、项目干系人和关键路径 WBS是WORK BREAKDOWN STRUCTRE ,工作分解结构 WBS的定义还是很麻烦的,PM要召开团队进行讨论,向成员提供与项目相关的所有详细资料,并把WBS树分解到二层三层。然后要花上一段时间让成员 进行头脑风暴式(BRAINING STORM)思考,制订工作产出和相应人员的职责,记录每一个工作包的完成标准。 比如我们要结婚了,怎么来分解呢 无非是办酒席,拍结婚照,,等等,这个在论坛上曾有人做了详细的分解,大家都可以找到。 我们说为什么WBS重要,而且大部分项目管理的咨询都是针对WBS的咨询 因为WBS做好了,以后工作就有了参考物,你就知道在不同的阶段你应该干什么,完成到什么进度。 其实WBS的划分是没有规则的,主要的考虑角度是方便你做各类的统计工作,为管理服务。 同样的一个项目其管理的侧重点不同,WBS结构的划分也可能是完全不同的。 衡量划分好坏的标准应该是看其是否满足你管理的需要。 甘特图也叫横道图等,很多名称,我们说它是甘特在第一次世界大战时开始使用,它就是在WBS的基础上将WBS形象化老控制进度 对于基准,我象举个例子。 我们在没有结婚之前,你脚踩几只船? 我们说法律允许但道德不允许,但你可以脚踩N只船:) 但当有一天你和你的朋友进了一个小黑屋子,然后带了两个盖章的本本的时候,你还可以脚踩N只船吗? 我们说此时就不允许了,因为你过了一个基准线(BASELINE) 如果你还想脚踩N只船就需要重新回小黑屋子再盖两个章就可以了。 那我们的项目要越轨怎么办,也就是项目变更? 我们说对这样的项目变更会影响各要素比如时间,成本,质量等 我们应该统一由项目管理办公室来进行控制,如果你要变更基准,必须要进行严格的限制。 在客户提出变更请求时,要建立变更申请登记表和变更申请表,并让客户签字。 有时候一些不是非常关键的模块PM也不至于一点不讲情面,该卖面子的时候还是要卖,尤其是当着对方领导的面,千万要 卖面子,但是也别卖的太干脆,不要让他们得到的太容易。 PM在变更管理中需要做的是分析变更请求,评估变更可能带来的风险和修改基准文件。 项目管理过程本人做项目经理工作多年,感到做这个工作最要紧的就是要明白什么是因地制宜、因势利导,只有最合适的,没有什么叫对的,什么叫错的,项目经理最忌讳的就是完美主义倾向,尤其是做技术人员出身的,喜欢寻找标准答案,耽误了工作进度,也迷茫了自己。以下是本人一些做项目的个人体会,写出来供大家指点,在讨论过程中共同提高水平。 项目开始阶段是一个最重要的阶段。项目经理在接手一个新项目的时候,首先要尽可能地多从各个方面了解项目的情况,如: 1. 这个项目是什么项目,具体大概做什么事情,是谁提出来的,目的是解决什么问题。在国内很多客户都很不成熟的情况下,千万不要根据项目的名称望文生义地去想象项目的目标。一个名为“办公自动化”的项目很有可能在你进场以后一个月才发现客户其实需要的是一个计算机生产管理辅助信息系统系统。前期了解情况的工作越详细,后面的惊讶就越少,项目的风险就越小。 2 .这个项目里牵涉哪些方面的人,如投资方、具体业务干系方、项目建成后的运营方、技术监督方等等,很多项目里除了业主单位的结构很复杂以外,还有一些其他单位也会牵涉进来,如项目监理公司、业主的行业主管机构等。项目经理需要了解每个方面的人对这个项目的看法和期望是什么。事先了解各个方面的看法和期望,可以让你在做项目碰到问题的时候,就每件事情分析哪些人会在什么方面支持你,哪些人会出于什么目的反对你,从而提前准备联合朋友去对抗敌人,让事情向你所希望的方向发展。没有永远的朋友,也没有永远的敌人,只有一致的利益,这句话作为项目经理是一定要记住的; 3.基本了解了客户的情况后,下面的事情就是了解自己公司各方面对这个项目的看法。首先是高层领导是否重视,这个决定了你在需要资源的时候,公司是否会根据你的要求提供最有力的支持。领导口头肯定是说支持的,你需要做的是了解公司对这个项目的实际期望,是想把项目越做越大还是想赚钱?是想做样板工程还是干脆想敷衍了事,公司领导对项目的态度决定了你做这个项目的战略,而这个战略方针将对你做项目计划产生直接的影响; 4.在做整体项目计划前,还要大致计算一下你手上的资源。首先是时间,现在市场竞争激烈,往往很多项目要求在几乎不可能的时间范围里完成。对于这一点,你在做项目的风险控制计划的时候要充分考虑。其次是人员,根据项目预算和已往经验,大致计算一下未来的项目小组有多少种角色,每个角色目前公司是否有人,是否能完全归这个项目使用,是否需要另外招聘一些人员,招聘的准备工作要尽早启动。最后就是一些设备的准备,项目所需大件关键设备要尽早预定,以后不管发生设备等人还是人等设备的情况,浪费的都是你的时间; 5.现在是做项目说明书的时候了。一份好的项目说明书不仅将要做的事情描述得很清楚(主要是讲做什么,而不是说怎么做),而且把如何检查也说明得很透彻。也就是说它不仅说明白了要做哪些事情,也让客户的业务人员(一般不懂技术)知道项目做成什么样就算完成了。简单地说,项目说明书描述项目做哪些事情和每件事情做到什么程度以及如何检查每一个结果。 6. 是到做总体计划的时间了吗?不,你现在已经知道了客户的目标和你手上的资源,那么做计划以前,你还需要和你的经理和客户充分沟通资源的问题。因为很多资源是还不明确的,你需要写一份报告,详细分析这个项目的风险以及对资源的需求情况。如果一些问题不能得到解决的话,将发生什么样的后果。如果资源不够,就要高层改变策略,增加对这个项目的投入。甚至在条件许可的情况下,有些公司会放弃这个项目。总之,没有人能完成一个不可能完成的任务,如果项目经理不能尽早发现风险,那么就只能去当烈士了。 7. 明白了要做哪些事情和你手上的筹码以及你做这个项目的总体策略,现在是成立项目小组的时候了。很多项目经理都没有自己选择组员的权利,那么,就尽量发挥你的影响力去寻找那些你想要的人吧。成员的组成根据项目不同,相差较大,很难有什么具体要求,但是,一定要有精通客户业务的人,很多小项目里,这个人就是项目经理本人,大项目里会配备行业专家(Industry expert),这样和客户沟通起来才不会鸡同鸭讲,双方才可以相互理解。我经常看到的情况是我们的技术人员和客户交谈时满口的专业术语,结果搞得客户一头雾水,反过来,他还指责客户不懂技术。其实,明白自己想做什么的客户已经是很好的客户了,不知道自己要做什么,更不懂怎么做还要指手画脚的客户到处存在,但是要明白,是客户选择了你,而不是你选择了客户,有了客户你才有工资拿,心平气和一点吧; 8.现在你要面对三群人:你的领导、你的组员和你的客户,和这些人沟通,让他们知道你打算怎么做,什么时候要他们做什么准备这些事情将是你的主要工作。既然沟通这么重要,那些事先定义一下沟通的原则也是一件很要紧的事情。很多沟通原则都是潜规则,如果你在一个部门时间做长了,对这些规则的运用觉得是一件理所应当的事情,但是,你现在面对的是多个部门甚至多个单位,不把沟通规则说清楚,你以后就会吃亏。下面的东西看起来无聊,其实还是很管用的:第一个是规定信息的流动方式和介质,是推还是拉。推的意思就是项目经理将主动发布信息,不管通过电话、邮件还是书面方式,保证将信息传达到每个人。这种情况适合小项目,人少;拉的意思就是项目经理就是一个类似web服务器,你自己需要什么信息就去问他。当然,没有项目经理把自己搞得那么累,他会用发布信息到公共介质的方式公布信息,简单的是白板,复杂一点的是项目的公共信息交互区,潜规则就是我发了你没去看就不要说我没告诉你。说这些看似很无聊,其实里面牵涉信息传达不完全的责任问题。当然,这些都是指一般的方式,而且不要绝对化,一般情况下,主动沟通和被动访问是同时存在的,尤其是对领导,项目经理更加应该主动去和领导沟通。第二个问题就是文档问题,很多人怕写文档,但是项目经理一定要牢记“好记性不如烂笔头”的道理。有理有时候为什么会说不清呢?就是因为没有证据。所以项目经理开始就要和客户说清楚有些文档是必须签字的,比如项目经理的项目日志,每个星期至少让客户签字,另外所有达成共识的东西,比如会议纪要,甚至领导的讲话记录,都要写成文档,双方签字,这样以后扯皮的时候,就能做到有据可查。记住:说了的就和没说一样,只有写下来大家签字后才算真正发生了的。还有一些问题,比如你提交的报告,给领导(包括本方领导和客户领导)做一个选择题,结果领导压住不批,让你无所适从,结果拖延了进度。这时候,你可以等,但是注意要留记录,标明是谁的责任;另外,如果你在开始阶段就和领导商定:如果批示提交三天后没有得到领导答复就算对方同意,这样你就会主动很多。再比如不同事件的审批流程问题:什么等级的事情记录在项目日志里、什么等级的事情要双方项目经理专门签署备忘录、什么等级的事情要双方领导出面签署合同附件等等。事先想得越周到,以后的工作就越主动。 9. 好了,做了很多前期工作,定义了一些游戏规则,现在是坐下来做计划的时候了。这一节,任意找一本项目管理的书都会说得比我好,所以我就少写一点,说一些自己的体会就是了。首先是找几个关键组员,比如客户业务专家、系统分析员等等,做一下项目模块划分工作。项目分成几块去做,每一块完成什么,模块之间的信息如何交换等等。需求定义的是做什么的问题,而这里说的是怎么做的问题。这里要强调一点:完成一个目标有很多种方式,你要选一种你最熟悉的,而不是看上去最完美的,这个思路会让你的项目减少很多风险。有时候客户会被某种新技术打动,坚持要你采用那种新技术,你就应该告诉他:你选我做这个项目,就应该容许我采用自己最喜欢的方式做事情,新技术之所以有诱惑力,就是因为吃亏的人还不多,我不希望你成为第一批受害者。采用一个计划会让你的工作更加明确,比如用微软的Project软件,你填写完表格以后,就可以知道这个项目有多少件事情要做,每件事情需要什么资源,他们之间的前后关系如何,消耗的时间有多长,完成后有什么标志等。所有的结果最后用一个叫做甘特图的形式表现出来。你做完这个表以后会惊奇地发现,甘特图上项目的结束时间会远远落后于你的计划结束时间(签合同的人永远不会先征求你的意见的)。当然,学过项目管理的人会大谈什么WBS、优化路径之类的东西,但是我的经验是你再优化也不可能把这些东西安排到计划的时间结束。如果你没碰到这个问题,在我恭喜你挑了一个轻松活之前,请你再去确认你是否罗列了所有要做的事情和正确评估了他们所需要的时间。这时候,你就要考虑牺牲一些任务的时间(也意味着质量)了。按照什么标准牺牲?这个项目的战略!我们在第三节提到过的战略。我的经验是如果你什么都赶进度,其结果可能就是十件事情你一件也没做好,想想多么失败啊。所以,把资源投到你熟悉和有把握的事情上,最后的结果是十件事情,你有三件做成了精品,三件完成,还有四件因为某些原因延误,成绩单是否靓丽了很多呢?战略决定优先级,而正确排列事情的优先级是一个项目经理能力的主要体现。 好,现在项目已经完成了前期工作,了解了项目的目标、搞清楚了手上的资源,制定了项目的策略,然后编制了项目的整体计划,项目进入实施阶段。进入这个阶段反而是项目经理比较空闲的时候,不像前期的时候项目经理要象记者一样到处和不同的人接触,搞清楚他们在说什么,努力猜测他们在想什么和他们的真正目的,那才是最累人的事情。当然,小项目的项目经理往往自己也是一个资源,要做很多事情,这时候反而比谁都苦。项目经理这段时间的主要工作是保持和客户领导以及自己领导的沟通。和客户领导沟通时特别要注意,除非你需要对方给你支持,那么你才需要讲得具体一点,否则,告诉他一切正常就可以了,而且态度要积极一些,千万不要说一些领导不懂的细节,比如:“王局长,最近项目进度还算正常,就是JVM经常发生一些内存泄漏的情况…” 10月21日 SQLServer和Oracle常用函数对比数学函数 1.绝对值 S:select abs(-1) value O:select abs(-1) value from dual 2.取整(大) S:select ceiling(-1.001) value O:select ceil(-1.001) value from dual 3.取整(小) S:select floor(-1.001) value O:select floor(-1.001) value from dual 4.取整(截取) S:select cast(-1.002 as int) value O:select trunc(-1.002) value from dual 5.四舍五入 S:select round(1.23456,4) value 1.23460 O:select round(1.23456,4) value from dual 1.2346 6.e为底的幂 S:select Exp(1) value 2.7182818284590451 O:select Exp(1) value from dual 2.71828182 7.取e为底的对数 S:select log(2.7182818284590451) value 1 O:select ln(2.7182818284590451) value from dual; 1 8.取10为底对数 S:select log10(10) value 1 O:select log(10,10) value from dual; 1 9.取平方 S:select SQUARE(4) value 16 O:select power(4,2) value from dual 16 10.取平方根 S:select SQRT(4) value 2 O:select SQRT(4) value from dual 2 11.求任意数为底的幂 S:select power(3,4) value 81 O:select power(3,4) value from dual 81 12.取随机数 S:select rand() value O:select sys.dbms_random.value(0,1) value from dual; 13.取符号 S:select sign(-8) value -1 O:select sign(-8) value from dual -1 14.圆周率 S:SELECT PI() value 3.1415926535897931 O:不知道 15.sin,cos,tan 参数都以弧度为单位 例如:select sin(PI()/2) value 得到1(SQLServer) 16.Asin,Acos,Atan,Atan2 返回弧度 17.弧度角度互换(SQLServer,Oracle不知道) DEGREES:弧度-〉角度 RADIANS:角度-〉弧度 数值间比较 18. 求集合最大值 S:select max(value) value from (select 1 value union select -2 value union select 4 value union select 3 value)a O:select greatest(1,-2,4,3) value from dual 19. 求集合最小值 S:select min(value) value from (select 1 value union select -2 value union select 4 value union select 3 value)a O:select least(1,-2,4,3) value from dual 20.如何处理null值(F2中的null以10代替) S:select F1,IsNull(F2,10) value from Tbl O:select F1,nvl(F2,10) value from Tbl 21.求字符序号 S:select ascii('a') value O:select ascii('a') value from dual 22.从序 号求字符 S:select char(97) value O:select chr(97) value from dual 23.连接 S:select '11'+'22'+'33' value O:select CONCAT('11','22') 33 value from dual 23.子串位置 --返回3 S:select CHARINDEX('s','sdsq',2) value O:select INSTR('sdsq','s',2) value from dual 23.模糊子串的位置 --返回2,参数去掉中间%则返回7 S:select patindex('%d%q%','sdsfasdqe') value O:oracle没发现,但是instr可以通过第四个参数控制出现次数 select INSTR('sdsfasdqe','sd',1,2) value from dual 返回6 24.求子串 S:select substring('abcd',2,2) value O:select substr('abcd',2,2) value from dual 25.子串代替 返回aijklmnef S:SELECT STUFF('abcdef', 2, 3, 'ijklmn') value O:SELECT Replace('abcdef', 'bcd', 'ijklmn') value from dual 26.子串全部替换 S:没发现 O:select Translate('fasdbfasegas','fa','我' ) value from dual 27.长度 S:len,datalength O:length 28.大小写转换 lower,upper 29.单词首字母大写 S:没发现 O:select INITCAP('abcd dsaf df') value from dual 30.左补空格(LPAD的第一个参数为空格则同space函数) S:select space(10)+'abcd' value O:select LPAD('abcd',14) value from dual 31.右补空格(RPAD的第一个参数为空格则同space函数) S:select 'abcd'+space(10) value O:select RPAD('abcd',14) value from dual 32.删除空格 S:ltrim,rtrim O:ltrim,rtrim,trim 33. 重复字符串 S:select REPLICATE('abcd',2) value O:没发现 34.发音相似性比较(这两个单词返回值一样,发音相同) S:SELECT SOUNDEX ('Smith'), SOUNDEX ('Smythe') O:SELECT SOUNDEX ('Smith'), SOUNDEX ('Smythe') from dual SQLServer中用SELECT DIFFERENCE('Smithers', 'Smythers') 比较soundex的差 返回0-4,4为同音,1最高 日期函数 35.系统时间 S:select getdate() value O:select sysdate value from dual 36.前后几日 直接与整数相加减 37.求日期 S:select convert(char(10),getdate(),20) value O:select trunc(sysdate) value from dual select to_char(sysdate,'yyyy-mm-dd') value from dual 38.求时间 S:select convert(char(8),getdate(),108) value O:select to_char(sysdate,'hh24:mm:ss') value from dual 39.取日期时间的其他部分 S:DATEPART 和 DATENAME 函数 (第一个参数决定) O:to_char函数 第二个参数决定 参数---------------------------------下表需要补充 year yy, yyyy quarter qq, q (季度) month mm, m (m O无效) dayofyear dy, y (O表星期) day dd, d (d O无效) & lt; BR> week wk, ww (wk O无效) weekday dw (O不清楚) Hour hh,hh12,hh24 (hh12,hh24 S无效) minute mi, n (n O无效) second ss, s (s O无效) millisecond ms (O无效) ---------------------------------------------- 40.当月最后一天 S:不知道 O:select LAST_DAY(sysdate) value from dual 41.本星期的某一天(比如星期日) S:不知道 O:SELECT Next_day(sysdate,7) vaule FROM DUAL; 42.字符串转时间 S:可以直接转或者select cast('2004-09-08'as datetime) value O:SELECT To_date('2004-01-05 22:09:38','yyyy-mm-dd hh24-mi-ss') vaule FROM DUAL; 43.求两日期某一部分的差(比如秒) S:select datediff(ss,getdate(),getdate()+12.3) value O:直接用两个日期相减(比如d1-d2=12.3) SELECT (d1-d2)*24*60*60 vaule FROM DUAL; 44.根据差值求新的日期(比如分钟) S:select dateadd(mi,8,getdate()) value O:SELECT sysdate+8/60/24 vaule FROM DUAL; 45.求不同时区时间 S:不知道 O:SELECT New_time(sysdate,'ydt','gmt' ) vaule FROM DUAL; -----时区参数,北京在东8区应该是Ydt------- AST ADT 大西洋标准时间 BST BDT 白令海标准时间 CST CDT 中部标准时间 EST EDT 东部标准时间 GMT 格林尼治标准时间 HST HDT 阿拉斯加?夏威夷标准时间 MST MDT 山区标准时间 NST 纽芬兰标准时间 PST PDT 太平洋标准时间 YST YDT YUKON标准时间 10月16日 Rose建模初步(转载)
10月15日 PostMessage 和 SendMessage的区别对话版A君问:PostMessage 和 SendMessage什么区别 B君答: PostMessage只负责将消息放到消息队列中,不确定何时及是否处理 SendMessage要等到受到消息处理的返回码(DWord类型)后才继续 PostMessage执行后马上返回 SendMessage必须等到消息被处理后才会返回。 A君:不明白 B君:SendMessage看成ShowModal,PostMessage看成Show A君:明白 10月14日 更改系统菜单procedure TForm1.FormCreate(Sender: TObject);
begin
Application.OnMessage := OnAppMessage;
//定义自己的消息处理过程
// 定义一分割线
AppendMenu(GetSystemMenu(Form1.Handle,FALSE), MF_SEPARATOR, 0, '');
// 定义'提交'子菜单
AppendMenu(GetSystemMenu(Form1.Handle, FALSE), MF_POPUP,pm1.Handle,'换皮肤');
end;
procedure TForm1.OnAppMessage(var Msg: TMsg; var Handled: Boolean);
begin
if (Msg.message = WM_COMMAND) AND (Msg.wParam < WM_USER) then
begin
Case Msg.wParam of
// N1.Command = 1
1:ShowMessage('范例一 Command:'+ IntToStr(t1.Command));
// N1.Command = 2
2:ShowMessage('范例二 Command:'+ IntToStr(t2.Command));
// N1.Command = 3
3:ShowMessage('范例三 Command:'+ IntToStr(t3.Command));
end;
Handled := True;
end;
end;
///////改动Form的系统菜单
///Form1.Handle---〉application.Handle; WM_COMMAND----> WM_SYSCOMMAND则增加了任务栏上的菜单 10月7日 建立“适当”的索引一、因情制宜,建立“适当”的索引 建立“适当”的索引是实现查询优化的首要前提。 索引(index)是除表之外另一重要的、用户定义的存储在物理介质上的数据结构。当根据索引码的值搜索数据时,索引提供了对数据的快速访问。事实上,没有索引,数据库也能根据SELECT语句成功地检索到结果,但随着表变得越来越大,使用“适当”的索引的效果就越来越明显。注意,在这句话中,我们用了“适当”这个词,这是因为,如果使用索引时不认真考虑其实现过程,索引既可以提高也会破坏数据库的工作性能。 (一)深入浅出理解索引结构 实际上,您可以把索引理解为一种特殊的目录。微软的SQL SERVER提供了两种索引:聚集索引(clustered index,也称聚类索引、簇集索引)和非聚集索引(nonclustered index,也称非聚类索引、非簇集索引)。下面,我们举例来说明一下聚集索引和非聚集索引的区别: 其实,我们的汉语字典的正文本身就是一个聚集索引。比如,我们要查“安”字,就会很自然地翻开字典的前几页,因为“安”的拼音是“an”,而按照拼音排序汉字的字典是以英文字母“a”开头并以“z”结尾的,那么“安”字就自然地排在字典的前部。如果您翻完了所有以“a”开头的部分仍然找不到这个字,那么就说明您的字典中没有这个字;同样的,如果查“张”字,那您也会将您的字典翻到最后部分,因为“张”的拼音是“zhang”。也就是说,字典的正文部分本身就是一个目录,您不需要再去查其他目录来找到您需要找的内容。 我们把这种正文内容本身就是一种按照一定规则排列的目录称为“聚集索引”。 如果您认识某个字,您可以快速地从自动中查到这个字。但您也可能会遇到您不认识的字,不知道它的发音,这时候,您就不能按照刚才的方法找到您要查的字,而需要去根据“偏旁部首”查到您要找的字,然后根据这个字后的页码直接翻到某页来找到您要找的字。但您结合“部首目录”和“检字表”而查到的字的排序并不是真正的正文的排序方法,比如您查“张”字,我们可以看到在查部首之后的检字表中“张”的页码是672页,检字表中“张”的上面是“驰”字,但页码却是63页,“张”的下面是“弩”字,页面是390页。很显然,这些字并不是真正的分别位于“张”字的上下方,现在您看到的连续的“驰、张、弩”三字实际上就是他们在非聚集索引中的排序,是字典正文中的字在非聚集索引中的映射。我们可以通过这种方式来找到您所需要的字,但它需要两个过程,先找到目录中的结果,然后再翻到您所需要的页码。 我们把这种目录纯粹是目录,正文纯粹是正文的排序方式称为“非聚集索引”。 通过以上例子,我们可以理解到什么是“聚集索引”和“非聚集索引”。 进一步引申一下,我们可以很容易的理解:每个表只能有一个聚集索引,因为目录只能按照一种方法进行排序。 (二)何时使用聚集索引或非聚集索引 下面的表总结了何时使用聚集索引或非聚集索引(很重要)。 动作描述 使用聚集索引 使用非聚集索引 列经常被分组排序 应 应 返回某范围内的数据 应 不应 一个或极少不同值 不应 不应 小数目的不同值 应 不应 大数目的不同值 不应 应 频繁更新的列 不应 应 外键列 应 应 主键列 应 应 频繁修改索引列 不应 应 事实上,我们可以通过前面聚集索引和非聚集索引的定义的例子来理解上表。如:返回某范围内的数据一项。比如您的某个表有一个时间列,恰好您把聚合索引建立在了该列,这时您查询2004年1月1日至2004年10月1日之间的全部数据时,这个速度就将是很快的,因为您的这本字典正文是按日期进行排序的,聚类索引只需要找到要检索的所有数据中的开头和结尾数据即可;而不像非聚集索引,必须先查到目录中查到每一项数据对应的页码,然后再根据页码查到具体内容。 (三)结合实际,谈索引使用的误区 理论的目的是应用。虽然我们刚才列出了何时应使用聚集索引或非聚集索引,但在实践中以上规则却很容易被忽视或不能根据实际情况进行综合分析。下面我们将根据在实践中遇到的实际问题来谈一下索引使用的误区,以便于大家掌握索引建立的方法。 1、主键就是聚集索引 这种想法笔者认为是极端错误的,是对聚集索引的一种浪费。虽然SQL SERVER默认是在主键上建立聚集索引的。 通常,我们会在每个表中都建立一个ID列,以区分每条数据,并且这个ID列是自动增大的,步长一般为1。我们的这个办公自动化的实例中的列Gid就是如此。此时,如果我们将这个列设为主键,SQL SERVER会将此列默认为聚集索引。这样做有好处,就是可以让您的数据在数据库中按照ID进行物理排序,但笔者认为这样做意义不大。 显而易见,聚集索引的优势是很明显的,而每个表中只能有一个聚集索引的规则,这使得聚集索引变得更加珍贵。 从我们前面谈到的聚集索引的定义我们可以看出,使用聚集索引的最大好处就是能够根据查询要求,迅速缩小查询范围,避免全表扫描。在实际应用中,因为ID号是自动生成的,我们并不知道每条记录的ID号,所以我们很难在实践中用ID号来进行查询。这就使让ID号这个主键作为聚集索引成为一种资源浪费。其次,让每个ID号都不同的字段作为聚集索引也不符合“大数目的不同值情况下不应建立聚合索引”规则;当然,这种情况只是针对用户经常修改记录内容,特别是索引项的时候会负作用,但对于查询速度并没有影响。 在办公自动化系统中,无论是系统首页显示的需要用户签收的文件、会议还是用户进行文件查询等任何情况下进行数据查询都离不开字段的是“日期”还有用户本身的“用户名”。 通常,办公自动化的首页会显示每个用户尚未签收的文件或会议。虽然我们的where语句可以仅仅限制当前用户尚未签收的情况,但如果您的系统已建立了很长时间,并且数据量很大,那么,每次每个用户打开首页的时候都进行一次全表扫描,这样做意义是不大的,绝大多数的用户1个月前的文件都已经浏览过了,这样做只能徒增数据库的开销而已。事实上,我们完全可以让用户打开系统首页时,数据库仅仅查询这个用户近3个月来未阅览的文件,通过“日期”这个字段来限制表扫描,提高查询速度。如果您的办公自动化系统已经建立的2年,那么您的首页显示速度理论上将是原来速度8倍,甚至更快。 在这里之所以提到“理论上”三字,是因为如果您的聚集索引还是盲目地建在ID这个主键上时,您的查询速度是没有这么高的,即使您在“日期”这个字段上建立的索引(非聚合索引)。下面我们就来看一下在1000万条数据量的情况下各种查询的速度表现(3个月内的数据为25万条): (1)仅在主键上建立聚集索引,并且不划分时间段: Select gid,fariqi,neibuyonghu,title from tgongwen 用时:128470毫秒(即:128秒) (2)在主键上建立聚集索引,在fariq上建立非聚集索引: select gid,fariqi,neibuyonghu,title from Tgongwen where fariqi> dateadd(day,-90,getdate()) 用时:53763毫秒(54秒) (3)将聚合索引建立在日期列(fariqi)上: select gid,fariqi,neibuyonghu,title from Tgongwen where fariqi> dateadd(day,-90,getdate()) 用时:2423毫秒(2秒) 虽然每条语句提取出来的都是25万条数据,各种情况的差异却是巨大的,特别是将聚集索引建立在日期列时的差异。事实上,如果您的数据库真的有1000万容量的话,把主键建立在ID列上,就像以上的第1、2种情况,在网页上的表现就是超时,根本就无法显示。这也是我摒弃ID列作为聚集索引的一个最重要的因素。 得出以上速度的方法是:在各个select语句前加:declare @d datetime set @d=getdate() 并在select语句后加: select [语句执行花费时间(毫秒)]=datediff(ms,@d,getdate()) 2、只要建立索引就能显著提高查询速度 事实上,我们可以发现上面的例子中,第2、3条语句完全相同,且建立索引的字段也相同;不同的仅是前者在fariqi字段上建立的是非聚合索引,后者在此字段上建立的是聚合索引,但查询速度却有着天壤之别。所以,并非是在任何字段上简单地建立索引就能提高查询速度。 从建表的语句中,我们可以看到这个有着1000万数据的表中fariqi字段有5003个不同记录。在此字段上建立聚合索引是再合适不过了。在现实中,我们每天都会发几个文件,这几个文件的发文日期就相同,这完全符合建立聚集索引要求的:“既不能绝大多数都相同,又不能只有极少数相同”的规则。由此看来,我们建立“适当”的聚合索引对于我们提高查询速度是非常重要的。 3、把所有需要提高查询速度的字段都加进聚集索引,以提高查询速度 上面已经谈到:在进行数据查询时都离不开字段的是“日期”还有用户本身的“用户名”。既然这两个字段都是如此的重要,我们可以把他们合并起来,建立一个复合索引(compound index)。 很多人认为只要把任何字段加进聚集索引,就能提高查询速度,也有人感到迷惑:如果把复合的聚集索引字段分开查询,那么查询速度会减慢吗?带着这个问题,我们来看一下以下的查询速度(结果集都是25万条数据):(日期列fariqi首先排在复合聚集索引的起始列,用户名neibuyonghu排在后列) (1)select gid,fariqi,neibuyonghu,title from Tgongwen where fariqi>'2004-5-5' 查询速度:2513毫秒 (2)select gid,fariqi,neibuyonghu,title from Tgongwen where fariqi>'2004-5-5' and neibuyonghu='办公室' 查询速度:2516毫秒 (3)select gid,fariqi,neibuyonghu,title from Tgongwen where neibuyonghu='办公室' 查询速度:60280毫秒 从以上试验中,我们可以看到如果仅用聚集索引的起始列作为查询条件和同时用到复合聚集索引的全部列的查询速度是几乎一样的,甚至比用上全部的复合索引列还要略快(在查询结果集数目一样的情况下);而如果仅用复合聚集索引的非起始列作为查询条件的话,这个索引是不起任何作用的。当然,语句1、2的查询速度一样是因为查询的条目数一样,如果复合索引的所有列都用上,而且查询结果少的话,这样就会形成“索引覆盖”,因而性能可以达到最优。同时,请记住:无论您是否经常使用聚合索引的其他列,但其前导列一定要是使用最频繁的列。 (四)其他书上没有的索引使用经验总结 1、用聚合索引比用不是聚合索引的主键速度快 下面是实例语句:(都是提取25万条数据) select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi='2004-9-16' 使用时间:3326毫秒 select gid,fariqi,neibuyonghu,reader,title from Tgongwen where gid<=250000 使用时间:4470毫秒 这里,用聚合索引比用不是聚合索引的主键速度快了近1/4。 2、用聚合索引比用一般的主键作order by时速度快,特别是在小数据量情况下 select gid,fariqi,neibuyonghu,reader,title from Tgongwen order by fariqi 用时:12936 select gid,fariqi,neibuyonghu,reader,title from Tgongwen order by gid 用时:18843 这里,用聚合索引比用一般的主键作order by时,速度快了3/10。事实上,如果数据量很小的话,用聚集索引作为排序列要比使用非聚集索引速度快得明显的多;而数据量如果很大的话,如10万以上,则二者的速度差别不明显。 3、使用聚合索引内的时间段,搜索时间会按数据占整个数据表的百分比成比例减少,而无论聚合索引使用了多少个 select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi>'2004-1-1' 用时:6343毫秒(提取100万条) select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi>'2004-6-6' 用时:3170毫秒(提取50万条) select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi='2004-9-16' 用时:3326毫秒(和上句的结果一模一样。如果采集的数量一样,那么用大于号和等于号是一样的) select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi>'2004-1-1' and fariqi<'2004-6-6' 用时:3280毫秒 4 、日期列不会因为有分秒的输入而减慢查询速度 下面的例子中,共有100万条数据,2004年1月1日以后的数据有50万条,但只有两个不同的日期,日期精确到日;之前有数据50万条,有5000个不同的日期,日期精确到秒。 select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi>'2004-1-1' order by fariqi 用时:6390毫秒 select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi<'2004-1-1' order by fariqi 用时:6453毫秒 (五)其他注意事项 “水可载舟,亦可覆舟”,索引也一样。索引有助于提高检索性能,但过多或不当的索引也会导致系统低效。因为用户在表中每加进一个索引,数据库就要做更多的工作。过多的索引甚至会导致索引碎片。 所以说,我们要建立一个“适当”的索引体系,特别是对聚合索引的创建,更应精益求精,以使您的数据库能得到高性能的发挥。 当然,在实践中,作为一个尽职的数据库管理员,您还要多测试一些方案,找出哪种方案效率最高、最为有效。 二、改善SQL语句 很多人不知道SQL语句在SQL SERVER中是如何执行的,他们担心自己所写的SQL语句会被SQL SERVER误解。比如: select * from table1 where name='zhangsan' and tID > 10000 和执行: select * from table1 where tID > 10000 and name='zhangsan' 一些人不知道以上两条语句的执行效率是否一样,因为如果简单的从语句先后上看,这两个语句的确是不一样,如果tID是一个聚合索引,那么后一句仅仅从表的10000条以后的记录中查找就行了;而前一句则要先从全表中查找看有几个name='zhangsan'的,而后再根据限制条件条件tID>10000来提出查询结果。 事实上,这样的担心是不必要的。SQL SERVER中有一个“查询分析优化器”,它可以计算出where子句中的搜索条件并确定哪个索引能缩小表扫描的搜索空间,也就是说,它能实现自动优化。 虽然查询优化器可以根据where子句自动的进行查询优化,但大家仍然有必要了解一下“查询优化器”的工作原理,如非这样,有时查询优化器就会不按照您的本意进行快速查询。 在查询分析阶段,查询优化器查看查询的每个阶段并决定限制需要扫描的数据量是否有用。如果一个阶段可以被用作一个扫描参数(SARG),那么就称之为可优化的,并且可以利用索引快速获得所需数据。 SARG的定义:用于限制搜索的一个操作,因为它通常是指一个特定的匹配,一个值得范围内的匹配或者两个以上条件的AND连接。形式如下: 列名 操作符 <常数 或 变量> 或 <常数 或 变量> 操作符列名 列名可以出现在操作符的一边,而常数或变量出现在操作符的另一边。如: Name=’张三’ 价格>5000 5000<价格 Name=’张三’ and 价格>5000 如果一个表达式不能满足SARG的形式,那它就无法限制搜索的范围了,也就是SQL SERVER必须对每一行都判断它是否满足WHERE子句中的所有条件。所以一个索引对于不满足SARG形式的表达式来说是无用的。 介绍完SARG后,我们来总结一下使用SARG以及在实践中遇到的和某些资料上结论不同的经验: 1、Like语句是否属于SARG取决于所使用的通配符的类型 如:name like ‘张%’ ,这就属于SARG 而:name like ‘%张’ ,就不属于SARG。 原因是通配符%在字符串的开通使得索引无法使用。 2、or 会引起全表扫描 Name=’张三’ and 价格>5000 符号SARG,而:Name=’张三’ or 价格>5000 则不符合SARG。使用or会引起全表扫描。 3、非操作符、函数引起的不满足SARG形式的语句 不满足SARG形式的语句最典型的情况就是包括非操作符的语句,如:NOT、!=、<>、!<、!>、NOT EXISTS、NOT IN、NOT LIKE等,另外还有函数。下面就是几个不满足SARG形式的例子: ABS(价格)<5000 Name like ‘%三’ 有些表达式,如: WHERE 价格*2>5000 SQL SERVER也会认为是SARG,SQL SERVER会将此式转化为: WHERE 价格>2500/2 但我们不推荐这样使用,因为有时SQL SERVER不能保证这种转化与原始表达式是完全等价的。 4、IN 的作用相当与OR 语句: Select * from table1 where tid in (2,3) 和 Select * from table1 where tid=2 or tid=3 是一样的,都会引起全表扫描,如果tid上有索引,其索引也会失效。 5、尽量少用NOT 6、exists 和 in 的执行效率是一样的 很多资料上都显示说,exists要比in的执行效率要高,同时应尽可能的用not exists来代替not in。但事实上,我试验了一下,发现二者无论是前面带不带not,二者之间的执行效率都是一样的。因为涉及子查询,我们试验这次用SQL SERVER自带的pubs数据库。运行前我们可以把SQL SERVER的statistics I/O状态打开。 (1)select title,price from titles where title_id in (select title_id from sales where qty>30) 该句的执行结果为: 表 'sales'。扫描计数 18,逻辑读 56 次,物理读 0 次,预读 0 次。 表 'titles'。扫描计数 1,逻辑读 2 次,物理读 0 次,预读 0 次。 (2)select title,price from titles where exists (select * from sales where sales.title_id=titles.title_id and qty>30) 第二句的执行结果为: 表 'sales'。扫描计数 18,逻辑读 56 次,物理读 0 次,预读 0 次。 表 'titles'。扫描计数 1,逻辑读 2 次,物理读 0 次,预读 0 次。 我们从此可以看到用exists和用in的执行效率是一样的。 7、用函数charindex()和前面加通配符%的LIKE执行效率一样 前面,我们谈到,如果在LIKE前面加上通配符%,那么将会引起全表扫描,所以其执行效率是低下的。但有的资料介绍说,用函数charindex()来代替LIKE速度会有大的提升,经我试验,发现这种说明也是错误的: select gid,title,fariqi,reader from tgongwen where charindex('刑侦支队',reader)>0 and fariqi>'2004-5-5' 用时:7秒,另外:扫描计数 4,逻辑读 7155 次,物理读 0 次,预读 0 次。 select gid,title,fariqi,reader from tgongwen where reader like '%' + '刑侦支队' + '%' and fariqi>'2004-5-5' 用时:7秒,另外:扫描计数 4,逻辑读 7155 次,物理读 0 次,预读 0 次。 8、union并不绝对比or的执行效率高 我们前面已经谈到了在where子句中使用or会引起全表扫描,一般的,我所见过的资料都是推荐这里用union来代替or。事实证明,这种说法对于大部分都是适用的。 select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi='2004-9-16' or gid>9990000 用时:68秒。扫描计数 1,逻辑读 404008 次,物理读 283 次,预读 392163 次。 select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi='2004-9-16' union select gid,fariqi,neibuyonghu,reader,title from Tgongwen where gid>9990000 用时:9秒。扫描计数 8,逻辑读 67489 次,物理读 216 次,预读 7499 次。 看来,用union在通常情况下比用or的效率要高的多。 但经过试验,笔者发现如果or两边的查询列是一样的话,那么用union则反倒和用or的执行速度差很多,虽然这里union扫描的是索引,而or扫描的是全表。 select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi='2004-9-16' or fariqi='2004-2-5' 用时:6423毫秒。扫描计数 2,逻辑读 14726 次,物理读 1 次,预读 7176 次。 select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi='2004-9-16' union select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi='2004-2-5' 用时:11640毫秒。扫描计数 8,逻辑读 14806 次,物理读 108 次,预读 1144 次。 9、字段提取要按照“需多少、提多少”的原则,避免“select *” 我们来做一个试验: select top 10000 gid,fariqi,reader,title from tgongwen order by gid desc 用时:4673毫秒 select top 10000 gid,fariqi,title from tgongwen order by gid desc 用时:1376毫秒 select top 10000 gid,fariqi from tgongwen order by gid desc 用时:80毫秒 由此看来,我们每少提取一个字段,数据的提取速度就会有相应的提升。提升的速度还要看您舍弃的字段的大小来判断。 10、count(*)不比count(字段)慢 某些资料上说:用*会统计所有列,显然要比一个世界的列名效率低。这种说法其实是没有根据的。我们来看: select count(*) from Tgongwen 用时:1500毫秒 select count(gid) from Tgongwen 用时:1483毫秒 select count(fariqi) from Tgongwen 用时:3140毫秒 select count(title) from Tgongwen 用时:52050毫秒 从以上可以看出,如果用count(*)和用count(主键)的速度是相当的,而count(*)却比其他任何除主键以外的字段汇总速度要快,而且字段越长,汇总的速度就越慢。我想,如果用count(*), SQL SERVER可能会自动查找最小字段来汇总的。当然,如果您直接写count(主键)将会来的更直接些。 11、order by按聚集索引列排序效率最高 我们来看:(gid是主键,fariqi是聚合索引列) select top 10000 gid,fariqi,reader,title from tgongwen 用时:196 毫秒。 扫描计数 1,逻辑读 289 次,物理读 1 次,预读 1527 次。 select top 10000 gid,fariqi,reader,title from tgongwen order by gid asc 用时:4720毫秒。 扫描计数 1,逻辑读 41956 次,物理读 0 次,预读 1287 次。 select top 10000 gid,fariqi,reader,title from tgongwen order by gid desc 用时:4736毫秒。 扫描计数 1,逻辑读 55350 次,物理读 10 次,预读 775 次。 select top 10000 gid,fariqi,reader,title from tgongwen order by fariqi asc 用时:173毫秒。 扫描计数 1,逻辑读 290 次,物理读 0 次,预读 0 次。 select top 10000 gid,fariqi,reader,title from tgongwen order by fariqi desc 用时:156毫秒。 扫描计数 1,逻辑读 289 次,物理读 0 次,预读 0 次。 从以上我们可以看出,不排序的速度以及逻辑读次数都是和“order by 聚集索引列” 的速度是相当的,但这些都比“order by 非聚集索引列”的查询速度是快得多的。 同时,按照某个字段进行排序的时候,无论是正序还是倒序,速度是基本相当的。 12、高效的TOP 事实上,在查询和提取超大容量的数据集时,影响数据库响应时间的最大因素不是数据查找,而是物理的I/0操作。如: select top 10 * from ( select top 10000 gid,fariqi,title from tgongwen where neibuyonghu='办公室' order by gid desc) as a order by gid asc 这条语句,从理论上讲,整条语句的执行时间应该比子句的执行时间长,但事实相反。因为,子句执行后返回的是10000条记录,而整条语句仅返回10条语句,所以影响数据库响应时间最大的因素是物理I/O操作。而限制物理I/O操作此处的最有效方法之一就是使用TOP关键词了。TOP关键词是SQL SERVER中经过系统优化过的一个用来提取前几条或前几个百分比数据的词。经笔者在实践中的应用,发现TOP确实很好用,效率也很高。但这个词在另外一个大型数据库ORACLE中却没有,这不能说不是一个遗憾,虽然在ORACLE中可以用其他方法(如:rownumber)来解决。在以后的关于“实现千万级数据的分页显示存储过程”的讨论中,我们就将用到TOP这个关键词。 到此为止,我们上面讨论了如何实现从大容量的数据库中快速地查询出您所需要的数据方法。当然,我们介绍的这些方法都是“软”方法,在实践中,我们还要考虑各种“硬”因素,如:网络性能、服务器的性能、操作系统的性能,甚至网卡、交换机等。 三、实现小数据量和海量数据的通用分页显示存储过程 建立一个web 应用,分页浏览功能必不可少。这个问题是数据库处理中十分常见的问题。经典的数据分页方法是:ADO 纪录集分页法,也就是利用ADO自带的分页功能(利用游标)来实现分页。但这种分页方法仅适用于较小数据量的情形,因为游标本身有缺点:游标是存放在内存中,很费内存。游标一建立,就将相关的记录锁住,直到取消游标。游标提供了对特定集合中逐行扫描的手段,一般使用游标来逐行遍历数据,根据取出数据条件的不同进行不同的操作。而对于多表和大表中定义的游标(大的数据集合)循环很容易使程序进入一个漫长的等待甚至死机。 更重要的是,对于非常大的数据模型而言,分页检索时,如果按照传统的每次都加载整个数据源的方法是非常浪费资源的。现在流行的分页方法一般是检索页面大小的块区的数据,而非检索所有的数据,然后单步执行当前行。 最早较好地实现这种根据页面大小和页码来提取数据的方法大概就是“俄罗斯存储过程”。这个存储过程用了游标,由于游标的局限性,所以这个方法并没有得到大家的普遍认可。 后来,网上有人改造了此存储过程,下面的存储过程就是结合我们的办公自动化实例写的分页存储过程: CREATE procedure pagination1 (@pagesize int, --页面大小,如每页存储20条记录 @pageindex int --当前页码 ) as set nocount on begin declare @indextable table(id int identity(1,1),nid int) --定义表变量 declare @PageLowerBound int --定义此页的底码 declare @PageUpperBound int --定义此页的顶码 set @PageLowerBound=(@pageindex-1)*@pagesize set @PageUpperBound=@PageLowerBound+@pagesize set rowcount @PageUpperBound insert into @indextable(nid) select gid from TGongwen where fariqi >dateadd(day,-365,getdate()) order by fariqi desc select O.gid,O.mid,O.title,O.fadanwei,O.fariqi from TGongwen O,@indextable t where O.gid=t.nid and t.id>@PageLowerBound and t.id<=@PageUpperBound order by t.id end set nocount off 以上存储过程运用了SQL SERVER的最新技术――表变量。应该说这个存储过程也是一个非常优秀的分页存储过程。当然,在这个过程中,您也可以把其中的表变量写成临时表:CREATE TABLE #Temp。但很明显,在SQL SERVER中,用临时表是没有用表变量快的。所以笔者刚开始使用这个存储过程时,感觉非常的不错,速度也比原来的ADO的好。但后来,我又发现了比此方法更好的方法。 笔者曾在网上看到了一篇小短文《从数据表中取出第n条到第m条的记录的方法》,全文如下: 从publish 表中取出第 n 条到第 m 条的记录: SELECT TOP m-n+1 * FROM publish WHERE (id NOT IN (SELECT TOP n-1 id FROM publish)) id 为publish 表的关键字 我当时看到这篇文章的时候,真的是精神为之一振,觉得思路非常得好。等到后来,我在作办公自动化系统(ASP.NET+ C#+SQL SERVER)的时候,忽然想起了这篇文章,我想如果把这个语句改造一下,这就可能是一个非常好的分页存储过程。于是我就满网上找这篇文章,没想到,文章还没找到,却找到了一篇根据此语句写的一个分页存储过程,这个存储过程也是目前较为流行的一种分页存储过程,我很后悔没有争先把这段文字改造成存储过程: CREATE PROCEDURE pagination2 ( @SQL nVARCHAR(4000), --不带排序语句的SQL语句 @Page int, --页码 @RecsPerPage int, --每页容纳的记录数 @ID VARCHAR(255), --需要排序的不重复的ID号 @Sort VARCHAR(255) --排序字段及规则 ) AS DECLARE @Str nVARCHAR(4000) SET @Str='SELECT TOP '+CAST(@RecsPerPage AS VARCHAR(20))+' * FROM ('+@SQL+') T WHERE T.'+@ID+'NOT IN (SELECT TOP '+CAST((@RecsPerPage*(@Page-1)) AS VARCHAR(20))+' '+@ID+' FROM ('+@SQL+') T9 ORDER BY '+@Sort+') ORDER BY '+@Sort PRINT @Str EXEC sp_ExecuteSql @Str GO 其实,以上语句可以简化为: SELECT TOP 页大小 * FROM Table1 WHERE (ID NOT IN (SELECT TOP 页大小*页数 id FROM 表 ORDER BY id)) ORDER BY ID 但这个存储过程有一个致命的缺点,就是它含有NOT IN字样。虽然我可以把它改造为: SELECT TOP 页大小 * FROM Table1 WHERE not exists (select * from (select top (页大小*页数) * from table1 order by id) b where b.id=a.id ) order by id 即,用not exists来代替not in,但我们前面已经谈过了,二者的执行效率实际上是没有区别的。 既便如此,用TOP 结合NOT IN的这个方法还是比用游标要来得快一些。 虽然用not exists并不能挽救上个存储过程的效率,但使用SQL SERVER中的TOP关键字却是一个非常明智的选择。因为分页优化的最终目的就是避免产生过大的记录集,而我们在前面也已经提到了TOP的优势,通过TOP 即可实现对数据量的控制。 在分页算法中,影响我们查询速度的关键因素有两点:TOP和NOT IN。TOP可以提高我们的查询速度,而NOT IN会减慢我们的查询速度,所以要提高我们整个分页算法的速度,就要彻底改造NOT IN,同其他方法来替代它。 我们知道,几乎任何字段,我们都可以通过max(字段)或min(字段)来提取某个字段中的最大或最小值,所以如果这个字段不重复,那么就可以利用这些不重复的字段的max或min作为分水岭,使其成为分页算法中分开每页的参照物。在这里,我们可以用操作符“>”或“<”号来完成这个使命,使查询语句符合SARG形式。如: Select top 10 * from table1 where id>200 于是就有了如下分页方案: select top 页大小 * from table1 where id> (select max (id) from (select top ((页码-1)*页大小) id from table1 order by id) as T ) order by id 在选择即不重复值,又容易分辨大小的列时,我们通常会选择主键。下表列出了笔者用有着1000万数据的办公自动化系统中的表,在以GID(GID是主键,但并不是聚集索引。)为排序列、提取gid,fariqi,title字段,分别以第1、10、100、500、1000、1万、10万、25万、50万页为例,测试以上三种分页方案的执行速度:(单位:毫秒) 页 码 方案1 方案2 方案3 1 60 30 76 10 46 16 63 100 1076 720 130 500 540 12943 83 1000 17110 470 250 1万 24796 4500 140 10万 38326 42283 1553 25万 28140 128720 2330 50万 121686 127846 7168 从上表中,我们可以看出,三种存储过程在执行100页以下的分页命令时,都是可以信任的,速度都很好。但第一种方案在执行分页1000页以上后,速度就降了下来。第二种方案大约是在执行分页1万页以上后速度开始降了下来。而第三种方案却始终没有大的降势,后劲仍然很足。 在确定了第三种分页方案后,我们可以据此写一个存储过程。大家知道SQL SERVER的存储过程是事先编译好的SQL语句,它的执行效率要比通过WEB页面传来的SQL语句的执行效率要高。下面的存储过程不仅含有分页方案,还会根据页面传来的参数来确定是否进行数据总数统计。 -- 获取指定页的数据 CREATE PROCEDURE pagination3 @tblName varchar(255), -- 表名 @strGetFields varchar(1000) = '*', -- 需要返回的列 @fldName varchar(255)='', -- 排序的字段名 @PageSize int = 10, -- 页尺寸 @PageIndex int = 1, -- 页码 @doCount bit = 0, -- 返回记录总数, 非 0 值则返回 @OrderType bit = 0, -- 设置排序类型, 非 0 值则降序 @strWhere varchar(1500) = '' -- 查询条件 (注意: 不要加 where) AS declare @strSQL varchar(5000) -- 主语句 declare @strTmp varchar(110) -- 临时变量 declare @strOrder varchar(400) -- 排序类型 if @doCount != 0 begin if @strWhere !='' set @strSQL = "select count(*) as Total from [" + @tblName + "] where "+@strWhere else set @strSQL = "select count(*) as Total from [" + @tblName + "]" end --以上代码的意思是如果@doCount传递过来的不是0,就执行总数统计。以下的所有代码都是@doCount为0的情况 else begin if @OrderType != 0 begin set @strTmp = "<(select min" set @strOrder = " order by [" + @fldName +"] desc" --如果@OrderType不是0,就执行降序,这句很重要! end else begin set @strTmp = ">(select max" set @strOrder = " order by [" + @fldName +"] asc" end if @PageIndex = 1 begin if @strWhere != '' set @strSQL = "select top " + str(@PageSize) +" "+@strGetFields+ " from [" + @tblName + "] where " + @strWhere + " " + @strOrder else set @strSQL = "select top " + str(@PageSize) +" "+@strGetFields+ " from ["+ @tblName + "] "+ @strOrder --如果是第一页就执行以上代码,这样会加快执行速度 end else begin --以下代码赋予了@strSQL以真正执行的SQL代码 set @strSQL = "select top " + str(@PageSize) +" "+@strGetFields+ " from [" + @tblName + "] where [" + @fldName + "]" + @strTmp + "(["+ @fldName + "]) from (select top " + str((@PageIndex-1)*@PageSize) + " ["+ @fldName + "] from [" + @tblName + "]" + @strOrder + ") as tblTmp)"+ @strOrder if @strWhere != '' set @strSQL = "select top " + str(@PageSize) +" "+@strGetFields+ " from [" + @tblName + "] where [" + @fldName + "]" + @strTmp + "([" + @fldName + "]) from (select top " + str((@PageIndex-1)*@PageSize) + " [" + @fldName + "] from [" + @tblName + "] where " + @strWhere + " " + @strOrder + ") as tblTmp) and " + @strWhere + " " + @strOrder end end exec (@strSQL) GO 上面的这个存储过程是一个通用的存储过程,其注释已写在其中了。 在大数据量的情况下,特别是在查询最后几页的时候,查询时间一般不会超过9秒;而用其他存储过程,在实践中就会导致超时,所以这个存储过程非常适用于大容量数据库的查询。 笔者希望能够通过对以上存储过程的解析,能给大家带来一定的启示,并给工作带来一定的效率提升,同时希望同行提出更优秀的实时数据分页算法。 四、聚集索引的重要性和如何选择聚集索引 在上一节的标题中,笔者写的是:实现小数据量和海量数据的通用分页显示存储过程。这是因为在将本存储过程应用于“办公自动化”系统的实践中时,笔者发现这第三种存储过程在小数据量的情况下,有如下现象: 1、分页速度一般维持在1秒和3秒之间。 2、在查询最后一页时,速度一般为5秒至8秒,哪怕分页总数只有3页或30万页。 虽然在超大容量情况下,这个分页的实现过程是很快的,但在分前几页时,这个1-3秒的速度比起第一种甚至没有经过优化的分页方法速度还要慢,借用户的话说就是“还没有ACCESS数据库速度快”,这个认识足以导致用户放弃使用您开发的系统。 笔者就此分析了一下,原来产生这种现象的症结是如此的简单,但又如此的重要:排序的字段不是聚集索引! 本篇文章的题目是:“查询优化及分页算法方案”。笔者只所以把“查询优化”和“分页算法”这两个联系不是很大的论题放在一起,就是因为二者都需要一个非常重要的东西――聚集索引。 在前面的讨论中我们已经提到了,聚集索引有两个最大的优势: 1、以最快的速度缩小查询范围。 2、以最快的速度进行字段排序。 第1条多用在查询优化时,而第2条多用在进行分页时的数据排序。 而聚集索引在每个表内又只能建立一个,这使得聚集索引显得更加的重要。聚集索引的挑选可以说是实现“查询优化”和“高效分页”的最关键因素。 但要既使聚集索引列既符合查询列的需要,又符合排序列的需要,这通常是一个矛盾。 笔者前面“索引”的讨论中,将fariqi,即用户发文日期作为了聚集索引的起始列,日期的精确度为“日”。这种作法的优点,前面已经提到了,在进行划时间段的快速查询中,比用ID主键列有很大的优势。 但在分页时,由于这个聚集索引列存在着重复记录,所以无法使用max或min来最为分页的参照物,进而无法实现更为高效的排序。而如果将ID主键列作为聚集索引,那么聚集索引除了用以排序之外,没有任何用处,实际上是浪费了聚集索引这个宝贵的资源。 为解决这个矛盾,笔者后来又添加了一个日期列,其默认值为getdate()。用户在写入记录时,这个列自动写入当时的时间,时间精确到毫秒。即使这样,为了避免可能性很小的重合,还要在此列上创建UNIQUE约束。将此日期列作为聚集索引列。 有了这个时间型聚集索引列之后,用户就既可以用这个列查找用户在插入数据时的某个时间段的查询,又可以作为唯一列来实现max或min,成为分页算法的参照物。 经过这样的优化,笔者发现,无论是大数据量的情况下还是小数据量的情况下,分页速度一般都是几十毫秒,甚至0毫秒。而用日期段缩小范围的查询速度比原来也没有任何迟钝。 聚集索引是如此的重要和珍贵,所以笔者总结了一下,一定要将聚集索引建立在: 1、您最频繁使用的、用以缩小查询范围的字段上; 2、您最频繁使用的、需要排序的字段上。 结束语: 本篇文章汇集了笔者近段在使用数据库方面的心得,是在做“办公自动化”系统时实践经验的积累。希望这篇文章不仅能够给大家的工作带来一定的帮助,也希望能让大家能够体会到分析问题的方法;最重要的是,希望这篇文章能够抛砖引玉,掀起大家的学习和讨论的兴趣,以共同促进,共同为公安科技强警事业和金盾工程做出自己最大的努力。 最后需要说明的是,在试验中,我发现用户在进行大数据量查询的时候,对数据库速度影响最大的不是内存大小,而是CPU。在我的P4 2.4机器上试验的时候,查看“资源管理器”,CPU经常出现持续到100%的现象,而内存用量却并没有改变或者说没有大的改变。即使在我们的HP ML 350 G3服务器上试验时,CPU峰值也能达到90%,一般持续在70%左右。 監視資源管理器的文件變化unit Unit2; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, shlobj, activex, StdCtrls, FileCtrl,strUtils; const shcne_renameitem = $1; shcne_create = $2; shcne_delete = $4; shcne_mkdir = $8; shcne_rmdir = $10; shcne_mediainserted = $20; shcne_mediaremoved = $40; shcne_driveremoved = $80; shcne_driveadd = $100; shcne_netshare = $200; shcne_netunshare = $400; shcne_attributes = $800; shcne_updatedir = $1000; shcne_updateitem = $2000; shcne_serverdisconnect = $4000; shcne_updateimage = $8000; shcne_driveaddgui = $10000; shcne_renamefolder = $20000; shcne_freespace = $40000; shcne_assocchanged = $8000000; shcne_diskevents = $2381F; shcne_globalevents = $C0581E0; shcne_allevents = $7FFFFFFF; shcne_interrupt = $80000000; shcnf_idlist = 0; // lpitemidlist shcnf_patha = $1; // path name shcnf_printera = $2; // printer friendly name shcnf_dword = $3; // dword shcnf_pathw = $5; // path name shcnf_printerw = $6; // printer friendly name shcnf_type = $FF; shcnf_flush = $1000; shcnf_flushnowait = $2000; shcnf_path = shcnf_pathw; shcnf_printer = shcnf_printerw; wm_shnotify = $401; noerror = 0; type TForm1 = class(TForm) Button1: TButton; Memo1: TMemo; DirectoryListBox1: TDirectoryListBox; DriveComboBox1: TDriveComboBox; Label1: TLabel; Button2: TButton; procedure FormClose(Sender: TObject; var Action: TCloseAction); procedure Button1Click(Sender: TObject); procedure Button2Click(Sender: TObject); private { Private declarations } procedure wmshellreg(var message: tmessage); message wm_shnotify; public { Public declarations } end; type pshnotifystruct = ^shnotifystruct; shnotifystruct = record dwitem1: pitemidlist; dwitem2: pitemidlist; end; type pshfileinfobyte = ^shfileinfobyte; _shfileinfobyte = record hicon: integer; iicon: integer; dwattributes: integer; szdisplayname: array[0..259] of char; sztypename: array[0..79] of char; end; shfileinfobyte = _shfileinfobyte; type pidlstruct = ^idlstruct; _idlstruct = record pidl: pitemidlist; bwatchsubfolders: integer; end; idlstruct = _idlstruct; function shnotify_register(hwnd: integer): bool; function shnotify_unregister: bool; function sheventname(strpath1, strpath2: string; lparam: integer): string; function shchangenotifyderegister(hnotify: integer): integer; stdcall; external 'shell32.dll' index 4; function shchangenotifyregister(hwnd, uflags, dweventid, umsg, citems: longword; lpps: pidlstruct): integer; stdcall; external 'shell32.dll' index 2; function shgetfileinfopidl(pidl: pitemidlist; dwfileattributes: integer; psfib: pshfileinfobyte; cbfileinfo: integer; uflags: integer): integer; stdcall; external 'shell32.dll' name 'shgetfileinfoa'; var Form1: TForm1; m_hshnotify: integer; m_pidldesktop: pitemidlist; implementation {$R *.dfm} function sheventname(strpath1, strpath2: string; lparam: integer): string; var sevent: string; begin case lparam of //根据参数设置提示消息 shcne_renameitem: sevent := 'rename' + strpath1 + ':' + strpath2; shcne_create: sevent := '建立文件 文件名:' + strpath1; shcne_delete: sevent := '删除文件 文件名:' + strpath1; shcne_mkdir: sevent := '新建目录 目录名:' + strpath1; shcne_rmdir: sevent := '删除目录 目录名:' + strpath1; shcne_mediainserted: sevent := strpath1 + '中插入可移动存储介质'; shcne_mediaremoved: sevent := strpath1 + '中移去可移动存储介质' + strpath1 + ' ' + strpath2; shcne_driveremoved: sevent := '移去驱动器' + strpath1; shcne_driveadd: sevent := '添加驱动器' + strpath1; shcne_netshare: sevent := '改变目录' + strpath1 + '的共享属性'; shcne_attributes: sevent := '改变文件目录属性 文件名' + strpath1; shcne_updatedir: sevent := '更新目录' + strpath1; shcne_updateitem: sevent := '更新文件 文件名:' + strpath1; shcne_serverdisconnect: sevent := '断开与服务器的连接' + strpath1 + ' ' + strpath2; shcne_updateimage: sevent := 'shcne_updateimage'; shcne_driveaddgui: sevent := 'shcne_driveaddgui'; shcne_renamefolder: sevent := '重命名文件夹' + strpath1 + '为' + strpath2; shcne_freespace: sevent := '磁盘空间大小改变'; shcne_assocchanged: sevent := '改变文件关联'; else sevent := '未知操作' + inttostr(lparam); end; result := sevent; end; function shnotify_register(hwnd: integer): bool; var ps: pidlstruct; begin {$R-} result := false; if m_hshnotify = 0 then begin //获取桌面文件夹的pidl if shgetspecialfolderlocation(0, CSIDL_DESKTOP, m_pidldesktop) <> noerror then form1.close; if boolean(m_pidldesktop) then begin new(ps); try ps.bwatchsubfolders := 1; ps.pidl := m_pidldesktop; // 利用shchangenotifyregister函数注册系统消息处理 m_hshnotify := shchangenotifyregister(hwnd, (shcnf_type or shcnf_idlist), (shcne_allevents or shcne_interrupt), wm_shnotify, 1, ps); result := boolean(m_hshnotify); finally FreeMem(ps); end; end else // 如果出现错误就使用 cotaskmemfree函数来释放句柄 cotaskmemfree(m_pidldesktop); end; {$R+} end; function shnotify_unregister: bool; begin result := false; if boolean(m_hshnotify) then //取消系统消息监视,同时释放桌面的pidl if boolean(shchangenotifyderegister(m_hshnotify)) then begin {$R-} m_hshnotify := 0; cotaskmemfree(m_pidldesktop); result := true; {$R-} end; end; procedure tform1.wmshellreg(var message: tmessage); //系统消息处理函数 var strpath1, strpath2: string; charpath: array[0..259] of char; pidlitem: pshnotifystruct; vPath,vFile:string; begin pidlitem := pshnotifystruct(message.wparam); // 获得系统消息相关得路径 shgetpathfromidlist(pidlitem.dwitem1, charpath); strpath1 := charpath; shgetpathfromidlist(pidlitem.dwitem2, charpath); strpath2 := charpath; vPath:=ExtractFilePath(strPath1); vFile:=ExtractFileName(strPath1); if (message.lparam=shcne_create) and (vPath=(Label1.Caption+'\')) then begin // memo1.lines.add(sheventname(strpath1, strpath2, message.lparam) + chr(13) + chr(10)); if not AnsiContainsText(Memo1.Lines.Text,vFile) then memo1.lines.add(vFile); end; end; procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction); begin //在程序退出的同时删除监视 if boolean(m_pidldesktop) then shnotify_unregister; end; procedure TForm1.Button1Click(Sender: TObject); begin Memo1.Clear; m_hshnotify := 0; if shnotify_register(form1.handle) then begin //注册shell监视 showmessage('shell监视程序成功注册'); button1.enabled := false; end else showmessage('shell监视程序注册失败'); end; procedure TForm1.Button2Click(Sender: TObject); var i:integer; begin i:=Memo1.Lines.IndexOf(Memo1.SelText); Memo1.Lines.Delete(i); end; end. ODBC以上是建立一个系统DSN的基本信息(其它信息如选项或高级选项等信息也在这里设置,只不过因采用默认信息,注册表里没有列出),我们在程序中按上述步骤操作注册表,同样也能增加一个系统DSN或修改其配置.在下面的例子程序中,将按以上步骤建立一个系统DSN,请注意程序中的注释. {******************************************************* 在本程序中,将创建一个ODBC系统数据源(DSN),数据源名称:MyAccess 数据源描述:我的新数据源数据库类型:ACCESS97 对应数据库:C:\Inetpub\wwwroot\test.mdb *******************************************************} { 注意应在USES语句中包含Registry } procedure TForm1.Button1Click(Sender: TObject); var registerTemp : TRegistry; bData : array[ 0..0 ] of byte; begin registerTemp := TRegistry.Create; //建立一个Registry实例 with registerTemp do begin RootKey:=HKEY_LOCAL_MACHINE;//设置根键值为HKEY_LOCAL_MACHINE //找到Software\ODBC\ODBC.INI\ODBC Data Sources if OpenKey(’Software\ODBC\ODBC.INI\ODBC Data Sources’,True) then begin //注册一个DSN名称 WriteString( ’MyAccess’, ’Microsoft Access Driver (*.mdb)’ ); end else begin//创建键值失败 memo1.lines.add(’增加ODBC数据源失败’); exit; end; CloseKey; //找到或创建Software\ODBC\ODBC.INI\MyAccess,写入DSN配置信息 if OpenKey(’Software\ODBC\ODBC.INI\MyAccess’,True) then begin WriteString( ’DBQ’, ’C:\inetpub\wwwroot\test.mdb’ );//数据库目录 WriteString( ’Description’, ’我的新数据源’ );//数据源描述 WriteString( ’Driver’, ’C:\PWIN98\SYSTEM\odbcjt32.dll’ );//驱动程序DLL文件 WriteInteger( ’DriverId’, 25 );//驱动程序标识 WriteString( ’FIL’, ’Ms Access;’ );//Filter依据 WriteInteger( ’SafeTransaction’, 0 );//支持的事务操作数目 WriteString( ’UID’, ’’ );//用户名称 bData[0] := 0; WriteBinaryData( ’Exclusive’, bData, 1 );//非独占方式 WriteBinaryData( ’ReadOnly’, bData, 1 );//非只读方式 end else//创建键值失败 begin memo1.lines.add(’增加ODBC数据源失败’); exit; end; CloseKey; //找到或创建Software\ODBC\ODBC.INI\MyAccess\Engines\Jet //写入DSN数据库引擎配置信息 if OpenKey(’Software\ODBC\ODBC.INI\MyAccess\Engines\Jet’,True) then begin WriteString( ’ImplicitCommitSync’, ’Yes’ ); WriteInteger( ’MaxBufferSize’, 512 );//缓冲区大小 WriteInteger( ’PageTimeout’, 10 );//页超时 WriteInteger( ’Threads’, 3 );//支持的线程数目 WriteString( ’UserCommitSync’, ’Yes’ ); end else//创建键值失败 begin memo1.lines.add(’增加ODBC数据源失败’); exit; end; CloseKey; memo1.lines.add(’增加新ODBC数据源成功’); Free; end; end; 以上程序在PWIN98+DELPHI3.0下调试通过. 下面是创建常见数据库类型的DSN需要设置的信息([]为注释内容,除特殊注释外,各参数可见前面说明): 1.Access(Microsoft Access Driver(*.mdb)) DBQ、Description、Driver[odbcjt32.dll]、DriverID[25]、FIL[Ms Access;]、 SafeTransaction[默认为0]、UID[默认为空]、 Engines\Jet\ImplicitCommitSync[默认为Yes]、Engines\Jet\MaxBufferSize[默认512]、 Engines\Jet\PageTimeout[默认为512]、Engines\Jet\Threads[默认为3]、 Engines\Jet\UserCommitSync[默认为Yes] 可选设置:SystemDb[字符串,系统数据库的路径]、 ReadOnly[二进制,是否以只读方式打开,1为是,默认为0]、 Exclusive[二进制,是否以独占方式打开,1为是,默认为0]、 PWD[字符串,用户密码] 2.EXCEL(Microsoft Excel Driver(*.xls)) DBQ[Excel97(=path\xxx.xls)、5.0/7.0(=path\xxx.xls)、4.0(=path)、3.0(=path)]、 Description、Driver[odbcjt32.dll]、 DefaultDir[Excel97(<>DBQ)、5.0/7.0(<>DBQ)、4.0(=DBQ)、3.0(=DBQ)]、 DriverID[790(Excel97)、22(5.0/7.0)、278(4.0)、534(3.0)]、 FIL[Excel5.0;]、ReadOnly、SafeTransaction、UID、 Engines\Excel\ImplicitCommitSync、Engines\Excel\MaxScanRows[数字,扫描行数,默认为8]、 Engines\Excel\Threads、Engines\Excel\UserCommitSync、 Engines\Excel\FirstRowHasName[二进制,第一行是否是域名,1表示是,默认为1] 注: Excel97和Excel7.0/5.0的DBQ对应一个XLS文件,而Excel4.0和Excel3.0则对应一个目录; DefaultDir对应一个目录,在Excel97和Excel7.0/5.0中是DBQ所对应的路径,而在 Excel4.0和Excel3.0下则与DBQ相同;各个版本的DriverID不同. 3.dBase(Microsoft dBase Driver(*.dbf)) DefaultDir[字符串,数据库文件所在目录]、Description、Driver[odbcjt32.dll]、 DriverID[277(IV)、533(5.0)]、FIL[dbase III;]、SafeTransaction、UID、 Engines\Xbase\ImplicitCommitSync、 Engines\Xbase\Collating[字符串,排序依据,可为ASCII、International、Norwegian-Danish、 Swedish-Finnish]、 Engines\Xbase\Deleted[二进制,是否不显示被软删除的记录,0表示显示,默认为1]、 Engines\Xbase\PageTimeout[默认为600]、Engines\Xbase\UserCommitSync、 Engines\Xbase\Threads、Engines\Xbase\Statistics[二进制,是否用大约的行数,1为是,默认0] 注:(dBaseIV和dBase5.0两个版本的DriverId有不同) 4.Foxpro(Microsoft Foxpro Driver(*.dbf)) DefaultDir[数据库文件所在目录]、Description、Driver[odbcjt32.dll]、 DriverID[536(2.6)、280(2.5)]、FIL[Foxpro 2.0;]、SafeTransaction、UID、 Engines\Xbase\Collating[字符串,排序依据,可为ASCII、International]、 Engines\Xbase\Deleted[二进制,是否不显示被软删除的记录,0表示显示,默认为1]、 Engines\Xbase\PageTimeout[默认为600]、Engines\Xbase\UserCommitSync、 Engines\Xbase\Threads、Engines\Xbase\Statistics[二进制,是否用大约的行数,1为是,默认0] 注:(Foxpro2.5和Foxpro2.6两个版本的DriverId有不同) 把上面程序做成一个COM或ActiveX控件吧,在很多高级程序设计语言如DELPHI、C++Buider、VB、VC、PB中都能用到的. 8月29日 关闭指定的执行文件procedure TVBjGuiMain.CloseExe(ExeFile: String); const PROCESS_TERMINATE=$0001; var ExeFileName: String; ContinueLoop: BOOL; FSnapshotHandle: THandle; FProcessEntry32: TProcessEntry32; begin ExeFileName := ExeFile; FSnapshotHandle := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); FProcessEntry32.dwSize := Sizeof(FProcessEntry32); ContinueLoop := Process32First(FSnapshotHandle,FProcessEntry32); while integer(ContinueLoop) <> 0 do begin if ((UpperCase(ExtractFileName(FProcessEntry32.szExeFile)) = UpperCase(ExeFileName)) or (UpperCase(FProcessEntry32.szExeFile) = UpperCase(ExeFileName))) then TerminateProcess(OpenProcess(PROCESS_TERMINATE, BOOL(0), FProcessEntry32.th32ProcessID), 0); ContinueLoop := Process32Next(FSnapshotHandle,FProcessEntry32); end; end; 8月28日 软件工程师的务实职业生涯规划(good)[1] [2] [3] [4] [5] [6] [7] [8] [9] [10] 这篇文章是我偶然在论坛上看到的,看后觉得挺有感触的。朋友们可以参考下,我一直认为做人一定要做个多面手,不是说你作技术就不能做别的,其实多学点对自己是没有坏处的,还有平时多注意积累其他方面的知识,这样在自己不做技术的时候还有别的可做。做个有准备的人岂不是更好! TWebBrowser 1、初始化和终止化(Initialization & Finalization) 大家在执行TWebBrowser的某个方法以进行期望的操作,如ExecWB等的时候可能都碰到过“试图激活未注册的丢失目标”或“OLE对象未注册”等错误,或者并没有出错但是得不到希望的结果,比如不能将选中的网页内容复制到剪贴板等。以前用它编程的时候,我发现ExecWB有时侯起作用但有时侯又不行,在Delphi生成的缺省工程主窗口上加入TWebBrowser,运行时并不会出现“OLE对象未注册”的错误。同样是一个偶然的机会,我才知道OLE对象需要初始化和终止化(懂得的东东实在太少了)。 我用我的前一篇文章《Delphi程序窗口动画&正常排列平铺的解决》所说的方法编程,运行时出了上面所说的错误,我便猜想应该有OleInitialize之类的语句,于是,找到并加上了下面几句话,终于搞定!究其原因,我想大概是由于TWebBrowser是一个嵌入的OLE对象而不算是用Delphi编写的VCL吧。 initialization OleInitialize(nil); finalization try OleUninitialize; except end; 这几句话放在主窗口所有语句之后,“end.”之前。 ----------------------------------------------------------------------------------- 2、EmptyParam 在Delphi 5中TWebBrowser的Navigate方法被多次重载: procedure Navigate(const URL: WideString); overload; procedure Navigate(const URL: WideString; var Flags: OleVariant); overload; procedure Navigate(const URL: WideString; var Flags: OleVariant; var TargetFrameName: OleVariant); overload; procedure Navigate(const URL: WideString; var Flags: OleVariant; var TargetFrameName: OleVariant; var PostData: OleVariant); overload; procedure Navigate(const URL: WideString; var Flags: OleVariant; var TargetFrameName: OleVariant; var PostData: OleVariant; var Headers: OleVariant); overload; 而在实际应用中,使用后几种方法调用时,由于我们很少用到后面几个参数,但函数声明又要求是变量参数,一般的做法如下: var t:OleVariant; begin webbrowser1.Navigate(edit1.text,t,t,t,t); end; 需要定义变量t(还有很多地方要用到它),很麻烦。其实我们可以用EmptyParam来代替(EmptyParam是一个公用的Variant空变量,不要对它赋值),只需一句话就可以了: webbrowser1.Navigate(edit1.text,EmptyParam,EmptyParam,EmptyParam,EmptyParam); 虽然长一点,但比每次都定义变量方便得多。当然,也可以使用第一种方式。 webbrowser1.Navigate(edit1.text) ----------------------------------------------------------------------------------- 3、命令操作 常用的命令操作用ExecWB方法即可完成,ExecWB同样多次被重载: procedure ExecWB(cmdID: OLECMDID; cmdexecopt: OLECMDEXECOPT); overload; procedure ExecWB(cmdID: OLECMDID; cmdexecopt: OLECMDEXECOPT; var pvaIn: OleVariant); overload; procedure ExecWB(cmdID: rOLECMDID; cmdexecopt: OLECMDEXECOPT; var pvaIn: OleVariant; var pvaOut: OleVariant); overload; 打开: 弹出“打开Internet地址”对话框,CommandID为OLECMDID_OPEN(若浏览器版本为IE5.0, 则此命令不可用)。 另存为:调用“另存为”对话框。 ExecWB(OLECMDID_SAVEAS,OLECMDEXECOPT_DODEFAULT, EmptyParam, EmptyParam); 打印、打印预览和页面设置: 调用“打印”、“打印预览”和“页面设置”对话框(IE5.5及以上版本才支持打 印预览,故实现应该检查此命令是否可用)。 ExecWB(OLECMDID_PRINT, OLECMDEXECOPT_DODEFAULT, EmptyParam, EmptyParam); if QueryStatusWB(OLECMDID_PRINTPREVIEW)=3 then ExecWB(OLECMDID_PRINTPREVIEW, OLECMDEXECOPT_DODEFAULT, EmptyParam,EmptyParam); ExecWB(OLECMDID_PAGESETUP, OLECMDEXECOPT_DODEFAULT, EmptyParam, EmptyParam); 剪切、复制、粘贴、全选: 功能无须多说,需要注意的是:剪切和粘贴不仅对编辑框文字,而且对网页上的非编 辑框文字同样有效,用得好的话,也许可以做出功能特殊的东东。获得其命令使能状 态和执行命令的方法有两种(以复制为例,剪切、粘贴和全选分别将各自的关键字替 换即可,分别为CUT,PASTE和SELECTALL): A、用TWebBrowser的QueryStatusWB方法。 if(QueryStatusWB(OLECMDID_COPY)=OLECMDF_ENABLED) or OLECMDF_SUPPORTED) then ExecWB(OLECMDID_COPY, OLECMDEXECOPT_DODEFAULT, EmptyParam, EmptyParam); B、用IHTMLDocument2的QueryCommandEnabled方法。 var Doc: IHTMLDocument2; begin Doc :=WebBrowser1.Document as IHTMLDocument2; if Doc.QueryCommandEnabled('Copy') then Doc.ExecCommand('Copy',false,EmptyParam); end; 查找: 参考第九条“查找”功能。 ----------------------------------------------------------------------------------- 4、字体大小 类似“字体”菜单上的从“最大”到“最小”五项(对应整数0~4,Largest等假设为五个菜单项的名字,Tag 属性分别设为0~4)。 A、读取当前页面字体大小。 var t: OleVariant; Begin WebBrowser1.ExecWB(OLECMDID_ZOOM, OLECMDEXECOPT_DONTPROMPTUSER, EmptyParam,t); case t of 4: Largest.Checked :=true; 3: Larger.Checked :=true; 2: Middle.Checked :=true; 1: Small.Checked :=true; 0: Smallest.Checked :=true; end; end; B、设置页面字体大小。 Largest.Checked :=false; Larger.Checked :=false; Middle.Checked :=false; Small.Checked :=false; Smallest.Checked :=false; TMenuItem(Sender).Checked :=true; t :=TMenuItem(Sender).Tag; WebBrowser1.ExecWB(OLECMDID_ZOOM, OLECMDEXECOPT_DONTPROMPTUSER, t,t); ----------------------------------------------------------------------------------- 5、添加到收藏夹和整理收藏夹 const CLSID_ShellUIHelper: TGUID = '{64AB4BB7-111E-11D1-8F79-00C04FC2FBE1}'; var p:procedure(Handle: THandle; Path: PChar); stdcall; procedure TForm1.OrganizeFavorite(Sender: Tobject); var H: HWnd; begin H := LoadLibrary(PChar('shdocvw.dll')); if H <> 0 then begin p := GetProcAddress(H, PChar('DoOrganizeFavDlg')); if Assigned(p) then p(Application.Handle, PChar(FavFolder)); end; FreeLibrary(h); end; procedure TForm1.AddFavorite(Sender: TObject); var ShellUIHelper: ISHellUIHelper; url, title: Olevariant; begin Title := Webbrowser1.LocationName; Url := Webbrowser1.LocationUrl; if Url <> '' then begin ShellUIHelper := CreateComObject(CLSID_SHELLUIHELPER) as IShellUIHelper; ShellUIHelper.AddFavorite(url, title); end; end; 用上面的通过ISHellUIHelper接口来打开“添加到收藏夹”对话框的方法比较简单,但是有个缺陷,就是打开的窗口不是模式窗口,而是独立于应用程序的。可以想象,如果使用与OrganizeFavorite过程同样的方法来打开对话框,由于可以指定父窗口的句柄,自然可以实现模式窗口(效果与在资源管理器和IE中打开“添加到收藏夹”对话框相同)。问题显然是这样的,上面两个过程的作者当时只知道shdocvw.dll中DoOrganizeFavDlg的原型而不知道DoAddToFavDlg的原型,所以只好用ISHellUIHelper接口来实现(或许是他不够严谨,认为是否是模式窗口无所谓?)。 下面的过程就告诉你DoAddToFavDlg的函数原型。需要注意的是,这样打开的对话框并不执行“添加到收藏夹”的操作,它只是告诉应用程序用户是否选择了“确定”,同时在DoAddToFavDlg的第二个参数中返回用户希望放置Internet快捷方式的路径,建立.Url文件的工作由应用程序自己来完成。 procedure TForm1.AddFavorite(IE: TEmbeddedWB); procedure CreateUrl(AUrlPath, AUrl: PChar); var URLfile: TIniFile; begin URLfile := TIniFile.Create(String(AUrlPath)); RLfile.WriteString('InternetShortcut', 'URL', String(AUrl)); RLfile.Free; end; var AddFav: function(Handle: THandle; UrlPath: PChar; UrlPathSize: Cardinal; Title: PChar; TitleSize: Cardinal; FavIDLIST: pItemIDList): Bool; stdcall; FDoc: IHTMLDocument2; UrlPath, url, title: array[0..MAX_PATH] of char; H: HWnd; pidl: pItemIDList; FRetOK: Bool; begin FDoc := IHTMLDocument2(IE.Document); if FDoc = nil then exit; StrPCopy(Title, FDoc.Get_title); StrPCopy(url, FDoc.Get_url); if Url <> '' then begin H := LoadLibrary(PChar('shdocvw.dll')); if H <> 0 then begin SHGetSpecialFolderLocation(0, CSIDL_FAVORITES, pidl); AddFav := GetProcAddress(H, PChar('DoAddToFavDlg')); if Assigned(AddFav) then FRetOK :=AddFav(Handle, UrlPath, Sizeof(UrlPath), Title, Sizeof(Title), pidl) end; FreeLibrary(h); if FRetOK then CreateUrl(UrlPath, Url); end end; ----------------------------------------------------------------------------------- 6、使WebBrowser获得焦点 TWebBrowser非常特殊,它从TWinControl继承来的SetFocus方法并不能使得它所包含的文档获得焦点,从而不能立即使用Internet Explorer本身具有得快捷键,解决方法如下:< procedure TForm1.SetFocusToDoc; begin if WebBrowser1.Document <> nil then with WebBrowser1.Application as IOleobject do DoVerb(OLEIVERB_UIACTIVATE, nil, WebBrowser1, 0, Handle, GetClientRect); end; 除此之外,我还找到一种更简单的方法,这里一并列出: if WebBrowser1.Document <> nil then IHTMLWindow2(IHTMLDocument2(WebBrowser1.Document).ParentWindow).focus 刚找到了更简单的方法,也许是最简单的: if WebBrowser1.Document <> nil then IHTMLWindow4(WebBrowser1.Document).focus 还有,需要判断文档是否获得焦点这样来做: if IHTMLWindow4(WebBrowser1.Document).hasfocus then ----------------------------------------------------------------------------------- 7、点击“提交”按钮 如同程序里每个窗体上有一个“缺省”按钮一样,Web页面上的每个Form也有一个“缺省”按钮——即属性为“Submit”的按钮,当用户按下回车键时就相当于鼠标单击了“Submit”。但是TWebBrowser似乎并不响应回车键,并且,即使把包含TWebBrowser的窗体的KeyPreview设为True,在窗体的KeyPress事件里还是不能截获用户向TWebBrowser发出的按键。 我的解决办法是用ApplicatinEvents构件或者自己编写TApplication对象的OnMessage事件,在其中判断消息类型,对键盘消息做出响应。至于点击“提交”按钮,可以通过分析网页源代码的方法来实现,不过我找到了更为简单快捷的方法,有两种,第一种是我自己想出来的,另一种是别人写的代码,这里都提供给大家,以做参考。 A、用SendKeys函数向WebBrowser发送回车键 在Delphi 5光盘上的Info\Extras\SendKeys目录下有一个SndKey32.pas文件,其中包含了两个函数SendKeys和AppActivate,我们可以用SendKeys函数来向WebBrowser发送回车键,我现在用的就是这个方法,使用很简单,在WebBrowser获得焦点的情况下(不要求WebBrowser所包含的文档获得焦点),用一条语句即可: Sendkeys('~',true);// press RETURN key SendKeys函数的详细参数说明等,均包含在SndKey32.pas文件中。 B、在OnMessage事件中将接受到的键盘消息传递给WebBrowser。 procedure TForm1.ApplicationEvents1Message(var Msg: TMsg; var Handled: Boolean); {fixes the malfunction of some keys within webbrowser control} const StdKeys = [VK_TAB, VK_RETURN]; { standard keys } ExtKeys = [VK_DELETE, VK_BACK, VK_LEFT, VK_RIGHT]; { extended keys } fExtended = $01000000; { extended key flag } begin Handled := False; with Msg do if ((Message >= WM_KEYFIRST) and (Message <= WM_KEYLAST)) and ((wParam in StdKeys) or {$IFDEF VER120}(GetKeyState(VK_CONTROL) < 0) or {$ENDIF} (wParam in ExtKeys) and ((lParam and fExtended) = fExtended)) then try if IsChild(Handle, hWnd) then { handles all browser related messages } begin with {$IFDEF VER120}Application_{$ELSE}Application{$ENDIF} as IOleInPlaceActiveObject do Handled := TranslateAccelerator(Msg) = S_OK; if not Handled then begin Handled := True; TranslateMessage(Msg); DispatchMessage(Msg); end; end; except end; end; // MessageHandler (此方法来自EmbeddedWB.pas) ----------------------------------------------------------------------------------- 8、直接从TWebBrowser得到网页源码及Html 下面先介绍一种极其简单的得到TWebBrowser正在访问的网页源码的方法。一般方法是利用TWebBrowser控件中的Document对象提供的IPersistStreamInit接口来实现,具体就是:先检查WebBrowser.Document对象是否有效,无效则退出;然后取得IPersistStreamInit接口,接着取得HTML源码的大小,分配全局堆内存块,建立流,再将HTML文本写到流中。程序虽然不算复杂,但是有更简单的方法,所以实现代码不再给出。其实基本上所有IE的功能TWebBrowser都应该有较为简单的方法来实现,获取网页源码也是一样。下面的代码将网页源码显示在Memo1中。 Memo1.Lines.Add(IHtmlDocument2(WebBrowser1.Document).Body.OuterHtml); 同时,在用TWebBrowser浏览HTML文件的时候要将其保存为文本文件就很简单了,不需要任何的语法解析工具,因为TWebBrowser也完成了,如下: Memo1.Lines.Add(IHtmlDocument2(WebBrowser1.Document).Body.OuterText); ----------------------------------------------------------------------------------- 9、“查找”功能 查找对话框可以在文档获得焦点的时候通过按键Ctrl-F来调出,程序中则调用IOleCommandTarget对象的成员函数Exec执行OLECMDID_FIND操作来调用,下面给出的方法是如何在程序中用代码来做出文字选择,即你可以自己设计查找对话框。 var Doc: IHtmlDocument2; TxtRange: IHtmlTxtRange; begin Doc :=WebBrowser1.Document as IHtmlDocument2; Doc.SelectAll; //此处为简写,选择全部文档的方法请参见第三条命令操作 //这句话尤为重要,因为IHtmlTxtRange对象的方法能够操作的前提是 //Document已经有一个文字选择区域。由于接着执行下面的语句,所以不会 //看到文档全选的过程。 TxtRange :=Doc.Selection.CreateRange as IHtmlTxtRange; TxtRange.FindText('Text to be searched',0.0); TxtRange.Select; end; 还有,从Txt.Get_text可以得到当前选中的文字内容,某些时候是有用的。 ----------------------------------------------------------------------------------- 10、提取网页中所有链接 这个方法来自大富翁论坛hopfield朋友的对一个问题的回答,我本想自己试验,但总是没成功。 var doc:IHTMLDocument2; all:IHTMLElementCollection; len,i:integer; item:OleVariant; begin doc:=WebBrowser1 .Document as IHTMLDocument2; all:=doc.Get_links; //doc.Links亦可 len:=all.length; for i:=0 to len-1 do begin item:=all.item(i,varempty); //EmpryParam亦可 memo1.lines.add(item.href); end; end; ----------------------------------------------------------------------------------- 11、设置TWebBrowser的编码 为什么我总是错过很多机会?其实早就该想到的,但是一念之差,便即天壤之别。当时我要是肯再多考虑一下,多试验一下,这就不会排到第11条了。下面给出一个函数,搞定,难以想象的简单。 procedure SetCharSet(AWebBrowser: TWebBrowser; ACharSet: String); var RefreshLevel: OleVariant; Begin IHTMLDocument2(AWebBrowser.Document).Set_CharSet(ACharSet); RefreshLevel :=7; //这个7应该从注册表来,帮助有Bug。 AWebBrowser.Refresh2(RefreshLevel); End; XML
|
|
|