深入Delphi

 深入Delphi (一)

工程文件

by machine

  Delphi是我最喜欢使用的编程语言,功能强大,一般情况下均可代替C,并开放所有控件的源代码,很容易就可以编写自己的控件。而我最喜欢Delphi的地方就是编译速度非常快(Borland号称Delphi的编译器是世界上最快的),能大大提高软件开发速度,所以我一般都使用Delphi,而不用Borland C++ Builder。
  好了,废话说完了,为什么我要写这篇东西呢?市面上的Delphi教程已经够多的了,不过,有朋友抱怨说很多Delphi教程都是差不多,只是很表面的说说控件的使用,看完之后还想对Delphi有更深的了解,所以我就打算写一些一般书上没有提到的地方,就是这样,不过要有心理准备,我写东西真的很臭,又不懂如何顾及各种读者的要求,所以大家可能要反复看几次才能明白我到底想说什么呢。
  首先要说说的是Delphi中一个project(工程)的结构。工程的主文件是必不可少的,扩展名为.dpr(Delphi PRoject),格式跟一般的unit(单元)基本一样。然后在这个dpr中一般还要引用到其他的单元(在Delphi中使用“use”一词,类似C中的include),这些单元文件一般以pas作为扩展名,格式当然就是从以前的Pascal扩展的。前面说到dpr的格式与单元文件的格式基本一样,而且dpr是整个工程程序的入口,与Pascal中是一模一样的!这在本章后面会详细说明。Delphi中使用菜单命令Project->View Source就可以查看到dpr中的内容。
  另外如果工程中添加了form(窗体),则还会有一些.dfm(Delphi ForM)文件,其实是包含窗体及其中控件属性的一个资源文件,也就是说可以像res文件一样打开。提到res文件,这种文件也是工程中常见的,里面包含程序图标、版本信息等资源,以后将会说明这种文件在不同场合的用途。
  对应dpr文件一般还会有一个dof(Delphi OptionFile)和一个cfg文件。Dof文件中包含编译器、连接器的参数,以及其他在Delphi菜单命令Project->Options中可以设置的内容。Cfg文件是根据dof文件生成的编译器和连接器的命令行参数。
  最后一种文件是dcu(Delphi Compiled Unit)文件,也就是已编译的单元文件,根据工程的编译器设置,里面一般还会包含调试信息等,默认设置是编译器生成调试信息到dcu中,但连接器不会把调试信息连接到exe里面。
  以上的东西可能一时还不能理解,不过不要紧,我只是大概说一说。 上面说过,只有dpr文件是必不可少的,也就是说,一个工程可以只包含一个dpr文件,像“广外女生”和“广外幽灵”都是这样的。下面是一个非常简单的例子:

program Project1;

uses
 Windows;

begin
  MessageBox(0, '文件损坏!', nil, MB_OK or MB_ICONSTOP);
end.

  编译运行这个工程,你会发现生成出来的文件非常小,不到20K!是不是很过瘾呢?哎呀,忘了说怎样建立一个这样的工程了。在Delphi中新建一个工程的时候,会自动添加一个窗体和单元,不过你可以把窗体和单元文件都关闭,并在Delphi问你是否保存的时候选择否,然后在菜单中选择Project->View Source打开工程文件,最后把里面的内容修改为上面例子中的那样就行了!
  这个程序运行后会显示一个错误提示框,就像“女生”中的那样,按确定之后,程序就退出了。
  源代码应该是够容易理解的了,很显然,程序从begin处开始执行,到end.处结束,中间只做了一件事情,就是调用MessageBox这个API函数显示一个消息框,由于MessageBox这个函数是在Windows这个单元中声明的,所以必须把单元名称Windows添加到uses语句中。
  什么叫声明(declare)呢?声明就是向Delphi描述一样它不知道的东西。在Delphi中可以声明自定义的数据类型(data type)、函数(function)、过程(procedure,与函数类似,但没有返回值)及变量(variable)和常量(constant)。其中很多人搞不清数据类型与类(class)的区别,其实如果会C++的人都知道,class与structure一样,都是数据类型中的一种,但这种数据结构很特殊,是数据的集合,也就是说,类里面可以包含多个数据域(field)。Record(记录)也是一种集合数据类型,但类还有继承等特性(class一词本身就有分级的意思,但不知道是谁首先翻译成类的)。像下面这个声明:

