65.9K
CodeProject 正在变化。 阅读更多。
Home

未公开的列表视图功能

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (50投票s)

2009年4月8日

公共领域

11分钟阅读

viewsIcon

132061

downloadIcon

3284

使用 Windows Vista 列表视图 API 的未文档化部分来增强您的应用程序。

引言

仔细查看 Windows Vista 的资源管理器,您可能会注意到它在列表视图部分做了许多事情,而这些事情是无法通过 MSDN 中文档化的列表视图 API 来实现的。其中一些事情包括:

  • 页脚区域 - 资源管理器使用页脚区域在用户使用搜索框进行快速搜索后提供高级搜索功能。
  • 子集化分组 - 如果空间不足,欢迎中心仅显示组的两个项目行。通过点击每个组底部的链接可以显示剩余的项目。
  • 虚拟模式下的分组 - Windows 资源管理器的列表视图控件以虚拟模式运行,即设置了 LVS_OWNERDATA 样式。这并不妨碍它显示项目组。任何尝试在虚拟列表视图控件中设置组的人都知道,虚拟模式实际上排除了分组。
  • 子项控件 - 资源管理器将驱动器的填充级别可视化为一个类似于进度条的条形图。这只是子项控件的一种用法。

本文将解释如何实现这些功能。它使用了列表视图 API 的未文档化部分。

背景

我是 Visual Basic 6.0 的一个免费列表视图 ActiveX 控件的作者。该控件为 VB6 应用程序提供了项目分组和磁贴视图等新列表视图功能。我一直在寻找一种方法来支持虚拟模式下的项目分组,并最终找到了Geoff Chappell 的网站。在“研究/Windows Shell”部分,Geoff 发表了许多有趣的内容。该网站为我提供了我需要的接口定义。我所要做的就是一些参数猜测和一些试错。因此,非常感谢 Geoff!

页脚区域

listviewundoc/footer.png

要在我们的列表视图中插入页脚区域,我们首先需要定义两个接口:IListViewFooterIListViewFooterCallback。让我们从 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;
};

Doxygen 注释应该解释每个方法的作用。我还没有弄清楚 InsertButton 的第三个参数的用途。我以为它会是工具提示文本,但当我将此参数设置为一些文本时,并没有显示工具提示。您可以指定的图标取自页脚区域图像列表。可以通过发送 LVM_SETIMAGELIST 并将 wParam 设置为 4 来设置此图像列表。

正如您可能注意到的,该接口没有提供任何方法来删除单个项目或更改项目的属性(如文本)。因此,每当您想更改页脚项目时,实际上必须删除所有项目并插入新的项目。另一个限制是您不能添加超过四个页脚项目。

那么,IListViewFooterCallback 看起来是怎样的?

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;
};

该接口很简单:一个方法用于通知您的应用程序页脚项目点击事件,另一个方法用于通知它页脚项目删除事件。但是,请注意:这两个方法仅在页脚项目的 lParam 不为 0 时才会被调用。

那么,我们如何使用这些接口呢?嗯,IListViewFooterCallback 必须由我们的应用程序实现,IListViewFooter 由列表视图控件实现。要获取列表视图的 IListViewFooter 实现的指针,我们向它发送(当然是未文档化的)消息 LVM_QUERYINTERFACE

#define LVM_QUERYINTERFACE (LVM_FIRST + 189)

IListViewFooter* pLvwFooter = NULL;
SendMessage(hWndLvw, LVM_QUERYINTERFACE, reinterpret_cast<WPARAM>(
  &IID_IListViewFooter), reinterpret_cast<LPARAM>(&pLvwFooter));

然后,我们可以设置介绍文本,插入一些项目,并使页脚区域可见,提供我们实现的 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();

子集化分组

listviewundoc/subsetedgroups.png

如果列表视图控件不够大,无法在不滚动的情况下显示所有内容,它会查找标记为子集化的组,并隐藏其中一些组的项目。这样的组底部会显示一个链接,点击该链接将恢复隐藏的项目。要将组标记为子集化,我们必须设置 LVGS_SUBSETED 状态和链接的标题。

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));

现在,我们所要做的就是告诉列表视图控件应保留多少行项目可见。在上图所示的情况下,这将是 2。此值通过 IListView 接口设置。此接口的完整定义可在Geoff 的网站和本文的源代码中找到。我们需要 SetGroupSubsetCount 方法。

// 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;
    // ...
};

我们再次使用 LVM_QUERYINTERFACE 消息来检索列表视图的 IListView 实现,然后调用 SetGroupSubsetCount 并传入将保持可见的行数(此处为 2)。

IListView* pListView = NULL;
SendMessage(hWndLvw, LVM_QUERYINTERFACE, reinterpret_cast<WPARAM>(&IID_IListView), 
            reinterpret_cast<LPARAM>(&pListView));
pListView->SetGroupSubsetCount(2);
pListView->Release();

注意: IListView 的定义已在 Windows 7 中更改(因此 IID 也有所变化)。在 IsItemVisibleGetGroupSubsetCount 之间插入了一个名为 EnableAlphaShadow 的新方法。

虚拟模式下的分组

