Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WTL

Undocumented List View Features

4.96/5 (54 votes)
30 Sep 2015Public Domain11 min read 129.2K   3.2K  
Use undocumented parts of the Windows Vista list view API to enhance your application.

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.
  • Sub-item controls - Explorer visualizes a drive's fill level as a bar similar to a progress bar. This is just one usage of sub-item controls.

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

listviewundoc/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.

C++
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?

C++
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:

C++
#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:

C++
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

listviewundoc/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:

C++
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:

C++
// 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).

C++
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:

C++
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:

C++
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:

C++
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:

C++
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.

C++
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):

C++
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:

C++
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.

Sub-Item Controls

Using a sub-item control to visualize a drive's fill level Using a sub-item control to visualize a photo's rating

Probably one of the most annoying limitations of the list view control has been that you cannot edit sub-items. Usually a simple text box would be enough, but sub-item controls are much more powerful. A sub-item control is a COM class that implements a set of interfaces, providing a visual representation of a sub-item, as well as a user interface for in-place editing the sub-item. Windows Explorer uses the visualization part to display a drive's used space and to display the rating of photos and songs as a bunch of stars. Starting with Windows 7, this rating also uses the in-place editing part of sub-item controls: You can move the mouse over the stars and change the rating by clicking a star.

In this article I will concentrate on how to use sub-item controls, not how to implement them. We'll make use of the sub-item controls that are implemented in the Windows Shell. The sub-item controls API is tied closely to the shell anyway.

You already know the IListView interface by now. We'll use its SetSubItemCallback method, which binds an implementation of the ISubItemCallback interface to the list view. So the first new interface is ISubItemCallback:

const IID IID_ISubItemCallback =
  {0x11A66240, 0x5489, 0x42C2, {0xAE, 0xBF, 0x28, 0x6F, 0xC8, 0x31, 0x52, 0x4C}};

class ISubItemCallback :
    public IUnknown
{
public:
  virtual HRESULT STDMETHODCALLTYPE GetSubItemTitle(int subItemIndex,
                                    LPWSTR pBuffer, int bufferSize) = 0;
  virtual HRESULT STDMETHODCALLTYPE GetSubItemControl(int itemIndex,
                                    int subItemIndex, REFIID requiredInterface,
                                    LPVOID* ppObject) = 0;
  virtual HRESULT STDMETHODCALLTYPE BeginSubItemEdit(int itemIndex,
                                    int subItemIndex, int mode,
                                    REFIID requiredInterface, LPVOID* ppObject) = 0;
  virtual HRESULT STDMETHODCALLTYPE EndSubItemEdit(int itemIndex,
                                    int subItemIndex, int mode,
                                    IPropertyControl* pPropertyControl) = 0;
  virtual HRESULT STDMETHODCALLTYPE BeginGroupEdit(int groupIndex,
                                    REFIID requiredInterface, LPVOID* ppObject) = 0;
  virtual HRESULT STDMETHODCALLTYPE EndGroupEdit(int groupIndex, int mode,
                                    IPropertyControl* pPropertyControl) = 0;
  virtual HRESULT STDMETHODCALLTYPE OnInvokeVerb(int itemIndex, LPCWSTR pVerb) = 0;
};

We'll need to implement this interface, so what are all the methods for?

  • GetSubItemTitle is called in extended tile view only. It retrieves the text to display in front of the sub-item's value.
  • GetSubItemControl is called to retrieve the sub-item control for a specific sub-item. It may request implementations for two interfaces:
    • IDrawPropertyControl ({E6DFF6FD-BCD5-4162-9C65-A3B18C616FDB})
    • Windows 10 only: An unknown interface with the IID {1572DD51-443C-44B0-ACE4-38A005FC697E}
    The shell's sub-item controls implement these interfaces, so I won't go more into detail.
  • BeginSubItemEdit is called when the user wants to start in-place editing a sub-item. The mode parameter specifies the user action that triggered the call:
    • 0 - The user has moved the mouse over the sub-item. It is up to the sub-item control implementation whether this starts the edit mode.
    • 1 - The user clicked onto the sub-item.
    The requested object must implement the IPropertyControl interface ({5E82A4DD-9561-476A-8634-1BEBACBA4A38}).
  • EndSubItemEdit is called when leaving edit mode for a specific sub-item. This is the place where the value entered by the user should be persisted. The mode parameter has the same meaning as for BeginSubItemEdit
  • BeginGroupEdit and EndGroupEdit. Yeap, group labels can be edited as well, but I won't cover this topic here. The usage of these methods is pretty much the same as for sub-items.
  • OnInvokeVerb is called when a sub-item control registered a user action. For instance, if the sub-item control displays a hyperlink and the user clicks this link, OnInvokeVerb will be called with the pVerb parameter containing the link's id.

