復制於:http://www.cnblogs.com/lidabo/archive/2012/08/23/2652796.html
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);
Ⅲ. 使用CListView時,需要在PreCreateWindow()函數中添加 cs.style | = LVS_REPORT;
來將其設置為LVS_REPORT風格,否則插入無效。還用另一種方法來設置風格,即在OnInitialUpate()中獲取CListCtrl控制權,然后修改風格,如下所示:
CListCtrl &theCtrl =GetListCtrl();
theCtrl.ModifyStyle(0, LVS_REPORT);
②插入操作
先插入列:
int InsertColumn( int nCol, LPCTSTR lpszColumnHeading, int nFormat, int nWidth, int nSubItem)
插入列時,可指明列號、列名稱、列名稱顯示樣式,列寬等信息。對於列號為0的那一列,始終是靠左顯示,后面會有修改使其劇中顯示的方法,其他列通過設置nFormat屬性可以居中顯示。
插入行:
int InsertItem( int nItem, LPCTSTRlpszItem )
直接插入一行,nItem指明行號,lpszItem指明該行第0列的信息。
設置信息:
BOOL SetItemText(int nItem, 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,可作修改后使用。
POSITION pos = pList->GetFirstSelectedItemPosition(); if (pos == NULL) TRACE0("No items were selected!\n"); else { while (pos) { int nItem = pList->GetNextSelectedItem(pos); TRACE1("Item %d was selected!\n", nItem); // you could do your own processing on nItem here } }
第二種方法,該示例來自我的項目,可作修改后使用。
//獲取單擊所在的行號 //找出鼠標位置 DWORD dwPos = GetMessagePos(); CPoint point( LOWORD(dwPos), HIWORD(dwPos) ); m_listCtrl.ScreenToClient(&point); //定義結構體 LVHITTESTINFO lvinfo; lvinfo.pt = point; //獲取行號信息 int nItem = m_listCtrl.HitTest(&lvinfo); if(nItem != -1) m_itemSel = lvinfo.iItem; //當前行號
對於LVHITTESTINFO 結構體,其有四個成員,在上述HitTest調用中,其第一個成員作為輸入,另外三個作為輸出。具體變量含義可查看MSDN。
typedef struct _LVHITTESTINFO { POINT pt; UINT flags; int iItem; int iSubItem; } 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。
示例如下所示:
void CXXXX::OnNMClickXXXX(NMHDR *pNMHDR, LRESULT *pResult) { //獲取單擊所在的行號 //找出鼠標位置 DWORD dwPos = GetMessagePos(); CPoint point( LOWORD(dwPos), HIWORD(dwPos) ); m_listCtrl.ScreenToClient(&point); //定義結構體 LVHITTESTINFO lvinfo; lvinfo.pt = point; //獲取行號信息 int nItem = m_listCtrl.HitTest(&lvinfo); if(nItem != -1) m_itemSel = lvinfo.iItem; //當前行號 //判斷是否點擊在CheckBox上 if(lvinfo.flags == LVHT_ONITEMSTATEICON) m_bHit = TRUE; *pResult = 0; } void CXXXX::OnLvnItemchangedXXXX(NMHDR *pNMHDR, LRESULT *pResult) { LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR); //判斷m_bHit,即是否點擊了CheckBox if(m_bHit) { m_bHit = FALSE; //復位 if(m_listCtrl.GetCheck(m_itemSel)) { //CheckBox被選中 //do your own processing } else { //CheckBox取消選擇 //do your own processing } } *pResult = 0; }
4、動態設置選中行的字體顏色
有時可能需要設置某行的文字為特殊顏色,以表示某種特殊含義,比如正在下載的信息用綠色,暫停下載的用灰色。
首先,給出一個CodeProject的鏈接,這篇文章講的非常好,主要是利用Custom Draw。http://www.codeproject.com/Articles/79/Neat-Stuff-to-Do-in-List-Controls-Using-Custom-Dra
然后,來談談我的方法,這里主要談對選中行的字體顏色進行動態修改,當然也是我通過上面文章和自己實踐結合得出的。
我們需要搞清楚以下幾點(可以結合下面修改某一行的字體顏色的方法來看):
① 當控件繪制時,會發送NM_CUSTOMDRAW 消息,該消息的消息響應函數為
void CXXXX::OnNMCustomdrawXXXX(NMHDR *pNMHDR, LRESULT *pResult) { LPNMLVCUSTOMDRAW pLVCD = reinterpret_cast<LPNMLVCUSTOMDRAW>(pNMHDR); // TODO: Add your control notification handler code here *pResult = CDRF_DODEFAULT; //……………… }
②其中,pNMHDR為輸入參數,其指向NMLVCUSTOMDRAW結構體,該結構包含了很多信息,包括字體顏色、背景等等,特別是第一個成員,為NMCUSTOMDRAW結構體變量,其包含了Current drawing stage(不知道怎么編譯比較好),其可能的值如下圖(截自MSDN)所示

③ pResult為輸出參數,該參數決定了接下來向windows發送什么消息(與繪制有關的),通過發送該消息我們可以進入下一步需要的處理階段。具體輸出哪個值取決於Current drawing stage,其可能的值如下圖(截自MSDN)所示

④ 有一點必須注意(英文的,我覺得看起來比翻譯過來更精確):
One thing to keep in mind is you must always check the draw stage before doing anything else, because your handler will receive many messages, 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,表示我們不再需要其他特殊的消息了,默認執行即可。
示例如下:
void CXXXX::OnNMCustomdrawXXXX(NMHDR *pNMHDR, LRESULT *pResult) { LPNMLVCUSTOMDRAW pLVCD = reinterpret_cast<LPNMLVCUSTOMDRAW>(pNMHDR); *pResult = CDRF_DODEFAULT; // First thing - check the draw stage. If it's the control's pre-paint 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 notification message for an item. //處理,將item改變背景顏色 if( /*符合條件*/ ) pLVCD->clrText = RGB(255,0,255); *pResult = CDRF_DODEFAULT; } }
上面談的方法主要用於設置靜態字體顏色,當然,如果你的列表的信息在不斷變化(即用SetItemText不斷修改),那么也就實現了動態改變了,否則需要在合適的地方調用重繪函數:
BOOL RedrawItems( int nFirst, int nLast )
表示在nFirst和nLast之間的行需要進行重繪。
5、設置選中行的背景顏色
設置選中行的背景顏色,可以將選中行以特殊顏色顯示,容易明白當前處理的是哪一行。盡管有高亮,但是高亮是基於焦點的,如果你選中了某一行,然后焦點轉移了,這是就無法判斷你選的是哪一行了。
設置選中行的背景顏色的方法和第四節中講的修改字體顏色的方法是相似的,都是利用Custom Draw。這里涉及到設置當前選中行為特殊顏色,同時要恢復前一次選中行的顏色,否則就亂了。因此需要記錄前一次選中行、當前選中行的行號,相信通過前面的總結,這點並不難實現。然后在當前選中行和前一次選中行之間進行重繪即可。
示例如下:
void CXXXX::OnNMClickXXXX(NMHDR *pNMHDR, LRESULT *pResult) { //………… //重繪item,更改背景顏色 int nFirst = min(m_itemSel,m_itemForeSel); int nLast = max(m_itemSel,m_itemForeSel); m_listCtrl.RedrawItems(nFirst, nLast); //在前一次選中的item和當前選中的Item之間進行重繪 *pResult = 0; } void CXXXX::OnNMCustomdrawXXXX(NMHDR *pNMHDR, LRESULT *pResult) { LPNMLVCUSTOMDRAW pLVCD = reinterpret_cast<LPNMLVCUSTOMDRAW>(pNMHDR); *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; } else if ( CDDS_ITEMPREPAINT == pLVCD->nmcd.dwDrawStage ) { // This is the notification message for an item. //處理,將item改變背景顏色 if(m_itemSel == pLVCD->nmcd.dwItemSpec) { //當前選中的item pLVCD->clrTextBk = RGB(255,0,0); } else if(m_itemForeSel == pLVCD->nmcd.dwItemSpec) { //前一次選中的item,恢復為白色 pLVCD->clrTextBk = RGB(255,255,255); } *pResult = CDRF_DODEFAULT; } }
6、禁止拖動表頭
重載OnNotify消息響應函數,屏蔽兩個消息通知碼:HDN_BEGINTRACKW 和HDN_DIVIDERDBLCLICKW。示例如下:
BOOL CXXXX::OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult) { // TODO: Add your specialized code here and/or call the base class //屏蔽兩個消息通知碼,使得禁止拖動List表頭 NMHEADER* pNMHeader = (NMHEADER*)lParam; if(((pNMHeader->hdr.code == HDN_BEGINTRACKW) | (pNMHeader->hdr.code == HDN_DIVIDERDBLCLICKW))) { *pResult = TRUE; return TRUE; } return CDialog::OnNotify(wParam, lParam, pResult); }
7、讓第一列居中顯示
在插入列時,我們可以通過參數nFormat來設置文本居中顯示,但是這種設置對於第一列是沒有作用的。這時我們可以考慮將我們的內容從第二列開始插入(設置為居中顯示)。先插入第一列,然后刪除第一列,這樣原先的第二列就充當了第一列。
8、設置行高和字體
設置CListCtrl的行高沒有函數接口,可以通過自繪來實現,但是比較麻煩。有一個比較簡單的方法是通過使用一個空白的圖像將行撐起來,使其高度發生變化。示例如下:
CImageList m_image; m_image.Create(1,24,ILC_COLOR32,1,0); m_listInfo.SetImageList(&m_image, LVSIL_SMALL);
對於字體的設置,我們可以使用SetFont函數來實現。以修改CListView的字體為例,在OnInitialUpdate函數中插入列之前調用SetFontSelf函數(該函數自定義,如下示例所示)。首先創建一個字體,然后調用SetFont進行設置。需要注意的是,在退出時需要delete 掉創建的字體,避免內存泄露。
//設置字體和大小 void CMyListView::SetFontSelf(int nHeight, LPCTSTR lpszFacename) { //先刪除原有字體 if(m_font != NULL) delete m_font; m_font = new CFont; //創建字體 m_font->CreateFont( nHeight, // nHeight 0, // nWidth 0, // nEscapement 0, // nOrientation FW_NORMAL, // nWeight FALSE, // bItalic FALSE, // bUnderline 0, // cStrikeOut ANSI_CHARSET, // nCharSet OUT_DEFAULT_PRECIS, // nOutPrecision CLIP_DEFAULT_PRECIS, // nClipPrecision DEFAULT_QUALITY, // nQuality DEFAULT_PITCH | FF_SWISS, // nPitchAndFamily lpszFacename); // lpszFacename //設置字體 CListCtrl &theCtrl = GetListCtrl(); //獲取控制權,引用變量 theCtrl.SetFont(m_font, TRUE); }
9、虛擬列表技術
給一個鏈接,介紹的比較詳細:http://hi.baidu.com/qi_xian/blog/item/929b04ce27d02c0592457ef8.html
當數據量大時,使用InsertItem插入數據的過程是很漫長的。這時我們有兩個方法來解決該問題:一是使用CListCtrl的虛擬列表技術,二是采用分頁顯示的方法。對於虛擬列表技術,上述鏈接中的文章講的很詳細,我用過它的比較簡單的方法,后來改用了分頁方法。
使用虛擬列表技術,有三點需要搞清楚:
① 使用虛擬技術時,需要將CListCtrl控件的Owner Data屬性設置為ture。
② 給虛擬列表添加元素時,不需要使用InserItem函數,通過調用SetItemCount設置數據總個數,然后由系統產生不同的消息,在相應的消息響應函數中完成插入工作。
③ 虛擬列表向父窗口發送的消息有三種: ⑴ 當它需要數據時,發送LVN_GETDISPINFO消息; ⑵ 當用戶試圖查找某個元素時,發送LVN_ODFINDITEM消息; ⑶當需要緩沖數據時,發送 LVN_ODCACHEHINT消息。
當我們使用LVN_GETDISPINFO 的消息處理函數來插入元素時,必須首先檢查列表請求的是什么數據(如LVIF_TEXT、LVIF_IMAGE等),然后插入不同的子項。示例如下:
void CDataAnalysis::OnLvnGetdispinfoAnalysisList(NMHDR *pNMHDR, LRESULT *pResult) { NMLVDISPINFO *pDispInfo = reinterpret_cast<NMLVDISPINFO*>(pNMHDR); // TODO: Add your control notification handler code here LV_ITEM* pItem= &(pDispInfo)->item; int iItemIndex= pItem->iItem; size_t converted = 0; wchar_t wStr[30]; //Unicode字符串 if (pItem->mask & LVIF_TEXT) //字符串緩沖區有效 { switch(pItem->iSubItem) { case 0: //填充數據項的名字,xxxxx表示要填充的字符 mbstowcs_s(&converted, wStr, 30, xxxxxx, _TRUNCATE); lstrcpy(pItem->pszText,wStr); break; case 1: //填充子項1 mbstowcs_s(&converted, wStr, 30, xxxxxx, _TRUNCATE); lstrcpy(pItem->pszText,wStr); break; case 2: //填充子項2 mbstowcs_s(&converted, wStr, 30, xxxxxx, _TRUNCATE); lstrcpy(pItem->pszText,wStr); break; case 3: //填充子項3 lstrcpy(pItem->pszText,xxxxxx); break; } } *pResult = 0; }
10、點擊表頭時進行歸類排序
系統通過發送LVM_SORTITEMS消息來處理歸類問題,在該消息的處理函數中需要調用一個回調函數,這個回調函數需要我們來設計,以完成不同的歸類方法。回調函數原型如下:
int CALLBACK CompareFunc(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort)
針對上述回調函數,有以下幾點需要搞清楚:
① 對於參數lparam1和lparam2,分別為CListCtrl的兩行數據,是用於比較的對象。通過CListCtrl的成員函數SetItemData來設置,該函數原型:
int SetItemData(int nIndex, DWORD_PTR dwItemData )
其第一個參數為行號,第二個參數指明了該行對應的參數。參數dwItemData 通常設為一行參數的數組,如: pData[2][2] = {{1, 3},{2, 3}}; 每次使用pData[i]作為dwItemData。
② 對於參數lParamSort,用於指明列項,即第幾列。該參數和回調函數一同通過CListCtrl的成員函數SortItems來設置,其函數原型為:
BOOL SortItems( PFNLVCOMPARE pfnCompare,DWORD_PTR dwData )
參數 pfnCompare 為回調函數入口地址, 參數dwData 為列項。
③ SetItemData在初始插入數據時進行調用來設置,SortItems則在點擊列表頭時響應的消息處理函數中進行設置。
示例如下:
//初始化列表視圖控件 BOOL CDataAnalysis::InitListCtl() { //其他處理,包括設置風格,插入列等等 //插入行 for(int i=0; i<LineNum; i++) { //要將char*轉換為wchar_t* mbstowcs_s(&converted, wStr, 30, m_analysis[i].Date, _TRUNCATE); m_listAnalysis.InsertItem(i, wStr); //日期 mbstowcs_s(&converted, wStr, 30, m_analysis[i].Time, _TRUNCATE); m_listAnalysis.SetItemText(i, 1, wStr); //時間 mbstowcs_s(&converted, wStr, 30, m_analysis[i].ID, _TRUNCATE); m_listAnalysis.SetItemText(i, 2, wStr); //ID m_listAnalysis.SetItemText(i, 3, m_analysis[i].lpszEvent); //事件 //設置回調函數的參數 m_listAnalysis.SetItemData(i, (LPARAM)(m_analysis+i)); } return TRUE; } void CDataAnalysis::OnHdnItemclickAnalysisList(NMHDR *pNMHDR, LRESULT *pResult) { LPNMHEADER phdr = reinterpret_cast<LPNMHEADER>(pNMHDR); // TODO: Add your control notification handler code here //設置回調函數的參數和入口地址 m_listAnalysis.SortItems(SortFunc, phdr->iItem); *pResult = 0; } //排序的回調函數 int CALLBACK SortFunc(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort) { int result; //返回值 //兩行的參數,用於比較 ANALYSISFORMAT* pAnalysis1 = (ANALYSISFORMAT*)lParam1; ANALYSISFORMAT* pAnalysis2 = (ANALYSISFORMAT*)lParam2; //排序 switch(lParamSort) { case 0: //日期 result = strcmp(pAnalysis1->Date, pAnalysis2->Date); break; case 1: //時間 result = strcmp(pAnalysis1->Time, pAnalysis2->Time); break; case 2: //ID result = strcmp(pAnalysis1->ID, pAnalysis2->ID); break; case 3: //事件 result = wcscmp(pAnalysis1->lpszEvent, pAnalysis2->lpszEvent); break; default: break; } return result; }
11、向上與向下移動
有時需要向上或向下移動表項內容,這里給出向上移動的方法,向下移動的方法類似。
① 利用第2節所述的內容獲取行號nItem,判斷行號是否為行首,如果不是行首則進入②;
② 獲取第nItem行的所有子項內容;
③ 刪除第nItem行,並在nItem-1的位置重新插入原先的第nItem行的內容;
④ 使nItem-1的位置高亮顯示
示例如下:
/*************************上移子項**************************/ void CStudyTestDlg::OnPageup() { if (nItem == 0) { MessageBox("該子項已經位於第一行!"); return; } // 提取內容 CString temp[4]; int i; for(i=0;i<4;i++) temp[i] = m_ListCtrl.GetItemText(nItem, i); // 刪除 m_ListCtrl.DeleteItem(nItem); // 在nItem-1位置處插入 for (i=0; i<4; i++) m_ListCtrl.SetItemText(nItem-1,i,temp[i]); //高亮顯示 UINT flag = LVIS_SELECTED|LVIS_FOCUSED; m_ListCtrl.SetItemState(--nItem, flag, flag); } /*************************下移子項**************************/ void CStudyTestDlg::OnPagedown() { if (nItem == m_ListCtrl.GetItemCount()-1) { MessageBox("該子項已經位於最后一行!"); return; } // 提取內容 CString temp[4]; int i; for (i=0; i<4; i++) temp[i] = m_ListCtrl.GetItemText(nItem, i); // 刪除 m_ListCtrl.DeleteItem(nItem); // 在nItem+1位置處插入 for (i=0; i<4; i++) m_ListCtrl.SetItemText(nItem+1, i,temp[i]); //高亮顯示 UINT flag = LVIS_SELECTED|LVIS_FOCUSED; m_ListCtrl.SetItemState(++nItem, flag, flag); }
12、避免閃爍問題
這個問題在我的前面一篇博文有提到。
http://blog.csdn.net/zwgdft/article/details/7394318
13、動態調整大小
有時由於不確定軟件運行時的電腦屏幕大小,需要根據屏幕大小動態設置CListCtrl控件的大小。動態大小的設置時,需要注意不要將高度和寬度設置的超過區域限制,否則就沒有滾動條了,導致部分內容無法查看。以我遇到的一個例子來說,其情況見第12節提到的那篇博文所述:將View划分為三個窗格,在左上角View上有個CPropertySheet,其上有幾個CPropertyPage,每個屬性頁上有個CListCtrl,供用戶查看信息。那么這時需要設置的CListCtrl的大小即為:
寬度 = 左上角View寬度
高度 = 左上角View高度 - 屬性頁的Tab項高度
調用MoveWindow函數進行設置即可。
