1. 程式人生 > >Delphi中正常窗口的實現

Delphi中正常窗口的實現

gin inno 其他 nap 文件 nac 2.3 default hellip

摘要:

在Delphi的VCL庫中,為了使用以及實現的方便,應用對象Application創建了一個用來
處理消息響應的隱藏窗口。而正是這個窗口,使得用VCL開發出來的程序存在著與其他窗口不
能正常排列平鋪等顯得有些畸形的問題。本文通過對VCL的深入分析,給出了一個只需要對應
用程序項目文件作3行代碼的修改就能解決問題的方案,且不需要原有的編程方式作任何改變。

一、引言

  用Delphi所提供的VCL類庫編寫的Windows應用程序,有一個明顯不同於標準Windows窗口
的特點--主窗口的系統菜單與任務欄上的系統菜單不相同。一般情況下,主窗口的系統菜單
有六個菜單項而任務欄系統菜單只有三個菜單項。實際使用中我們發現用VCL開發的程序有以

下幾個方面的尷尬:

  1)不夠美觀。這是肯定的,與標準不符自然會顯得有些畸形。
  2)主窗口最小化時沒有動畫效果。
  3)窗口不能正常與其它窗口排列平鋪。
  4)任務欄系統菜單具有最高的優先級。在存在模態窗口的情況下整個程序仍然可以被最
小化,與模態窗口的設計相違背。

  主窗口最小化動畫效果的問題在Delphi 5.0以後的版本中已通過Forms.pas中的
ShowWinNoAnimate函數解決,但其余幾個問題則一直存在。盡管多數情況下這不會對應用程
序帶來什麽影響,但在一些追求專業效果的場合確實不可接受的。由於C++ Builder與Delph
i使用的是同一套類庫,所以上述問題同樣存在於使用C++ Builder編寫的Windows應用程序中

。在以前的文章裏(阿甘的家中可以找到),我已討論過這個問題,當時的敘述看起來基本
上是一種取巧的方法,而我也是在偶然之中才找到那個方法的。本文的任務就是通過對VCL類
庫作一些分析,說明那樣做的原理,其次再給出一個只用3行代碼的方法,完完全全地解決Delphi
中這個"非正常窗口"的問題。

二、 原理

2.1 應用程序的創建過程

  下面是一個典型的應用程序的Delphi工程文件,我們註意到一開始就有一個對Applicat
ion對象的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.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窗口正常排列平鋪。但帶來的
問題是窗口無法最小化。怎麽回事呢?還是老方法,跟蹤一下。


2.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的所有版本。


>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
註意事項:
 1. 在Application.Handle := FormMain.Handle;之後還需要加上 FormMain.BringToFront;
否則主窗體是inactive的狀態(非獲得焦點狀態)。

 2.這樣的應用可能隱含了一部分潛在和未知的問題,建議不要輕易使用。

 

Delphi中正常窗口的實現