MFC Virtual List Control

Environment: VC6 SP3, IE 4.01
This is mainly for the list control's report view look & feel - you can use older controls, it just won't be so pretty - set in CDlgTest::OnInitDialog().

You may have noticed that the standard CListCtrl gets slow once you need to display lots of items. The time for both the fill and sort start to upset your users who have nothing but a flickering scroll bar to entertain them. Furthermore, if you've already got this data in an array, copying it into a list control is very wasteful. Microsoft's documentation says this can all be solved by virtual lists, but these are quite intimidating and most of the sample code is using classic 'Petzold' SDK style. This article gives some help on how to implement a virtual list control in an MFC project, and provides a demonstration application.

This dialog box MFC application contains both a virtual list control (IDC_LIST1) and a normal list control (IDC_LIST2) with 50,000 items so you can easily compare the differences. It also demonstrates how to:
  • make the list box display in a 'grid' style (CDlgTest::OnInitDialog)
  • use C++ classes with the qsort templates (CDlgTest::SortByCol and global CompareByLabelName / CompareByLabelAddress)
  • add icons to the list (CDlgTest::GetDispInfo)
  • override the standard sort algorithm (CDlgTest::OnColClick, CDlgTest::SortByCol)
  • change the header labels on the fly (CDlgTest::OnColClick)
  • support going to an item from a keystroke (CDlgTest::OnOdfinditem)

Virtual listing is enable by setting the 'Owner Data' property in the resources:
List Control Properties dialog box.

The dialog controls are then mapped via the DDX mechanism to data members in the dialog box class:

void CDlgTest::DoDataExchange(CDataExchange* pDX)
{
 CDialog::DoDataExchange(pDX);
 //{{AFX_DATA_MAP(CDlgTest)
 DDX_Control(pDX, IDC_LIST2, m_List2);
 DDX_Control(pDX, IDC_LIST1, m_List);
 //}}AFX_DATA_MAP
}

It's essential to supply a message handler for the LVN_GETDISPINFO message, but you can also override the LVN_COLUMNCLICK if you want to do something when the user clicks on the header button (for example sort by column content), and the LVN_ODFINDITEM if you want to respond to a normal keypress (for example move to the next item starting with the keypress).

BEGIN_MESSAGE_MAP(CDlgTest, CDialog)
 //{{AFX_MSG_MAP(CDlgTest)
 ON_NOTIFY(LVN_GETDISPINFO, IDC_LIST1, GetDispInfo)
 ON_NOTIFY(LVN_COLUMNCLICK, IDC_LIST1, OnColClick)
 ON_NOTIFY(LVN_ODFINDITEM, IDC_LIST1, OnOdfinditem)
 //}}AFX_MSG_MAP
END_MESSAGE_MAP()

You then need to initialise your report lists:

BOOL CDlgTest::OnInitDialog()
{
 CDialog::OnInitDialog();

 // Set the icon for this dialog.  The framework does this automatically
 //  when the application's main window is not a dialog
 SetIcon(m_hIcon, TRUE);			// Set big icon
 SetIcon(m_hIcon, FALSE);		// Set small icon

 // Insert the columns.
 CString Header;
 int arColWidth[]={80,100};
 int iNumCols = 2;
 for(int i=0; i<iNumCols; i++)
 {
  Header.LoadString(IDS_LISTCOL+i);
  m_List.InsertColumn(i,Header,LVCFMT_LEFT,arColWidth[i]);
  m_List2.InsertColumn(i,Header,LVCFMT_LEFT,arColWidth[i]);
 }

 // Optional stuff from here:
 // The icons are added to an image list and passed on to the 
 // virtual list (I haven't added them to the normal list)

 // Configure the break icon array.
 m_ImageList.Create(16, 16, ILC_COLOR4, 3, 1);
 m_ImageList.Add(::AfxGetApp()->LoadIcon(IDI_BP_ENABLED));
 m_ImageList.Add(::AfxGetApp()->LoadIcon(IDI_BP_DISABLED));
 m_ImageList.Add(::AfxGetApp()->LoadIcon(IDI_BP_NONE));
 m_List.SetImageList(&m_ImageList, LVSIL_SMALL);
 
 // Here's the code for the grid style. Note that there's no 
 // definition for LVS_EX_LABELTIP in the MFC header files, so 
 // it's explicitely declared here - you could add it to your 
 // stdafx.h file if you intend to use it more than once.

 // Configure the look & feel.
 const int LVS_EX_LABELTIP = 0x00004000;
 m_List.SetExtendedStyle(LVS_EX_FULLROWSELECT 
                         | LVS_EX_GRIDLINES 
                         | LVS_EX_LABELTIP);

 m_List2.SetExtendedStyle(LVS_EX_FULLROWSELECT 
                          | LVS_EX_GRIDLINES 
                          | LVS_EX_LABELTIP);

 return TRUE;  // return TRUE  unless you set the focus to a control
}