type TColor24 = record
 Red: Byte;
 Green: Byte;
 Blue: Byte;
end;

定义了一种新的数据类型,指明这种数据类型的名称为TColor32,属于记录(record),并定义其成员内容Red、Green和Blue的名称和数据类型,总之,就是告诉Delphi这种数据类型的结构,以使Delphi知道如何储存这种数据类型的变量。有关数据类型以后将详细说明。

  声明数据类型使用关键字type,声明变量则使用关键字var,比如:

var RGBColor: TColor24;

声明一个变量RGBColor,并指明该变量的数据类型为TColor24(前面声明的数据类型,自定义的数据类型可以像Delphi预定义的一样使用)。所谓声明变量,就是告诉Delphi要为该变量分配其数据类型所需的内存(这就是为什么一定要指明数据类型的原因),并记录该内存的地址,以便存取该变量的时候,存取相应分配的内存。这种对应是由编译器完成的,学VB的朋友可能对数据类型和内存分配并不太关心,但我觉得知道这些还是很有好处的!数据类型有时候还会决定函数中的参数如何传递,这个以后再说。好了,最后再说一句,变量只是一个名称,决定这个变量特性的是它的数据类型。

  声明常量的关键字是const。声明常量与声明变量十分相像,只是除了要指明常量的名称和数据类型之外,还要指明常量的值,因为常量的值是不能改变的,不能像变量那样在程序中赋值:

const RGBYellow: TColor24 = (Red: 255; Green: 255; Blue: 0);

我觉得Delphi中record常量的声明语法有些怪,不过先别管它。上面说了好多东西,包括uses、type、var、const、begin、end.这些关键字,还有例子工程中的以行里面的program,都是在老Pascal中就已经有的了,在一个工程中,这些关键字必须以一定的顺序出现,大概就是像这个样子:

program Sample1;

uses
 Windows,
 SysUtils;

var s: string;

type LONG = Integer;

const ErrCode: LONG = -2147483640;

begin
 s := SysErrorMessage(ErrCode);
 MessageBox(0, PChar(s), nil, MB_OK or MB_ICONSTOP);
end.

  Program指定工程的名称,这一语句一定要出现,并且工程名称和工程文件名必须一致,否则编译时会出错。由于Delphi的编译顺序是和按照源代码的书面顺序进行的,所以编写源代码的时候必须注意,只能先声明,后调用,而不能先调用,后声明。比如,uses语句必须紧跟program语句,也就是在除program语句之外的所有语句之前出现,这和C中的include有点不同,以后在说unit的格式时就知道为什么了。然后是数据类型、变量、常量的声明,这三种声明顺序没有限制,而且可以多次出现,也就是说可以有多个type、var和const语句,但同样要遵循先声明、后调用的规则,比如上面常量ErrCode的声明使用了数据类型LONG,则LONG必须先于ErrCode声明,如此类推。工程的最后一部分是主程序段,以begin开头,end.结束。一般函数中结尾的end是接分号的,但主程序段的end接的是句号,以示整个程序的结束,这个规定也是从老Pascal中遗传下来的,好无聊吧。
  下一章将会说明unit的格式,可以说是project格式的扩展,因此就请大家先弄明白本章的内容吧。

 


深入Delphi (二)

单元文件/编译器条件标识符

by machine

  对比起工程文件,单元文件明显多了一点东西:

unit Unit1;

interface

uses
 Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs;

type
 TForm1 = class(TForm)
 private
  { 私有声明 }
 public
  { 公有声明 }
 end;

var Form1: TForm1;

implementation

{$R *.DFM}

initialization

{ 在这里进行初始化 }

finalization

{ 在这里进行销解化 }

