1. 程式人生 > >虛擬列表控制元件---載入大資料

虛擬列表控制元件---載入大資料

平常所用到的列ListView/ListCtrl控制元件,都是隻有行至幾百行資料,直至今日,在專案中遇到了上10W量級資料條,終於感覺到普通載入的艱辛,遂到網上亂找一通,發現大同小異,轉載了這篇比較詳細的,後面程式碼所用到的m_Items,為存放的列表的資料結構列表,

這篇文章雖詳盡,改日做一個DEMO,

  Demo

 

一、什麼是虛擬列表控制元件

虛擬列表控制元件是指帶有LVS_OWNERDATA風格的列表控制元件。。

二、為什麼使用虛擬列表控制元件

我們知道,通常使用列表控制元件CListCtrl,需要呼叫InsertItem把要顯示的資料插入列表中,之後我們就不必關心資料在哪裡了,這是因為控制元件自己開闢了記憶體空間來儲存這些資料。現在假設我們要顯示一個數據庫,裡面的資訊量很大,有幾十萬條記錄。通常有兩種方法解決這個問題:1是僅僅在ListCtrl中插入少量的資料,比如100個,然後通過[上一頁][下一頁]兩個按鈕進行控制,某一時刻顯示的只是從xxx到xxx+100之間的記錄。2是把所有資料全部插入到ListCtrl中,然後讓使用者通過滾動來檢視資料。無疑,很多使用者喜歡採用第二種方式,特別是對於已經排序的資料,使用者只需用鍵盤輸入某行的開頭字元,就可以快速定位到某一行。但是,如果這樣做,InsertItem插入資料的過程將是很漫長的,而且使用者會看到ListCtrl重新整理速度也很慢,而且所有資料都位於記憶體中消耗了大量的記憶體,當資料多達上萬以後幾乎是不能忍受的。

為此,mfc特別提供了虛擬列表的支援。一個虛擬列表看起來和普通的ListCtrl一樣,但是不用通過InsertItem來插入資料,它僅僅知道自己應該顯示多少資料。但是它如何知道要顯示什麼資料呢?祕密就在於當列表控制元件需要顯示某個資料的時候,它向父視窗要。假設這個列表控制元件包含100個元素,第10到20個元素(行)是可見的。當列表控制元件重畫的時候 ,它首先請求父視窗給它第10個元素的資料,父視窗收到請求以後,把資料資訊填充到列表提供的一個結構中,列表就可以用來顯示了,顯示第10個數據後,列表會繼續請求下一個資料。

在虛擬的樣式下,ListCtrl可以支援多達DWORD個數據項。(預設的listctrl控制元件最多支援int個數據項)。但是,虛擬列表的最大優點不在於此,而是它僅僅需要在記憶體中保持極少量的資料,從而加快了顯示的速度。所以,在使用列表控制元件顯示一個很大的資料庫的情況下,採用虛擬列表最好不過了。

不僅CListCtrl提供虛擬列表的功能, MFC的CListView類也有同樣的功能。

三、虛擬列表控制元件的訊息


虛擬列表總共傳送三個訊息給父視窗:當它需要資料的時候,它傳送LVN_GETDISPINFO訊息。這是最重要的訊息。當用戶試圖查詢某個元素的時候,它傳送LVN_ODFINDITEM訊息;還有一個訊息是LVN_ODCACHEHINT,用來緩衝資料,基本上很少用到這個訊息。

虛擬列表控制元件使用起來非常簡單。它總共只有三個相關的訊息,如果你直接使用CListCtrl,應該在對話方塊中響應這三個訊息。如果你使用CListCtrl派生類,可以在派生類中響應這三個訊息的反射訊息。這三個訊息分別是:

  (1)LVN_GETDISPINFO 控制元件請求某個資料
  (2)LVN_ODFINDITEM  查詢某個資料
  (3)LVN_ODCACHEHINT 緩衝某一部分資料

我們必須響應的訊息是(1),多數情況下要響應(2),極少數的情況下需要響應(3)

四、如何使用虛擬列表控制元件

1、首先要建立控制元件,建立一個虛擬列表和建立一個正常的 CListCtrl差不多。先在資源編輯器裡面新增一個list control資源。然後選中"Owner data"屬性,然後給它捆綁一個CListCtrl變數。新增列,新增imagelist等都和使用正常的listctrl一樣。

2、給虛擬列表新增元素。假設 m_list 是這個列表的控制變數。通常的情況下這樣新增資料:

