Delphi對象變成Windows控件的前世今生(關鍵是設置句柄和回調函數)goodx


----------------------------------------------------------------------
第一步,准備工作:預定義一個全局Win控件變量,以及一個精簡化的Win控件類
var
CreationControl: TWinControl = nil; // 定義全局變量,用來表示每次剛創建的Win控件

TWinControl = class(TControl)
private
FDefWndProc: Pointer; // 記錄原有的窗口過程,但只有真正創建句柄的時候才會記錄。只有Windows控件才有默認窗口處理過程,而TControl有FWindowProc,不是一回事
FObjectInstance: Pointer; // 普通指針(連函數指針都不是)。當轉發消息的時候,使用這個普通窗口函數地址(不是類窗口函數地址)。控件創建的時候就會做轉換。
FHandle: HWnd; // Windows窗口的真實句柄
FParentWindow: HWnd; // 父窗口的句柄也要記錄下來。父控件(類,具有許多額外功能)與父句柄(Windows指針,特簡單)不是一回事。這個屬性在一般VCL控件里根本用不到,只有ActiveX可能用到
protected
procedure MainWndProc(var Message: TMessage); // 非虛函數,調用WindowProc函數,不希望被覆蓋(如果要覆蓋就覆蓋WndProc函數,而且這也不是唯一的辦法)
procedure WndProc(var Message: TMessage); override; // 虛函數,處理少部分消息,最后調用父類同名函數
// 創建和銷毀窗口句柄,按調用順序排列:
procedure CreateHandle; virtual; // 虛函數,關鍵入口,被UpdateShowing和HandleNeeded調用,事實是子類從來沒有被覆蓋。
procedure CreateWnd; virtual; // 虛函數,注冊窗口類。很多子類都覆蓋它,為的是加上一些額外的功能,比如TEdit
procedure CreateParams(var Params: TCreateParams); virtual; // 第一次出現。只有windows控件才需要准備一大堆內容
procedure CreateWindowHandle(const Params: TCreateParams); virtual; // 虛函數,簡單函數,調用API, 看名字就很清楚功能。子類有時候覆蓋它,TEdit和TMemo
end;

----------------------------------------------------------------------
第二步,調用控件構造函數,申請Delphi控件對象的內存空間。此時這個內存中的控件:
1. 沒有Windows句柄,
2. 預備了一個MakeObjectInstance轉換后的窗口回調函數指針FObjectInstance,它封裝了MainWndProc函數(或者說,它就是MainWndProc函數)。MainWndProc封裝了程序員要用到的窗口回調函數WndProc。但這步僅僅是預備窗口函數指針FObjectInstance,並沒有做任何使用和設置,使它與一個Windows窗口聯系起來。
到這步,在內存中還僅僅是簡單的Delphi內存對象,並沒有把它與Windows操作系統聯系起來使之真正成為一個Windows窗口對象。

TButton.Create;
調用inherited Create(AOwner);

TWinControl.Create;
調用inherited Create(AOwner);
調用FObjectInstance := Classes.MakeObjectInstance(MainWndProc); // 全局函數,把類函數指針MainWndProc轉換成 普通指針(連函數指針都不是)。注意只有Windows控件才有這項

