這將是最後一個元件了,目標定為非視覺化,事實上非視覺化元件要比視覺化元件難做,因為是從TComponent繼承而來,就沒有了很多屬性和事件。而這些都要我們從頭來做過。
這個非視覺化元件,我決定為光碟機元件,其中用到的技術較多,我不如列一個表出來,然後再來講解好一點。另外,可能篇幅會多一些,請耐心看。
用到的技術:
1.作為核心功能,當然是光碟機的應用啦。
2.光碟機元件怎麼樣影響到主視窗最小化時隱藏
3.光碟機如何處理訊息
4.元件編輯器的用法
上面每一個技術都非常有趣,讓我們一個個來看吧:
一、光碟機,是系統殼編程的一個功能,相信我們也看過很多啦,大概知道它用起來是什麼樣子的。
那麼它是如何實現的呢,
Windows定義了這樣一個結構來存放光碟機的資訊:
typedef struct _NOTIFYICONDATA { // nid
DWORD cbSize;
HWND hWnd;
UINT uID;
UINT uFlags;
UINT uCallbackMessage;
HICON hIcon;
char szTip[64];
} 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面板中了。
而這時候,我的任務也完畢了。
結語
這次的組件之旅終於走完了,也許有人會笑我淺薄,認為這麼簡單的東西,有必要拿出來麼。也許是比較簡單吧,但一定有人會需要的,相信我的文章會給他們幫助 的。因為這些是我曾經學到的知道,遇到的問題並解決它。所以我個人覺得是很珍貴的。並且經過寫這幾篇,我把這些知識記得更牢了。這種利己利人的事,何樂而 不為呀。
在此,謝謝大家的閱讀,也許下次還有機會再見面,不過現在要說再見了。祝你們愉快。