Board logo

Subject: DELPHI的非正常窗体 [Print This Page]

Author: bahamut8348    Time: 2007-9-4 10:40     Subject: DELPHI的非正常窗体

最近碰到一个问题,那就是: 我需要有一个窗体无标题栏也没有边框(即bsNone风格窗体),而且需要标准的WINDOWS窗体的系统菜单, 用过DELPHI的都知道,DELPHI创建出来的窗体的菜单只有"最小化","还原"和"关闭"几项.在查阅一些资料以后,了解原来是Application在作祟,以下是《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的所有版本。


    按照上面的方法,在正常情况下,是可以使DELPHI中的窗体拥有一个标准的系统菜单,但是如果把FORM设置成bsNone的话,那么窗体是没有菜单的,而且ShowWindow类似的函数也是不起作用的,在HELP里查了查资料,貌似已经解决这个问题,还没有做测试,不知道是不是有什么问题,下面是代码:

program MainPro;
uses
  FormStyle in 'FormStyle.pas',
  Forms,
  Windows,
  Main in 'Main.pas' {FrmMain};

{$R *.res}

begin
   Application.Initialize;
   Application.CreateForm(TFrmMain, FrmMain);
   Application.Handle:= Application.MainForm.Handle;
   Application.MainForm.BringToFront;
  Application.Run;
end.

unit FormStyle;

interface

uses
  Windows;

function SetFormStyle(hwnd: HWND): Boolean;

implementation

function SetFormStyle(hwnd: HWND): Boolean;
  var
    Style: Integer;
begin
  Result:= False;
  Style:= GetWindowLong(hwnd, GWL_STYLE);
  if Style = 0 then
    Exit;

  Style:= Style and not WS_CAPTION;     
  Style:= Style and not WS_SIZEBOX;     
  Style:= Style and not WS_MAXIMIZEBOX;
  Result:= SetWindowLong(hwnd, GWL_STYLE, Style) <> 0;
end;

initialization
  IsLibrary:= True;
end.

unit Main;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, FormStyle;
type
  TFrmMain = class(TForm)
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  FrmMain: TFrmMain;

implementation

{$R *.dfm}

procedure TFrmMain.FormCreate(Sender: TObject);
begin
  SetFormStyle(Self.Handle);
end;

end.

最后说下,我前段时间在盒子上看到了有修改版的FORM.PAS单元,听说可以达到效果,但是个人感觉比较麻烦,毕竟要动VCL源码,呵
Author: Rainstorey    Time: 2007-9-4 10:47

D2007解决了这个问题,不过还是有些小缺陷.
Author: Passion    Time: 2007-9-4 16:48

貌似关键是不让Application创建隐藏窗口,然后用主窗口替代它。
不知道副作用如何。
Author: shenloqi    Time: 2007-9-4 17:09

之间没有怎么看代码,但是看了这个代码,感觉在D7下应该有不少副作用,可能对于巴哈姆特的工程来说不是问题,但是对于别的工程来说就不行了,我没有测试,但是我估计对于MDI窗体,或者是替换Application的事件消息处理或者是当主窗体最小化之后需要让所有窗体最小化等等各种窗体之间的控制情况来说会有问题,只是猜测,要是有人测试出结果可以告知一下。
Author: bahamut8348    Time: 2007-9-4 20:33

经过测试,的确有不少问题,主要还是在消息处理上,我想如果要完美解决的话,可能需要把整个APPLICATION的消息分发搬过来。呵
Author: Rainstorey    Time: 2007-9-5 08:44

在程序启动时加上这一句:
AppendMenu(GetSystemMenu(Application.Handle,True),MF_SEPARATOR,0,nil);
Author: bahamut8348    Time: 2007-9-5 22:20

LS的方法是可以加上标准的系统菜单,但是问题是添加出来的系统菜单都是不可用的,刚刚翻了翻VCL的代码找到了出问题的代码行:

procedure TApplication.CreateHandle;
begin
    SysMenu := GetSystemMenu(FHandle, False);
    DeleteMenu(SysMenu, SC_MAXIMIZE, MF_BYCOMMAND);
    DeleteMenu(SysMenu, SC_SIZE, MF_BYCOMMAND);
    if NewStyleControls then DeleteMenu(SysMenu, SC_MOVE, MF_BYCOMMAND);
  end;