end.

  首先是program换成了unit,然后还有interface和implementation两个关键字把整个单元分为了两部分,即接口和实现部分。
  接口部分相当于C中的头文件部分,如果一个单元被uses了,那其实只是被包括了接口的部分。在这一部分中可以定义函数原型,但函数(包括类中的函数)的具体实现部分只能出现在implementation部分里面。与C类似,接口部分和实现部分都可以出现uses语句,以实现全局和局部的限制。接口部分的uses必须紧接着interface关键字出现,实现部分的uses也必须紧接着implementation关键字,就像它们是合在一起似的。
  至于实现部分,其实如果把工程文件里面的program一行改成implementation,看起来就成了单元里面的实现部分,实际上也是如此,因为首先,工程文件不能被uses,因此工程总是局部的;另外,实现部分里面也可以出现工程中的begin...end.块。
  实现部分的begin...end.块实际上就起到了初始化的作用,但这主要是保留了以前Pascal的语法,Delphi建议使用新增的initialization和finalization来初始化和销解化。在实现部分,begin...end.块、初始化块和销解化块都不是必须出现的,但整个单元必须以end.结尾。
  关键字initialization标志着初始化块的开始,当一个编译后的工程运行时,每个单元的初始化块被首先执行,至于那个单元首先被初始化,则取决于被uses的顺序,先被uses的就先被初始化。初始化块和begin...end.块不能同时出现,因此,建议使用初始化块,以便也能使用销解化块的功能。
  销解化块只能伴随初始化会出现,但初始化块可以单独出现。销解化块在程序结束前被自动执行,其执行顺序与该单元被uses的顺序相反。要注意的是,初始化块可能在完全被执行之前程序就因某种错误而被中止了,但Delphi确保证销解化块被执行,因此销解块中的代码必须考虑未完全初始化的情况。

  然后是对uses语句的详细说明。总结前面所说,uses总共可以在三处地方出现。在单元的两处地方的uses语句里,被uses的单元必须是在工程的搜索路径中,而工程文件中的uses语句则可以直接指明文件的路径:

uses
 Forms,
 Main,
 Extra in '../EXTRA/EXTRA.PAS';

  在单元文件中,如果在接口部分引用了另外一个单元,则实现部分也可以调用该单元,但反之在实现部分引用的单元则不能在接口部分被调用。在编程的时候应该尽量在一个单元的实现部分引用其他单元,而减少在接口部分的引用,以避免所谓的“循环引用”问题。比如说,单元A在接口部分引用了单元B,单元B以同样的方法引用了单元C,最后单元C又在接口部分引用了单元A,则编译时会出现循环引用的错误。解决方法是把全局的声明放到集中的单元中,尽量把引用放到实现部分。
  有时候会遇到一个问题,如果一个变量,或者常量、函数,又或者是数据类型,总之,一样相同名称的东西同时在两个被引用的单元中,那么调用的时候到底会调用到哪一个呢?答案是uses语句的单元列表中最后一个,而不是首先出现的那个!这是要注意的。如果不能确定将会调用那个版本,还可以在所调用的东西前面加上“单元名称+句号”的前缀,以强制Delphi调用相应的单元版本。
  在单元和工程中还有一类用于控制编译器的特殊命令(Compiler Directive)。这种命令包含在大括号或者“(*”和“*)”的组合中,看起来就像注释,但用“$”这个符号开头。比如例子中的$R就是告诉编译器你要包含一些资源文件到工程中。其中有一组命令,$DEFINE、$UNDEF、$IFDEF、$IFNDEF、$ELSE和$ENDIF,看起来很像C中的宏定义命令,但Delphi中的这种定义只起到开关的作用,而不能定义宏替换,也就是说可以定义一个开关名称,使其处于开状态,而如果没有定义这个开关,则认为其处于关状态,但不能像C中那样定义这个开关为一段程序代码,并在编译是自动用代码替换宏,Delphi没有提供这样的宏功能。

  要是对编译器指令不了解,那么请继续看下去,否则就可以直接跳到下一章了。好,首先,什么叫做编译呢?编译就是把你编写的源代码,转译为CPU能执行的指令。通常情况下,编译器把每个单元文件单独编译,然后连接器负责把编译后的单元文件连接为可执行文件(exe文件)。之所以要这样做,是因为不同的操作系统,其可执行文件的格式是不一样的,比如DOS、Windows、Linux这些操作系统。但只要CPU是兼容的型号,则可以使用同样的编译文件格式,因为转译过程是一样的。比如C的编译文件OBJ文件,即使是在DOS下编译的,仍然可以被连接为Windows的EXE,编译器只对CPU感兴趣,而对要输出的执行文件的格式不感兴趣。
  我个人觉得对要学一样东西如果对其了解得越多,学起来就更能融会贯通,所以有时候可能会扯得很远。还有,要学好一样东西,耐心是必不可少的哦!所以一时弄不明白不要泄气,说不定过一段时间之后就突然明白了呢,我以前也试过这样子的。好了,回到刚才Delphi的DEFINE问题。初学者很容易搞不明白这样的定义和常量/变量声明的作用有什么不同:

