[VB6, Vista+] List all file properties, locale/unit formatted, by modern PROPERTYKEY
Previous VB6 methods for listing file properties haven't used the newer methods, which are especially handy if you're already working with IShellItem. This code is a tour of the modern property system, covering PROPERTYKEY, IPropertyStore, IPropertyDescription, and propsys.dll APIs to take raw values and format them according to the system locale; e.g. adding 'pixels' or 'dpi' to image properties, showing dates/times according to system settings, changing the unreadable number representing attributes into letters, etc. It also goes on to show the raw data, exposing an important method if you do need to work with PROPVARIANT in VB.
Note that the project does have the option to hide empty properties:
Requirements
-Requires oleexp 4.0 or higher (for IDE only, add reference to oleexp.tlb)
-mIID.bas (included with oleexp)
-Only works with Windows Vista and higher
UPDATE: (2015Dec05) - I think I would have heard about it if it was always the case; but on my system, office documents were counted as slow items even on the OS drive, and getting the IPropertyStore for them failed (0x80040154 Class not registered). This is resolved by adding flags to open slow items and always return a property store, even if some properties couldn't be loaded.
UPDATE: (2015Dec05-2) - I'm attaching a sample project that shows these properties in a ListView. The values are locally formatted, printed to the LV in Unicode, and the project shows how to load ALL (as opposed to some/most-- trickier than you think) properties into a management array, for not only showing files like the demo, but as an easy system to track Details columns in an Explorer-type file list. Project requirements- attached sample project
(the requirements for the sample code in this post haven't changed, but the attached project does things different and uses more recent things)
-Windows Vista or higher
-oleexp v4.0 or higher
-oleexp addon mIID.bas for IID_ entries (included with oleexp)
Code
Code:
Public Declare Function ILCreateFromPathW Lib "shell32" (ByVal pwszPath As Long) As Long
Public Declare Function SHCreateItemFromIDList Lib "shell32" (ByVal pidl As Long, riid As UUID, ppv As Any) As Long
Public Declare Function CoInitialize Lib "ole32.dll" (ByVal pvReserved As Long) As Long
Public Declare Sub CoTaskMemFree Lib "ole32.dll" (ByVal pv As Long) ' Frees memory allocated by the shell
Public Declare Function SysReAllocString Lib "oleaut32.dll" (ByVal pBSTR As Long, Optional ByVal pszStrPtr As Long) As Long
Public Declare Function PSGetNameFromPropertyKey Lib "propsys.dll" (PropKey As PROPERTYKEY, ppszCanonicalName As Long) As Long
Public Declare Function PSGetPropertyDescription Lib "propsys.dll" (PropKey As PROPERTYKEY, riid As UUID, ppv As Any) As Long
Public Declare Function PSFormatPropertyValue Lib "propsys.dll" (ByVal pps As Long, ByVal ppd As Long, ByVal pdff As PROPDESC_FORMAT_FLAGS, ppszDisplay As Long) As Long
Public Declare Function PropVariantToVariant Lib "propsys.dll" (ByRef propvar As Any, ByRef var As Variant) As Long
Public Sub EnumFileProperties(sPath As String)
'sPath can be a file or a folder. Other objects that you might want properties
'for, a slight re-work can be used to start from its pidl or IShellItem directly
Dim isif As IShellItem2
Dim pidlt As Long
Dim pProp As IPropertyDescription
Dim pk As PROPERTYKEY
Dim pPStore As IPropertyStore
Dim lpe As Long
Dim lpProp As Long
Dim i As Long, j As Long
Dim vProp As Variant
Dim vrProp As Variant
Dim vte As VbVarType
Dim sPrName As String
Dim sFmtProp As String
Call CoInitialize(0)
'Create a reference to IShellItem2
pidlt = ILCreateFromPathW(StrPtr(sPath))
Call SHCreateItemFromIDList(pidlt, IID_IShellItem2, isif)
Call CoTaskMemFree(pidlt)
If (isif Is Nothing) Then
Debug.Print "Failed to get IShellItem2"
Exit Sub
End If
'Get the IPropertyStore interface
isif.GetPropertyStore GPS_DEFAULT Or GPS_BESTEFFORT Or GPS_OPENSLOWITEM, IID_IPropertyStore, pPStore
If (pPStore Is Nothing) Then
Debug.Print "Failed to get IPropertyStore"
Exit Sub
End If
'Get the number of properties
pPStore.GetCount lpe
Debug.Print "Total number of properties=" & lpe
On Error GoTo eper
For i = 0 To (lpe - 1)
'Loop through each property; starting with information about which property we're working with
pPStore.GetAt i, pk
PSGetNameFromPropertyKey pk, lpProp
sPrName = BStrFromLPWStr(lpProp)
Debug.Print "Property Name=" & sPrName & ",SCID={" & Hex$(pk.fmtid.Data1) & "-" & Hex$(pk.fmtid.Data2) & "-" & Hex$(pk.fmtid.Data3) & "-" & Hex$(pk.fmtid.Data4(0)) & Hex$(pk.fmtid.Data4(1)) & "-" & Hex$(pk.fmtid.Data4(2)) & Hex$(pk.fmtid.Data4(3)) & Hex$(pk.fmtid.Data4(4)) & Hex$(pk.fmtid.Data4(5)) & Hex$(pk.fmtid.Data4(6)) & Hex$(pk.fmtid.Data4(7)) & "}, " & pk.pid
'Some properties don't return a name; if you don't catch that it leads to a full appcrash
If Len(sPrName) > 1 Then
'PSFormatPropertyValue takes the raw data and formats it according to the current locale
'Using these APIs lets us completely avoid dealing with PROPVARIANT, a huge bonus.
'If you don't need the raw data, this is all it takes
PSGetPropertyDescription pk, IID_IPropertyDescription, pProp
PSFormatPropertyValue ObjPtr(pPStore), ObjPtr(pProp), PDFF_DEFAULT, lpProp
sFmtProp = BStrFromLPWStr(lpProp)
Debug.Print "Formatted value=" & sFmtProp
Else
Debug.Print "Unknown Propkey; can't get formatted value"
End If
'Now we'll display the raw data
isif.GetProperty pk, vProp
PropVariantToVariant vProp, vrProp 'PROPVARIANT is exceptionally difficult to work with in VB, but at
'least for file properties this seems to work for most
vte = VarType(vrProp)
If (vte And vbArray) = vbArray Then 'this always seems to be vbString and vbArray, haven't encountered other types
For j = LBound(vrProp) To UBound(vrProp)
Debug.Print "Value(" & j & ")=" & CStr(vrProp(j))
Next j
Else
Select Case vte
Case vbDataObject, vbObject, vbUserDefinedType
Debug.Print "<cannot display this type>"
Case vbEmpty, vbNull
Debug.Print "<empty or null>"
Case vbError
Debug.Print "<vbError>"
Case Else
Debug.Print "Value=" & CStr(vrProp)
End Select
End If
Next i
Exit Sub
eper:
Debug.Print "Property conversion error->" & Err.Description
Resume Next
End Sub
'Supporting functions
Public Function IID_IShellItem2() As UUID
'7e9fb0d3-919f-4307-ab2e-9b1860310c93
Static IID As UUID
If (IID.Data1 = 0) Then Call DEFINE_UUID(IID, &H7E9FB0D3, CInt(&H919F), CInt(&H4307), &HAB, &H2E, &H9B, &H18, &H60, &H31, &HC, &H93)
IID_IShellItem2 = IID
End Function
Public Function IID_IPropertyDescription() As UUID
'(IID_IPropertyDescription, 0x6f79d558, 0x3e96, 0x4549, 0xa1,0xd1, 0x7d,0x75,0xd2,0x28,0x88,0x14
Static IID As UUID
If (IID.Data1 = 0) Then Call DEFINE_UUID(IID, &H6F79D558, CInt(&H3E96), CInt(&H4549), &HA1, &HD1, &H7D, &H75, &HD2, &H28, &H88, &H14)
IID_IPropertyDescription = IID
End Function
Public Function IID_IPropertyStore() As UUID
'DEFINE_GUID(IID_IPropertyStore,0x886d8eeb, 0x8cf2, 0x4446, 0x8d,0x02,0xcd,0xba,0x1d,0xbd,0xcf,0x99);
Static IID As UUID
If (IID.Data1 = 0) Then Call DEFINE_UUID(IID, &H886D8EEB, CInt(&H8CF2), CInt(&H4446), &H8D, &H2, &HCD, &HBA, &H1D, &HBD, &HCF, &H99)
IID_IPropertyStore = IID
End Function
Public Sub DEFINE_UUID(Name As UUID, L As Long, w1 As Integer, w2 As Integer, B0 As Byte, b1 As Byte, b2 As Byte, B3 As Byte, b4 As Byte, b5 As Byte, b6 As Byte, b7 As Byte)
With Name
.Data1 = L
.Data2 = w1
.Data3 = w2
.Data4(0) = B0
.Data4(1) = b1
.Data4(2) = b2
.Data4(3) = B3
.Data4(4) = b4
.Data4(5) = b5
.Data4(6) = b6
.Data4(7) = b7
End With
End Sub
Public Function BStrFromLPWStr(lpWStr As Long, Optional ByVal CleanupLPWStr As Boolean = True) As String
SysReAllocString VarPtr(BStrFromLPWStr), lpWStr
If CleanupLPWStr Then CoTaskMemFree lpWStr
End Function
Also, if your user is selecting which properties to display, which is still done by column IDs, you can map a column id to a PROPERTYKEY like this, where isfPar is the IShellFolder2 the properties are selected from:
Reminder: The attached project uses a slightly different method than the code in this post, and has a bunch of other features.
NOTE
If you're missing a property you can see in Explorer, like I noticed GPS longitude/latitude, they're not in the PDEF_VIEWABLE property enumeration this sample uses, which avoids properties that don't apply to files and lots of duplicates, but also some less common ones like GPS. So if you're looking for a missing property you saw in Explorer, at the bottom of the InitPKEYs function, change it to PDEF_SYSTEM and then if that doesn't have it, PDEF_ALL.
Last edited by fafalone; May 18th, 2018 at 04:49 AM.
Reason: Attached project updated to reference oleexp.tlb 4.0 or higher
Re: [VB6, Vista+] List all file properties, locale/unit formatted, by modern PROPERTY
As an addition, I mentioned how column select is still usually done with IShellFolder2, but there is a way to do it with the identical friendly names directly with the modern property system:
Code:
Public Declare Function PSGetPropertySystem Lib "propsys.dll" (riid As UUID, ppv As Any) As Long
Dim ppsys As IPropertySystem
PSGetPropertySystem IID_IPropertySystem, ppsys
ppsys.EnumeratePropertyDescriptions PDEF_ALL, IID_IPropertyDescriptionList, pPropList
pPropList.GetCount nProp
For i = 0 To (nProp - 1)
pPropList.GetAt i, IID_IPropertyDescription, pProp
pProp.GetDisplayName lpProp
sProp = BStrFromLPWStr(lpProp)
Debug.Print "Prop=" & sProp
pProp.GetCanonicalName lpProp
sProp = BStrFromLPWStr(lpProp)
Debug.Print "CanonicalName=" & sProp
Next i
Exit Sub
Public Function IID_IPropertySystem() As UUID
'IID_IPropertySystem, 0xca724e8a, 0xc3e6, 0x442b, 0x88,0xa4, 0x6f,0xb0,0xdb,0x80,0x35,0xa3
Static IID As UUID
If (IID.Data1 = 0) Then Call DEFINE_UUID(IID, &HCA724E8A, CInt(&HC3E6), CInt(&H442B), &H88, &HA4, &H6F, &HB0, &HDB, &H80, &H35, &HA3)
IID_IPropertySystem = IID
End Function
Public Function IID_IPropertyDescriptionList() As UUID
'IID_IPropertyDescriptionList, 0x1f9fc1d0, 0xc39b, 0x4b26, 0x81,0x7f, 0x01,0x19,0x67,0xd3,0x44,0x0e
Static IID As UUID
If (IID.Data1 = 0) Then Call DEFINE_UUID(IID, &H1F9FC1D0, CInt(&HC39B), CInt(&H4B26), &H81, &H7F, &H1, &H19, &H67, &HD3, &H44, &HE)
IID_IPropertyDescriptionList = IID
End Function
The first string displays the friendly name (e.g. 'Author'), and the canonical name is like 'System.Author'. You can get the actual PROPERTYKEY then with pProp.GetPropertyKey.
Last edited by fafalone; Jun 1st, 2015 at 11:00 AM.
Re: [VB6, Vista+] List all file properties, locale/unit formatted, by modern PROPERTY
There's quite a few PS__ APIs.. obviously there's using the enumeration above, but if you want to search by the canonical name, you can get a property key directly with PSGetPropertyKeyFromName. Were you thinking of searching by partial match? If so testing display/canonical name as you loop through them in the posted enum would be the easiest.
Searching by value... not sure what situation you'd have the value before the property you queried for it, but using IPropertyEnumTypeList seems a little awkward... it seems to derive only from IPropertyDescription, which describes a single property.
Edit: I won't have time to play around with it more until tonight, but if you or anyone else wants to, attached is a version of oleexp with IPropertyEnumType, IPropertyEnumType2, and IPropertyEnumTypeList added it. Edit: Removed; these interfaces are now in the main oleexp release.
Last edited by fafalone; Dec 5th, 2015 at 02:53 AM.
Re: [VB6, Vista+] List all file properties, locale/unit formatted, by modern PROPERTY
fafalone, your example is probably good enough. I wasn't familiar with the usage of those interfaces and when reading through this thread, the question I thought of was, "ok, cool, but how do I test to see if a property exists and what it's value is without enumerating"? Thought an answer to that question would be a useful addition to your thread. Thanx.
Insomnia is just a byproduct of, "It can't be done"
Re: [VB6, Vista+] List all file properties, locale/unit formatted, by modern PROPERTY
Office file bugfix: The code in this post and others relating to IPropertyStore has been updated. I'm not sure if it was every system since I hadn't heard of this bug before, but getting an IPropertyStore for Office files was failing with 0x80040154-Class not registered. For some reason, they were marked as "slow items", even when on the primary drive/partition. Changing GPS_DEFAULT to GPS_DEFAULT Or GPS_BESTEFFORT Or GPS_OPENSLOWITEM fixes this bug, and the inclusion of best effort should also eliminate any similar bugs as it means a store should always be returned, even if some properties can't be loaded.
---
PROJECT UPDATED: Added sample project that uses a slightly better method, has some extra features (most significantly showing things in a listview), and provides a property tracking array.
Last edited by fafalone; Dec 5th, 2015 at 09:20 AM.