The implementations of GetSubItemControl and BeginSubItemEdit usually are very similar, so we'll forward the call from GetSubItemControl to BeginSubItemEdit:

virtual STDMETHODIMP GetSubItemControl(int itemIndex, int subItemIndex,
  REFIID requiredInterface, LPVOID* ppObject)
{
  return BeginSubItemEdit(itemIndex, subItemIndex, 0,
           requiredInterface, ppObject);
}

So to make sub-item controls display and allow entering edit-mode, only BeginSubItemEdit is needed. We'll cover persisting edited sub-items afterwards.

Within BeginSubItemEdit we first have to decide which sub-item control to use for the given list view sub-item. The Windows shell's property system implements the following sub-item controls (and maybe some more), which can be instantiated by calling CoCreateInstance:

  • CLSID_CBooleanControl ({1E8F0D70-7399-41BF-8598-7949A2DEC898}) - Displays a drop-down list to choose between "Yes" and "No".
  • CLSID_CCustomDrawMultiValuePropertyControl ({e2183960-9d58-4e9c-878a-4acc06ca564a}) - Mentioned for completeness, I never managed to make this sub-item control work.
  • CLSID_CCustomDrawPercentFullControl ({AB517586-73CF-489c-8D8C-5AE0EAD0613A}) - The control that is used by Windows Explorer to visualize a drive's fill level.
  • CLSID_CCustomDrawProgressControl ({0d81ea0d-13bf-44b2-af1c-fcdf6be7927c}) - Mentioned for completeness, I never managed to make this sub-item control work.
  • CLSID_CHyperlinkControl ({15756be1-a4ad-449c-b576-df3df0e068d3}) - Displays a hyperlink that the user can click.
  • CLSID_CIconListControl ({53a01e9d-61cc-4cb0-83b1-31bc8df63156}) - Mentioned for completeness, I never managed to make this sub-item control work.
  • CLSID_CInPlaceCalendarControl ({6A205B57-2567-4a2c-B881-F787FAB579A3}) - Displays a date time picker control to select a date from.
  • CLSID_CInPlaceDropListComboControl ({0EEA25CC-4362-4a12-850B-86EE61B0D3EB}) - Displays a drop-down list to select a value from.
  • CLSID_CInPlaceEditBoxControl ({A9CF0EAE-901A-4739-A481-E35B73E47F6D}) - Displays a single-line text box.
  • CLSID_CInPlaceMLEditBoxControl ({8EE97210-FD1F-4b19-91DA-67914005F020}) - Displays a multi-line text box.
  • CLSID_CInPlaceMultiValuePropertyControl ({8e85d0ce-deaf-4ea1-9410-fd1a2105ceb5}) - Mentioned for completeness, I never managed to make this sub-item control work.
  • CLSID_CRatingControl ({85e94d25-0712-47ed-8cde-b0971177c6a1}) - Displays five stars to rate the item.
  • CLSID_CStaticPropertyControl ({527c9a9b-b9a2-44b0-84f9-f0dc11c2bcfb}) - Displays the sub-item's value as plain text.

Some of the sub-item controls, for instance those who display a drop-down list, need a list of possible values to display. This list has to be provided by an object that implements the IPropertyDescription interface, which is documented on MSDN. In this article we simply display some of the Windows property system's properties and use the system's IPropertyDescription implementation.

So this is the first part of our implementation of ISubItemCallback::BeginSubItemEdit:

