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
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.
Sub-Item Controls
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 , REFIID requiredInterface, LPVOID* ppObject)
{
if(!ppObject) {
return E_POINTER;
}
if(subItemIndex != 1) {
return E_NOINTERFACE;
}
HRESULT hr = E_NOINTERFACE;
CComPtr<IPropertyDescription> pPropertyDescription = NULL;
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:
hr = CoCreateInstance(CLSID_CCustomDrawPercentFullControl, NULL,
CLSCTX_INPROC_SERVER, requiredInterface, ppObject);
break;
case 2:
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:
hr = CoCreateInstance(CLSID_CBooleanControl, NULL,
CLSCTX_INPROC_SERVER, requiredInterface, ppObject);
PSGetPropertyDescriptionByName(L"System.Generic.Boolean",
IID_PPV_ARGS(&pPropertyDescription));
break;
case 5:
hr = CoCreateInstance(CLSID_CInPlaceCalendarControl, NULL,
CLSCTX_INPROC_SERVER, requiredInterface, ppObject);
PSGetPropertyDescriptionByName(L"System.Generic.DateTime",
IID_PPV_ARGS(&pPropertyDescription));
break;
case 6:
hr = CoCreateInstance(CLSID_CInPlaceDropListComboControl, NULL,
CLSCTX_INPROC_SERVER, requiredInterface, ppObject);
PSGetPropertyDescriptionByName(L"System.Photo.MeteringMode",
IID_PPV_ARGS(&pPropertyDescription));
break;
case 7:
hr = CoCreateInstance(CLSID_CHyperlinkControl, NULL,
CLSCTX_INPROC_SERVER, requiredInterface, ppObject);
break;
}
if(SUCCEEDED(hr)) {
}
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);
}
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;
}
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 , 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