MFC總結之CListCtrl用法及技巧
本文根據本人在專案中的應用,來談談CListCtrl的部分用法及技巧。當初學習時,查了很多資料,零零碎碎的作了些記錄,現在主要是來做個總結,方便以後查閱。主要包括以下十三點內容:基本操作、獲取選中行的行號、複選框操作、動態設定選中行的字型顏色、設定選中行的背景顏色、禁止拖動表頭、讓第一列居中顯示、設定行高與字型、虛擬列表技術、點選表頭時進行歸類、向上與向下移動、動態調整大小問題、避免閃爍問題。
分為兩篇來進行總結。本篇重點總結:基本操作、獲取選中行的行號、複選框操作、動態設定選中行的字型顏色、設定選中行的背景顏色
1、基本操作
分別從下面四點來介紹CListCtrl的基本操作:
①設定列表檢視顯示方式
Ⅰ. CListCtrl有四種樣式:LVS_ICON、LVS_SMALLICON、LVS_LIST、LSV_REPORT,可通過控制元件屬性來設定。本文所述均為LSV_REPORT屬性。
Ⅱ. 擴充套件樣式:
常用的擴充套件樣式有三種:LVS_EX_FULLROWSELECT、LVS_EX_GRIDLINES、LVS_EX_CHECKBOXES,分別對應作用選中某行時使正行高亮、設定網格線、item前生成Ckeckbox控制元件。
使用SetExtendedStyle(style)函式設定擴充套件樣式,使用GetExtendedStyle()函式獲取樣式,如:
m_listInfo.SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES);
來將其設定為LVS_REPORT風格,否則插入無效。還用另一種方法來設定風格,即在OnInitialUpate()中獲取CListCtrl控制權,然後修改風格,如下所示:
CListCtrl &theCtrl =GetListCtrl();
theCtrl.ModifyStyle(0, LVS_REPORT);
②插入操作
先插入列:
int InsertColumn
插入列時,可指明列號、列名稱、列名稱顯示樣式,列寬等資訊。對於列號為0的那一列,始終是靠左顯示,後面會有修改使其劇中顯示的方法,其他列通過設定nFormat屬性可以居中顯示。
插入行:
int InsertItem( int nItem,LPCTSTRlpszItem )
直接插入一行,nItem指明行號,lpszItem指明該行第0列的資訊。
設定資訊:
BOOL SetItemText(intnItem, int nSubItem, LPCTSTR lpszText )
設定第nItem行nSubItem列的資訊(nItem:0,1,2,3……; nSubItem:1,2,3……)
③刪除操作
有三個操作函式:
BOOL DeleteAllItems() -------刪除所有的行
BOOL DeleteItem(nItem)--------刪除某一行
BOOL DeleteColumn(nCol)-----刪除某一列
④獲取/設定屬性函式
有很多函數了,就不一一介紹了。常用的有
int GetItemCount() -------- 獲取已插入資訊的行數
BOOL SetItemState(int iLink, UINTstate,UINTstateMask ) ---------設定行狀態,如高亮顯示等
等等
2、獲取選中行的行號
獲取選中行的行號,然後對該行進行相關處理,這點在程式設計中用的非常多。
當滑鼠單擊item時,控制元件向父視窗傳送NM_CLICK訊息,其響應函式為OnNMClickXXXX(NMHDR *pNMHDR, LRESULT*pResult),在該函式下來編寫程式碼獲取滑鼠點選的行號。
有兩種方法來獲取行號:第一種是使用GetFirstSelectedItemPosition和GetNextSelectedItem配合來獲取;第二種是先獲取滑鼠位置資訊,然後呼叫HitTest函式來找出行號。示例分別如下:
第一種方法,該示例截自MSDN,可作修改後使用。
1. POSITION pos = pList->GetFirstSelectedItemPosition();
2. if (pos == NULL)
3. TRACE0("No items were selected!\n");
4. else
5. {
6. while (pos)
7. {
8. int nItem = pList->GetNextSelectedItem(pos);
9. TRACE1("Item %d was selected!\n", nItem);
10.// you could do your own processing on nItem here
11. }
12.}
第二種方法,該示例來自我的專案,可作修改後使用。
1. //獲取單擊所在的行號
2. //找出滑鼠位置
3. DWORD dwPos = GetMessagePos();
4. CPoint point( LOWORD(dwPos), HIWORD(dwPos) );
5. m_listCtrl.ScreenToClient(&point);
6.
7. //定義結構體
8. LVHITTESTINFO lvinfo;
9. lvinfo.pt = point;
10.
11.//獲取行號資訊
12.int nItem = m_listCtrl.HitTest(&lvinfo);
13.if(nItem != -1)
14. m_itemSel = lvinfo.iItem; //當前行號
對於LVHITTESTINFO 結構體,其有四個成員,在上述HitTest呼叫中,其第一個成員作為輸入,另外三個作為輸出。具體變數含義可檢視MSDN。
1. typedefstruct _LVHITTESTINFO {
2. POINT pt;
3. UINT flags;
4. int iItem;
5. int iSubItem;
6. } LVHITTESTINFO, *LPLVHITTESTINFO;
3、複選框操作
有時需要在item前面新增一個CheckBox,供使用者選擇,然後對所有選中項進行處理。
這裡涉及到兩個問題:第一個,如何新增CheckBox風格;第二個,如何判斷某一行的CheckBox狀態是否發生改變。
對於第一個問題,在基本操作裡已經有所闡述了,即通過SetExtendedStyle函式新增LVS_EX_CHECKBOXES擴充套件風格。
這裡重點探討第二個問題,首先,操作複選框狀態的有兩個函式:
BOOL GetCheck(int nItem)-------獲取複選框狀態
BOOL SetCheck( int nItem, BOOL fCheck = TRUE )-------設定複選框狀態
其次,我們要搞清楚以下四點:
①當列表的項item改變時,控制元件會向父視窗傳送LVN_ITEMCHANGED訊息,因此可以在LVN_ITEMCHANGED訊息的響應函式中對複選框的狀態進行處理(查詢或設定)。
②滑鼠點選CheckBox時,訊息的順序是 NM_CLICK —> LVN_ITEMCHANGED,即CheckBox的狀態是在 NM_CLICK訊息函式結束後才會發生變化,在NM_CLICK中使用GetCheck無效。
③滑鼠點選Item(非CheckBox區域)時,訊息的順序是 LVN_ITEMCHANGED —> NM_CLICK。
④呼叫InsertItem 函式時,也會產生LVN_ITEMCHANGED訊息。鑑於此,通常會自定義一個BOOL型變數m_bHit 來判斷是點選操作還是插入操作,該變數初始賦FALSE,當有滑鼠點選item時賦TRUE, 檢測完是否有CheckBox被點選後重新復位為FALSE。
示例如下所示:
1. void CXXXX::OnNMClickXXXX(NMHDR *pNMHDR, LRESULT *pResult)
2. {
3. //獲取單擊所在的行號
4. //找出滑鼠位置
5. DWORD dwPos = GetMessagePos();
6. CPoint point( LOWORD(dwPos), HIWORD(dwPos) );
7. m_listCtrl.ScreenToClient(&point);
8. //定義結構體
9. LVHITTESTINFO lvinfo;
10. lvinfo.pt = point;
11.//獲取行號資訊
12. int nItem = m_listCtrl.HitTest(&lvinfo);
13.if(nItem != -1)
14. m_itemSel = lvinfo.iItem; //當前行號
15.
16.//判斷是否點選在CheckBox上
17.if(lvinfo.flags == LVHT_ONITEMSTATEICON)
18. m_bHit = TRUE;
19.
20. *pResult = 0;
21.}
22.
23.void CXXXX::OnLvnItemchangedXXXX(NMHDR *pNMHDR, LRESULT *pResult)
24.{
25. LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
26.//判斷m_bHit,即是否點選了CheckBox
27.if(m_bHit)
28. {
29. m_bHit = FALSE; //復位
30.
31.if(m_listCtrl.GetCheck(m_itemSel))
32. { //CheckBox被選中
33.//do your own processing
34. }
35.else
36. { //CheckBox取消選擇
37.//do your own processing
38. }
39. }
40.
41. *pResult = 0;
42.}
4、動態設定選中行的字型顏色
有時可能需要設定某行的文字為特殊顏色,以表示某種特殊含義,比如正在下載的資訊用綠色,暫停下載的用灰色。
然後,來談談我的方法,這裡主要談對選中行的字型顏色進行動態修改,當然也是我通過上面文章和自己實踐結合得出的。
我們需要搞清楚以下幾點(可以結合下面修改某一行的字型顏色的方法來看):
①當控制元件繪製時,會發送NM_CUSTOMDRAW 訊息,該訊息的訊息響應函式為
1. void CXXXX::OnNMCustomdrawXXXX(NMHDR *pNMHDR, LRESULT *pResult)
2. {
3. LPNMLVCUSTOMDRAW pLVCD = reinterpret_cast<LPNMLVCUSTOMDRAW>(pNMHDR);
4. // TODO: Add your control notification handler code here
5. *pResult = CDRF_DODEFAULT;
6. //………………
7. }
②其中,pNMHDR為輸入引數,其指向NMLVCUSTOMDRAW結構體,該結構包含了很多資訊,包括字型顏色、背景等等,特別是第一個成員,為NMCUSTOMDRAW結構體變數,其包含了Current drawingstage(不知道怎麼編譯比較好),其可能的值如下圖(截自MSDN)所示
③ pResult為輸出引數,該引數決定了接下來向windows傳送什麼訊息(與繪製有關的),通過傳送該訊息我們可以進入下一步需要的處理階段。具體輸出哪個值取決於Current drawing stage,其可能的值如下圖(截自MSDN)所示
④有一點必須注意(英文的,我覺得看起來比翻譯過來更精確):
One thing to keep in mind is you must always check the draw stagebefore doing anything else, because your handler will receive manymessages, and the draw stage determines what action your code takes.
下面我們來看看如何修改某一行的字型顏色:
①首先,我們應該明白要修改字型顏色,應該在pre-paint 階段來完成
②因此,在訊息響應函式中,我們首先判斷是否處於pre-paint stage(即pLVCD->nmcd.dwDrawStage == CDDS_PREPAINT),然後通過修改輸出值pResult 的值來通知windows我們需要處理每個item的訊息(即設定 *pResult = CDRF_NOTIFYITEMDRAW)。
③再次進入訊息響應函式時,我們判斷是否處於Item的pre-paint stage(即pLVCD->nmcd.dwDrawStage== CDDS_ITEMPREPAINT),如果是則進行相關處理,即修改字型顏色等等。
④處理完了後重新設定 *pResult = CDRF_DODEFAULT,表示我們不再需要其他特殊的訊息了,預設執行即可。
示例如下:
1. void CXXXX::OnNMCustomdrawXXXX(NMHDR *pNMHDR, LRESULT *pResult)
2. {
3. LPNMLVCUSTOMDRAW pLVCD = reinterpret_cast<LPNMLVCUSTOMDRAW>(pNMHDR);
4. *pResult = CDRF_DODEFAULT;
5.
6. // First thing - check the draw stage. If it's the control's pre-paint stage,
7. // then tell Windows we want messages for every item.
8. if ( CDDS_PREPAINT == pLVCD->nmcd.dwDrawStage )
9. {
10. *pResult = CDRF_NOTIFYITEMDRAW;
11. }
12.elseif ( CDDS_ITEMPREPAINT == pLVCD->nmcd.dwDrawStage )
13. {
14.// This is the notification message for an item.
15.//處理,將item改變背景顏色
16.if( /*符合條件*/ )
17. pLVCD->clrText = RGB(255,0,255);
18.
19. *pResult = CDRF_DODEFAULT;
20. }
21.}
上面談的方法主要用於設定靜態字型顏色,當然,如果你的列表的資訊在不斷變化(即用SetItemText不斷修改),那麼也就實現了動態改變了,否則需要在合適的地方呼叫重繪函式:
BOOL RedrawItems( int nFirst, int nLast )
表示在nFirst和nLast之間的行需要進行重繪。
5、設定選中行的背景顏色
設定選中行的背景顏色,可以將選中行以特殊顏色顯示,容易明白當前處理的是哪一行。儘管有高亮,但是高亮是基於焦點的,如果你選中了某一行,然後焦點轉移了,這是就無法判斷你選的是哪一行了。
設定選中行的背景顏色的方法和第四節中講的修改字型顏色的方法是相似的,都是利用Custom Draw。這裡涉及到設定當前選中行為特殊顏色,同時要恢復前一次選中行的顏色,否則就亂了。因此需要記錄前一次選中行、當前選中行的行號,相信通過前面的總結,這點並不難實現。然後在當前選中行和前一次選中行之間進行重繪即可。
示例如下:
1. void CXXXX::OnNMClickXXXX(NMHDR *pNMHDR, LRESULT *pResult)
2. {
3. //…………
4.
5. //重繪item,更改背景顏色
6. int nFirst = min(m_itemSel,m_itemForeSel);
7. int nLast = max(m_itemSel,m_itemForeSel);
8. m_listCtrl.RedrawItems(nFirst, nLast); //在前一次選中的item和當前選中的Item之間進行重繪
9.
10. *pResult = 0;
11.}
12.void CXXXX::OnNMCustomdrawXXXX(NMHDR *pNMHDR, LRESULT *pResult)
13.{
14. LPNMLVCUSTOMDRAW pLVCD = reinterpret_cast<LPNMLVCUSTOMDRAW>(pNMHDR);
15. *pResult = CDRF_DODEFAULT;
16.
17.// First thing - check the draw stage. If it's the control's prepaint
18.// stage, then tell Windows we want messages for every item.
19.if ( CDDS_PREPAINT == pLVCD->nmcd.dwDrawStage )
20. {
21. *pResult = CDRF_NOTIFYITEMDRAW;
22. }
23.elseif ( CDDS_ITEMPREPAINT == pLVCD->nmcd.dwDrawStage )
24. {
25.// This is the notification message for an item.
26.//處理,將item改變背景顏色
27.if(m_itemSel == pLVCD->nmcd.dwItemSpec)
28. { //當前選中的item
29. pLVCD->clrTextBk = RGB(255,0,0);
30. }
31.elseif(m_itemForeSel == pLVCD->nmcd.dwItemSpec)
32. { //前一次選中的item,恢復為白色
33. pLVCD->clrTextBk = RGB(255,255,255);
34. }
35.
36. *pResult = CDRF_DODEFAULT;
37. }
38.}
6、禁止拖動表頭
過載OnNotify訊息響應函式,遮蔽兩個訊息通知碼:HDN_BEGINTRACKW和HDN_DIVIDERDBLCLICKW。示例如下:
1. BOOL CXXXX::OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult)
2. {
3. // TODO: Add your specialized code here and/or call the base class
4. //遮蔽兩個訊息通知碼,使得禁止拖動List表頭
5. NMHEADER* pNMHeader = (NMHEADER*)lParam;
6. if(((pNMHeader->hdr.code == HDN_BEGINTRACKW) |
7. (pNMHeader->hdr.code == HDN_DIVIDERDBLCLICKW)))
8. {
9. *pResult = TRUE;
10.return TRUE;
11. }
12.
13.return CDialog::OnNotify(wParam, lParam, pResult);
14.}
7、讓第一列居中顯示
在插入列時,我們可以通過引數nFormat來設定文字居中顯示,但是這種設定對於第一列是沒有作用的。這時我們可以考慮將我們的內容從第二列開始插入(設定為居中顯示)。先插入第一列,然後刪除第一列,這樣原先的第二列就充當了第一列。
8、設定行高和字型
設定CListCtrl的行高沒有函式介面,可以通過自繪來實現,但是比較麻煩。有一個比較簡單的方法是通過使用一個空白的影象將行撐起來,使其高度發生變化。示例如下:
1. CImageList m_image;
2. m_image.Create(1,24,ILC_COLOR32,1,0);
3. m_listInfo.SetImageList(&m_image, LVSIL_SMALL);
對於字型的設定,我們可以使用SetFont函式來實現。以修改CListView的字型為例,在OnInitialUpdate函式中插入列之前呼叫SetFontSelf函式(該函式自定義,如下示例所示)。首先建立一個字型,然後呼叫SetFont進行設定。需要注意的是,在退出時需要delete 掉建立的字型,避免記憶體洩露。
1. //設定字型和大小
2. void CMyListView::SetFontSelf(int nHeight, LPCTSTR lpszFacename)
3. {
4. //先刪除原有字型
5. if(m_font != NULL)
6. delete m_font;
7. m_font = new CFont;
8. //建立字型
9. m_font->CreateFont(
10. nHeight, // nHeight
11. 0, // nWidth
12. 0, // nEscapement
13. 0, // nOrientation
14. FW_NORMAL, // nWeight
15. FALSE, // bItalic
16. FALSE, // bUnderline
17. 0, // cStrikeOut
18. ANSI_CHARSET, // nCharSet
19. OUT_DEFAULT_PRECIS, // nOutPrecision
20. CLIP_DEFAULT_PRECIS, // nClipPrecision
21. DEFAULT_QUALITY, // nQuality
22. DEFAULT_PITCH | FF_SWISS, // nPitchAndFamily
23. lpszFacename); // lpszFacename
24.
25.//設定字型
26. CListCtrl &theCtrl = GetListCtrl(); //獲取控制權,引用變數
27. theCtrl.SetFont(m_font, TRUE);
28.}
9、虛擬列表技術
當資料量大時,使用InsertItem插入資料的過程是很漫長的。這時我們有兩個方法來解決該問題:一是使用CListCtrl的虛擬列表技術,二是採用分頁顯示的方法。對於虛擬列表技術,上述連結中的文章講的很詳細,我用過它的比較簡單的方法,後來改用了分頁方法。
使用虛擬列表技術,有三點需要搞清楚:
①使用虛擬技術時,需要將CListCtrl控制元件的Owner Data屬性設定為ture。
②給虛擬列表新增元素時,不需要使用InserItem函式,通過呼叫SetItemCount設定資料總個數,然後由系統產生不同的訊息,在相應的訊息響應函式中完成插入工作。
③虛擬列表向父視窗傳送的訊息有三種:⑴當它需要資料時,傳送LVN_GETDISPINFO訊息;⑵當用戶試圖查詢某個元素時,傳送