end;

    就是这里删除了3个菜单项,如果只是简单的注释掉的话,和LS的效果一样,如果要使这几个菜单项可用的话,需要在这个之前的
    FHandle := CreateWindow(WindowClass.lpszClassName, PChar(FTitle),
      WS_POPUP or WS_CAPTION or WS_CLIPSIBLINGS or WS_SYSMENU
      or WS_MINIMIZEBOX,
      GetSystemMetrics(SM_CXSCREEN) div 2,
      GetSystemMetrics(SM_CYSCREEN) div 2,
      0, 0, 0, 0, HInstance, nil);
    这里改成
    FHandle := CreateWindow(WindowClass.lpszClassName, PChar(FTitle),
      WS_POPUP or WS_CAPTION or WS_CLIPSIBLINGS or WS_SYSMENU
      or WS_MINIMIZEBOX or WS_MAXIMIZEBOX or WS_SIZEBOX,
      GetSystemMetrics(SM_CXSCREEN) div 2,
      GetSystemMetrics(SM_CYSCREEN) div 2,
      0, 0, 0, 0, HInstance, nil);

    问题是,如果这样修改了以后,那么就需要在消息处理过程WndProc里添上相应的响应消息的代码:
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;
end;
这里需要加上那三个系统菜单的消息响应代码,我之前是这样改的:
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
            begin
              Result:= SendMessage(FMainForm.Handle, Msg, wParam, lParam);
              Default;
            end;
      end;
end;

但是这样修改存在问题,同样需要在TCustomForm的消息处理里面添加相应的消息来操作Application的MainForm对象,而且,在触发SC_MOVE消息的时候那个原本隐藏的窗体竟然会显示标题栏出来,现在还没找到更好的方法,哎
Author: Passion    Time: 2007-9-5 22:52

巴哈姆特这么有钻研精神,以前还真没见过呢。
Author: bahamut8348    Time: 2007-9-6 10:27

BS啸啸,偶一直都这么有钻研精神的,竟然说以前没见过,强烈的BS你,
Author: Passion    Time: 2007-9-7 22:18

嘿嘿。
Author: stanleyxu2005    Time: 2007-9-18 16:22



QUOTE:
原帖由 Passion 于 2007-9-4 16:48 发表
貌似关键是不让Application创建隐藏窗口,然后用主窗口替代它。
不知道副作用如何。

Delphi 种种非标准处理方法我一直是很痛恨的。尽管看上去没什么问题,但很多第三方控件都在这个基础上被开发被使用。
换句话说,一旦Delphi 本身修正了某个非标准的处理,可能导致一大批第三方控件工作不正常。
就像我上次解决了非标准tooltip的问题,但是VirtualTree就不能正常使用了。因为VT自己重写了THintWindow。
Author: stanleyxu2005    Time: 2007-9-18 16:28

我在想,能不能当鼠标右键taskbar的程序按钮时,动态修改菜单内容?
我个人不主张使用d2007的办法,因为这样的话,隐藏窗口就彻底没了。这个窗口有一定的好处。
而且可能导致第三方软件不正常。
Author: Dalas    Time: 2007-10-15 23:35

以前一直没注意,今天看到了,我试了一下:

我开了4个程序(IE、Explorer.exe、Delphi2007安装程序、我自己用D2007写的程序),
在任务栏点右键-->并排显示窗口/层叠窗口/堆叠显示窗口  都正常啊,系统菜单也看不出有什么分别,我是在Vista上试的。
Author: stanleyxu2005    Time: 2007-10-29 17:40



QUOTE:
原帖由 Dalas 于 2007-10-15 23:35 发表
以前一直没注意,今天看到了,我试了一下:

我开了4个程序(IE、Explorer.exe、Delphi2007安装程序、我自己用D2007写的程序),
在任务栏点右键-->并排显示窗口/层叠窗口/堆叠显示窗口  都正常啊,系统菜单也看不出有什么分 ...

那是因为你使用了d2007的关系。d7就不行。




Welcome to CnPack Forum (http://bbs.cnpack.org/) Powered by Discuz! 5.0.0