Delphi 2007体验!
baidu
内容摘要:CodeGear(From Borland) 公司公布了最新的Delphi 2007 For Win32版本号。作为一个 Delphi 的使用者,第一时间下载、安装并进行了体验,现将一些使用感受记录例如以下
CodeGear(From Borland) 公司公布了最新的Delphi 2007 For Win32版本号。作为一个 Delphi 的使用者,第一时间下载、安装并进行了体验,现将一些使用感受记录例如以下:
注:以下的对照主要是针对Delphi7与Delphi2007,以下列出的部分功能可能在Delphi8/2005/2006中已存在。
1、下载
可在CodeGear官方站点下载试用版,或在VeryCD上寻找ISO,整个安装文件约1.2G多一些。
2、安装
Delphi 2007放弃了InstallShield,採用了InstallAware安装软件,整个安装操作非常友好。在安装结束时,会提示是否在Delphi启 动时自己主动检查更新,建议不要选择此项,由于安装完毕后无法禁用。Delphi 2007在開始菜单中建立了自己主动检查更新的快捷方式。
3、启动
Delphi 2007的启动速度没有传说中那么快,但和Delphi7相比也差不了多少,整体感觉不错。
4、界面
Delphi 2007的界面和之前的BDS 8/2005/2006界面风格是一样的,个人感觉不是太好,由于整个IDE颜色偏暗,Visual Studio 2005那种白亮色的界面应该更好一些。Splash和Welcome Page作的还是那么粗糙,CodeGear应该尽快找个好美工啊。
5、速度
IDE启动速度还不错,IDE的反映速度也非常好,尤其是程序的编译速度,个人感觉比Delphi7还快。
6、返回传统界面
习 惯了Delphi7及之前版本号的界面,对Delphi2007的这样的一体式界面多少有些难以适应,尤其是窗口设计器。尽管能够通过更改Desktop Layout为Classic Undocked让其与Delphi7有些相像,但却失去了Delphi7的那种*设计的效果。
内容摘要:CodeGear(From Borland) 公司公布了最新的Delphi 2007 For Win32版本号。作为一个 Delphi 的使用者,第一时间下载、安装并进行了体验,现将一些使用感受记录例如以下
在Delphi2007中,更改一个选项,可让IDE的窗口设计器返回Delphi的传统风格:Tools–Options–VCL Designer,取消选中Embedded Designer。
此选项仅仅有在IDE重新启动后才会生效,生效后整个界面和Delphi7差点儿相同,但组件面板无法达到传统界面的效果。但此设计似乎有BUG,当IDE最小化的时候,窗口设计器却没有一同最小化。
7、新增属性:Application.MainFormOnTaskBar
用Delphi2007新建一project,然后查看project文件的源码,发现多一行代码:
Application.MainFormOnTaskBar := True;
Delphi2007默认已将MainForm显示于任务栏,而不是之前版本号的Application。这个功能在曾经非常多Delphier都讨论过,如今Delphi自身支持了。设计此属性非常明显,因该是为了兼容Windows Vista。
当然任务栏的右键菜单也发生了变化:
(Delphi 7 任务栏右键菜单)
(Delphi 2007 任务栏右键菜单)
8、新Project Option: Enable Runtime Themes
该project选项默认启用,用Delphi 2007编写的程序默认将启用Themes,这是一个非常好的功能,曾经必须用组件:Win32-XPManifest。
Delphi 2007 IDE本身、窗口设计器已支持操作系统Themes。
9、TeeChart升级为了TeeChart Standard 7.10
TeeChart最终升级了新版本号。
10、报表组件
Delphi 2007似乎没有附带不论什么报表组件,QuickReport和Rave消失了。
内容摘要:CodeGear(From Borland) 公司公布了最新的Delphi 2007 For Win32版本号。作为一个 Delphi 的使用者,第一时间下载、安装并进行了体验,现将一些使用感受记录例如以下
11、DBExpress
DBExpress重大升级至v4,架构已重写,使用此技术的Delphier能够试试,本人非常少使用。
12、模态窗口下的窗口闪动
在当前窗口用ShowModal显示一个模态窗口后,再次点击当前窗口,此时显示出的模态窗口会闪动,Delphi 2007编译的程序最终已能实现此效果,这也是Windows程序的标准效果。
13、Project Clean 功能
在Project Manager中右键点击project名称,选择Clean,会自己主动清除project的全部暂时文件和dcu文件。
14、实用的快捷键
最终为Build Project和Run Without Dedugging功能设置了快捷键。
15、比Delphi7超强的编辑器
Delphi2007的编辑器功能强大,这也应该是放弃Delphi7的重要理由,如输入Begin,自己主动生成End,代码重构,语法实时检查,显示行号等。
只是Delphi2007的那个代码帮助提示信息的窗口真是太丑了。
16、窗口设计器控件感应对齐
窗口设计器中的控件能够感应对齐,相当好的功能啊。
17、中文变量名
如今的Delphi已经支持中文变量名了,你能够试试。
18、新的组件
TTrayIcon、TFlowPanel、TGridPanel三个控件非常实用。Delphi2007新增Vista Dialogs组件,这些组件效果非常好,可是基于Vista API的,所以仅仅能在Windows Vista下使用。
19、TLabel控件可在内容超出范围时显示省略号
此功能非常实用,尤其是在Label中显示一个文件路径时,设置TLabel的EllipsisPosition属性就可以。
20、新增了一些实用的属性
Delphi2007对大多数常规组件增加了一些实用的属性,如Margins、Padding、TForm.PopupMode等,细致查看一些控件的属性列表,你就会发现非常多陌生的属性,但他们确实都非常实用。
用了Delphi 2007一段时间,但也发现一些小问题:
1、在Project Manager中更改PAS文件的名称后,不自己主动更新uses列表中的名称,呵呵~,这个要求不知道过只是分。
2、TMainMenu组件在窗口设计器中不可预览。
3、编辑器错误提示功能会误报,如Application.MainFormOnTaskBar属性,有时会提示不存在该属性。
4、新安装的组件,不但要在Tool–Options中设置Library Path,还必须设置Browseing Path,组件才干被正常使用。
用了N年的Delphi7了,体验了Delphi2007后觉得真应该换换了,综合来看,Delphi2007是一个非常好的版本号,IDE速度及功能性各方面都已经非常优秀,你准备使用Delphi2007吗?
Delphi 2007 初步印象
baidu
内容摘要:经过苦苦的等待,最终等来了新一代 Delphi 2007 的下载链接。昨天从 emule 上下载时,发觉有非常多的人在下载,让我非常感动。原来和我一样,对 Delphi 关注的人还不少啊。速度还算快,到夜里就下完了。接着開始了2个小时的试用,尽管不怎么细致,但从 Delphi 1 一路用过来的我,对里面的变化还比較敏感的。
经过苦苦的等待,最终等来了新一代 Delphi 2007 的下载链接。昨天从 emule 上下载时,发觉有非常多的人在下载,让我非常感动。原来和我一样,对 Delphi 关注的人还不少啊。速度还算快,到夜里就下完了。接着開始了2个小时的试用,尽管不怎么细致,但从 Delphi 1 一路用过来的我,对里面的变化还比較敏感的。
相比 Delphi 2006 来说,我觉得这个版本号的 Delphi 2007 是个超强的优化版,功能方面,仅仅有少些改变。说她是优化版,那是由于她的启动速度,编译效率,IDE 速度是相当的快(PS:我的电脑是PM1.4G,512MB)。李维先生所说的比 Delphi 7 快一点也不为过。近期用 VS2005,打开一个 C++ 控制台程序,要经过相当长的时间,编译就更不用说了。Welcome Page 里的“Where developers matter”真是让人感动啊。
至于其它方面的改进,对我来说,并不是非常有吸引力。Help 系统改用 MSDN 的那套最新的 help,支持了VISTA,可是我的电脑是不能跑了。对 Together 的集成,这个非常实用。控件多了几个。IntraWeb 变成了 CodeGear 的 VCL 部分了,但却不支持调试,这点有些奇怪。数据库接口统一了。特别的一点就是,原来 Borland 的标识,如今全变成了 CodeGear。
事实上,我并不了解多少开发,更不知道开发者究竟须要 Delphi 2007 增加些什么功能。我中心一直觉得 Delhpi 发展到今天,变化的可能已经越来越少。就像 VC 一样,非常少变动。Win32 RAD 的开发王者,应该还是属于 Delphi(C++上的RAD工具非常少,即便有,也是对语言进行了一些恶心扩展),速度,效率,谁能相比。如今唯一的希望是 Delphi 2007 的语言再加强一些,如 template。近期用 C++,一直在学习 template,所以也希望 Delphi 能跟上时代。
内容摘要:本文介绍delph i2007学习笔记
如今学的是delphi 的类,原D7的类我不就不记了,记下与D7不同的地方
a.class abstract 纯虚类,不能实例化的类
type
TAbstractClass = class abstract
procedure SomeProcedure;
end;
曾经的做法是在 procedure 的后面与 abstract ,如今仅仅移类的说明上,仅仅是意思一样,就是直观点少打字 呵呵.
b.class sealed 这个我眼下不知是什么意思,可能是不能继承的类
type
TAbstractClass = class sealed
procedure SomeProcedure;
end;
c.class const 类的常量,这个地方在D7内能够定类的方法一样能实现
type
TClassWithConstant = class
public
const SomeConst = 'This is a class constant';
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
ShowMessage(TClassWithConstant.SomeConst); //引用时,仅仅写类名就可能引用,不必实例化
end;
d.class type 类的类型, 在类的层次下能够定record,class子类什么的,这个将数据的集中体现....
type
TClassWithClassType = class
private
type
TRecordWithinAClass = record
SomeField: string;
end;
public
class var
RecordWithinAClass: TRecordWithinAClass;
end;
...
procedure TForm1.FormCreate(Sender: TObject);
begin
TClassWithClassType.RecordWithinAClass.SomeField := 'This is a field of a class type declaration';
ShowMessage(TClassWithClassType.RecordWithinAClass.SomeField);
end;
type
TOuterClass = class
strict private
MyField: Integer;
public
type
TInnerClass = class
public
MyInnerField: Integer;
procedure InnerProc;
end;
procedure OuterProc;
end;
procedure TOuterClass.TInnerClass.InnerProc;
begin
...
end;
让人期待的Delphi 2007 for Win32
baidu
内容摘要:从李维的Blog和一些网友的反馈来看,D2007确实是比較让人期待的一个版本号。作为Delphi的忠实支持者,我期待着Delphi的逐步回归,希望她能重登昔日的王者地位!
自从D7之后,Delphi似乎開始走了下坡路,到D2005时,让非常多人感到了失望,而D2006也是非常不easy才挽回了一点局面。大家都知道如今Delphi跟Borland分家,归属于CodeGear继续发展了。只是分出去或许会是好事,这能让一帮人更为专注地发展IDE的技术。Delphi 2007 for Win32的公布会(包括Delphi for PHP和InterBase 2007)前几天正在密锣紧鼓地进行着,从李维的Blog和一些网友的反馈来看,D2007确实是比較让人期待的一个版本号。作为Delphi的忠实支持者,我期待着Delphi的逐步回归,希望她能重登昔日的王者地位!(这次不要当VB杀手了,来当当C#和Java杀手吧.... XD)
Delphi2007 for Win32一些技术亮点:
1、IDE工具採用.Net2.0来编写,安装时候须要.Net FrameWork2.0,但编译出来的Exe是纯正的Win32程序,公布不须要.Net FrameWork2.0。新IDE採用.Net FrameWork2.0是为了更好的统一IDE平台,利用.Net的反射、泛型等高级特性,节省编写IDE时间。新的IDE确实比D7启动还要快,大概是五六秒时间左右就启动,跟VS.Net2005差点儿相同。演示中,新的IDE编译新建project比D7还要快一点。
2、Delphi2007支持Vista界面,封装了Vsita的新API函数。
3、Delphi2007有一个功能非常有意思,支持D2006的全部BPL组件。意思是,假设你用D2006编译过的BPL,没有源码和DCU,也能够直接安装在Delphi2007。哈哈,这个是delphi版本号上的史无前例,即使第三方控件包来不及支持Delphi2007,也最好还是碍开发者从D2006升级到Delphi2007做项目开发。
4、支持MS Build。比方支持Debug、Release。也支持编译前和编译后事件,可调用BAT文件。比方,你编译前邮件告诉老板,你要给我加薪50%,否则十分钟后销毁源码。另外,也能够改动其XML格式,仅仅编译特定的程序代码。
5、Delphi2007 for Win32数据集控件全面支持Unicode。比方,你的数据库表字段可採用中文名称作字段。Filter也支持Unicode。唉,为了这个filter支持Unicode,有些人在delphiBBS上苦苦守候了六七年。但delphi2007的UI(可视化界面)还不是全面支持Unicode。比方,你的DBGrid就不能支持中文、韩文(不说狗屁日文)、阿拉伯文等同一时候显示。原有的Vcl框架大量採用string声明而不是WideString,据李维讲,CodeGear会将Vcl全面支持Unicode,但要考虑一种最佳的平衡方法。
6、非常棒的DBX4。dbExpress将要统一混乱的数据库连接组件,底层全部改写。无论在Win32还是Win64上,可兼容本机代码和托管代码。呵呵,举个样例,DBX4可支持ADO,也能够支持ADO.Net。这一点,M$也做不到。DBX4新增加了Connection Pool高级组件。更令人心动的是,开放Driver Source,可自行扩展属性和方法,哈哈,ColreLab公司这回可高价卖不出去dbExpress驱动了。DBX4也全面支持Uicode。
7、Delphi2007 for Win32支持Ajax技术—Intraweb9.0。Intraweb9.0组件封装了Ajax,以事件方式来驱动程序。Intraweb9.0的Ajax技术支持断点跟踪调试,简单到跟你调试其它delphi程序一样。Intraweb9.0不愧是封装javascript的上乘之作。事实上,话又说回来了,这一两年流行的Ajax技术,事实上无非就是javascript封装而已。而Intraweb已经在六七年将javascript技术做得炉火纯青。更令人汗颜的是,很多所谓的web2.0新技术调试Ajax时候,仿佛又回到20多年前的C编程时代,不停地用Printf打印调试。Ajax技术也并不是是什么高级技术,仅仅是web编程一种无奈的选择。不久未来,应该是属于智能client平台。
个人的几点看法:
1、说实话这里我经历过大喜->大悲->大喜的三次变化。最初听说Delphi重回Win32,当然是大喜,个人比較不爽.Net,由于给别人敲代码还要别人装无用的东西才干运行(当然对象是企业的话,没什么所谓);之后在CSDN听到一些残缺不全的消息说D2007又要装.Net了?转而失望;幸而从这里看,仅仅是装.Net的开发时环境而已,运行库并不须要,至此疑虑全消。并且听说D2007还能针对2000、XP和Vista多种系统公布不同的程序?这个功能太棒了!曾经写的程序,总是部分人能用部分人不能,想出多版本号的话,自己多装个系统在那个系统下编译吧,这不是一般的麻烦。希望此功能是真的。
2、启动和编译速度比D7还快,这个也非常吸引人。Delphi的编译速度本来就非常有名了(用过C系列的就能对照出来)。之后听说D2006又用一种新技术改进了内存管理,可惜D2006还没装,没能体验。这下D2007竟然比D7还快了,希望不是仅仅针对特殊项目弄出来的“演示效果”。
3、关于Unicode,强烈期待实现整个IDE环境的Unicode化,某次写跟韩文有关的程序已经被整慘过了.... -_,- 这点Delphi须要向C#靠拢。
4、尽管IntraWeb自上次写日志来,还暂时没安排到学习时间。只是偶还是相信那位Delphi达人的话,相信它的前途。如今都出到9.0了,我觉得假设Delphi在B/S上面能抢回份额,微软将会面临非常大的挑战。
Delphi 2007 下安装 Shell 组件
baidu
内容摘要:本文介绍Delphi 2007 下安装 Shell 组件
Delphi 2007 中没有 Shell 组件,但在Delphi的lib文件夹存在相关的源文件,并且在C:WindowsSystem32文件夹下存在vclshlctrls100.bpl的运行时包,只有没有设计时包。
但在Delphi的Demo中有一个ShellControls,安装其文件夹下的 dclshlctrls.dpk 就可以安装成功Shell组件。
注1:Shell组件安装后还是在组件面板的Sample页中;
注2:Delphi2007的Demo文件夹在:我的电脑-共享文档-RAD Studio-Demos-DelphiWin32-VCLWin32;
在 使用Shell这样的组件时,程序编译后都会出现以下的警告:[DCC Warning] Unit1.pas(7): W1005 Unit ‘ShellCtrls’ is specific to a platform ,假设您不想让他显示的话,能够在你project相关源文件的开头加上以下代码:
{$WARN UNIT_PLATFORM OFF}
Delphi归来,重燃开发者的信心
baidu
内容摘要:江元麟表示:「眼下仅仅有Microsoft Visual C++支持64位,但我们累积了非常多Delphi的Library和组件,基于稳定度及开发时效的考虑,并不希望换开发环境,眼下做法是花非常多力气和C++整合。……我今天就是来问Delphi什么时候支持64Bit?」
CodeGear*区产品经理李维介绍完新产品,听众迫不及待的走向讲台,纷纷问起新产品的兼容问题,知网生物识别科技技术长江元麟也是当中一位,他特地来问一个问题,由于这将影响到公司未来产品的开发效率。
Windows Vista出现带来64位新挑战。知网生物识别科技去年面临客户要求在Vista的Content Menu技术上支持64位档案指纹加密。江元麟表示:「眼下仅仅有Microsoft Visual C++支持64位,但我们累积了非常多Delphi的Library和组件,基于稳定度及开发时效的考虑,并不希望换开发环境,眼下做法是花非常多力气和C++整合。……我今天就是来问Delphi什么时候支持64Bit?」
从1995年发表1.0版后,12年来,Delphi历经11个版本号更迭,从16位的1.0到.NET平台的BDS 2006。开发部门独立成立CodeGear 后,又回到原生Win32环境下的Delphi 2007 for Win32。江元麟24年程序开发经验,一路见证了Delphi的变化。
从1987年開始接触Borland,江元麟用过Turbo Pascal和Turbo C。1995年,由于工作须要開始使用Delphi。2000年,他投入生物识别产业,继续使用Delphi 5开发,他指出:「Delphi有一个非常好的长处是能够开发自己的组件,它的组件让我们的产品开发加速非常快。新进project师能立即就作一些简单的开发,这是Delphi最优秀的地方。」
相较于当时其它开发工具,他觉得:「VB当时没办法全然用对象导向的方式去开发组件,比較不是给Engineer用,而是给Power User使用。而C++要客制化组件难度颇高,它的平台没有那么灵活。」
为何一直用Delphi?江元麟解释说:「是由于它的平台,非常多Source Code都有释出,所以你能够开发一些真的是自己会用到的基层组件。我们公司的组件已经累积5年到10年都有,一个组件能够撑那么久,代表它非常稳定了,相对的我们公司的产品出来质量是非常好的,这也是Delphi的贡献……这也是为什么,我们宁可在Delphi上花力气结合C++来处理新挑战。」3年前,知网的识别软件能让Pentium 4 处理器在1秒内辨识十万枚指纹,是当时国外最高速度的3倍,他说:「这当中有一部份是由Delphi编译出来的程序代码效率相当好的贡献。」
尽管当天江元麟的问题没有立即的解决方式,但对于脱离Borland后的GodeGear,他表示:「蛮喜欢分割出来的CodeGear,曾经步调非常慢,如今步调非常快,我比較喜欢,听李维传递的讯息,感觉比較有活力,但希望能维持曾经的速度和质量。两年前看到Borland公司非常乱,觉得非常遗憾,周围的人两年前已经慢慢转到C#去了。」,他接着说:「我们本来去年要考虑转成C#,如今要又一次考虑了
编码的艺术
baidu
我在本文中要谈的不是编码的技术实现,我所关注的是关于编码的风格的问题。我在编写了e速的编码规范之后,产生了要写一些关于程序编码风格的念头;因此,就有了以下的文章,这些仅仅是本人的想法,可能在文章中还有一些未尽如人意的地方,所以肯请大家能够给与谅解。
非常多人在谈到编码的艺术时,总会说我的程序怎么怎么的厉害,功能多么的强大,好像什么事情都能完毕一样;可是去运行他的程序,bug不断;连他自己都不知道错在了什么地方。打开他的程序一看,代码写的凌乱不堪;命名上不规范,为了偷懒和简便有些命名干脆就用一个字母或者其它的简单符号取代,甚至于有些代码连他自己也搞不清是干什么了,更不要说怎样让别人去改动了….本人编码也快4个年头了,像上述的样例遇见过不少,整个程序改动起来实在是头疼。
的确,一件好的艺术品不在于其功能是多么的完好,而在于别人赞赏起来是否有它内在的美和是否非常easy就把它从杂货堆里一眼就能辨认出来;毕竟它是艺术品而非日用品。我们敲代码也是同样,假设程序中的格式非常随意,比如对数组做循环,一会儿採用下标变量从下到上的方式,一会儿又用从上到下的方式;对字符串一会儿用s t r c p y做复制,一会儿又用f o r循环做复制;等等。这些变化就会使人非常难看清实际上究竟是怎么回事了。
写好一个程序,当然须要使它符合语法规则、修正当中的错误和使它运行得足够快,可是实际应该做的远比这多得多。程序不仅须要给计算机读,也要给程序猿读。一个写得好的程序比那些写得差的程序更easy读、更easy改动。经过了怎样写好程序的训练,生产的代码更可能是正确的。
凝视:凝视是帮助程序读者的一种手段。可是,假设在凝视中仅仅说明代码本身已经讲明的事情,或者与代码矛盾,或是以精心编排的形式干扰读者,那么它们就是帮了倒忙。最好的凝视是简洁地点明程序的突出特征,或是提供一种概观,帮助别人理解程序。在标注凝视的同一时候,应该注意以下的问题:
不要大谈明显的东西。凝视不要去说明明确白的事,比方i + +能够将i值加1等等。凝视应该提供那些不能一下子从代码中看到的东西,或者把那些散布在很多代码里的信息收集到一起。当某些难以捉摸的事情出现时,凝视能够帮助澄清情况。假设操作本身非常明了,反复谈论它们就是画蛇添足了;给函数和全局数据加凝视。凝视当然能够有价值。对于函数、全局变量、常数定义、结构和类的域等,以及不论什么其它加上简短说明就能够帮助理解的内容,我们都应该为之提供凝视。全局变量常被分散使用在整个程序中的各个地方,写一个凝视能够帮人记住它的意义,也能够作为參考。放在每个函数前面的凝视能够成为帮人读懂程序的台阶。假设函数代码不太长,在这里写一行凝视就足够了。有些代码原本非常复杂,可能是由于算法本身非常复杂,或者是由于数据结构非常复杂。在这些情况下,用一段凝视指明有关文献对读者也非常有帮助。此外,说明做出某种决定的理由也非常有价值。
职业程序猿也常被要求凝视他们的全部代码。可是,应该看到,盲目遵守这些规则的结果却可能是丢掉了凝视的真谛。凝视是一种工具,它的作用就是帮助读者理解程序中的某些部分,而这些部分的意义不easy通过代码本身直接看到。我们应该尽可能地把代码写得easy理解。在这方面你做得越好,须要写的凝视就越少。好的代码须要的凝视远远少于差的代码。
编码的风格:全局变量应该採用具有描写叙述意义的名字,局部变量用短名字。函数採用动作性的名字。给神奇的数起个名字。现实中存在很多命名约定或者本地习惯。常见的比方:指针採用以p结尾的变量名,比如n o d e p;全局变量用大写开头的变量名,比如G l o b a l;常量用全然由大写字母拼写的变量名,如C O N S T A N T S等。命名约定能使自己的代码更easy理解,对别人写的代码也是一样。这些约定也使人在写代码时更easy决定事物的命名。对于长的程序,选择那些好的、具有说明性的、系统化的名字就更加重要。
保持一致性。要准确。以缩行形式显示程序结构。使用表达式的自然形式。利用括号排除歧义。分解复杂的表达式。要清晰。当心副作用。使用一致的缩行和加括号风格。为了一致性,使用习惯使用方法。用else-if 处理多路选择。避免使用函数宏。给宏的体和參数都加上括号。这些都是非常琐碎的事情,但却又是非常有价值的,就像保持书桌整洁能使你easy找到东西一样。与你的书桌不同的是,你的程序代码非常可能还会被别人使用。
用缩行显示程序的结构。採用一种一致的缩行风格,是使程序呈现出结构清晰的最省力的方法。
用加括号的方式排除二义性。括号表示分组,即使有时并不必要,加了括号也可能把意图表示得更清晰。在混合使用互相无关的运算符时,多写几个括号是个好主意。C语言以及与之相关的语言存在非常险恶的优先级问题,在这里非常easy犯错误。比如,由于逻辑运算符的约束力比赋值运算符强,在大部分混合使用它们的表达式中,括号都是必需的。
利用语言去计算对象的大小。不要大谈明显的东西。给函数和全局数据加凝视。不要凝视不好的代码,应该重写。不要与代码矛盾。澄清情况,不要添乱。
界面的风格:隐蔽实现的细节。不要在用户背后搞小动作。在各处都用同样方式做同样的事。释放资源与分配资源应该在同一层次进行。在低层检查错误,在高层处理。仅仅把异经常使用在异常的情况。
写良好的代码更easy阅读和理解,差点儿能够保证当中的错误更少。进一步说,它们通常比那些马马虎虎地堆起来的、没有细致推敲过的代码更短小。在这个拼命要把代码送出门、去赶上最后期限的时代,人们非常easy把风格丢在一旁,让将来去管它们吧。可是,这非常可能是一个代价非常昂贵的决定。上面的一些陈述性的言语充分的说明了,假设对好风格问题重视不够,程序中哪些方面可能出毛病。草率的代码是非常坏的代码,它不仅难看、难读,并且经常崩溃。好风格应该成为一种习惯。假设你在開始写代码时就关心风格问题,假设你花时间去审视和改进它,你将会逐渐养成一种好的编程习惯。一旦这样的习惯变成自己主动的东西,你的潜意识就会帮你照料很多细节问题,甚至你在工作压力下写出的代码也会更好
Delphi面向对象的编程方法
baidu
Delphi的编程语言是以Pascal为基础的。Pascal语言具有可读性好、编写easy的特点,这使得它非常适合作为基础的开发语言。同一时候,使用编译器创建的应用程序仅仅生成单个可运行文件(.EXE),正是这样的结合,使得Pascal成为Delphi这样的先进开发环境的编程语言。
本章中,我们将讨论Object Pascal的主要特点,并解说怎样在事件处理过程和其它应用程序中,使用它来编制程序代码。本章将解说Delphi应用程序中最经常使用的Object Pascal语法,而不是Pascal语言的一切细节。假设您全然不熟悉Pascal编程,请參阅一些基础的Pascal教程。假设您具有编程经验,并能熟练地使用其它流行程序语言,您将在本章的Object Pascal中发现一些同样的概念。假设您已经熟悉了Borland Pascal,就能够高速浏览或跳过本章。
2.1 编写Object Pascal程序代码
在前边的章节中,我们通过例程,已经编写了几行简单的代码。在本章中,我们将从熟悉Pascal编程的角度,配合实例,解说Object Pascal编程的基本方法。
在编写自己的Object Pascal程序时,要注意程序的可读性。Pascal语言是英式结构语言,在程序中选择合适的缩排、大写和小写风格,并在须要时将程序代码分行,会使得程序代码能够非常easy地被自己和他人读懂。一般的程序猿都有这样的体验:假设不给程序加上适当的注解,一段时间后,自己也难以理清程序的流程。给程序及时地加上凝视是良好的编程习惯。Delphi的凝视须要加注在{}之间,编辑器会把它们处理成为空白。Delphi保留了Borland Pascal编辑器的风格,keyword採用黑体字,被凝视的部分会变暗,这使得编程风格良好,易读易写。
2.1.1 编写赋值语句
在事件处理过程中,最经常使用到的工作就是把一个新值赋给一个属性或变量。在设计用户界面时,能够使用Object Inspector(Object Inspector)来改变其属性;但有时须要在程序运行时改变属性的值,并且有些属性仅仅能在运行时改变,这些属性在Delphi的在线帮助的“Proprety”主题中被标为运行期属性。进行这样的改变,就必须使用赋值语句。
下文的赋值语句表征一个OnClick事件。当button按动后,将编辑框部件Edit1的Color属性置为clRed:
procedure TForm1.Button1Click(Sender: TObject);
begin
Edit1.Color := clRed;
end;
当按动button后赋值语句被运行,编辑框变成红色。
在语句中,部件的名称在属性前,中间用“.”表示属性的所属关系。这样就准确地指定了要将clRed值赋给哪一部件的哪一属性。赋值号为“:=”,不论给属性还是给变量赋值,都是将右边的值赋给左边的属性或变量。
当将一个属性值、变量、常量或文本数据赋给属性或变量时,所赋值的类型和接受此值的属性或变量的类型应同样或兼容。一个属性或变量的类型定义了此属性或变量的可能值集合,也定义了程序代码能够运行的运算。在前边的例程中,编辑框部件的Color属性和clRed的类型都是TColor。能够在在线帮助中找到一个属性的类型;第二种方法是在Object Inspector中选定该属性值段,并按下F1键,则类型将在属性说明的结尾处列出,比如Color属性列出下边的语句:
Property Color : TColor;
有些属性是仅仅读(Read Only)的,它们仅仅能被读取,不能被改变。请查阅在线帮助,在Delphi中这些仅仅读属性都有注解。
2.1.2 标识符的说明与使用
标识符是Delphi应用程序中一些量的名称,这些量包括变量(var)、常量(const)、类型(type)、过程(procedure)、方法(Method)及其它,Object Pascal 在应用标识符时,必须首先说明它们。Object Pascal是强类型语言,它的编译器能够检查确保赋给变量或属性的值是正确的类型,以便于您改正错误。由于Object Pascal是编译语言,所以Delphi的运行速度要比使用解释语言快得多。在使用标识符前说明它们,能够降低程序错误并增加代码的效率。
2.1.2.1 变量
变量是程序代码中代表一个内存地址的标识符,而此地址的内存内容在程序代码运行时能够被改变。在使用变量前必须对它进行说明,即对它进行命名,并说明它的类型。在全部变量说明曾经加上保留字var。变量说明左边是变量的名称,右边则是该变量的类型,中间用(:)隔开。
var
Value ,Sum : Integer;
Line : String;
在窗口中增加一个名称为Edit1的编辑框,再增加一个名称(属性Name)为Add的button部件,并建立例如以下的事件处理过程:
procedure TForm1.addClick(Sender: TObject);
var
X , Y: Integer;
begin
X := 100;
Y := 20;
Edit1.Text := IntToStr(X + Y);
end;
在本例中,当按动ADDbutton时,编辑框中显示值120。在Object Pascal中,必须确保变量或属性被赋予类型同样或兼容的值。您能够尝试将赋给X的值改为100.0,或去掉IntToStr函数,在编译时会出现类型不匹配的错误,这也说明了Object Pascal强类型语言的特点。
2.1.2.2 提前定义类型
Object Pascal有多个提前定义的数据类型,您能够说明不论什么这些类型的变量:
整形:Integer的范围是-32768到32767,占2字节的内存;Shortint从-128到127,占1字节内存;Longint从-2147443648到2147483647 占4字节内存;Byte从0到255,占1字节;Word从0到65535,占2字节内存。它们都是没有小数部分的数字。
实型:Single能够包括7到8位有效小数部分,占用4字节的内存;Double类能够包括15到16位有效小数部分,占用8字节的内存;Extended类型包括19到20位有效小数部分,占用10字节内存;Comp能够包括19到20位有效小数部分,占用8字节内存。以上实数类型仅仅有在8087/80287选项[N+]打开才干够使用。Real能够包括11到12位有效小数部分,占用6字节内存。它仅仅有在和曾经Borland Pascal兼容的情况下才使用,否则应使用Double或Extended。
布尔型:Boolean,仅仅包括true或False两个值,占用1字节内存。
字符型:Char,一个ASCII字符;字符串类型String一串最长可达255个ASCII字符。
指针型:Pointer,能够指向不论什么特定类型。
字符串型:PChar,是一个指向以零结尾的字符串的指针。
除了提前定义类型外,Delphi还有自行定义的类型。上述例程的TColor就是这样的类型。此外,用户还能够定义自己的数据类型,这部分内容将在下文中具体讲述。
整型类别和实型类别都各有五种类型,同一类别中,全部的类型与其它同类别的都相容,您能够将一种类型的值赋给同样类别中不同类型的变量或属性,而仅仅须要这个值的范围在被赋值的变量或属性的可能值范围内。比如,对于一个Shortint型的变量,能够接受在-128到127范围内的随意整数,比如Shortint类型的7;您不能将300赋给它,由于300已经超出了Shortint的范围了。将范围检查功能打开(选用Options|Project,并在Compiler Options Page中选择Range Checking),将会检查出一个范围错误;假设Range Checking没有被打开,那么程序代码将能够运行,但被赋值的值将不是您期望的值。
在一些情况下,您能够进行不同类型的变量或属性的赋值。一般来说,能够将一个较小范围的值赋给一个较大范围的值。比如,您能够将整型值10赋给一个接受实型值的Double属性而使得值成为10.0,但假设将一个Double类型的值赋给整形变量,则会出现类型错误。假设您不清晰类型的兼容性,能够參阅Delphi的在线帮助中“Type Compatibility and Assignment Compatibility”主题。
2.1.2.3 常量
常量在说明时就被赋予了一个值,在程序运行过程中是不可改变的。以下的样例说明了三个常量:
const
Pi = 3.14159;
Answer = 342;
ProductName = "Delphi";
象变量一样,常量也有类型。不同的是,常量假设其类型就是常量说明中其所代表的值的类型。上文的三个常量的类型各自是real型、整形、字符串型。常量用“= " 表示两边的值是相等的。
2.1.3 过程与函数
过程与函数是程序中运行特定工作的模块化部分。Delphi的运行库包括很多过程与函数以供您的应用程序调用。您不必了解过程与函数的逻辑,但要知道过程与函数的用途。在对象中说明的过程和函数称为方法(Method)。全部的事件处理过程都是过程,以保留字procedure开头。每个事件处理过程仅仅包括了当这一事件发生时须要运行的程序代码。在事件处理过程中使用Delphi已经存在的过程与函数,仅仅需在程序代码中调用它们就可以。
2.1.3.1 一个调用Delphi方法的简单例程
下文将通过对一个Memo部件的文本进行剪切、拷贝、粘贴、清除等编辑的应用程序编制,介绍使用Delphi过程和函数的调用方法。
Memo(备注)部件有一个CutToClipboard方法,实现将用户在memo中选择的文本移到剪贴板上去。由于这个功能已经被建立在此方法中了,所以您仅仅需知道这种方法做什么以及怎样使用它就可以。
以下的语句表明怎样调用一个名为Memo1的memo部件的CutToClipboard方法:
Memo1.CutToClipboard;
通过指定Memo1的名称,说明调用哪一个部件的CutToClipboard方法。假设不指明对象名称,Delphi会显示Unknown identifier错误。当该事件处理过程被触发,程序会运行CutToclipboard中的语句,将Memo1中的文本剪贴到剪贴板上去。
下文的例程展示了怎样调用Delphi的方法,实现将备注部件的文本信息剪切、复制到剪贴板上;将剪贴板上的标记文本粘贴到备注中,清除备注部件中的全部文本等四个功能。
打开一个新的空窗口,增加一个memo部件和四个button,并排列整齐。改变button部件的Name属性,分别命名为Cut,Copy,Paste,Clear。您会发现,当Name属性发生改变时,Caption属性将发生对应的变化。在Caption属性前加标“&”号设立加速键
将memo部件的ScrollBars属性设为ScVertical,以便加上滚行条。将WordWrap属性设置为True,这样当用户输入文本到达Memo部件的右边缘时会自己主动回行。将Line属性第一行的Memo1文本删除,使得memo部件在初始显示时为空的。
为每个button建立例如以下的事件处理过程:
procedure TForm1.CutClick(Sender: TObject);
begin
Memo1.CutToClipboard;
end;
procedure TForm1.CopyClick(Sender: TObject);
begin
Memo1.CopyToClipboard;
end;
procedure TForm1.PasteClick(Sender: TObject);
begin
Memo1.PasteFromClipboard;
end;
procedure TForm1.ClearClick(Sender: TObject);
begin
Memo1.clear;
end;
运行此程序。您能够在备注部件中输入文本,在进行了文本的标记后,能够随意地进行剪切、拷贝、粘贴和清除。当button被按动时,就调用对应的过程进行处理。用户能够通过查阅在线帮助进行Memo部件的Topic Search,在Memo Component项中查阅Method,会得到以上过程的具体说明。
2.1.3.2 调用Delphi的含參过程
有些过程要求用户指明參数。被调用的过程会在运行时使用传入的參数值,这些值在过程中被觉得是已经被说明的变量。比如,LoadFromFile方法在TString对象中被说明为:
Procedure LoadFromFile(const FileName: String);
在调用这一过程时,应指明FileName參数是要装入的文件名。以下的程序将先打开Open对话框,当您选择了一个文件后,Delphi将把该文件读入一个Memo部件:
begin
OpenDialog.Execute;
Memo1.lines.LoadFromFile(OpenDialog.FileName);
end;
2.1.3.3 使用Delphi函数
与过程一样,函数的程序代码也运行特定的工作。它和过程的差别为:函数运行时会返回一个值,而过程则没有返回值。函数能够用来赋给一个属性或变量;也能够使用返回值来决定程序的流程。
前文中我们实际上已经接触过了函数。在讲述变量时,曾用到过以下的程序段: Edit1.Text := IntToStr(X + Y);当中,IntToStr(Value)把一个LongInt类型的数值转化为字符串的值,Value是IntToStr唯一的參数,它能够是一个整形的值、变量、属性或产生整形值的表达式。调用函数,必须把返回值赋给和此返回值类型兼容的变量或属性。
有些函数返回一个True或False的布尔量,用户的程序能够依据返回值来决定跳转。下文的例程讲述了函数返回值为Boolean的推断使用方法:
在窗口中增加一个ColorDialog对象和一个Name属性为ChangeColor的button。为button的OnClick事件建立事件处理步骤例如以下:
procedure TForm1.ChangeColorClick(Sender: TObject);
begin
if ColorDialog1.Execute then
Form1.Color := ColorDialog1.Color
else
Form1.Color := clRed;
end;
此事件处理过程使用一个返回Boolean值的Execute方法。按动button,并在颜色对话框中选择一个颜色。假设按动OKbutton,ColorDialog.Execute方法将返回True,则Form1.Color将被赋值为ColorDialog1.Color,窗口显现您选用的颜色;假设按动颜色对话框的Cancelbutton,方法将返回False值,窗口将变为红色。
2.1.4 跳转语句
Object Pascal的跳转语句有if和case两个。
2.1.4.1 if语句
if语句会计算一个表达式,并依据计算结果决定程序流程。在上文的例程中,依据ColorDialog.Execute的返回值,决定窗口的背景颜色。if保留字后尾随一个生成Boolean值True或False的表达式。一般用“=”作为关系运算符,比較产生一个布尔型值。当表达式为True时,运行then后的语句。否则运行else后的代码,if语句也能够不含else部分,表达式为False时自己主动跳到下一行程序。
if语句能够嵌套,当使用复合语句表达时,复合语句前后需加上begin…end。else保留字前不能加“;”,并且,编译器会将else语句视为属于最靠近的if语句。必要时,须使用begin…end保留字来强迫else部分属于某一级的if语句。
2.1.4.2 case语句
case语句适用于被推断的变量或属性是整形、字符型、枚举型或子界型时(LongInt除外)。用case语句进行逻辑跳转比编写复杂的if语句easy阅读,并且程序代码整形较快。
以下的例程显示一个使用case语句的窗口:
建立例如以下的事件处理过程:
procedure TForm1.Button1Click(Sender: TObject);
var
Number : Integer;
begin
Number := StrToInt(Edit1.Text);
case Number of
1,3,5,7,9: Label2.Caption := '奇数';
0,2,4,6,8: Label2.Caption := '偶数';
10..100:
begin
Label2.Caption := '在10到100之间';
Form1.Color := clBlue;
end;
else
Label2.Caption := '大于100或为负数';
end;
end;
运行程序,当Edit1部件接受到一个值,并按动“OK”button触发程序后,Number便被赋值为用户输入的数值。case语句依据Number的值推断该运行哪一条语句。象if语句一样。case语句也有可选择的else部分。case语句以end结尾。
2.1.5 循环语句
Object Pascal的循环语句有三种:repeat、while和for语句。
2.1.5.1 repeat语句
repeat语句会反复运行一行或一段语句直到某一状态为真。语句以repeat開始,以until结束,其后尾随被推断的布尔表达式。參阅以下的例程:
i := 0;
repeat
i := i+1;
Writen(i);
until i=10;
当此语句被运行时,窗口的下方会出现1到10的数字。布尔表达式 i=10 (注意,与其它语言不同的是,“=”是关系运算符,而不能进行赋值操作)直到repeat..until程序段的结尾才会被计算,这意味着repeat语句至少会被运行一次。
2.1.5.2 while语句
while语句和repeat语句的不同之处是,它的布尔表达式在循环的开头进行推断。while保留字后面必须跟一个布尔表达式。假设该表达式的结果为真,循环被运行,否则会退出循环,运行while语句后面的程序。
以下的例程达到和上面的repeat例程达到同样的效果:
i := 0;
while ibegin
i := i+1;
writeln(i);
end;
2.1.5.3 for语句
for语句的程序代码会运行一定的次数。它须要一个循环变量来控制循环次数。您须要说明一个变量,它的类型能够是整形、布尔型、字符型、枚举型或子界型。
以下的程序段会显示1到5的数字,i为控制变量:
var
i : integer;
for i := 1 to 5 do
writeln(i);
以上介绍了三种循环语句。假设您知道循环要运行多少次的话,能够使用for语句。for循环运行速度快,效率比較高。假设您不知道循环要运行多少次,但至少会运行一次的话,选用repeat..until语句比較合适;当您觉得程序可能一次都不运行的话,最好选用while..do语句。
2.1.6 程序模块
程序模块在Object Pascal中是非常重要的概念。它们提供了应用程序的结构,决定了变量、属性值的范围及程序运行的过程。它由两个部分组成:可选择的说明部分和语句部分。假设有说明部分,则必在语句部分之前。说明部分包括变量说明、常量说明、类型说明、标号说明、程序,函数,方法的说明等。语句部分叙述了可运行的逻辑行动。
在Delphi中,最常见的程序模块便是事件处理过程中的程序模块。以下的事件处理过程是含有变量说明部分的程序模块:
procedure TForm.Button1Click(Sender Tobject);
var {程序模块的说明部分}
Name : string;
begin {程序模块的语句部分}
Name := Edit1.Text;
Edit2.Text := 'Welcome to Delphi'+Name;
end; {程序模块结束}
库单元也是程序模块。库单元的interface部分含有库函数、类型、私有,公有域的说明,也能够含有常量、变量的说明。这一部分能够作为程序模块的说明部分。在库单元的implementation部分中通常含有各种事件处理过程,它们能够视为模块的语句部分,是事件处理模块。库单元模块结束于库单元结束的end.处。
程序模块中能够包括其它的程序模块。上文库单元模块中含有事件处理模块。而库单元模块实际是在project程序模块中。
全部的Delphi应用程序都有同样的基本结构。当程序逐渐复杂时,在程序中增加模块就可以。比如在库单元模块中增加事件处理模块,向project中增加库单元模块等。模块化编程使得程序结构良好,并且对数据具有保护作用。
2.1.7 关于作用范围
2.1.7.1 标识符的作用范围
一个变量、常量、方法、类型或其它标识符的范围定义了这个标识符的活动区域。对于说明这个标识符的最小程序模块而言,此标识符是局部的。当您的应用程序在说明一个标识符的程序模块外运行时,该标识符就不在此范围内。这意味着此时运行的程序无法訪问这个标识符,仅仅有当程序再度进入说明这个标识符的程序模块时,才干够訪问它。
以下的示意图表示一个含有两个库单元的project,每个库单元中又各有三个过程或事件处理过程。
2.1.7.2 訪问其它程序模块中的说明
您能够在当前的程序模块中訪问其它程序模块中的说明。比如您在库单元中编写一个事件处理过程来计算利率,则其它的库单元能够訪问这个事件处理过程。要訪问不在当前库单元中的说明,应在这个说明之前加上其它应用程序的名称和一个点号(.)。比如,在库单元Unit1中有事件处理过程CalculateInterest过程,如今您想在库单元Unit2中调用这一过程,则能够在Unit2的uses子句中增加Unit1,并使用以下的说明:
Unit1.CalculateInterest(PrincipalInterestRate : Double);
应用程序的代码不能在一个模块外訪问它说明的变量。事实上,当程序运行跳出一个模块后,这些变量就不存在于内存中了。这一点对于不论什么标识符都是一样的,无论事件处理过程、过程、函数还是方法,都具有这一性质。这样的标识符称为局部变量。
2.1.7.3 依照作用范围说明标识符
您能够在应用程序的不同地方说明一个标识符,而仅仅需保证它们的有效范围不同就可以。编译器会自己主动訪问最靠近当前范围的标识符。
库单元的全局变量一般能够说明在保留字implementation后面。比如,以下的例程实现将两个编辑框中的整数相加,显示在第三个编辑框中。用到了一个整形的全局变量Count:
…implememntation
var
Count : Integer;
procedure TForm1.AddClick(Sender:TObject);
var
FirstNumber,SecondNumber:Integer;
begin
Count := Count + 1;
Counter.Text := IntToStr(Count);
FirstNumber := StrToInt(Edit1.Text);
SecondNumber := StrToInt(Edit2.Text);
Edit3.Text := IntToStr(FirstNumber+SecondNumber);
end;
…
为了实现每按动一次buttonCount增加一次,必须对全程变量Count进行初始化处理。在程序库单元的结尾处,最后一个end.保留字之前,增加保留字initialization和初始化Count的代码:
…
initialization
Count := 0;
这样当事件处理过程AddClick被触发时,Count就会被增加一次,以表征计算次数。假设用面向对象编程,则Count能够说明成窗口的一个域,这在下一节中将有讲述。
2.1.8 编写一个过程或函数
在您开发Delphi应用程序时,所需的大部分代码都编写在事件处理过程中,但有时仍然须要编写不是事件处理过程的函数或过程。比如,您能够把在多个事件处理过程中用得到语句编写成过程,然后不论什么事件处理过程、过程、函数都能够象调用已经存在的过程或函数一样直接调用它。长处是您仅仅需编写一次代码,并且程序代码会比較清晰。
2.1.8.1 一个自行编写的函数例程
在上文两个数相加的程序中,假设编辑框中无值,则会使得程序出错中断。为避免这样的情况,编写以下的函数,检查编辑框中是否有值,如无值,则提醒用户输入:
function NoValue(AnEditBox:TEdit):Boolean;
begin
if AnEditBox.Text='' then
begin
AnEditBox.Color := clRed;
AnEditBox.Text := '请输入整数值';
Result := True;
end
else
begin
AnEditBox.Color := clWindow;
Result := False;
end;
end;
NoValue函数会检查编辑框是否为空,假设是,编辑框颜色变红,并提醒用户输入一个整数,然后函数返回真值;Result保留字在Delphi中用来专指函数返回值。在上文的例程中增加NoValue函数:
procedure TForm1.AddClick(Sender: TObject);
var
FirstNumber,SecondNumber : Integer;
begin
if NoValue(Edit1)or NoValue(Edit2) then
exit;
Count := Count + 1;
Counter.Text := IntToStr(Count);
FirstNumber := StrToInt(Edit1.Text);
SecondNumber := StrToInt(Edit2.Text);
Edit3.Text := IntToStr(FirstNumber+SecondNumber);
end;
假设当中的不论什么一个返回真值,则表示有编辑框空,会运行exit过程,使得当前的程序模块停止运行,并使得编辑框出现输值提示。当新值被输入后,再运行程序时,红色提示被隐去,恢复正常的计算状态。
2.1.8.2 过程和函数的标题
每个过程或函数都以标题開始,当中包括过程或函数的名称和它使用的參数。过程以保留字procedure開始,函数以保留字function開始。參数位于括号中面,每个參数以分号分隔。比如:
procedure validateDate(Day : Integer; month : Integer; Year : Integer);
您也能够将同样类型的參数组合在一起,则上述过程头写作:
procedure ValidateDate(Day, Month, Year : Integer);
函数在标题中还多了一项:返回值的类型。以下是一个返回值为Double型的函数标题:
function CalculateInterest(principal,InterestRate:Double):Double;
2.1.8.3 函数和过程中的类型说明
一个过程或函数程序模块也含有说明部分和语句部分。说明部分能够包括类型说明、变量说明、常量说明等。除了Object Pascal语言中已经定义的类型之外,Delphi的应用程序还能够建立新的数据类型。类型说明部分有保留字type開始。以下是一些类型的说明:
type
Tcount = Integer;
TPrimaryColor = (Red,Yellow,Blue);
TTestIndex = 1..100;
TTextValue = -99..99;
TTestList = array [TTestIndex] of TTestValue;
TCharVal = Ord('A')..Ord('Z') ;
Today = (Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,
Sunday) ;
在类型标识符后面,用“=”号定义了新的类型。类型界定了变量的取值范围,比如,TCount类型的变量必须是整形值;一个TPrimaryColor类型的变量仅仅能是red、yellow或blue等等。每个类型的名称都是由字母T開始,这并不是必须的,但它是Delphi的惯例,在差别类型名和标识符时非常实用。类型说明能够是局部的,也能够是全局的。假设您把它放在implementation后面,则表明对于库单元来讲,它是全局的,全部的事件处理过程和其它的过程、函数都能够调用它。假设类型是在过程中被说明的,则是局部的,离开这一过程,该类型将失效。
一般来讲,在过程和函数中,不论什么类型说明都在变量说明之前,而不论什么变量说明都在常量之前。可是,仅仅要遵从说明必须在过程与函数的标题之后,并且在程序代码之前,即是有效的。
2.1.8.4 过程和函数的语句部分
过程或函数的语句部分由begin開始,end结束。函数须要一个返回值。能够将返回值赋给函数名称,也能够将返回值赋给Result变量。以下的例程将返回值赋给函数名称:
function CalculateInterest(Principal,InterestRate: Double):Double;
begin
CalculateInterest := Principal * InterestRate;
end;
将返回值赋给Result变量也是能够的,则上面的程序改为:
Result := Principal*InterestRate;
以下是这个函数的调用方法:
InterestEarned :=CalculateInterest(2000,0.012);
在Implementation后面的过程和函数,能够且仅仅能被此库单元的事件处理过程使用。要让过程和函数能够被其它的程序库单元使用,则须要将过程或函数的标题部分放在库单元中的interface部分,而把含标题的整个过程或函数放在库单元的inplementation部分,并在要訪问这个过程或函数的库单元的uses子句中增加说明这个过程或函数的库单元名称。
2.1.8.5 函数的递归调用
在Object Pascal中,过程或函数必须先说明再调用。上文的NoValue函数必须在使用它的事件处理过程之前说明和运行,否则程序会报告一个未知标识符的错误。
以上规则在递归调用时是例外情况。所谓递归调用,是指函数A调用函数B,而函数B又调用函数A的情况。在递归调用中,函数要进行前置,即在函数或过程的标题部分最后加上保留字forword。下文的例程是一个递归调用的典型样例:
…
implementation
var
alpha:Integer;
procedure Test2(var A:Integer):forword;
{Test2被说明为前置过程}
procedure Test1(var A:Integer);
begin
A :=A-1;
if A>0 then
test2(A); {经前置说明,调用未运行的过程Test2}
writeln(A);
end;
procedure Test2(var A:Integer);{经前置说明的Test2的运行部分}
begin
A :=A div 2;
if A>0 rhen
test1(A); {在Test2中调用已运行的过程Test1}
end;
procedure TForm1.Button1Click(Sender:TObject);
begin
Alpha := 15; {给Alpha赋初值}
Test1(Alpha); { 第一次调用Test1,递归開始}
end;
button的OnClick事件处理过程给Alpha赋初值,并实现先减1再除2的循环递归调用,直到Alpha小于0为止。
2.1.8.6 过程和函数的參数
当您的程序代码在调用一个过程或函数时,通经常使用參数传递数据到被调用的过程或函数中。最经常使用的參数有数值參数、变量參数和常量參数三种。
由被调用过程或函数定义的參数为形參,而由调用过程或函数指明的參数叫实參。在NoValue函数中,说明函数体中的AnEditBox是形參,而调用时在if NoValue(Edit1)…中,Edit1是实參。
数值參数在运行过程中仅仅改变其形參的值,不改变事实上參的值,即參数的值不能传递到过程的外面。试看以下的例程:
procedure Calculate(CalNo:Integer);
begin
CalNo := CalNo*10;
end;
用以下例程调用Calculate函数:
…
Number := StrToInt(Edit1.Text);
Calculate(Number);
Edit2.Text := IntToStr(Number);
…
Number接受由编辑框1输入的数值,经Calculate过程运算。它是一个数值型实參。在进入Calculate函数后,会把Number实參拷贝给形參CalNo,在过程中CalNo增大十倍,但并未传递出来,因此Number值并未改变,在编辑框2中显示仍然是编辑框1中的输入值。形參和实參占用不同的内存地址,在过程或函数被调用时,将实參的值复制到形參占用的内存中。因此出了过程或函数后,形參和实參的数值是不同的,但实參的值并不发生变化。
假设您想改变传入的參数值,就须要使用变量參数,即在被调用程序的參数表中的形參前加上保留字var。比如:
procedure Calculate(var CalNo : Integer);
则CalNo并不在内存中占领一个位置,而是指向实參Number。当一个变參被传递时,不论什么对形參所作的改变会反映到实參中。这是由于两个參数指向同一个地址。将上一个例程中过程头的形參CalNo前面加上var,再以同样的程序调用它,则在第二个编辑框中会显示计算的结果,把第一个编辑框中的数值放大十倍。这时形參CalNo和实參Number的值都是Nnmber初始值的10倍。
假设当过程或函数运行是要求不改变形參的值,最保险的办法是使用常量參数。在參数表的參数名称前加上保留字const能够使一个形參成为常量參数。使用常量參数取代数值參数能够保护您的參数,使您在不想改变參数值时不会意外地将新的值赋给这个參数。
2.1.9 定义新的数据类型
Object Pascal有一些系统提前定义的数据类型,在2.1.2中已经对它们作了介绍。您能够利用这些数据类型以建立新的数据类型来满足程序的特定须要。以下简单地叙述了您能建立的主要数据类型,如枚举型、子界型、数组型、集合型、记录型、对象型等。
2.1.9.1 枚举类型
一个枚举型的说明列出了全部这样的类型能够包括的值:
type
Tdays=( Sunday ,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday);
能够定义上述枚举类型的变量:
var
DayOfWeek:TDays;
在枚举型中,括号中的每个值都有一个由说明它的位置决定的整形值。比如Sunday有整形值0,Monday有整形值1等。您能够把DayOfWeek说明为一个整形变量,并将一星期的每一天赋一个整形值以达到同样的效果,但用枚举型会使得程序可读性好,编写easy。当您在枚举型中列出值时,您同一时候说明了这个值是一个标识符。比如您的程序中假设已经含有TDays类型且说明了DayOfWeeks变量,则程序中便不能使用Monday变量,由于它已经被说明为标识符了。
2.1.9.2 子界类型
子界型是下列这些类型中某范围内的值:整形、布尔量、字符型或枚举型。在您想限制一个变量的取值范围时,子界型是非常实用的。
type
Thours = 0..23;
TValidLetter = 'A' .. 'F';
TDays = ( Sunday ,Monday,Tuesday,Wednesday,Thursday,
Friday,Saturday); {枚举型}
TWorkDay = Monday..Friday; {一个TDays型的子界}
子界型限定了变量的可能取值范围。当范围检查打开时,(在库单元的Implementation后面有{$R*.DFM}字样表示范围检查打开,否则您能够在Options|Project|Complier Options中选择Range Cheking来打开范围检查),假设变量取到子界以外的值,会出现一个范围检查错误。
2.1.9.3 数组类型
数组是某种数据类型的有序组合,当中每个元素的值由其相对位置来指定,您能够在数组的某个位置上放置数据,并在须要时使用这些数据。以下的类型说明了一个Double型的数组变量:
var
Check : array [1..10] of Double;
它表示Check指向一个含有10个Double型元素的数据串列,代表每个元素的是1到10之间的数字,称为索引。数组的每一项由数组名称加上[]中的索引来表示。Check包括10个变量,Check[1]表示第一个变量。您也能够把数组定义成类型:
type
TCheck = array[1..10] of Double;
则变量说明改为:
var
Check :TCheck;
您能够通过给数组赋值等方法来使用数组。以下的语句将0.0赋给Check数组中的全部元素:
for J := 1 to 10 do
Check[J] := 0.0;
数组也能够是多维的,以下的类型定义了一个20行、20列的数组。
type
Ttable = array[1..20,1..20] of Double;
var
table1:TTable;
想将这一表格的全部数据初始化为0.0,您能够使用for循环:
var
Col,Row:Integer;
…
for Col :=1 to 20 do
for Row := 1 to 20 do
Table1[Col,Row] := 0.0;
2.1.9.4 字符串类型
字符串类型事实上是一个一维的字符数组。当您说明一个字符串型的变量时,您应当指明这个字符串的大小,以下是说明字符串类型的样例:
type
MyString: string[15];
var
MyName: MyString;
则变量MyName被说明成为最多能够包括15个字符。假设您没有说明字符串的大小,Delphi会觉得字符串包括最大值255个字符。给字符串赋值能够直接使用单引號括起的字串赋值:
MyName := 'Frank.Smith';
或MyName := '张明';
由于MyName是一个能够包括15个字符的MyString型变量,上文的两个的变量都是有效的,一个汉字能够视作两个字符。当您给字符串型变量赋的值多于定义数值时,比如将MyName赋为‘FrankSmith.Franklin’,则Delphi仅仅会接受前15个字符‘FrankSmith.Fran’。在内存中,字符串通常占用比所说明的大小多一个字节的空间,由于第一个位置是一个包括这个数组大小的字节。您能够使用索引值来訪问字符串的字符,MyName[1]能够得到MyName的第一个字符'F'。
您能够使用Delphi丰富的运算符、过程和函数来处理字符串型的变量和属性。以下介绍几个经常使用的运算符和Delphi过程或函数:
Concat和(+)功能同样,都能够将多个字符串组合在一起,建立一个较大的字符串;Copy会返回一个字符串中的子字符串;Delete在一个字符串中从一个指定位置起删除一定数目的字符;Insert在一个字符串中插入一个字符串;Length返回字符串的长度;Pos返回一个子字符串在一个字符串中的位置,即索引值。
2.1.9.5 集合类型
集合类型是一群同样类型元素的组合,这些类型必须是有限类型如整形、布尔型、字符型、枚举型和子界型。在检查一个值是否属于一个特定集合时,集合类型非常实用。以下的例程能够说明集合类型的使用方法:
在窗口上增加一个编辑框和一个button,清除编辑框中的文字,在其上加上Caption为“输入元音”的标签Label,并在编辑框的下方增加一个空的标签,将button的Default属性改为True,建立button的事件处理步骤例如以下:
procedure TForm1.Button1Click(Sender:TObject);
type
Tvowels=set of Char;
var
Vowels:TVowels;
begin
Vowels := ['a','e','i','o','u'];
if Edit1.Text[1] in Vowels then
Lable2.Caption := '是元音';
else
Lable2.Caption := '请再试';
end;
运行这个程序,在编辑框中输入字母,表达式Edit1.Text[1] in Vowels的结果是布尔型的,in是运算符,用来推断字母是否存在于集合中。输入的判别结果会显示在编辑框的下方。以上就用到了集合类型TVowels。
2.1.9.6 记录类型
记录是您的程序能够成组訪问的一群数据的集合。以下的例程说明了一个记录类型的使用方法:
type
TEmployee=record
Name : string[20];
YearHired:1990..2000;
Salsry: Double;
Position: string[20];
end;
记录包括能够保存数据的域,每个域有一个数据类型。上文的记录TEmployee类型就含有四个域。您能够用以下的方式说明记录型的变量:
var
NewEmployee,PromotedEmployee:TEmployee;
用例如以下的方法能够訪问记录的单域:
NewEmployee.Salary := 1000;
编写例如以下的语句能够给整个记录赋值:
with PromotedEmployee do
begin
Name :='';
YearHired := 1993;
Salary := 2000.00
Position := 'editor';
end;
您的程序能够将记录当成单一实体来操作:
PromptEmployee := NewEmployee;
以上介绍了用户经常使用的自己定义类型。在Delphi的编程中,对象是非常重要的用户自己定义数据类型。象记录一样,对象是结构化的数据类型,它包括数据的域(Field),也包括作为方法的过程和函数。在Delphi中,当您向窗口中增加一个部件,也就是向窗口对象中增加了一个域;每个部件也是对象,每当您建立一个事件处理过程使得部件能够响应一个事件时,您即自己主动地在窗口中增加了一个方法。在本章第2节中,将具体讲述Delphi面向对象编程的方法和技巧。
2.1.10 Object Pascal的库单元Unit
Units是常量、变量、数据类型、过程和函数的集合,并且能够被多个应用程序所共享。Delphi已经拥有很多提前定义的程序库单元可供您建立您的程序库单元使用。Delphi的Visual Component Library由多个程序库单元组成,它们说明了对象、部件以供您的应用程序用来设计用户界面。比如,当您在窗口中增加一个Check Box时,Delphi自己主动在您的程序库单元中增加了Stdctrls库单元,由于TCheckBox部件是在StdCtrls库单元中说明的。
当您设计您的窗口时,Delphi自己主动建立一个和您的窗口有关的库单元。您的库单元不必都和窗口有关,也能够使用提前定义的仅仅包括数学运算函数的库单元,或是自行编写数学函数库单元。在一个库单元中全部的说明都相互有关系,比如,CDialogs程序库单元包括了在您的应用程序中使用的普通对话框的全部说明。
2.1.10.1 Object Pascal程序库单元的结构
无论一个库单元是否和一个窗口有关,库单元的结构都是同样的。其结构例如以下:
unit
interface
uses
{公有说明}
implementation
uses
{私有说明}
{过程和函数的运行部分}
initialization {选择性的}
{选择性的初始化程序}
end.
2.1.10.2 程序库单元的接口部分
interface是库单元的接口部分,它决定了本库单元对其它不论什么库单元或程序的可见(可訪问)部分。您能够在接口部分说明变量、常量、数据类型、过程和函数等等。Delphi在您设计窗口的库单元中,将窗口数据类型、窗口变量和事件处理过程都说明在这一部分。
interface标志库单元接口部分的開始。在interface中的说明对要使用这些说明的其它库单元或应用程序是可见的。一个库单元能够使用其它Unit的说明,仅仅须要在uses子句中指明那些库单元就可以。比如,您在库单元A中编敲代码代码,且您想调用UnitB于interface部分说明的程序。您能够把库单元B的名称增加到A的interface部分的uses子句中,则不论什么A中的程序都能够调用B中说明的程序。并且,假设B中interface部分的uses子句中出现C库单元,尽管A中未曾出现C,A同样能够调用B、C库单元在interface中说明的程序。但假设B出如今A的interface部分的uses子句中,那么库单元A便不能出如今B的interface的uses子句中。由于这样会产生对库单元的循环訪问。当试图编译时,会产生出现错误信息。
2.1.10.3 程序库单元的实现部分
实现部分implementation中包括interface中说明的过程、函数、事件处理过程的具体实现程序代码。这一部分能够有自己的额外说明,但这些说明是私有的,外部程序不能调用这些说明。在interface中说明的函数实体必须在implementation部分出现,能够使用标题简写:仅仅输入procedure或function保留字,后面跟过程或函数的名称就可以,其后则是程序的实现部分了。假设您在implementation部分说明不论什么常式,其标题并未出如今interface部分,则必须写全其标题部分。
在implementation部分的uses子句中指定的库单元,仅仅供给本库单元的程序使用其interface中说明的程序。其它使用本库单元的库单元,不能訪问这些在implementation的udes子句中库单元的说明,由于在implementation后进行的库单元包括是私有的。所以上例中,假设C出如今B的implementation部分,则A不能使用C的公有部分,除非C出如今A的uses子句中。在implementation中出现的循环訪问是Delphi所同意的,假设A的implemetation的uses子句中出现B,则B的implementation部分也能够出现A。
2.1.10.4 程序库单元的初始化部分
初始化当前库单元所使用的数据,或是通过interface部分将数据提供给其它应用程序、库单元使用时,您能够在库单元中增加一个initialization部分,在库单元的end前加上您的初始化语句。当一个应用程序使用一个库单元时,在库单元中的initialization部分会先于其它的代码运行。假设一个应用程序使用了多个库单元,则每个库单元的初始化部分都会在全部的程序代码前运行。
2.1.10.5 使用Delphi的可视化部件及其库单元
当您在窗口中增加可视化部件时,假设该部件在可视化部件库中,Delphi会在您的库单元的interface部分的uses子句中自己主动加上须要使用的库单元名称。但有些对象在Delphi的环境中并没有可视化部件存在,比如,您想在库单元中增加一个提前定义的信息框,则您必须把MsgDlg库单元增加您的uses子句中。假设您要使用TPrinter对象的话,必须将Printer库单元增加uses子句中。在在线帮助中能够查到对象所属的提前定义库单元。
要使用在其它库单元中说明的函数,应在函数的前面加上这一库单元的名称,并用‘.’号隔开。比如,要在Unit2中使用Unit1中说明的Calculate函数,应使用以下的方法:
Number := Unit1.Calculate(10);
您能够在不论什么标识符如属性、常量、变量、数据类型、函数等之前加上库单元的名称。您能够在*地在不论什么Delphi库单元中增加程序代码,但不要改变由Delphi生成的程序。
2.1.10.6 建立与窗口无关的新库单元
假设您想在project中建立一个和不论什么窗口无关的新库单元,能够现选用File|New Unit。这时一个新的库单元增加了project,新库单元的代码例如以下:
unit Unit2;
interface
implementation
end.
Delphi将依据您的project中的文件数目为您的库单元选择名称,您能够在程序骨架间增加您的程序代码。
当编译您的project时,这个新增加的库单元会被编译为一个具有.DCU后缀的文件。这个新生成的文件是链接到project的可运行文件上的机器代码。
2.1.10.7 将库单元增加project
将库单元增加project是比較简单的。无论是您自己建立的库单元还是Delphi建立的与窗口有关的库单元,假设已经完毕,则先打开您想增加库单元的project(能够用Open Project打开project);再选用File|Open File,然后选择您想增加的源程序(.PAS文件),并选择OK就可以。则库单元被增加到应用程序中。
2.2 用Delphi的对象进行编程
Delphi是基于面向对象编程的先进开发环境。面向对象的程序设计(OOP)是结构化语言的自然延伸。OOP的先进编程方法,会产生一个清晰而又easy扩展及维护的程序。一旦您为您的程序建立了一个对象,您和其它的程序猿能够在其它的程序中使用这个对象,全然不必又一次编制繁复的代码。对象的反复使用能够大大地节省开发时间,切实地提高您和其它人的工作效率。
2.2.1 什么是对象
一个对象是一个数据类型。对象就象记录一样,是一种数据结构。按最简单的理解,我们能够将对象理解成一个记录。但实际上,对象是一种定义不确切的术语,它经常使用来定义抽象的事务,是构成应用程序的项目,其内涵远比记录要丰富。在本书中,对象可被理解为可视化部件如button、标签、表等。
了解对象,最关键的是掌握对象的特性。一个对象,其最突出的特征有三个:封装性、继承性、多态性。
2.2.1.1 对象的封装性
对对象最基本的理解是把数据和代码组合在同一个结构中,这就是对象的封装特性。将对象的数据域封闭在对象的内部,使得外部程序必需并且仅仅能使用正确的方法才干对要读写的数据域进行訪问。封装性意味着数据和代码一起出如今同一结构中,假设须要的话,能够在数据周围砌上“围墙”,仅仅实用对象类的方法才干在“围墙”上打开缺口。
2.2.1.2 对象的继承性
继承性的含义直接并且显然。它是指把一个新的对象定义成为已存在对象的后代;新对象继承了旧类的一切东西。在往新对象中增加不论什么新内容曾经,父类的每个字段和方法都已存在于子类中,父类是创建子类的基石。
2.2.1.3 对象的多态性
多态性是在对象体系中把设想和实现分开的手段。假设说继承性是系统的布局手段,多态性就是其功能实现的方法。多态性意味着某种概括的动作能够由特定的方式来实现,这取决于运行该动作的对象。多态性同意以相似的方式处理类体系中相似的对象。依据特定的任务,一个应用程序被分解成很多对象,多态性把高级设计处理的设想如新对象的创建、对象在屏幕上的重显、程序运行的其它抽象描写叙述等,留给知道该怎样完美的处理它们的对象去实现。
2.2.1.4 通过Delphi实例了解对象
让我们结合Delphi的实例讨论对象的概念:
当您要建立一个新project时,Delphi 将显示一个窗口作为设计的基础。在程序编辑器中,Delphi将这个窗口说明为一个新的对象类型,并同一时候在与窗口相关联的库单元中生成了创建这个新窗口对象的程序代码。
unit Unit1;
interface
uses SysUtils, Windows, Messages, Classes, Graphics, Controls, Forms, Dialogs;
type
TForm1 = class(TForm) {窗口的类型说明開始}
private
{ Private declarations }
public
{ Public declarations }
end; {窗口的类型说明结束}
var
Form1: TForm1; {说明一个窗口变量}
implementation
{$R *.DFM}
end.
新的窗口类型是TForm1,它是从TForm继承下来的一个对象。它具有对象的特征:含有域或方法。由于您未给窗口增加不论什么部件,所以它仅仅有从TForm类中继承的域和方法,在窗口对象的类型说明中,您是看不到不论什么域、方法的说明的。Form1称为TForm1类型的实例(instance)。您能够说明多个对象类型的实例,比如在多文档界面(MDI)中管理多个子窗口时就要进行这样的说明。每个实例都有自己的说明,但全部的实例却共用同样的代码。
假设您向窗口中增加了一个button部件,并对这个button建立了一个OnClick事件处理过程。再查看Unit1的源程序,会发现TForm1的类型说明部分例如以下:
type
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
如今TForm1对象有了一个名为Button1的域:它是您在窗口中增加的button。TButton是一个对象类型,Button1是Tbutton的一个实例。它被TForm1对象所包括,作为它的数据域。每当您在窗口中增加一个部件时,部件的名称就会作为TFom1的域增加到类型说明中来。在Delphi中,您所编写的事件处理过程都是窗口对象的方法。每当您建立一个事件处理过程,就会在窗口的对象类型中说明一个方法。
当您使用Object Inspector来改变对象(部件)的名称时,这个名称的改变会反映到程序中。比如,在Object Inspector中将Form1的Name属性命名为ColorBox,您会发如今类型说明部分,会将前文的TForm1改为:
TColorBox=class(TForm);
并且在变量说明部分,会说明ColorBox为TColorBox类型的变量,由Delphi自己主动产生的事件处理过程名称会自己主动改为TColorBox.Button1Click;但您自行编写的实现部分的代码却不会被自己主动改动。因此,假设您在改变Name属性前编写了程序,则您必须将事件处理过程中的对象名称进行改变。所以,原先的Form1.Color要改为ColorBox.Color。
2.2.2 从一个对象中继承数据和方法
前面的TForm1类型是非常简单的,由于它仅仅含有域Button1和方法Button1Click。可是在这个窗口上,您能够改变窗口的大小、增加或删除窗口的最大最小化button,或设置这个窗口为MDI界面。对于一个仅仅包括一个域和方法的对象来讲,您并没有看到显式的支持程序。在窗口上单击鼠标或用Object Inspector的上端的Object Selector选中Form1对象,按动F1查阅它的在线帮助,您会在Properties和Method中找到它的继承到的全部属性和方法。这些是在TForm类型中说明的,TForm1是TForm的子类,直接继承了它全部的域、方法、属性和事件。比如窗口的颜色属性Color就是在TForm中说明的。当您在project中增加一个新窗口时,就等于增加了一个基本模型。通过不断地在窗口中增加部件,您就自行定义了一个新的窗口。要自己定义不论什么对象,您都将从已经存在的对象中继承域和方法,建立一个该种对象的子类。比如对象TForm1就被说明为对象TForm的子类,拥有一个窗口部件的基本属性或方法。仅仅有当您在窗口中增加了部件或编写了事件处理过程时,Form1才成为您自己的类型。
一个比較特殊的对象是从一个范围较广或较一般的对象中继承下来的,它是这个特别对象的祖先,这个对象则称为祖先的后代。一个对象仅仅能有一个直接的祖先,可是它能够有很多后代。TForm是TForm1类型的祖先,全部的窗口对象都是TForm的后代。
用F1查阅窗口的在线帮助时,您会发现TForm被称为component(部件)。这是由于全部的部件都是对象。
在这个结构中全部的部件都是对象。部件类型TComponent从TObject类型中继承数据和程序代码,并具有额外的能够用作特殊用途的属性、方法、事件,所以部件能够直接和用户打交道,记录它的状态并存贮到文件里等等。控制类型TControl从TComponent中继承而来,又增加了新的功能,如它能够显示一个对象。在上图中,尽管TCheckBox不是直接由TObject继承来的,可是它仍然有不论什么对象所拥有的属性,由于在VCL结构中,TCheckBox终究还是从TObject 中继承了全部功能的特殊对象,但它还有些自行定义的独到的功能,如能够选择记录状态等。
2.2.3 对象的范围
2.2.3.1 关于对象的范围
一个对象的范围决定了它的数据域、属性值、方法的活动范围和訪问范围。在一个对象的说明部分说明的数据域、属性值、方法都仅仅是在这个对象的范围中,并且仅仅有这个对象和它的后代才干拥有它们。尽管这些方法的实际程序代码可能是在这个对象之外的程序库单元中,但这些方法仍然在这个对象的范围内,由于它们是在这个对象的说明部分中说明的。
当您在一个对象的事件处理过程中编敲代码代码来訪问这个对象的属性值、方法或域时,您不须要在这些标识符之前加上这个对象变量的名称。比如,假设您在一个新窗口上增加一个button和一个编辑框,并为这个button编写OnClick事件处理过程:
procedure TForm1.Button1Click(Sender:Tobject);
begin
Color :=clFuchsia;
Edit1.Color :=clLime;
end;
当中的第一行语句是为整个窗口Form1着色。您也能够编写例如以下:
Form1.Color :=clFuchsia;
但您能够不必加上Form1.,由于Button1Click方法是在TForm1对象的范围里。当您在一个对象的范围中时,您能够省略全部这个对象中的属性值、方法、域之前的对象标识符。可是当您编写第二个语句改变编辑框的底色时,由于此时您想訪问的是TEdit1对象的Color属性,而不是TForm1类型的,所以您须要通过在属性前面加上编辑框的名称来指明Color属性值的范围。假设不指明,Delphi会象第一个语句一样,将窗口的颜色变成绿色。由于Edit1部件是在窗口中的,它是窗口的一个数据域,所以您同样不必指明其从属关系。
假设Edit1是在其它窗口中,那么您须要在编辑框之前加上这个船体对象的名称了。比如,假设Edit1是在Form2之中,那它是Form2说明的一个数据域,并位于Form2的范围中,那么您须要将第二句改为:
Form2.Edit1.Color := clLime;
并且须要把Unit2增加Unit1的uses子句中。
一个对象的范围扩展到这个对象的全部后代。TForm的全部属性值、方法和事件都在TForm1的范围中,由于TForm1是TForm的后代。您的应用程序不能说明和祖先的数据域重名的类型、变量等。假设Delphi显示了一个标识符被反复定义的信息,就有可能是一个数据域和其祖先对象(比如TForm)的一个数据域有了同样的名称。能够尝试改变这个标识符的名称。
2.2.3.2 重载一个方法
您能够重载(Override)一个方法。通过在后代对象中说明一个与祖先对象重名的方法,就能够重载一个方法。假设想使这种方法在后代对象中作和祖先对象中一样的工作可是使用不同的方式时,您就能够重载这种方法。Delphi不推荐您经常重载方法,除非您想建立一个新的部件。重载一个方法,Delphi编译器不会给出错误或警告提示信息。
2.2.4 对象公有域和私有域的说明
当使用Delphi的环境来建立应用程序时,您能够在一个TForm的后代对象中增加数据域和方法,也能够通过直接改动对象类型说明的方法来为一个对象加上域和方法,而不是把一个部件增加窗口或事件处理过程中。
您能够在对象的Public或Private部分增加新的数据域和方法。Public和Private是Object Pascal的保留字。当您在project中增加新的窗口时,Delphi開始建立这个新窗口对象。每个新的对象都包括public和private指示,以便您在代码中增加数据域和方法。在public部分中说明其它库单元中对象的方法也能够訪问的数据域或方法。在private部分的说明有訪问的限制。假设您在private中说明域和方法,那么它在说明这个对象的库单元外是不透明的,并且不能被訪问。private中能够说明仅仅能被本库单元方法訪问的数据域和本库单元对象訪问的方法。过程或函数的程序代码能够放在库单元的implementation部分。
2.2.5 訪问对象的域和方法
当您想要改变一个窗口对象的一个域的某个属性,或是调用它的一个方法时,您必须在这个属性名称或调用方法之前加上这个对象的名称。比如,假设您的窗口上有一个编辑框部件,而您须要在运行中改变它的Text属性,须要编写下列的代码:
Edit1.Text := 'Welcome to Delphi';
同样,清除编辑框部件中选中的文本,能够调用TEdit部件的对应方法:
Edit1.ClearSelection;
假设您想改变一个窗口对象中一个对象域的多个属性或调用多个方法时,使用with语句能够简化您的程序。with语句在对象中能够和在记录中一样方便地使用。以下的事件处理过程在响应OnClick事件时,会对一个列表框作多个调整:
procedure TForm1.Button1Click(Sender:TObject);
begin
ListBox1.Clear;
ListBox1.MultiSelect :=True;
ListBox1.Item.Add('One');
ListBox1.Item.Add('Two');
ListBox1.Item.Add('Three');
ListBox1.Sorted :=Ture;
ListBox1.FontStyle :=[fsBold];
ListBox1.Font.Color :=clPurple;
ListBox1.Font.Name :='Times New Roman';
ListBox1.ScaleBy(125,100);
end;
假设使用了With语句,则程序例如以下:
procedure TForm1.Button1Click(Sender:TObject);
begin
with (ListBox1) do
begin
Clear;
MultiSelect :=True;
Item.Add('One');
Item.Add('Two');
Item.Add('Three');
Sorted :=Ture;
FontStyle :=[fsBold];
Font.Color :=clPurple;
Font.Name :='Times New Roman';
ScaleBy(125,100);
end;
end;
使用with语句,您不必在每个属性或方法前加上ListBox1标识符,在With语句之内,全部的属性或调用方法对于ListBox这个对象而言都是在它的范围内的。
2.2.6 对象变量的赋值
假设两个变量类型同样或兼容,您能够把当中一个对象变量赋给还有一个对象变量。比如,对象TForm1和TForm2都是从TForm继承下来的类型,并且Form1和Form2已被说明过,那么您能够把Form1赋给Form2:
Form2 :=Form1;
仅仅要赋值的对象变量是被赋值的对象变量的祖先类型,您就能够将一个对象变量赋给还有一个对象变量。比如,以下是一个TDataForm的类型说明,在变量说明部分一共说明了两个变量:AForm和DataForm。
type
TDataForm = class(TForm)
Button1:TButton;
Edit1:TEdit;
DataGrid1:TDataGrid;
Database1:TDatabase;
TableSet1:TTableSet;
VisibleSession1:TVisibleSession;
private
{私有域说明}
public
{公有域说明}
end;
var
AForm:TForm;
DataForm:TDataForm;
由于TDataForm是TForm类型的后代,所以Dataform是AForm的后代,因此以下的赋值语句是合法的:
AForm :=DataForm;
这一点在Delphi中是极为重要的。让我们来看一下应用程序调用事件处理过程的过程,以下是一个button部件的OnClick事件处理过程:
procedure TForm1.Button1Click(Sender:TObject);
begin
end;
您能够看到TObject类在Delphi的Visual Component Library的顶部,这就意味着全部的Delphi对象都是TObject的后代。由于Sender是TObject类型,所以不论什么对象都能够赋值给它。尽管您没有看见赋值的程序代码,但事实上发生事件的部件或控制部件已经赋给Sender了,这就是说Sender的值是响应发生事件的部件或控制部件的。
您能够使用保留字is来測试Sender以便找到调用这个事件处理过程的部件或控制部件的类型。Delphi中的一个显示drag-and-drop的DRAGDROP.DPRproject。载入它,能够查阅到DROPFONT.PAS库单元的代码,在Memo1DragOver方法中检查了一个对象变量的类型。在这样的情形下,參数是Source而不是Sender。
procrdure TForm1.Memo1DragOver(SenderSource:TObject;X,Y:integer;
State:TDragState;var Accept:Boolean);
begin
Accept :=Source is TLabel;
end;
Source參数也是TObject类型,Source被赋值为那个被拖曳的对象。用Memo1DragOver方法的目的是确保仅仅有标签能够被拖曳。Accept是布尔型參数,假设Accept为True,那么用户选择的部件能够被拖曳;反之当Accept的值为False时,用户就不能够拖曳选择控制部件。is保留字检查Source是否TLabel的类型,所以Accept仅仅有在用户拖曳一个标签时才为真,并作为变參输出到函数之外。
以下的drag-and-drop展示的Memo1DragDrop事件处理过程中也使用了Source參数。这种方法是为了把Memo部件的字型改变成和放入这个备注控制部件的标签一样的字型:
procedure TForm1.Memo1DragDrop(SenderSource:TObject;
X,Y:Integer);
begin
Memo1.Font := (Source as TLabel).Font;
end;
当您在这个事件处理过程中编写赋值语句时,开发者并不知道用户会放入哪一个标签,仅仅有通过參考这个标签的名称(Source as TLabel)用户才干知道,并把标签类型赋给Memo1.TFont。Source包括了用户拖放控制部件的名称,仅仅有当Source是一个标签时,这个事件处理过程才同意这个赋值发生。
2.2.7 建立非可视化对象
您在Delphi中使用的大部分对象都是您在设计和运行期间能够看见的部件,比如编辑框、button等;一些部件,如通用对话框(Common dialog box)等,在设计时看不见,而在运行时能够看见;另外有些部件,比如计时器(Timer)、数据源(Data Source)部件等,在程序的运行期间没有不论什么可视化的显示,但您却能够在您的应用程序中使用它们。
2.2.7.1说明一个非可视化对象
以下,通过一个简单的样例讲述怎样建立自己的非可视化对象:
您能够用例如以下的方法,建立一个自己的TEmployee非可视化对象:
type
Temployee = class(TObject);
Name := String[25];
Title := String[25];
HourlyPayRate : Double;
function CalculatePayAmount:Double;
end;
在这样的情况下,TEmployee从TObject继承下来,且包括三个域和一个方法。把您建立的类型说明放在库单元中的说明部分,并和窗口说明放在一起。在这个程序库单元的变量说明部分,说明一个新类型的变量:
var
Employee : TEmployee;
2.2.7.2用Create方法建立对象实例
TEmployee仅仅是一个对象类型。除非通过一个构造函数的调用从而被实例取代或创建,否则一个对象并不存储在内存中。构造函数是一个方法,它为新对象配置内存并且指向这个新的对象。这个新的对象也被称为这个对象类型的一个实例。
建立一个对象的实例,须要调用Create方法,然后构造函数把这个实例赋给一个变量。假设您想说明一个TEmployee类型的实例,在您訪问这个对象的不论什么域之前,您的程序代码必须调用Create。
Employee := TEmployee.Create;
Create方法并没有在TEmployee类型中说明,它继承自TObject类型。由于TEmployee是TObject的子类,所以它能够调用Create方法而创建一个TEmployee实例。然后把它赋给Employee变量。在创建了一个这样的对象后,您就能够象使用其它的Delphi对象一样訪问Employee对象了。
2.2.7.3 撤销对象
当您使用完对象后,您应该及时撤销它,以便把这个对象占用的内存释放出来。您能够通过调用一个注销方法来撤销您的对象,它会释放分配给这个对象的内存。
Delphi的注销方法有两个:Destroy和Free。Delphi建议使用Free,由于它比Destroy更为安全,同一时候调用Free会生成效率更高的代码。
您能够用下列的语句释放用完的Employee对象:
Employee.Free;
和Create方法一样,Free方法也是TEmployee从TObject中继承过来的。把您的注销放在try…finally程序模块的finally部分,而把对象的程序代码放在try部分是编程的好习惯。这样,即使您的程序代码在使用对象时发生了异常事件,也会确保您为这个对象分配的内存会被释放。关于异常处理和try…finally程序模块的信息以及建立非可视化对象的样例,在后文中还将细致讲述。
用hook实现dll注入具体解释
baidu
须要一个用来注入的dll(inject.dll)及一个调用程序(caller.exe)
流程:
caller.exe
procedure TestHook;
var pwnd,hChild, hwndInject :hwnd;
msg:tmsg;
begin
//通过窗口标题用FindWindow找到要注入的程序的主窗口句柄pwnd
pwnd := findwindow('Progman',nil);
//用FindwindowEx(hMain,0,nil,nil)找到要处理的子窗口句柄hChild
hChild := findWindowEx(pwnd,0,nil,nil);
//用getwindowThreadProcessid(hChild,nil)找到要注入的线程
dwThreadID := getwindowThreadProcessid(hChild,nil);
//调用 inject.dll的SetInjectHook方法
SetInjectHook(dwThreadID);
//等待消息返回
getmessage(msg,0,0,0);
//找到注入的窗口
hwndInject:= findwindow(nil,'InjectForm');
//发送控制消息,将目标窗口的句柄作为wparam,控制參数以lparam传入
sendMessage( hwndInject, wm_app,hChild,integer(true));
//关闭注入的窗口
sendMessage( hwndInject,wm_close,0,0);
//等待窗口关闭
sleep(500);
//检查是否成功关闭
assert(not iswindow( hwndInject));
//去掉挂钩
setDipsHook(0);
end;
//以下说明 Inject.dll的SetInjectHook的具体操作
在全局定义以下变量
var
g_hhook :Hhook=0;
g_dwThreadidInject :dword=0;
g_hInjectfrm:hwnd;
function SetInjectHook(dwThreadid:DWORD):boolean;
begin
result := false;
//假设线程标志为0则用于去掉钩子,否则进行动态库注入
if dwThreadid0 then
begin
assert(g_hhook=0);
//保存当前线程的ID到 g_dwThreadidInject
g_dwThreadidInject := getCurrentThreadid;
//下一个GetMessage的钩子到目标线程
//GetMsgProc是在以下定义的一个函数,在第一次调用时将自己定义的form在目标线程中创建出来
//这样就能通过这个自己定义的form对目标线程进行进程内控制了
g_hhook := setWindowsHookEx(wh_getMessage,GetMsgProc,hInstance,dwThreadid);
result := g_hhook null;
if result then
//发一个空的信息以便于立即创建这个自己定义form
result := postThreadMessage(dwThreadid, wm_Null,0,0);
//等待半秒钟,以保证调用者能够找到这个刚创建的form
sleep(500);
end else
begin
assert(g_hhook0);
//去掉钩子
result := unHookWindowsHookEx(g_hhook);
g_Hhook := 0;
end;
end;
//定义一个全局的是否第一个消息的标志
var
fFirstTime:boolean = true;
//这个函数用于在收到第一个消息时创建自己定义窗口,以便于远程控制
function GetMsgProc(code: Integer; wparam: WPARAM; lparam: LPARAM): LRESULT; stdcall;
begin
//假设是第一次
if fFirstTime then
begin
fFirstTime := false;
//创建窗口
InjectFrm := TinjectFrm.create(nil);
//保存窗口句柄
g_hInjectfrm := InjectFrm.handle;
end;
//调用默认处理,这一句可不能忘记
result := callNexthookEx(g_hhook,code,wparam,lparam);
end;
《COM 原理与应用》学习笔记 - 第一部分 COM原理
baidu
⊙ 第一章 概述
===================================================
COM 是什么
---------------------------------------------------
COM 是由 Microsoft 提出的组件标准,它不仅定义了组件程序之间进行交互的标准,并且也提供了组件程序运行所需的环境。在 COM 标准中,一个组件程序也被称为一个模块,它能够是一个动态链接库,被称为进程内组件(in-process component);也能够是一个可运行程序(即 EXE 程序),被称作进程外组件(out-of-process component)。一个组件程序能够包括一个或多个组件对象,由于 COM 是以对象为基本单元的模型,所以在程序与程序之间进行通信时,通信的两方应该是组件对象,也叫做 COM 对象,而组件程序(或称作 COM 程序)是提供 COM 对象的代码载体。
COM 对象不同于一般面向对象语言(如 C++ 语言)中的对象概念,COM 对象是建立在二进制可运行代码级的基础上,而 C++ 等语言中的对象是建立在源码级基础上的,因此 COM 对象是语言无关的。这一特性使用不同编程语言开发的组件对象进行交互成为可能。
---------------------------------------------------
COM 对象与接口
---------------------------------------------------
相似于 C++ 中对象的概念,对象是某个类(class)的一个实例;而类则是一组相关的数据和功能组合在一起的一个定义。使用对象的应用(或还有一个对象)称为客户,有时也称为对象的用户。
接口是一组逻辑上相关的函数集合,其函数也被称为接口成员函数。依照习惯,接口名常是以“I”为前缀。对象通过接口成员函数为客户提供各种形式的服务。
在 COM 模型中,对象本身对于客户来说是不可见的,客户请求服务时,仅仅能通过接口进行。每个接口都由一个 128 位的全局唯一标识符(GUID,Global Unique Identifier)来标识。客户通过 GUID 来获得接口的指针,再通过接口指针,客户就能够调用其对应的成员函数。
COM 原理与应用》学习笔记 - 第一部分 COM原理
baidu
与接口相似,每个组件也用一个 128 位 GUID 来标识,称为 CLSID(class identifer,类标识符或类 ID),用 CLSID 标识对象能够保证(概率意义上)在全球范围内的唯一性。实际上,客户成功地创建对象后,它得到的是一个指向对象某个接口的指针,由于 COM 对象至少实现一个接口(没有接口的 COM 对象是没有意义的),所以客户就能够调用该接口提供的全部服务。依据 COM 规范,一个 COM 对象假设实现了多个接口,则能够从某个接口得到该对象的随意其它接口。从这个过程我们也能够看出,客户与 COM 对象仅仅通过接口打交道,对象对于客户来说仅仅是一组接口。
---------------------------------------------------
COM 进程模型
---------------------------------------------------
COM 所提供的服务组件对象在实现时有两种进程模型:进程内对象和进程外对象。假设是进程内对象,则它在客户进程空间中运行;假设是进程外对象,则它运行在同机器上的还有一个进程空间或者在远程机器的空间。
进程内服务程序:
服务程序被载入到客户的进程空间,在 Windows 环境下,通常服务程序的代码以动态连接库(DLL)的形式实现。
本地服务程序:
服务程序与客户程序运行在同一台机器上,服务程序是一个独立的应用程序,通常它是一个 EXE 文件。
远程服务程序:
服务程序运行在与客户不同的机器上,它既能够是一个 DLL 模块,也能够是一个 EXE 文件。假设远程服务程序是以 DLL 形式实现的话,则远程机器会创建一个代理进程。
尽管 COM 对象有不同的进程模型,但这样的差别对于客户程序来说是透明的,因此客户程序在使用组件对象时能够无论这样的差别的存在,仅仅要遵照 COM 规范就可以。然而,在实现 COM 对象时,还是应该谨慎选择进程模型。进程内模型的长处是效率高,但组件不稳定会引起客户进程崩溃,因此组件可能会危及客户;(savetime 注:这里有点问题,假设组件不稳定,进程外模型也同样会出问题,可能是由于进程内组件和客户同处一个地址空间,出现冲突的可能性比較大?)进程外模型的长处是稳定性好,组件进程不会危及客户程序,一个组件进程能够为多个客户进程提供服务,但进程外组件开销大,并且调用效率相对低一点。
《COM 原理与应用》学习笔记 - 第一部分 COM原理
baidu
---------------------------------------------------
COM 可重用性
---------------------------------------------------
由于 COM 标准是建立在二进制代码级的,因此 COM 对象的可重用性与一般的面向对象语言如 C++ 中对象的重用过程不同。对于 COM 对象的客户程序来说,它仅仅是通过接口使用对象提供的服务,它并不知道对象内部的实现过程,因此,组件对象的重用性可建立在组件对象的行为方式上,而不是具体实现上,这是建立重用的关键。COM 用两种机制实现对象的重用。我们假定有两个 COM 对象,对象1 希望能重用对象2 的功能,我们把对象1 称为外部对象,对象2 称为内部对象。
(1)包容方式。
对象1 包括了对象2,当对象1 须要用到对象2 的功能时,它能够简单地把实现交给对象2 来完毕,尽管对象1 和对象2 支持同样的接口,但对象1 在实现接口时实际上调用了对象2 的实现。
(2)聚合方式。
对象1 仅仅需简单地把对象2 的接口递交给客户就可以,对象1 并没有实现对象2 的接口,但它把对象2 的接口也暴露给客户程序,而客户程序并不知道内部对象2 的存在。
===================================================
⊙ 第二章 COM 对象模型
===================================================
全局唯一标识符 GUID
---------------------------------------------------
COM 规范採用了 128 位全局唯一标识符 GUID 来标识对象和接口,这是一个随机数,并不须要专门机构进行分配和管理。由于 GUID 是个随机数,所以并不绝对保证唯一性,但发生标识符相重的可能性非常小。从理论上讲,假设一台机器每秒产生 10000000 个 GUID,则能够保证(概率意义上)的 3240 年不反复)。
GUID 在 C/C++ 中能够用这样的结构来描写叙述:
typedef struct _GUID
{
DWORD Data1;
WORD Data2;
WORD Data3;
BYTE Data4[8];
} GUID;
例:{64BF4372-1007-B0AA-444553540000} 能够例如以下定义一个 GUID:
extern "C" const GUID CLSID_MYSPELLCHECKER =
{ 0x54BF0093, 0x1048, 0x399D,
{ 0xB0, 0xA3, 0x45, 0x33, 0x43, 0x90, 0x47, 0x47} };
Visual C++ 提供了两个程序生成 GUID: UUIDGen.exe(命令行) 和 GUIDGen.exe(对话框)。COM 库提供了以下 API 函数能够产生 GUID:
HRESULT CoCreateGuid(GUID *pguid);
假设创建 GUID 成功,则函数返回 S_OK,并且 pguid 将指向所得的 GUID 值。
---------------------------------------------------
COM 对象
---------------------------------------------------
在 COM 规范中,并没有对 COM 对象进行严格的定义,但 COM 提供的是面向对象的组件模型,COM 组件提供给客户的是以对象形式封装起来的实体。客户程序与 COM 程序进行交互的实体是 COM 对象,它并不关心组件模型的名称和位置(即位置透明性),但它必须知道自己在与哪个 COM 对象进行交互。
---------------------------------------------------
COM 接口
---------------------------------------------------
从技术上讲,接口是包括了一组函数的数据结构,通过这组数据结构,客户代码能够调用组件对象的功能。接口定义了一组成员函数,这组成员函数是组件对象暴露出来的全部信息,客户程序利用这些函数获得组件对象的服务。
通常我们把接口函数表称为虚函数表(vtable),指向 vtable 的指针为 pVtable。对于一个接口来说,它的虚函数表是确定的,因此接口的成员函数个数是不变的,并且成员函数的先后先后顺序也是不变的;对于每个成员函数来说,其參数和返回值也是确定的。在一个接口的定义中,全部这些信息都必须在二进制一级确定,无论什么语言,仅仅要能支持这样的内存结构描写叙述,就能够使用接口。
接口指针 ----> pVtable ----> 指针函数1 -> |----------|
m_Data1 指针函数2 -> | 对象实现 |
m_Data2 指针函数3 -> |----------|
每个接口成员函数的第一个參数为指向对象实例的指针(=this),这是由于接口本身并不独立使用,它必须存在于某个 COM 对象上,因此该指针能够提供对象实例的属性信息,在被调用时,接口能够知道是对哪个 COM 对象在进行操作。
在接口成员函数中,字符串变量必须用 Unicode 字符指针,COM 规范要求使用 Unicode 字符,并且 COM 库中提供的 COM API 函数也使用 Unicode 字符。所以假设在组件程序内部使用到了 ANSI 字符的话,则应该进行两种字符表达的转换。当然,在即建立组件程序又建立客户程序的情况下,能够使用自己定义的參数类型,仅仅要它们与 COM 所能识别的參数类型兼容。
Visual C++ 提供两种字符串的转换:
namespace _com_util {
BSTR ConvertStringToBSTR(const char *pSrc) throw(_com_error);
BSTR ConvertBSTRToString(BSTR pSrc) throw(_com_error);
}
BSTR 是双字节宽度字符串,它是最经常使用的自己主动化数据类型。
---------------------------------------------------
接口描写叙述语言 IDL
---------------------------------------------------
COM 规范在採用 OSF 的 DCE 规范描写叙述远程调用接口 IDL (interface description language,接口描写叙述语言)的基础上,进行扩展形成了 COM 接口的描写叙述语言。接口描写叙述语言提供了一种不依赖于不论什么语言的接口的描写叙述方法,因此,它能够成为组件程序和客户程序之间的共同语言。
COM 规范使用的 IDL 接口描写叙述语言不仅可用于定义 COM 接口,同一时候还定义了一些经常使用的数据类型,也能够描写叙述自己定义的数据结构,对于接口成员函数,我们能够定义每个參数的类型、输入输出特性,甚至支持可变长度的数组的描写叙述。IDL 支持指针类型,与 C/C++ 非常相似。比如:
interface IDictionary
{
HRESULT Initialize()
HRESULT LoadLibrary([in] string);
HRESULT InsertWord([in] string, [in] string);
HRESULT DeleteWord([in] string);
HRESULT LookupWord([in] string, [out] string *);
HRESULT RestoreLibrary([in] string);
HRESULT FreeLibrary();
}
Microsoft Visual C++ 提供了 MIDL 工具,能够把 IDL 接口描写叙述文件编译成 C/C++ 兼容的接口描写叙述头文件(.h)。
---------------------------------------------------
IUnknown 接口
---------------------------------------------------
IUnknown 的 IDL 定义:
interface IUnknown
{
HRESULT QueryInterface([in] REFIID iid, [out] void **ppv);
ULONG AddRef(void);
ULONG Release(void);
}
IUnkown 的 C++ 定义:
class IUnknown
{
virutal HRESULT _stdcall QueryInterface(const IID& iid, void **ppv) = 0;
virtual ULONG _stdcall AddRef() = 0;
virutal ULONG _stdcall Release() = 0;
}
---------------------------------------------------
COM 对象的接口原则
---------------------------------------------------
COM 规范对 QueryInterface 函数设置了以下规则:
1. 对于同一个对象的不同接口指针,查询得到的 IUnknown 接口必须全然同样。也就是说,每个对象的 IUnknown 接口指针是唯一的。因此,对两个接口指针,我们能够通过推断其查询到的 IUnknown 接口是否相等来推断它们是否指向同一个对象。
2. 接口自反性。对一个接口查询其自身总应该成功,比方:
pIDictionary->QueryInterface(IID_Dictionary, ...) 应该返回 S_OK。
3. 接口对称性。假设从一个接口指针查询到还有一个接口指针,则从第二个接口指针再回到第一个接口指针必然成功,比方:
pIDictionary->QueryInterface(IID_SpellCheck, (void **)&pISpellCheck);
假设查找成功的话,则再从 pISpellCheck 查回 IID_Dictionary 接口肯定成功。
4. 接口传递性。假设从第一个接口指针查询到第二个接口指针,从第二个接口指针能够查询到第三个接口指针,则从第三个接口指针一定能够查询到第一个接口指针。
5. 接口查询时间无关性。假设在某一个时刻能够查询到某一个接口指针,则以后不论什么时间再查询同样的接口指针,一定能够查询成功。
总之,无论我们从哪个接口出发,我们总能够到达不论什么一个接口,并且我们也总能够回到最初的那个接口。
===================================================
⊙ 第三章 COM 的实现
===================================================
COM 组件注冊信息
---------------------------------------------------
当前机器上全部组件的信息 HKEY_CLASS_ROOT/CLSID
进程内组件 HKEY_CLASS_ROOT/CLSID/guid/InprocServer32
进程外组件 HKEY_CLASS_ROOT/CLSID/guid/LocalServer32
组件所属类别(CATID) HKEY_CLASS_ROOT/CLSID/guid/Implemented Categories
COM 接口的配置信息 HKEY_CLASS_ROOT/Interface
代理 DLL/存根 DLL HKEY_CLASS_ROOT/CLSID/guid/ProxyStubClsid
HKEY_CLASS_ROOT/CLSID/guid/ProxyStubClsid32
类型库的信息 HKEY_CLASS_ROOT/TypeLib
字符串命名 ProgID HKEY_CLASS_ROOT/ (比如 "COMCTL.TreeCtrl")
组件 GUID HKEY_CLASS_ROOT/COMTRL.TreeControl/CLSID
缺省版本号号 HKEY_CLASS_ROOT/COMTRL.TreeControl/CurVer
(比如 CurVer = "COMTRL.TreeCtrl.1", 那么
HKEY_CLASS_ROOT/COMTRL.TreeControl.1 也存在)
当前机器全部组件类别 HKEY_CLASS_ROOT/Component Categories
COM 提供两个 API 函数 CLSIDFromProgID 和 ProgIDFromCLSID 转换 ProgID 和 CLSID。
假设 COM 组件支持同样一组接口,则能够把它们分到同一类中,一个组件能够被分到多个类中。比方全部的自己主动化对象都支持 IDispatch 接口,则能够把它们归成一类“Automation Objects”。类别信息也用一个 GUID 来描写叙述,称为 CATID。组件类别最基本的用处在于客户能够高速发现机器上的特定类型的组件对象,否则的话,就必须检查全部的组件对象,并把组件对象装入到内存中实例化,然后依次询问是否实现了必要的接口,如今使用了组件类别,就能够节省查询过程。
---------------------------------------------------
注冊 COM 组件
---------------------------------------------------
RegSrv32.exe 用于注冊一个进程内组件,它调用 DLL 的 DllRegisterServer 和 DllUnregisterServer 函数完毕组件程序的注冊和注销操作。假设操作成功返回 TRUE,否则返回 FALSE。
对于进程外组件程序,情形稍有不同,由于它自身是个可运行程序,并且它也不能提供入口函数供其它程序使用。因此,COM 规范中规定,支持自注冊的进程外组件必须支持两个命令行參数 /RegServer 和 /UnregServer,以便完毕注冊和注销操作。命令行參数大写和小写无关,并且 “/” 能够用 “-” 替代。假设操作成功,程序返回 0,否则,返回非 0 表示失败。
---------------------------------------------------
类厂和 DllGetObjectClass 函数
---------------------------------------------------
类厂(class factory)是 COM 对象的生产基地,COM 库通过类厂创建 COM 对象;对应每个 COM 类,有一个类厂专门用于该 COM 类的对象创建操作。类厂本身也是一个 COM 对象,它支持一个特殊的接口 IClassFactory:
class IClassFactory : public IUnknown
{
virtual HRESULT _stdcall CreateInstance(IUnknown *pUnknownOuter,
const IID& iid, void **ppv) = 0;
virtual HRESULT _stdcall LockServer(BOOL bLock) = 0;
}
CreateInstance 成员函数用于创建对应的 COM 对象。第一个參数 pUnknownOuter 用于对象类被聚合的情形,一般设置为 NULL;第二个參数 iid 是对象创建完毕后客户应该得到的初始接口 IID;第三个參数 ppv 存放返回的接口指针。
LockServer 成员函数用于控制组件的生存周期。
类厂对象是由 DLL 引出函数 DllGetClassObject 创建的:
HRESULT DllGetClassObject(const CLSID& clsid, const IID& iid, (void **)ppv);
DllGetClassObject 函数的第一个參数为待创建对象的 CLSID。由于一个组件可能实现了多个 COM 对象类,所以在 DllGetClassObject 函数的參数中有必要指定 CLSID,以便创建正确的 class factory。另两个參数 iid 和 ppv 分别指于指定接口 IID 和存放类厂接口指针。
COM 库在接到对象创建的指令后,它要调用进程内组件的 DllGetClassObject 函数,由该函数创建类厂对象,并返回类厂对象的接口指针。COM 库或客户一旦拥有类厂的接口指针,它们就能够通过 IClassFactory 的成员函数 CreateInstance 创建对应的 COM 对象。
---------------------------------------------------
CoGetClassObject 函数
---------------------------------------------------
在 COM 库中,有三个 API 可用于对象的创建,它们各自是 CoGetClassObject、CoCreateInstnace 和 CoCreateInstanceEx。通常情况下,客户程序调用当中之中的一个完毕对象的创建,并返回对象的初始接口指针。COM 库与类厂也通过这三个函数进行交互。
HRESULT CoGetClassObject(const CLSID& clsid, DWORD dwClsContext,
COSERVERINFO *pServerInfo, const IID& iid, (void **)ppv);
CoGetClassObject 函数先找到由 clsid 指定的 COM 类的类厂,然后连接到类厂对象,假设须要的话,CoGetClassObject 函数装入组件代码。假设是进程内组件对象,则 CoGetClassObject 调用 DLL 模块的 DllGetClassObject 引出函数,把參数 clsid、iid 和 ppv 传给 DllGetClassObject 函数,并返回类厂对象的接口指针。通常情况下 iid 为 IClassFactory 的标识符 IID_IClassFactory。假设类厂对象还支持其它可用于创建操作的接口,也能够使用其它的接口标识符。比如,可请求 IClassFactory2 接口,以便在创建时,验证用户的许可证情况。IClassFactory2 接口是对 IClassFactory 的扩展,它加强了组件创建的安全性。
參数 dwClsContext 指定组件类别,能够指定为进程内组件、进程外组件或者进程内控制对象(相似于进程外组件的代理对象,主要用于 OLE 技术)。參数 iid 和 ppv 分别对应于 DllGetClassObject 的參数,用于指定接口 IID 和存放类对象的接口指针。參数 pServerInfo 用于创建远程对象时指定server信息,在创建进程内组件对象或者本地进程外组件时,设置 NULL。
假设 CoGetClassObject 函数创建的类厂对象位于进程外组件,则情形要复杂得多。首先 CoGetClassObject 函数启动组件进程,然后一直等待,直到组件进程把它支持的 COM 类对象的类厂注冊到 COM 中。于是 CoGetClassObject 函数把 COM 中对应的类厂信息返回。因此,组件外进程被 COM 库启动时(带命令行參数“/Embedding”),它必须把所支持的 COM 类的类厂对象通过 CoRegisterClassObject 函数注冊到 COM 中,以便 COM 库创建 COM 对象使用。当进程退出时,必须调用 CoRevokeClassObject 函数以便通知 COM 它所注冊的类厂对象不再有效。组件程序调用 CoRegisterClassObject 函数和 CoRevokeClassObject 函数必须配对,以保证 COM 信息的一致性。
---------------------------------------------------
CoCreateInstance / CoCreateInstanceEx 函数
---------------------------------------------------
HRESULT CoCreateInstance(const CLSID& clsid, IUnknown *pUnknownOuter,
DWORD dwClsContext, const IID& iid, (void **)ppv);
CoCreateInstance 是一个被包装过的辅助函数,在它的内部实际上也调用了 CoGetClassObject 函数。CoCreateInstance 的參数 clsid 和 dwClsContext 的含义与 CoGetClassObject 对应的參数一致,(CoCreateInstance 的 iid 和 ppv 參数与 CoGetClassObject 不同,一个是表示对象的接口信息,一个是表示类厂的接口信息)。參数 pUnknownOuter 与类厂接口的 CreateInstance 中对应的參数一致,主要用于对象被聚合的情况。CoCreateInstance 函数把通过类厂创建对象的过程封装起来,客户程序仅仅要指定对象类的 CLSID 和待输出的接口指针及接口 ID,客户程序能够不与类厂打交道。CoCreateInstance 能够用以下的代码实现:
(savetime 注:以下代码中 ppv 指针的应用,好像应该是 void **)
HRESULT CoCreateInstance(const CLSID& clsid, IUnknown *pUnknownOuter,
DWORD dwClsContext, const IID& iid, void *ppv)
{
IClassFactory *pCF;
HRESULT hr;
hr = CoGetClassObject(clsid, dwClsContext, NULL, IID_IClassFactory,
(void *) pCF);
if (FAILED(hr)) return hr;
hr = pCF->CreateInstance(pUnknownOuter, iid, (void *)ppv);
pFC->Release();
return hr;
}
从这段代码我们能够看出,CoCreateInstance 函数首先利用 CoGetClassObject 函数创建类厂对象,然后用得到的类厂对象的接口指针创建真正的 COM 对象,最后把类厂对象释放掉并返回,这样就把类厂屏蔽起来。
可是,用 CoCreateInstance 并不能创建远程机器上的对象,由于在调用 CoGetClassObject 时,把第三个用于指定server信息的參数设置为 NULL。假设要创建远程对象,能够使用 CoCreateInstance 的扩展函数 CoCreateInstanceEx:
HRESULT CoCreateInstanceEx(const CLSID& clsid, IUnknown *pUnknownOuter,
DWORD dwClsContext, COSERVERINFO *pServerInfo, DWORD dwCount,
MULTI_QI *rgMultiQI);
前三个參数与 CoCreateInstance 一样,pServerInfo 与 CoGetClassOjbect 的參数一样,用于指定server信息,最后两个參数 dwCount 和 rgMultiQI 指定了一个结构数组,能够用于保存多个对象接口指针,其目的在于一次获得多个接口指针,以便降低客户程序与组件程序之间的频繁交互,这对于网络环境下的远程对象是非常有意义的。
---------------------------------------------------
COM 库的初始化
---------------------------------------------------
调用 COM 库的函数之前,为了使函数有效,必须调用 COM 库的初始化函数:
HRESULT CoInitialize(IMalloc *pMalloc);
pMalloc 用于指定一个内存分配器,可由应用程序指定内存分配原则。普通情况下,我们直接把參数设为 NULL,则 COM 库将使用缺省提供的内存分配器。
返回值:S_OK 表示初始化成功
S_FALSE 表示初始化成功,但这次调用不是本进程中首次调用初始化函数
S_UNEXPECTED 表示初始化过程中发生了错误,应用程序不能使用 COM 库
通常,一个进程对 COM 库仅仅进行一次初始化,并且,在同一个模块单元中对 COM 库进行多次初始化并没有意义。唯一不须要初始化 COM 库的函数是获取 COM 库版本号的函数:
DWORD CoBuildVersion();
返回值:高 16 位 主版本号号
低 16 位 次版本号号
COM 程序在用完 COM 库服务之后,一般是在程序退出之前,一定要调用终止 COM 库服务函数,以便释放 COM 库所维护的资源:
void CoUninitialize(void);
注意:凡是调用 CoInitialize 函数返回 S_OK 的进程或程序模块一定要有对应的 CoUninitialize 函数调用,以保证 COM 库有效地利用资源。
(? 假设在一个模块中调用 CoInitialize 返回 S_OK,那么它调用 CoUnitialize 函数后,其它也在使用 COM 库的模块是否会出错误?还是 COM 库会自己主动检查有哪些模块在使用?)
---------------------------------------------------
COM 库的内存管理
---------------------------------------------------
由于 COM 组件程序和客户程序是通过二进制级标准建立连接的,所以在 COM 应用程序中凡是涉及客户、COM 库和组件三者之间内存交互(分配和释放不在同一个模块中)的操作必须使用一致的内存管理器。COM 提供的内存管理标准,实际上是一个 IMalloc 接口:
// IID_IMalloc: {00000002-0000-0000-C000-000000000046}
class IMalloc: public IUnknown
{
void * Alloc(ULONG cb) = 0;
void * Realloc(void *pv, ULONG cb) = 0;
void Free(void *pv) = 0;
ULONG GetSize(void *pv) = 0; // 返回分配的内存大小
int DidAlloc(void *pv) = 0; // 确定内存指针是否由该内存管理器分配
void HeapMinimize() = 0; // 使堆内存尽可能降低,把没用到的内存还给
// 操作系统,用于性能优化
}
获得 IMalloc 接口指针:
HRESULT CoGetMalloc(DWORD dwMemContext, IMalloc **ppMalloc);
CoGetMalloc 函数的第一个參数 dwMemContext 用于指定内存管理器的类型。COM 库中包括两种内存管理器,一种就是在初始化时指定的内存管理器或者其内部缺省的管理器,也称为作业管理器(task allocator),这样的管理器在本进程内有效,要获取该管理器,在 dwMemContext 參数中指定为 MEMCTX_TASK;还有一种是跨进程的共享分配器,由 OLE 系统提供,要获取这样的管理器,dwMemContext 參数中指定为 MEMCTX_SHARED,使用共享管理器的便利是,能够在一个进程内分配内存并传给第二个进程,在第二个进程内使用此内存甚至释放掉此内存。
仅仅要函数的返回值为 S_OK,则 ppMalloc 就指向了 COM 库的内存管理器接口指针,能够使用它进行内存操作,使用完毕后,应该调用 Release 成员函数释放控制权。
COM 库封装了三个 API 函数,可用于内存分配和释放:
void * CoTaskMemAlloc(ULONG cb);
void CoTaskFree(void *pv);
void CoTaskMemRealloc(void *pv, ULONG cb);
这三个函数分配对应于 IMalloc 的三个成员函数:Alloc、Realloc 和 Free。
例:COM 程序怎样从 CLSID 值找到对应的 ProgID 值:
WCHAR *pwProgID;
char pszProgID[128];
hResult = ::ProgIDFromCLSID(CLSID_Dictionary, &pwProgID);
if (hResult != S_OK) {
...
}
wcstombs(pszProgID, pwProgID, 128);
CoTaskMemFree(pwProgID); // 注意:必须释放内存
在调用 COM 函数 ProgIDFromCLSID 返回之后,由于 COM 库为输出变量 pwProgID 分配了内存空间,所以应用程序在用完 pwProgID 变量之后,一定要调用 CoTaskMemFree 函数释放内存。该样例说明了在 COM 库中分配内存,而在调用程序中释放内存的一种情况。COM 库中其它一些函数也有相似的特性,尤其是一些包括不定长度输出參数的函数。
---------------------------------------------------
组件程序的装载和卸载
---------------------------------------------------
进程内组件的装载:
客户程序调用COM 库的 CoCreateInstance 或 CoGetClassObject 函数创建 COM 对象,在 CoGetClassObject 函数中,COM 库依据系统注冊表中的信息,找到类标识符 CLSID 对应的组件程序(DLL 文件)的全路径,然后调用 LoadLibrary(实际上是 CoLoadLibrary)函数,并调用组件程序的 DllGetClassObject 引出函数。DllGetClassObject 函数创建对应的类厂对象,并返回类厂对象的 IClassFactory 接口。至此 CoGetClassObject 函数的任务完毕,然后客户程序或者 CoCreateInstance 函数继续调用类厂对象的 CreateInstance 成员函数,由它负责 COM 对象的创建工作。
CoCreateInstance
|-CoGetClassObject
|-Get CLSID -> DLLfile path
|-CoLoadLibrary
|-DLLfile.DllGetClassObject
|-return IClassFactory
|-IClassFactory.CreateInstnace
进程外组件的装载:
在 COM 库的 CoGetClassObject 函数中,当它发现组件程序是 EXE 文件(由注冊表组件对象信息中的 LocalServer 或 LocalServer32 值指定)时,COM 库创建一个进程启动组件程序,并带上“/Embedding”命令行參数,然后等待组件程序;而组件程序在启动后,当它检查到“/Embedding”命令行參数后,就会创建类厂对象,然后调用 CoRegisterClassObject 函数把类厂对象注冊到 COM 中。当 COM 库检查到组件对象的类厂之后,CoGetClassObject 函数就把类厂对象返回。由于类厂与客户程序运行在不同的进程中,所以客户程序得到的是类厂的代理对象。一旦客户程序或 COM 库得到了类厂对象,它就能够完毕组件对象的创建工作。
进程内对象和进程外对象的不同创建过程仅仅影响了 CoGetClassObject 函数的实现过程,对于客户程序来说是全然透明的。
CoGetClassObject
|-LocalServer/LocalServer32
|-Execute EXE /Embedding
|-Create class factory
|-CoRegisterClassObject ( class factory )
|-return class factory (proxy)
进程内组件的卸载:
仅仅有当组件程序满足了两个条件时,它才干被卸载,这两个条件是:组件中对象数为 0,类厂的锁计数为 0。满足这两个条件时,DllCanUnloadNow 引出函数返回 TRUE。COM 提供了一个函数 CoFreeUnusedLibraries,它会检測当前进程中的全部组件程序,当发现某个组件程序的 DllCanUnloadNow 函数返回 TRUE 时,就调用 FreeLibrary 函数(实际上是 CoFreeLibrary 函数)把该组件从程序从内存中卸出。
该由谁来调用 CoFreeUnusedLibraries 函数呢?由于在组件程序运行过程中,它不可能把自己从内存中卸出,所以这个任务应该由客户来完毕。客户程序随时都能够调用 CoFreeUnusedLibraries 函数完毕卸出工作,但通常的做法是,在程序的空暇处理过程中调用 CoFreeUnusedLibraries 函数,这样做既能够避免程序中处处考虑对 CoFreeUnusedLibraries 函数的调用,又能够使不再使用的组件程序得到及时清除,提高资源的利用率,COM 规范也推荐这样的做法。
进程外组件的卸载:
进程外组件的卸载比較简单,由于组件程序运行在单独的进程中,一旦其退出的条件满足,它仅仅要从进程的主控函数返回就可以。在 Windows 系统中,进程的主控函数为 WinMain。
前面曾经说过,在组件程序启动运行时,它调用 CoRegisterClassObject 函数,把类厂对象注冊到 COM 中,注冊之后,类厂对象的引用计数始终大于 0,因此单凭类厂对象的引用计数无法控制进程的生存期,这也是引入类厂对象的加锁和减锁操作的原因。进程外组件的载条件与 DllCanUnloadNow 中的推断相似,也须要推断 COM 对象是否还存在、以及推断是否锁计数器为 0,仅仅有当条件满足了,进程的主函数才干够退出。
从原则上讲,进程外组件程序的卸载就是这么简单,但实际上情况可能复杂一些,由于有些组件程序在运行过程中能够创建自己的对象,或者包括用户界面的程序在运行过程中,用户手工关闭了进程,那么进程对这些动作的处理要复杂一些。比如,组件程序在运行过程中,用户又打开了一个文件并进行操作,那么即使原先创建的对象被释放了,并且锁计数器也为 0,进程也不能退出,它必须继续为用户服务,就像是用户打开的进程一样。对这样的程序,能够增加一个“用户控制”标记 flag,假设 flag 为 FALSE,则能够按简单的方法直接退出程序就可以;假设 flag 为 TRUE,则表明用户參与了控制,组件进程不能立即退出,但应该调用 CoRevokeClassObject 函数以便与 CoRegisterClassObject 调用相响呼应,把进程留给用户继续进行。
假设组件程序在运行过程中,用户要关闭进程,而此时并不满足进程退出条件,那么进程能够採取两种办法:第一种方法,把应用隐藏起来,并把 flag 标记设置为 FALSE,然后组件程序继续运行直到卸载条件满足为止;还有一种办法是,调用 CoDisconnectObject 函数,强迫脱离对象与客户之间的关系,并强行终止进程,这样的方法比較粗暴,不提倡採用,但不得已时能够也使用,以保证系统完毕一些高优先级的操作。
---------------------------------------------------
COM 库经常使用函数
---------------------------------------------------
初始化函数 CoBuildVersion 获得 COM 库的版本号号
CoInitialize COM 库初始化
CoUninitialize COM 库功能服务终止
CoFreeUnusedLibraries 释放进程中全部不再使用的组件程序
GUID 相关函数 IsEqualGUID 推断两个 GUID 是否相等
IsEqualIID 推断两个 IID 是否相等
IsEqualCLSID 推断两个 CLSID 是否相等 (*为什么要3个函数)
CLSIDFromProgID 字符串组件标识转换为 CLSID 形式
StringFromCLSID CLSID 形式标识转化为字符串形式
IIDFromString 字符串转换为 IID 形式
StringFromIID IID 形式转换为字符串
StringFromGUID2 GUID 形式转换为字符串(*为什么有 2)
对象创建函数 CoGetClassObject 获取类厂对象
CoCreateInstance 创建 COM 对象
CoCreateInstanceEx 创建 COM 对象,可指定多个接口或远程对象
CoRegisterClassObject 登记一个对象,使其它应用程序能够连接到它
CoRevokeClassObject 取消对象的登记
CoDisconnectObject 断开其它应用与对象的连接
内存管理函数 CoTaskMemAlloc 内存分配函数
CoTaskMemRealloc 内存又一次分配函数
CoTaskMemFree 内存释放函数
CoGetMalloc 获取 COM 库内存管理器接口
---------------------------------------------------
HRESULT 类型
---------------------------------------------------
大多数 COM 函数以及一些接口成员函数的返回值类型均为 HRESULT 类型。HRESULT 类型的返回值反映了函数中的一些情况,其类型定义规范例如以下:
31 30 29 28 16 15 0
|-----|--|------------------------|-----------------------------------|
类别码 (30-31) 反映函数调用结果:
00 调用成功
01 包括一些信息
10 警告
11 错误
自己定义标记(29) 反映结果是否为自己定义标识,1 为是,0 则不是;
操作码 (16-28) 标识结果操作来源,在 Windows 平台上,其定义例如以下:
#define FACILITY_WINDOWS 8
#define FACILITY_STORAGE 3
#define FACILITY_RPC 1
#define FACILITY_SSPI 9
#define FACILITY_WIN32 7
#define FACILITY_CONTROL 10
#define FACILITY_NULL 0
#define FACILITY_INTERNET 12
#define FACILITY_ITF 4
#define FACILITY_DISPATCH 2
#define FACILITY_CERT 11
操作结果码(0-15) 反映操作的状态,WinError.h 定义了 Win32 函数全部可能返回结果。
以下是一些经经常使用到的返回值和宏定义:
S_OK 函数运行成功,其值为 0 (注意,其值与 TRUE 相反)
S_FALSE 函数运行成功,其值为 1
S_FAIL 函数运行失败,失败原因不确定
E_OUTOFMEMORY 函数运行失败,失败原由于内存分配不成功
E_NOTIMPL 函数运行失败,成员函数没有被实现
E_NOTINTERFACE 函数运行失败,组件没有实现指定的接口
不能简单地把返回值与 S_OK 和 S_FALSE 比較,而要用 SECCEEDED 和 FAILED 宏进行推断。
===================================================
⊙ 第四章 COM 特性
===================================================
可重用性:包容和聚合
---------------------------------------------------
包容模型:
组件对象在接口的实现代码中运行自身创建的还有一个组件对象的接口函数(客户/server模型)。这个对象同一时候实现了两个(或很多其它)接口的代码。
聚合模型:
组件对象在接口的查询代码中把接口传递给自已创建的还有一个对象的接口查询函数,而不实现该接口的代码。还有一个对象必须实现聚合模型(也就是说,它知道自己正在被还有一个组件对象聚合),以便 QueryInterface 函数能够正常运作。
在组件对象被聚合的情况下,当客户请求它所不支持的接口或者请求 IUnknown 接口时,它必须把控制交给外部对象,由外部对象决定客户程序的请求结果。
聚合模型体现了组件软件真正意义上的重用。
聚合模型实现的关键在 CoCreateInstance 函数和 IClassFactory 接口:
HRESULT CoCreateInstance(const CLSID& clsid, IUnknown *pUnknownOuter,
DWORD dwClsContext, const IID& iid, (void **)ppv);
// class IClassFactory : public IUnknown
virtual HRESULT _stdcall CreateInstance(IUnknown *pUnknownOuter,
const IID& iid, void **ppv) = 0;
当中 pUnknownOuter 參数用于指定组件对象是否被聚合。假设 pUnknownOuter 參数为 NULL,说明组件对象正常使用,否则说明被聚合使用,pUnknownOuter 是外部组件对象的接口指针。
聚合模型下的被聚合对象的引用计数成员函数也要进行特别处理。在未被聚合的情况下,能够使用一般的引用计数方法。在被聚合时,由客户调用 AddRef/Release 函数时,必须转向外部组件对象的 AddRef/Release 方法。这时,外部组件对象要控制被聚合的对象必须採用其它的引用计数接口。
---------------------------------------------------
进程透明性 (待学)
安全性(待学)
多线程特性(待学)
---------------------------------------------------
===================================================
⊙ 第五章 用 Visual C++ 开发 COM 应用
===================================================
Win32 SDK 提供的一些头文件的说明
---------------------------------------------------
Unknwn.h 标准接口 IUnknown 和 IClassFacatory 的 IID 及接口成员函数的定义
Wtypes.h 包括 COM 使用的数据结构的说明
Objidl.h 全部标准接口的定义,就可以用于 C 语言风格的定义,也可用于 C++ 语言 Comdef.h 全部标准接口以及 COM 和 OLE 内部对象的 CLSID ObjBase.h 全部的 COM API 函数的说明 Ole2.h 全部经过封装的 OLE 辅助函数
---------------------------------------------------
与 COM 接口有关的一些宏
---------------------------------------------------
DECLARE_INTERFACE(iface)
声明接口 iface,它不从其它的接口派生
DECLARE_INTERFACE_(iface, baseiface)
声明接口 iface,它从接口 baseiface 派生
STDMETHOD(method)
声明接口成员函数 method,函数返回类型为 HRESULT
STDMETHOD_(type, method) 声明接口成员函数 method,函数返回类型为 type
===================================================
⊙ 结 束
===================================================