轉自http://blog.csdn.net/cathyeagle
1 引言
用Delphi所提供的VCL類庫編寫的Windows應用程式,有一個明顯不同於標準Windows視窗的特點--主視窗的系統功能表與工作列上的系統功能表不相同。一般情況下,主視窗的系統功能表有六個功能表項目而工作列系統功能表只有三個功能表項目。實際使用中我們發現用VCL開發的程式有以下幾個方面的尷尬:
1)不夠美觀。這是肯定的,與標準不符自然會顯得有些畸形。
2)主視窗最小化時沒有動畫效果。
3)視窗不能正常與其它視窗排列平鋪。
4)工作列系統功能表具有最高的優先順序。在存在模態視窗的情況下整個程式仍然可以被最小化,與模態視窗的設計相違背。
主視窗最小化動畫效果的問題在Delphi 5.0以後的版本中已通過Forms.pas中的ShowWinNoAnimate函數解決,但其餘幾個問題則一直存在。儘管多數情況下這不會對應用程式帶來什麼影響,但在一些追求專業效果的場合確實不可接受的。由於C++ Builder與Delphi使用的是同一套類庫,所以上述問題同樣存在於使用C++ Builder編寫的Windows應用程式中。
在以前的文章裡(阿甘的家中可以找到),我已討論過這個問題,當時的敘述看起來基本上是一種取巧的方法,而我也是在偶然之中才找到那個方法的。本文的任務就是通過對VCL類庫作一些分析,說明那樣做的原理,其次再給出一個只用3行代碼的方法,完完全全地解決Delphi中這個"非正常視窗"的問題。
2 原理
2.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.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;
3 實現
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的所有版本。