m_list.InsertItem(0, _T("Hello world"));

但是對於虛擬列表,不能這麼做。只需告訴列表有多少個數據:

//總共顯示100行
m_list.SetItemCount(100);

3、處理它的通知訊息。

五、如何響應虛擬列表的訊息

1、處理 LVN_GETDISPINFO 通知訊息

當虛擬列表控制元件需要某個資料的時候,它給父視窗傳送一個 LVN_GETDISPINFO通知訊息,表示請求某個資料。因此列表的所有者視窗(或者它自己)必須處理這個訊息。例如派生類的情況 (CMyListCtrl是一個虛擬列表類物件):

//這裡處理的是反射訊息

BEGIN_MESSAGE_MAP(CMyListCtrl, CListCtrl)
   //{{AFX_MSG_MAP(CMyListCtrl)
   ON_NOTIFY_REFLECT(LVN_GETDISPINFO, OnGetdispinfo)
   //}}AFX_MSG_MAP
END_MESSAGE_MAP()

在LVN_GETDISPINFO的處理函式中,必須首先檢查列表請求的是什麼資料,可能的值包括:

(1)LVIF_TEXT   必須填充 pszText
(2)LVIF_IMAGE  必須填充 iImage
(3)LVIF_INDENT 必須填充 iIndent
(4)LVIF_PARAM  必須填充 lParam
(5)LVIF_STATE  必須填充 state

根據它的請求,填充所需的資料即可。

//================= 例子程式碼=====================================

下面的給出一個例子,填充的是列表所需的某個資料項的文字以及影象資訊:

LV_DISPINFO* pDispInfo = (LV_DISPINFO*)pNMHDR;
LV_ITEM* pItem= &(pDispInfo)->item;

int iItemIndx= pItem->iItem;

if (pItem->mask & LVIF_TEXT) //字串緩衝區有效
{
    switch(pItem->iSubItem){
        case 0: //填充資料項的名字
            lstrcpy(pItem->pszText,m_Items[iItemIndx].m_strItemText);
            break;
        case 1: //填充子項1
            lstrcpy(pItem->pszText,m_Items[iItemIndx].m_strSubItem1Text);
            break;
        case 2: //填充子項2
            lstrcpy(pItem->pszText,m_Items[iItemIndx].m_strSubItem2Text);
            break;
    }
}
/*注意,多數情況下要使用lstrcpyn ,因為最多複製字元的個數由pItem->cchTextMax給出:
        lstrcpyn(pItem->pszText, text, pItem->cchTextMax);
*/

if (pItem->mask & LVIF_IMAGE) //是否請求影象
        pItem->iImage= m_Items[iItemIndx].m_iImageIndex;

甚至連某行資料是否選中(當有checkbox的情況下)的資訊也需要由使用者自己來維護,例如:
//是否顯示該行的選擇資訊?

if(IsCheckBoxesVisible()) //自定義函式
{
    pItem->mask |= LVIF_STATE;
    pItem->stateMask = LVIS_STATEIMAGEMASK;

    if(m_database[itemid].m_checked)
    {
         pItem->state = INDEXTOSTATEIMAGEMASK(2);
    }
    else
    {
         pItem->state = INDEXTOSTATEIMAGEMASK(1);
     }
}


2、處理 LVN_ODFINDITEM 訊息

在資源管理器裡面,定位到某個資料夾,會顯示很多檔案,如果按下鍵盤的‘A’,則資源管理器會自動找到名字以 'A'打頭的資料夾或者檔案, 並選擇該檔案。繼續按 A,如果還有其它名字以'A'打頭的檔案,則下一個檔案被選中。如果輸入 "AB",則 'AB'打頭的檔案被選中。這就是列表控制元件的自動查詢功能。

當虛擬列表收到一個LVM_FINDITEM訊息,它也會發送這個訊息通知父視窗查詢目標元素。要搜尋的資訊通過 LVFINDINFO 結構給出。它是 NMLVFINDITEM 結構的一個成員。當找到要搜尋的資料後,應該把該資料的索引(行號)返回,如果沒有找到,則返回-1。

以對話方塊為例,響應函式大致如下:

