# 第 ⑧ 章 · 属性表处理器（IShellPropSheetExt）

> 往文件「属性」对话框里加一个自定义标签页——IShellExtInit 拿文件、AddPages 用对话框模板造页、PSPCB 回调管生命周期。

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

右键文件 →「属性」弹出的那个多标签对话框，就是**属性表（property sheet）**。本章往里加一个自己的标签页。配套代码：[`08-propsheet`](https://github.com/davidapp/shell-extension-demos/tree/main/08-propsheet)。

## 一个初始化 + 一个页

- **`IShellExtInit::Initialize`** —— 老规矩，从 `IDataObject` 取出被查看属性的文件（见第 ① 章）。
- **`IShellPropSheetExt::AddPages`** —— Shell 在组装属性对话框时调它，你用 `CreatePropertySheetPage` 造一个页、交给回调 `lpfnAddPage`。

页本身是一个**子对话框**：模板放在 `.rc` 资源里，行为写在 `DlgProc` 里。

```cpp title="PropSheetHandler.cpp（节选）"
IFACEMETHODIMP PropSheetHandler::AddPages(LPFNADDPROPSHEETPAGE lpfnAddPage, LPARAM lParam) {
    PROPSHEETPAGE psp = {};
    psp.dwSize     = sizeof(psp);
    psp.dwFlags    = PSP_USETITLE | PSP_USECALLBACK;
    psp.hInstance  = g_hInst;
    psp.pszTemplate= MAKEINTRESOURCEW(IDD_PROPPAGE);
    psp.pszTitle   = L"Shell 示例";                        // 标签页标题
    psp.pfnDlgProc = PropPageDlgProc;
    psp.lParam     = (LPARAM)_wcsdup(m_szFile);            // 传给页的数据（路径副本）
    psp.pfnCallback= PropPageCallback;
    HPROPSHEETPAGE hPage = CreatePropertySheetPageW(&psp);
    if (!hPage) { free((void*)psp.lParam); return E_OUTOFMEMORY; }
    if (!lpfnAddPage(hPage, lParam)) { DestroyPropertySheetPage(hPage); return E_FAIL; }
    return S_OK;
}
```

`DlgProc` 在 `WM_INITDIALOG` 里从 `PROPSHEETPAGE.lParam` 取到路径，算出大小，`SetDlgItemText` 填进静态控件。

## 生命周期：PSPCB 回调

属性对话框是**异步**的——`AddPages` 返回后对话框才真正显示、用户看完才关闭。所以要用 `PSP_USECALLBACK` + 回调管好生命周期：

```cpp
static UINT CALLBACK PropPageCallback(HWND, UINT uMsg, LPPROPSHEETPAGE ppsp) {
    if (uMsg == PSPCB_ADDREF)  DllAddRef();                       // 页在，DLL 别被卸载
    else if (uMsg == PSPCB_RELEASE) { free((void*)ppsp->lParam); DllRelease(); }
    return 1;
}
```

<Callout type="warn">
  两个易漏点：**① DLL 保活**——页还开着时 DLL 不能被卸载，靠 `PSPCB_ADDREF`/`RELEASE` 里 `DllAddRef`/`DllRelease`。**② 释放传给页的数据**——`lParam` 里 `_wcsdup` 的路径副本要在 `PSPCB_RELEASE` 里 `free`，否则内存泄漏。
</Callout>

<Callout type="info">
  `.rc` 资源里**只放 ASCII**，避免 `rc.exe` 在中文 locale 下的编码坑；标题、正文等中文在运行时用 `pszTitle` / `SetDlgItemText` 填。这和第 ① 章 `/utf-8` 是同一类问题的两种应对。
</Callout>

## 注册

属性表处理器可挂多个，注册在 ProgID 的 `shellex\PropertySheetHandlers\<任取的名字>` 下：

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

## 参考

- [IShellPropSheetExt](https://learn.microsoft.com/zh-cn/windows/win32/api/shobjidl_core/nn-shobjidl_core-ishellpropsheetext) · [属性表处理器](https://learn.microsoft.com/zh-cn/windows/win32/shell/propsheet-handlers)
- [CreatePropertySheetPage](https://learn.microsoft.com/zh-cn/windows/win32/api/prsht/nf-prsht-createpropertysheetpagew)