You'll need to add some items to your list. Here's the essential code for doing this - there's more in CDlgTest::OnAdd().
m_arLabels is the data array containing your data - note that you must maintain and clean up this array.
m_LabelCount is the number of items added to the array, this is the link between your data and the list box control via SetItemCountEx
You have to call Invalidate, otherwise the list box control won't know that the array contents have changed.

// Fill class data from dialog.
UpdateData(TRUE);

m_arLabels.SetAtGrow(...);
m_LabelCount=...;

// Tell the list box to update itself.
m_List.SetItemCountEx(m_LabelCount);
m_List.Invalidate();

When the list box control content has been marked as invalid, it will try to refresh it. However, the trick about virtual lists, is that only the visible data items are requested through the LVS_GETDISPINFO message for each 'cell' it is trying to display. It is absolutely essential that you handle these messages, otherwise the control will remain stoically blank.

void CDlgTest::GetDispInfo(NMHDR* pNMHDR, LRESULT* pResult) 
{
 LV_DISPINFO* pDispInfo = (LV_DISPINFO*)pNMHDR;
 LV_ITEM* pItem= &(pDispInfo)->item;
 CLabelItem rLabel = m_arLabels.ElementAt(pItem->iItem);

 if (pItem->mask & LVIF_TEXT) //valid text buffer?
 {
  // then display the appropriate column
  switch(pItem->iSubItem)
  {
   case 0:
    lstrcpy(pItem->pszText, rLabel.m_strText);
   break;
  
   case 1:
    sprintf(pItem->pszText, "0x%08LX", rLabel.m_Addr);
   break;
  
   default:
    ASSERT(0);
   break;
  }
 }
.
 *pResult = 0;
}
There are two parameters - NMHDR* pNMHDR and LRESULT* pResult - the first is a pointer to a LV_DISPINFO structure which in turn contains a pointer to an LV_ITEM structure. It's this item information you need, as it contains the information about what the virtual list control is trying to display. You need the item's index, subitem index, and the mask - using this you can fetch the necessary information out of your data array. Note that if the request is for text, you need to copy the string to the virtual list control to be displayed. I'm not sure what the result value is used for - 0 works - it's probably reserved for something...

Anyway, at this point you should have some working virtual list. You can add more functionality by handling the other LVN_... messages, and the source code contains handlers for LVN_COLUMNCLICK and LVN_ODFINDITEM. The LVN_COLUMNCLICK is worth looking at because it shows how to optimize the sorting - as far as I can tell, the list control always uses string comparison to do the sort, which is slow if your array contains numerical data - have a look at the time difference between sorting by address on the virtual and normal list controls. I've used the qsort algorithm as it was easy to implement - which is why the string comparison on the Label column is slower than the normal list - but there's many more efficient ones out there. I'd be interested if anyone gets Dan Kozub's HybridList to work with a virtual list control.

Happy coding!

Downloads

Download source - 14 Kb
Download demo project - 8 Kb