如果您想在列表视图中显示大量项目,您应该使用虚拟模式。在虚拟模式下,列表视图不存储关于每个项目的任何详细信息。相反,您的应用程序会告诉列表视图要显示多少项目,列表视图使用 LVN_GETDISPINFO 通知来按需从您的应用程序查询任何项目详细信息,如文本和图标。

然而,Microsoft 忘记了 ;-) 文档化如何在虚拟模式下分组项目。要实现这一点,我们需要前面提到的 IListView 接口和新的 IOwnerDataCallback 接口。

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;
};

此接口必须由您的应用程序实现。要让列表视图控件知道您的实现,必须调用 IListView::SetOwnerDataCallback

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

现在,您可以插入组。是的,组仍然完全由列表视图控件管理。插入组时,必须指定每个组包含的项目数。

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

控件还需要知道项目的总数。

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

注意: 如果您指定的总项目数大于所有组的项目数之和,您可以让一个项目出现在多个组中。您还必须更改 IOwnerDataCallback 的实现。我不会详细解释这一点,因为它与本段内容不太相关,而且我还没有掌握同一项目出现在多个组中的所有细节。

处理 LVN_GETDISPINFO 通知与没有分组时的情况没有区别,所以我不会在这里解释。缺失的部分是如何实现 IOwnerDataCallback。只有它的三个方法是真正需要的:GetItemGroupCountGetItemGroupGetItemInGroupOnCacheHintLVN_ODCACHEHINT 通知大致相同,区别在于 OnCacheHint 被设计用来支持项目出现在多个组中。

GetItemGroupCount 中,我们所要做的就是将第二个参数(我称之为 pOccurenceCount)设置为 1,因为我们希望每个项目只出现一次。如果您希望一个项目出现在多个组中,您需要将该参数设置为该项目应该出现的组数。

virtual STDMETHODIMP GetItemGroupCount(int itemIndex, PINT pOccurenceCount)
{
    *pOccurenceCount = 1;
    return S_OK;
}

GetItemInGroup 用于列表视图控件确定哪个项目属于哪个组。这是如何做到的?该方法的作用类似于“给我第 m 个组中的第 n 个项目的总项目索引”。因此,如果您有三个组,并且想要项目 0 在组 0 中,项目 1 在组 1 中,项目 2 在组 2 中,项目 3 在组 0 中,依此类推,总项目索引将是组内项目索引 (n) 乘以 3 加上组索引 (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 以相反的方向映射索引。它接收一个总项目索引,并返回该项目的组索引。

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;
}

就是这样。occurenceIndex 参数仅在您希望单个项目出现在多个组中时才有用。

子项控件

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

列表视图控件最令人恼火的限制之一可能是无法编辑子项。通常一个简单的文本框就足够了,但子项控件功能更强大。子项控件是一个 COM 类,它实现了一组接口,提供子项的可视化表示,以及用于就地编辑子项的用户界面。Windows 资源管理器使用可视化部分来显示驱动器的已用空间,并使用一串星号来显示照片和歌曲的评分。从 Windows 7 开始,此评分也使用子项控件的就地编辑部分:您可以将鼠标移到星号上,然后通过点击星号来更改评分。

在本文中,我将专注于如何使用子项控件,而不是如何实现它们。我们将利用 Windows Shell 中实现的子项控件。子项控件 API 与 Shell 紧密相关。

您现在应该已经熟悉 IListView 接口了。我们将使用它的 SetSubItemCallback 方法,该方法将 ISubItemCallback 接口的实现绑定到列表视图。所以第一个新接口是 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;
};

我们需要实现这个接口,那么这些方法都是用来做什么的呢?

  • GetSubItemTitle 仅在扩展磁贴视图中调用。它检索要在子项值前面显示的文本。
  • GetSubItemControl 被调用以检索特定子项的子项控件。它可能请求两个接口的实现:
    • IDrawPropertyControl ({E6DFF6FD-BCD5-4162-9C65-A3B18C616FDB})
    • 仅 Windows 10:一个未知的接口,IID 为 {1572DD51-443C-44B0-ACE4-38A005FC697E}
    Shell 的子项控件实现了这些接口,因此我将不再详细介绍。
  • BeginSubItemEdit 在用户想要开始就地编辑子项时被调用。mode 参数指定了触发调用的用户操作:
    • 0 - 用户将鼠标移到了子项上。是否启动编辑模式取决于子项控件的实现。
    • 1 - 用户点击了子项。
    请求的对象必须实现 IPropertyControl 接口({5E82A4DD-9561-476A-8634-1BEBACBA4A38})。
  • EndSubItemEdit 在退出特定子项的编辑模式时被调用。这是应该持久化用户输入值的地方。mode 参数的含义与 BeginSubItemEdit 相同。
  • BeginGroupEditEndGroupEdit。是的,组标签也可以编辑,但我不会在这里讨论这个话题。这些方法的使用与子项的使用方式基本相同。
  • 当子项控件注册了用户操作时,会调用 OnInvokeVerb。例如,如果子项控件显示一个超链接并且用户点击了该链接,那么 OnInvokeVerb 将被调用,其中 pVerb 参数包含链接的 ID。

