以C++ Builder處理Windows 訊息(Message)
前言
雖然C++Builder爲一RAD式的程式發展工具,程式設計師在大多數情況下不需理會Windows訊息的細節,只要將心思放在軟體元件的事件處理函式即可。然而由於Windows作業系統終究是一個以訊息驅動的系統,因此架構其上的的應用程式自然無法自外於系統之外,在遭遇到C++Builder沒有定義的事件時,Windows訊息處理能力仍然是C++Builder程式人不可或缺的能力。
不可否認地,C++Builder所提供的事件處理能力已具備了某一程度的完備性,然而我們也必須承認,在C++Buider建構的VCL美麗新世界中,仍然不免有漏網之魚。例如使用者自定訊息的處理,Winsock訊息的處理及一些Windows訊息如WM_NC**** 系列的訊息都是C++Builder的物件模型所未包含的。
在本文中我將告訴你如何以C++Builder來處理Windows訊息,並透過此一能力,來達成在一般VCL元件所無法做到的功能。
何謂Window訊息(Message)
大家都知道 Windows是一套以訊息驅動(Message Driven)的作業系統。然而對於訊息本身卻諱莫如深,只知其然而不知其所以然,雖然C++Builder將某些Windows訊息封裝於事件(Event)系統中,但身爲一個Windows程式設計師,實有必要了解Windows的訊息系統。
所謂訊息是由Windows作業系統送往程式的事件。它是系統中各個物件溝通的方式,舉例來說,當移動滑鼠、按下滑鼠鍵、改變視窗大小時,Windows都會送出訊息以通知程式。當然,爲了要辨別事件的內容,Windows系統中定義了許多的訊息,如WM_PAINT,WM_CHAR等等。
當事件發生時,Windows會判斷該事件必須由那個程式接收,然後將事件以訊息的方式送往程式的視窗中。雖然在Windows系統中包含了數以百計的事件,但是作業系統並沒有爲各個事件設計不同的訊息結構,而是以一個一般性的結構來描述訊息,這個結構在C++Builder就稱是TMessage。
當然,隨着事件的不同,對於訊息的解釋也有所不同,在C++Builder中也爲各種常用的訊息定義了專屬的結構,你可以直接使用它們來解釋訊息。這些訊息定義在C++Builder目錄下的Include/vcl/messages.hpp中,你可以決定要自行解釋TMessage參數或是直接將其轉換成專屬的結構。很抽象嗎?我舉個例子吧,以WM_NCHITTEST訊息來說,C++Builder爲它定義了TWMNCHitTest的專屬結構,所以你可以直接經由它來得到XPos、YPos等值。或者你也可以直接由TMessage的LParam取得其值,端看你使用的方便。仔細觀察TMessage及TWMNCHitTest兩個結構,你會發現它們是等價的,也就是說它們的大小是一致的,因此你可以直接用強制轉型互相轉換(這有點類似union的方法)。
struct TMessage
{
Cardinal Msg;
union
{
struct
{
Word WParamLo;
Word WParamHi;
Word LParamLo;
Word LParamHi;
Word ResultLo;
Word ResultHi;
};
struct
{
long WParam;
long LParam;
long Result;
};
};
};
struct TWMNCHitTest
{
Cardinal Msg;
long Unused;
union
{
struct
{
Windows::TSmallPoint Pos;
long Result;
};
struct
{
short XPos;
short YPos;
};
};
} ;
在收到訊息後,程式必須處理該訊息,若是不處理,則可直接將它交給Windows的內定處理程序來處理之,若是程式需要傳回值,也可以在此時傳回,Windows會將該值傳回給呼叫方。如此就完成了訊息傳遞的程序。
複雜嗎?一點也不!瞭解Windows訊息系統的運作後,我們來看看可以利用它來做些什麼有趣的事吧!
訊息使用範例一 使用者自定標題棒的實作
一般Windows程式的標題棒位於視窗的上方,我們可以利用該標題棒來移動視窗。以下我將爲你示範如何利用C++Builder實作出置於視窗左方的標題棒。如圖一:
圖一 標題棒在左方的視窗。
如上圖,你可以很清楚地看到,這個視窗和其他的視窗有很大的不同;它的標題棒位於左方,而且其顏色爲綠色,同時其文字的走向爲由下而上的90度字形,而其功能則和一般的標題棒相同,你可以將滑鼠移至該處,然後移動該視窗。到底這是如何達成的呢?
WM_NCHITTEST訊息的奧祕
WM_NCHITTEST訊息是一個很特殊的訊息。它是用來決定目前滑鼠所在位置屬性的訊息,因此我們可以利用此特性,當滑鼠移至指定的位置時,傳回 HTCAPTION,使得系統以爲滑鼠目前位於標題棒,如此你就可以移動視窗了。如何?是不是很神奇呢?
由上可知,只要我們適時地攔截WM_NCHITTEST訊息,然後傳回HTCAPTION,就可以順利地欺騙系統,達成在任何位置模擬出標題棒的效果。
C++ Builder的處理訊息的巨集
在C++Builder爲了處理訊息的方便,因此定義了叄個處理訊息的巨集(Macro)。
BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(WM_NCHITTEST,TMessage,OnNcHitTest)
END_MESSAGE_MAP(TForm)
以上的叄個巨集BEGIN_MESSAGE_MAP、MESSAGE_HANDLER及END_MESSAGE就是C++ Builder定義的巨集,其中比較重要的是MESSAGE_HANDLER;它共需要叄個參數,第一個參數代表訊息的ID,第二個代表參數型態,最後一個則是訊息事件處理函數。
乍看之下,這個巨集似乎和MFC及OWL所使用的巨集有幾分神似,沒錯,不過其機制卻更爲簡單及簡潔,我們可以看看C++Builder對於這叄個巨集的原始定義:
#define BEGIN_MESSAGE_MAP virtual void __fastcall Dispatch(void *Message) /
{ /
switch (((PMessage)Message)->Msg) /
{
#define MESSAGE_HANDLER(msg,type,meth) /
case msg: /
meth(*((type *)Message)); /
break;
#define END_MESSAGE_MAP(base) default: /
base::Dispatch(Message); /
break; /
} /
}
相較於MFC或 OWL的可怕巨集,它實在是簡單多了,這是因爲C++Builder已替你完成了大部份的工作。其實若我們把以上的巨集展開後,可以得到以下的結果:
virtual void __fastcall Dispatch(void *Message)
{
switch (((PMessage)Message)->Msg)
{
case WM_NCHITTEST:
OnNcHitTest(*((TMessage *)Message));
break;
default:
TForm::Dispatch(Message);
break;
}
}
怎麼樣?展開之後是不是有恍然大悟的感覺,要弄清楚這個巨集在賣啥膏藥是很容易的,如果你玩過MFC的訊息處理機制,再看到以上的巨集,相較之下,實在是小兒科,不過也就因其簡單,所以C++Builder的優勢益加彰顯。
我簡單地說明以上的程式:在每個TForm中都定義一個名爲Dispatch的虛擬函式,它就是用來處理Windows的訊息的,在大部份情況下,訊息都是呼叫C++Builder所提供的處理函式,因此你不需要修改它。
換句話說,我們只要改寫Dispatch函式,就可以藉以處理指定的訊息了。前面提到的叄個巨集只是將這個程序簡化而已,沒什麼大不了。
自定標題的繪製
由於我們要使用自定的標題,所以你必須將程式所使用的 TForm的BorderStyle性質設爲 bsNone,如此你的TForm就不會有標題棒了。
再來你就必須自行繪製標題棒,我們希望繪製一個位於左於的標題,因此我們必須處理TForm的OnPaint事件,然後在此事件中繪製標題棒。以下即爲其事件處理函式:
void __fastcall TForm1::FormPaint(TObject *Sender)
{
RECT rc;
::SetRect(&rc,0,0,ClientWidth,ClientHeight);
DrawButtonFace(Canvas,rc,1);
Canvas->Pen->Color=clGreen;
Canvas->Brush->Color=clGreen;
Canvas->Rectangle(0,0,20,ClientHeight);
:// 以下略去
:
}
你可以看到,我們畫出一個寬爲20,顏色爲綠色的標題棒。因此我們處理WM_NCHITTEST訊息的處理函式也必須做相對應的修改:
void __fastcall TForm1::OnNcHitTest(TMessage& Msg)
{
TPoint pt;
pt.x=LOWORD(Msg.LParam);
pt.y=HIWORD(Msg.LParam);
pt =ScreenToClient(pt);
RECT rc;
::SetRect(&rc,0,0,20,ClientHeight);
if (PtInRect(&rc,pt))
Msg.Result = HTCAPTION;
else
DefaultHandler(&Msg);
}
OnNcHitTest函式首先取得目前滑鼠所在點,注意,WM_NCHITTEST訊息所傳入的點爲相對於螢幕的絕對座標,因此在取得該點後必須利用ScreenToClient函數將它轉爲TForm的相對座標值,然後再據以判斷是否落於我們所定義的標題棒範圍內,若是則傳回HTCAPTION值,否則就交由內定的處理函式DefaultHandler來處理。如此就完成了一個位於左方的標題棒了。
旋轉文字的輸出
仔細觀察圖一,你會發現它所使用的標題字元的方向,已經因應標題棒的轉向而成爲90旋轉的文字,這是如何達成的呢?
其實說穿了沒什麼,只是利用傳統SDK的繪圖方法來畫出來的。因爲在C++Builder的TFont物件並沒有定義文字旋轉的屬性,所以我們只好透過傳統的GDI繪圖方法來達成這個目標。
char* msg=Caption.c_str();
LOGFONT fontRec;
memset(&fontRec,0,sizeof(LOGFONT));
fontRec.lfHeight = -13;
fontRec.lfWeight = FW_NORMAL;
fontRec.lfEscapement = 900; // 旋轉文字的關鍵
lstrcpy(fontRec.lfFaceName,"細明體");
HFONT hFont=CreateFontIndirect(&fontRec);
HFONT hOld=::SelectObject(Canvas->Handle,hFont);
::SetRect(&rc,0,0,20,ClientHeight);
::SetTextColor(Canvas->Handle,RGB(255,255,255));
::TextOut(Canvas->Handle,3,ClientHeight-3,msg,lstrlen(msg));
::SelectObject(Canvas->Handle,hOld);
::DeleteObject(hFont);
以上的程式我不打算詳加說明,簡單地說,它就是建立一個旋轉90度的字形,然後將字串以此字形畫於螢幕上,此段程式碼的關鍵在於你必須知道Canvas->Handle即是代表GDI繪圖的HDC。其餘的函式說明你都可以在一般講解傳統Windows SDK繪圖的書籍中找到。
由此我們也可以得到一個經驗:雖然C++Builder的快速程式發展環境已經取代了傳統SDK式的程式設計中大部份的工作,然而通曉一些必要的SDK程式技巧卻可以使你上一層樓。所以我建議你在『行有餘力』時,不妨可以看看SDK相關書籍,充實基礎知識。或許我們可以名之爲『立足 BCB,放眼 SDK』的學習態度吧!
其他說明
在本程式中因爲TForm的BorderStyle性質爲bsNone。因此並沒有外框,爲了美化視窗,所以我寫了幾個輔助函式來繪出立體框。若你在其他程式中有類似的需求,也可以使用之。
void DoRect(TCanvas* Canvas,RECT& rect,COLORREF cTopColor,COLORREF cBottomColor)
{
POINT p[3];
p[0].x = rect.right;
p[0].y = rect.top;
p[1].x = rect.left;
p[1].y = rect.top;
p[2].x = rect.left;
p[2].y = rect.bottom;
Canvas->Pen->Color=TColor(cTopColor);
Canvas->Polyline(p,3);
p[1].x = rect.right;
p[1].y = rect.bottom;
p[2].x--;
Canvas->Pen->Color=TColor(cBottomColor);
Canvas->Polyline(p,3);
}
void Frame3D(TCanvas* Canvas,RECT& rect,COLORREF cTopColor,COLORREF cBottomColor,int iColWidth)
{
rect.bottom--; rect.right--;
while (iColWidth > 0)
{
iColWidth--;
DoRect(Canvas,rect,cTopColor,cBottomColor);
InflateRect(&rect,-1,-1);
}
rect.bottom++; rect.right++;
}
void DrawButtonFace(TCanvas* Canvas,RECT& rect,int nBevelWidth)
{
Canvas->Brush->Color=clBtnFace;
Canvas->FillRect(TRect(rect));
Frame3D(Canvas,rect,::GetSysColor(COLOR_BTNSHADOW),::GetSysColor(COLOR_WINDOWFRAME),nBevelWidth);
Frame3D(Canvas,rect,::GetSysColor(COLOR_BTNHIGHLIGHT),::GetSysColor(COLOR_BTNSHADOW),nBevelWidth);
}
這叄個函式中最重要的就是 DrawButtonFace,它是用來在一個矩形範圍中畫出一個類似Button的立體方框,在本程式中我用它來畫出TForm的邊框。你可以由圖一看出它的視覺效果。
程式的改進
前面我們提到改進bsNone視窗視覺效果的方式是利用自行撰寫的DrawButtonFace函式來達成,它雖不失爲一個解決問題的方法,但是卻也因此增加了程式的複雜度,再來我爲你示範一種利用改寫CreateParams函式的技巧來達成類似功能的方法。
CreateParams是一個虛擬函式,你可以經由它來修改windows的style,因爲原先在C++Builder中所定義的Form是一個Dialog(交談窗),而交談窗的外形內定是有標題棒的,然而如果我們如前面的方法將外框設爲bsNone 的話,那就必須自行畫出假的視窗外框,否則看起來不好看。
但是在Windows系統中除了前面的Dialog式的視窗之外,還提供了另一種POPUP式的視窗,只不過在C++Builder並未提供該選項罷了。因此我們其實可以透過改寫CreateParams的方式來產生WS_POPUP形式的視窗,如此一來我們就不必煞費周章地撰寫畫外框的函式了。它的程式其實很簡單,只是將Params.Style的WS_DLGFRAME (代表使用Dialog外框),改成另一種WS_POPUP (彈出式視窗)。要做到以上效果,只要利用and及or運算就可以達到了。以下即爲其程式碼:
void __fastcall TForm1::CreateParams(TCreateParams& Params)
{
TForm::CreateParams(Params);
Params.Style |= WS_POPUP;
Params.Style ^= WS_DLGFRAME;
}
圖二爲改寫後的程式執行結果,不僅程式簡潔了許多,而且外觀也較好看,那是因爲我們在畫標題棒時,不會像前面一樣將外框蓋住的緣故。
圖二 利用CreateParams技巧的新程式。
訊息使用範例二 在程式中使用材質背景
許多人在使用網際網路瀏覽器如Internet Explorer、Netscape上網站時,會發現許多網頁上普遍使用了材質圖案做爲背景,大大加強了它的視覺效果,也使用網頁看起來更爲美侖美奐,這時也許你會想:這個材質背景是如何做出來的呢?
在以下的文章中,我會示範如何利用C++Builder做出上述的材質背景效果,讓你的程式也可以做出如Browser般的效果。此程式的執行效果如圖叄
圖叄 具有材質片背景的Form
WM_ERASEBKGND訊息說明
WM_ERASEBGGN是在Windows背景將要被清除時,所觸發的訊息。在此訊息發生時,會傳入要清除的Windows的HDC ( 還記這個SDK中用來繪圖的重要角色吧?)。因此我們可以取得此HDC,然後將Canvas的Handle值設爲該值,如此便可以在Canvas上作畫了。
宣告使用WM_ERASEGKGN
class TForm1 : public TForm
{
__published: // IDE-managed Components
TPanel *Panel1;
private: // User declarations
public: // User declarations
__fastcall TForm1(TComponent* Owner);
void virtual __fastcall OnWMEraseBkgnd(TWMEraseBkgnd& Msg);
BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(WM_ERASEBKGND,TWMEraseBkgnd,OnWMEraseBkgnd)
END_MESSAGE_MAP(TForm)
};
爲了要攔截WM_ERASEBMGN訊息,因此我們必須利用前面談過的巨集來宣告之,在此我們採用C++Builder爲WM_ERASEBKGND定義的TWMEraseBkgnd訊息結構做爲參數,同時定義了一個訊息處理函數 OnWMEraseGkgnd。當然,它所傳入的訊息參數是前述的TWMEraseBkgnd&。
以材質圖案填滿畫面
在完成了訊息處理函數的定義之後,再來我們就必須撰寫實際的程式碼。爲了在所傳入的HDC中做畫,我們必須new一個 Canvas,然後自TWMEraseBkgnd中取得HDC的值。
接着爲了要將材質背景載入,我們必須new一個Graphics::TBitmap (在這裏加上Graphics:: 是因爲尚有另一種Tbitmap ,是位於Windows的namespace中的,因此必須以Graphics:: 來區別它的名稱空間)。然後,我們就可以利用LoadFromFile將材質背景圖案載入。
在完成了以上兩個必要的準備動作之後,我們就可以正式將材質背景畫在Canvas上面了,首先當然要計算所畫的次數,然後利用迴圈以Canvas的Draw指令,將它填滿整個螢幕。最後不要忘了將new產生的物件,以delete刪除之。以下爲其程式列表:
void __fastcall TForm1::OnWMEraseBkgnd(TWMEraseBkgnd& Msg)
{
TCanvas* canvas = new TCanvas;
Graphics::TBitmap* bitmap = new Graphics::TBitmap;
bitmap->LoadFromFile("back.bmp");
canvas->Handle = Msg.DC;
int cx = ClientWidth/bitmap->Width + 1;
int cy = ClientHeight/bitmap->Height + 1;
for (int i=0; i<cy; i++)
for (int j=0; j<cx; j++)
canvas->Draw(j*bitmap->Height,i*bitmap->Width,bitmap);
Msg.Result = true;
delete bitmap;
delete canvas;
}
怎麼樣?不錯吧!其實只要多多充實關於Windows訊息的知識,雖然在C++Builder中直接使用Windows訊息的機會並不多,但是在某些時候,它卻可以發揮小兵立大功的效果,在像本節所舉的例子一般。
結論
在本文中我爲你示範了在C++Builder中處理訊息的方法,同時以一個實際的自定標題棒視窗及材質背景圖爲範例,仔細說明了其中之技巧。除此之外,在Windows系統中,訊息(Message)是無所不在的,它是許多傳統的視窗元件用以互相溝通的元件,因此除非你能保證你永遠不會使用到別的標準元件,否則你就必須具備訊息處理的能力。所以說,瞭解Windows訊息是你不可或缺的技巧,唯有如此,你才能『百尺竿頭,更進一步』,不會被RAD給侷限住。