拓冰建站拓冰建站
首页 / 资讯中心 / 正文

C++开发DWG缩略图Shell扩展:资源管理器预览实现指南

简介在Windows资源管理器与文件打开对话框中实现DWG图纸缩略图预览是不少CAD工具开发者的常见需求。该示例基于Visual C编写Shell扩展插件通过自定义缩略图提供程序实现上述效果同时涵盖驱动器控件、文件文件夹控件的使用适合需要扩展Windows文件管理交互的C开发者参考。压缩包共包含19个文件以7个h头文件和6个cpp源文件为主另有rc资源描述、图标及工程配置等文件体量仅约18KB结构精简便于直接阅读或编译验证。目前已有255人浏览学习资源量不大但聚焦明确。代码中可看到DWG缩略图生成、列表项处理与图片绘制等核心模块能直观了解Shell扩展的注册方式、预览接口实现及文件列表控件对接流程对从事CAD辅助工具或文件管理器定制的开发者具备实操价值。1. 文件打开对话框能显示DWG预览不是白来的Windows 资源管理器预览窗格和文件打开对话框里的“缩略图视图”看起来只是换了个地方显示图片实际上资源管理器连 DWG 是什么都不关心。它只会按注册表去查.dwg这个扩展名挂接了什么 Shell 扩展然后把文件路径交给一个进程内 COM 对象去要HBITMAP。VC 预览 DWG 这个项目拆开的正是这条链路一个缩略图提供程序、一个负责识别 DWG 的解析模块以及一个把解析结果画成预览图的绘制模块。也就是说想让打开文件窗口里选中 DWG 就出预览图不需要让 AutoCAD 常驻后台只需要这个 30 KB 左右的 DLL 被 Explorer 加载起来。下面整个拆解围绕“接口、文件头、注册、联调”四件事展开中途会用到一个可编译的 C 骨架和实际注册表路径。2. Shell 扩展的接缝IThumbnailProvider、CLSID 与进程内 DLL2.1 为什么新代码不再写 IExtractImageVC6 时代的 Shell 扩展资料里“预览”和“缩略图”大多指向IExtractImage许多老的 VC 工程也沿用这个接口。IExtractImage需要从IShellFolder拿上下文还要实现GetLocation与Extract两个方法比较适合当年那种“图标和缩略图共用一套机制”的设计。Vista 开始微软把缩略图单独拆成IThumbnailProvider只要求实现Initialize和GetThumbnail两个方法由资源管理器传入一个文件路径扩展返回一张 32 位位图。公共文件对话框右侧的预览窗格和资源管理器预览窗格都会优先走这条新链路所以这里尽量用新接口只有要兼容 Windows XP 才需要回过头去包一层IExtractImage。下面这张表是我决定接口时习惯先列出来的判断依据接口加载场景返回值适合目标IExtractImageWin2000~XP 图标/缩略图文件名 HBITMAP老系统兼容、图标扩展IThumbnailProviderVista 资源管理器HBITMAP 透明通道标志文件缩略图、打开对话框预览IPreviewHandler预览窗格内的交互 UI创建HWND承载内容需要旋转、翻页、选择文本的场景文件打开对话框的预览面板更多是展示静态视觉内容不需要交互因此做缩略图提供程序就够了。预览处理器适合 PDF、TXT 这类需要滚动或翻页的格式涉及 OLE 对象承载复杂度会高出一个量级。2.2 最小 ATL DLL 骨架与导出函数Shell 扩展必须以 DLL 形式注册进CLSID由 Explorer 进程按需加载。这意味着 DLL 不能有全局单例依赖也不要试图弹任何消息框。工程可以基于 ATL 写也可以在纯 Win32 上手搓 COM需要导出的函数只有四个DllGetClassObject、DllCanUnloadNow、DllRegisterServer、DllUnregisterServer。ATL 用CComModule帮忙管理这些是最省事的写法。// dllmain.cpp - 纯 ATL 版最小实现 #include windows.h #include atlbase.h #include atlcom.h CComModule _Module; extern C BOOL WINAPI DllMain(HINSTANCE hInst, DWORD reason, LPVOID reserved) { // 资源管理器加载扩展 DLL 时进程已经存在这里只初始化 COM 模块 _Module.Init(nullptr, hInst); return TRUE; } STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv) { return _Module.GetClassObject(rclsid, riid, ppv); } STDAPI DllCanUnloadNow() { // 扩展对象全部释放后Explorer 才能安全卸载本 DLL return _Module.GetLockCount() ? S_FALSE : S_OK; } STDAPI DllRegisterServer() { return _Module.RegisterServer(TRUE); } STDAPI DllUnregisterServer() { return _Module.UnregisterServer(); }DllMain里不能做复杂初始化因为 Explorer 加载 DLL 时可能处于非模式消息循环中。GetLockCount是 ATL 对活动对象的计数只要某个IThumbnailProvider实例还没释放DLL 就不允许卸载。RegisterServer(TRUE)里的布尔参数表示是否同时写入LocalServer32这里只用进程内 DLL传TRUE也没有副作用但更严格的写法是传FALSE并手动写注册表项。2.3 实现 IThumbnailProvider 的类骨架ATL 类声明只需要继承IThumbnailProvider并用BEGIN_COM_MAP把接口暴露出去。资源管理器拿到 COM 对象后会先调用Initialize再调用GetThumbnail顺序不能反。// dwgthumbnail.h #include thumbcache.h #include shlwapi.h class ATL_NO_VTABLE CDwgThumb final : public CComObjectRootExCComSingleThreadModel, public CComCoClassCDwgThumb, public IThumbnailProvider { public: CDwgThumb() : m_grfMode(0) {} BEGIN_COM_MAP(CDwgThumb) COM_INTERFACE_ENTRY(IThumbnailProvider) END_COM_MAP() DECLARE_NOT_AGGREGATABLE(CDwgThumb) STDMETHODIMP Initialize(LPCWSTR pszFile, DWORD grfMode); STDMETHODIMP GetThumbnail(UINT cx, HBITMAP* phbmp, WTS_ALPHATYPE* pdwAlpha); private: std::wstring m_filePath; DWORD m_grfMode; };Initialize的第一个参数是文件完整路径第二个参数是打开模式常见取值是STGM_READ。GetThumbnail的cx是由调用方给定的“最小边长”。Explorer 会告诉扩展它希望多大比如 256 或 512扩展应当返回不小于该尺寸的位图。如果把STGM_READWRITE传给Initialize但随后又没真正写入某些版本的资源管理器反而会超时所以默认只请求读权限。2.4 Shell 对返回值与位图格式的约定GetThumbnail返回值只允许S_OK或错误码。若返回S_OKphbmp必须指向一个 32 位 DIB 位图Shell 会负责释放它pdwAlpha应填写WTS_ALPHATYPE_ARGB表示位图带 alpha 通道。资源管理器对缩略图的尺寸要求是“至少等于请求的cx”但也不能超过cx * 2否则缩略图缓存会存一张超大图内存占用明显升高。这里有一个容易踩的坑直接CreateCompatibleBitmap生成的位图是屏幕 DC 的格式不一定带 alpha影响圆角裁剪效果。正确做法是手工构造BITMAPINFO并使用CreateDIBSection或者先用 GDI 绘制到 32bpp 内存位图再提取像素。3. 从 DWG 文件里挖预览图头部版本识别与 GDI 绘制3.1 DWG 版本头是最简单的“验尸”入口DWG 从 R11 之后都在文件开头前 6 个字节写明版本字符串常见值包括AC1009、AC1015、AC1027、AC1032。当一个文件扩展名是.dwg但打开后什么图形都没有先看一眼这个头基本能判断是不是伪装的 DWG。下面这张表可以当作快速检查清单6 字节头对应产品说明AC1009AutoCAD R12早期格式缩略图信息分布不规律AC1015AutoCAD 2000引入了更稳定的 object map 结构AC1018AutoCAD 2004大量图纸预渲染缩略图开始完整AC1021AutoCAD 2007默认启用远程文本与缩略图缓存AC1024AutoCAD 2010与后续版本兼容性较好AC1032AutoCAD 2018当前主流常见中文图纸读取版本头只需要CreateFileW加一次ReadFile不需要读完整文件。注意 DLL 被资源管理器加载时没有权限限制但应保留FILE_SHARE_READ否则文件正被 AutoCAD 占用时预览会失败。// dwgthumbnail.cpp - 版本头读取 #include windows.h BOOL ReadDwgVersion(LPCWSTR path, WCHAR out[16], size_t maxLen) { HANDLE hFile CreateFileW( path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (hFile INVALID_HANDLE_VALUE) return FALSE; char head[6] {0}; DWORD bytesRead 0; BOOL ok ReadFile(hFile, head, 6, bytesRead, nullptr); CloseHandle(hFile); if (!ok || bytesRead ! 6) return FALSE; return MultiByteToWideChar(CP_ACP, 0, head, 6, out, (int)maxLen) 0; }FILE_SHARE_READ | FILE_SHARE_WRITE两个共享标志都加上可以避免与 CAD 自动保存抢占文件。GENERIC_READ永远不要改成GENERIC_ALL否则某些策略环境下Initialize会触发权限校验弹窗。bytesRead必须严格等于 6短读一般出现在文件正在被写入或文件被截断的场景。3.2 用 GDI 画一张不唬人的缩略图拿到版本号之后真正解析 DWG 图形几何体通常要依赖 ObjectARX 或者开源库 libredwg。本例资源主要演示 Shell 扩展流程所以dwgthumbnail.cpp里最稳定的做法是先验证文件头然后用 GDI 绘制一个包含文件名和版本信息的预览图。将这张图替换成真实图形解析结果时只动绘制函数内部即可Shell 扩展其他部分完全不用改。// dwgthumbnail.cpp - GetThumbnail 实现片段 #include gdiplus.h #include vector #pragma comment(lib, gdiplus.lib) STDMETHODIMP CDwgThumb::GetThumbnail( UINT cx, HBITMAP* phbmp, WTS_ALPHATYPE* pdwAlpha) { if (cx 0 || phbmp nullptr || pdwAlpha nullptr) return E_INVALIDARG; WCHAR ver[16] {0}; if (!ReadDwgVersion(m_filePath.c_str(), ver, 15)) return E_FAIL; // 创建一个接近请求尺寸的正方形画布 Gdiplus::Bitmap canvas((INT)cx, (INT)cx, PixelFormat32bppARGB); Gdiplus::Graphics g(canvas); g.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias); Gdiplus::SolidBrush bg(Gdiplus::Color(36, 38, 42)); g.FillRectangle(bg, 0, 0, (INT)cx, (INT)cx); Gdiplus::Font font(LMicrosoft YaHei, (REAL)(cx / 12.0)); Gdiplus::SolidBrush textBrush(Gdiplus::Color(220, 220, 220)); g.DrawString(m_filePath.c_str(), -1, font, Gdiplus::PointF((REAL)(cx / 12), (REAL)(cx / 12)), textBrush); // 真实工程中在这一行替换为 CAD 图形渲染 Gdiplus::Pen pen(Gdiplus::Color(90, 160, 255), (REAL)(cx / 60.0)); g.DrawRectangle(pen, cx / 8, cx / 8, cx * 3 / 4, cx * 3 / 4); // 拷贝成连续显存布局的 DIB Gdiplus::BitmapData data {}; Gdiplus::Rect rc(0, 0, (INT)cx, (INT)cx); canvas.LockBits(rc, Gdiplus::ImageLockModeRead, PixelFormat32bppARGB, data); int rowBytes (int)cx * 4; std::vectorBYTE pixels(rowBytes * (int)cx); for (int y 0; y (int)cx; y) { memcpy(pixels.data() y * rowBytes, (BYTE*)data.Scan0 y * data.Stride, rowBytes); } canvas.UnlockBits(data); *phbmp CreateBitmap((int)cx, (int)cx, 1, 32, pixels.data()); if (*phbmp nullptr) return E_OUTOFMEMORY; *pdwAlpha WTS_ALPHATYPE_ARGB; return S_OK; }说明三点Gdiplus::BitmapData.Stride和rowBytes * 4不一定相等因为 GDI 会按 4 字节对齐内部行所以必须逐行拷贝CreateBitmap接受的是连续位BOTTOM-UP或TOP-DOWN的内存这里直接给 GDI 解码出来的原始像素Windows 会在共享缩略图时自动处理方向PixelFormat32bppARGB中的 alpha 通道需要显式填充如果使用PixelFormat24bppRGBWTS_ALPHA就不能填WTS_ALPHATYPE_ARGB。3.3 处理器方法的调用现场Initialize 到 GetThumbnailInitialize只负责保存路径不打开文件真正的 IO 发生在GetThumbnail。有些实现喜欢在Initialize里读文件头看似提前校验实际上如果资源管理器在等待缩略图线程时线程池繁忙会延长文件打开对话框的响应时间。我一般把Initialize做成轻量级记录所有耗时代码都放在GetThumbnail。STDMETHODIMP CDwgThumb::Initialize(LPCWSTR pszFile, DWORD grfMode) { if (pszFile nullptr || ::PathFileExistsW(pszFile) FALSE) return E_INVALIDARG; m_filePath pszFile; m_grfMode grfMode; return S_OK; }PathFileExistsW本身既判断文件是否存在也判断访问权限。注意这里不要调用GetFileAttributesW之后立刻去ReadFile因为 Shell 可能把同一个文件路径传给多个缩略图线程造成并发打开。给m_filePath加一个临界区或直接使用std::wstring的只读语义即可。GetThumbnail中若需要写日志应使用OutputDebugStringW避免在资源管理器进程中写出普通日志文件文件句柄可能被杀毒软件打开导致预览阻塞。4. 注册表路径、regsvr32 与打开文件对话框联调4.1 注册表到底要写哪些键一个缩略图提供程序要生效注册表里必须同时存在两类键一类是.dwg扩展名下的处理程序指示另一类是CLSID下的InProcServer32实现信息。资源管理器不看扩展名对应的默认打开程序只看ShellEx下是否有缩略图提供程序。下面是注册表项的完整结构注册表位置数据作用HKCR\CLSID\{你的GUID}\InProcServer32DLL 完整路径声明 COM 组件实现文件HKCR\CLSID\{你的GUID}\InProcServer32\ThreadingModelApartment指定线程模型HKCR\.dwg\ShellEx\{e357fccd-a995-4576-b01f-234630154e96}{你的GUID}将缩略图处理程序挂到扩展名上HKCR\DWGThumbnail\CurVer版本描述与资源无关的备注信息{e357fccd-a995-4576-b01f-234630154e96}是IThumbnailProvider在扩展名注册表中的固定键必须一字不差。如果注册表里写错这个 GUID最典型的表现是资源管理器一无所知地忽略扩展连一条日志都不会输出。4.2 自注册脚本与 RGS 文件ATL 工程的DllRegisterServer会读取 RGS 脚本。RGS 文件的好处是能把多处键集中管理配合 MSI 打包时也更容易生成合并模块。// VC预览DWG.rgs NoRemove CLSID { ForceRemove {6A2E7E51-3F10-4D4F-A7F5-1D8F3C7B6E21} s DWG Thumbnail Provider { InprocServer32 s %MODULE% { val ThreadingModel s Apartment } TypeLib s {8B1A2C80-12E1-4F5A-9A3B-54C3E10B22D0} } }ForceRemove会在注册时强制删除旧键再重建避免因为上一次残留的无效 DLL 路径导致 Explorer 加载失败。%MODULE%是 ATL 自注册时替换的特殊占位符代表当前 DLL 的绝对路径。这里手动写的{6A2E7E51-...}只是示例 GUID实际工程应该用 Visual Studio 的“创建 GUID”工具生成一个新的避免跟网上现成代码冲突。4.3 用 regsvr32 注册、更新缩略图缓存完成编译后在管理员命令行里执行regsvr32 /s D:\build\Release\VC预览DWG.dll/s表示静默成功不弹提示框。若提示“内存位置访问无效”或者拒绝访问通常是因为 explorer.exe 或“照片应用”已经加载了旧版本 DLL。需要先结束所有explorer.exe再从任务管理器里“运行新任务”重新启动资源管理器然后再注册。注册完成不要立刻去看预览还要刷新缩略图缓存ie4uinit.exe -show或者用 PowerShell 调用 Shell 的关联变更通知Add-Type -TypeDefinition using System; using System.Runtime.InteropServices; public class Sh { [DllImport(shell32.dll)] public static extern void SHChangeNotify(int wEventId, uint uFlags, IntPtr dwItem1, IntPtr dwItem2); } [Sh]::SHChangeNotify(0x08000000, 0, [IntPtr]::Zero, [IntPtr]::Zero)0x08000000是SHCNE_ASSOCCHANGED也就是“文件关联变化”事件。很多注册完看不到缩略图的情况并不是代码有问题而是 Explorer 的缩略图缓存还保留着旧失败记录。SHChangeNotify只发事件不清理磁盘上的缓存文件必要时可以到%LocalAppData%\Microsoft\Windows\Explorer删除thumbcache_*.db但删除前要结束 explorer。4.4 在文件打开对话框里做最终验证用 Win32 调用公共文件对话框时选中文件尾部会显示预览面板前提是不要设置FOS_HIDEPREVIEWPANE。如果使用 MFC 的CFileDialog可以在构造函数后设置标志位同时确保对话框版本是 Vista 风格。验证流程是随便打开一个“打开文件”窗口将文件类型切到“所有文件”选中一个 DWG右侧预览面板应显示图。注意文件打开对话框的预览区域对小尺寸图片会放大因此缩略图尺寸最好以cx256为基准生成小于 256 的位图会被拉伸到模糊。5. 预览窗格不刷新时的排错顺序与两条实用技巧5.1 先查注册表再查代码最常见的失败现场是注册成功DLL 存在但打开对话框怎么都不显示预览。我的排查顺序固定为“注册表 → 进程 → 缓存 → 代码”。先看注册表里的 DLL 路径是否真实存在再看ThreadingModel是否填了Apartment然后看 explorer 是否有多个进程最后才去GetThumbnail里加调试输出。现象大概率原因快速判断所有文件都没预览注册表键名错误检查{e357fccd...}是否拼写正确只有 DWG 没预览DLL 未注册到.dwg查HKCR\.dwg\ShellEx预览过一会儿消失线程阻塞Shell 超时GetThumbnail里避免同步等待预览图是黑色方块位图未填充 alpha确认使用 32bppARGB注册时报“文件被占用”explorer 加载了旧 DLL重启 explorer 再注册5.2 用 PowerShell 一键核对 DLL 路径注册表看多了容易眼花我习惯用一段 PowerShell 把关键内容打出来$guid {6A2E7E51-3F10-4D4F-A7F5-1D8F3C7B6E21} $key Registry::HKEY_CLASSES_ROOT\CLSID\$guid\InprocServer32 (Get-ItemProperty $key).(default) (Get-ItemProperty $key).ThreadingModel输出第一行是 DLL 路径第二行是Apartment。如果第二行为空说明 RGS 里的ThreadingModel没有生效需要重新注册。也可以顺手检查默认值是否被系统expanded成C:\Windows\System32\...这种错误位置。5.3 遇到“预览文件可能有害”提示时如何处理当 DWG 是从网上下载的资源管理器会给文件附加Zone.Identifier标记预览面板就会显示“你尝试预览的文件可能对你的计算机有害”。这其实是 Windows 对下载文件的保护机制跟缩略图扩展本身没有关系。确认文件来源可信后在文件“属性”页勾选“解除锁定”或者执行Unblock-FilePowerShell 命令即可。如果是自己创建的测试文件也出现该提示需要先确认Attrib R只读属性没有影响读取但通常跟缩略图提供程序无关。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门