GetSubItemControlBeginSubItemEdit 的实现通常非常相似,所以我们将从 GetSubItemControl 将调用转发给 BeginSubItemEdit

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

因此,要使列表视图显示子项控件并允许进入编辑模式,只需要 BeginSubItemEdit。稍后我们将介绍如何持久化编辑后的子项。

BeginSubItemEdit 中,我们首先必须决定使用哪个子项控件来处理给定的列表视图子项。Windows Shell 的属性系统实现了以下子项控件(可能还有更多),可以通过调用 CoCreateInstance 来实例化:

  • CLSID_CBooleanControl ({1E8F0D70-7399-41BF-8598-7949A2DEC898}) - 显示一个下拉列表,用于在“是”和“否”之间选择。
  • CLSID_CCustomDrawMultiValuePropertyControl ({e2183960-9d58-4e9c-878a-4acc06ca564a}) - 为了完整性而提及,我从未成功地使这个子项控件工作。
  • CLSID_CCustomDrawPercentFullControl ({AB517586-73CF-489c-8D8C-5AE0EAD0613A}) - Windows 资源管理器用于可视化驱动器填充级别的控件。
  • CLSID_CCustomDrawProgressControl ({0d81ea0d-13bf-44b2-af1c-fcdf6be7927c}) - 为了完整性而提及,我从未成功地使这个子项控件工作。
  • CLSID_CHyperlinkControl ({15756be1-a4ad-449c-b576-df3df0e068d3}) - 显示一个用户可以点击的超链接。
  • CLSID_CIconListControl ({53a01e9d-61cc-4cb0-83b1-31bc8df63156}) - 为了完整性而提及,我从未成功地使这个子项控件工作。
  • CLSID_CInPlaceCalendarControl ({6A205B57-2567-4a2c-B881-F787FAB579A3}) - 显示一个日期时间选择控件,用于选择日期。
  • CLSID_CInPlaceDropListComboControl ({0EEA25CC-4362-4a12-850B-86EE61B0D3EB}) - 显示一个下拉列表,用于选择值。
  • CLSID_CInPlaceEditBoxControl ({A9CF0EAE-901A-4739-A481-E35B73E47F6D}) - 显示一个单行文本框。
  • CLSID_CInPlaceMLEditBoxControl ({8EE97210-FD1F-4b19-91DA-67914005F020}) - 显示一个多行文本框。
  • CLSID_CInPlaceMultiValuePropertyControl ({8e85d0ce-deaf-4ea1-9410-fd1a2105ceb5}) - 为了完整性而提及,我从未成功地使这个子项控件工作。
  • CLSID_CRatingControl ({85e94d25-0712-47ed-8cde-b0971177c6a1}) - 显示五个星号来评分项目。
  • CLSID_CStaticPropertyControl ({527c9a9b-b9a2-44b0-84f9-f0dc11c2bcfb}) - 将子项的值显示为纯文本。

一些子项控件,例如显示下拉列表的控件,需要一个可能的显示值列表。此列表必须由实现 IPropertyDescription 接口的对象提供,该接口在 MSDN 上有文档。在本文中,我们仅显示 Windows 属性系统的一些属性,并使用系统的 IPropertyDescription 实现。

所以,这是我们 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;
}

接下来,我们需要配置子项控件,即设置当前值、文本颜色和字体,以及要使用的窗口主题。要设置子项的当前值,我们需要一个实现 IPropertyValue 接口的对象(参见 MSDN)。我们稍后会回到这个实现,先完成 BeginSubItemEdit

  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;
  }

为了让列表视图使用我们的 ISubItemCallback 实现,我们需要调用 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();
  }

为了持久化子项的编辑,我们需要实现 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();
}

最后缺失的部分是 IPropertyValue 的实现,这非常简单。该类需要一个 PROPERTYKEY 成员和一个 PROPVARIANT 成员。SetPropertyKeyGetPropertyKey 方法设置和获取 PROPERTYKEY 成员。InitValueGetValue 方法使用 PropVariantCopy 设置和获取 PROPVARIANT 成员。

示例项目还包括将所有这些与 LVN_GETDISPINFO 通知结合的代码,以便按需加载子项控件。

关注点

列表视图控件实现了比本文提到的更多的 COM 接口。以下是它们的列表:

致谢

再次非常感谢 Geoff Chappell。没有他的工作,本文就不会存在。还要感谢SpeedCommander 的作者 Sven Ritter,他通知我,不仅 IListView 的 IID 在 Windows 7 中发生了变化,其定义也发生了变化。

历史

  • v1.3 - 2015 年 9 月 30 日
    • 添加了关于子项控件的章节。
  • v1.2 - 2009 年 5 月 26 日
    • 通过修复 Windows 7 的 IListView 定义,使子集化分组在 Windows 7 上工作。
  • v1.1 - 2009 年 4 月 17 日
    • 为 Windows 7 添加了 IID_IListView 的定义。
    • 更新了 Windows 7 的演示项目。
  • v1.0 - 2009 年 4 月 8 日
    • Created
© . All rights reserved.