Good programmers always try to speed up their applications as much as they can, whether it's a simple loop to sort an array of integers or something more complex, like a paint program. Nowadays, more people have started coding, including those who aren't geniuses or wholly dedicated to the task. This means that sometimes efficiency takes a back seat to ease of use. Luckily, there are a few areas in which we can achieve increased efficiency. The list control is one of these.
Contributed by Gabor Bernat Rating: / 4 March 03, 2009
The object of this article is to introduce you to the concept of the virtual list and you can benefit from it during your daily coding experience. For this, I will create a class list that will contain only a column of characters near a column full of numbers. This will help us to concentrate on the lesson rather than on the task of handling and creating the list control. This will also let me introduce you to all of the advantages of the virtual list.
Before we venture further, let me tell you that this is the second part of my three-part article series related to the secrets of the controls inside MFC. The first chapter appeared here on the Asp Free site under the name of Controls Inside MFC. If you do not have a good understanding of the list controls and their purpose, or how you can customize them, I strongly advise you to read through that previous part.
However, if you already have this knowledge, keep reading. If you've already used the list control in an application where you should have added more than just a couple of items, you've already observed that this could take a long time.
The reason for this can be chalked up to the fact that not only must all of the new text be inserted one by one inside the control's container, but the control also must sort the list, if that is required. When we start to talk about more than 500 items, this can be really time consuming, and you will leave the user in the front of a frozen application with the busy/working pointer turned on.
Furthermore, often you have already stored the data within an internal container of your application. Making sure that this is working in cooperation with the list control is not only time consuming, but you are also saving all the information twice, thus wasting precious memory.
The perfect way out is the virtual list. It is nothing more than a common list control for which you specify that you do not want to have an internal data member. In accordance with this, the list control will retain all of its control properties. All you need to do is take care of the part where the internal data management is in action.
This can be restricted to a few parts: adding new members, removing members, modifying members, sorting the members after a specific rule, key “snap” after a key push, or any other task you can imagine. As the best way to learn about something new is with a good example, I will create a class and demonstrate all of this within it. The name of this class is gbReportList, and I derived it from the CListControl.
If we do not have internal data management, it is obvious that we will have to create our own container. Let there be a vector and hold, for ease of use, an Element type. This Element type contains one row. In our case this is a text as a string and an integral number:
typedef struct
{
std::wstring m_name;
int m_age;
} Element;
First, we need to specify that the internal data management system be excluded. This is a simple task at the time of creation; just add the LVS_OWNERDATA flag inside the constructor. Be aware that inside the creation method of the list control, I have added more flags, but for the sake of clarity this is enough -- and this must be set from here, otherwise it will not work. Just as important in our case is the LVS_EX_GRIDLINES mask that actually creates the gridline look of the Microsoft Excel.
gbReportListCtrl m_rpListCtrl;
//…
m_rpListCtrl.Create(LVS_OWNERDATA, rect ,this, OUR_REPORT_LIST_ID);
The add procedure is simple to write; just put in a couple of string and int after adding this to the vector. Nevertheless, wait a second before you just add the item. I told you that the task of creating and handling the data of the control is ours; however, there is still one item that we need to set, even though it is the task of the container control.
It concerns the number of the items inside the list. This is required so that the control can calculate the size on its own, so it will know if it needs to modify the size of the scroll bars and other tasks that are related to the size of the list control. Setting this is a seamless job with the SetItemCount, yet it is necessary to have the thing inside your container. Adjust the code snippet:
void gbReportListCtrl::add( std::wstring name, int age )
{
Element newItem;
newItem.m_name = name;
newItem.m_age = age;
m_data.push_back(newItem);
// increase the number
int size= m_data.size();
this->SetItemCount(size);
this->Invalidate(); // show the increase
}
I must add only one more item. We need to explicitly call the invalidate function so the object will be redrawn, or else you will not see any kind of change within the application. I will not present the ”remove” task as this is just the opposite; all we need to change is the item count number. I leave this as a task for you to practice, since we still need to finish another process before we provide the text for drawing it on the screen.
Up to now, we've simply entered and created the text; we're not at the point of anything appearing on the screen. The virtual list implies that we will send the text of the container when that is required. Translated to common language, this means that the application needs to display the text on the screen.
Now that the control no longer handles the text display, the operating system will also send a LVN_GETDISPINFO message that we need to catch. Also here we can include a message when you click on a column, and require a sort under the message LVN_COLUMNCLICK or LVN_ODFINDITEM that will tell it what to do when a specific key is pressed (minus the system keys like the return, escape, and so forth).
All that remains for us is to provide the data within the function that will be drawn on the screen. For starters we will have some information that we need to display, or more exactly send the text for. This can be found inside the first parameter pointer, for which we need to do a cast, followed by an item information extraction.
Inside the item, there is a mask of what we are requested to pass on further. This in our case is obviously a text, but if you embed images inside the list, this can just as easily be a LVIF_IMAGE or other task specific items. Once we know what we need to pass and from where (this you can deduce with no effort at all from the coordinates (pItem->iItem; pItem->iSubItem)) and fill in the blank space, a simple copy to the pItem->pszText will do the job.
if (pItem->mask & LVIF_TEXT) // do we need to add a text ?
Following the idea mentioned earlier, you could make an easy assignment for the sort and the item query happening after you push down a key. As we did first, we extract from the first parameter of the function all of the information required to do the task we have to, and we follow on with its solution.
For the sort key, all we must know is what column we have to sort. This is stored inside a pNMLV->iSubItem as pointed out below in the proper code snippet. Before I show you the solution from the example, I need to explain that this can be a little shorter. Nevertheless, since I want to give you an accurate idea of how Windows behaves, we also have the sort order depending on what order the column was sorted, and changes to the direction of the sort if it was already a sorted column.
We simply look to see if the column is sorted in an ascending order, and if so, we make the sort go in a descending order; otherwise it does a normal sort. The good thing in this is that, by default, in a list control, the comparison is made via the text compare, and that is slower than comparing integers. The sorting function is as simple as it can get:
The final step is the item query. Here the key point for a change is at the end, because, depending on the result we sent back, an item would be selected and brought into view. You should notice here that we have a single line selection active, and it will only refer to the item queried after the first column. During our next meeting, I will show you how to implement a fully customized select with some in-place editing options, so stay tuned.
Here the task is simple. Start up a search in a circular order, and the first found item of the first char will be the result we send back to be selected.
// The result will contain the item for what the selection // will go
// pFindInfo->lvfi.psz => the char
// pFindInfo->iStart => currently at item
std::vector<Element>::iterator it, end;
// first assume that we are at start and try to find it // there
for (it = m_data.begin() + pFindInfo->iStart,
end = m_data.end(); it != end; ++it)
{
if (it->m_name[0] == *pFindInfo->lvfi.psz)
{
break;
}
}
if (it == end) // not found ? look at start
{
for (it = m_data.begin() ,
end = m_data.begin() + pFindInfo->iStart + 1 ; it != end; ++it)
{
if (it->m_name[0] == *pFindInfo->lvfi.psz)
{
break;
}
}
if (it == end)
{
*pResult = pFindInfo->iStart;
}
else
*pResult = std::distance(m_data.begin(), it);
}
else
*pResult = std::distance(m_data.begin(), it);
// *pResult- th item will be selected in the list control
}
With this we have arrived at the end of my article. I would like to invite you to come and express your thoughts, either here on the blog comment area you find just after the article, or in our friendly ever-growing forum over at Dev Hardware. Remember to watch for the final part of this series and Live with Passion!