{$DEFINE Debug}

begin
 {$IFDEF Debug}
  Writeln('Debug is on');
 {$ENDIF}
end;

然后对比一下这个例子:

const Debug = True;

begin
 if Debug then begin
  Writeln('Debug is on');
 end;
end;

  两个例子运行起来结果是一模一样的。但事实上编译出来的程序是不一样的。在第二个例子中,首先,程序被编译,然后运行,CPU首先判断Debug的值是否不为零,如果是,则继续执行调用Writeln,没什么特别之处。但第一个例子的情况就不一样了,在编译时,编译器发现了$DEFINE Debug这条指令,于是它在内部设立标识,记录了Debug这个定义(DEFINE),然后继续编译过程,之后,编译器又发现了$IFDEF Debug这条指令,编译器查看自己的内部标识表,发现Debug是已经定义的,因此它继续进行$IFDEF和$ENDIF之间的源代码的编译,否则的话,它将忽略这段源代码,就好像他们根本不存在的样子!所以,第一段例子编译出来的程序与以下例子的是一模一样的:

begin
 Writeln('Debug is on');
end;

  这样,源代码在编译的时候编译器就已经作了判断,编译出相应的程序,而不是程序在运行的时候,CPU再做出判断,调用相应的代码。这样一来,程序的运行效率明显提高了,C就是大量使用这种特性的例子,因此C的执行效率比较高,同理,C的编译速度却是非常的慢,因为编译器要维护非常大的标识列表(别忘了C的宏定义比Delphi中的复杂,而且C中到处都是这样的宏定义),而且要不断地搜索列表并根据对应定义来修改程序源代码,最后才能进行CPU指令翻译。以后会讲到如何在Delphi中使用其它语言编写的代码,包括汇编和C等,这样一来,程序的事件处理部分可以用Delphi来完成,而在需求速度的地方可以使用汇编或者C来写,开发速度就能够大大提高,而程序运行效率仍然可以维持在高水平上。

  使用DEFINE指令还能做到其他的一些事情,比如向上面的例子那样,定义一个调试(Debug)的编译器条件标识符(Conditional Symbol),然后在代码中插入一些只想在调试时才想执行的代码,比如记录程序每调用一个函数的返回值之类的,用$IFDEF来控制,是非常方便的,如果等到程序要发布,不想要再编译那些调试用的代码,只需注释掉Debug的定义就可以了,而不需要把每个调试用的代码都注释掉。在某些情况下,无法使用Delphi的调试器,比如正在使用Direct Draw,或者调试一个同时被多个程序使用的DLL那样,就只能把要调试的内容记录一个文件中,用查看文件记录的方式来调试,这样子本来就很烦了,如果要维护这样的程序,最好使用DEFINE,否则来回改动程序只会使自己更烦。
  $DEFINE的作用还可以继续引申开去,凡是程序有多个代码版本的情况,都可以使用$DEFINE的方便功能。好了,这一章就说到这里吧。请继续留意下一章的出现。

深入Delphi (三)

Windows 消息机制

