dcsimg

MFC Virtual List Control

WEBINAR:
On-Demand

Desktop-as-a-Service Designed for Any Cloud ? Nutanix Frame


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


Most Popular Programming Stories

More for Developers

RSS Feeds

Thanks for your registration, follow us on our social networks to keep up-to-date