common control 4.7版本介紹了一個新的特性叫作Custom Draw,這個名字顯得模糊不清,讓人有點摸不着頭腦,並且MSDN裏也只給出了一些如風的解釋和例子,沒有誰告訴你你想知道的,和究竟這個特性有什麼好處。
Custom draw能夠被想象成一個輕量級的,容易使用的重繪方法(重繪方法還有幾種,例如Owner Draw等)。這種容易來自於咱們只須要處理一個消息(NM_CUSTOMDRAW),就可讓Windows爲你幹活了,你就不用被逼去處理"重繪過程"中全部的髒活了。
這篇文章的焦點是如何在一個LISTCTRL控件上使用Custom Draw消息。究其緣由,一部分是由於我已經在個人工做上使用了Custom Draw有一段時間了,我很熟悉它。另外一個緣由是這個機制確實是很是好用,你只須要寫不多量的代碼就能夠達到很好的效果。使用 Custom draw 來對控件外觀編程甚至能夠代替不少的古老方法。
如下代碼是在WIN98 和VC6 SP2的環境下寫的,common controls DLL的版本是5.0。我已經對其在WinNT 4上進行了測試。系統要運行這些代碼,它的common controls DLL的版本必須至少是4.71。但隨着IE4 的發佈,這已經不是問題了。(IE會夾帶着這個DLL一塊兒發佈)
Custom Draw 基礎
我將會盡我所能把Custom Draw的處理描述清楚,而不是簡單的引用MSDN的文檔。這些例子都須要你的程序有一個ListCtrl在對話框上,而且這個
ListCtrl處於Report和多列模式。
Custom Draw 的消息映射入口
Custom draw 是一個相似於回調的處理過程,Windows在繪製List Ctrl的某個時間點上經過 Notification 消息通知你的程序,你能夠選擇忽略全部的通知(這樣你就會看到標準的ListCtrl),或者處理某部分的繪製(實現簡單的效果),甚至整個的控件都由你來繪製(就象使用Owner-Drawing同樣)。這個機制的真正賣點是:你只須要實現一些你須要的,其他的可讓Windows爲你代勞。
好了,如今你能夠開始爲你的ListCtrl添加Custom Draw去作一些個性化的事情了。你首先要有正確的Comm Ctrl Dll版本,而後Windows會爲你發送NM_CUSTOMDRAW消息,你只
須要添加一個處理函數以便開始使用Custom draw。
首先添加一個消息映射,象下面同樣:
ON_NOTIFY ( NM_CUSTOMDRAW, IDC_MY_LIST, OnCustomdrawMyList )處理函數的原形以下:
afx_msg void OnCustomdrawMyList ( NMHDR* pNMHDR, LRESULT* pResult );這就
告訴MFC你要處理從你的ListCtrl控件發出的WM_NOTIFY消息,ID爲IDC_MY_LIST,通知碼爲NM_CUSTOMDRAW,OnCustomdrawMyList就是你的處理函數。
若是你有一個從ClistCtr派生的類,你想爲它添加custom draw,你就可使用ON_NOTIFY_REFLECT來代替。以下:
ON_NOTIFY_REFLECT ( NM_CUSTOMDRAW, OnCustomdraw )
OnCustomdraw的原形和上面的函數一致,但它是聲明在你的派生類裏的。
Custom draw將控件的繪製分爲兩部分:擦除和繪畫。Windows在每部分的開始和結束都會發送NM_CUSTOMDRAW消息。因此總共就有4個消息。可是實際上你的程序所收到消息可能就只有1個或者多於四個,這取決於你想要讓WINDOWS怎麼作。每次發送消息的時段被稱做爲一個「繪畫段」。你必須牢牢抓住這個概念,由於它貫穿於整個「重繪」的過程。
因此,你
將會在如下的時間點收到通知:
l 一個item被畫以前——「繪畫前」段
l 一個item被畫以後——「繪畫後」段
l 一個item被擦除以前——「擦除前」段
l 一個item被擦除以後——「擦除後」段
並非全部的消息都是同樣有用的,實際上,我
不須要處理全部的消息,直到這篇文章完成以前,我還沒使用過擦除前和擦除後的消息。因此,不要被這些消息嚇到你。
NM_CUSTOMDRAW Messages提供給你的信息:
l NM_CUSTOMDRAW消息將會給你提供如下的信息:
l ListCtrl的句柄
l ListCtrl的ID
l 當前的「繪畫段」
l 繪畫的DC,讓你能夠用它來畫畫
l 正在被繪製的控件、item、subitem的RECT值
l 正在被繪製的Item的Index值
l 正在被繪製的SubItem的Index值
l 正被繪製的Item的狀態值(selected, grayed, 等等)
l Item的LPARAM值,就是你使用CListCtrl::SetItemData所設的那個值
上述全部的信息對你來講可能都很重要,這取決於你想實現什麼效果,但最常常用到的就是「繪畫段」、「繪畫DC」、「Item Index」、「LPARAM」這幾個值。
一個簡單的例子:
好了,通過上面的無聊的細節以後,咱們是時候來看一些簡單的代碼了。第一個例子很是的簡單,它只是
改變了一下
控件中文字的顏色。
處理的代碼以下:
void CPanel1::OnCustomdrawList ( NMHDR* pNMHDR, LRESULT* pResult ){ NMLVCUSTOMDRAW* pLVCD = reinterpret_cast<NMLVCUSTOMDRAW*>( pNMHDR );
// Take the default processing unless we set this to something else below. *pResult = 0;
// First thing - check the draw stage. If it's the control's prepaint // stage, then tell Windows we want messages for every item. if ( CDDS_PREPAINT == pLVCD->nmcd.dwDrawStage ) { *pResult = CDRF_NOTIFYITEMDRAW; } else if ( CDDS_ITEMPREPAINT == pLVCD->nmcd.dwDrawStage ) { // This is the prepaint stage for an item. Here's where we set the // item's text color. Our return value will tell Windows to draw the // item itself, but it will use the new color we set here. // We'll cycle the colors through red, green, and light blue. COLORREF crText;
if ( (pLVCD->nmcd.dwItemSpec % 3) == 0 ) crText = RGB(255,0,0); else if ( (pLVCD->nmcd.dwItemSpec % 3) == 1 ) crText = RGB(0,255,0); else crText = RGB(128,128,255);
// Store the color back in the NMLVCUSTOMDRAW struct. pLVCD->clrText = crText;
// Tell Windows to paint the control itself. *pResult = CDRF_DODEFAULT; }}
結果以下,你能夠看到行和行間的顏色的交錯顯示,多酷,而這隻須要兩個if的判斷就能夠作到了。
有一件事情必須記住,
在作任何的繪畫以前,你都要檢查正處身的「繪畫段」,由於你的處理函數會接收到很是多的消息,而「繪畫段」將決定你代碼的行爲。
一個更小的簡單例子:
下面的例子將演示怎麼去處理subitem的繪畫(其實subitem也就是列)
在ListCtrl控件繪畫前處理NM_CUSTOMDRAW消息。
告訴Windows咱們想對每一個Item處理NM_CUSTOMDRAW消息。
當這些消息中的一個到來,告訴Windows咱們想在每一個SubItem的繪製前處理這個消息
當這些消息到達,咱們就爲每一個SubItem設置文字和背景的顏色。
void CMyDlg::OnCustomdrawMyList ( NMHDR* pNMHDR, LRESULT* pResult )
{
NMLVCUSTOMDRAW* pLVCD = reinterpret_cast<NMLVCUSTOMDRAW*>( pNMHDR );
// Take the default processing unless we set this to something else below.
*pResult = CDRF_DODEFAULT;
// First thing - check the draw stage. If it's the control's prepaint
// stage, then tell Windows we want messages for every item.
if ( CDDS_PREPAINT == pLVCD->nmcd.dwDrawStage )
{
*pResult = CDRF_NOTIFYITEMDRAW;
}
elseif ( CDDS_ITEMPREPAINT == pLVCD->nmcd.dwDrawStage )
{
// This is the notification message for an item. We'll request
// notifications before each subitem's prepaint stage.
*pResult = CDRF_NOTIFYSUBITEMDRAW;
}
elseif ( (CDDS_ITEMPREPAINT | CDDS_SUBITEM) == pLVCD->nmcd.dwDrawStage )
{
// This is the prepaint stage for a subitem. Here's where we set the
// item's text and background colors. Our return value will tell
// Windows to draw the subitem itself, but it will use the new colors
// we set here.
// The text color will cycle through red, green, and light blue.
// The background color will be light blue for column 0, red for
// column 1, and black for column 2.
COLORREF crText, crBkgnd;
if ( 0 == pLVCD->iSubItem )
{
crText = RGB(255,0,0);
crBkgnd = RGB(128,128,255);
}
elseif ( 1 == pLVCD->iSubItem )
{
crText = RGB(0,255,0);
crBkgnd = RGB(255,0,0);
}
else
{
crText = RGB(128,128,255);
crBkgnd = RGB(0,0,0);
}
// Store the colors back in the NMLVCUSTOMDRAW struct.
pLVCD->clrText = crText;
pLVCD->clrTextBk = crBkgnd;
// Tell Windows to paint the control itself.
*pResult = CDRF_DODEFAULT;
}
}
執行的結果以下:
這裏須要注意兩件事:
l clrTextBk的顏色只是針對每一列,在最後一列的右邊那個區域顏色也仍是和ListCtrl控件的背景顏色一致。
l 當我從新看文檔的時候,我注意到有一篇題目是「NM_CUSTOMDRAW (list view)」的文章,它說你能夠在最開始的custom draw消息中返回CDRF_NOTIFYSUBITEMDRAW就能夠處理SubItem了,而不須要在CDDS_ITEMPREPAINT繪畫段中去指定CDRF_NOTIFYSUBITEMDRAW。可是我試了一下,發現這種方法並不起做用,你仍是須要處理CDDS_ITEMPREPAINT段。
處理「繪畫以後」的段
到如今爲止的例子都是處理「繪畫前」的段,當Windows繪製List Item以前就改變它的外觀。然而,在「繪製前」,你的繪製行爲時被限制的,你只能改變字體的顏色或者外觀。若是你想改變圖標的繪製,你能夠在「繪畫前」把整個 Item重畫或者在「繪畫後」去作這件事。當你作在繪畫後去作「自定義繪畫」是,你的「繪畫處理函數」就會在Windows畫完整個Item或者SubItem的時候被調用,你就能夠爲所欲爲的亂畫了!!
在這個例子裏,我將建立一個ListCtrl,通常的ListCtrl的Item若是被選擇了,則其Icon也會呈現出被選擇的狀態。而我建立的這個ListCtrl的Icon是不會呈現被選擇的狀態的。步驟以下:
對ListCtrl在「繪畫前」處理NM_CUSTOMDRAW消息。
告訴Windows咱們想在每一個Item被畫的時候得到NM_CUSTOMDRAW消息。
當這些消息來臨,告訴Windows咱們想在你畫完的時候獲取NM_CUSTOMDRAW消息。
當這些消息來到的時候,咱們就從新畫每個Item的圖標。
void CPanel3::OnCustomdrawList ( NMHDR* pNMHDR, LRESULT* pResult ){ NMLVCUSTOMDRAW* pLVCD = reinterpret_cast<NMLVCUSTOMDRAW*>( pNMHDR ); *pResult = 0; // If this is the beginning of the control's paint cycle, request // notifications for each item. if ( CDDS_PREPAINT == pLVCD->nmcd.dwDrawStage ) { *pResult = CDRF_NOTIFYITEMDRAW; } else if ( CDDS_ITEMPREPAINT == pLVCD->nmcd.dwDrawStage ) { // This is the pre-paint stage for an item. We need to make another // request to be notified during the post-paint stage. *pResult = CDRF_NOTIFYPOSTPAINT; } else if ( CDDS_ITEMPOSTPAINT == pLVCD->nmcd.dwDrawStage ) { // If this item is selected, re-draw the icon in its normal // color (not blended with the highlight color). LVITEM rItem; int nItem = static_cast<int>( pLVCD->nmcd.dwItemSpec ); // Get the image index and state of this item. Note that we need to // check the selected state manually. The docs _say_ that the // item's state is in pLVCD->nmcd.uItemState, but during my testing // it was always equal to 0x0201, which doesn't make sense, since // the max CDIS_ constant in commctrl.h is 0x0100. ZeroMemory ( &rItem, sizeof(LVITEM) ); rItem.mask = LVIF_IMAGE | LVIF_STATE; rItem.iItem = nItem; rItem.stateMask = LVIS_SELECTED; m_list.GetItem ( &rItem ); // If this item is selected, redraw the icon with its normal colors. if ( rItem.state & LVIS_SELECTED ) { CDC* pDC = CDC::FromHandle ( pLVCD->nmcd.hdc ); CRect rcIcon; // Get the rect that holds the item's icon. m_list.GetItemRect ( nItem, &rcIcon, LVIR_ICON ); // Draw the icon. m_imglist.Draw ( pDC, rItem.iImage, rcIcon.TopLeft(), ILD_TRANSPARENT ); *pResult = CDRF_SKIPDEFAULT; } }}
重複,custom draw讓咱們能夠作儘量少的工做,上面的例子就是讓Windows幫咱們作徹底部的工做,而後咱們就從新對選擇狀態的Item的圖標作重畫,那就是咱們看到的那個圖標。執行結果以下:惟一的不足是,這樣的方法會讓你感受到一點閃爍。由於圖標被畫了兩次(雖然很快)。 用Custom Draw代替Owner Draw另一件優雅的事情就是你可使用Custom Draw來代替Owner Draw。它們之間的不一樣在我看來就是:
l 寫Custom Draw的代碼比寫Owner Draw的代碼更容易。
若是你只須要改變某行的外觀,你能夠不用管其餘的行的繪畫,讓WINDOWS去作就好了。但若是你使用
Owner Draw,你必需要對全部的行做處理。當你想對控件做全部的處理時,你能夠在處理NM_CUSTOMDRAW
消息的最後返回CDRF_SKIPDEFAULT,這有點和咱們到目前爲止所作的有些不一樣。CDRF_SKIPDEFAULT
告訴Windows由咱們來作全部的控件繪畫,你不用管任何事。 我沒有在這裏包含這個例子的代碼,由於它有點長,可是你能夠一步步地在調試器中調試代碼,你能夠看到每一 步發生了什麼。若是你把窗口擺放好,讓你能夠看到調試器和演示的程序,那在你一步步的調試中,你能夠看到 控件每一步的繪製,這裏的ListCtrl是很簡單的,只有一列而且沒有列頭。