主题:[转帖]Delphi中正常窗口的实现
用Delphi所提供的VCL类库编写的Windows应用程序,有一个明显不同于标准Windows窗口的特点--主窗口的系统菜单与任务栏上的系统菜单不相同。一般情况下,主窗口的系统菜单有六个菜单项而任务栏系统菜单只有三个菜单项。实际使用中我们发现用VCL开发的程序有以下几个方面的尴尬:
1)不够美观。这是肯定的,与标准不符自然会显得有些畸形。
2)主窗口最小化时没有动画效果。
3)窗口不能正常与其它窗口排列平铺。
4)任务栏系统菜单具有最高的优先级。在存在模态窗口的情况下整个程序仍然可以被最小化,与模态窗口的设计相违背。
主窗口最小化动画效果的问题在Delphi 5.0以后的版本中已通过Forms.pas中的ShowWinNoAnimate函数解决,但其余几个问题则一直存在。尽管多数情况下这不会对应用程序带来什么影响,但在一些追求专业效果的场合确实不可接受的。由于C++ Builder与Delphi使用的是同一套类库,所以上述问题同样存在于使用C++ Builder编写的Windows应用程序中。
本文的任务就是通过对VCL类库作一些分析,说明那样做的原理,其次再给出一个只用3行代码的方法,完完全全地解决Delphi中这个"非正常窗口"的问题。
原理
1 应用程序的创建过程
下面是一个典型的应用程序的Delphi工程文件,我们注意到一开始就有一个对Application对象的Initialize方法的引用,我们的分析也就从这里开始:
program Project1;
uses
Forms,
Unit1 in 'Unit1.pas' {Form1};
{$R *.res}
begin
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.Run;
end.
隐藏的窗口是由Application对象创建的,那么Application对象又从何而来呢?在Delphi的代码编辑窗口中按住Ctrl点击Application就会发现,Application对象是在Forms.pas单元中定义的几个全局对象之一。这还不够,我们想要知道的是Application对象是在什么地方创建的,因为必须成功创建了TApplication类的实例我们才能引用它。
想一下,有什么代码会在Application.Initialize之前执行呢?对了,是initialization代码段中的代码。认真调试过VCL源码就可以知道,VCL中很多单元都有initialization代码段,启动Delphi程序时,先是按照uses的顺序执行每个单元中initialization代码段的代码,完成所有的初始化动作之后才执行Application的Initialize方法以初始化Application,所以很显然,Application对象是在某个单元的initialization代码段中创建的。
以"TApplication.Create"为关键字在VCL源码目录中搜索一番,我们果然在Controls.pas单元中找到了创建Application对象的代码。在Controls.pas单元的initialization代码段,有一句对InitControls过程的调用,而InitControls的实现则如下所示:
Unit Controls;
…
initialization
...
InitControls;
procedure InitControls;
begin
...
Mouse := TMouse.Create;
Screen := TScreen.Create(nil);
Application := TApplication.Create(nil);
...
end;
好,到这里我们的分析就完成了第一步,因为要解决非正常窗口的问题,我们必须要在Application对象初始化之前做一件事,因此了解应用程序的初始化过程就非常重要了。
2 IsLibrary变量
IsLibrary变量是在System.pas单元中定义的全局标志变量之一。如果IsLibrary的值为true则表明程序模块是一个动态链接库,反之就是一个可执行程序。VCL类库中的某些过程就根据这个标志变量的不同值完成不同的动作。也就是这个变量,在解决Delphi的非正常窗口问题中起到了关键性的作用。
前面说过,为了方便,Application对象初始化时创建了一个看不见的窗口(也就是用Spy++之类的工具看到的那个以"TApplication"为类名的窗口),但也正是因为这个看不见的窗口,才使得用Delphi开发出来的程序呈现诸多畸形。好了,如果我们能够去掉这个看不见的窗口(同时去掉任务栏系统菜单),代之以我们的应用程序主窗口,岂不是所有的问题都解决了?
说说简单,但实现起来需要对VCL源代码动大手术吗?如果那样岂不是有点本末倒置了?答案当然是不会,否则也不会有这篇文章了。在此我想说的是,在接下来的分析中,我们将会看到,所谓"编程之道,存乎一心",TApplication设计中无心插柳的做法,实则为我们解决这一问题留下了接口。不做源代码的分析,你可能要绕打圈子,而实际上我们会看到,天才的设计留给我们用的东西,不多也不少,刚刚好。
打开TApplication类的构造函数Create,我们会发现这样一行代码。
constructor TApplication.Create(AOwner: TComponent);
begin
...
if not IsLibrary then CreateHandle;
...
end;
这里说的是,如果程序模块不是动态链接库,那么就执行CreateHandle,而CreateHandle所做的工作在帮助中是这样说的:"如果不存在应用程序窗口,那就创建一个",这里的"应用程序窗口"就是上面所说的看不见的窗口,也即是罪魁祸首之所在,在TApplication类中用FHandle变量来保存其窗口句柄。这里就是根据IsLibrary的值完成了不同的动作,因为在动态链接库中一般并不需要消息循环的,但用VCL开发动态链接库还是要用到Application对象,所以有了这里的设计。好,我们只需要欺骗一下Application对象,在它创建之前把IsLibrary赋值为true,即可滤掉CreateHandle的执行,去掉这个讨厌的窗口了。
为IsLibrary赋值的代码显然也应该放在某个单元的initialization代码段中,而且由于initialization代码段中的代码是按照包含的单元的顺序执行的,为了保证在Application对象创建之前把IsLibrary赋值为true,在工程文件中我们必需将包含赋值代码的单元放在Forms单元之前,如下(假设该单元名为UnitDllExe.pas):
program Template;
uses
UnitDllExe in 'UnitDllExe.pas',
Forms,
FormMain in 'FormMain.pas' {MainForm},
...
UnitDllExe.pas代码清单如下:
unit UnitDllExe;
interface
implementation
initialization
IsLibrary := true;
//告诉Applciation对象,这是一个动态链接库,不需要创建隐藏窗口。
end.
好了,编译运行一下,我们看到,由于没有创建隐藏窗口,原先任务栏上的系统菜单消失了,换成了主窗口的系统菜单,主窗口也能够与其它Windows窗口正常排列平铺。但带来的问题是窗口无法最小化。怎么回事呢?还是老方法,跟踪一下。
3 主窗口最小化
最小化属于系统命令,最终必定是调用API函数DefWindowProc来将窗口最小化,所以我们毫无困难地就找到了TCustomForm中响应WM_SYSCOMMAND消息的函数WMSysCommand,其中清楚地写到将最小化的消息重定向到Application.WndProc去处理:
procedure TCustomForm.WMSysCommand(var Message: TWMSysCommand);
begin
with Message do
begin
if (CmdType and $FFF0 = SC_MINIMIZE) and (Application.MainForm = Self) then
Application.WndProc(TMessage(Message))
...
end;
end;
而在Application.WndProc中,响应最小化消息时又调用了Application的Minimize方法,所以症结一定是在Minimize过程。
procedure TApplication.WndProc(var Message: TMessage);
...
begin
...
with Message do
case Msg of
WM_SYSCOMMAND:
case WParam and $FFF0 of
SC_MINIMIZE: Minimize;
SC_RESTORE: Restore;
else
Default;
...
end;
最后,找到TApplication.Minimize,就一切都明白了。这里对于DefWindowProc函数的调用没有产生任何效果,为什么呢?由于前面我们欺骗Application对象,滤掉了CreateHandle的调用,没有创建Application对象响应消息所需要的窗口,因此导致其句柄FHandle为0,调用当然不成功了。如果能将FHandle指向我们的应用程序主窗口就能解决问题。
procedure TApplication.Minimize;
begin
...
DefWindowProc(FHandle, WM_SYSCOMMAND, SC_MINIMIZE, 0);
//这里FHandle值为0
...
end;
实现
Borland的天才们无心插柳的设计再一次让我们找到了解决问题的办法。由前面的分析我们知道,在用VCL开发的动态链接库中并没有创建隐藏的窗口来接收Windows消息(CreateHandle不执行),但在动态链接库中如果要显示窗口的话又需要一个父窗口。如何解决这个问题呢?VCL的设计者将保存看不见的窗口句柄的FHandle变量设计为可写,于是我们实际上可以简单地给FHandle赋一个值来为需要显示的子窗口提供一个父窗口。例如,在某个动态链接库插件中要显示窗体,我们通常会在主模块可执行文件中将Application对象的句柄通过动态链接库的某个函数传入并赋值给动态链接库的Application.Handle,类似于:
procedure SetApplicationHandle(MainAppWnd: HWND)
begin
Application.Handle := MainAppWnd;
end;
好了,既然Aplication.Handle实际上只是一个在内部用来响应消息的窗口句柄,而原本应该创建的看不见的窗口被我们去掉了,那我们只需要给出一个窗口的句柄,用来代替那个原本多余的隐藏窗口的句柄不就行了?这样的窗口去哪里找?应用程序的主窗口正是上上之选,于是有了下面的代码。
program Template;
uses
UnitDllExe in 'UnitDllExe.pas',
Forms,
FormMain in 'FormMain.pas' {MainForm};
{$R *.res}
begin
Application.Initialize;
Application.CreateForm(TFormMain, FormMain);
Application.Handle := FormMain.Handle;
Application.Run;
end.
于是,一切问题都解决了。你不需要对VCL源码作任何修改,不需要对原有的程序作任何修改,只要在工程文件中增加两行代码,加上UnitDllExe.pas中的一行,共三行代码,即可使得你的应用程序窗口完全和任何一个标准Windows窗口一样正常。
1)任务栏和窗口标题栏拥有一致的系统菜单。
2)主窗口最小化时有动画效果。
3)窗口能够正常与其它窗口排列平铺。
4)存在模态窗口时不能对其父窗口进行操作。
以上实现代码使用于Delphi的所有版本。
**************************************
本人补充:在Application.Handle := FormMain.Handle后面再加上FormMain.BringToFront,即:
begin
Application.Initialize;
Application.CreateForm(TFormMain, FormMain);
Application.Handle := FormMain.Handle;
FormMain.BringToFront;
Application.Run;
end.
否则运行了以后窗体处于非活动状态。
1)不够美观。这是肯定的,与标准不符自然会显得有些畸形。
2)主窗口最小化时没有动画效果。
3)窗口不能正常与其它窗口排列平铺。
4)任务栏系统菜单具有最高的优先级。在存在模态窗口的情况下整个程序仍然可以被最小化,与模态窗口的设计相违背。
主窗口最小化动画效果的问题在Delphi 5.0以后的版本中已通过Forms.pas中的ShowWinNoAnimate函数解决,但其余几个问题则一直存在。尽管多数情况下这不会对应用程序带来什么影响,但在一些追求专业效果的场合确实不可接受的。由于C++ Builder与Delphi使用的是同一套类库,所以上述问题同样存在于使用C++ Builder编写的Windows应用程序中。
本文的任务就是通过对VCL类库作一些分析,说明那样做的原理,其次再给出一个只用3行代码的方法,完完全全地解决Delphi中这个"非正常窗口"的问题。
原理
1 应用程序的创建过程
下面是一个典型的应用程序的Delphi工程文件,我们注意到一开始就有一个对Application对象的Initialize方法的引用,我们的分析也就从这里开始:
program Project1;
uses
Forms,
Unit1 in 'Unit1.pas' {Form1};
{$R *.res}
begin
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.Run;
end.
隐藏的窗口是由Application对象创建的,那么Application对象又从何而来呢?在Delphi的代码编辑窗口中按住Ctrl点击Application就会发现,Application对象是在Forms.pas单元中定义的几个全局对象之一。这还不够,我们想要知道的是Application对象是在什么地方创建的,因为必须成功创建了TApplication类的实例我们才能引用它。
想一下,有什么代码会在Application.Initialize之前执行呢?对了,是initialization代码段中的代码。认真调试过VCL源码就可以知道,VCL中很多单元都有initialization代码段,启动Delphi程序时,先是按照uses的顺序执行每个单元中initialization代码段的代码,完成所有的初始化动作之后才执行Application的Initialize方法以初始化Application,所以很显然,Application对象是在某个单元的initialization代码段中创建的。
以"TApplication.Create"为关键字在VCL源码目录中搜索一番,我们果然在Controls.pas单元中找到了创建Application对象的代码。在Controls.pas单元的initialization代码段,有一句对InitControls过程的调用,而InitControls的实现则如下所示:
Unit Controls;
…
initialization
...
InitControls;
procedure InitControls;
begin
...
Mouse := TMouse.Create;
Screen := TScreen.Create(nil);
Application := TApplication.Create(nil);
...
end;
好,到这里我们的分析就完成了第一步,因为要解决非正常窗口的问题,我们必须要在Application对象初始化之前做一件事,因此了解应用程序的初始化过程就非常重要了。
2 IsLibrary变量
IsLibrary变量是在System.pas单元中定义的全局标志变量之一。如果IsLibrary的值为true则表明程序模块是一个动态链接库,反之就是一个可执行程序。VCL类库中的某些过程就根据这个标志变量的不同值完成不同的动作。也就是这个变量,在解决Delphi的非正常窗口问题中起到了关键性的作用。
前面说过,为了方便,Application对象初始化时创建了一个看不见的窗口(也就是用Spy++之类的工具看到的那个以"TApplication"为类名的窗口),但也正是因为这个看不见的窗口,才使得用Delphi开发出来的程序呈现诸多畸形。好了,如果我们能够去掉这个看不见的窗口(同时去掉任务栏系统菜单),代之以我们的应用程序主窗口,岂不是所有的问题都解决了?
说说简单,但实现起来需要对VCL源代码动大手术吗?如果那样岂不是有点本末倒置了?答案当然是不会,否则也不会有这篇文章了。在此我想说的是,在接下来的分析中,我们将会看到,所谓"编程之道,存乎一心",TApplication设计中无心插柳的做法,实则为我们解决这一问题留下了接口。不做源代码的分析,你可能要绕打圈子,而实际上我们会看到,天才的设计留给我们用的东西,不多也不少,刚刚好。
打开TApplication类的构造函数Create,我们会发现这样一行代码。
constructor TApplication.Create(AOwner: TComponent);
begin
...
if not IsLibrary then CreateHandle;
...
end;
这里说的是,如果程序模块不是动态链接库,那么就执行CreateHandle,而CreateHandle所做的工作在帮助中是这样说的:"如果不存在应用程序窗口,那就创建一个",这里的"应用程序窗口"就是上面所说的看不见的窗口,也即是罪魁祸首之所在,在TApplication类中用FHandle变量来保存其窗口句柄。这里就是根据IsLibrary的值完成了不同的动作,因为在动态链接库中一般并不需要消息循环的,但用VCL开发动态链接库还是要用到Application对象,所以有了这里的设计。好,我们只需要欺骗一下Application对象,在它创建之前把IsLibrary赋值为true,即可滤掉CreateHandle的执行,去掉这个讨厌的窗口了。
为IsLibrary赋值的代码显然也应该放在某个单元的initialization代码段中,而且由于initialization代码段中的代码是按照包含的单元的顺序执行的,为了保证在Application对象创建之前把IsLibrary赋值为true,在工程文件中我们必需将包含赋值代码的单元放在Forms单元之前,如下(假设该单元名为UnitDllExe.pas):
program Template;
uses
UnitDllExe in 'UnitDllExe.pas',
Forms,
FormMain in 'FormMain.pas' {MainForm},
...
UnitDllExe.pas代码清单如下:
unit UnitDllExe;
interface
implementation
initialization
IsLibrary := true;
//告诉Applciation对象,这是一个动态链接库,不需要创建隐藏窗口。
end.
好了,编译运行一下,我们看到,由于没有创建隐藏窗口,原先任务栏上的系统菜单消失了,换成了主窗口的系统菜单,主窗口也能够与其它Windows窗口正常排列平铺。但带来的问题是窗口无法最小化。怎么回事呢?还是老方法,跟踪一下。
3 主窗口最小化
最小化属于系统命令,最终必定是调用API函数DefWindowProc来将窗口最小化,所以我们毫无困难地就找到了TCustomForm中响应WM_SYSCOMMAND消息的函数WMSysCommand,其中清楚地写到将最小化的消息重定向到Application.WndProc去处理:
procedure TCustomForm.WMSysCommand(var Message: TWMSysCommand);
begin
with Message do
begin
if (CmdType and $FFF0 = SC_MINIMIZE) and (Application.MainForm = Self) then
Application.WndProc(TMessage(Message))
...
end;
end;
而在Application.WndProc中,响应最小化消息时又调用了Application的Minimize方法,所以症结一定是在Minimize过程。
procedure TApplication.WndProc(var Message: TMessage);
...
begin
...
with Message do
case Msg of
WM_SYSCOMMAND:
case WParam and $FFF0 of
SC_MINIMIZE: Minimize;
SC_RESTORE: Restore;
else
Default;
...
end;
最后,找到TApplication.Minimize,就一切都明白了。这里对于DefWindowProc函数的调用没有产生任何效果,为什么呢?由于前面我们欺骗Application对象,滤掉了CreateHandle的调用,没有创建Application对象响应消息所需要的窗口,因此导致其句柄FHandle为0,调用当然不成功了。如果能将FHandle指向我们的应用程序主窗口就能解决问题。
procedure TApplication.Minimize;
begin
...
DefWindowProc(FHandle, WM_SYSCOMMAND, SC_MINIMIZE, 0);
//这里FHandle值为0
...
end;
实现
Borland的天才们无心插柳的设计再一次让我们找到了解决问题的办法。由前面的分析我们知道,在用VCL开发的动态链接库中并没有创建隐藏的窗口来接收Windows消息(CreateHandle不执行),但在动态链接库中如果要显示窗口的话又需要一个父窗口。如何解决这个问题呢?VCL的设计者将保存看不见的窗口句柄的FHandle变量设计为可写,于是我们实际上可以简单地给FHandle赋一个值来为需要显示的子窗口提供一个父窗口。例如,在某个动态链接库插件中要显示窗体,我们通常会在主模块可执行文件中将Application对象的句柄通过动态链接库的某个函数传入并赋值给动态链接库的Application.Handle,类似于:
procedure SetApplicationHandle(MainAppWnd: HWND)
begin
Application.Handle := MainAppWnd;
end;
好了,既然Aplication.Handle实际上只是一个在内部用来响应消息的窗口句柄,而原本应该创建的看不见的窗口被我们去掉了,那我们只需要给出一个窗口的句柄,用来代替那个原本多余的隐藏窗口的句柄不就行了?这样的窗口去哪里找?应用程序的主窗口正是上上之选,于是有了下面的代码。
program Template;
uses
UnitDllExe in 'UnitDllExe.pas',
Forms,
FormMain in 'FormMain.pas' {MainForm};
{$R *.res}
begin
Application.Initialize;
Application.CreateForm(TFormMain, FormMain);
Application.Handle := FormMain.Handle;
Application.Run;
end.
于是,一切问题都解决了。你不需要对VCL源码作任何修改,不需要对原有的程序作任何修改,只要在工程文件中增加两行代码,加上UnitDllExe.pas中的一行,共三行代码,即可使得你的应用程序窗口完全和任何一个标准Windows窗口一样正常。
1)任务栏和窗口标题栏拥有一致的系统菜单。
2)主窗口最小化时有动画效果。
3)窗口能够正常与其它窗口排列平铺。
4)存在模态窗口时不能对其父窗口进行操作。
以上实现代码使用于Delphi的所有版本。
**************************************
本人补充:在Application.Handle := FormMain.Handle后面再加上FormMain.BringToFront,即:
begin
Application.Initialize;
Application.CreateForm(TFormMain, FormMain);
Application.Handle := FormMain.Handle;
FormMain.BringToFront;
Application.Run;
end.
否则运行了以后窗体处于非活动状态。