by machine

  大家是不是很奇怪为什么我还没说到Delphi的控件呢?不过不用着急,有关深入控件的内容,将会很快出现了,但在这之前,还得了解Windows图形界面程序的机制——Windows的消息机制。
  使用过Delphi的朋友都知道,Delphi是一个真正面向对象的编程环境,但是不但如此,Delphi的这种面向对象的机制是单纯的建立在Windows的消息机制上的Delphi代码,而不是像VB、VFP之类的调用DLL、OCX,通过查看Delphi控件的源代码,你可以知道整个机制是怎样组织起来的,而且你可以完完全全地控制这些控件,因为它们只是用Delphi代码编写的,而不是存在在DLL中看不见的东西。
  那么Windows所谓的消息(Message)机制到底是什么呢?还记得以前学Basic的时候,根本没有什么事件之类的东西,整个程序是用流程图来描绘的,在程序需要键盘输入的地方,整个程序就停下来等待输入,然后根据输入来做不同的事情。这样做本来是没什么问题的,但是到了图形界面的时候,情况不同了,鼠标输入成了一个很大的问题,而且在Windows这样的多任务系统下,不可能让一个程序不断的测试设备状态那样子来获得输入。
  总之呢,Windows下的消息机制是完全不同的,即使与单纯的中断事件比起来,还是有很多不同的地方。
  简单的说来,一个线程在创建窗体的时候,会自动生成一个消息队列,但窗体不是必要的,可以通过其他途径来创建消息队列。然后,其他的程序,或者Windows系统本身,可以向这个线程发送消息到它的消息队列中,通知这个线程有什么东西发生了,这就是所谓的事件。每个进程都可以使用GetMessage函数获得它的消息队列中最前面的一个消息,GetMessage同时会自动将此消息从消息队列中删除掉,当然也可以指定不删除消息,这个以后再说。光听可能还是不能想象出来,那就看以下的例子吧:

program Sample3;

uses
 Windows,
 Messages;

var Msg: TMsg;

begin
  PostMessage(0, WM_USER, 0, 0); // 首先强制生成消息队列
  PeekMessage(Msg, 0, WM_USER, WM_USER, 0);
  // 然后这里就是所谓的消息循环,只有当收到WM_QUIT的消息时,GetMessage()才会返回False
  while GetMessage(Msg, 0, 0, 0) do begin
  end;
  // 这里可以做程序结束前(收到WM_QUIT后)的工作
end.

  这个程序运行后不会干任何事情,同时也会忽略一切Windows发给它的消息,除了WM_QUIT之外,因为GetMessage这个函数有一个特点,当收到其他消息的时候,GetMessage的返回值是TRUE,而在收到WM_QUIT的时候返回值则为FALSE,因此消息循环就被打破了。在Windows关闭的时候,Windows会自动发一个WM_QUIT的消息到这个程序的主线程,然后程序就退出了。
  绝大多数的程序的主体就是这个样子,都有一个消息循环,也就是说,每一个程序都是不断的使用GetMessage尝试获得新的消息,然后处理,周而复始,直到收到WM_QUIT为止。而其中高明之处,就是GetMessage在被调用的时候,如果检查出消息队列中没有消息,则函数不会马上返回,而是使线程转入睡眠状态,因而线程不会因为不断的循环而浪费CPU时间。在有新的消息收到之后,线程会重新苏醒,GetMessage把收到的消息放到一个TMsg类型的参数里面返回,于是程序就可以处理这个消息了。
  好了,这个消息机制是如何和窗体程序结合在一起的呢?换句话说,如果程序生成了窗体,那么程序又如何通过这个消息机制获取用户的输入消息呢?这就要从创建窗体的过程说起了。以下是一个比较复杂一点的例子:

program Sample4;

uses
 Windows,
 Messages;

var
 Msg: TMsg;
 wc: TWndClass; // RegisterClass()所需要的参数
 hWnd: THandle; // 主窗体的句柄

const
 ClassName = 'MainWClass';

function MainWndProc(Handle: THandle; MsgID: UINT; wParam, lParam: Integer): LRESULT; stdcall;
begin
  Result := 1;
  case MsgID of

    WM_CLOSE: begin // 关闭窗体所产生的消息
      if MessageBox(Handle,
        '要关闭这个程序吗?',
        '例子程序-4',
        MB_ICONQUESTION or MB_YESNO
          ) = IDYES then
        DestroyWindow(hWnd)
      else
        Result := 0;
      Exit;
    end;

    WM_DESTROY: begin // DestroyWindow()所产生的消息
      PostQuitMessage(0);
    end;

  end;
  // 剩下的消息交给Windows预设的处理函数就可以了,比如画窗体的WM_NCPAINT消息等
  Result := DefWindowProc(Handle, MsgID, wParam, lParam);