----------------------------------------------------------------------
第三步,依次調用函數,注冊Windows窗口類,使之與當前Delphi對象聯系起來(其實是Delphi對象包含它,因為Delphi對象包括了許多其它內容),最關鍵的有:
0. 在CreateHandle中(即入口函數),它會調用CreateWnd函數,而CreateHandle本身又會被UpdateShowing和HandleNeeded調用,其中UpdateShowing會被TWinControl.UpdateControlState;調用,UpdateControlState會被TWinControl.InsertControl調用,InsertControl會被TControl.SetParent調用,詳情見:
http://www.cnblogs.com/findumars/p/3917061.html
http://www.cnblogs.com/findumars/p/3667031.html
1. 在CreateWnd中,根據Delphi控件的值,准備Params
2. 在CreateWnd中,強行取消注冊當前Delphi類(比如TButton),然后設置窗口函數Params.WindowClass.lpfnWndProc := @InitWndProc;
3. 在CreateWnd中,重新注冊了Windows窗口Windows.RegisterClass(Params.WindowClass)
4. 在CreateWnd中,執行CreationControl := Self; 此時這個CreationControl就是代表Delphi內存控件
5. 在CreateWnd中,執行CreateWindowHandle(Params); 真正創建Windows窗口,並立即給這個窗口發送WM_NCCREATE消息,在函數返回之前,就跳轉到回調函數InitWndProc里執行(即后面的6~10),然后才將其句柄賦值給Delphi控件屬性FHandle(這個賦值其實多余,去掉賦值照樣沒問題,因為在回調函數里已經賦值了)。
注意1,通過實驗發現,在CreateWindowEx這個WINAPI返回之前,就已經發送了WM_NCCREATE消息,因此WINAPI返回之前就會執行InitWndProc回調函數。可以這樣理解:CreateWindowEx函數的內部實現就是先創造FHandle,然后就是SendMessage(FHandle, WM_NCCREATE),回調函數會立刻工作,而此時還沒有跳出CreateWindowEx函數呢,因為后面還有兩個消息要發送,外加其它善后事宜。
我的理解是,只要成功創建了這個windows窗口就會有句柄(這是將來消息找到這個窗口的唯一依據),不管這個windows窗口是否顯示,更不管它是否與Delphi對象相聯系,Windows都會給它發送WM_NCCREATE消息。注意這個Windows的窗口函數在注冊Windows窗口類的時候就已經存在了(即InitWndProc),所以一定可以執行和處理這個消息。
注意2,由於在回調函數里已經給FHandle屬性賦值了,所以FHandle := CreateWindowEx(ExStyle...),這里的FHanle賦值可以去掉,運行幾個demo都正常。但是百思不得其解的是,把InitWndProc的CreationControl.FHandle := HWindow;屏蔽掉,留下FHandle := CreateWindowEx(ExStyle...)卻始終報錯A call to an OS function failed。經過檢測,發現此時CreateWindowEx的返回值為0,不懂為什么。
6. 在InitWndProc中,當第一個消息(WM_NCCREATE)來的時候,就執行CreationControl.FHandle := HWindow;,這樣當前Delphi控件第一次有了句柄(最關鍵的第一步)。
注意,必須執行這一步,如果屏蔽這句話就會出現A call to an OS function failed的錯誤。即使想了個花招(這招可以使主Form和Button正常創建,然后用Button動態創建TEdit,且Edit.tag=100,這樣可以專用測試),if (CreationControl.tag<>100) CreationControl.FHandle := HWindow; 也不行。報錯的語句顯然是if FHandle = 0 then RaiseLastOSError; 通過單步測試,此時InitWndProc仍可正常執行,但不知道為什么FHandle := CreateWindowEx(ExStyle...)的返回值就變0了。
7. 在InitWndProc中,重新設置以HWindow代表的Windows窗口實例(也就是Delphi控件實例)的窗口函數為預設的FObjectInstance,這樣當前Delphi控件的窗口回調函數就是FObjectInstance了,即指向Delphi類的虛函數WndProc了(最關鍵的第二步)。
8. 在InitWndProc中,對回調函數所需要的4個參數依次壓棧,使之符合Windows標准回調函數的stdcall口味
9. 在InitWndProc中,將CreationControl的地址值轉移到EAX,並將CreationControl清空,即CreationControl代表的Delphi控件實例的臨時任務完成了,准備讓下一個新的Delphi控件實例使用
10.在InitWndProc中,使用EAX到內存中找到當前Delphi控件,把它轉化成TWinControl,然后直接調用它的FObjectInstance函數處理消息,參數就是剛才壓棧的那些參數,這樣第一個消息就處理完畢了。處理這個消息的目的有多個,都十分重要,依次為:
1)記錄windows控件的句柄到Delphi對象的屬性里
2)把這個Windows窗口的回調函數替換為Delphi對象的FObjectInstance,使之間接調用Delphi對象的虛函數WndProc,方便程序員改寫
3)用三種方法在全局記錄這個windows句柄的ID
4) 上述三個主要目的已經達到,所以盡管WM_NCCREATE消息本身沒什么用(一般情況下,因為程序員仍可改寫),但消息來了必須處理,所以通過變換手段之后,使用新的回調函數FObjectInstance對消息進行處理。如果程序員也需要使用WM_NCCREATE消息執行某些邏輯,仍可在WndProc和動態函數中正常執行。有一個疑問是,如果屏蔽這段匯編就會出錯,錯誤停留在TWinControl.DefaultHandler的CallWindowProc(FDefWndProc,FHandle,Msg,WParam,LParam);處;把這段匯編改成CALL DefWindowProc也是一樣的錯誤,原因可能是消息必須處理?
11.在CreateHandle中,以當前Delphi控件的FHandle屬性為依據,調用SetWindowPos顯示了這個Windows窗口,對於一般程序員的理解,就是顯示了這個Delphi控件
需要強調的是,以上11個步驟,每次生成Delphi實例(比如TButton實例)都要這樣來一遍,但2、3兩步不必再次執行,因為Delphi類(比如TButton類)的默認窗口函數始終指向InitWndProc(這也是為什么每個TButton實例都會首先執行InitWndProc窗口函數的原因,Delphi強力保證了這一點,一旦整個類的默認窗口函數被改變,那么取消注冊后重新注冊,因為InitWndProc內容的第一遍執行對每個Delphi實例來說實在太重要了,怎么說都不過分),然后重新替換那個Delphi實例的回調函數為它自己的FObjectInstance。

