Delphi中DLL的消息處理
事情的導火線是GIF圖片的顯示. 在應用程序中, 利用三方的GIFImage.pas可以很好的顯示GIF圖片.
這次, 要在一個DLL中顯示一個GIF圖片. 還是像往常一樣拖個TImage放到窗體上, 打開一個動態GIF圖片. 編譯, 運行.
怪了: GIF圖片顯示是靜態的. 還以為Delphi又出Bug了. 於是又把圖片放到程序主窗體上一運行, 動的. 這下頭可大了!
找相關的資料, 沒有.
看來事情還得自己解決: 於是專心研究起GIFImage.pas, 首先看的當然是重畫部分的代碼了( 呵呵, 這是我一貫的風格: 覺得是哪裡出問題就先看哪裡, 不管是誰的代碼 ). 經過一番搜索.
把目標定位在線程上. GIFImage.pas的重畫其實就是調用一個線程, 在線程內讀取文件中相應的圖像數據畫到目標位置.
在線程內重畫是調用線程的Synchronize過程. 以前知道這個過程是為了避免多個線程同時訪問同一個數據或對象的. 現在得對它的執行方法做一番了解才行.
經過一翻摸索, 找到了解決方法. 在DLL的窗體上放一個TTimer控件. Interval盡量小. OnTimer只添加一行代碼: CheckSynchronize;
運行. OK. 圖片動起來了......( 這種方法所存的問題就不用再多說了吧. )
但接下來的一個問題卻很惱火的: 在DLL的窗體上放一個TSpeedButton控件, Flat屬性設置為True. 運行. 當鼠標從TSpeedButton上移過時, TSpeedButton怎麼也還原不了. 試著調用它的重畫等功能. 全部沒用. 好幾天的時間一直在思考這個問題.
後來在處理應用程序的消息的時候, 突然想到: DLL雖然有自己Application, 但它並沒有自己的消息循環, 而線程的Synchronize不能執行, TSpeedButton不能還原都是因為有些消息沒有得到相應的處理而導致的.
也就是說, 只要給DLL加上一個消息循環, 上面的這些問題都會全部解決.
剛開始的時候想從主程序發送消息給DLL. 可消息截取的結果是: 很多DLL裡產生的消息並沒有發送給主程序. 看來這個方法是行不通的. 只得另尋方法.
在看到以下幾行大家很熟悉的代碼後想到.
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.Run;
可不可以給DLL也加上這們的代碼呢?
動手實驗, 創建一個DLL, DLL裡包含一個窗體DLLForm. 從DLL裡導出一個函數. 加上上面的代碼. 如下:
procedure InitDLL; stdcall;
begin
Application.Initialize;
Application.CreateForm(TDLLForm, DLLForm);
Application.Run;
end;
再到主程序窗體的創建事件代碼如下:
procedure TForm1.FormCreate(Sender: TObject);
begin
InitDLL;
end;
運行, 結果不對. 它是先打開主窗體了... :( 郁悶. 並且 InitDLL; 也不是立即返回, 而是當DLL裡主窗體關閉後才返回. 其實早就應該想到了.
把OnCreate的代碼放到一個TTimer控件裡. Interval為1. OnTimer的代碼如下.
procedure TForm1.Timer1Timer(Sender: TObject);
begin
TTimer(Sender).Enabled := False;
InitDLL;
end;
這下可以了. 但不能讓DLL裡的窗體一開始就顯示出來吧. 得. 再改改InitDLL. 如下:
procedure InitDLL; stdcall;
begin
Application.Initialize;
Application.ShowMainForm := False;
Application.CreateForm(TDLLForm, DLLForm);
Application.Run;
end;
主窗體不顯示了, 得加上一個, 看看效果:)
再到DLL裡加上一個Form( 命名為 DLLChildForm ), 在窗體上放一個TSpeedButton控件.
再給DLL導出一個函數, 如下:
procedure CreateChildForm; stdcall;
begin
with TDLLChildForm.Create(Application) do
begin
Show;
end;
end;
再到主窗體中添加一個按鈕. 點擊事件代碼如下.
procedure TForm1.Button1Click(Sender: TObject);
begin
CreateChildForm;
end;
運行. 結果理想: TSpeedButton在鼠標移過後能還原了. 呵呵...... 真爽!
不過, 問題又來了. 程序退出時報異常了. 想一下, 哦. DLL裡的窗體資源還沒有釋放呢. 得, 再從DLL裡導出一個過程, 代碼如下:
[delphi]
procedure DestoryDLL; stdcall;
var
i: Integer;
begin
for i := Application.ComponentCount - 1 downto 0 do
begin
if Application.Components[i].ClassNameIs('TDLLChildForm') then
begin
TDLLChildForm(Application.Components[i]).Release;
end;
end;
if DLLForm = nil then
begin
Exit;
end;
DLLForm.Release;
DLLForm := nil;
end;
procedure DestoryDLL; stdcall;
var
i: Integer;
begin
for i := Application.ComponentCount - 1 downto 0 do
begin
if Application.Components[i].ClassNameIs('TDLLChildForm') then
begin
TDLLChildForm(Application.Components[i]).Release;
end;
end;
if DLLForm = nil then
begin
Exit;
end;
DLLForm.Release;
DLLForm := nil;
end;
再給主程序主窗體的OnCloseQuery添加代碼如下:
procedure TForm1.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
DestoryDLL;
end;
運行. 靠, 雖然DLL裡的窗體全關閉了, 可主程序還是退不出啊. 換換方法, 把 DLLForm.Release; 這裡改成Application.Terminate; 試試. 還是不行. 咋回事?
反復調試, 發現雖然Terminate了, 可Run仍在循環. 並沒有結束.
再研究Run的代碼. 呵呵. 有了.
把Application.Terminate;換成PostMessage(Application.Handle, WM_QUIT, 0, 0);
運行, 還是不行. 但Run是循環是退出了. 那哪裡還會有問題呢? 該不會是窗體沒有釋放吧. 好, 在PostMessage前加上DLLForm.Release;這時, DestoryDLL過程的代碼如下:
[delphi]
procedure DestoryDLL; stdcall;
var
i: Integer;
begin
for i := Application.ComponentCount - 1 downto 0 do
begin
if Application.Components[i].ClassNameIs('TDLLChildForm') then
begin
TDLLChildForm(Application.Components[i]).Release;
end;
end;
if DLLForm = nil then
begin
Exit;
end;
DLLForm.Release;
// Application.Terminate;
PostMessage(Application.Handle, WM_QUIT, 0, 0);
DLLForm := nil;
end;
procedure DestoryDLL; stdcall;
var
i: Integer;
begin
for i := Application.ComponentCount - 1 downto 0 do
begin
if Application.Components[i].ClassNameIs('TDLLChildForm') then
begin
TDLLChildForm(Application.Components[i]).Release;
end;
end;
if DLLForm = nil then
begin
Exit;
end;
DLLForm.Release;
// Application.Terminate;
PostMessage(Application.Handle, WM_QUIT, 0, 0);
DLLForm := nil;
end;
運行. OK. 完美解決...
再加上線程試試( 這時InitDLL過程要改成如下, 這樣才能真正的處理所有的消息 ) . 真爽. 與想像的一樣.
[delphi]
rocedure InitDLL(AHandle: Thandle); stdcall;
begin
Application.Initialize;
Application.ShowMainForm := False;
Application.CreateForm(TDLLForm, DLLForm);
// 保存原來的句柄
DLLForm.Tag := Application.Handle;
// DLL 從屬的句柄 ( 如果沒有此行, 線程的執行不能達到理想效果 )
// 並且這樣才能真正的讓消息循環處理它應處理的所有消息
Application.Handle := AHandle;
Application.Run;
Application.Handle := DLLForm.Tag;
end;
procedure InitDLL(AHandle: Thandle); stdcall;
begin
Application.Initialize;
Application.ShowMainForm := False;
Application.CreateForm(TDLLForm, DLLForm);
// 保存原來的句柄
DLLForm.Tag := Application.Handle;
// DLL 從屬的句柄 ( 如果沒有此行, 線程的執行不能達到理想效果 )
// 並且這樣才能真正的讓消息循環處理它應處理的所有消息
Application.Handle := AHandle;
Application.Run;
Application.Handle := DLLForm.Tag;
end;