1. 程式人生 > >自繪按鈕的實現

自繪按鈕的實現

如果你希望能夠在自己的程式中表現出新意,那麼你一定不會僅僅滿足於MFC提供那些標準控制元件。這時,我們就必須自己另外多做些工作了。就改變控制元件外觀這一點來說,主要是利用控制元件的自繪功能(Owner Draw)實現的。本篇將和各位一起定義一個XP風格的CXPButton按鈕類,目的不在於介紹CXPButton類的使用技巧,而在於向各位闡述實現自繪按鈕的方法。當然如果你覺得CXPButton有用的話,也可以把它的原始檔儲存下來,直接加入到自己的專案中。

本篇要點:

一、準備工作

二、實現原理及難點

三、按鈕類的使用

四、小結與提示

五、附錄

一、準備工作

在開始編碼之前,首先應該確定好,更準確的說應該是設計好按鈕在各種狀態下的外觀。按鈕控制元件的幾中基本狀態包括:

Normal狀態,就是按鈕一開始顯示時的樣子。

Over狀態,滑鼠指標移動到按鈕上面時按鈕顯示的樣子。

Down狀態,按下按鈕時顯示的樣子。

Focus狀態,按鈕按下後鬆開的樣子,例如標準按鈕按下鬆開之後會看到按鈕內部有一個虛線框。

Disable狀態,當然就是按鈕被設定成無效的時候的樣子啦。

我參考了一下WindowsXP中普通按鈕的實際樣子,設計出XP按鈕各種狀態的外觀,如下圖所示:

至於Down狀態主要是在Over狀態的基礎上將文字往右下的方向稍微平移,以實現下壓的效果。

二、實現原理及難點

下面我們開始類的建立,在Workspace的ClassView頁中右擊列表樹的根結點,選擇New Class…

在彈出視窗中進行派生類的定義,如下圖所示,注意,你需要填寫的只有Name和Base class兩項,其餘的選項保持預設值就可以了。

按OK按鈕退出之後,我們可以在ClassView裡面看到新建立的類的名字。接下來我們可以為CXPButton類新增各種成員變數。因為自繪控制元件說穿了就是畫圖,所以在成員變數中可以看到各種與畫圖有關的資料型別,一般來說成員變數會在類的建構函式中初始化,在類的解構函式中銷燬。詳細程式碼請參見本篇附帶的源程式。

下面簡要敘述一下按鈕的實現原理:

1. 在控制元件初始化時為按鈕新增Owner Draw的屬性。這是因為在MFC中,要想啟用控制元件的自繪功能,要求該控制元件的屬性中必須包含屬性值BS_OWNERDRAW,這一步我們可以通過類嚮導為CXPButton類新增PreSubclassWindow()函式,在該函式中完成屬性值的設定。當啟用控制元件的自繪功能之後,每次控制元件狀態改變的時候都會執行函式DrawItem(),該函式的作用就是繪製控制元件在各種狀態下的外觀。

2. 新增WM_MOUSELEAVE訊息函式,當滑鼠指標離開按鈕時,觸發該訊息函式,我們在函式中新增程式碼,通知DrawItem函式滑鼠指標已經離開了,讓按鈕重繪。

3. 新增WM_MOUSEHOVER訊息函式,當滑鼠指標位於按鈕之上時,觸發該訊息函式,我們在函式重新增程式碼,通知DrawItem函式滑鼠指標現在正在按鈕的上面,讓按鈕重繪。

4. 新增DrawItem函式。在DrawItem中根據按鈕當前的狀態繪製按鈕的外觀。可以說自繪控制元件的大部分功能都是在這個函式中實現的。DrawItem函式包含了一個LPDRAWITEMSTRUCT的指標,本篇會在稍後予以講解。

瞭解了基本的設計思路之後,剩下就看我們怎麼去實現了。我本人覺得這裡有兩個難點,首先是WM_MOUSELEAVE和WM_MOUSEHOVER不是標準的Windows訊息函式,它們不能通過類嚮導來新增,所有的新增工作都需要通過手工輸入程式碼來完成。另一個難點是DrawItem中的LPDRAWITEMSTRUCT指標,它指向了一個DRAWITEMSTRUCT的結構,這個結構中包含了控制元件的各種細節,為我們提供了實現自繪功能的必要資訊。

難點一:事實上WM_MOUSELEAVE和WM_MOUSEHOVER兩個Windows訊息是通過WM_MOUSEMOVE訊息觸發的,而WM_MOUSEMOVE是標準的Windows訊息,因此我們可以通過類嚮導來為CXPButton類新增WM_MOUSEMOVE訊息函式。

函式的程式碼見如下,這段程式碼非常有用,在其它的自繪控制元件中,如果想觸發WM_MOUSELEAVE和WM_MOUSEHOVER訊息,也是使用類似的方法實現的。

