-
Oct 26th, 2015, 08:21 PM
#1
[VB6, Vista+] A compact function to retrieve any property by name, locally formatted
This is related to the greatly expanded property system available in Vista+, and is closely related to the more complete tour of the system in my other projects.
While this method is inefficient and shouldn't be used for large numbers of properties or large numbers of files*, if you just need a few specific properties from a single file this method is a quick way to get them. The results appear as they do in Explorer's Details view; according to your locale, with units, etc. The key shortcut here is the SHGetPropertyStoreFromParsingName function and other PS_ APIs, which let us skip over all the IShellItem interface work.
Requirements
-Windows Vista or higher
-oleexp 2.0 or higher (no new release related to this code)
Usage
After putting the below code in a module, just call the GetPropertyDisplayString(file, property) function, it will return a string with the property as it appears in Explorer. For example, System.Dimensions on a JPG file might return "640 x 480", or System.Width as "100 pixels"; or an AVI's System.Length as "01:30:20". It's more than just raw numbers (although those can be retrieved too; see the larger project).
sResult = GetPropertyDisplayString("C:\myfile.jpg", "System.Width")
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.
Code
Code:
Public Declare Function PSGetPropertyKeyFromName Lib "propsys.dll" (ByVal pszName As Long, ppropkey As PROPERTYKEY) 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 SHGetPropertyStoreFromParsingName Lib "shell32" (ByVal pszPath As Long, pbc As Any, ByVal Flags As GETPROPERTYSTOREFLAGS, riid As UUID, ppv As Any) As Long
Public Declare Function PSGetPropertyDescription Lib "propsys.dll" (PropKey As PROPERTYKEY, riid As UUID, ppv As Any) As Long
Public Declare Function SysReAllocString Lib "oleaut32.dll" (ByVal pBSTR As Long, Optional ByVal pszStrPtr As Long) As Long
Public Declare Sub CoTaskMemFree Lib "ole32.dll" (ByVal PV As Long) ' Frees memory allocated by the shell
Public Function GetPropertyDisplayString(szFile As String, szProp As String) As String
'Gets the string value of the given canonical property; e.g. System.Company, System.Rating, etc
'This would be the value displayed in Explorer if you added the column in details view
Dim pkProp As PROPERTYKEY
Dim pps As IPropertyStore
Dim lpsz As Long
Dim ppd As IPropertyDescription
PSGetPropertyKeyFromName StrPtr(szProp), pkProp
SHGetPropertyStoreFromParsingName StrPtr(szFile), ByVal 0&, GPS_DEFAULT Or GPS_BESTEFFORT Or GPS_OPENSLOWITEM, IID_IPropertyStore, pps
PSGetPropertyDescription pkProp, IID_IPropertyDescription, ppd
PSFormatPropertyValue ObjPtr(pps), ObjPtr(ppd), PDFF_DEFAULT, lpsz
SysReAllocString VarPtr(GetPropertyDisplayString), lpsz
CoTaskMemFree lpsz
End Function
Include the following in your module only if you're not using the mIID.bas module from the oleexp thread:
Code:
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 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 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
ALTERNATIVE: Get directly by PROPERTYKEY
Now that I've published a complete list of PROPERTYKEY's from propkey.h, if you include the mPKEY.bas module from the oleexp project, you can use those directly like this:
Code:
Public Function GetPropertyKeyDisplayString(szFile As String, pkProp As PROPERTYKEY) As String
Dim pps As IPropertyStore
Dim lpsz As Long
Dim ppd As IPropertyDescription
SHGetPropertyStoreFromParsingName StrPtr(szFile), ByVal 0&, GPS_DEFAULT Or GPS_BESTEFFORT Or GPS_OPENSLOWITEM, IID_IPropertyStore, pps
PSGetPropertyDescription pkProp, IID_IPropertyDescription, ppd
PSFormatPropertyValue ObjPtr(pps), ObjPtr(ppd), PDFF_DEFAULT, lpsz
SysReAllocString VarPtr(GetPropertyKeyDisplayString), lpsz
CoTaskMemFree lpsz
End Function
Common Properties
For a full list of system properties, see propkey.h in the SDK (or unofficial copies online); or the larger projects I have that will enumerate them all.
Otherwise, see the MSDN post Metadata Properties for Media Files for the popular ones.
-------------------------------
* - When working with large numbers of files, or user-selectable properties, it's best to implement IShellItem and IPropertySystem based solutions from the ground up.
Last edited by fafalone; Dec 5th, 2015 at 02:48 AM.
-
Mar 31st, 2017, 03:57 AM
#2
Junior Member
Re: [VB6, Vista+] A compact function to retrieve any property by name, locally format
Just to mention...
This function can be done without any references using the Shell object:
Code:
Function GetPropertyDisplayString2(szFile As String, szProp As String) As String
Dim sFld As String, sFile As String
Dim oSH As Object 'Shell32.Shell
Dim oFld As Object 'Shell32.Folder3
Dim oItm As Object 'Shell32.ShellFolderItem
Set oSH = CreateObject("Shell.Application")
sFld = Left(szFile, InStrRev(szFile, "\") - 1)
sFile = Mid(szFile, InStrRev(szFile, "\") + 1)
Set oFld = oSH.NameSpace(CStr(sFld))
Set oItm = oFld.Items.Item(CStr(sFile))
GetPropertyDisplayString2 = oItm.ExtendedProperty(szProp)
End Function
But honors to you for the oleexp library which I just came across! Really great!
-
Apr 1st, 2017, 08:44 PM
#3
Re: [VB6, Vista+] A compact function to retrieve any property by name, locally format
Well apart from that being far less fun, there's a few actual differences... one biggie being that it's read-only; using the property system directly can alter some items, and the other being integration with the larger shell API set. Like say the file you're interested in is picked by the user with a common dialog (the new, non-deprecated IFileDialog); that returns an IShellItem with which you can work with the property system immediately instead of resolving the path then passing off (in addition to being able to work with virtual items you can't resolve to a path, making shell.application impossible). This applies more to other ways accessing the property system than the shortcut function here tho; some of my other examples show that and more extensive tasks that the shell object you're using can't do (like enumerate all properties). As always, my work is all about when basic stuff isn't good enough and you really want to dig into advanced shell stuff, or just like doing things the hard way
-
Jan 26th, 2018, 10:52 AM
#4
Hyperactive Member
Re: [VB6, Vista+] A compact function to retrieve any property by name, locally format
Deleted since I see the answer is listed in the original post...
Last edited by AAraya; Jan 26th, 2018 at 03:49 PM.
-
Jan 26th, 2018, 03:21 PM
#5
Hyperactive Member
Re: [VB6, Vista+] A compact function to retrieve any property by name, locally format
Two further questions:
1) I believe that this solution is going to be limited to files whose path lengths do not exceed MAX_PATH? Shell API does not support long file names. Correct?
2) Are unicode folder/file paths supported by the Shell automation approach suggested by SashaT? I would think not since we're passing the file as a string rather than as a pointer to the string...
-
Jan 30th, 2018, 02:25 AM
#6
Re: [VB6, Vista+] A compact function to retrieve any property by name, locally format
1) Yes all shell APIs and features (including property stores) are limited to MAX_PATH. If you have Windows 10 Redstone 2, Build 15063 (Creators Update), you can apparently create an IShellItem for files in long paths, but I don't know if you could open a property store for it.
2) My guess is no but can't hurt to test, I'll check it out tomorrow.
-
May 11th, 2018, 04:28 PM
#7
Hyperactive Member
Re: [VB6, Vista+] A compact function to retrieve any property by name, locally format
When I include the OLEEXP type lib in my project, I get random crashes and general instability. I'm guessing it's because I've already got many of the same APIs, types, constants declared in this large project but that there are some differences between the two?
I am interested in the property store API however.
Can this be used without a type lib?
-
May 12th, 2018, 06:18 PM
#8
Re: [VB6, Vista+] A compact function to retrieve any property by name, locally format
Originally Posted by AAraya
2) Are unicode folder/file paths supported by the Shell automation approach suggested by SashaT? I would think not since we're passing the file as a string rather than as a pointer to the string...
Yes, full Unicode. This is not like using the crippled Declare Function approach to making flat DLL calls.
-
May 13th, 2018, 02:13 AM
#9
Re: [VB6, Vista+] A compact function to retrieve any property by name, locally format
Originally Posted by AAraya
When I include the OLEEXP type lib in my project, I get random crashes and general instability. I'm guessing it's because I've already got many of the same APIs, types, constants declared in this large project but that there are some differences between the two?
I am interested in the property store API however.
Can this be used without a type lib?
Can you give an example of what's causing issues?
Preference is given for locally declared APIs and Types... if they're defined in your project, the oleexp version can only be used if you explicitly type it that way (Dim x As oleexp.CommonType or Call oleexp.CommonAPI). Only other time would be if it was defined in a different external reference that was lower in priority (the order they appear in References)*. Even in cases of such conflicts, if it's the wrong type you get a wrong type error, not a crash. And the entire typelib isn't loaded; only what's used, which is why it only adds a few bytes to an exe, so oleexp can't effect anything that's not directly calling it.
There are some particular interfaces that have stability issues; though not any of the ones relating to this thread AFAIK.
If there is an issue, I'd definitely like to fix it if you could provide more detail.
While you could use write a class to do all the property store stuff without a typelib, it's very complicated stuff; have a look at e.g. LaVolpe's IFileDialog project to see how you can use the interface vtable without a typelib. In addition to the complexity, you lose the ability to just have typed variables to pass around to interfaces and APIs, making overall design much more complicated.
* - If your project uses other typelibs with public Windows APIs or Windows system COM interfaces, like OLEGuids or ISHFl_Ex, make sure a newly added oleexp is last in that list so that its definitions don't supercede the other TLB. And never have olelib as a reference-- this project is a fork of that, so if you do, just remove that reference as oleexp contains everything in it. Same for ISHFl_Ex; that is all duplicates as well.
Last edited by fafalone; May 13th, 2018 at 02:29 AM.
-
May 13th, 2018, 08:09 AM
#10
Re: [VB6, Vista+] A compact function to retrieve any property by name, locally format
Originally Posted by fafalone
... Only other time would be if it was defined in a different external reference that was lower in priority (the order they appear in References)*. Even in cases of such conflicts, if it's the wrong type you get a wrong type error, not a crash.
One reason for such a crash in that scenario is ByRef, ByVal differences. Passing ByVal when ByRef is expected and vice versa is a good recipe for crashes and "general instability".
Maybe, just maybe this could be a generic issue. Users must know what is expected from the TypeLib and API parameters. Sometimes we are so used to a specific API's parameter passed a certain way, we don't notice that the code being added to a project modified the ByVal,ByRef portion of a parameter. Not that this is the case, but CopyMemory API is common culprit. Vast majority of time, you'll see the 1st two parameters of that API declared ByRef. But some will declare them ByVal. If you don't pay attention then crashes can happen. I know that specific case has bitten me a couple times testing OPC (other people's code)
Last edited by LaVolpe; May 13th, 2018 at 08:33 AM.
-
May 13th, 2018, 07:05 PM
#11
Re: [VB6, Vista+] A compact function to retrieve any property by name, locally format
Forgot about that but yeah it's definitely an issue; quite a few APIs differ in that manner. I never really liked the inclusion of APIs in the TLB, as I've been hit with that issue a lot because of code reuse (individual functions). But I didn't want to break backwards compatibility, so they stayed; I added a few, but almost all uncommon shell APIs.
CopyMemory doesn't appear in the TLB; the biggest one for this issue though might be SendMessage, which does. The TLB, and normal use, have the last parameter as ByRef Any, but I've seen ByVal Long too.
It's still subject to those APIs actually being called though; if there was a Private Declare for one in the form/mod/class, or a Public Declare anywhere in the current project, then the TLB version wouldn't be called. So just adding a TLB to an existing project shouldn't cause an issue as presumably the API not being defined would have caused an error already.
-
Jan 10th, 2021, 07:05 AM
#12
Junior Member
Re: [VB6, Vista+] A compact function to retrieve any property by name, locally format
Just because I am curious:
An alternative to the ShellFolderItem.ExtendedProperty method (up in #2) would be Folder3.GetDetailsOf ([ShellFolderItem], columnid) (https://docs.microsoft.com/en-us/win...r-getdetailsof)
Although there are mentioned just six Column-IDs you can indeed use higher integer values. E.g. for a video file use 285 to get the width of it, 283 to get the height. I found this out enumerating values in the range of 0-1000. Somewhere around 300 seems to be the upper limit.
Question: How does ColumnID relate to FMTID and PID in a PROPERTYKEY or SHCOLUMNID structure? I did not find any function to convert between these; tried alot. Also I am not sure if the columnids are fixed values or if they are system dependent calculated values - maybe stored in the registry. Any thoughts?
-
Jan 10th, 2021, 06:04 PM
#13
Re: [VB6, Vista+] A compact function to retrieve any property by name, locally format
One of the tradeoffs for a simpler experience using shell32 instead of the lower level things like IShellFolder is the higher level stuff doesn't include some of the more technical functionality. Column IDs are used to reference information that tells you which system property is displayed by the given column; to get the lower level information on that for a particular folder, the IShellFolder2 interface has MapColumnToSCID. SHCOLUMNID and PROPERTYKEY are the same thing; you can switch the values around without issue.
For example, if you use MapColumnToSCID for column 0 and 1 (assuming your IShellFolder2 represents a normal folder), and then look at the FMTID/PID of the resulting SHCOLUMNID, you'll see for column 0 it's {B725F130-47EF-101A-A5F1-02608C9EEBAC, 10}, and for column 1 it's {B725F130-47EF-101A-A5F1-02608C9EEBAC, 12} -- which are exactly equal to PKEY_ItemNameDisplay and PKEY_Size, respectively.
The values you get enumerating the IDs in GetDetailsOf can't be relied on to be constant. They represent different properties not only on different versions of Windows, but special folders will have their own sets of values; e.g. Computer/ThisPC on Win7 the first 6 are Name, Type, Total Size, Free Space, File System, Comments and Network Location. Fonts, Printers.. again, entirely different sets.
If you're going to be doing something complicated with the properties system, I strongly recommend avoiding the Shell32 ShellFolderItem/etc system all together and going straight to the IShellItem and IPropertyStore system. (Important to note also, if you need support for Unicode, and/or the ability to write properties where applicable, the shell32.dll objects don't support these two things at all).
To describe this on an even lower level... the ColumnID represents an index in the Column Manager for a given folder. This is how I process columns in my shell browser. You can request an IColumnManager interface for a given folder and enumerate the columns in two ways, first, the default set, which you see when you open it in Explorer, and second, all of the supported columns. This has the advantage of including custom columns supported only by that folder, which won't appear elsewhere.
But in any case, IColumnManager returns an enumeration in the form of an array of PROPERTYKEYs, and the column IDs are indexes to this.
Consider the following code,
Code:
Private Sub EnumProps()
Dim siItem As IShellItem
Dim psv As oleexp.IShellView
Dim pfv As oleexp.IFolderView
Dim pColMgr As oleexp.IColumnManager
Dim nCol As Long, nMax As Long
Dim lpNm As Long
Dim pkCols() As PROPERTYKEY
SHCreateItemFromParsingName StrPtr("C:\"), Nothing, IID_IShellItem, siItem
siItem.BindToHandler 0&, BHID_SFViewObject, IID_IShellView, psv
If (psv Is Nothing) = False Then
Set pfv = psv
If (pfv Is Nothing) = False Then
Set pColMgr = pfv
If (pColMgr Is Nothing) = False Then
pColMgr.GetColumnCount CM_ENUM_ALL, nMax
ReDim pkCols(nMax - 1)
pColMgr.GetColumns CM_ENUM_ALL, pkCols(0), nMax
For nCol = 0 To 10 'Max=UBound(pkCols)
PSGetNameFromPropertyKey pkCols(nCol), lpNm
Debug.Print "Property(" & nCol & ")=" & LPWSTRtoStr(lpNm)
Next nCol
End If
End If
End If
End Sub
(The code above is applicable to standard folders only, to handle special folders with entirely custom columns, you'd need to get the name from pColMgr.GetColumnInfo, but that's unnecessarily complex for the overview here)
The first 10 items in the key list returned by the manager are:
Property(0)=System.ItemNameDisplay
Property(1)=System.DateModified
Property(2)=System.ItemTypeText
Property(3)=System.Size
Property(4)=System.DateCreated
Property(5)=System.Author
Property(6)=System.Keywords
Property(7)=System.Title
Property(8)=System.ItemType
Property(9)=System.DateAccessed
Property(10)=System.FileAttributes
and sure enough if we keep going, we find your video properties in the 280s
Property(280)=System.Video.Compression
Property(281)=System.Video.Director
Property(282)=System.Video.EncodingBitrate
Property(283)=System.Video.FrameHeight
Property(284)=System.Video.FrameRate
Property(285)=System.Video.FrameWidth
Property(286)=System.Video.TotalBitrate
Last edited by fafalone; Jan 10th, 2021 at 11:26 PM.
-
Jan 11th, 2021, 07:17 AM
#14
Junior Member
Re: [VB6, Vista+] A compact function to retrieve any property by name, locally format
Ah, thanx alot for the solution! So this means it cannot be retrieved without an IShellview interface as the columns rely on the specific folder representation.
Great!
-
Jan 11th, 2021, 06:37 PM
#15
Re: [VB6, Vista+] A compact function to retrieve any property by name, locally format
The top and bottom half of my post are two different ways of accomplishing the same goal; you can use IShellFolder2.GetDetailsOf and .GetDetailsEx without going into any of the lower level stuff in the 2nd half.
This code will produce the list, including the values of the properties, for a file in C:\
Code:
Dim i As Long
Dim lpNm As Long
Dim shc As SHCOLUMNID
Dim pk As PROPERTYKEY
Dim szc As String, szp As String
Dim vpv As Variant
Dim psfDesk As IShellFolder
Dim psf2 As IShellFolder2
Dim pidl As Long, pidlChild As Long, pidlRel As Long
Dim psfDir As IShellFolder
Set psfDesk = SHGetDesktopFolder()
pidl = ILCreateFromPathW(StrPtr("C:\"))
psfDesk.BindToObject pidl, 0&, IID_IShellFolder, psfDir
CoTaskMemFree pidl
pidlChild = ILCreateFromPathW(StrPtr("C:\procexp.exe"))
pidlRel = ILFindLastID(pidlChild)
Set psf2 = psfDir
For i = 0 To 10
psf2.MapColumnToSCID i, shc
psf2.GetDetailsEx pidlRel, shc, vpv
pk.fmtid = shc.fmtid
pk.pid = shc.pid
PSGetNameFromPropertyKey pk, lpNm
Debug.Print "Col(" & i & ")->" & LPWSTRtoStr(lpNm) & "=" & CStr(vpv)
Next i
CoTaskMemFree pidlChild
-
You'll generally want to enumerate the files rather than manually target a single specific file; you do that with the IShellFolder.EnumObjects function which provides an IEnumIDList object:
Code:
Dim pEnum As IEnumIDList, pcl As Long
psfDir.EnumObjects 0&, SHCONTF_NONFOLDERS Or SHCONTF_FOLDERS, pEnum
Do While pEnum.Next(1&, pidlRel, pcl) = S_OK
That's where your pidlRel for GetDetailsOf/Ex will come from, instead of the manual single file way with pidlChild I put in the code to keep it compact and clear)
Last edited by fafalone; Jan 11th, 2021 at 06:47 PM.
Posting Permissions
- You may not post new threads
- You may not post replies
- You may not post attachments
- You may not edit your posts
-
Forum Rules
|
Click Here to Expand Forum to Full Width
|