2011年2月22日 星期二

繼承 TCustomPanel

//-------- .h
class MyPanel : public TCustomPanel

{
private:
public:
__fastcall MyPanel(TComponent* Owner);
virtual void __fastcall Paint();
};
//------建構含式
__fastcall MyPanel::MyPanel(TComponent* Owner)
: TCustomPanel(Owner)
{
}

void __fastcall MyPanel::Paint()
{
Canvas-> TextOutA(0,0, "哈哈,我繼承了! ");
}


站在巨人的肩膀

編寫元件的第一件事情就是確定我們從那裡繼承的問題,選取一個好的祖先類是編寫一個好的元件的第一步,那麼到底如何選取他山之石呢?一般性的規則是這樣的:


1.對於有界面的顯示的,需要處理鍵盤事件的,又不是容器的組件從TCustomControl繼承
2.對於有界面的顯示的,需要不處理鍵盤事件的,需要處理鼠標事件的從TGraphicsControl繼承
3.對於沒有界面顯示的,類似與TOpenDialog/TXpMenu這樣的控件從TComponent繼承
4.如果你想擴展某個指定的控件,比如TPanel,你最好從TCustomPanel繼承,而不要從TPanel直接繼承。

注意上面第4條規則,基本上所有組件都有TCustomXXX的父類,這也是VCL鼓勵的繼承對象,原因在於你可以定製元件屬性的可見性,最重要的是他們的構造函數和析構函數是虛擬的。




VCL元件DIY

Writing VCL Component in Borland C++ Builder 1.0
前言
Borland出的Delphi在市場上耕耘了數年了,會用Delphi的人,大都會自己有自己寫的元件,幾年下來,VCL的資源豐 富,大部份各位想要的元件大都已有人寫出來了.然而萬一(真的萬一)有一天,你要元件但是找不到.....嘿嘿嘿...就只有DIY了.今天不是給各位魚 吃,而是教各位釣魚的方法.那麼就將小弟嘗試(不敢說研究)的結果,供大家參考,也許哪天各位放棄M$派的語言,轉而投向Borland陣營時,這一篇就 會有小小的參考價值.....

建立一個新的元件
咱們廢話少說,先New一個新的元件出來吧.在上方選單上有一個"Component",選擇後再選"New",這時會 開一個Component Wizard要你定義Class Name(你所要寫的元件的名字),Ancestor type(這個元件的父類別)和Palette Page(你要將元件放於哪一個元件Page).假設我們定Class Name = TMycomponent , Ancestor type = TCustomControl按OK之後出現一個畫面(如圖一).



咱們先儲存起來吧,再把這個CPP檔和H檔打開.這時C++ Builder已經建立了一個最基本的元件(就是什麼功能都沒有的那一種).想裝裝看嗎??找上方選單的"Component",選擇後 選"Install",按"Add"把你的元件加進去,按"OK"等它Compile完就可以了.想知道你的元件在哪出現嗎??到你元件中設定的 Palette Page中找找看吧...

繪出元件的外形.
其實對windows程式有點瞭解的人都知道,攔截WM_PAINT可以重繪外形,做Repaint()的動作.所以我 們勢必要Handle這個函數,幸運的是WM_PAINT自動地被C++ Builder所攔截,我們只要在類別中implement這個Paint()函數就可以了.然後在Paint()中利用Canvas去畫出你的元件的外 型.

例如將元件繪成一個黑心實心的矩形:
在H檔的protected中
void __fastcall Paint();
在CPP檔中
void __fastcall TMycomponent :: Paint()
{ Canvas->Brush->Color=clBlack;
Canvas->Brush->Style=bsSolid;
Canvas->Rectangle(0,0,Width,Height);
}

不難,對吧??

增加元件的函數.
平常我們在寫類別時,多少都會帶上一些Method.同樣的VCL也可以,因為VCL本身就是一個類別.那也就是說,我們只要寫類似一般C++類別的Method就可以了.夠簡單吧??

如何在元件中再加入另一個元件?
這一點我舉一個例子來說明好了.
例如在我們寫的元件(TMycomponent)中加入一個Timer.
請在H檔中的private中加入
TTimer *Timer1;
void __fastcall Timer1Timer(TObject *Sender);
請在CPP檔的建構子中加入
Timer1 = new TTimer (this);
Timer1->Interval=1000; // 1秒觸發一次
Timer1->Enabled=true; // 允許Timer Enable
Timer1->OnTimer= Timer1Timer;
// 設定Timer的Event為Timer1Timer函數
然後寫Timer1Timer這個函數.
void __fastcall TMycomponent::Timer1Timer(TObject *Sender)
{
// 你想做的事.........
}

不錯吧.當然這只是個例子,你可以加其他的元件進來.


