天天看點

虛拟清單控件

http://blog.sina.com.cn/s/blog_49c93b880100iw2k.html

虛拟清單控件

一、什麼是虛拟清單控件

虛拟清單控件是指帶有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;

    }

}

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;

    *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、虛表的另一個優點是可以根據需要産生資料。比如在某一列加上行号。