關于ListCtrol自繪的技巧
一、給控件添加排序功能
report風格的list控件很多情況下都需要支持排序功能,而且最好支持按不同列進行排序。CListCtrl的類方法SortItems支持排序功能,但是在排序過程中,兩個數據真正的比較過程是通過SortItems的第一個參數指向的回調比較函數來完成的。這個函數通過比較SetItemData函數設置的每個Item對應數值,返回一個代表比較結果的值,SortItems便可以依據這個返回值進行排序實現。可以看出,這種方法雖然簡單,
卻有一個比較嚴重的缺陷,即不能支持按照不同列數據進行排序。
對于自繪控件來說,要想支持按不同列排序功能,需要完成以下幾件事:
1、list控件響應LNV_COLUMNCLICK消息,得到需要排序的列號,并通知HeaderCtrl在列頭上繪制正序或倒序三角標識;
2、寫一個比較函數,功能類似上面提到的回調函數,只不過這個函數將由我們自己的排序函數調用。為了支持多種類型的列數值排序,這個函數應該能對不同類型的數值進行比較;
3、寫一個list控件行交換函數,排序函數在調用比較函數后,根據返回值會調用此交換函數完成排序效果。實現此函數既可以通過調用DeleteItem和InsertItem兩個類功能函數實現,也可以通過GetItem和SetItem兩個函數交換兩行的數據信息實現;
4、寫一個排序函數,這個函數類似上面SortItems函數完成的功能。通過指定列號、排序方式(正序或倒序)以及排序開始和結束的行號,按照某種排序策略(插入排序、快速排序等),通過調用比較函數和行交換函數完成排序效果。如果list中的數據項不是很多的話,用遞歸調用實現快速排序,效果還是十分令人滿意的。
二、添加進度條
給list控件添加進度條也是常見的需求之一,可惜CListCtrl好像并沒有提供直接的方法可以解決這個問題。思路有兩個:一是創建進度條控件,將其嵌入到list控件相應的位置上,并通過響應消息來處理進度條顯示;而對于自繪控件來說,直接繪制自己的進度條可能是一個更簡單有效的方法,而且這種思路出來的效果可能給人更多驚喜。
這里介紹一種利用貼圖自繪進度條的方法,效果非常不錯。首先,需要定義兩個CBitmap類型的變量加載進度條背景和前景的位圖資源;然后計算進度條所在subitem的區域大小、進度條背景區域大小和前景區域大小;最后,在控件自繪函數DrawItem中需要繪制進度條的位置,調用DC的StretchBlt函數以拉伸方式顯示進度條背景和前景位圖即可。
三、支持多行文本
如果某個subitem的顯示文本太長的話,多行文本便顯得很重要。其實多行文本顯示很大程度上體現了自繪控件的思路綱領:在需要繪制的時刻,自己計算繪制區域的大小,自己計算顯示內容所需區域大小,自己制定合理的策略以正確合理的方式進行顯示。
支持多行文本顯示,首先計算顯示區域的大小;然后通過GetLogFont函數查詢當前設置的字體屬性,得到字體高度和寬度;之后便可以計算出顯示文本內容需要顯示幾行,每一行顯示多少個字。當然實際情況中由于有中英文的區別,不能簡單的按照字體寬度計算;可以用DC的GetTextExtent函數得到文本實際需要區域大小,然后進行相應調整。另外,當顯示區域填滿仍然不能顯示所有文本時,可以用“...”表示剩余字符。
四、關于滾動條
list控件的滾動條非常有用,在此我不想多談滾動條的自繪等內容,那是一個比較復雜的話題。其實MFC的CWnd類是可以設置WS_VSCROLL和WS_HSCROLL風格的,分別代表支持豎直滾動條和水平滾動條,CListCtrl是從CWnd繼承而來,自然也不例外。更讓我們欣喜的是,CListCtrl基類已經實現了滾動條的
功能和控制,不過這里的滾動條并不是一個ScrollBar控件,而是CListCtrl自己繪制的。
雖然不用我們自己實現滾動條功能,但是關于CListCtrl中滾動條一些屬性和特點還是要有概念,因為有的時候就要用到。比如,我們利用DrawItem函數繪制每個Item的顯示;那么就要在背景刷新函數OnEraseBkgnd中刷新剩余區域,這時就要根據item的個數和行高計算剩余區域的位置,這時我們就要考慮
滾動條的位置。通過GetScrollInfo函數可以得到滾動條信息,其中SCROLLINFO類型的信息結構體需要說明一下。定義如下:
typedef struct tagSCROLLINFO
{
UINT cbSize;
UINT fMask;
int nMin;
int nMax;
UINT nPage;
int nPos;
int nTrackPos;
} SCROLLINFO, FAR *LPSCROLLINFO;
typedef SCROLLINFO CONST FAR *LPCSCROLLINFO;
其中nMin,nMax分別表示目前滾動條設置的滾動范圍,而nPos則表示當前滾動塊在滾動條中的位置。這個位置正是相對于nMin和nMax而言的位置,比如設置的范圍為(10,20),如果滾動塊位于滾動條的中間,則其值為15。nPage表示滾動條每頁的滾動位置數,通俗一點講,其實就是點擊滾動條中滾動塊之外的位置時,滾動條就會向上或向下翻頁,翻頁時滾動的位置數即為nPage的值。由上可知,滾動一個位置對應的像素數,是可以根據滾動窗口大小和設置的滾動范圍計算出來的。那么CListCtrl中,根據實際調試可知,豎直滾動條滾動一個位置的像素數其實就是list控件的行高,也就是說,info中nMin為0,nMax為隱藏區域高度可以包含的行數。
五、防止刷新時的閃爍
這個問題幾乎是所有控件和視圖刷新都要面對的問題,解決的手段也幾乎被全部歸算到“雙緩沖”技術上。其實有的時候雙緩沖不一定能解決問題,有的時候不一定像雙緩沖那么復雜。最重要的一點,就是要搞清楚為什么會有閃爍現象,除去顯卡性能的因素,閃爍的罪魁禍首就是刷新前后的畫面差別太大。知道了這一點,很多現象都可以清楚地分析其緣由了。比如,你明明在刷新函數中完成了雙緩沖,但是卻遺憾的發現閃爍依然存在。我想很可能是你沒有制止MFC自己默認對窗口背景的刷新動作,你需要做的就是覆蓋掉基類的OnEraseBkgnd函數,自己繪制背景,并返回TRUE,來告訴Windows不用幫你繪制背景了。
所以我們防止閃爍的手段其實有很多種,我們可以在OnEraseBkgnd中自己處理背景來消除背景差異過大引起的閃爍;我們可以在OnPaint或OnDraw函數中用雙緩沖技術來減少由于繪制復雜畫面導致頻繁刷新引起的閃爍;我們可以在調用引起區域或窗口無效的函數時,設置參數防止背景重繪;我們可以在強制刷新時,盡量細化重繪區域,使重繪區域減到最小...
六、調整列寬引起的麻煩事
當你不經意調整HeaderCtrl的列寬時,對于自繪list控件,你可能突然發現不少問題:
1、你的第一列是一個縮略圖,你根本不想這一列的列寬被調整;
2、某一列列寬被你調整到0,你竟然看不到它了,再把它弄出來似乎也很費勁;或者你將某列列寬減小到一定程度時,發現兩個列的繪制竟然重合了;
3、你縮小了某一列列寬,發現最后一列向左移動了相應位置,但HeaderCtrl最右邊卻露出了不同背景顏色的區域。(這一點對于不同版本似乎情況不盡相同,Unicode有這個問題,MultiBytes版本沒有,未搞清楚原因)
對于第一個問題,需要支持某列不支持調整列寬;第二個問題,需要設置一個最小列寬;第三個問題,一個有效的解決辦法是動態調整最右邊一列的寬度,使之總是符合list控件窗口寬度。
1、支持某列列寬固定。
很簡單,只要在HeaderCtrl中重寫虛函數OnChildNotify,當消息類型為HDN_BEGINTRACKW及HDN_BEGINTRACKA,且列號為需要固定的那一列時,直接給參數pLResult賦值為TRUE,并返回TRUE即可。
這樣調整列之間的間隔條的消息就被屏蔽,因此list控件就不會收到對應消息。當然,如果想做的漂亮一點,可以把此時Cursor改變這個動作也屏蔽掉。
2、設置某列最小寬度
也很簡單,重寫list控件的虛函數OnNotify,當消息類型為HDN_ITEMCHANGINGW或HDN_ITEMCHANGINGA,列號為需要設置最小寬度的那一列,且此時列寬小于設置列寬時,直接給參數pResult賦值為TRUE,并返回TRUE即可。這里需要說明一個問題,當你的list控件設置了ImageList后,則所有的subitem最小寬度和高度為ImageList中Image的大小,因此當你在DrawItem函數中調用GetSubItemRect查詢subitem大小時,返回的結果與你看到界面上的結果是不一樣的,這一點一定要注意。這也是很引起上面提到的兩列繪制內容重合問題的原因。
3、動態設置最右一列寬度為合適大小
在同上函數的位置,處理某列寬度被調整的消息時,調整最右一列的寬度。需要注意的是,由于調用SetColumnWidth函數又會觸發這個消息,所以要判斷當前調整列是否為最右一列,否則就會不斷循環下去,使程序崩潰。
另外,調用SetColumnWidth函數時設置參數為LVSCW_AUTOSIZE_USEHEADER,并不會使寬度立即更新,而是需要設置具體的數值。猜想可能是LVSCW_AUTOSIZE_USEHEADER這個參數不會立即強迫list控件刷新,只有在list控件下次刷新時才起作用。
CListCtrl控件是MFC控件中功能最豐富的控件之一,能總結和學習的很多,其他可以研究和豐富的功能還有ToolTip、自繪滾動條、編輯subitem、拖拽、組功能、虛擬列表等
這兩種方法應該是控件自繪中最常用到的普遍方法。(當然如果只是改變控件顏色只需要處理WM_CTLCOLOR消息就可以了。)但是對于這兩者的區別,可能很多開發人員并不是很清楚。如果你做過控件自繪,可能對owner-draw已經很熟悉了。一般只要設置控件的自繪風格屬性,并實現owner-draw的消息(WM_DRAWITEM)響應虛函數(DrawItem)就可以了。可以應用這種方法的控件包括擁有自繪風格的Button、ComboBox、ListCtrl、Menu、StatusBar、HeaderCtrl、TabCtrl等大部分控件,MFC在控件需要重繪的時候調用繪制函數,并傳遞DC及控件位置、大小等信息,我們需要做的就是利用這些信息來繪制自己需求的控件外觀。但是這種方式不能用于EditCtrl,也不能用于非report風格的ListCtrl。
custom-draw方式是響應的NM_CUSTOMDRAW消息,與WM_DRAWITEM消息不同,它是被包含在WM_NOTIFY消息中被發送的,需要在類實現中加入消息映射。與owner-draw方式比較,這種繪制方式最大的優勢是對繪制的階段進行了嚴格控制,可以在不同的響應階段進行不同的繪制策略,比如既可以進行默認繪制,也可以重載函數進行特殊繪制,還可以只改變一些變量的值讓MFC自己去按照要求重繪。我們知道owner-draw方式的繪制函數中,對于所有的繪制細節都需要進行GDI或GDI+的代碼控制,而custom-draw方式中,我們可能僅僅改變幾個變量值(比如控件顏色)就完成了需求。custom-draw方式支持的控件包括ListView、ToolBar、ToolTip、TreeView等,其中對于ListCtrl支持所有的樣式。關于custom-draw的詳細信息,可以參考這篇文章http://msdn.microsoft.com/zh-cn/library/ms364048(VS.80).aspx。
二、加載縮略圖
這個其實很簡單,自己創建一個CImageList類型的對象,并自定義圖像的大小及像素類型,然后調用CListCtrl的SetImageList函數設置就可以了。需要注意的一點是,normal和small兩種type中,small類型必須設置。
三、自定義表頭
需要寫一個繼承CHeaderCtrl的子類,實現DrawItem函數,在其中進行表頭背景和字體、文本顏色等設置并進行繪制;如果要改變表頭的高度,可以映射HDM_LAYOUT消息響應函數,在其中設置控件布局。之后在自己的ListCtrl類中聲明一個自定義的HeaderCtrl類型變量,并在PreSubclassWindow函數中調用HeaderCtrl的SubclassWindow函數使其子類化,然后在初始化的時候使其各Item的format具有HDF_OWNERDRAW風格就可以了。
四、調整CListCtrl的背景、字體、文本顏色和行高
實現思路與上述表頭的方法基本相同。當然要設置list的自繪風格,并選擇自繪的方式。另外對于調整行高,如果加載了縮略圖的話,行高就會隨之調整了。另一個簡單的辦法就是設置字體的大小來實現,與縮略圖是一個道理。如果想自己精確定義行高,則比較麻煩一點。首先設置list的自繪風格,然后重載MeasureItem函數,在其中設置結構體中的item高度變量的值,再在消息映射中添加ON_WM_MEASUREITEM_REFLECT(),就可以讓list在合適的時候響應來改變行高。需要注意的兩點是:
1、MeasureItem與WM_MEASUREITEM消息響應函數OnMeasureItem是不同的;
2、觸發MeasureItem函數調用的WM_MEASUREITEM消息是在一定的情況下才被發送,比較簡單的方法是send一個WM_WINDOWPOSCHANGED消息來觸發。
3、設置LVS_OWNERDRAWFIXED風格需要在Create或者PreSubclassWindow函數中進行,否則MeasureItem不會被調用。