Comments

  • Has anybody tried to encapsulate this in a control (class)?

    Posted by Mr. X on 03/03/2005 09:18am

    V. interesting article, thanks. The sample is CDialog-specific though; has anyone tried creating a self-contained "CVirtualListControl"-class? BTW For VS.NET 2003 you need to comment out the line: const int LVS_EX_LABELTIP = 0x00004000; ...in CDlgTest::OnInitDialog() -- it already exists as a define.

    Reply
  • Dont't use lstrcpy in OnGetDispinfo

    Posted by Orinoco on 10/14/2004 02:21pm

    If You fill the pItem->pszText by lstrcpy You get an error. This is becouse the system allocates memory for the text, and if You copy a long string ( in my case 800 char long ), the buffer overruns. Instead of writing 'lstrcpy(pItem->pszText, m_strText)' write 'pItem->pszText = m_strText.GetBuffer( 0 )'

    • Thanks

      Posted by Samsaeng on 05/01/2011 08:55pm

      i solved problom!! thank you!!!

      Reply
    • Memory Leak

      Posted by Synetech on 12/05/2008 09:46pm

      ...not to mention that the originally allocated memory becomes orphaned and is now a leak.

      Reply
    • Memory

      Posted by toterbot on 05/29/2005 04:41pm

      He had it right though. I'm not sure who frees this memory, but I believe the application does on exit? In that case you have to be careful with your example or you'll free it twice.

      Reply
    Reply
  • Thank you for this article.

    Posted by RedFury on 08/17/2004 01:19pm

    After searching through a few MFC texts and searching online. You were the only good resource I could find to explain virtual list controls. You did a good job at keeping things simple and I thank you very much.

    Reply
  • How to make checkbox response to clicking

    Posted by Legacy on 12/31/2003 12:00am

    Originally posted by: Alan Sim

    I have used the SetCallBackMask(hWnd, LVIS_SELECTED|LVIS_STATEIMAGEMASK|LVIS_OVERLAYMASK) method to have the checkboxes in the virtual list ctrl, but the checkboxes does not seem to response (change state) to mouse click. It also does not send LVN_ITEMCHANGED. Any advise will be greatly appreciated.

    Reply
  • Problem with virtual listview

    Posted by Legacy on 12/02/2003 12:00am

    Originally posted by: PoperoH

    i have a listview with LVS_OWNERDATA and on the LVN_GETDISPINFO i get the record from a cedatabase using the item number of the listview to retrieve the data from the database.
    
    

    database product:
    1 - code
    2 - description - sorted ascending
    3 - classification - sorted ascending

    by default i opened the cedatabase the sort property on the description. This part is working just perfect, but now i have a problem: i need to show on the listview just the product of a specified classification sorted by description. how to do it??

    follow the code:

    case LVN_GETDISPINFO:
    pLVdi = (NMLVDISPINFO *)lParam;

    // function that read the cedatabase and put the data into a struct called product
    GetItemProduct (pLVdi->item.iItem, &Product);

    if (pLVdi->item.mask & LVIF_IMAGE)
    pLVdi->item.iImage = 0;
    if (pLVdi->item.mask & LVIF_PARAM)
    pLVdi->item.lParam = 0;
    if (pLVdi->item.mask & LVIF_STATE)
    pLVdi->item.state = 0;
    if (pLVdi->item.mask & LVIF_TEXT) {
    switch (pLVdi->item.iSubItem) {
    case 0:
    wsprintf(auxi, TEXT("%s"), Product.Description);
    _tcscpy (pLVdi->item.pszText, auxi);
    break;
    case 1:
    wsprintf(auxi, TEXT("%s"), Product.Classification);
    _tcscpy (pLVdi->item.pszText, auxi);
    break;
    case 2:
    wsprintf(auxi, TEXT("%i"), Product.Code);
    _tcscpy (pLVdi->item.pszText, auxi);
    break;
    }

    Reply
  • Thanks! for the Scroll to Letter help

    Posted by Legacy on 05/23/2003 12:00am

    Originally posted by: Keri

    I've implemented virtual listctrls in my app, but couldn't figure out the Scroll to letter - thanks for the LVN_ODFINDITEM explanation, and Phil Hartmann's improvement was appreciated too!

    Reply
  • Thumbnails in ListView ?

    Posted by Legacy on 02/20/2003 12:00am

    Originally posted by: VikasKB

    Hi, Thanks for the article. But I am facing a different problem. I need to display morethan 10000 images in a ListView. I have created a Database for speeding up the process of rendering each time. But the whole process takes nearly a minute !! How can I speed up the whole process of putting thumbnail in Virtual ListView ?

    I am currently using
    CImageList::Replace()
    CListCtrl::InsertItem(). Any workaround to use Virtual List Ctrl?

    Thanks
    Vikas

    Reply
  • why pItem->mask & LVIF_TEXT is not true??

    Posted by Legacy on 11/11/2002 12:00am

    Originally posted by: Yue Guang Ling

    how to set the flag?
    in my program,the code of setting value cant be excuted!
    help!

    Reply
  • Better LVN_ODFINDITEM Handling

    Posted by Legacy on 10/17/2002 12:00am

    Originally posted by: Phil Hartmann

    This is how to handle it properly, like how explorer and other programs do it
    
    

    void <classname>::OnOdFindItem(NMHDR* pNMHDR, LRESULT* pResult)
    {
    NMLVFINDITEM* pFindInfo = (NMLVFINDITEM*)pNMHDR;
    LVFINDINFO fndItem = pFindInfo->lvfi;

    if(fndItem.flags & LVFI_STRING)
    {
    int nLength = strlen(fndItem.psz);

    // Search to end.
    for(int i = pFindInfo->iStart; i < <yourArray.Size()>; i++ )
    {
    if(_strnicmp(fndItem.psz, <yourArray[i]>,nLength) == 0)
    {
    *pResult = i;
    return;
    }
    }

    // Search from 0 to start.
    for( i = 0; i < pFindInfo->iStart; i++ )
    {
    if(_strnicmp(fndItem.psz, <yourArray[i]>, nLength) == 0)
    {
    *pResult = i;
    return;
    }
    }
    }

    *pResult = -1; // Default action.
    }

    Reply
  • Using owner draw/virtual lists with data from DB

    Posted by Legacy on 09/30/2002 12:00am

    Originally posted by: Peter Brightman

    I think that virtual lists are fine because you don't have
    to load all the data into the control and so waste lot's of
    memory. With owner draw mode, you'll render the data to screen just when the appropriate lines appear in the visible
    part of the control's client area. The only thing we need to do is to add all the entries and give each a handle that is the key to the data. I have done this some years ago with a list control and it worked fine. The key was a record number to a record entry in a DB.

    The thing about sorting is that you really have to remove all entries in the control and add them in the order you wish them to appear. Because the entries hold just handles/keys and not values, so the sorting is really a complete removal and new insert of all entries.

    I'd like to ask the community if there is any example or if someone can give hints in how to use virtual lists along with data retrieved from a SQL DB. Is it ok to retrieve a record for each fired event of the control by using the primary key? Usually one would want to retrieve a result set with more than one entry but like this, one would only use the event for the first line in the control and ignore the other events, because the first event would retrieve n records that would hold all other entries that have to displayed too in the control. Then the 2nd to the last event just would be the cursor into the result set. I highly appreciate any hints or links regarding this topic.

    Peter Brightman

    Reply
  • Loading, Please Wait ...

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • When it comes to desktops – physical or virtual – it's all about the applications. Cloud-hosted virtual desktops are growing fast because you get local data center-class security and 24x7 access with the complete personalization and flexibility of your own desktop. Organizations make five common mistakes when it comes to planning and implementing their application management strategy. This eBook tells you what they are and how to avoid them, and offers real-life case studies on customers who didn't …

  • Download the Information Governance Survey Benchmark Report to gain insights that can help you further establish business value in your Records and Information Management (RIM) program and across your entire organization. Discover how your peers in the industry are dealing with this evolving information lifecycle management environment and uncover key insights such as: 87% of organizations surveyed have a RIM program in place 8% measure compliance 64% cannot get employees to "let go" of information for …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds