Click here to Skip to main content
Click here to Skip to main content

Undocumented List View Features

By , 26 May 2009
Rate this:
Please Sign up or sign in to vote.

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

footer.png

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:
    /// \brief Retrieves whether the footer area is currently displayed
    ///
    /// Retrieves whether the list view control's footer area is currently displayed.
    ///
    /// \param[out] pVisible \c TRUE if the footer area is visible; otherwise \c FALSE.
    ///
    /// \return An \c HRESULT error code.
    virtual HRESULT STDMETHODCALLTYPE IsVisible(PINT pVisible) = 0;
    /// \brief Retrieves the caret footer item
    ///
    /// Retrieves the list view control's focused footer item.
    ///
    /// \param[out] pItemIndex Receives the zero-based index
    ///      of the footer item that has the keyboard focus.
    ///
    /// \return An \c HRESULT error code.
    virtual HRESULT STDMETHODCALLTYPE GetFooterFocus(PINT pItemIndex) = 0;
    /// \brief Sets the caret footer item
    ///
    /// Sets the list view control's focused footer item.
    ///
    /// \param[in] itemIndex The zero-based index
    ///        of the footer item to which to set the keyboard focus.
    ///
    /// \return An \c HRESULT error code.
    virtual HRESULT STDMETHODCALLTYPE SetFooterFocus(int itemIndex) = 0;
    /// \brief Sets the footer area's caption
    ///
    /// Sets the title text of the list view control's footer area.
    ///
    /// \param[in] pText The text to display in the footer area's title.
    ///
    /// \return An \c HRESULT error code.
    virtual HRESULT STDMETHODCALLTYPE SetIntroText(LPCWSTR pText) = 0;
    /// \brief Makes the footer area visible
    ///
    /// Makes the list view control's footer area visible
    /// and registers the callback object that is notified
    /// about item clicks and item deletions.
    ///
    /// \param[in] pCallbackObject The \c IListViewFooterCallback
    ///   implementation of the callback object to register.
    ///
    /// \return An \c HRESULT error code.
    virtual HRESULT STDMETHODCALLTYPE Show(IListViewFooterCallback* pCallbackObject) = 0;
    /// \brief Removes all footer items
    ///
    /// Removes all footer items from the list view control's footer area.
    ///
    /// \return An \c HRESULT error code.
    virtual HRESULT STDMETHODCALLTYPE RemoveAllButtons(void) = 0;
    /// \brief Inserts a footer item
    ///
    /// Inserts a new footer item with the specified properties
    /// at the specified position into the list view
    /// control.
    ///
    /// \param[in] insertAt The zero-based index at which to insert the new footer item.
    /// \param[in] pText The new footer item's text.
    /// \param[in] pUnknown ???
    /// \param[in] iconIndex The zero-based index of the new footer item's icon.
    /// \param[in] lParam The integer data that will be associated 
    /// with the new footer item.
    ///
    /// \return An \c HRESULT error code.
    virtual HRESULT STDMETHODCALLTYPE InsertButton(int insertAt, 
            LPCWSTR pText, LPCWSTR pUnknown, UINT iconIndex, LONG lParam) = 0;
    /// \brief Retrieves a footer item's associated data
    ///
    /// Retrieves the integer data associated with the specified footer item.
    ///
    /// \param[in] itemIndex The zero-based index
    ///       of the footer for which to retrieve the associated data.
    /// \param[out] pLParam Receives the associated data.
    ///
    /// \return An \c HRESULT error code.
    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:
    /// \brief Notifies the client that a footer item has been clicked
    ///
    /// This method is called by the list view control to notify
    /// the client application that the user has
    /// clicked a footer item.
    ///
    /// \param[in] itemIndex The zero-based index
    ///     of the footer item that has been clicked.
    /// \param[in] lParam The application-defined integer
    ///    value that is associated with the clicked item.
    /// \param[out] pRemoveFooter If set to \c TRUE, the list view
    ///    control will remove the footer area.
    ///
    /// \return An \c HRESULT error code.
    virtual HRESULT STDMETHODCALLTYPE OnButtonClicked(int itemIndex, 
                    LPARAM lParam, PINT pRemoveFooter) = 0;
    /// \brief Notifies the client that a footer item has been removed
    ///
    /// This method is called by the list view control to notify
    /// the client application that it has removed a
    /// footer item.
    ///
    /// \param[in] itemIndex The zero-based index of the footer item 
    /// that has been removed.
    /// \param[in] lParam The application-defined integer
    ///   value that is associated with the removed item.
    ///
    /// \return An \c HRESULT error code.
    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);
