# 第 ⑤ 章 · 图标处理器与图标叠加

> 用 IExtractIcon 让同类文件逐个显示不同图标（GIL_PERINSTANCE 是关键），并讲 IShellIconOverlayIdentifier 图标叠加与它著名的“15 个槽位”限制。

- 作者：David（道雾轩）
- 专栏：Windows Shell 扩展指南（https://daiw.org/manual/windows-shell-ext.md）
- 最后更新：2026-07-12
- 原文：https://daiw.org/manual/windows-shell-ext/icon
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

注册表能给一整类文件配一个固定图标，但**做不到逐文件不同**。要“这个 `.txt` 一个图标、那个 `.txt` 另一个图标”，就得写图标处理器。本章还捎带讲**图标叠加**（就是同步盘、Git 客户端在文件角上加的那个小箭头/对勾）。

配套代码：[`05-icon`](https://github.com/davidapp/shell-extension-demos/tree/main/05-icon)。

## IExtractIcon：逐文件图标

两个接口配合：`IPersistFile::Load` 拿到文件路径，`IExtractIconW` 决定图标。

```cpp title="IconHandler.cpp（节选）"
IFACEMETHODIMP IconHandler::GetIconLocation(UINT, PWSTR pszIconFile, UINT cchMax,
                                            int* piIndex, UINT* pwFlags) {
    ULONGLONG size = /* 用 GetFileAttributesEx 取 m_szFile 大小 */;
    wchar_t sysdir[MAX_PATH]; GetSystemDirectoryW(sysdir, ARRAYSIZE(sysdir));
    StringCchPrintfW(pszIconFile, cchMax, L"%s\\shell32.dll", sysdir);
    *piIndex = (size < 1024) ? 71 : (size < 1024*1024) ? 70 : 47;  // 按大小选图标
    *pwFlags = GIL_PERINSTANCE;   // ★ 关键
    return S_OK;
}
IFACEMETHODIMP IconHandler::Extract(PCWSTR, UINT, HICON* l, HICON* s, UINT) {
    if (l) *l = nullptr; if (s) *s = nullptr;
    return S_FALSE;   // 让 Shell 按上面给的“文件+索引”自己加载
}
```

<Callout type="warn">
  **`GIL_PERINSTANCE` 不能漏**。不置它，Shell 会认为“同类型文件图标都一样”，把图标按类型缓存成一个——逐文件就白做了。
</Callout>

`GetIconLocation` 返回一个“文件+索引”，`Extract` 返回 `S_FALSE` 让 Shell 自行加载即可；只有当图标需要**动态生成**（不是现成的资源）时，才在 `Extract` 里亲自返回 `HICON`。

注册在 ProgID 的 `shellex\IconHandler` 下：

```cpp
RegWriteString(HKEY_CLASSES_ROOT, L"txtfile\\shellex\\IconHandler", nullptr, clsidStr);
```

<Callout type="info">
  `IExtractIcon` 现在也不再强制 `IPersistFile`——现代可改用 `IInitializeWithItem` 拿 `IShellItem`。本例用 `IPersistFile` 因为它对 IconHandler 最通用、最省事。
</Callout>

## 图标叠加：IShellIconOverlayIdentifier

叠加图标不针对某类文件，而是**全局**注册、由系统对每个图标询问“你要不要在这个文件上叠个小图标？”。接口只有三个方法，且**不需要初始化接口**（路径直接给到 `IsMemberOf`）：

```cpp
class OverlayHandler : public IShellIconOverlayIdentifier {
    // 这个文件要不要叠加？（本例：只读文件才叠）
    IFACEMETHODIMP IsMemberOf(PCWSTR pwszPath, DWORD dwAttrib) {
        return (dwAttrib & FILE_ATTRIBUTE_READONLY) ? S_OK : S_FALSE;
    }
    // 叠加用哪个图标
    IFACEMETHODIMP GetOverlayInfo(PWSTR pwszIconFile, int cchMax, int* pIndex, DWORD* pdwFlags) {
        wchar_t sysdir[MAX_PATH]; GetSystemDirectoryW(sysdir, ARRAYSIZE(sysdir));
        StringCchPrintfW(pwszIconFile, cchMax, L"%s\\shell32.dll", sysdir);
        *pIndex = 47;
        *pdwFlags = ISIOI_ICONFILE | ISIOI_ICONINDEX;
        return S_OK;
    }
    IFACEMETHODIMP GetPriority(int* pPriority) { *pPriority = 50; return S_OK; }
};
```

注册在 **HKLM**（不是 HKCR）的 overlay 列表下：

```
HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\ShellIconOverlayIdentifiers\
    <名字>   (默认) = {CLSID}
```

<Callout type="warn">
  **著名的“15 个槽位”限制**：整个系统的图标叠加**总共只有 15 个名额**，超出的直接被忽略。而且这些项按**名字的字母序**排队——这就是为什么 OneDrive、Dropbox、TortoiseGit 的叠加名都带一堆前导空格（`  OneDrive`）来抢排在前面。改动 overlay 注册后必须**重启资源管理器**才生效。
</Callout>

> 叠加处理器的代码骨架（类工厂、DLL 导出、注册）和本章的图标处理器完全一样，只是实现的接口和注册位置不同——照着 `05-icon` 改即可。

## 参考

- [IExtractIcon](https://learn.microsoft.com/zh-cn/windows/win32/api/shobjidl_core/nn-shobjidl_core-iextracticonw) · [创建图标处理器](https://learn.microsoft.com/zh-cn/windows/win32/shell/how-to-create-icon-handlers)
- [IShellIconOverlayIdentifier](https://learn.microsoft.com/zh-cn/windows/win32/api/shobjidl_core/nn-shobjidl_core-ishelliconoverlayidentifier) · [实现图标叠加处理器](https://learn.microsoft.com/zh-cn/windows/win32/shell/how-to-implement-icon-overlay-handlers)
