Delphi中正常窗口的實現

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