end;

begin
  // 首先使用RegisterClass()注册窗体的类,这可不是Delphi数据类型中的类哦!
  wc.style := CS_HREDRAW or CS_VREDRAW;
  wc.lpfnWndProc := @MainWndProc; // 消息处理函数的地址
  wc.hInstance := hInstance; // 程序的句柄,同时也是基地址
  wc.hIcon := LoadIcon(0, PChar(IDI_APPLICATION));
  wc.hCursor := LoadCursor(0, IDC_ARROW); // 图标
  wc.hbrBackground := GetStockObject(WHITE_BRUSH); // 背景画刷
  wc.lpszClassName := ClassName; // 前面定义的常量
  if RegisterClass(wc) = 0 then Halt(0);
  hWnd := CreateWindowEx(0,
    ClassName, // 刚才注册的类的名称
    'Sample', // 窗体的标题
    WS_OVERLAPPEDWINDOW, // 窗体有标题栏、系统菜单、最大小化菜单,以及拉伸边框
    Integer(CW_USEDEFAULT),
    Integer(CW_USEDEFAULT),
    Integer(CW_USEDEFAULT),
    Integer(CW_USEDEFAULT),
    0,
    0,
    hInstance,
    nil
  );
  if hWnd = 0 then Halt(0);
  ShowWindow(hWnd, CmdShow);
  UpdateWindow(hWnd);
  while GetMessage(Msg, 0, 0, 0) do begin
    TranslateMessage(Msg);
    DispatchMessage(Msg); // 该API将消息分派到相应的窗体消息处理函数
  end;
  ExitCode := Msg.wParam;
end.

  由于要说的东西比较多,其中的API说明、定义就请各位自己查看SDK了。在创建窗体之前,首先需要向Windows注册窗体的类。所谓的注册窗体类,就是要填充一个TWndClass结构的数据,设定这个类的属性,然后传递给RegisterWindowClass()。在这些属性当中,就包括这个类的窗体消息处理函数的指针,然后还有这个类的名称。在用CreateWindowEx创建主窗体的时候,就可以根据类的名称创建这个类的窗体了。那么窗体消息处理函数是用来干什么的呢?
  需要注意的是,一个线程可以创建多个窗体,这些窗体可以是你的程序创建的,也可能是Windows在你的程序运行过程当中创建的,比如用户点击窗体左上角的系统图标时Windows会生成一个系统菜单——在Windows里,所有你能看到的东西,包括编辑框、按钮这些东西都叫做窗体,并非程序主窗体、子窗体才叫做窗体的哦!那么问题也就随之而来了,一个线程中消息队列只有一个,但窗体有这么多,如果所有消息都在主程序的消息循环中处理,那么编写大的程序将非常困难,而且结构混乱的程序维护起来也很麻烦。于是这个窗体消息处理函数(Window Procedure)就起作用了,由于可以为每一个类编写消息处理函数,因此只需要在收到相应窗体的消息的时候,把消息传递给相应类的消息处理函数处理就可以了,整个程序就变得十分结构化。程序中甚至不需要记录每一个窗体对应的类消息处理函数是哪一个,直接调用DispatchMessage() Windows就会自动使用相应的消息处理函数了。
  另一个函数DefWindowPro()也是一个关键之处,试一下把这一行省略,看看程序运行后有什么效果?事实上,这样做之后,甚至连窗体你都不会看得到,Windows只是为你所创建的窗体在屏幕上保留了一个位置。为什么呢?你的程序并没有画窗体啊!别以为创建了窗体之后Windows就会为你完成一切,其实是DefWindowProc()处理了WM_PAINT和WM_NCPAINT的消息,完成了画窗体的工作,DefWindowProc为一般的程序主窗体做了很多这样的幕后工作哦!也由此可见,消息并不只是用户鼠标、键盘的输入消息,而是程序和Windows系统的联系工具,以后就会知道,消息还有很多用处呢,而编写Delphi控件很多时候也离不开消息。好了,这一章就讲到这里吧。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值