💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
# 第九章 ## Windows 编程 Delphi 利用Object Pascal 和可视控件库(VCL)对底层的Windows API 进行了完美的封装,所以很少需要使用基础Pascal 语言来建立Windows应用程序,也无需直接调用Windows API 函数。尽管如此,如果遇到特殊情况,VCL 又不支持,Delphi程序员还得直接面对Windows编程。不过只有在极其特殊的情况下,例如:基于不寻常API 调用的Delphi新控件开发, 你才需要这样做,这里我不想讨论这方面内容,我只想让大家看一下与操作系统交互的几个Delphi元素以及Delphi程序员能从中获益的Windows编程技术。 ### Windows 句柄 Delphi从Windows 引入了不少数据类型,其中句柄最重要。这种数据类型名为THandle,该类型在Windows 单元中定义: ~~~ type THandle = LongWord; ~~~ 句柄数据类型通过数字实现,但并不当数字用。在Windows 中,句柄是一个系统内部数据结构的引用。例如,当你操作一个窗口,或说是一个Delphi 窗体时,系统会给你一个该窗口的句柄,系统会通知你:你正在操作142号窗口,就此,你的应用程序就能要求系统对142号窗口进行操作——移动窗口、改变窗口大小、把窗口极小化为图标,等等。实际上许多Windows API 函数把句柄作为它的第一个参数,如GDI (图形设备接口)句柄、菜单句柄、实例句柄、位图句柄等等,不仅仅局限于窗口函数,。 换句话说,句柄是一种内部代码,通过它能引用受系统控制的特殊元素,如窗口、位图、图标、内存块、光标、字体、菜单等等。Delphi中很少需要直接使用句柄,因为句柄藏在窗体、位图及其他Delphi对象的内部。当你要调用Delphi不支持的Windows API 函数时,句柄才会有用。 现在举一个简单的Windows句柄例子,完善这节内容。例WHandle 程序的窗体很简单,只有一个按钮。正如下面主窗体文本所定义的那样,我在代码中添加了窗体的OnCreate 事件和按钮的OnClick 事件: ~~~ object FormWHandle: TFormWHandle Caption = 'Window Handle' OnCreate = FormCreate object BtnCallAPI: TButton Caption = 'Call API' OnClick = BtnCallAPIClick end end ~~~ 窗体一创建,程序就会通过窗体本身的Handle 属性,获取窗体对应的窗口句柄。调用IntToStr ,把句柄数值转换为一个字符串,然后再把它添加到窗体标题中,如图9.1: ~~~ procedure TFormWHandle.FormCreate(Sender: TObject); begin Caption := Caption + ' ' + IntToStr (Handle); end; ~~~ 因为FormCreate 是窗体类的方法,它可直接访问同类的其他属性和方法。因此,在这个过程中我们能够直接访问窗体的Caption 属性和Handle 属性。 **图 9.1: 例 WHandle 显示窗体句柄,每次运行程序得到的句柄值不同** ![](https://box.kancloud.cn/490b6414a84f2639ecf2eaca6e38ccda_259x115.png) 如果你多此次执行该程序,通常会获得不同的句柄值。这个值实际上是由Windows 操作系统确定并返回给应用程序的。(句柄从来不是由程序决定的,而且句柄没有预定义值,句柄是由系统决定的,每执行一次程序,产生一个新值。) 当你单击按钮,程序将调用Windows API 函数SetWindowText,它会根据第一个传递参数改变窗口的标题。更准确地说,所用的API 函数其第一个参数是需要修改窗体的句柄: ~~~ procedure TFormWHandle.BtnCallAPIClick(Sender: TObject); begin SetWindowText (Handle, 'Hi'); end; ~~~ 这段代码与前面所讲的事件处理程序等效,它通过给窗体的Caption 属性赋一个新值,改变窗体的标题。对上面这种情况,调用一个API 函数没有什么意义,因为用Delphi来做更简单。然而有些API在Delphi中没有相应的函数,就需要直接调用API,这一点你会在后面的高级例子中看到。 ### 外部声明 Windows 编程中涉及的另一个重要元素是外部声明。外部声明原先用于在Pascal代码中连接汇编语言写的外部函数,现在外部声明用于Windows编程,用来调用动态连接库DLL函数。在Delphi的Windows 单元中有许多这种声明: ~~~ // forward declaration function LineTo (DC: HDC; X, Y: Integer): BOOL; stdcall; // external declaration (instead of actual code) function LineTo; external 'gdi32.dll' name 'LineTo'; ~~~ 这段声明表示函数LineTo 的代码同名保存在GDI32.DLL 动态链接库中(最重要的Windows 系统库之一)。实际应用时,外部声明中的函数名与DLL中的函数名可以不同。 一般你不需要象刚才所例举的那样写声明,因为Windows 单元和一些Delphi 系统单元中已包含了这些声明。只有在调用自定义DLL,或调用Delphi 中未定义的Windows 函数时,你才能需要写外部声明。 > 注意:在16位Delphi中,外部声明使用不带扩展名的库名,后面跟name指令(如上所示)或是一个index指令,后面跟DLL中函数的序号。尽管Win32 仍然允许通过序号访问DLL函数,但是微软公司已经声明未来将不支持这种访问方式,这一改变反映了系统库访问方式的改变。还要注意的是:目前Delphi的Windows 单元已取代了16位Delphi的WinProcs 和WinTypes 单元。 ### 回调函数 从第六章已经了解到Objet Pascal 支持过程类型。过程类型常用于给Windows API函数传递回调函数。 首先,什么是回调函数呢?回调函数就是能对一系列系统内部元素执行给定操作的API函数,例如能对所有同类窗口进行操作的函数。这种函数也叫枚举函数,它是作为参数传递的函数,代表对所有内部元素执行的操作,该函数或过程的类型必须与给定的过程类型兼容。Windows 回调函数的应用不止上述一种,不过这里仅研究以上简单应用。 现在考虑 EnumWindows API 函数,它的原型如下(从Win32 帮助文件拷贝而来): ~~~ BOOL EnumWindows( WNDENUMPROC lpEnumFunc, // address of callback function LPARAM lParam // application-defined value ); ~~~ 当然,这是个C语言的定义。我们可以查看Windows 单元,从中找到相应的Pascal 语言定义: ~~~ function EnumWindows ( lpEnumFunc: TFNWndEnumProc; lParam: LPARAM): BOOL; stdcall; ~~~ 查阅帮助文件,我们发现作为参数传递的函数应该属于下面的类型(也是在C中): ~~~ BOOL CALLBACK EnumWindowsProc ( HWND hwnd, // handle of parent window LPARAM lParam // application-defined value ); ~~~ 这与下面的Delphi 过程类型定义一致: ~~~ type EnumWindowsProc = function (Hwnd: THandle; Param: Pointer): Boolean; stdcall; ~~~ 其中第一个参数是各主窗体的句柄,第二个参数则是调用EnumWindows 函数时所传递的值。实际上,Pascal 中没有相应的TFNWndEnumProc类型定义 ,它只是个指针。这意味着我们需要传递一个带有合适参数的函数,将它用作一个指针,也就是取函数的地址而不是调用它。不幸的是,这也意味着如果某个参数类型出现错误时,编译器不会给予提示。 每当调用Windows API函数或传递一个回调函数给系统时,Windows 要求程序员遵循stdcall 调用协定。缺省情况下,Delphi使用另一种更高效的调用协定,其关键字为register。 下面是一个与定义兼容的回调函数,此函数把窗口的标题读到字符串中,然后添加到给定窗体的一个列表框中: ~~~ function GetTitle (Hwnd: THandle; Param: Pointer): Boolean; stdcall; var Text: string; begin SetLength (Text, 100); GetWindowText (Hwnd, PChar (Text), 100); FormCallBack.ListBox1.Items.Add ( IntToStr (Hwnd) + ': ' + Text); Result := True; end; ~~~ 窗体有一个几乎覆盖整个窗体的列表框,窗体顶部有一个小面板,面板上有一个按钮。当按下按钮时,EnumWindows API函数被调用,并且GetTitle 函数作为参数传递给它: ~~~ procedure TFormCallback.BtnTitlesClick(Sender: TObject); var EWProc: EnumWindowsProc; begin ListBox1.Items.Clear; EWProc := GetTitle; EnumWindows (@EWProc, 0); end; ~~~ 你可以直接调用GetTitle函数,不必先把值保存到过程类型临时变量中,上例这么做是为了使回调过程更清楚。程序运行结果确实很有意思,正如你在图9.2中看到的那样,结果显示了系统中正在运行的所有主窗口,其中大部分是隐藏的,你通常看不到,许多实际上没有标题。 **图 9.2: 例CallBack输出结果--当前所有主窗体,其中包括可见及隐藏的窗体** ![](https://box.kancloud.cn/eafcc413c5fb474bd12cdf950127a7b1_304x352.png) ### 最小的Windows 程序 为了完整介绍Windows 编程及Pascal 语言,现在我展示一个简单但完整的应用程序,建立该程序没有使用VCL库。这个程序只是简单地采用命令行参数(保存在系统全程变量cmdLine中),并利用ParamCount 和 ParamStr 这两个Pascal 函数从参数中提取信息。其中第一个函数返回参数的个数,第二个返回给定位置的参数。 尽管在图形用户界面环境下用户很少操纵命令行参数,但是Windows 命令行参数对系统本身却很重要。例如,一旦你定义了文件扩展名和应用程序的关联,只要双击所关联的文件就能执行这个程序。实际上,当你双击一个文件,Windows 即启动关联程序并把选定的文件作为命令行参数传递给它。 下面是工程文件的完整源代码(一个DPR 文件,不是PAS 文件): ~~~ program Strparam; uses Windows; begin // show the full string MessageBox (0, cmdLine, 'StrParam Command Line', MB_OK); // show the first parameter if ParamCount > 0 then MessageBox (0, PChar (ParamStr (1)), '1st StrParam Parameter', MB_OK) else MessageBox (0, PChar ('No parameters'), '1st StrParam Parameter', MB_OK); end. ~~~ 输出代码使用MessageBox API 函数,很容易就避开了VCL库。实际上,象上面那样纯粹的Windows 程序,其优点就是占的内存少,程序执行文件大约才16k字节。 为了给程序提供命令行参数,你可以用Delphi的 Run > Parameters 菜单命令。另一个方法是:打开Windows 资源管理器,查找包含程序执行文件的目录,然后把你要执行的文件拖到可执行文件上,Windows 资源管理器会把拖放的文件名用作命令行参数,开始执行程序。图9.3显示了资源管理器及相应的输出。 **图9.3: 把一个文件拖放到执行文件上,给例StrParam提供命令行参数** ![](https://box.kancloud.cn/ce2a1830b41b26b48e35c78e324755c0_495x100.png) * * * * * ### 结束语 在这一章中,我们对Windows 编程的底层内容进行了介绍,讨论了句柄和简单的Windows 程序。对于常规的Windows 编程任务,通常只需使用Delphi 提供的可视开发工具及VCL可视控件库。但是这超出了本书讨论的范围,因为本书讨论的是Pascal 语言。 下一章介绍variant类型。对Pascal 数据类型系统来说,它是一个非常特殊的外来物,引入它是为了提供完全的OLE 支持。