//================= 例子程式碼=====================================
void CVirtualListDlg::OnOdfinditemList(NMHDR* pNMHDR, LRESULT* pResult)
{
    // pNMHDR 裡面是要查詢的元素的資訊
    // 要選中的目標元素的行號最後要儲存在 pResult 中, 這是關鍵!

    NMLVFINDITEM* pFindInfo = (NMLVFINDITEM*)pNMHDR;

    /* pFindInfo->iStart 是查詢的起始位置,一直到最後,然後從頭開始,如果沒有找到合適的,最終停留在iStart*/

    *pResult = -1;

    //是否按照文字查詢?
    if( (pFindInfo->lvfi.flags & LVFI_STRING) == 0 )
    {
        return;
    }

    //這是我們要找的字串
    CString searchstr = pFindInfo->lvfi.psz;

    int startPos = pFindInfo->iStart;//儲存起始位置

    //判斷是否最後一行
    if(startPos >= m_list.GetItemCount())
        startPos = 0;

    int currentPos=startPos;
   
    //開始查詢
    do
    {       
        if( _tcsnicmp(m_database[currentPos].m_name,
                 searchstr, searchstr.GetLength()) == 0)
        {
            //選中這個元素,停止查詢
            *pResult = currentPos;
            break;
        }

        currentPos++;

        //從頭開始
        if(currentPos >= m_list.GetItemCount())
            currentPos = 0;

    }while(currentPos != startPos);       
}

顯然,如果資料很多,必須實現一個快速查詢的方法。

關於pFindInfo->lvfi裡面的資訊的詳細說明可以參考 MSDN。

3、處理 LVN_ODCACHEHINT 訊息。

假如我們從資料庫或者其它地方讀取資料的速度比較慢,則可以利用這個訊息,批量讀取一些資料,然後根據請求,逐個提供給虛擬列表。LVN_ODCACHEHINT訊息的用途就是給程式一個緩衝資料的機會。以提高程式的效能。

//================= 例子程式碼=====================================
使用 ClassWizard 過載 OnChildNotify 函式,檢查是否 LVN_ODCACHEHINT 訊息,然後準備緩衝資料:

NMLVCACHEHINT* pcachehint=NULL;

NMHDR* phdr = (NMHDR*)lParam;

if(phdr->code == LVN_ODCACHEHINT)
{
     pcachehint= (NMLVCACHEHINT*) phdr;
     //自定義函式,準備指定範圍的資料到緩衝區
     PrepCache(pcachehint->iFrom, pcachehint->iTo);
}
else ...

注意,如果訊息不是 LVN_ODCACHEHINT,則要傳遞給基類進行處理。

五、如何修改ListCtrl顯示的資料。

由於是程式自己維護資料,所以只需修改資料庫中的資料,然後呼叫CListCtrl::RedrawItems函式進行重畫即可。

六、資料的選擇狀態和選擇框

CListCtrl可以顯示checkbox選擇框。有些情況下是很有用的。對於正常的listctrl,使用者可以用滑鼠來修改某個元素的選擇狀態,但是對於虛擬列表就不行了。必須自己處理一些訊息,然後自己儲存資料的選中狀態:

void CVirtualListDlg::ToggleCheckBox(int item)
{
    m_database[item].m_checked = !m_database[item].m_checked;
    m_list.RedrawItems(item, item);
}

處理 LVN_KEYDOWN訊息,新增對空格鍵 的響應,用於切換選擇狀態:

void CVirtualListDlg::OnKeydownList(NMHDR* pNMHDR, LRESULT* pResult)
{
    LV_KEYDOWN* pLVKeyDown = (LV_KEYDOWN*)pNMHDR;

    if( pLVKeyDown->wVKey == VK_SPACE )
    {
       int item = m_list.GetSelectionMark();
        if(item != -1)
            ToggleCheckBox(item);
    }

    *pResult = 0;
}

然後處理 NM_CLICK 訊息:

void CVirtualListDlg::OnClickList(NMHDR* pNMHDR, LRESULT* pResult)
{
    NMLISTVIEW* pNMListView = (NM_LISTVIEW*)pNMHDR;

    LVHITTESTINFO hitinfo;
    hitinfo.pt = pNMListView->ptAction;

    int item = m_list.HitTest(&hitinfo);

    if(item != -1)
    {
        //看看滑鼠是否單擊在 check box上面了?
        if( (hitinfo.flags & LVHT_ONITEMSTATEICON) != 0)
        {
            ToggleCheckBox(item);
        }
    }
   
    *pResult = 0;
}

七、備註:

    1、虛擬列表無法進行排序。

    2、虛表的一個優點是容易保持和資料庫的同步。修改資料庫中的資料,然後重畫list十分容易而且高效。

    3、虛表的另一個優點是可以根據需要產生資料。比如在某一列加上行號。

 

 

