Introduction
Taking a closer look at the Explorer of Windows Vista, you might notice that the list view part of it does many things that can't be done using just the list view API documented in MSDN. Among these things are:
- Footer areas - Explorer uses a footer area to offer an advanced search after the user has done a quick search using the search box.
- Subseted groups - The Welcome Center displays only two rows of items of a group, if space is not sufficient. The remaining items can be made visible by clicking a link at the bottom of each group.
- Grouping in virtual mode - The list view control of Windows Explorer operates in virtual mode, i.e., the
LVS_OWNERDATA style is set. This doesn't hinder it from displaying item groups. Everyone who has tried to setup groups in a virtual list view control will know that virtual mode actually excludes grouping.
This article will explain how to implement these features. It uses undocumented parts of the list view API.
Background
I'm the author of a freeware list view ActiveX control for Visual Basic 6.0. This control makes new list view features like item grouping and tiles view accessible to VB6 apps. I was looking for a way to support item grouping in virtual mode, and finally found Geoff Chappell's site. Under the section Studies/The Windows Shell, Geoff has published a lot of interesting stuff. This website gave me the interface definitions I needed. All I had to do was some parameter guessing and some trial and error. So, many thanks to Geoff!
Footer Areas
To insert a footer area into our list view, we first need to define two interfaces: IListViewFooter and IListViewFooterCallback. Let's start with IListViewFooter.
const IID IID_IListViewFooter = {0xF0034DA8, 0x8A22, 0x4151,
{0x8F, 0x16, 0x2E, 0xBA, 0x76, 0x56, 0x5B, 0xCC}};
class IListViewFooter :
public IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE IsVisible(PINT pVisible) = 0;
virtual HRESULT STDMETHODCALLTYPE GetFooterFocus(PINT pItemIndex) = 0;
virtual HRESULT STDMETHODCALLTYPE SetFooterFocus(int itemIndex) = 0;
virtual HRESULT STDMETHODCALLTYPE SetIntroText(LPCWSTR pText) = 0;
virtual HRESULT STDMETHODCALLTYPE Show(IListViewFooterCallback* pCallbackObject) = 0;
virtual HRESULT STDMETHODCALLTYPE RemoveAllButtons(void) = 0;
virtual HRESULT STDMETHODCALLTYPE InsertButton(int insertAt,
LPCWSTR pText, LPCWSTR pUnknown, UINT iconIndex, LONG lParam) = 0;
virtual HRESULT STDMETHODCALLTYPE GetButtonLParam(int itemIndex, LONG* pLParam) = 0;
};
The Doxygen comments should explain what each method does. I have not yet found out the purpose of the third parameter of InsertButton. I thought it would be a tool tip text, but no tool tip shows up when I set this parameter to some text. The icons that you can specify are taken from the footer area image list. This image list can be set by sending LVM_SETIMAGELIST with wParam set to 4.
As you may have noticed, the interface doesn't provide any method to remove a single item or to change the properties (like text) of an item. So, whenever you want to change the footer items, you actually have to remove all of them and insert new ones. Another limitation is that you cannot add more than four footer items.
So, how does IListViewFooterCallback look like?
const IID IID_IListViewFooterCallback = {0x88EB9442, 0x913B, 0x4AB4,
{0xA7, 0x41, 0xDD, 0x99, 0xDC, 0xB7, 0x55, 0x8B}};
class IListViewFooterCallback :
public IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE OnButtonClicked(int itemIndex,
LPARAM lParam, PINT pRemoveFooter) = 0;
virtual HRESULT STDMETHODCALLTYPE OnDestroyButton(int itemIndex, LPARAM lParam) = 0;
};
This interface is simple: one method to notify your app of footer item clicks, and one to notify it of footer item deletions. But, beware: both methods are called only for footer items that have a lParam other than 0.
So, how do we use the interfaces? Well, IListViewFooterCallback must be implemented by our app, IListViewFooter is implemented by the list view control. To get a pointer to the list view's implementation of IListViewFooter, we send it the (of course, undocumented) message LVM_QUERYINTERFACE:
#define LVM_QUERYINTERFACE (LVM_FIRST + 189)
IListViewFooter* pLvwFooter = NULL;
SendMessage(hWndLvw, LVM_QUERYINTERFACE, reinterpret_cast<WPARAM>(
&IID_IListViewFooter), reinterpret_cast<LPARAM>(&pLvwFooter));
Then, we can set an intro text, insert some items, and make the footer area visible, providing a pointer to our implementation of IListViewFooterCallback:
pLvwFooter->SetIntroText(L"Hello World!");
pLvwFooter->InsertButton(0, L"Click me!", NULL, 0, 1);
pLvwFooter->Show(...);
pLvwFooter->Release();
Subseted Groups
If the list view control is not large enough to display all its content without scrolling, it looks for groups that are marked for subseting and hides some of these groups' items. A link will be displayed at the bottom of such groups, which will restore the hidden items if clicked. To mark a group for subseting, we have to set the LVGS_SUBSETED state and a title for the link:
LVGROUP group = {0};
group.cbSize = sizeof(LVGROUP);
group.pszSubsetTitle = L"Display all items";
group.cchSubsetTitle = lstrlenW(group.pszSubsetTitle);
group.state = LVGS_SUBSETED;
group.stateMask = LVGS_SUBSETED;
group.mask = LVGF_STATE | LVGF_SUBSET;
SendMessage(hWndLvw, LVM_SETGROUPINFO, groupID,
reinterpret_cast<LPARAM>(&group));
Now, all we have to do is tell the list view control how many item rows shall remain visible. In the picture above, this would be 2. This value is set through the IListView interface. The complete definition of this interface is available on Geoff's site, and in the source code for this article. We need the SetGroupSubsetCount method:
const IID IID_IListView = {0x2FFE2979, 0x5928, 0x4386,
{0x9C, 0xDB, 0x8E, 0x1F, 0x15, 0xB7, 0x2F, 0xB4}};
const IID IID_IListView = {0xE5B16AF2, 0x3990, 0x4681,
{0xA6, 0x09, 0x1F, 0x06, 0x0C, 0xD1, 0x42, 0x69}};
class IListView :
public IOleWindow
{
public:
virtual HRESULT STDMETHODCALLTYPE
GetGroupSubsetCount(PINT pNumberOfRowsDisplayed) = 0;
virtual HRESULT STDMETHODCALLTYPE SetGroupSubsetCount(int numberOfRowsToDisplay) = 0;
};
We use the LVM_QUERYINTERFACE message again to retrieve the list view's implementation of IListView, then just call SetGroupSubsetCount passing the number of rows that will remain visible (here, 2).
IListView* pListView = NULL;
SendMessage(hWndLvw, LVM_QUERYINTERFACE, reinterpret_cast<WPARAM>(&IID_IListView),
reinterpret_cast<LPARAM>(&pListView));
pListView->SetGroupSubsetCount(2);
pListView->Release();
Note: The definition of IListView has changed for Windows 7 (hence the new IID). A new method called EnableAlphaShadow has been inserted between IsItemVisible and GetGroupSubsetCount.
Grouping in Virtual Mode
If you want to display a really huge number of items in a list view, you should use virtual mode. In virtual mode, the list view doesn't store any details about each item. Instead, your app tells the list view how many items to display, and the list view uses the LVN_GETDISPINFO notification to query any item details like text and icon from your app as needed.
However, Microsoft has forgotten ;-) to document how to group items in virtual mode. To achieve this, we need the already mentioned IListView interface and the new IOwnerDataCallback interface:
const IID IID_IOwnerDataCallback =
{0x44C09D56, 0x8D3B, 0x419D, {0xA4, 0x62, 0x7B, 0x95, 0x6B, 0x10, 0x5B, 0x47}};
class IOwnerDataCallback :
public IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE GetItemPosition(int itemIndex,
LPPOINT pPosition) = 0;
virtual HRESULT STDMETHODCALLTYPE SetItemPosition(int itemIndex,
POINT position) = 0;
virtual HRESULT STDMETHODCALLTYPE GetItemInGroup(int groupIndex,
int groupWideItemIndex, PINT pTotalItemIndex) = 0;
virtual HRESULT STDMETHODCALLTYPE GetItemGroup(int itemIndex,
int occurenceIndex, PINT pGroupIndex) = 0;
virtual HRESULT STDMETHODCALLTYPE GetItemGroupCount(int itemIndex,
PINT pOccurenceCount) = 0;
virtual HRESULT STDMETHODCALLTYPE OnCacheHint(LVITEMINDEX firstItem,
LVITEMINDEX lastItem) = 0;
};
This interface must be implemented by your app. To make your implementation known to the list view control, IListView::SetOwnerDataCallback must be called:
const IID IID_IListView = {0x2FFE2979, 0x5928, 0x4386,
{0x9C, 0xDB, 0x8E, 0x1F, 0x15, 0xB7, 0x2F, 0xB4}};
class IListView :
public IOleWindow
{
public:
virtual HRESULT STDMETHODCALLTYPE SetOwnerDataCallback(
IOwnerDataCallback* pCallback) = 0;
};
#define LVM_QUERYINTERFACE (LVM_FIRST + 189)
IListView* pListView = NULL;
SendMessage(hWndLvw, LVM_QUERYINTERFACE, reinterpret_cast<WPARAM>(&IID_IListView),
reinterpret_cast<LPARAM>(&pListView));
pListView->SetOwnerDataCallback(...);
Now, you can insert the groups. Yes, the groups are still managed completely by the list view control. When inserting the groups, the number of items that each group contains must be specified:
LVGROUP group = {0};
group.cbSize = sizeof(LVGROUP);
group.mask = LVGF_ALIGN | LVGF_GROUPID | LVGF_HEADER | LVGF_ITEMS;
group.iGroupId = 1;
group.uAlign = LVGA_HEADER_LEFT;
group.cItems = 300; group.pszHeader = _T("Group 1");
SendMessage(hWndLvw, LVM_INSERTGROUP, 0, reinterpret_cast<LPARAM>(&group));
SendMessage(hWndLvw, LVM_ENABLEGROUPVIEW, TRUE, 0);
The control also needs to know the total number of items:
SendMessage(hWndLvw, LVM_SETITEMCOUNT, 900, 0);
Note: If you specify a total item count larger than the sum of all groups' item counts, you can make an item appear in multiple groups. You'll also have to change the implementation of IOwnerDataCallback. I won't explain this in detail, because it doesn't really fit into this paragraph, and I do not yet know all the details about having the same item in multiple groups.
Handling the LVN_GETDISPINFO notification isn't different from how it would be without groups, so I won't explain it here. The missing part is how to implement IOwnerDataCallback. Only three of its methods are really needed: GetItemGroupCount, GetItemGroup, and GetItemInGroup. OnCacheHint is more or less the same as the LVN_ODCACHEHINT notification, with the difference that OnCacheHint is designed to support items being in multiple groups.
All we have to do in GetItemGroupCount is set the second parameter (I've called it pOccurenceCount) to 1 as we want each item to appear only once. If you want an item to appear in multiple groups, you would have to set the parameter to the number of groups that shall contain the item.
virtual STDMETHODIMP GetItemGroupCount(int itemIndex, PINT pOccurenceCount)
{
*pOccurenceCount = 1;
return S_OK;
}
GetItemInGroup is used by the list view control to determine which item belongs to which group. How is this done? The method works like Give me the total item index of the nth item in the mth group. So, if you have three groups and want item 0 in group 0, item 1 in group 1, item 2 in group 2, item 3 in group 0, and so on, the total item index would be the sum of the group-wide item index (n) multiplied by 3 and the group index (m):
virtual STDMETHODIMP GetItemInGroup(int groupIndex,
int groupWideItemIndex, PINT pTotalItemIndex)
{
*pTotalItemIndex = groupIndex + groupWideItemIndex * 3;
return S_OK;
}
GetItemGroup maps indexes in the opposite direction. It is called with a total item index, and returns this item's group index:
virtual STDMETHODIMP GetItemGroup(int itemIndex,
int occurenceIndex, PINT pGroupIndex)
{
*pGroupIndex = itemIndex % 3;
return S_OK;
}
That's it. The occurenceIndex parameter is useful only if you want single items to appear in multiple groups.
Points of Interest
The list view control implements some more COM interfaces than the ones mentioned in the article. Here's a list of them:
While working with the interfaces IListView and ISubItemCallback, I also got some other things working that I didn't even know were possible. E.g., it's possible to label-edit sub-items and group headers. Maybe, I'll extend this article in future.
Credits
Many thanks to Geoff Chappell again. Without his work, this article wouldn't exist. Thanks also goes to the author of SpeedCommander, Sven Ritter, who notified me that not only the IID of IListView has changed for Windows 7, but also the definition.
History
- v1.2 - 26 May 2009
- Made subseted groups work on Windows 7 by fixing the definition of
IListView for this system
- v1.1 - 17 April 2009
- Added definition of
IID_IListView for Windows 7
- Updated the demo project for Windows 7
- v1.0 - 08 April 2009
I studied Computer Science at the Dresden University of Technology and have graduated in 2009. Currently I'm working as C++ and VB6 developer for a German software company. My main task is the development of ActiveX controls.
I started programming in 1996 I think, when I was 14 years old. First on a Casio CFX9850G calculator, later also on a PC. My preferred languages are Visual Basic 6, C++ and C#, but I also speak a couple of other languages.
I've advanced skills in Windows shell programming, Windows GUI programming and ActiveX control programming.