增加Properties
用過Delphi或C++ Builder的人都該知道每次編寫一個Form時,會有一個視窗(Object Inspector)列出各個元件的Properties,大家都可以在上面改變元件其中幾個Properties的值,那你就會問我,這個東西應該如何 做呢??

其實這個東西不難,但是該注意的地方很多,請君耐心待我說來:

在H檔中的__published或public部份(一般來說是__published)寫下(我們假設要多一個property為Test ,Data type為int )
__property int Test = {read = MyTest ,write = MyTest ,nodefault};

在H檔中的private部份加入
int MyTest;

這 時一個property就加進去了(興奮嗎??Rebuild Library,然後new一個新的Form,去拉你所寫的元件到這個Form,仔細看一下Object Inspector,看到沒??沒關係,你可以靠近一點.沒看到??沒關係,你可以再靠近一點.....^_^)
說明一下:
__property int Test = {read = MyTest ,write = MyTest ,nodefault};
在大括號中
read=MyTest //當有人要讀Test的值時,就傳MyTest的值回去
//這也可以給Function的名稱 .
write=MyTest//當有人要寫Test的值時,就寫到MyTest
//這也可以給Function的名稱.

!![注意]
若要給Function名稱,則此Function宣告必須在private中,
而在function前加入__fastcall.

例如:
在H檔的private中
int MyTest;
void __fastcall SetMyTest(int Value);

在H檔的__published中
__property int Test = {read = MyTest,write=SetMyTest,nodefault};
在CPP檔中
void __fastcall TMycomponent :: SetMyTest(int Value)
{
MyTest=Value;
}

不錯吧.也許你會問我,這個的效果不是和剛剛那個一樣嗎??那我們幹嗎要換??? 這一點請看下一個小主題 <> 你就會明白了.一些Event如OnChange等的Event,就是在這裡被觸發的!!

 

增加新的Event
這是另一個重點,因應需要,很多情形下我們必需要觸發一些自訂的Event.這就會讓人想起了一些問題,我何時才要觸 發Event??我如何觸發Event??我的Event的作用是啥??這些想清楚了之後,再來寫會比較好.再回到上一個小主題的最後一部份,我們談到了 可以用Function存取Properties時,順帶著觸發Event.那就舉上一個例子做為其之延伸吧!!

例: 我們希望當使用者修改Test的值時會觸發OnChange
的Event.
在H檔的private中加入
TNotifyEvent FOnChange;
int MyTest;
void __fastcall SetMyTest(int Value);
在H檔的__published中
__property int Test = {read = MyTest,write=SetMyTest,nodefault};
__property TNotifyEvent OnChange = {read=FOnChange, write=FOnChange};
在CPP檔中
void __fastcall TMycomponent :: SetMyTest(int Value)
{
MyTest=Value;
if ( FOnChange!=NULL)
FOnChange(this);
}
如此當有人修改了Test的值時,而且Event所指向的Function
有被你所設過的話,就會去執行該Function的程式碼.
如此完成了自訂Event的觸發.帥氣,不是嗎??

攔截訊息.
在Windows中漫佈著Message的流動,我想這是每一個瞭解Windows程式設計的人都會知道的.因應著某些需要, 我們必須去攔截某一些我們要的訊息.例如當元件的大小改變時,我們必須做出相對應的動作,如重繪元件等,那就必須去攔截WM_SIZE.如果我們需要在某 個地方設定Focus,那麼我們就必須去攔截WM_SETFOCUS來達到我們的要求.

重點是:我們該如何去攔截我們要的Message呢??我們舉例子說明:以攔截WM_SIZE來說.

在H檔的private中寫入
MESSAGE void __fastcall WMSize(TWMSize &Message);
在__published中寫入
BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(WM_SIZE, TWMSize, WMSize);
//第一個參數:攔截的訊息名稱
//第二個參數:對應到C++ Builder中的結構宣告
//第三個參數:攔入訊息的Function名稱.
END_MESSAGE_MAP(TCustomControl);
^^^^^^^^^^^^^^^這是父類別
在你的CPP檔中寫入
void __fastcall TMycomponent::WMSize(TWMSize &Message)
{
//你要的處理
}

大功告成.你已攔截了你要的訊息了.簡單吧,以各位的聰明才智舉一反三,甚至是反十,我想都不會有什麼問題,只是改一些小地方而已.

到此,我們已把最基本的元件寫法,談了一遍,大家可以喝杯茶輕鬆一下,後面的幾個小題將提到一些觀念和一些各位可能會問的問題.先去看個電視再來看其他的吧.....*^_^*

我應該從哪一個類別來繼承呢??
這是一個好問題,每個人第一次寫元件都會不知道該從哪一個類別繼承..在VCL中有一些所謂的"自訂"類別,許多許多的控制項就直接從這些"自訂類別"裡 繼承下來.例如說:TMemo是繼承自TCustomMemo類別 , TRichEdit則是從TCustomRichEdit繼承下來的,如果你寫的元件是寫類似這些功能的話,那你可以直接繼承相對應的自訂類別.