留下一個疑問是,CreationControl是全局變量,InitWndProc是全局函數,且都沒有加任何保護,因此在多線程里,VCL是不安全的。以前在書上也多次見過,說VCL是線程不安全的,不知道是不是這個意思。其實WM_NCCREATE作為第一個創建窗口就自動發送過來的消息,幾乎是電光火花之間的事情,幾乎不可能亂套,盡管理論上存在這種可能。但是一旦錯亂將是非常嚴重的問題,因為怎么可能Button1使用的是Button2的FObjectInstance,反之亦然,同時ID,FHandle都錯位。不過貌似加上臨界區保護也不難。為什么Delphi的設計者不去這么做?

實際調用關系如下:
TWinControl.CreateHandle;
調用CreateWnd;
調用SetWindowPos(FHandle,SWP_NOMOVE + SWP_NOSIZE + SWP_NOACTIVATE);

TWinControl.CreateWnd;
申請Params: TCreateParams;
調用CreateParams(Params);
調用FDefWndProc := Params.WindowClass.lpfnWndProc; // 更改之前,先記錄到Delphi的類屬性
調用Params.WindowClass.lpfnWndProc := @InitWndProc; // 更改Delphi類(比如TButton類)的窗口函數為Delphi的全局函數,以后也不會有改變,改變的是Delphi實例的窗口函數
調用CreationControl := Self; // 全局變量,只此一處使用,記錄下來以供InitWndProc使用。注意,每次的Self值是不同的,實際上是不同的Delphi對象的地址值。

TWinControl.CreateParams;
申請Params: TCreateParams; // 是一個Record,即在棧上分配內存。出了這個函數,這部分內存就被收回。
調用CreateParams(Params); // 虛函數,類函數,就這一處被調用。
調用Params.WindowClass.lpfnWndProc := @DefWindowProc; // API,某個Delphi的默認窗口函數,會很快被替換掉。這只是給類的窗口函數,但對於每個實例,它們的每個窗口函數都被換掉了。

TWinControl.CreateWindowHandle;
調用FHandle := CreateWindowEx(ExStyle, WinClassName, Caption, Style, X, Y, Width, Height, WndParent, 0, WindowClass.hInstance, Param);
這個API會發送 WM_NCCREATE, WM_NCCALCSIZE, 和 WM_CREATE,實際上執行了前兩個消息對應的函數,最后一個消息的功能被構造函數替代了。
創建后取得窗口句柄,存儲在Delphi對象的FHandle屬性里