// insert a pointer to the implementation of IListViewFooterCallback here
pLvwFooter->Show(...);
pLvwFooter->Release();

Subseted Groups

subsetedgroups.png

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:

// for Windows Vista and 2008:
const IID IID_IListView = {0x2FFE2979, 0x5928, 0x4386, 
      {0x9C, 0xDB, 0x8E, 0x1F, 0x15, 0xB7, 0x2F, 0xB4}};
// for Windows 7 and probably 2008 R2:
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 Wink | ;-) 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;
    /// \brief Will be called to retrieve an item's
    ///         zero-based control-wide index
    ///
    /// This method is called by the list view control
    /// to retrieve an item's zero-based control-wide index.
    /// The item is identified by a zero-based group index,
    /// which identifies the list view group in which
    /// the item is displayed, and a zero-based group-wide
    /// item index, which identifies the item within its group.
    ///
    /// \param[in] groupIndex The zero-based index of the list view
    ///            group containing the item.
    /// \param[in] groupWideItemIndex The item's zero-based
    ///            group-wide index within the list view group
    ///            specified by \c groupIndex.
    /// \param[out] pTotalItemIndex Receives the item's zero-based control-wide index.
    ///
    /// \return An \c HRESULT error code.
    virtual HRESULT STDMETHODCALLTYPE GetItemInGroup(int groupIndex, 
            int groupWideItemIndex, PINT pTotalItemIndex) = 0;
    /// \brief Will be called to retrieve the group
    ///        containing a specific occurrence of an item

    ///
    /// This method is called by the list view control to retrieve
    /// the list view group in which the specified
    /// occurrence of the specified item is displayed.
    ///
    /// \param[in] itemIndex The item's zero-based (control-wide) index.
    /// \param[in] occurrenceIndex The zero-based index
    ///            of the item's copy for which the group membership is
    ///            retrieved.
    /// \param[out] pGroupIndex Receives the zero-based index
    ///             of the list view group that shall contain the
    ///             specified copy of the specified item.
    ///
    /// \return An \c HRESULT error code.
    virtual HRESULT STDMETHODCALLTYPE GetItemGroup(int itemIndex, 
                    int occurenceIndex, PINT pGroupIndex) = 0;
    /// \brief Will be called to determine how often
    ///          an item occurs in the list view control
    ///
    /// This method is called by the list view control to determine
    /// how often the specified item occurs in the
    /// list view control.
    ///
    /// \param[in] itemIndex The item's zero-based (control-wide) index.
    /// \param[out] pOccurrencesCount Receives the number
    ///             of occurrences of the item in the list view control.
    ///
    /// \return An \c HRESULT error code.
    virtual HRESULT STDMETHODCALLTYPE GetItemGroupCount(int itemIndex, 
                                      PINT pOccurenceCount) = 0;
    /// \brief Will be called to prepare the client app that the data
    ///         for a certain range of items will be required very soon
    ///
    /// This method is similar to the \c LVN_ODCACHEHINT notification.
    /// It tells the client application that
    /// it should preload the details for a certain range
    /// of items because the list view control is about to
    /// request these details. The difference to \c LVN_ODCACHEHINT
    /// is that this method identifies the items
    /// by their zero-based group-wide index and the zero-based index
    /// of the list view group containing the item.
    ///
    /// \param[in] firstItem The first item to cache.
    /// \param[in] lastItem The last item to cache.
    ///
    /// \return An \c HRESULT error code.
    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(...);