如果你要寫視覺元件,可以繼承自TGraphicControl,不過這有個缺點,就是沒有Window Handle,亦不能接受輸入焦點.

如果你要寫類似Timer元件(拉至Form上還是一樣一個小圖)的話,可以繼承TComponent

如果你要寫有關視窗元件的話,那你可以繼承自TWinControl因為它將已存在的視窗控制項包裝起來,如Windows標準控制項或VBX元件.

為何我的元件不能處理方向鍵??
這一點你就必須去攔截一個叫WM_GETDLGCODE的訊息才能處理方向鍵的訊息,如果你不這樣做,那方向鍵的功能就只能用來移動視窗的焦點而已.

在H檔案中的private加入
MESSAGE void __fastcall WMGetDlgCode ( TWMGetDlgCode &Message);
在__published的部份
BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(WM_GETDLGCODE,TWMGetDlgCode,WMGetDlgCode);
END_MESSAGE_MAP(TCustomControl);
^^^^^^^^^^^^^^^
注意這是你元件的Ancestor type
在CPP檔中
void __fastcall TMycomponent::WMGetDlgCode(TWMGetDlgCode &Message)
{
Message.Result=DLGC_WANTARROWS|DLGC_WANTCHARS;
}

[另一種做法]
攔截CM_WANTSPECIALKEY這個元件訊息!!
這一個元件訊息提供給你比攔截WM_GETDLGCODE這個
視窗訊息更容易且靈活的判斸方法來決定是否需要某些特殊
鍵的訊息.特殊鍵抱括:VK_TAB,VK_LEFT,VK_RIGHT,
VK_UP,VK_DOWN,VK_RET,VK_ESCAPE和VK_CANCEL.
如果訊息傳回值是非零值,這個鍵就會被送至KeyPress方法
以供處理,否則這個鍵的訊息會被送至元件的父控制項,以預
設方式來處理.


// 在H檔的private:
Message void __fastcall CMWantSpecialKey(TCMWantSpecialKey &Message);
// 在H檔__published:
BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(CM_WANTSPECIALKEY,
TCMWantSpecialKey,CMWantSpecialKey);
END_MESSAGE_MAP(TCustomControl);
// 在CPP檔中
void __fastcall CMWantSpecialKey(TCMWantSpecialKey &Message)
{
switch(Message.CharCode)
{ case VK_LEFT:
case VK_RIGHT:
case VK_UP:
case VK_DOWN:
Message.Result=1;
break;
}
}


為何我的元件重繪時的會有閃動的情形??
引起閃動的原因很多,主要是WM_PAINT和WM_ERASEBKGND這兩個訊息的處理.當 VCL控制收到WM_ERASEBKGND這個訊息時,它會將元件背景擦掉,然後設定成背景預設的顏色.也就是說,再整個元件重繪之前,會先被清成背景的 顏色,然成再動WM_PAINT予以重繪.如此就是造成閃動的最主要原因了.這個情形尤以繼承自TWinControl的元件較多此類之情形.

如何解決???就是告訴Windows你要自行解決"所有的"繪圖動作,如此Windows就不會做清成背景色的動作.但是有一個很重要 的前提,就是你一定要確定你的Paint Method的確把整個元件重繪過,如果漏了什麼地方忘了畫,那麼那個地方的資料會好像透明的元件一樣,有視窗在元件上方就會留下殘影,可以想見其情況 否??

這個方法可以加速元件的繪圖動作,這可以想見的,因為少了填背景色的動作.

// 在H檔的private:
Message void __fastcall WMEraseBkgnd(TWMEraseBkgnd &Message);
// 在H檔__published:
BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(WM_ERASEBKGND,
TWMEraseBkgnd,WMEraseBkgnd);
END_MESSAGE_MAP(TCustomControl);
// 在CPP檔中
void __fastcall WMEraseBkgnd(TWMEraseBkgnd &Message)
{
//不要重繪背景,會造成元件閃動地喔!!
Message.Result=1;
}

[另外一種可能]
就是在你寫Paint Method時,在裡面用了類似以下的函數來
繪元件,如Canvas中的FillRect(),這個函數在每次畫繪時會做
類似WM_ERASEBKGND訊息的動作,所以要避開這些動作.


VCL在C++ Builder 1.0和C++ Builder 3.0的不同點:
C++ Builder1.0 繼承了Delphi 2.0的傳統,C++ Builder3.0則相似於Delphi 3.0,其主要的不同點在於Package.Borland在推出Delphi3. 0加入Package的觀念,就是將各個類似的元件置於同一個Package中,某天覺得這個Package的元件,暫時不適合留著用,你可以先 Disable這個package,而不用把它從Library中移除,若是以後有用到的一天,就把它Enable回來就好了.Package也就提昇了 管理上的方便.