微軟4.70 版本的common controls 提供了一個叫custom draw的特性。這個名字給了你一個模糊的
提示關於custom draw是幹什麼。MSDN文檔給了冗長的解釋和例子,但是它沒告訴你你想要的東西
。也就是,簡單的,custom draw的好處在哪。Custom draw 可以被看成是一種輕量級的,容易使
用的Owner draw.容易使用的原因是因爲:custom draw 只有一個消息(NM_CUSTOMDRAW)需要處理,
你可以讓Windows Os 爲你做一些工作。所以你沒必要把部分owner-drawing的所有工作全部去做一
遍。
文章會把注意力放在列表控件上的custom draw.不盡因爲我已經在工作中做了部分 custom draw
列表控件,我熟悉這個流程;而且可以用少量代碼來取得不錯的效果。Custom draw 的代碼能取代
一些The Code Porject 上的老的關於Custom Draw的列表控件文章。
這篇文章的代碼能夠在裝有 Microsoft Visual C++ 6 SP2,common control版本爲version 5.0的
Windows 98上運行良好。我也在Unicode on NT 4上運行過改代碼,但這是最少需要4.71的common
control.儘管這樣,因爲IE4(標榜着VC6),所以,這個不是問題。
Custom draw 基礎
我儘可能的概述一下這裏的custom draw 流程,不會再重申這些文檔了。這些例子,都是假設我們
已經有了一個列表控件,這個控件放在對話框上,而且是報表模式,含有很多列。
填寫custom draw消息映射項
Custom draw 和回調差不多。Windows在繪製列表控件過程的某個時間通過發送消息來通知你的程
序。你可以選擇全部忽略這些消息(這樣的話,你將看到標準的控件),可以選擇一部分進行繪製
(一些簡單的例子)或者乾脆自己把控件全部畫了。你可以只畫部分你需要的,而讓window去做其
它剩餘的。
假設你想往一個控件中加一些閃光。假設你已經有了合適的common controls dll,當Windows已經
發送 NM_CUSTOMDRAW 消息給你,你只要加上以下消息處理函數就可以利用custom draw了。處理函
數會像這樣子:
ON_NOTIFY ( NM_CUSTOMDRAW, IDC_MY_LIST, OnCustomdrawMyList )
原型會是像這樣:
afx_msg void OnCustomdrawMyList ( NMHDR* pNMHDR, LRESULT* pResult );
這個告訴MFC,你想處理由IDC_MY_LIST發送通知代碼是NM_CUSTOMDRAW的WM_NOTIFY消息。處理函數
名字爲OnCustomDrawMylist.如果你爲一個派生clistctrl增加custom draw功能,你可以用
ON_NOTIFY_REFLECT instead:
ON_NOTIFY_REFLECT ( NM_CUSTOMDRAW, OnCustomdraw )
這個消息處理函數和上面的具有相同的原型,但是用派生類代替。
Custom Draw Stages:
Custom Draw 可以分兩個繪製過程:擦除和繪製。Windows 在每部分的的開始都會發送
NM_CUSTOMDRAW 消息。所以這裏總共有4個消息。但是,根據你返回給Windows的值,你的應用程序
會收到1個或者比4個還多。Windows發送NM_CUSTOMDRAW的那個時間點叫做"draw stages".你需要很
好的理解這個概念,因爲它會貫穿整個Custom draw.所以,總結一下,就是,你需要了解以下四個
階段點:
1 列表項被繪製前階段;
2 列表項被繪製後階段;
3 列表項被擦除前階段;
4 列表項被擦除後階段;
並不是所有以上狀態都有用。在實際中,我沒有在多於兩個的階段中響應過消息。事實上,我在寫
這篇文章前做過實驗,Windows並不會在列表被擦除後和列表擦除前發送消息。所以,不要被這段
把你給嚇着了。
響應 NM_CUSTOMDRAW 消息
你從custom draw 消息處理函數返回的值是一個至關重要的信息,它會告訴Widwows你已經在繪製
過程中做了多少事,所以也間接的告訴了Windows,它應該爲你做些什麼。你可以在你的消息處理函
數中返回5種值,他們是:
1 我不想做任何事;Windows 需要做所有事情來繪製這個控件或者是控件的項。這就好像你根
本沒有參與Custom draw。
2 我改變被控件使用的字體;Windows需要重新計算被繪製項的面積。
3 我繪製整個控件或者列表項。Windows不要做任何事。
4 在列表項被繪製的過程中,我想接收另外的NM_CUSTOMDRAW消息
5 在列表項的子項(及列)被繪製時,我想接收另外的NM_CUSTOMDRAW消 息。
你發現沒,“控件或者項”在這裏經常出現.我說過,你可能會接收到4個或者更多的
NM_CUSTOMDRAW 消息。以上5項就是發生這個現象的原因。你收到的第一個NM_CUSTOMDRAW適用於怎
個控件。如果你響應以上第四個消息(在每個列表項中請求消息),隨着每一項的繪製,你將會收
到很多消息。如果你響應第5個,隨着子項的繪製,你會收到更加多的消息。
根據你想到達的效果不同,在報表模式的控件中,你可以使用任何那些響應消息。稍後,我會
展現一些例子來說明怎樣響應NM_CUSTOMDRAW.
NM_CUSTOMDRAW 消息提供的信息
NM_CUSTOMDRAW 消息給你的消息處理函數傳輸了一個 NMLVCUSTOMDRAW 結構體的指針,它包含了以
下信息:
1 控件的窗口句柄;
2 控件的ID;
3 這個控件目前的繪製進度;
4 你可以用來繪製圖像的設備上下文的句柄;
5 被繪製的控件,列表項,子項的面積(即RECT);
6 被繪製的列表子項的索引;
7 指示被繪製列表狀態的標誌位;
8 被繪製列表項的lLPARAM 參數,它是有setitemdata()函數設置的。
根據你想要的效果,以上信息的作用也會不同。但繪製階段和設備上下文是你經常要用到的,項
索引和1lparam也非常有用。
一個簡單的例子:
在陳述了一下枯燥的細節後,讓我們看幾段代碼。第一個例子非常簡單,我們會去改變列表裏的文
字的顏色。這些顏色在紅,綠,藍三種顏色上切換。這個涉及到以下四個步驟:
1 在控件的繪製前這個階段處理NM_CUSTOMDRAW消息;
2 告訴Windows我們想爲每個列表項獲得NM_CUSTOMDRAW消息;
3 處理隨後的爲每個列表項發送的NM_CUSTOMDRAW消息;
4 爲每項設置文字的顏色。
看下處理函數:
void CMyDlg::OnCustomdrawMyList ( NMHDR* pNMHDR, LRESULT* pResult )
{
NMLVCUSTOMDRAW* pLVCD = reinterpret_cast<NMLVCUSTOMDRAW*>( pNMHDR );
//默認處理除非我們把它設爲其它值;
*pResult = CDRF_DODEFAULT;
//首先,檢查繪製階段。如果是控件繪製前的階段,就告訴Windows我們希望接受到每個列表
//項的NM_CUSTOMDRAW 消息;
if ( CDDS_PREPAINT == pLVCD->nmcd.dwDrawStage )
{
*pResult = CDRF_NOTIFYITEMDRAW;
}
else if ( CDDS_ITEMPREPAINT == pLVCD->nmcd.dwDrawStage )
{
// 這是每項繪製前的階段。這裏我們可以設置每項的文字顏色。我們的返回值告訴
//Windows自己繪製每一項;
// 但Windows會用我們在這裏設置的顏色;顏色會在紅,綠,藍之間循環;
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);
//把顏色保存在NMLVCUSTOMDRAW結構體中;
pLVCD->clrText = crText;
// 告訴 Windows 自己繪製控件;
*pResult = CDRF_DODEFAULT;
}
}
上面代碼的結果如下圖所示,
看看每行Windows用的顏色,很酷吧。這隻用了十幾條語句;
我們需要記住一點就是:在我們做任何事情時必須總是先去檢查繪製階段。因爲你的處理函數會接
受到很多消息,繪製階段決定你的代碼怎麼寫。
一個複雜一點的例子
下面的例子展示怎樣自定義繪製列表項子項(也就是列)。我們的處理函數會設置每個方格的文字
和背景顏色,但不會比前一個複雜多少,只多了一個if語句。在處理項子項時涉及到的步驟有:
1 在繪製整個控件前時處理NM_CUSTOMDRAW消息;
2 告訴Window我們想獲得每個項的NM_CUSTOMDRAW消息;
3 當接受到以上消息時,在繪製項子項前告訴Windows我們想獲得每個項子項的NM_CUSTOMDRAW消
息;
4 當每個項子項的NM_CUSTOMDRAW消息到來時,設置文字和背景的顏色。
發現我們會獲得每一項的NM_CUSTOMDRAW消息和每個項子項的NM_CUSTOMDRAW。以下是代碼.
void CMyDlg::OnCustomdrawMyList ( NMHDR* pNMHDR, LRESULT* pResult )
{
NMLVCUSTOMDRAW* pLVCD = reinterpret_cast<NMLVCUSTOMDRAW*>( pNMHDR );
//默認處理除非我們把它設爲其它值;
*pResult = CDRF_DODEFAULT;
//首先,檢查繪製階段。如果是控件繪製前階段,告訴Windows我們想獲得每項的消息;
if ( CDDS_PREPAINT == pLVCD->nmcd.dwDrawStage )
{
*pResult = CDRF_NOTIFYITEMDRAW;
}
else if ( CDDS_ITEMPREPAINT == pLVCD->nmcd.dwDrawStage )
{
//這是列表項的子項通知信息。我們會在每個項子項的繪製前階段請求通知消息
*pResult = CDRF_NOTIFYSUBITEMDRAW;
}
else if ( (CDDS_ITEMPREPAINT | CDDS_SUBITEM) == pLVCD->nmcd.dwDrawStage )
{
//這是項子項的繪製前的階段。在這裏我們設置文字和背景的顏色。我們的返回值告訴
//Windows自己繪製項子項,但它會用我們在這裏設置的新顏色。文字的顏色會在紅,綠
//,藍循環,第一列的顏色爲藍,第二列顏色爲紅,第三列爲黑。
COLORREF crText, crBkgnd;
if ( 0 == pLVCD->iSubItem )
{
crText = RGB(255,0,0);
crBkgnd = RGB(128,128,255);
}
else if ( 1 == pLVCD->iSubItem )
{
crText = RGB(0,255,0);
crBkgnd = RGB(255,0,0);
}
else
{
crText = RGB(128,128,255);
crBkgnd = RGB(0,0,0);
}
//把顏色保存在NMLVCUSTOMDRAW結構體中;
pLVCD->clrText = crText;
pLVCD->clrTextBk = crBkgnd;
// 告訴 Windows 自己繪製控件;
*pResult = CDRF_DODEFAULT;
}
}
以上代碼的結果如圖所示:
需要注意的一些地方:
1 背景顏色只繪製在一列中。右邊的列和下方的行的背景色依然是控件的 背景色。
2 當我回看這篇文章時,看到標題“NM_CUSTOMDRAW(列表視圖)”,這篇 文章說你能夠在
第一個自定義繪製消息時返回標誌 DRF_NOTIFYSUBITEMDRAW,而沒有處理CDDS_ITEMPREPAINT繪
制階段。我 測試過,但是,這個不行。你必須要處理CDDS_ITEMPREPAINT這個繪製 階段
。
處理繪製後階段:
到次爲止,例子都是處理繪製前階段,來改變列表項的展現情況。但是,在繪製前階段,你的能力
只能限制在改變一下文字或者背景的顏色上。如果你想改變一下圖標是怎樣繪製的,你可以在繪製
前階段繪製整個控件(矯枉過正),或者在繪製後階段繪製。如果你在繪製後階段做自定義繪製,
自定義繪製函數會在windows繪製好整個項或子項時被調用,你可以做任何額外的繪製。
在這個例子中,我會創建一個列表項被選擇後,但圖標顏色不會改變的控件。涉及到的步驟爲:
1 對整個控件在繪製前階段處理NM_CUSTOMDRAW消息;
2 告訴Windwos我們想爲每個列表項獲取NM_CUSTOMDRAW消息;
3 當列表項消息到來時,告訴windows我們想在繪製後階段爲每個列表項獲取NM_CUSTOMDRAW消
息。
4 當每個列表項的NM_CUSTOMDRAW消息來時,在必要時重繪圖標。
代碼如下:
void CMyDlg::OnCustomdrawMyList ( NMHDR* pNMHDR, LRESULT* pResult )
{
NMLVCUSTOMDRAW* pLVCD = reinterpret_cast<NMLVCUSTOMDRAW*>( pNMHDR );
*pResult = 0;
//如果是繪製控件週期的開始時,爲每個列表項請求NM_CUSTOMDRAW消息;
if ( CDDS_PREPAINT == pLVCD->nmcd.dwDrawStage )
{
*pResult = CDRF_NOTIFYITEMDRAW;
}
else if ( CDDS_ITEMPREPAINT == pLVCD->nmcd.dwDrawStage )
{
//這是在列表項繪製前階段。我們需要請求一個在繪製後的NM_CUSTOMDRAW消息;
*pResult = CDRF_NOTIFYPOSTPAINT;
}
else if ( CDDS_ITEMPOSTPAINT == pLVCD->nmcd.dwDrawStage )
{
// 如果列表項被選擇,用正常的顏色重繪圖標(效果是不會高亮起來)
LVITEM rItem;
int nItem = static_cast<int>( pLVCD->nmcd.dwItemSpec );
//獲取圖像的索引和項的狀態。我們需要人工檢查列表項是否被選擇。msdn文檔說
//列表項的狀態在 pLVCD->nmcd.uItemState上,但在我的測試中,他一直等於 0x0201,
//沒有任何意義,因爲在commctrl.h中最大的 CDIS_*常量爲 0x0100.
ZeroMemory ( &rItem, sizeof(LVITEM) );
rItem.mask = LVIF_IMAGE | LVIF_STATE;
rItem.iItem = nItem;
rItem.stateMask = LVIS_SELECTED;
m_list.GetItem ( &rItem );
//如果這個列表項被選擇,用正常的顏色重繪圖標。
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;
}
}
}
再次,自定義繪製能夠讓我們儘可能的做少點的事情,以上例子是讓Windows做一切繪製工作,然
後我們重繪被選擇項的圖標。這樣的效果
就是用戶看到的是我們重繪的圖標。效果見下圖,被選擇項的圖標和未被選擇的圖標一致。
用自定義繪製代替自己繪製
另一個比較好的事就是你可以用custom draw 代替owner draw.這兩者的區別在於實現相同的效果
,custom draw 編寫的代碼更加少,也更加容易理解。還有一個優點就是如果你使用custom draw
繪製,你可以選擇只繪製其中幾行,其它行就交給windows來繪製。如果你使用owner draw,所有事
情都是你自己做,儘管你不想在某些行中實現特殊效果。你在每項繪製前階段處理NM_CUSTOMDRAW
消息,做好所有的繪製工作,然後返回 CDRF_SKIPDEFAULT標誌。這個和我們前面做的都不相同。
CDRF_SKIPDEFAULT告訴Windows不要再做任何繪製了,因爲我們已經做好了繪製工作。
我不會把這些代碼貼出來,因爲有點長。但是你可以一步一步的試一下,看下結果如何。如果你把
代碼下下來,你會看到演示程序的對話框,並同時看到代碼。你將會一步一步的看到繪製過程。列
表控件很簡單,只有一列,但沒有頭部,如圖:
其它一些你能做的事(也許)
用點想象力,你能用custom draw繪製一些其它的效果來。在最近的項目中,我繪製過這種控件,如
下圖:
我不會把這個產品的名字說出來,以免有人說我在做廣告,但是你能指出來,呵呵。注意到當文本
只有一行的數量時,它和正常的list control相同,但當文本多了,它變會分行。這樣,所有的文
本都能看到,用戶不需要向前或者向後拉動來看清所有的文字。我在控件繪製前階段繪製所有東西
從而來實現該效果。
爲什麼我把標題說成是也許呢。正如我前面提到的一樣,MSDN指出能在擦除前階段和擦除後階段繪
制。我從來沒有在這些階段繪製過圖形。所以,爲了寫這篇文章,我想做個例子在列表項被擦除後
繪製圖案。但是,我不能在列表項被擦除前或者擦除後獲得NM_CUSTOMDRAW消息。在我的處理函數
中,我試了很多,也實驗了不少,最終,我放棄了。對於這點,我很懷疑文檔的正確性,因爲在標
題爲“NM_CUSTOMDRAW(list view)”的那頁中,它列出了CDRF_NOTIFYITEMERASE的值,但這個值並
沒有在 commctrl.h頭文件中出現。當這麼一個重要的信息都錯了,我開始懷疑周圍的一些文檔的準確性。
在任何事件中,如果你在擦除前階段或者擦除後階段做了相關處理,你必須在繪製前階段做相應處
理。否則,Windwos默認的繪製行爲會被你在擦除階段做的相應處理給消滅掉。基於上述幾點,我
想不出你在擦除階段處理消息的任何理由。任何特殊的顯示效果都在繪製前階段輕鬆的完成。
演示工程
演示程序了包含了我上面提到的四個列表控件。程序裏包含了這個控件的完整的代碼和custom
draw 繪製處理函數。這些代碼能夠幫助你更好的感受一下custom draw.這些代碼也可以用到你自
己的程序中。
原文見:http://www.codeproject.com/KB/list/lvcustomdraw.aspx