標籤:des style blog http io ar color os 使用
----------------------------------------------------------------------
第一步,準備工作:預定義一個全域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個步驟,每次產生TButton執行個體都要這樣來一遍,尤其是第二步重新改寫Delphi類(比如TButton)的回呼函數,然後重新註冊,重新替換回呼函數為FObjectInstance。
實際調用關係如下:
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的全域函數,參數一致。
調用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作為普通視窗函數地址
調用
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); // 全域函數,釋放視窗函數的記憶體
Delphi對象變成Windows控制項的前世今生(關鍵是控制代碼和回呼函數)