01.void CXPButton::OnMouseMove(UINT nFlags, CPoint point) 02.{ 03.// TODO: Add your message handler code here and/or call default 04.if (!m_bTracking) 05.{ 06.TRACKMOUSEEVENT tme; 07.tme.cbSize = sizeof(tme); 08.tme.hwndTrack = m_hWnd; 09.tme.dwFlags = TME_LEAVE | TME_HOVER; 10.tme.dwHoverTime = 1; 11.m_bTracking = _TrackMouseEvent(&tme); 12.} 13.CButton::OnMouseMove(nFlags, point); 14.}

我們接著新增WM_MOUSELEAVE和WM_MOUSEHOVER訊息訊息函式。在CXPButton類的宣告中(即在XPButton.h檔案中)找到afx_msg void OnMouseMove(UINT nFlags, CPoint point);的函式宣告,緊接其下輸入

1.afx_msg LRESULT OnMouseLeave(WPARAM wParam, LPARAM lParam); 2.afx_msg LRESULT OnMouseHover(WPARAM wParam, LPARAM lParam);

然後在XPButton.cpp檔案中找到ON_WM_MOUSEMOVE(),緊接其後輸入

1.ON_MESSAGE(WM_MOUSELEAVE, OnMouseLeave) 2.ON_MESSAGE(WM_MOUSEHOVER, OnMouseHover)

當然最後還有函式的實現了,詳細程式碼可見本篇提供的源程式,這裡就不再重複了。

難點二:

下面我們看看DRAWITEMSTRUCE結構為我們提供了哪些有用資訊呢?

DRAWITEMSTRUCT結構的定義如下:

01.typedef struct tagDRAWITEMSTRUCT { 02.UINT   CtlType;                       //控制元件型別 03.UINT   CtlID;                          //控制元件ID 04.UINT   itemID;                        //選單項、列表框或組合框中某一項的索引值 05.UINT   itemAction;                   //控制元件行為 06.UINT   itemState;                     //控制元件狀態 07.HWND   hwndItem;                 //父視窗控制代碼或選單控制代碼 08.HDC    hDC;                           //控制元件對應的繪圖裝置控制代碼 09.RECT   rcItem;                        //控制元件所佔據的矩形區域 10.DWORD  itemData;                  //列表框或組合框中某一項的值 11.} DRAWITEMSTRUCT, *PDRAWITEMSTRUCT, *LPDRAWITEMSTRUCT;

其實不僅是按鈕控制元件,其它控制元件,如ComboBox、ListBox、StaticText等都是通過DRAWITEMSTRUCT來記錄控制元件資訊的。關於這個結構的詳細文件可參考本篇的附錄。

也許你早已看到許多自繪按鈕的例子,實際上自繪按鈕本身的函式結構都是差不多的,它們顯示效果的區別主要取決於程式碼編寫者對GDI作圖函式的運用與掌握程度。有興趣的朋友可以研究一下CXPButton類中DrawItem函式的資料結構,其實只要修改一下其中GDI繪圖函式的部分程式碼,馬上又能做出另一個自繪按鈕控制元件了。

三、按鈕類的使用

下面演示CXPButton類的使用。往對話方塊中新增一個按鈕控制元件,假設它的ID值為IDC_BUTTON1。進入類嚮導(Class Wizard)的Member Variables屬性頁,為IDC_BUTTON1新增一個變數m_btnNormal。確定退出後再進行編譯,就可以看到重新定義過XP風格按鈕了。

如果你是之間把CXPButton的原始檔引入自己的工程中的,那麼在上圖的Variable type中是看不到CXPButton選項的。但是可以通過以下方法加入:

1. 首先儲存工程後退出。

2. 在工程的目錄下找到一個字尾名為.clw的檔案,將其刪除。但是為了以防萬一還是建議你實現備份一下。

3. 重新開啟工程,進入類嚮導,此時會看到一下一個彈出對話方塊,我們選擇“是(Yes)”。

4. 再選擇“Add All”,這樣我們就可以在類嚮導中使用CXPButton的變數型別了。

四、小結與提示

對於按鈕來說,當按鈕上面任何可見的部分發生變換的時候,都要呼叫DrawItem函式進行重繪。自繪製按鈕必須設定BS_OWNERDRAW的屬性,設定的程式碼在PreSubclassWindows函式中完成。另外為了防止系統字型設定的變化影響控制元件的表達效果,還可以在該函式中為控制元件指定某種固定的字型。但是要注意的是這個

讓我們來回顧一下實現自繪按鈕的基本步驟:

a. 確定設計方案;

b. 初始化,但是記得要在函式退出前恢復先前的GDI物件,並釋放所佔領的資源;

c. 新增相應訊息函式;

d. 新增繪圖函式DrawItem,在DrawItem中作圖的順序一般是先畫外邊框,再上底色,接著寫文字,最後是畫內邊框。不過有些人也喜歡把邊框放到最後畫,這問題不大。

五、附錄

DRAWITEMSTRUCT結構文件(根據Msdn翻譯)

DRAWITEMSTRUCT

DRAWITEMSTRUCT 為需要自繪的控制元件或者選單項提供了必要的資訊。在需要繪製的控制元件或者選單項對應的WM_DRAWITEM訊息函式中得到一個指向該結構的指標。 DRAWITEMSTRUCT結構的定義如下:

01.typedef struct tagDRAWITEMSTRUCT { 02.UINT CtlType; 03.UINT CtlID; 04.UINT itemID; 05.UINT itemAction; 06.UINT itemState; 07.HWND hwndItem; 08.HDC hDC; 09.RECT rcItem; 10.ULONG_PTR itemData; 11.} DRAWITEMSTRUCT;

結構成員:

CtlType

指定了控制元件的型別,其取值如下表所示。

取值

描述

ODT_BUTTON

按鈕控制元件

ODT_COMBOBOX

組合框控制元件

ODT_LISTBOX

列表框控制元件

ODT_LISTVIEW

列表檢視控制元件

ODT_MENU

選單項

ODT_STATIC

靜態文字控制元件

ODT_TAB

Tab控制元件

CtlID

指定了自繪控制元件的ID值,而對於選單項則不需要使用該成員

itemID

表示選單項ID,也可以表示列表框或者組合框中某項的索引值。對於一個空的列表框或組合框,該成員的值為–1。這時應用程式只繪製焦點矩形(該矩形的座標由rcItem   成員給出)雖然此時控制元件中沒有需要顯示的項,但是繪製焦點矩形還是很有必要的,因為這樣做能夠提示使用者該控制元件是否具有輸入焦點。當然也可以設定itemAction   成員為合適值,使得無需繪製焦點。

itemAction  

指定繪製行為,其取值可以為下表中所示值的一個或者多個的聯合。

取值

描述

ODA_DRAWENTIRE

當整個控制元件都需要被繪製時,設定該值

ODA_FOCUS

如果控制元件需要在獲得或失去焦點時被繪製,則設定該值。此時應該檢查itemState成員,以確定控制元件是否具有輸入焦點。

ODA_SELECT

如果控制元件需要在選中狀態改變時被繪製,則設定該值。此時應該檢查itemState 成員,以確定控制元件是否處於選中狀態。

itemState  

指定了當前繪製操作完成後,所繪項的可見狀態。例如,如果選單項應該被灰色顯示,則可以指定ODS_GRAYED狀態標誌。其取值可以為下表中所示值的一個或者多個的聯合。

取值

描述

ODS_CHECKED

如果選單項將被選中,則可設定該值。該值只對選單項有用。

ODS_COMBOBOXEDIT

在自繪組合框控制元件中只繪製選擇區域。

ODS_DEFAULT

預設值。

ODS_DISABLED

如果控制元件將被禁止,則設定該值。

ODS_FOCUS

如果控制元件需要輸入焦點,則設定該值。

ODS_GRAYED

如果控制元件需要被灰色顯示,則設定該值。該值只在繪製選單時使用。

ODS_HOTLIGHT

Windows 98/Me, Windows 2000/XP: 如果滑鼠指標位於控制元件之上,則設定該值,這時控制元件會顯示高亮顏色。

ODS_INACTIVE

Windows 98/Me, Windows 2000/XP: 表示沒有啟用的選單項。

ODS_NOACCEL

Windows 2000/XP: 控制元件是否有快速鍵盤。

ODS_NOFOCUSRECT

Windows 2000/XP: 不繪製捕獲焦點的效果。

ODS_SELECTED

選中的選單項。

hwndItem    

指定了組合框、列表框和按鈕等自繪控制元件的視窗控制代碼;如果自繪的物件時選單項,則表示包含該選單項的選單控制代碼。

hDC

指定了繪製操作所使用的裝置環境。

rcItem

指定了將被繪製的矩形區域。這個矩形區域就是上面hDC的作用範圍。系統會自動裁剪組合框、列表框或按鈕等控制元件的自繪製區域以外的部分。也就是說rcItem中的座標點(0,0)指的就是控制元件的左上角。但是系統不裁剪選單項,所以在繪製選單項的時候,必須先通過一定的換算得到該選單項的位置,以保證繪製操作在我們希望的區域中進行

itemData

對於選單項,該成員的取值可以是由

CMenu::AppendMenu、

CMenu::InsertMenu或者

CMenu::ModifyMenu

等函式傳遞給選單的值。

對於列表框或這組合框,該成員的值可以為由

ComboBox::AddString、

CComboBox::InsertString、

CListBox::AddString或者

CListBox::InsertString

等傳遞給控制元件的值。

如果ctlType 的取值是ODT_BUTTON或者ODT_STATIC, itemData的取值為0。