網上看到了防止CLIstCtrl 閃爍的方法,挺詳細的,就是第五種方法有點問題,做了下修改,就可以用了:

1.使用SetRedraw禁止視窗重繪,操作完成後,再恢復視窗重繪

m_ctlList.SetRedraw(FALSE);

//以下為更新資料操作

//……

//恢復視窗重繪

m_ctlList.SetRedraw(TRUE);

2.使用LockWindowUpdate禁止視窗重繪,操作完成後,用UnlockWindowUpdate恢復視窗重繪

m_ctlList.LockWindowUpdate();

//以下為更新資料操作

//……

//恢復視窗重繪

m_ctlList.UnlockWindowUpdate();

3.使用ListCtrl的內部雙緩衝

m_ctlLisit.SetExtendedStyle(m_ctlLisit.GetExtendedStyle()|LVS_EX_DOUBLEBUFFER);

VC6未定義LVS_EX_DOUBLEBUFFER巨集,使用者可以自定義,如下:

#define LVS_EX_DOUBLEBUFFER 0×00010000

4.Virtual List

首先要設定ListCtrl風格為LVS_REPORT | LVS_OWNERDATA或在ListCtrl屬裡中的More Styles頁面中選中Owner data複選框。

其次要嚮應LVN_GETDISPINFO訊息;

void OnGetdispinfoList(NMHDR* pNMHDR, LRESULT* pResult)
{
LV_DISPINFO* pDispInfo = (LV_DISPINFO*)pNMHDR;

LV_ITEM *pItem = &(pDispInfo)->item;
char szText[128] = {0};
if (pItem->mask & LVIF_TEXT)
{

//使緩衝區資料與表格子項對應

//m_ArrayBuff為二維陣列

//定義如下 int m_ArrayBuff[2048][4];
_stprintf(szText,_T(“%d”),m_ArrayBuff[pItem->iItem][pItem->iSubItem]);
pItem->pszText = szText;
}
*pResult = 0;
}

最後便是生成緩衝區資料

void Insertdata()
{

//刪除之前的資料
m_ctlList.SetItemCountEx(0);
m_ctlList.Invalidate();
m_ctlList.UpdateWindow();
srand( (unsigned)time( NULL ));

//生成新的資料緩衝區
int nItemCount = 2048;
for (int i = 0;i < nItemCount; i ++)
{
for (int k = 0;k < 4;k ++)
{
m_ArrayBuff[i][k] = rand()%2048 + 1;
}
}
if (nItemCount < 2)
m_ctlList.SetItemCountEx(1);
else
m_ctlList.SetItemCountEx(nItemCount);
m_ctlList.Invalidate();
}

若要修改資料,只要修改緩衝區m_ArrayBuff的資料即可以

5.Custom Redraw

參考適用範圍:資料不是很多,而且用SetItemText修改Item的某一列的資料的時候。

既然是自繪,首先當然是過載CListCtrl類,並接管WM_ERASEBKGND訊息,去掉預設的處理,改為不處理

BOOL CListCtrlEx::OnEraseBkgnd(CDC* pDC)
{

//響應WM_ERASEBKGND訊息
return false;
//遮蔽預設處理
//return CListCtrl::OnEraseBkgnd(pDC);
}

void CListCtrlEx::OnPaint()

{

//響應WM_PAINT訊息

CPaintDC dc(this); // device context for painting

CRect rect;

CRect headerRect;

CDC MenDC;//記憶體ID表

CBitmap MemMap;

GetClientRect(&rect);

GetDlgItem(0)->GetWindowRect(&headerRect);

MenDC.CreateCompatibleDC(&dc);

MemMap.CreateCompatibleBitmap(&dc,rect.Width(),rect.Height());

MenDC.SelectObject(&MemMap);

MenDC.FillSolidRect(&rect,RGB(255,255,255));  //這裡的RGB(255,255,255)是ListCtrl的背景色

//這一句是呼叫預設的OnPaint(),把圖形畫在記憶體DC表上

DefWindowProc(WM_PAINT,(WPARAM)MenDC.m_hDC,(LPARAM)0);

//輸出

dc.BitBlt(0,headerRect.Height(),rect.Width(), rect.Height(),&MenDC,0, headerRect.Height(),SRCCOPY);

MenDC.DeleteDC();

MemMap.DeleteObject();

}

 

--------------------- 本文來自 qdqht2009 的CSDN 部落格 ,全文地址請點選:https://blog.csdn.net/qdqht2009/article/details/42387953?utm_source=copy