virtual STDMETHODIMP BeginSubItemEdit(int itemIndex, int subItemIndex,
        int /*mode*/, REFIID requiredInterface, LPVOID* ppObject)
{
  if(!ppObject) {
    return E_POINTER;
  }
  if(subItemIndex != 1) {
    // We want to handle only the first sub-item of each item.
    return E_NOINTERFACE;
  }

  HRESULT hr = E_NOINTERFACE;
  CComPtr<IPropertyDescription> pPropertyDescription = NULL;

  // use another sub-item control for each item
  switch(itemIndex) {
    case 0:
      hr = CoCreateInstance(CLSID_CInPlaceMLEditBoxControl, NULL,
              CLSCTX_INPROC_SERVER, requiredInterface, ppObject);
      PSGetPropertyDescriptionByName(L"System.Generic.String",
              IID_PPV_ARGS(&pPropertyDescription));
      break;
    case 1:
      // the sub-item text must be a value from 0 to 100
      hr = CoCreateInstance(CLSID_CCustomDrawPercentFullControl, NULL,
              CLSCTX_INPROC_SERVER, requiredInterface, ppObject);
      break;
    case 2:
      // the sub-item text must be a value from 1 to 5
      hr = CoCreateInstance(CLSID_CRatingControl, NULL,
              CLSCTX_INPROC_SERVER, requiredInterface, ppObject);
      break;
    case 3:
      if(requiredInterface == IID_IDrawPropertyControl ||
         requiredInterface == IID_IWin10Unknown) {
        hr = CoCreateInstance(CLSID_CStaticPropertyControl, NULL,
                CLSCTX_INPROC_SERVER, requiredInterface, ppObject);
      } else {
        hr = CoCreateInstance(CLSID_CInPlaceEditBoxControl, NULL,
                CLSCTX_INPROC_SERVER, requiredInterface, ppObject);
      }
      PSGetPropertyDescriptionByName(L"System.Generic.String",
              IID_PPV_ARGS(&pPropertyDescription));
      break;
    case 4:
      // the sub-item text must be -1 or 0
      hr = CoCreateInstance(CLSID_CBooleanControl, NULL,
              CLSCTX_INPROC_SERVER, requiredInterface, ppObject);
      PSGetPropertyDescriptionByName(L"System.Generic.Boolean",
              IID_PPV_ARGS(&pPropertyDescription));
      break;
    case 5:
      // the sub-item text must be a date in the format: YYYY/MM/dd:HH:mm:ss.fff
      hr = CoCreateInstance(CLSID_CInPlaceCalendarControl, NULL,
              CLSCTX_INPROC_SERVER, requiredInterface, ppObject);
      PSGetPropertyDescriptionByName(L"System.Generic.DateTime",
              IID_PPV_ARGS(&pPropertyDescription));
      break;
    case 6:
      // the sub-item text must be an integer number >=0
      hr = CoCreateInstance(CLSID_CInPlaceDropListComboControl, NULL,
              CLSCTX_INPROC_SERVER, requiredInterface, ppObject);
      PSGetPropertyDescriptionByName(L"System.Photo.MeteringMode",
              IID_PPV_ARGS(&pPropertyDescription));
      break;
    case 7:
      // the sub-item text must have this format:
      // <a id="Some unique id">http://www.google.com</a>
      hr = CoCreateInstance(CLSID_CHyperlinkControl, NULL,
              CLSCTX_INPROC_SERVER, requiredInterface, ppObject);
      break;
  }
  
  if(SUCCEEDED(hr)) {
    // See below...
  }
  return hr;
}

Next we need to configure the sub-item control, i.e. set the current value, set the text color and font, and set the window theme to use. To set the sub-item's current value, we'll need an object that implements IPropertyValue interface (see MSDN). We'll come back to this implementation later, let's complete BeginSubItemEdit first:

IPropertyControlBase* pControl =
  *reinterpret_cast<IPropertyControlBase**>(ppObject);
CComBSTR themeAppName = L"Explorer";
CComBSTR themeIDList = NULL;
HFONT hFont = GetFont();
COLORREF textColor =
  static_cast<COLORREF>(SendMessage(hWndLvw, LVM_GETTEXTCOLOR, 0, 0));
if(textColor == CLR_NONE) {
  textColor = GetSysColor(COLOR_WINDOWTEXT);
}

// get the sub-item's current value
LPWSTR pBuffer =
  reinterpret_cast<LPWSTR>(HeapAlloc(GetProcessHeap(),
                           0, (1024 + 1) * sizeof(WCHAR)));