// insert a pointer to the implementation of IOwnerDataCallback here

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;        // we want the group to contain 300 items
group.pszHeader = _T("Group 1");
SendMessage(hWndLvw, LVM_INSERTGROUP, 0, reinterpret_cast<LPARAM>(&group));
SendMessage(hWndLvw, LVM_ENABLEGROUPVIEW, TRUE, 0);        // enable groups

The control also needs to know the total number of items:

SendMessage(hWndLvw, LVM_SETITEMCOUNT, 900, 0);
// say we have 3 groups à 300 items, then we have 900 items in total

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)
{
    // we want group 0 to contain items 0, 3, 6...
    //         group 1            items 1, 4, 7...
    //         group 2            items 2, 5, 8...
    *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)
{
    // group 0 contains items 0, 3, 6...
    // group 1 contains items 1, 4, 7...
    // group 2 contains items 2, 5, 8...
    *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
    • Created

License

This article, along with any associated source code and files, is licensed under A Public Domain dedication

About the Author

Timo Kunze
Software Developer (Junior)
Germany Germany
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.

Comments and Discussions

 
GeneralMy vote of 5 PinmemberMihai MOGA12-May-12 18:37 
Generalcan't compile Pinmembercpienta26-Jul-10 4:39 
GeneralRe: can't compile PinmemberTimo Kunze26-Jul-10 7:49 
GeneralRe: can't compile Pinmembercpienta30-Jul-10 9:14 
GeneralRe: can't compile PinmemberTimo Kunze30-Jul-10 9:21 
GeneralRe: can't compile Pinmembercpienta2-Aug-10 7:29 
GeneralRe: can't compile PinmemberTimo Kunze2-Aug-10 11:25 
GeneralRe: can't compile Pinmembercpienta3-Aug-10 3:01 
GeneralRe: can't compile PinmemberTimo Kunze3-Aug-10 5:59 
GeneralMarquee (lasso) selection in full row selection mode PinmemberTimo Kunze13-Mar-10 9:55 
Questionthat can work on windows XP? Pinmemberregeradm26-Oct-09 17:07 
AnswerRe: that can work on windows XP? PinmemberTimo Kunze26-Oct-09 23:01 
GeneralBug in Windows 7 using ListView Control of a CFileDialog PinmemberANIL KUMAR SHARMA (INDIA)21-Oct-09 18:04 
GeneralGood one.. PinmemberSteppenwolfe26-May-09 10:34 
GeneralRe: Good one.. PinmemberVytas27-May-09 7:09 
Questionhave you tried windows 7? Pinmemberumeca748-Apr-09 19:45 
AnswerRe: have you tried windows 7? PinmemberTimo Kunze8-Apr-09 22:14 
GeneralRe: have you tried windows 7? Pinmemberumeca748-Apr-09 22:40 
GeneralRe: have you tried windows 7? PinmemberTimo Kunze9-Apr-09 7:26 
GeneralRe: have you tried windows 7? PinmemberTimo Kunze17-Apr-09 7:26 
AnswerRe: have you tried windows 7? PinmemberTroy Russell26-May-09 10:12 
Generalcan't download demo & src files... Pinmemberfmaeseele8-Apr-09 17:13 
GeneralRe: can't download demo & src files... PinmemberLoveVc8-Apr-09 21:37 
GeneralRe: can't download demo & src files... PinmemberTimo Kunze8-Apr-09 22:06 
JokeRe: can't download demo & src files... PinmemberTroy Russell26-May-09 11:12 
GeneralRe: can't download demo & src files... PinmemberTroy Russell26-May-09 11:31 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140415.2 | Last Updated 26 May 2009
Article Copyright 2009 by Timo Kunze
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid