這將是最后一個
組件了,目標定為非可視化,事實上非可視化組件要比可視化組件難做,因為是從TComponent繼承而來,就沒有了很多屬性和事件。而這些都要我們從頭來做過。
這個非可視化組件,我決定為托盤組件,其中用到的技術較多,我不如列一個表出來,然后再來講解好一點。另外,可能篇幅會多一些,請耐心看。
用到的技術:
1作為核心功能,當然是托盤的應用啦。
2 托盤組件怎么樣影響到主窗口最小化時隱藏
3 托盤如何處理消息
4 組件編輯器的用法
上面每一個技術都非常有趣,讓我們一個個來看吧:
一 托盤,是系統殼編程的一個功能,相信我們也看過很多啦,大概知道它用起來是什么樣子的。
那么它是如何實現的呢,
Windows定義了這樣一個結構來存放托盤的信息:
typedef struct _NOTIFYICONDATA { // nid
HWND hWnd;
UINT uID;
UINT uFlags;
UINT uCallbackMessage;
HICON hIcon;
} NOTIFYICONDATA, *PNOTIFYICONDATA;
cbSize是NOTIFYICONDATA結構的尺寸,我們一般用Sizeof就可以了
hWnd一個窗口句柄,用于檢索托盤消息的。然而我們的非可視組件并沒有窗口呀,這就是技術列表第三條要講的,這里從略
uID 唯 一標識托盤圖標的,我們可以隨便指定一個數,但如果同時有不同的圖標,則數應該不同
uFlags是NIF_ICON,NIF_MESSAGE,NIF_TIP中的一個或多個,我們全用就可以了。
uCallbackMessage;托盤消息,是我們自定義的消息,這里我們定義為:
const
WM_TrayMsg=WM_USER+10;
hIcon托盤圖標句柄
szTip這個是托盤提示,當托盤出現時,鼠標移到哪里,就會出現該提示。
Delphi將這個結構重定義為TNotifyIconData,我們照這個來用就行了
我們應用托盤要用到API函數Shell_NotifyIcon,其中有兩個參數,第一個為
NIM_ADD,NIM_DELETE ,NIM_MODIFY中的一個,分別表示添加托盤(圖標出現)
修改托盤(比如圖標,提示),刪除(圖標消失)第二個參數是NOTIFYICONDATA的指針
嗯,托盤應該差不多了。
二 這個組件能夠決定主窗體最小化時,是否是正常最小化并沒有托盤圖標。還是最小化到屏幕之外,使我們看不見,且托盤區出現了圖標。這里有一個成員為FActive來決定。
那么我們是怎么樣影響到主窗體呢,也即怎么截獲窗體的最小化消息呢。
全局變量
application有一個方法為
PRocedure HookMainWindow(Hook: TWindowHook);
顧名思義,就是鉤到主窗口的所有消息。里面的參數是TWindowHook類型,它是一個方法指針,定義如下:
type TWindowHook = function(var Message: TMessage): Boolean of object;
我們要自己定義過程的,然后傳給HookMainWindow:
function AppMsgHook(var Msg:TMessage):Boolean;
Application.HookMainWindow(AppMsgHook);
這樣做之后,主窗口的所有消息都會經過AppMsgHook方法啦,最小化消息也不例外,則我們可以在里面截獲這個消息,并做一些操作:
做什么操作呢,先判斷組件是否為設計時,如果是,不進行操作,如果不是進行下一步
if not (csDesigning in ComponentState) then
這樣的意圖是很明顯的,因為當設計時的主窗其實是Delphi的IDE,如果讓他處理該消息,其實是處理IDE的最小化消息,這時如果你最小化IDE,就會出現托盤啦。所以不能。
下一步是是否截獲了最小化消息,以及FActive是否為真:
if (Msg.Msg=WM_SYSCOMMAND) and(FActive) then
兩樣都成立,執行里面的代碼,代碼中有解釋,這里只說兩個:
SetWindowLong(Application.Handle,GWL_EXSTYLE ,WS_EX_TOOLWINDOW);
設置了這個屬性后,窗口最小化就不會停在任務欄了,而是停在屏幕的某個位置,這個位置在哪里呢,由
placement.flags:=WPF_SETMINPOSITION;
placement.ptMinPosition.x:=1050;
placement.ptMinPosition.y:=800;
SetWindowPlacement(Application.Handle,@placement);
決定,具體的看代碼,自己查幫助吧,這里不多說
而上說的設置SetWindowLong后,問題來了,窗口最小化的風格一變了,當你把Factive設為False,再最小化窗口,此時是沒有托盤圖標,但窗口還是最小化到屏幕的那個位置去了,我們看不到,又不能使其恢復(沒有托盤)。怎么辦呢,
原來還有一個GetWindowLong函數會返回當前風格的值,我們可以在控件的構造函數中這樣調用
OldStyleEX:=GetWindowLong(Application.Handle,GWL_EXSTYLE);
這時,OldStyleEX:就保存了窗口原來最小化的風格了,窗口最小化,調用SetWindowLong,設置了新的最小風格。而當我們觸發托盤事件,使窗體恢復大小時,我們在處理函數中調用
SetWindowLong(Application.Handle,GWL_EXSTYLE ,OldStyleEX);
這樣,窗口又回到了原來的風格,這時我們設Factive為False,則窗口就能正常最小化了。
到控件被釋放時,我們一定要調用Application.UnhookMainWindow(AppMsgHook);來解除鉤子
其實這里也有一個不完善的地方,應該再設一個成員變量,確定設置托盤時,窗口是正常最小化,還是最小化到看不見。而我沒有這么做,直接如果FActive為True,最小化會出現托盤圖標,并且窗口最小化到看不見。不過影響不大,有興趣的朋友看了之后可以幫我完善一下,也當做自己的練習嗎。
三 托盤如果處理消息,上面說到,要設置托盤結構,一定要有一個窗口句柄,才能檢索托盤消息,那么這個句柄是什么呢,非可視組件沒有窗口句柄呀。
如果你有看過TTimer的
源碼,一定知道這一句代碼:
FWindowHandle := AllocateHWnd(WndProc);
它創建一個看不見的窗口,返回他的句柄,并指定WndProc為窗口的消息處理過程
我們何不效仿它呢。
于是也定義一個成員句柄:
FHandle: HWnd;
把該句柄賦給NOTIFYICONDATA的hWnd字段
再定義一個消息處理過程:
procedure WndProc(var Msg: TMessage);
再在組件構造函數中:
FHandle := AllocateHWnd(WndProc);
如此之后,組件就可以截獲托盤的消息了,并在WndProc過程中作相應處理。這里有必要對托盤的自定義消息做一個介紹:
我們自定義了這個消息WM_TrayMsg,它的lParam與托盤的uID相同,wParam是鼠標在圖標上發生的事件消息,比如單擊,雙擊等。
我們就要把這些消息轉化為事件,供給用戶處理,所以定義幾個事件調度函數:
//以下為事件的調度函數
procedure DblClick; dynamic;
procedure Click; dynamic;
procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); dynamic;
procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); dynamic;
procedure MouseMove(Shift: TShiftState; X, Y: Integer); dynamic;
意思很明顯,不多說,
當然也有幾個事件方法指針:
FOnIconClick: TNotifyEvent;
FOnIconDblClick: TNotifyEvent;
FOnIconMouseMove: TMouseMoveEvent;
FOnIconMouseDown: TMouseEvent;
FOnIconMouseUp: TMouseEvent;
然后在WndProc中判斷消息,并調用相應的事件調度函數。看代碼吧,有解釋。
好了,三個技術解決了,第四個呢,還是等代碼出來以后再加組件編輯器吧。以下是源代碼:
unit MyTray;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs, ShellApi, ExtCtrls,StdCtrls;
const
//自定義托盤消息
WM_TrayMsg=WM_USER+10;
type
//恢復窗口的方式,左雙擊,右雙擊,左單擊,右雙擊
TRMode=(LDbClick,RDbClick,LCLick,RClick);
TMyTray=class(TComponent)
private
//私有成員
FIcon:TIcon; //圖標
FDfIcon:THandle; //應用程序的默認圖標
FSetDfIcon:Boolean; //是否用應用程序的圖標,如果為True,則Ficon為nil
FIconData: TNotifyIconData; //托盤數據結構
isMin:Boolean;//標識是否窗口最小化了
FHandle: HWnd; //不可視建窗體句柄,用于處理托盤事件
FActive: Boolean; //是否啟用托盤
FHint: string; //托盤提示字符串
FRMode:TRMode; //恢復窗口的方式
isClickIn:Boolean;//標識鼠標是否點在圖標上
OldStyleEX:longInt; //保存老的窗口風格
//事件成員
FOnIconClick: TNotifyEvent;
FOnIconDblClick: TNotifyEvent;
FOnIconMouseMove: TMouseMoveEvent;
FOnIconMouseDown: TMouseEvent;
FOnIconMouseUp: TMouseEvent;
//設置方法
procedure SetIcon(value:TIcon);
procedure SetDfIcon(value:boolean);
procedure SetActive(value:boolean);
procedure SetHint(value:string);
procedure SetRMode(value:TRMode);
//私有方法
procedure SetTray(Way:DWORD); //設置托盤樣式,修改,刪除,增加
function GetActiveIcon:THandle; //取得有用的圖標句柄
protected
//應用程序的消息鉤子,獲得主窗口的最小化消息
function AppMsgHook(var Msg:TMessage):Boolean;
procedure WndProc(var Msg: TMessage);//不可視窗口的窗口過程
//以下為事件的調度函數
procedure DblClick; dynamic;
procedure Click; dynamic;
procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); dynamic;
procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); dynamic;
procedure MouseMove(Shift: TShiftState; X, Y: Integer); dynamic;
public
constructor Create(AOwner:TComponent);override;
destructor Destroy;override;
published
property Active:Boolean read FActive write SetActive default False;
property Icon:TIcon read FIcon write SetICon;
property SetDfIconed: boolean read FSetDfIcon write SetDfIcon default true;
property Hint:String read FHint write SetHint;
property RMode:TRmode read FRmode write SetRMode default LDbClick;
//事件的方法指針
property OnIconClick: TNotifyEvent read FOnIconClick write FOnIconClick;
property OnIconDblClick: TNotifyEvent read FOnIconDblClick write FOnIconDblClick;
property OnIconMouseMove: TMouseMoveEvent read FOnIconMouseMove write FOnIconMouseMove;
property OnIconMouseDown: TMouseEvent read FOnIconMouseDown write FOnIconMouseDown;
property OnIconMouseUp: TMouseEvent read FOnIconMouseUp write FOnIconMouseUp;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Wind', [TMyTray]);
end;
///////////TmyTray////////////////////////////
constructor TMyTray.Create(AOwner:TComponent);
begin
inherited Create(AOwner);
//設置程序鉤子,指定AppMsgHook為處理函數,
//則,應用程序的任何消息都將經過這個函數
Application.HookMainWindow(AppMsgHook);
FICon:=TICon.Create;
//得到默認圖標的句柄,圖標為應用程序的圖標
FDfIcon:=Application.Icon.Handle;
FSetDfIcon:=True;
FActive:=False;
FRMode:=LDbClick;
isMin:=False;
//創建一個不可視窗口,并指定窗口過程,以處理托盤事件
FHandle := AllocateHWnd(WndProc);
//保存窗體的老的風格,在恢復窗口的同時也恢復原來的窗口風格
OldStyleEX:=GetWindowLong(Application.Handle,GWL_EXSTYLE);
end;
destructor TMyTray.Destroy;
begin
Application.UnhookMainWindow(AppMsgHook);
//對象釋放之前先消除托盤
SetTray(NIM_DELETE);
//釋放不可能窗口的句柄
DeallocateHWnd(FHandle);
FICon.Free;
inherited Destroy;
end;
//應用程序鉤子,可以截獲應用程序的所有消息
function TMyTray.AppMsgHook(var Msg:TMessage):Boolean;
var placement:WINDOWPLACEMENT;
begin
Result:=False;
//保證程序不會在設計時處理最小化消息
if not (csDesigning in ComponentState) then
if (Msg.Msg=WM_SYSCOMMAND) and(FActive) then
begin
if msg.WParam=SC_MINIMIZE Then
begin
//設置了這個屬性后,窗口最小化就不會停在任務欄了,而是停在屏幕,
//位置由SetWindowPlacement來決定
ShowWindow(Application.Handle,SW_HIDE);
SetWindowLong(Application.Handle,GWL_EXSTYLE ,WS_EX_TOOLWINDOW);
GetWindowPlacement(Application.Handle,@placement);
placement.flags:=WPF_SETMINPOSITION;
placement.ptMinPosition.x:=1050;
placement.ptMinPosition.y:=800;
SetWindowPlacement(Application.Handle,@placement);
SetTray(NIM_ADD );
end;
end;
end;
procedure TMyTray.SetIcon(Value:TIcon);
begin
FIcon.Assign(Value);
FsetDfIcon:=False; //有了自定義的圖標,則默認圖標自動設為False
if FIcon.Empty then
FsetDfIcon:=True;
if (isMin)and(Factive) then
SetTray(NIM_MODIFY );
end;
//設置是否為默認圖標,與FIcon為互相的變量,只能有其中一個
procedure TMyTray.SetDfIcon(Value:Boolean);
begin
if FSetDfIcon<>Value then
begin
FSetDfIcon:=Value;
if not FSetDfIcon then
begin
if FIcon.Empty then begin
FSetDfIcon:=True;
exit;
end;
end
else begin
if (IsMin)and(FActive) then
SetTray(NIM_MODIFY);
end;
end;
end;
procedure TMyTray.SetActive(Value:Boolean);
begin
if FActive<>Value then
begin
FActive:=Value;
end;
end;
procedure TMyTray.SetHint(Value:String);
begin
if FHint<>Value then
begin
FHInt:=Value;
if (IsMin)and(FActive) then
SetTray(NIM_MODIFY);
end;
end;
procedure TMyTray.SetRMode(Value:TRMode);
begin
if FRmode<>Value then
FRmode:=Value;
end;
//設置托盤方式,顯示,修改,刪掉,重要方法
procedure TMyTray.SetTray(Way:DWORD);
begin
FIconData.cbSize:=Sizeof(FIconData);
FIconData.Wnd:=FHandle;
FIConData.uID:=0;
FIConData.uFlags:=NIF_ICON or NIF_MESSAGE or NIF_TIP;
FIConData.uCallbackMessage:=WM_TrayMsg;
FIConData.hIcon:=GetActiveIcon;
StrLCopy(FIConData.szTip,Pchar(FHint),63);
Shell_NotifyIcon(Way,@FIconData);
end;
//取得可用的圖標
function TMyTray.GetActiveIcon:THandle;
begin
if not FSetDfIcon then
result:=FIcon.Handle
else
result:=FDfIcon;
end;
//托盤消息的截獲,以調用相應的事件調度方法
procedure TMyTray.WndProc(var Msg: TMessage);
var p:TPoint;
begin
if (Msg.Msg=WM_TrayMsg)and(FActive) then
begin
case Msg.LParam of
WM_LBUTTONDBLCLK://左雙擊
begin
GetCursorPos(p);
DblClick;
MouseDown(mbLeft, KeysToShiftState(TWMMouse(Msg).Keys)+[ssDouble], P.X, P.Y);
if FRmode=LDbclick then
begin
ShowWindow(Application.Handle,SW_SHOW);
//這里很重要的一個就是恢復窗口風格,不然下次把Active設為True
//最小化后,窗口依然會往左下角飛去,而托盤圖標卻看不見了.
SetWindowLong(Application.Handle,GWL_EXSTYLE ,OldStyleEX);
SendMessage(Application.Handle,WM_SYSCOMMAND,SC_RESTORE,0);
SetTray(NIM_DELETE);
end;
end;
WM_RBUTTONDBLCLK://右雙擊
begin
GetCursorPos(P);
DblClick;
MouseDown(mbRight, KeysToShiftState(TWMMouse(Msg).Keys)+[ssDouble], P.X, P.Y);
if FRmode=RDbclick then
begin
ShowWindow(Application.Handle,SW_SHOW);
SetWindowLong(Application.Handle,GWL_EXSTYLE ,OldStyleEX);
SendMessage(Application.Handle,WM_SYSCOMMAND,SC_RESTORE,0);
SetTray(NIM_DELETE );
end;
end;
WM_MOUSEMOVE: //鼠標移動
begin
GetCursorPos(P);
MouseMove(KeysToShiftState(TWMMouse(Msg).Keys), P.X, P.Y);
end;
WM_LBUTTONDOWN: //左單擊下
begin
GetCursorPos(P);
IsClickIn:=True;
MouseDown(mbLeft, KeysToShiftState(TWMMouse(Msg).Keys) + [ssLeft], P.X, P.Y);
end;
WM_LBUTTONUP: //左單擊彈起
begin
GetCursorPos(P);
if IsClickIn then
begin
IsClickIn:=False;
Click;
if FRmode=LClick then
begin
ShowWindow(Application.Handle,SW_SHOW);
SetWindowLong(Application.Handle,GWL_EXSTYLE ,OldStyleEX);
SendMessage(Application.Handle,WM_SYSCOMMAND,SC_RESTORE,0);
SetTray(NIM_DELETE );
end;
end;
MouseUp(mbLeft, KeysToShiftState(TWMMouse(Msg).Keys)+ [ssLeft], P.X, P.Y);
end;
WM_RBUTTONDOWN: //右單擊下
begin
GetCursorPos(P);
IsClickIn:=True;
MouseDown(mbRight, KeysToShiftState(TWMMouse(Msg).Keys) + [ssRight], P.X, P.Y);
end;
WM_RBUTTONUP: //右單擊彈起
begin
GetCursorPos(P);
if IsClickIn then
begin
IsClickIn:=False;
Click;
if FRmode=RClick then
begin
ShowWindow(Application.Handle,SW_SHOW);
SetWindowLong(Application.Handle,GWL_EXSTYLE ,OldStyleEX);
SendMessage(Application.Handle,WM_SYSCOMMAND,SC_RESTORE,0);
SetTray(NIM_DELETE );
end;
end;
MouseUp(mbRight, KeysToShiftState(TWMMouse(Msg).Keys)+ [ssRight], P.X, P.Y);
end;
end;
end
else
Msg.Result := DefWindowProc(FHandle, Msg.Msg, Msg.wParam, Msg.lParam);
end;
//以下為幾個事件的調度函數,比較簡單.
procedure TMyTray.DblClick;
begin
if Assigned(FOnIconDblClick) then
FOnIconDblClick(Self);
end;
procedure TMyTray.Click;
begin
if Assigned(FOnIconClick) then
FOnIconClick(Self);
end;
procedure TMyTray.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
if Assigned(FOnIconMouseDown) then
FOnIconMouseDown(Self, Button, Shift, X, Y);
end;
procedure TMyTray.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
if Assigned(FOnIconMouseUp) then
FOnIconMouseUp(Self, Button, Shift, X, Y);
end;
procedure TMyTray.MouseMove(Shift: TShiftState; X, Y: Integer);
begin
if Assigned(FOnIconMouseMove) then
FOnIconMouseMove(Self, Shift, X, Y);
end;
end.
組制作完畢,相信經過上面的講解,以及代碼的注釋,應該不難理解。接下來是什么呢,給我的托盤控件來點效果,即在設計器中,當雙擊該組件,或右擊快捷菜單第一項時,會彈出一個About對話框,來說明我的托盤組件。
這個就要用到組件編輯器啦 。幾本經典書中都有說及,比如Deplphi開發人員指南,我也是從那里學來的,不過卻遇到了一些問題,折磨了幾天才解決。
這里不想詳細介紹,去看一下那些書,大概也就知道了,只略說一下。
其原理就是實現一個繼承自TComponentEditor的子類TTrayIconEditor,并在其中覆蓋以下三個方法:
function GetVerbCount: Integer; override;
function GetVerb(Index: Integer): string; override;
procedure ExecuteVerb(Index: Integer); override;
可以精略理解為:
GetVerbCount指定控件快捷菜單的項數
GetVerb指定快捷菜單中的相關項的名字
ExecuteVerb執行點擊快捷菜單項后的動作
接著在Register方法中調用RegisterComponentEditor(TMyTray,TTrayIconEditor);
第一個參數為組件類名,第二個為組件編輯器的類名。
而上面的方法必須引用DesignIntf,DesignEditors。
當我在我的組件單元這樣做之后出現問題了,編譯安裝沒有問題。我建立測試程序,并拉一個托盤組件,雙擊它,可以出現About對話框,右擊菜單第一項也沒有問題。可是當我運行測試程序時,卻出現了這樣的編譯錯誤:
[Fatal Error] Unit1.pas(7): File not found: 'DesignEditors.dcu'
這讓我痛苦了好幾天,書上是這么說的,應該沒有什么錯誤呀。后來經過摸索,才找到了解決之道。
解決的辦法就是將組件編輯器類放在另一個單元中,并在這個單元引用我的托盤組件單元。
并安裝之。這才可以正常運行,這個編輯器單元如下:
unit AboutTray;
interface
uses
SysUtils,Classes,DesignIntf,DesignEditors,Forms,
MyTray;
type
TTrayIconEditor = class (TComponentEditor)
function GetVerbCount: Integer; override;
function GetVerb(Index: Integer): string; override;
procedure ExecuteVerb(Index: Integer); override;
end;
procedure Register;
implementation
///////TTrayIconEditor////////////////////////
procedure TTrayIconEditor.ExecuteVerb(index:integer);
begin
case index of
0: application.MessageBox('你好,這是風做的托盤組件!!','關于');
end;
end;
function TTrayIconEditor.GetVerb(index:integer):String;
begin
case index of
0:Result:='About MyTray';
end;
end;
function TTrayIconEditor.GetVerbCount:integer;
begin
Result:=1;
end;
procedure Register;
begin
RegisterComponentEditor(TMyTray,TTrayIconEditor);
end;
end.
至此,托盤組件完畢,拉下它放在窗體設計器中,雙擊,彈出對話框
里面內容為:“你好,這是風做的托盤組件!!”。哈哈,你成功啦
做為組件制作的最后一個內容,我想用一個包來把我的所有組件單元包含起來,并放在我自己新建的一個面板中。
這樣做之前,要把以前安裝下去的組件刪除。知道怎么樣刪除,如果不知道,請看我在第一篇中說的。
然后在打開所有的組件單元,把RegisterComponents(‘Samples', [TCoolMemo]);里面的
Samples改為Wind。然后保存
接著,在IDE中點File-》New-》Other…
彈出來的New Items對話框,選中New頁面,并選中其中的Package,
這里彈出一個新建的包編輯器。
先在IDE中點File-》Save。將包編輯器保存。保存在組件的單元所在的文件夾中
我的所有組件單元都放在Delphi7/MyCom文件夾中。因此這個包當然也保存在這里。
然后,點包編輯器上邊的Add,將所有的組件單元加進去,當然也保括上面說的組件編輯器單元啦。
加進去后,點包編輯器上邊的Compile,編譯完畢,再點Insall。
成功,看看面板。所有以前做過的組件全在Wind面板中了。
而這時候,我的任務也完畢了。
結語
這次的組件之旅終于走完了,也許有人會笑我淺薄,認為這么簡單的東西,有必要拿出來么。也許是比較簡單吧,但一定有人會需要的,相信我的文章會給他們幫助的。因為這些是我曾經學到的知道,遇到的問題并解決它。所以我個人覺得是很珍貴的。并且經過寫這幾篇,我把這些知識記得更牢了。這種利己利人的事,何樂而不為呀。
在此,謝謝大家的閱讀,也許下次還有機會再見面,不過現在要說再見了。祝你們愉快。