if(pBuffer) {
  LVITEMW item = {0};
  item.iSubItem = subItemIndex;
  item.cchTextMax = 1024;
  item.pszText = pBuffer;
  SendMessage(hWndLvw, LVM_GETITEMTEXTW, itemIndex,
    reinterpret_cast<LPARAM>(&item));
  if(itemIndex == 1 || itemIndex == 2 || itemIndex == 6) {
    PROPVARIANT tmp;
    PropVariantInit(&tmp);
    InitPropVariantFromString(item.pszText, &tmp);
    PropVariantChangeType(pPropertyValue, tmp, 0, VT_UI4);
    PropVariantClear(&tmp);
  } else if(itemIndex == 4) {
    PROPVARIANT tmp;
    PropVariantInit(&tmp);
    InitPropVariantFromString(item.pszText, &tmp);
    PropVariantChangeType(pPropertyValue, tmp, 0, VT_BOOL);
    PropVariantClear(&tmp);
  } else if(itemIndex == 5) {
    PROPVARIANT tmp;
    PropVariantInit(&tmp);
    InitPropVariantFromString(item.pszText, &tmp);
    PropVariantChangeType(pPropertyValue, tmp, 0, VT_FILETIME);
    PropVariantClear(&tmp);
  } else {
    InitPropVariantFromString(item.pszText, pPropertyValue);
  }
  HeapFree(GetProcessHeap(), 0, pBuffer);
  pBuffer = NULL;
}

// configure the sub-item control
CComPtr<IPropertyValue> pPropertyValueObj = NULL;
if(SUCCEEDED(IPropertyValueImpl::CreateInstance(NULL, IID_IPropertyValue,
    reinterpret_cast<LPVOID*>(&pPropertyValueObj))) &&
    pPropertyValueObj &&
    SUCCEEDED(pPropertyValueObj->InitValue(*pPropertyValue))) {
  if(pPropertyDescription) {
    pControl->Initialize(pPropertyDescription,
        static_cast<IPropertyControlBase::PROPDESC_CONTROL_TYPE>(0));
  }
  pControl->SetValue(pPropertyValueObj);
  pControl->SetTextColor(textColor);
  if(hFont) {
    pControl->SetFont(hFont);
  }
  pControl->SetWindowTheme(themeAppName, themeIDList);
} else {
  pControl->Destroy();
  hr = E_NOINTERFACE;
}

To make the list view use our ISubItemCallback implementation, we need to call IListView::SetSubItemCallback:

IListView* pLvw = NULL;
SendMessage(hWndLvw, LVM_QUERYINTERFACE,
    reinterpret_cast<WPARAM>(&IID_IListView),
    reinterpret_cast<LPARAM>(&pLvw));
if(pLvw) {
  ISubItemCallback* pSubItemCallback = NULL;
  QueryInterface(IID_ISubItemCallback,
      reinterpret_cast<LPVOID*>(&pSubItemCallback));
  pLvw->SetSubItemCallback(pSubItemCallback);
  pLvw->Release();
}

To persist sub-item edits, we need to implement EndSubItemEdit:

virtual STDMETHODIMP EndSubItemEdit(int itemIndex, int subItemIndex,
    int /*mode*/, IPropertyControl* pPropertyControl)
{
  if(!pPropertyControl) {
    return E_POINTER;
  }

  BOOL modified = FALSE;
  pPropertyControl->IsModified(&modified);
  if(modified) {
    CComPtr<IPropertyValue> pPropertyValue = NULL;
    if(SUCCEEDED(pPropertyControl->GetValue(IID_IPropertyValue,
        reinterpret_cast<LPVOID*>(&pPropertyValue))) && pPropertyValue) {
      PROPVARIANT propertyValue;
      PropVariantInit(&propertyValue);
      if(SUCCEEDED(pPropertyValue->GetValue(&propertyValue))) {
        LPWSTR pBuffer = NULL;
        if(SUCCEEDED(PropVariantToStringAlloc(propertyValue, &pBuffer)) &&
            pBuffer) {
          SetItemText(itemIndex, subItemIndex, pBuffer);
          CoTaskMemFree(pBuffer);
        }
        PropVariantClear(&propertyValue);
      }
    }
  }
  return pPropertyControl->Destroy();
}

The last piece missing is the implementation of IPropertyValue, which is quite simple. The class needs a PROPERTYKEY member and a PROPVARIANT member. The SetPropertyKey and GetPropertyKey methods set and get the PROPERTYKEY member. The InitValue and GetValue methods set and get the PROPVARIANT member (using PropVariantCopy).

The sample project also includes code that combines all this with the LVN_GETDISPINFO notification, so that the sub-item controls are loaded on demand.

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:

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.3 - 30 September 2015
    • Added a chapter about sub-item controls
  • 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