windows读取文件摘要元数据应使用shellexecuteex配合ishellitem2::getpropertystore,需先coinitializeex初始化com、用utf-16路径调用shcreateitemfromparsingname获取ishellitem2,再通过pkey_*键查询ipropertystore,注意propvariant类型分支处理及空值判断。

Windows 上用 ShellExecuteEx + IShellItem2::GetPropertyStore 读取文件摘要元数据
Windows 资源管理器右键“属性→详细信息”里显示的作者、标题、标签、拍摄日期等,底层靠的是 Windows Property System,不是传统文件系统属性,也不是 EXIF 或 ID3 的简单封装。直接读取需要走 COM 接口,IShellItem2::GetPropertyStore 是最稳定、兼容性最好的路径(比过时的 IPropertySetStorage 或手动解析格式可靠得多)。
常见错误是试图用 GetFileAttributes 或读取文件头硬解——这些完全拿不到“摘要信息”面板里的字段;也有人误用 SHGetPropertyStoreFromIDList,但它只支持 Shell 命名空间路径(如桌面、库),对普通磁盘路径会失败。
- 必须初始化 COM:调用
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED),否则IShellItem2获取失败 - 路径要用 UTF-16:
std::wstring,不能传std::string给SHCreateItemFromParsingName - 获取
IPropertyStore后,需用PKEY_Title、PKEY_Author等预定义 property key 查询,不是字符串名称 - 返回值可能是
PROPVARIANT,类型不固定(VT_LPWSTR、VT_FILETIME、VT_I4都可能),必须按vt字段分支处理
PKEY_* 常用键和对应 C++ 类型转换
Windows SDK 定义了上百个 PKEY_*,但摘要页常用就十几个。它们本质是 PROPERTYKEY 结构,不能当字符串用;且部分键在不同文件类型下返回 TYPEDEF 不同(比如 PKEY_DateTaken 对 JPG 是 VT_FILETIME,对 MP4 可能是 VT_LPWSTR)。
-
PKEY_Title→VT_LPWSTR,直接取pPropVar->pwszVal -
PKEY_Author→ 多值可能(逗号分隔),仍为VT_LPWSTR,需自行切分 -
PKEY_DateTaken→ 优先检查VT_FILETIME,再 fallback 到VT_LPWSTR(某些视频容器存的是字符串格式时间) -
PKEY_Keywords→VT_VECTOR | VT_LPWSTR,得用PropVariantGetVectorElements提取字符串数组 - 未设置的字段会返回
TYPEDEF为VT_EMPTY或VT_NULL,不判空直接访问pwszVal会崩溃
为什么不用 IMediaDet 或第三方库(如 Exiv2、TagLib)
这些方案看似省事,但和资源管理器显示不一致:一是 IMediaDet 已废弃,Win10+ 不保证工作;二是 Exiv2/TagLib 只解析媒体内嵌元数据(EXIF、XMP、ID3),而 Windows 属性面板还会合并 NTFS 替换流(Alternate Data Stream)、Shell 缓存(thumbcache_*.db)、甚至 OneDrive 云端标记——这些 IShellItem2 全部透明聚合了。
- TagLib 读不到用户在资源管理器里手动填的“标题”或“标签”,因为那是写进
Desktop.ini或 NTFS 属性流的,不在媒体帧里 - 用
SHGetPropertyStoreFromSearchResults可以读搜索结果的聚合属性,但普通文件必须走IShellItem2 - 性能上,首次访问某文件的属性 store 会有轻微延迟(触发 Shell 解析),后续调用极快——这是 Windows 缓存机制,无法绕过,也不该绕过
最小可运行示例的关键片段
以下不是完整工程,只展示核心链路和易错点:
// 必须先 CoInitializeEx
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
CComPtr<ishellitem> spItem;
HRESULT hr = SHCreateItemFromParsingName(L"C:\photo.jpg", nullptr, IID_IShellItem, (void**)&spItem);
if (SUCCEEDED(hr)) {
CComPtr<ishellitem2> spItem2;
hr = spItem->QueryInterface(IID_IShellItem2, (void**)&spItem2);
if (SUCCEEDED(hr)) {
CComPtr<ipropertystore> spStore;
hr = spItem2->GetPropertyStore(GPS_DEFAULT, IID_IPropertyStore, (void**)&spStore);
if (SUCCEEDED(hr)) {
PROPVARIANT propVar;
PropVariantInit(&propVar);
// 注意:PKEY_Title 是宏,展开为 PROPERTYKEY 结构
hr = spStore->GetValue(PKEY_Title, &propVar);
if (SUCCEEDED(hr) && propVar.vt == VT_LPWSTR && propVar.pwszVal) {
std::wcout
<p>漏掉 <code>PropVariantClear</code> 或没检查 <code>propVar.vt</code> 就用 <code>pwszVal</code>,是 crash 最高频原因;另外,所有 COM 指针必须用 <code>CComPtr</code> 或手动 <code>Release()</code>,裸指针容易悬垂。</p>
<p>真正难的不是调通接口,而是理解 Windows 属性系统是多层拼接的:文件自身元数据 + NTFS 属性流 + Shell 缓存 + 用户手动编辑项。想让程序行为和资源管理器完全一致,就得走它走的路,而不是另起炉灶解析格式。</p></ipropertystore></ishellitem2></ishellitem>C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











