简介:压缩包内是一套基于COM ATL的Shell Extension示例工程,目标是在Windows资源管理器中添加自定义工具条,适合有一定C++基础、正在学习Windows Shell扩展或COM组件开发的读者。工程通过ATL模板简化COM接口实现与类工厂生成,从源码层面展示如何构建可注册的Shell组件。压缩包共33个文件,大小约67KB,涵盖C++源文件、头文件、C文件、def与rgs注册配置、位图工具栏资源、工程文件及编译产物;其中源码部分对应ShellServer主服务、视图对象、文件夹对象等核心模块,rgs与def负责组件注册和接口导出,tlb类型库则记录COM接口元数据。已有252人学习。对希望理解Shell扩展工作原理的开发者而言,可对照代码梳理COM接口实现、类型库生成及DLL注册流程,并在此基础上自定义工具栏按钮与显示逻辑;同时需注意此类组件直接影响系统Shell,改动与安装前应做好备份,建议在虚拟机或测试环境中验证。
1. COM ATL Shell Extension 给 Windows 资源管理器添加工具条:桌面上最后一段受控的“自定义地带”
做企业桌面工具的人十有八九遇到过这个需求:用户一整天泡在资源管理器里,你想把“复制当前路径”“打开终端”“一键归档”这些高频操作,变成资源管理器窗口里一排随叫随到的按钮。可 Windows 从 Win7 一路改到 Win11,始终没有给第三方留一个干净的工具栏扩展入口。这时候,COM ATL Shell Extension 又成了必须掌握的手法:用 ATL 写一个 COM 组件,把自己实现成 DeskBand(带区),交给 explorer.exe 挂载到窗口边缘。下文就是我把这套链路从接口到注册表完整走通后的笔记,包括我在 Win10 / Win11 上遇到的兼容边界和几条真实踩坑记录,适合有 C++ 基础、想给团队或自己造桌面小工具的开发者。
2. DeskBand 是什么:先理解资源管理器为什么要你来写 COM
2.1 三条给资源管理器加工具条的路:选型比写代码更重要
给 explorer.exe 窗口塞一个工具条,从业者常见的方案有三条,我建议先做选型再动手。
第一条是 DeskBand。把自己实现成 COM 对象,支持 IDeskBand、IObjectWithSite、IPersistStream 等接口,再注册到 CLSID 和一个特殊的 Instance 键下。资源管理器会在带区里创建你的窗口,这就是标题里“com atl shell extension”组合的由来。微软最初为 IE 和 Explorer 设计了这套模型,API 稳定,Win7/Win8 时代非常成熟。
第二条是窗口子类化。找到 explorer 主窗口句柄,把自己的窗口 SetParent 进去,再通过 WM_WINDOWPOSCHANGED 或周期性刷新保持跟随。QTTabBar 这类第三方工具就是这样做的。优点是在 Win10 / Win11 上也还能跑,缺点是与系统版本强耦合,每次大版本更新都可能翻车,还容易被安全软件当成注入行为。
第三条是改 Ribbon。很多做过 Office 插件的人会下意识找资源管理器的 Ribbon XML 扩展点,但资源管理器并不是真正的 Ribbon,它只是长得像,微软没有开放任何命令栏定制接口。浪费时间,趁早放弃。
所以摆在你面前的选择很清晰:目标用户主要还在 Win7/Win8,或者你要做的是可分发、可控的企业内部工具,DeskBand 是成本最低的正规路线。目标用户是 Win11 长尾,那就得接受 DeskBand 的兼容性折扣,具体边界我在第 5 章单列。
2.2 接口链路:IDeskBand 家族到底要你戴哪几顶帽子
DeskBand 不是一个孤零零的接口,而是一组 COM 接口的复合体。Explorer 进程以 CoCreateInstance 方式创建你的对象后,会依次检查它需要的能力,每个能力对应一个接口。
IOleWindow 是所有窗口类扩展的地基。Explorer 通过它的 GetWindow 取到你的工具条窗口句柄,之后才能做消息循环、焦点切换和尺寸管理。IDockingWindow 继承自 IOleWindow,里面是 ShowDW、CloseDW、ResizeBorderDW 三个方法。ShowDW 控制你的窗口显示或隐藏,CloseDW 表示带区要被移除,ResizeBorderDW 用于响应停靠边界变化,多数情况返回 S_OK 即可。
IDeskBand 继承自 IDockingWindow,核心方法是 GetBandInfo。Explorer 会传一个 DESKBANDINFO 结构体让你填写尺寸、标题、模式标志。这是最影响观感的方法,后面我会贴完整实现。
IDeskBand2 是后加的接口,支持透明混合渲染和颜色方案。Win7 开始 explorer 窗口默认走 DWM 合成,想消除工具条的底色闪块,最好实现 CanRenderComposited 和 SetComposited。
IObjectWithSite 是让它“活起来”的关键。Explorer 通过 SetSite 把宿主指针(一般是 IShellBrowser 的所在站点)交给你;你把这个指针缓存下来,才可以反向获取地址栏、当前文件夹、导航接口。没有这个接口,你的工具条就只是一张贴图,点按钮不知道自己在哪个目录。
IPersistStream 负责保存带区状态。宽度、折叠状态这些信息会被序列化到流里,下次打开同一个窗口时能够还原。
把这些接口的继承关系理清后你会发现,需要实现的虚函数其实有十三个左右。纯手写 C++ COM 会很痛苦,ATL 正好把这些模板化的 QueryInterface、引用计数、接口映射吃得干干净净。这也是为什么这种扩展的经典组合永远是“COM 协议 + ATL 骨架”。
2.3 为什么不用 MFC、C++/CLI 或 C++/WinRT
MFC 可以写 COM,但它自带的窗口框架围绕 Doc/View 设计,跟 Shell UI 没有交点,静态链接进去体积大、加载慢,不划算。C++/CLI 也就是 .NET 方案,写起来确实快,可 explorer.exe 是原生进程,加载 CLR 的开销和不同 Framework 版本的环境差异,会让“注册成功、加载失败”变成常态。C++/WinRT 面向 WinRT/UWP 生态,对 explorer 这种老式 COM 反而要额外做转换,没必要。ATL 生成的 DLL 骨架一般只有几十 KB,加载时间几乎感知不到,这是桌面扩展最重要的品质。
3. 用 ATL 搭一个最小 DeskBand 工程:从向导到 GetBandInfo
3.1 创建 ATL DLL 工程与 COM 对象
Visual Studio 里新建项目选“ATL 项目”,向导中类型选“DLL”,不要勾选“静态链接 ATL”,让 DLL 动态依赖 ATL 运行库即可。创建完成后,在解决方案里添加一个“ATL 简单对象”,名字自定,比如 ShellToolbar。向导会为你生成一个只带常规接口的 COM 类,我们要做的就是把它的继承链改造成 DeskBand。
改造后类的声明如下,关键处的注释我标在了对应行:
// ShellToolbar.h class ATL_NO_VTABLE CShellToolbar : public CComObjectRootEx<CComMultiThreadModel>, public CComCoClass<CShellToolbar, &CLSID_ShellToolbar>, public IDeskBand2, public IObjectWithSite, public IPersistStream { public: CShellToolbar() : m_pSite(nullptr) {} BEGIN_COM_MAP(CShellToolbar) COM_INTERFACE_ENTRY(IDeskBand) COM_INTERFACE_ENTRY(IDeskBand2) COM_INTERFACE_ENTRY(IObjectWithSite) COM_INTERFACE_ENTRY(IPersistStream) END_COM_MAP() DECLARE_PROTECT_FINAL_CONSTRUCT() HRESULT FinalConstruct() { return S_OK; } void FinalRelease() { m_pSite.Release(); } // IOleWindow STDMETHODIMP GetWindow(HWND* phwnd); STDMETHODIMP ContextSensitiveHelp(BOOL fEnterMode) { return S_OK; } // IDockingWindow STDMETHODIMP ShowDW(BOOL fShow); STDMETHODIMP CloseDW(DWORD dwReserved) { return S_OK; } STDMETHODIMP ResizeBorderDW(LPCRECT prcBorder, IUnknown* punkToolbar, BOOL fReserved) { return S_OK; } // IDeskBand STDMETHODIMP GetBandInfo(DWORD dwBandID, DWORD dwViewMode, DESKBANDINFO* pdbi); // IDeskBand2 STDMETHODIMP CanRenderComposited(BOOL* pfCanRenderComposited); STDMETHODIMP SetComposited(BOOL fComposited); STDMETHODIMP GetColorScheme(DWORD* pdwColorScheme); // IObjectWithSite STDMETHODIMP SetSite(IUnknown* pUnkSite); STDMETHODIMP GetSite(REFIID riid, void** ppvSite); // IPersistStream STDMETHODIMP GetClassID(CLSID* pClassID); STDMETHODIMP IsDirty() { return S_FALSE; } STDMETHODIMP Load(IStream* pStm); STDMETHODIMP Save(IStream* pStm, BOOL fClearDirty) { return S_OK; } STDMETHODIMP GetSizeMax(ULARGE_INTEGER* pcbSize); CComPtr<IUnknown> m_pSite; CWindow m_wnd; // 工具条宿主窗口,ATL 对 HWND 的轻量封装 };这段代码里几个参数取舍值得说明:CComMultiThreadModel 保证对象可以被 explorer 的多线程环境安全释放,Shell 扩展经常遇到边界线程释放对象的场景;COM_INTERFACE_ENTRY 的注册顺序不是随意的,必须让 IDeskBand 先于 IDeskBand2,因为 IDeskBand2 继承自 IDeskBand,QueryInterface 时先命中基类才能继续向下兼容;m_wnd 用 CWindow 而不是 CWindowImpl,因为带区窗口的排布完全由 Explorer 控制,不需要额外 ATL 消息映射带来的复杂度。
3.2 GetBandInfo:尺寸、标题与模式标志一次说清
GetBandInfo 返回的 DESKBANDINFO 决定了 Explorer 如何绘制你的带区。漏填它,工具条就会以 0 尺寸出现,或者干脆被拒绝显示。下面是我调试后稳定可用的实现:
STDMETHODIMP CShellToolbar::GetBandInfo(DWORD /*dwBandID*/, DWORD /*dwViewMode*/, DESKBANDINFO* pdbi) { if (!pdbi) return E_POINTER; if (pdbi->dwMask & DBIM_MIN_SIZE) { pdbi->ptMinSize.x = 180; // 最小宽度:保证按钮和输入框不挤 pdbi->ptMinSize.y = 38; // 最小高度:水平模式常用 38~40 } if (pdbi->dwMask & DBIM_MAX_SIZE) { pdbi->ptMaxSize.x = -1; // -1 表示宽度不限制 pdbi->ptMaxSize.y = 44; // 高度上限,避免工具条遮挡内容 } if (pdbi->dwMask & DBIM_INTEGRAL) { pdbi->ptIntegral.x = 8; // 宽度按 8 像素步进对齐 pdbi->ptIntegral.y = 38; } if (pdbi->dwMask & DBIM_TITLE) { wcscpy_s(pdbi->wszTitle, L"My Shell Toolbar"); } if (pdbi->dwMask & DBIM_MODEFLAGS) { pdbi->dwModeFlags = DBIMF_NORMAL | DBIMF_VARIABLEHEIGHT; } if (pdbi->dwMask & DBIM_ACTUAL) { RECT rc{ 0, 0, 180, 38 }; if (m_wnd.IsWindow()) ::GetClientRect(m_wnd.m_hWnd, &rc); pdbi->ptActual.x = rc.right - rc.left; pdbi->ptActual.y = rc.bottom - rc.top; } return S_OK; }dwMask 是位掩码,表示 Explorer 想查询哪些字段;没被问到的字段不要去动,否则可能把其他带区的数据覆盖掉。DBIMF_NORMAL 表示普通带区,DBIMF_VARIABLEHEIGHT 可以让工具条在竖直停靠时自动换高度。ptIntegral 是尺寸调整的粒度,不设置的话用户拖动时宽度会变得很生硬。DBIM_ACTUAL 字段在窗口已创建时返回真实客户区尺寸,这样 Explorer 在调整周围布局时不会反复查询同一组静态值。
3.3 ShowDW 与 GetWindow:窗口何时现身
DeskBand 的窗口不是 Explorer 帮你创建的,而是你在 ShowDW(TRUE) 时自己创建。注意不要在 FinalConstruct 或第一个 QueryInterface 里就 CreateWindow,因为那时还没有站点上下文。我在项目里采用“懒汉式创建”,首次显示时才建窗口:
STDMETHODIMP CShellToolbar::GetWindow(HWND* phwnd) { if (!phwnd) return E_POINTER; *phwnd = m_wnd.m_hWnd; return *phwnd ? S_OK : S_FALSE; // 窗口未创建时返回 S_FALSE } STDMETHODIMP CShellToolbar::ShowDW(BOOL fShow) { if (fShow) { if (!m_wnd.IsWindow()) { // 父窗口先用桌面,SetSite 之后 Explorer 会重新显示 m_wnd.Create(CWindow::GetDesktopWindow(), CWindow::rcDefault, L"ShellToolbarBand", WS_CHILD | WS_CLIPCHILDREN | WS_CLIPSIBLINGS); } ::ShowWindow(m_wnd.m_hWnd, SW_SHOW); } else { if (m_wnd.IsWindow()) ::ShowWindow(m_wnd.m_hWnd, SW_HIDE); } return S_OK; }这里的风格是“别对调用顺序做假设”。不同版本的 explorer 调用 GetBandInfo、SetSite、GetWindow、ShowDW 的顺序不完全一致,我在 Win7 和 Win10 上分别见过两种顺序。所以任何依赖站点指针的工作都要放到 SetSite 之后再做,任何真实绘制都要等到 ShowDW(TRUE) 之后再执行。窗口创建出来之后,在窗口过程中处理 WM_SIZE,把按钮和编辑框重新铺一遍,这块属于常规 Win32 代码,不再展开。
4. 让工具条真正有用:把按钮动作接到 Shell 对象上
4.1 从 SetSite 里取 IShellBrowser,定位当前文件夹
工具条不能只是一排死按钮,它至少得知道用户正在浏览哪个目录。这个信息从 IObjectWithSite::SetSite 里传递进来的站点指针拿。站点可以被 QueryInterface 成 IServiceProvider,再向它请求 SID_STopLevelBrowser 服务,得到 IShellBrowser:
STDMETHODIMP CShellToolbar::SetSite(IUnknown* pUnkSite) { m_pSite = pUnkSite; m_pBrowser.Release(); CComPtr<IServiceProvider> spSP; if (pUnkSite) { HRESULT hr = pUnkSite->QueryInterface(IID_PPV_ARGS(&spSP)); if (SUCCEEDED(hr) && spSP) { spSP->QueryService(SID_STopLevelBrowser, IID_PPV_ARGS(&m_pBrowser)); } } // 站点就绪后启用按钮,否则置灰 if (m_wnd.IsWindow()) ::EnableWindow(GetDlgItem(m_wnd.m_hWnd, IDC_BTN_OPEN_TERMINAL), m_pBrowser != nullptr); return S_OK; }SID_STopLevelBrowser 在 shlguid.h 中定义,QueryService 会返回当前资源管理器窗口的 IShellBrowser 指针。拿到它之后,通过 QueryActiveShellView 取出 IShellView,再转成 IFolderView 得到 IShellFolder,最终用 SHGetIDListFromObject 和 SHGetPathFromIDList 还原成盘符路径。完整逻辑如下:
HRESULT GetCurrentPath(CComPtr<IShellBrowser> spBrowser, std::wstring& strPath) { CComPtr<IShellView> spView; HRESULT hr = spBrowser->QueryActiveShellView(&spView); if (FAILED(hr)) return hr; CComQIPtr<IFolderView> spFolderView = spView; if (!spFolderView) return E_NOINTERFACE; CComPtr<IShellFolder> spFolder; hr = spFolderView->GetFolder(IID_PPV_ARGS(&spFolder)); if (FAILED(hr)) return hr; CComHeapPtr<ITEMIDLIST> spPidl; hr = SHGetIDListFromObject(spFolder, &spPidl); if (FAILED(hr)) return hr; wchar_t szPath[MAX_PATH] = { 0 }; if (!SHGetPathFromIDListW(spPidl, szPath)) return E_FAIL; strPath = szPath; return S_OK; }这段代码有两个常见坑。SHGetIDListFromObject 返回的 PIDL 用 CoTaskMemAlloc 分配,必须交给 CComHeapPtr 管理,否则每一次点击都泄漏一块内存;SHGetPathFromIDList 对“回收站”“此电脑”这类虚拟文件夹会返回 FALSE,所以获取失败时不要硬拼路径,可以直接把 PIDL 丢给 SHOpenFolderAndSelectItems 等 API 处理虚拟位置。对真实场景来说,这个函数已经能覆盖绝大多数磁盘目录。
4.2 把按钮命令落地:打开终端或复制路径
拿到路径后,按钮动作就顺理成章了。以“打开终端”为例,常见做法是 ShellExecute 启动 cmd.exe,并把当前路径作为工作目录传入:
void OnOpenTerminal(HWND hWndBand, CComPtr<IShellBrowser> spBrowser) { std::wstring path; if (FAILED(GetCurrentPath(spBrowser, path))) return; SHELLEXECUTEINFOW sei = { sizeof(sei) }; sei.fMask = SEE_MASK_FLAG_NO_UI; sei.hwnd = hWndBand; sei.lpVerb = L"open"; sei.lpFile = L"cmd.exe"; sei.lpDirectory = path.c_str(); sei.nShow = SW_SHOWNORMAL; ShellExecuteExW(&sei); }SEE_MASK_FLAG_NO_UI 是为了避免当路径不可访问时弹一个错误对话框打断用户;lpDirectory 指定工作目录后 cmd 启动就会直接停在当前文件夹,体验接近右键里的“在此处打开命令窗口”。如果你要打开的是 PowerShell 或 Windows Terminal,把 lpFile 换成 powershell.exe 或 wt.exe 即可,参数结构完全一样。注意 ShellExecuteEx 的 hwnd 要传工具条自己的窗口句柄,这样错误提示会挂在正确所有者上。
4.3 注册表与 explorer 进程加载的生命周期
写好的 DLL 要能被资源管理器加载,必须正确注册两处关键信息:COM 服务器本体,以及 Explorer 的带区挂载点。ATL 工程自带的 DllRegisterServer 只负责 CLSID 和 InprocServer32,带区专有的 Instance 键它不管,需要手动补。下面的 .reg 文件是常见写法:
Windows Registry Editor Version 5.00 [HKEY_CLASSES_ROOT\CLSID\{B3C7C5D4-1234-4F1A-9C2B-ABCDEF123456}] @="My Shell Toolbar" [HKEY_CLASSES_ROOT\CLSID\{B3C7C5D4-1234-4F1A-9C2B-ABCDEF123456}\InprocServer32] @="C:\\Temp\\MyToolbar.dll" "ThreadingModel"="Apartment" [HKEY_CLASSES_ROOT\CLSID\{B3C7C5D4-1234-4F1A-9C2B-ABCDEF123456}\Instance] "CLSID"="{000214F0-0000-0000-C000-000000000046}"{000214F0-0000-0000-C000-000000000046} 是 Shell Band 的 CLSID,把它放在 Instance 键下,explorer 才会把 COM 对象识别为可停靠的带区。InprocServer32 里 ThreadingModel 必须写成 Apartment,explorer 在主 STA 线程上创建带区,写成 Free 有时也能注册成功但运行期会出怪异时序问题。注册完成后,还需要重启资源管理器进程才能看到效果。桌面会闪一下,但不会丢任务,工具栏里的第三方图标也会跟着消失再回来,属正常现象。
5. 避坑与排查:工具条注册了却不显示,问题出在哪
5.1 注册成功但右键菜单里没有“工具栏”项
现象:regsvr32 返回成功,CLSID 键也确认写入了,但在资源管理器的命令栏空白处点右键,看不到“工具栏”子菜单,更找不到你的组件。
原因:Shell 枚举可用带区时,读取的不是裸 CLSID,而是要求该 CLSID 下存在 Instance 键,并且 Instance 键里的 CLSID 指向 CLSID_ShellBand。没有这一层,explorer 根本不知道这是“带区候选”。
解决:检查注册表里 HKEY_CLASSES_ROOT\CLSID{你的GUID} 下是否创建了 Instance 子键,并确认其中 CLSID 值为 {000214F0-0000-0000-C000-000000000046}。ATL 的 rgs 脚本如果没写这一节,就按上面代码块里的 .reg 手动补录,或者把 Instance 键写进自己的 rgs 资源中。
5.2 工具栏出现在列表里,但勾选后看不见任何东西
现象:右键菜单里能看到工具条名称,勾上之后整个带区只有一条窄缝,或者一块空白的黑色矩形,没有任何控件。
原因:绝大多数是 GetBandInfo 没有正确申报尺寸。Explorer 会先用 ptMinSize 预留给你的带区,如果这里返回 0,带区就被压缩成一条线,后续子窗口再大也被裁剪。
解决:把 DBIM_MIN_SIZE 的 ptMinSize.x 和 ptMinSize.y 设为实际想要的最小值,宽度 180、高度 38 是经得起多数主题考验的起步值;再做窗口创建后的第一次 SetWindowPos 强制刷新布局。如果窗口里放的是按钮加编辑框,优先用 CreateWindow 静态创建而不是动态布局,减少被带区高度变化挤没的概率。
5.3 64 位与 32 位错位:explorer 是 64 位还是 32 位
现象:同一套 DLL 在开发机 Win7 32 位系统上能正常显示,换到 Win10 64 位系统上一注册,explorer 既不报错也不显示带区。
原因:Windows 10/11 的 explorer.exe 是 64 位进程,只能加载 64 位 COM DLL。有人习惯把工程编成 Win32 平台,在 32 位系统上没问题,拷到 64 位系统整体失效。
解决:开发与发布一律选用 x64 平台配置。如果还要兼顾 32 位系统,就分别产出 x86 和 x64 两个 DLL,注册脚本里用不同 CLSID 或按系统位数分别注册。注意同一份代码切换平台后要重新编译,不要拿 x86 的产物改名冒充 x64。这时候 regsvr32 很容易成功,因为 32 位注册器会把键写到 WOW6432Node 下,而 64 位 explorer 读的是原生 64 位视图,很容易漏看。
5.4 Win10/11 新命令栏不再暴露“工具栏”入口
现象:在 Win10 1809 之后的资源管理器里,右键命令栏空白处没有“工具栏”菜单;网上老教程的截图和你屏幕上的界面完全对不上。
原因:微软重构了资源管理器的命令栏 UI,旧的“右键显示工具栏”入口被移除。DeskBand 在 explorer 窗口侧边和顶部的挂载能力虽然底层还在,但用户没有入口把它调出来。这是微软有意为之,不是你的注册表写错。
解决:如果目标系统集中在这类新版本,建议把方案改为窗口子类化路线,做 SetParent 和窗口跟随,或者使用 QTTabBar 这类成熟方案作为运行时宿主。如果开发目标是 Win7/Win8 或企业内仍以 Win10 LTSC 2019 之前的版本为主,DeskBand 仍然值得投入。做之前先确认现场系统分布,这一步能帮你避免返工。
5.5 修改代码重新注册后,工具条还是旧表现
现象:改了按钮文字和逻辑,重新编译、regsvr32、重启了资源管理器,打开的带区仍然是旧界面。
原因:explorer.exe 在资源管理器重启后通常会重新加载 COM DLL,但如果你的 DLL 被其他进程(比如资源管理器窗口之外的预览进程)引用,或者注册表里还残留旧路径,就可能加载到旧副本。另外如果 regsvr32 用的是 32 位版本,注册到了 WOW6432Node,64 位系统上根本没生效。
解决:用 Process Explorer 或任务管理器检查 explorer 加载的 DLL 路径,确认注册表 InprocServer32 的字符串与当前编译输出一致;必要时在任务管理器里结束 explorer 再手动从任务管理器启动新 explorer。注意开发机上常见的“重启资源管理器”只是关掉所有窗口,explorer 进程退出后桌面和任务栏会暂时全消失,属于正常现象,再启动回来即可。最后养成习惯:每次改动后先看 clsid 键里的默认值,确认路径没有指向编译器中间目录。
6. 进阶:把工具条做成能感知现场状态的团队工具箱
前五章解决了“能不能显示”和“能不能点”,最后一章讲怎么把体验做细。这里最实用的一条经验是:不要在每个按钮点击时才去 QueryService 一遍站点,而是在 SetSite 时把 IShellBrowser 指针缓存住,并在每次 Activate 时刷新一次。资源管理器的活动文件夹会随标签页或窗口切换而变化,让你的按钮状态跟着当前视图走,用户才会觉得工具条属于窗口而不是悬浮的贴图。我在实践中维护一个 500ms 的定时器,只在窗口焦点变化时读取当前路径,避免高频调用 IShellFolder 接口带来卡顿。
验证方法上,我会在 SetSite、GetBandInfo、ShowDW 三个入口各写一条 OutputDebugString,用 DebugView 观察 explorer 调用的真实顺序。这比一遍遍重启资源管理器高效得多。发布前做一遍清单测试:首次显示尺寸、竖排停靠宽度、虚拟文件夹路径、explorer 重启后状态还原、32/64 位分别冒烟。每项都过一遍再推给团队,桌面扩展的“玄学崩溃”大多来自这些边界没压住。
如果要做成团队分发,最好补一个代码签名证书;无签名的 Shell 扩展容易被杀毒软件当作可疑加载器,企业内网批量分发时尤其常见。最后说个我的翻车经验:曾经在 GetBandInfo 里偷偷做了网络请求,去服务端拉取按钮文案,结果每次打开新窗口资源管理器都卡十几秒。不要把任何阻塞性工作放进 band 的接口链里,哪怕只慢 10ms,用户也会一眼看出资源管理器变“肉”了。把数据同步放到独立线程或者延迟到按钮首次点击时再做,这是一个桌面工具能否被用户接受的分水岭。希望这些经验帮你在自己的工具条方案上少走两圈弯路。
本文还有配套的精品资源,点击获取