Skip to content

fafalone/ListViewSubItemControls

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ListViewSubItemControls v1.1

Undocumented ListView SubItem Controls Demo

ScreenShot

This project has been my white whale. Back in 2015 I started a series of articles on undocumented ListView features available in Windows Vista+: Footer Items, Subsetted Groups, Groups in Virtual Mode, Column Backcolors, and Explorer-style selection. But the coolest undocumented feature of all was the automatic subitem controls shown in the picture above. I just could not get it. The work was based on a fully working project by Timo Kunze, but even though I had this C++ sample that worked, every effort to port it to VB6 failed. Weeks were spent on it. Then at least half a dozen major efforts over the following decade of a few days. Not willing to give it up, I tried again starting 2 days ago, only this time instead of trying to fix the giant mess of spaghetti code packed with debugging stuff and the remnants of numerous different approaches, I started over completely from scratch and tried to make the port as line-by-line identical as possible...

🥳🥳 ** IT WORKED ** 🥳🥳

I'll no doubt be digging into the old code to find out exactly what I could have possibly missed in all the other failed attempts, which only ever got as far as glitched rendering of one or two controls followed by a hard crash. But the bottom line is now every control is working perfectly! In both 32 and 64bit! In future versions I'll explore the control types not used in Timo's demo.

Requirements

  • Windows 7+ (Vista+ could be supported by switching the IListView version, but it's not done here in v1.0).
  • Windows Development Library for twinBASIC v9.1+
  • Common Controls 6.0 enabled by manifest

Updates

v1.1 (22 Jun 2025) Now supports toggling to Tiles view to show how they work great there too: image

How it works

This technique is based around the undocumented ISubItemCallback interface:

[InterfaceId("11A66240-5489-42C2-AEBF-286FC831524C")]
[OleAutomation(False)]
Interface ISubItemCallback Extends stdole.IUnknown
    Sub GetSubItemTitle(ByVal subitemIndex As Long, ByVal lpszBuffer As LongPtr, ByVal BufferSize As Long)
    Sub GetSubItemControl(ByVal itemIndex As Long, ByVal subItemIndex As Long, requiredInterface As UUID, ppObject As Any)
    Sub BeginSubItemEdit(ByVal itemIndex As Long, ByVal subItemIndex As Long, ByVal mode As Long, requiredInterface As UUID, ppObject As Any)
    Sub EndSubItemEdit(ByVal itemIndex As Long, ByVal subItemIndex As Long, ByVal mode As Long, ByVal ppc As IPropertyControl)
    Sub BeginGroupEdit(ByVal groupIndex As Long, requiredInterface As UUID, ppObject As Any)
    Sub EndGroupEdit(ByVal groupIndex As Long, ByVal mode As Long, ByVal pPropertyControl As IPropertyControl)
    Sub OnInvokeVerb(ByVal itemIndex As Long, ByVal pVerb As LongPtr)
End Interface

We implement in our Form then set it as the callback object via IListView's SetSubItemCallback method. The initial values of the controls we just set by the subitem text of the listview items, e.g. 46 for 46% on the percent bar, or the numeric value of a FILETIME for the Date/Time control. The control in the picture that says 'Center weighted average' is actually a EXIF property the shell displays for photos... it automatically populates the values just by giving it an IPropertyDescription for System.Photo.MeteringMode, and we just provide an index.

The BeginSubItemEdit and identically handled GetSubItemControl callback methods are where the interesting part happens. This is a difficult interface to use with Implements because these take void** (As Any) arguments. twinBASIC lets us implement these methods as-is by using ByRef LongPtr; the key is we use CoCreateInstance to create an object for these controls on the argument, only when the subitemIndex is 1.

        Select Case itemIndex
            Case 0
                hr = CoCreateInstance(CLSID_CInPlaceMLEditBoxControl, Nothing, CLSCTX_INPROC_SERVER, requiredInterface, ppObject)
                If pPropertyDescription Is Nothing Then
                    PSGetPropertyDescriptionByName("System.Generic.String", IID_IPropertyDescription, pPropertyDescription)
                End If
            Case 1
                hr = CoCreateInstance(CLSID_CCustomDrawPercentFullControl, Nothing, CLSCTX_INPROC_SERVER, requiredInterface, ppObject)
            Case 2
                hr = CoCreateInstance(CLSID_CRatingControl, Nothing, CLSCTX_INPROC_SERVER, requiredInterface, ppObject)
            Case 3
                If IsEqualIID(requiredInterface, IID_IDrawPropertyControl) Then
                    hr = CoCreateInstance(CLSID_CStaticPropertyControl, Nothing, CLSCTX_INPROC_SERVER, requiredInterface, ppObject)
                Else
                    hr = CoCreateInstance(CLSID_CInPlaceEditBoxControl, Nothing, CLSCTX_INPROC_SERVER, requiredInterface, ppObject)
                End If
                If pPropertyDescription Is Nothing Then
                    PSGetPropertyDescriptionByName("System.Generic.String", IID_IPropertyDescription, pPropertyDescription)
                End If

etc.

After that there's some voodoo regarding visual styles I don't fully understand; I think it's just getting the strings for the window themes from the GetProp accessible locations they're stored in; but Timo doesn't explain why we don't just pass "explorer" like the main ListView control. Finally, and this is where I think earlier efforts went wrong, is the method for storing and editing values with IPropertyValue. I didn't want tB doing anything behind my back, so almost everything there now uses a manually defined UDT version of PROPVARIANT. We have a class that implements it, and we populate it with the text values of the subitem coerced into their proper PROPVARIANT data type; VT_LPWSTR types are an epic pain and this time around I think battle-tested helpers I had for it made a difference.

            Dim pBuffer As LongPtr = HeapAlloc(GetProcessHeap(), 0, (1024 + 1) * 2 /* sizeof(WCHAR) */)
            If pBuffer Then
                Dim item As LVITEMW
                item.iSubItem = subItemIndex
                item.cchTextMax = 1024
                item.pszText = pBuffer
                SendMessage(hLV, LVM_GETITEMTEXTW, itemIndex, item)
                If (itemIndex = 1) Or (itemIndex = 2) Or (itemIndex = 7) Then
                    Dim tmp As Variant
                    PropVariantInit(tmp)
                    InitPropVariantFromString(item.pszText, tmp)
                    PropVariantChangeType(ByVal pPropertyValue, tmp, 0, VT_UI4)
                    PropVariantClear(tmp)
...
            Dim pPropertyValueObj As IPropertyValue
            Set pPropertyValueObj = New IPropertyValueImpl
            pPropertyValueObj.InitValue(propertyValue)
            If pPropertyDescription IsNot Nothing Then
                pControl.Initialize(pPropertyDescription, 0)
            End If
            pControl.SetValue(pPropertyValueObj)
            pControl.SetTextColor(textColor)
            If hFont Then
                pControl.SetFont(hFont)
            End If

etc.

When the values are changed through the control, it sends the EndSubItemEdit and we take the IPropertyValue interface and turn it back into a String we store as the item text:

    Private Sub ISubItemCallback_EndSubItemEdit(ByVal itemIndex As Long, ByVal subItemIndex As Long, ByVal mode As Long, ByVal ppc As IPropertyControl) Implements ISubItemCallback.EndSubItemEdit
...
        Dim modified As BOOL
        ppc.IsModified(modified)
        If modified Then
            Dim pPropertyValue As IPropertyValue
            ppc.GetValue(IID_IPropertyValue, pPropertyValue)
            If SUCCEEDED(Err.LastHresult) Then
                Dim propertyValue As PROPVARIANT
                PropVariantInit(propertyValue)
                pPropertyValue.GetValue(propertyValue)
                If SUCCEEDED(Err.LastHresult) Then
                    Dim pBuffer As LongPtr
                    If SUCCEEDED(PropVariantToStringAlloc(propertyValue, pBuffer)) AndAlso (pBuffer <> 0) Then
                        LVSetItemText(itemIndex, subItemIndex, pBuffer)
                        CoTaskMemFree(pBuffer)
                    End If
                    PropVariantClear(propertyValue)
                End If
            End If
        End If

So that's the broad strokes. There's obviously a ton of details I've left out, so grab the code and dig in! Later this summer I hope to bring these controls to ucShellBrowse, as they're far more stable than my current method of creating a bunch of new windows and drawing the stars myself in WM_PAINT handlers. 😄

Releases

No releases published