全局函數 InitWndProc(HWindow: HWnd; Message, WParam, LParam: Longint): Longint;
調用CreationControl.FHandle := HWindow;
調用SetWindowLong(HWindow, GWL_WNDPROC, Longint(CreationControl.FObjectInstance)); // 使用事先准備好的FObjectInstance作為普通窗口函數地址,千萬注意,這里替換的是某一個Windows窗口的窗口函數,不是整個類的窗口函數。
調用
PUSH LParam // 壓棧4個格子
PUSH WParam
PUSH Message
PUSH HWindow
MOV EAX,CreationControl // 把剛才創建的控件地址放到EAX寄存器里。混用匯編和Delphi,直接引用Delphi變量,給下一個函數准備參數。
MOV CreationControl,0 // 用完以后立刻清空,准備讓下一個新的Control使用
CALL [EAX].TWinControl.FObjectInstance // 根據寄存器里的內存地址在內存中找到控件,轉化為Win控件,並調用它的窗口函數,參數就在棧里
MOV Result,EAX // 處理完(WM_NCCREATE)消息后,把結果傳回來

到這里CreationControl和它的窗口函數都被替換了。留下的是一個Delphi對象,有了正確的Handle值,並有了單獨的窗口函數(FObjectInstance指向MainWndProc指向WndProc)。甚至還使用FDefWndProc記錄了默認窗口函數地址。

-----------------------------------------------------------
第四步,善后工作

TWinControl.Destroy;
調用if FObjectInstance <> nil then Classes.FreeObjectInstance(FObjectInstance); // 全局函數,釋放窗口函數的內存

-----------------------------------------------------------
題外話,由Handle找到Delphi控件(內存對象)

主要是利用了Controls單元里定義的1個全局函數FindControl和一個局部函數ObjectFromHWnd

function FindControl(Handle: HWnd): TWinControl;
var
OwningProcess: DWORD;
begin
Result := nil;
if (Handle <> 0) and (GetWindowThreadProcessID(Handle, OwningProcess) <> 0) and 
(OwningProcess = GetCurrentProcessId) then 
begin
// 第一種方法
if GlobalFindAtom(PChar(ControlAtomString)) = ControlAtom then
Result := Pointer(GetProp(Handle, MakeIntAtom(ControlAtom)))
// 第二種方法(通常用這種)
else
Result := ObjectFromHWnd(Handle); 
end;
end;

function ObjectFromHWnd(Handle: HWnd): TWinControl;
var
OwningProcess: DWORD;
begin
if (GetWindowThreadProcessID(Handle, OwningProcess) <> 0) and (OwningProcess = GetCurrentProcessID) then
Result := Pointer(SendMessage(Handle, RM_GetObjectInstance, 0, 0)) 
else
Result := nil;
end;

另外還有兩個有意思的函數,全局函數FindVCLWindow和局部函數IsDelphiHandle:

function FindVCLWindow(const Pos: TPoint): TWinControl;
var
  Handle: HWND;
begin
  Handle := WindowFromPoint(Pos); // API
  Result := nil;
  while Handle <> 0 do
  begin
    Result := FindControl(Handle); // 全局函數
    if Result <> nil then Exit;
    Handle := GetParent(Handle); // API
  end;
end;

function IsDelphiHandle(Handle: HWND): Boolean;
var
  OwningProcess: DWORD;
begin
  Result := False;
  if (Handle <> 0) and (GetWindowThreadProcessID(Handle, OwningProcess) <> 0) and (OwningProcess = GetCurrentProcessId) then
  begin
    if GlobalFindAtom(PChar(WindowAtomString)) = WindowAtom then // API
      Result := GetProp(Handle, MakeIntAtom(WindowAtom)) <> 0    // API
    else
      Result := ObjectFromHWnd(Handle) <> nil; 
  end;
end;

-----------------------------------------------------------

對第一個問題的解答:
第6行對窗口類的Fhandle進行賦值,這么做是必要的,因為正常情況下Fhandle只有到CreateWindowsEx返回之后才能得到賦值,在這個函數調用的過程中,系統發送WM_CREATE消息給窗口,在外部,我們可以得到WM_CREATE的處理器進行處理,如果沒有第6行的賦值,則那時我們將沒有辦法得到窗口句柄。我想這也是InitWndProc存在的原因之一。(注:我認為這個答案有啟發,但不完善)

參考:http://blog.csdn.net/linzhengqun/article/details/1451088 (有許多好東西)

此外,Delphi這種把類函數轉化成普通函數的手法稱為“Thunk技術”,網上有很多文章,比如:

http://www.cnblogs.com/memset/p/thunk_in_cpp.html

-----------------------------------------------------------

VC++里也有類似的問題,參考:
http://qiusuoge.com/8119.html

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM