Neat Stuff to Do in List Controls Using Custom Draw

 

微軟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

 

 

發佈了19 篇原創文章 · 獲贊 16 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章