今天的 Windows 外壳集成——右键菜单、文件关联与 Windows 11 的变化
· 更新日期: · Go Komura · Windows, 外壳扩展, 上下文菜单, 文件关联, COM, Windows 11, 文件资源管理器, MSIX, Windows 开发
更新记录(仅首版,2026年08月20日 发布)
- 首次发布
引用本文(DOI(已登记存档): 10.5281/zenodo.22176280)
以下 DOI 指向先前登记的存档,内容可能与当前正文不同。引用当前正文时,请使用本页网址。
Go Komura(2026)。《今天的 Windows 外壳集成——右键菜单、文件关联与 Windows 11 的变化》。小村软件有限公司。 https://comcomponent.com/zh-CN/blog/windows-shell-integration-context-menu-file-association/
- DOI(已登记存档)
- 10.5281/zenodo.22176280
- DOI(上次登记版本)
- 10.5281/zenodo.22176281
“换到 Windows 11 之后,找不到自家应用的右键菜单了。”实际确认后会发现项并没有消失,打开菜单末尾的“显示更多选项”就能看到——这类咨询背后不是故障,而是 Windows 11 对菜单做的设计变更。在现场,它会变成“多点了一次”“找不到菜单项”这类问询。
不过,“让文件用这个应用打开”和“把自定义命令放进新的右键菜单”是两件不同的事。 不先把两者分开就动手写外壳扩展 DLL,本来只要注册关联就能满足的需求也会被做复杂。
本文是面向中小企业信息系统负责人和维护业务应用的 Windows 开发者的入门文章。从关联的基础,讲到新旧菜单的差异、实现方式的选择、安装程序中的注册与删除,再到故障的排查,把它们串成一条线来说明。
1. 先给出结论
关联和 verb 的底层没有变,Windows 11 变的是菜单的呈现方式,分成了新旧两套。 先确定“想实现什么”,再只挑出为此必需的机制。
只要“打开”的话,先从关联和静态 verb 考虑
文件关联的底层,是注册表里“扩展名键→ProgID→verb”这样的三层结构。扩展名键指向 ProgID,ProgID 下的 shell\<verb>\command 保存要启动的命令行。如果只是想实现“用这个应用打开”,现在依然靠这套注册和静态 verb 就够了,不需要外壳扩展 DLL。Microsoft 也建议先从能满足需求的最简单的静态 verb 开始考虑。12
注册位置方面,面向全体用户就写 HKLM,按用户就写 HKCU。HKCR 是把两者的 Classes 叠加起来的合并视图,所以写入时要明确指定 HKLM 或 HKCU,把 HKCR 当作查看用。3
另外,注册为候选和被选为默认应用是两回事。 双击时打开的默认应用由用户选择,操作系统会保护这个选择。安装程序不应擅自抢走默认值,应用这边的职责到“正确注册为候选”为止。4
要把自定义命令放进新菜单,就需要 IExplorerCommand 和包标识
在 Windows 11 中,传统的 IContextMenu 扩展被移到用“显示更多选项”(Shift+F10)打开的旧菜单一侧。要把自定义命令放到新菜单上,需要实现 IExplorerCommand 并具备包标识。56
官方路径是:把实现了 IExplorerCommand 的本机 DLL 通过 MSIX 清单(desktop4:FileExplorerContextMenus)注册。如果现有应用无法整体改成 MSIX,可以用 sparse package(带外部位置的 MSIX)只赋予标识。67
把 DLL 的安全性和卸载时的善后清理一并考虑进来
传统外壳扩展是会被加载进文件资源管理器等进程内部的 COM DLL。扩展崩溃或变慢会波及整个宿主。64 位宿主需要 64 位 DLL,而用托管代码实现进程内扩展不受支持。89
注册、修改、删除关联之后,要用 SHChangeNotify(SHCNE_ASSOCCHANGED) 发出通知。卸载时删除自家的 ProgID 等,而扩展名键的默认值保留不删,这是官方指引。外壳集成不只是把菜单显示出来,还包括让变更生效和事后清理。110
按目的选择读法
| 想了解的内容、正在发生的症状 | 阅读位置 |
|---|---|
| 该选哪种实现方式 | 第 6 章的决策表。先区分是只做关联,还是要往新菜单加自定义命令 |
| 想支持双击和“打开方式” | 第 2 章的关联和第 3 章的静态 verb |
| 已经注册却没成为默认应用 | 2.4 节的 UserChoice。把候选注册和用户的选择分开看 |
| Windows 11 上菜单被藏进了更深一层 | 第 5 章的新旧菜单。处理办法见 5.2 节,无法改成 MSIX 时见 5.3 节 |
| 安装程序里该注册和注销什么 | 第 7 章。尤其是 7.3 节的按用户注册和 7.4 节的善后清理 |
| 菜单不出现、出现两次、文件资源管理器卡顿 | 第 8 章的排查。DLL 的限制见第 4 章 |
想从机制学起可以从第 2 章依次往下读,想给现有应用定处理方针可以从第 6 章开始读。
图中实线表示始终成立的关系,虚线表示带条件的关系(成立条件写在详情页各关系的说明中)。关系的完整列表(共 16 条,附依据与确信度)以及主要概念的定义,汇总在知识地图详情页(日文)。数据:JSON-LD / Turtle
2. 文件关联的机制——扩展名键→ProgID→verb 的三层结构
本章按文件侧的注册 → 写入位置 → 应用侧的注册 → 用户的选择这个顺序梳理。扩展名指向 ProgID、verb 保存命令行、复杂的扩展以进程内 COM DLL 运行——这套底层二十多年没有变过。
2.1. 用一个例子读懂三层结构
先用一个例子看关联的基本形态。扩展名键指向 ProgID,ProgID 里的 verb 保存要启动的命令。1 用户已经选定默认应用时的优先关系,在 2.4 节说明。
HKEY_CLASSES_ROOT
.kmrpt ← (1) 扩展名键
(Default) = KomuraSoft.Report.1 ← 只是指向 ProgID 的指针
OpenWithProgids
KomuraSoft.Report.1 ← “打开方式”的候选
KomuraSoft.Report.1 ← (2) ProgID(关联的实体)
(Default) = 小村报表文档
DefaultIcon
(Default) = "C:\Program Files\KomuraSoft\Report.exe",0
shell ← (3) verb(动词)一览
open
command
(Default) = "C:\Program Files\KomuraSoft\Report.exe" "%1"
- (1) 扩展名键(
.kmrpt)只是用默认值指向 ProgID 的名字。在这里直接写命令是错的。 - (2) ProgID(
KomuraSoft.Report.1)才是关联的实体,持有显示名称、图标和 verb 列表。 - (3) verb 是“打开”“打印”这类动词,
shell\open\command的默认值就是实际启动的命令行。
有了这种分离,就可以把多个扩展名(例如 .kmrpt 和 .kmrpt-file)指向同一个 ProgID,也可以在应用升级时替换 ProgID。
flowchart TB
accTitle: 文件关联的三层结构
accDescr: 扩展名键只是用默认值指向 ProgID 的指针,ProgID 才是持有显示名称、图标和 verb 列表的实体,verb 下 command 的默认值就是实际启动的命令行
ext["扩展名键 .kmrpt"] -->|默认值指向 ProgID| pid["ProgID KomuraSoft.Report.1"]
pid --> vb["verb(shell 下的 open 等)"]
vb --> cmd["command 的默认值"]
cmd --> exe["启动 Report.exe"]
pid -.-> attr["同时持有显示名称和 DefaultIcon"]
图1:扩展名键是指针,ProgID 是实体,verb 的 command 才是实际启动的命令行。
2.2. HKCR 是“合并视图”——写在哪里,含义就不同
上面的例子用 HKEY_CLASSES_ROOT(HKCR)来展示,但HKCR 不是物理存储位置,而是把 HKLM\Software\Classes 和 HKCU\Software\Classes 叠加起来的合并视图。同一个键两边都有时,HKCU 一侧胜出。3
flowchart TB
accTitle: HKCR 是合并视图
accDescr: HKLM 和 HKCU 各自的 Classes 叠加起来就是 HKCR,同一个键两边都有时 HKCU 一侧优先,注册写入要明确指定 HKLM 或 HKCU 而把 HKCR 当作读取用
hklm["HKLM\Software\Classes(全体用户)"] --> hkcr["HKCR(合并视图)"]
hkcu["HKCU\Software\Classes(按用户)"] --> hkcr
hkcu -.-> win["同一个键存在时 HKCU 胜出"]
hkcr -.-> ro["当作读取(查看)用"]
图2:HKCR 是 HKLM 和 HKCU 的 Classes 叠加后的呈现,写入位置必须明确指定其中之一。
| 写入位置 | 含义 | 需要的权限 |
|---|---|---|
HKLM\Software\Classes |
全体用户共用的注册 | 管理员权限 |
HKCU\Software\Classes |
仅该用户的注册 | 不需要 |
直接写入 HKCR |
依据已有键所在的位置被分派到某一侧 | 视情况而定 |
在实际工作中,注册一定要明确写到 HKLM 或 HKCU 其中之一,把 HKCR 只当作读取(查看)用,这样最稳妥。
不要把关联数据和 COM 注册的位数混为一谈
在 WOW64 的注册表重定向中,关联数据和 COM 注册的处理并不相同。扩展名键、ProgID 这类位于 HKLM\Software\Classes 下的关联数据,从 Windows 7 起在 32 位/64 位注册表视图之间是共享的,即使由 32 位安装程序写入,也不会跑到 Wow6432Node 一侧去。
另一方面,Classes\CLSID 等 COM 注册相关的部分子键属于重定向对象,在后面要讲的外壳扩展(进程内 COM)注册中,分别按 32 位/64 位写入就有了意义。详细内容在“注册表的 32 位/64 位重定向与虚拟化陷阱”中讨论。
2.3. 应用侧的注册——App Paths、Applications、RegisteredApplications
与文件侧(扩展名和 ProgID)配对的应用侧注册也有三种。11
- App Paths(
HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths):让ShellExecuteEx只凭可执行文件名就能启动的注册。因为不用污染 PATH 环境变量,Microsoft 推荐用这种方式。 - Applications(
HKCR\Applications\<应用.exe>):定义通过“打开方式”被传入任意文件时的默认打开方式,以及应用的显示名称(FriendlyAppName)。 - RegisteredApplications + Capabilities:声明应用能处理的扩展名和 MIME 类型,让它作为候选出现在 Windows 默认应用设置页面中的注册。
“我们的应用不出现在默认应用列表里”这类咨询,大多是注册了 ProgID 却省略了这项 Capabilities 注册。
flowchart TB
accTitle: 应用侧的三种注册
accDescr: 应用侧的注册有 App Paths、Applications 和 RegisteredApplications 三种,分别承担只凭可执行文件名启动、通过打开方式打开时的默认方式,以及出现在默认应用设置页面上的职责
app["应用侧的注册"] --> ap["App Paths"]
app --> apps["Applications"]
app --> ra["RegisteredApplications"]
ap --> r1["只凭文件名启动"]
apps --> r2["打开方式的默认项"]
ra --> r3["出现在默认应用页面"]
r3 -.-> cap["前提是用 Capabilities 声明"]
图3:应用侧的注册有三种,要出现在默认应用页面的候选中就需要 Capabilities 注册。
2.4. 默认应用属于用户——UserChoice 的保护
即便在扩展名键的默认值里写了 ProgID,也不一定就能成为默认应用。用户通过“打开方式”等明确选择的结果会保存在 HKCU\...\Explorer\FileExts\<扩展名>\UserChoice,解析关联时以它为优先。
不要设计成直接改写 UserChoice
Windows 不支持由程序更改默认应用。 默认应用的设置被设计为由用户通过系统设置 UI 完成,UserChoice 的数据经过混淆,筛选器驱动程序(UCPD.sys)会阻止应用写入。在受管环境中,组策略/MDM 策略才是官方手段。4
过去之所以出现 SetUserFTA 这类“模仿哈希再改写”的工具,正是这层保护的反面。
安装程序要做的是“准备好被选中”
要嵌进自家应用安装程序的,是下面三件事。
- 正确注册 ProgID 和 verb。
- 把自己加入 OpenWithProgIds。
- 必要时把用户引导到默认应用设置页面。
不是抢走默认值,而是把用户可以选择的状态准备好。
flowchart TB
accTitle: 默认应用的解析与 UserChoice 的保护
accDescr: 用户明确选择的结果保存在 UserChoice 中并在解析关联时优先,来自应用的改写会被 UCPD.sys 拦截,因此安装程序能做的只到注册为候选和引导用户到设置页面为止
uc["UserChoice(用户的选择)"] -->|优先| res["关联的解析"]
ext["扩展名键的默认值"] --> res
wr["来自应用的改写"] -.->|UCPD.sys 拦截| uc
res ~~~ inst["安装程序的职责"]
inst --> a1["注册 ProgID 和 verb"]
inst --> a2["加入 OpenWithProgIds"]
inst --> a3["引导到设置页面"]
图4:解析关联时以用户的选择(UserChoice)为优先,来自应用的改写由操作系统拦截保护。
3. “打开”之外的 verb——print、edit、runas 与自定义 verb
3.1. 标准 verb 与自定义 verb
verb 不止 open 一个。操作系统知道其含义的标准 verb,除 open 外还有 edit、print、play、preview 等,标准 verb 会自动获得与操作系统区域设置相符的显示名称。双击时使用的默认 verb,按 shell 键的默认值→注册表中的第一个 verb→open→openwith 的顺序确定。12
flowchart TB
accTitle: 默认 verb 的确定顺序
accDescr: 双击时使用的默认 verb 按 shell 键的默认值、注册表中的第一个 verb、open、openwith 的顺序取最先找到的那个
s1["shell 键的默认值"] -->|没有则| s2["注册表中的第一个 verb"]
s2 -->|没有则| s3["open"]
s3 -->|没有则| s4["openwith"]
图5:双击时的默认 verb,按这个顺序取最先找到的那个。
想添加自定义动词时,就注册自定义 verb。
KomuraSoft.Report.1
shell
open
command
(Default) = "C:\Program Files\KomuraSoft\Report.exe" "%1"
print
command
(Default) = "C:\Program Files\KomuraSoft\Report.exe" /print "%1"
verify ← 自定义 verb
(Default) = 验证报表(&V) ← 菜单上的显示名称
command
(Default) = "C:\Program Files\KomuraSoft\Report.exe" /verify "%1"
3.2. 区分提权、按 Shift 时的显示,以及老旧的 DDE
verb 还有下面这些用法。
- 注册名为 runas 的 verb,可以定义相当于“以管理员身份运行”的提权启动,从
ShellExecute系列 API 指定runas启动时也会用到它。 - 在 verb 键上放一个名为
Extended的空值,它就变成只有按住 Shift 键右键单击时才显示的扩展 verb。适合用来隐藏很少用到的危险操作。12 - 老应用的关联里,有时还留着用 DDE(
ddeexec键)把文档送进已有进程的做法,但通过 DDE 启动 verb 已经是被弃用(Deprecated)的遗留产物。没有理由在新代码里再写它。12
3.3. 用引号把 EXE 路径和所选文件的路径括起来
最容易出问题的是命令行里的引号。只要命令字符串的组成部分可能含有空格,就必须用引号括起来。C:\Program Files\... 这样的 EXE 路径自不必说,%1(所选文件的路径)也应该始终写成 "%1"。因为无法保证用户的文件路径里不含空格。没有引号的 My Program.exe 会被解释成“以 Program.exe 为参数启动 My”。13
flowchart TB
accTitle: 命令行引号引发的问题
accDescr: 没有引号的命令会在空格处被切开,被误解释成以 Program.exe 为参数启动 My,因此可能含空格的 EXE 路径和表示所选文件路径的 %1 都要始终用引号括起来
c1["没有引号的 command"] -->|按空格切分| bad["被误解释为启动别的 EXE"]
c2["带引号的 command"] --> good["按预期启动"]
c2 -.-> q1["用引号括住 EXE 路径"]
q1 -.-> q2["%1 也始终用引号括住"]
图6:没有引号的命令会在空格处被错误切分,因此 EXE 路径和 %1 都要始终用引号括起来。
3.4. 写 DLL 之前,先确认静态 verb 是否够用
到这里为止只用注册表的机制(静态 verb),一个 DLL 都不用写就能实现,也没有让文件资源管理器变得不稳定的风险。Microsoft 自己也反复强调“在写外壳扩展之前,先考虑能否用满足需求的最简单的静态 verb 解决”。2
4. 传统外壳扩展——运行在文件资源管理器内部的 DLL
本章要抓住的一点是:传统的扩展 DLL 运行在文件资源管理器等进程的内部。 理解了它运行在哪里,稳定性、位数和实现语言上的限制就都串起来了。
4.1. 外壳扩展的种类
遇到静态 verb 满足不了的需求,例如“根据所选内容动态改变菜单”“替换图标或属性页面”,就要用外壳扩展处理程序。有代表性的种类如下。8
| 处理程序 | 主要接口 | 能做什么 |
|---|---|---|
| 上下文菜单处理程序 | IContextMenu + IShellExtInit | 动态添加和控制菜单项 |
| 图标处理程序 / 图标覆盖 | IExtractIcon / IShellIconOverlayIdentifier | 按文件设置图标、叠加显示 |
| 属性页处理程序 | IShellPropSheetExt | 向属性页面添加选项卡 |
| 缩略图 / 信息提示 | IThumbnailProvider / IQueryInfo | 缩略显示、悬停时的说明 |
| 拖放 / 复制挂钩处理程序 | IDropTarget / ICopyHook | 在放下时、复制移动时介入 |
这些都作为 COM 类来实现,并把 CLSID 注册到注册表。COM 本身的思路请参阅“COM / ActiveX / OCX 是什么”。
4.2. “进程内 COM 服务器”意味着什么
传统外壳扩展的本质,是被加载进文件资源管理器(以及任何打开了通用文件对话框的应用)进程内部的进程内 COM 服务器(DLL)。所有注意事项都由此推导出来。8
- 扩展一旦崩溃,文件资源管理器就会被连带拖垮。一旦挂起,右键单击就会卡住几秒。而且受影响的不止文件资源管理器,还包括所有显示过“打开文件”对话框的应用。
- 菜单的构建在 UI 线程上进行,因此不能在显示菜单时执行网络访问、文件 I/O 这类耗时处理。
- 线程模型原则上要注册为
Apartment。
flowchart TB
accTitle: 进程内扩展的连带影响结构
accDescr: 外壳扩展 DLL 不只会被加载进文件资源管理器,也会被加载进任何打开了文件对话框的应用进程,因此扩展的崩溃和挂起会波及整个宿主进程
dll["外壳扩展 DLL"] -->|加载进进程内| exp["文件资源管理器"]
dll -->|加载进进程内| any["打开对话框的任意应用"]
exp --> dmg["崩溃和挂起会波及"]
any --> dmg
dmg -.-> rule["显示时不做耗时处理"]
图7:扩展 DLL 运行在宿主进程内部,因此崩溃和挂起会波及整个宿主。
调查“打开某个文件夹时文件资源管理器就卡住”“右键单击要等 5 秒”这类咨询时,发现原因不在自家应用而在第三方外壳扩展的情况并不少见。排查方法在第 8 章讨论。
4.3. 位数要一致——64 位环境必须用 64 位 DLL
进程内 DLL 必须与加载它的进程位数一致。64 位 Windows 的文件资源管理器是 64 位进程,所以只编译成 32 位的外壳扩展 DLL 根本不会被加载,菜单里也完全不会出现。因为连错误都不报,它是“注册了却不出现”的典型原因。
并不是说应用本体也必须改成 64 位
32 位应用本体加 64 位外壳扩展 DLL 是合法的组合,但要注意 COM 注册会按位数分开(Wow6432Node)。另外,从 verb 的 command 启动的是另一个进程的 EXE,所以不受这个限制(保持 32 位 EXE 也没问题)。
flowchart TB
accTitle: 外壳扩展 DLL 的位数一致
accDescr: 64 位文件资源管理器只能加载 64 位的外壳扩展 DLL,只有 32 位的 DLL 不报错也不会出现在菜单里,而从 verb 的 command 启动的 EXE 属于另一个进程因此不受限制
exp["64 位文件资源管理器"] -->|能加载| d64["64 位外壳扩展 DLL"]
exp -.->|无法加载| d32["只有 32 位的 DLL"]
d32 -.-> sym["不报错但菜单里不出现"]
exe["由 verb 启动的 EXE"] -->|另一个进程| ok32["保持 32 位也没问题"]
图8:64 位文件资源管理器只会加载 64 位 DLL,而由 verb 启动的 EXE 不受这个限制。
4.4. 为什么不能用托管代码来写
经常有人问“能不能用 C# 写外壳扩展”,但Microsoft 明确表示,不推荐用托管代码(.NET)编写进程内的外壳扩展,并且不提供支持。9
原因在于扩展会被加载进任意进程这一性质。让宿主应用变得不稳定的因素主要有以下三个。
- CLR 版本冲突。 在 .NET Framework 4 之前尤其容易出问题。
- 重入。 存在等待锁时 CLR 重入消息循环的问题。
- 对象生存期。 垃圾回收带来的生存期非确定性,与 COM 的引用计数约定相冲突。
其中有些问题在 .NET Framework 4 之后和现代 .NET 上有所缓解,但官方立场没有改变。
实际工作中的指引很简单。进程内扩展用本机 C++ 写。想用托管代码,就做成由 verb 的 command 启动的普通 EXE,或者做成在另一个进程中运行的进程外扩展(预览处理程序等)。9
flowchart TB
accTitle: 能否使用托管代码的判断
accDescr: 运行在文件资源管理器进程内部的进程内扩展原则上用本机 C++ 编写,想用托管代码时就做成由 verb 的 command 启动的普通 EXE 或在另一个进程中运行的进程外扩展
q1{"运行在进程内部的扩展?"} -->|是| cpp["用本机 C++ 编写"]
q1 -->|否| mg["可以用托管代码"]
cpp -.-> why["CLR 冲突和重入会让宿主不稳定"]
mg --> e1["由 verb 启动的普通 EXE"]
mg --> e2["预览等另一个进程的扩展"]
图9:进程内扩展原则上用本机 C++,托管代码只限于在另一个进程中运行的形态。
5. Windows 11 的新上下文菜单——菜单的双层化
这里按移到旧菜单的原因 → 向新菜单注册 → 保留现有安装程序的办法的顺序说明。只做“用这个应用打开”的关联,与添加自定义命令之间的差别,在 5.4 节确认。
5.1. 发生了什么
Windows 11 重做了文件资源管理器的右键菜单。剪切、复制等变成了顶部的图标行,“打开”和“打开方式”被集中放到上部,应用追加的命令则被归到外壳标准命令的下方。同一个应用追加多个命令时,它们会被收进带应用名的浮出菜单(子菜单)里。5
而最关键的一点是:基于传统 IContextMenu 的外壳扩展并没有被删除,而是被移到了用“显示更多选项”(Shift+F10)打开的旧菜单一侧,那里原样加载了 Windows 10 的菜单。5 开头那条咨询里“菜单被藏起来了”,真身就是这种双层化。
flowchart TB
accTitle: Windows 11 中双层化的右键菜单
accDescr: 右键单击首先打开的是新菜单,能出现在那里的只有以 IExplorerCommand 加包标识注册的命令,传统 IContextMenu 扩展被移到用显示更多选项打开的旧菜单一侧
rc["右键单击文件"] --> newm["新菜单(Windows 11)"]
newm --> newi["IExplorerCommand+标识的命令"]
newm -->|显示更多选项 Shift+F10| oldm["旧菜单(Windows 10 的菜单)"]
oldm --> oldi["传统 IContextMenu 扩展"]
newi -.-> fly["多个命令收进浮出菜单"]
图10:新菜单上只有 IExplorerCommand+标识的命令,传统扩展被移到旧菜单一侧。
5.2. 放上新菜单的官方路径——IExplorerCommand+清单注册
把自定义命令放进新菜单的官方路径,是实现了 IExplorerCommand 的本机 DLL,加上在 MSIX 清单中注册。6
清单里要写两种声明
下面的例子,前半段声明COM 服务器(CLSID 与实现 DLL),后半段声明上下文菜单扩展(目标与命令)。把两者连起来的是同一个 CLSID。
<!-- 包清单(节选) -->
<com:Extension Category="windows.comServer">
<com:ComServer>
<com:SurrogateServer DisplayName="Komura commands">
<com:Class Id="01234567-89AB-CDEF-0123-456789ABCDEF"
Path="KomuraCommand.dll" ThreadingModel="STA" />
</com:SurrogateServer>
</com:ComServer>
</com:Extension>
<desktop4:Extension Category="windows.fileExplorerContextMenus">
<desktop4:FileExplorerContextMenus>
<desktop5:ItemType Type=".kmrpt">
<desktop5:Verb Id="VerifyReport"
Clsid="01234567-89AB-CDEF-0123-456789ABCDEF" />
</desktop5:ItemType>
</desktop4:FileExplorerContextMenus>
</desktop4:Extension>
ItemType 的 Type 除了具体扩展名,还可以指定 *(所有文件)、Directory(文件夹)、Directory\Background(文件夹背景)。DLL 要与文件资源管理器的体系结构(64 位/ARM64)保持一致。6
菜单构建方法要快速返回
IExplorerCommand 本身是从 Windows 7 时代就有的接口,需要实现标题(GetTitle)、图标(GetIcon)、启用/禁用/隐藏的状态(GetState)和执行(Invoke)。这些方法由 UI 线程调用,因此禁止访问网络资源,菜单构建类的方法必须快速返回。耗时处理放到 Invoke 之后再做。146
flowchart TB
accTitle: 新菜单注册的清单结构
accDescr: MSIX 清单中的 COM 服务器声明把 CLSID 和 DLL 对应起来,上下文菜单扩展的声明用 ItemType 和 Verb 把目标与实现连起来,自定义命令由此显示在新菜单上
man["MSIX 清单"] --> com["COM 服务器声明"]
man --> ctx["菜单扩展的声明"]
com -->|把 CLSID 与 DLL 对应起来| impl["IExplorerCommand 实现 DLL"]
ctx -->|用 ItemType 和 Verb 指定| impl
impl --> shown["新菜单上显示命令"]
ctx -.-> tgt["目标是扩展名或全部文件等"]
图11:清单中的两个声明把实现 DLL 和目标连起来,命令就出现在新菜单上。
5.3. 非打包应用的选择——用 sparse package 只取得标识
当情况是“我们的应用只能用 MSI 分发,改成 MSIX 不现实”时,变通办法就是 sparse package(带外部位置的 MSIX)。制作并签名一个不含应用本体、只有清单的小型 MSIX,在现有安装程序的最后一步注册它。这样应用就取得了包标识,上面那种清单注册(即显示在新菜单上)也就可行了。
它从 Windows 10 版本 2004 起可用,并且包必须用目标计算机信任的证书签名。7 注册与注销的顺序,以及“注册是按用户生效”这一点的注意事项,在 7.3 节讨论。
flowchart TB
accTitle: 用 sparse package 取得标识的流程
accDescr: 用现有安装程序放好应用本体之后,把只含清单的 sparse package 以带外部位置的方式注册,应用就取得包标识,向新菜单做清单注册也就可行了
inst["现有安装程序"] --> files["放置应用本体"]
sp["sparse package"] -.-> only["只有清单没有本体"]
files --> reg["以带外部位置的方式注册"]
sp --> reg
reg --> id["取得包标识"]
id --> ok["可以注册到新菜单"]
sp -.-> sign["需要受信任的签名"]
图12:把不含本体的 sparse package 以带外部位置的方式注册后,应用就取得包标识。
最大的好处是不用替换安装程序,对已有 MSI/EXE 安装程序资产的应用来说是务实的解法。与全面迁移到 MSIX 的比较,也可参阅“Windows 应用发布方式怎么选”。
5.4. 关联的 verb 在新菜单上是什么样子
这一点容易被误解:第 2、3 章讲的关联(ProgID 和 verb)在新菜单上依然有效。双击的默认 verb、“打开”和“打开方式”的候选都由关联解析而来,并显示在新菜单的上部。也就是说,如果只想“让文件能用这个应用打开”,在 Windows 11 上不需要任何额外处理。
另一方面,关联并不是通用的菜单扩展机制,所以想把任意自定义命令放到新菜单的第一层,就需要 IExplorerCommand+标识——两者就是这样分工的。6
flowchart TB
accTitle: 关联与新菜单的分工
accDescr: ProgID 和 verb 的关联在新菜单上仍然用于解析双击的默认 verb 以及打开和打开方式并显示在上部,而要把任意自定义命令放到新菜单的第一层则需要 IExplorerCommand 和标识
assoc["关联(ProgID 和 verb)"] --> sol["解析默认 verb 与打开"]
sol --> top["显示在新菜单上部"]
assoc -.-> keep["Windows 11 上也无需额外处理"]
cmd["任意的自定义命令"] --> need["IExplorerCommand+标识"]
need --> first["显示在新菜单第一层"]
图13:关联在新菜单上仍然负责“打开”一类的解析,只有自定义命令需要 IExplorerCommand+标识。
6. 实际工作中的决策表——三个选项该取哪一个
先确认 (a) 是否够用,如果需要把自定义命令显示到新菜单上就选 (b)。 暂时维持现有传统扩展的是 (c)。
| 想实现的目标 | 推荐的手段 | 在 Windows 11 上的呈现 | 需要的工作与成本 |
|---|---|---|---|
| (a) 想通过双击或“打开”启动自家应用 | 关联+静态 verb(只做注册表注册) | 整合显示在新菜单的“打开”“打开方式”中 | 只需安装程序做注册表注册。不需要 DLL,也没有额外的签名要求 |
| (b) 想把针对所选文件/文件夹的自定义命令放进新菜单 | 实现 IExplorerCommand+MSIX 清单注册。非打包应用用 sparse package 赋予标识 | 新菜单的第一层(多个命令收进带应用名的浮出菜单) | 本机 C++ DLL+包标识+代码签名 |
| (c) 继续使用现有的传统 IContextMenu 扩展 | 暂时原样维持(新开发不要选它) | 只出现在“显示更多选项”(Shift+F10)的旧菜单一侧 | 维持 64 位构建和 COM 注册。将来规划迁移到 (b) |
判断 1:选择能满足需求的最简单方法
首要的是:(a) 就能解决的需求,不要搬出 (b) 或 (c)。外壳扩展从写下的那一刻起,就要为文件资源管理器的稳定性负责。
判断 2:把维持旧菜单和改善易用性分开考虑
(c) 只是“没坏”,在用户体验上会一直处于低一档的位置。日常操作中使用频率越高的命令,迁移到 (b) 的投入产出比就越大。
flowchart TB
accTitle: 三个选项的选法
accDescr: 只想通过双击或打开来启动时用关联和静态 verb 就够了,要把自定义命令放进新菜单就用 IExplorerCommand 和 MSIX 清单注册,无法改成 MSIX 时用 sparse package 赋予标识,现有的传统 IContextMenu 扩展则在旧菜单一侧暂时维持
q1{"只要能打开就行?"} -->|是| pa["关联+静态 verb"]
q1 -->|否| q2{"要往新菜单放自定义命令?"}
q2 -->|是| q3{"能改成 MSIX 吗?"}
q3 -->|是| pb1["IExplorerCommand+MSIX"]
q3 -->|否| pb2["用 sparse package 取得标识"]
q2 -->|否| pc["暂时维持传统方式"]
pc -.-> old["只显示在旧菜单一侧"]
pa -.-> dllfree["不需要 DLL,风险小"]
图14:根据需求,从静态 verb、IExplorerCommand+标识、维持传统方式这三者中选择。
7. 部署与注册的实务——安装程序、sparse package 与清理
实现方式定下来之后,把注册位置、变更通知、包的注册与注销、卸载时的善后清理嵌进安装程序。
7.1. 写 HKLM 还是 HKCU
注册位置要与安装程序的形态一致。面向全体用户(放到 Program Files、需要管理员权限)就写 HKLM\Software\Classes,按用户安装(不提权)就写 HKCU\Software\Classes。混着用就会催生“甲能打开、乙打不开”这类问询。
对于伴随 CLSID 注册的外壳扩展,虽然在应用内部使用 COM 时,可以用 Reg-Free COM 这种免去注册表注册本身的选项,但它无法用于文件资源管理器要加载的外壳扩展,所以还是得走正规的注册(“什么是 Reg-Free COM”)。
7.2. 改完要发出通知——SHChangeNotify
注册、修改或删除关联之后,要用 SHChangeNotify 发出 SHCNE_ASSOCCHANGED 事件通知。省掉这一步,变更有时要等到重启后文件资源管理器才会识别到。110
// 在安装程序的自定义操作等处,于更改关联之后调用一次
SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr);
7.3. sparse package 的注册与注销
sparse package 的注册与注销是安装程序的职责。注册放在文件放置之后,注销放在文件删除之前。7
# 安装时:放好文件后,把安装目录注册为外部位置
Add-AppxPackage -Path "C:\Program Files\KomuraSoft\KomuraReport.identity.msix" `
-ExternalLocation "C:\Program Files\KomuraSoft"
# 卸载时:在删除文件之前注销包注册
Remove-AppxPackage <包的完整名称>
区分“注册给执行用户”和“面向全体用户部署”
Add-AppxPackage 是针对执行它的用户注册的。如果从 per-machine 的 MSI 自定义操作里以 LocalSystem 执行,执行安装的用户并不会获得标识。因此要做成以用户模拟(impersonate)方式执行的结构。
不过,即使用模拟方式注册,得到注册的也只有执行这次安装的那个用户。 一台机器由多个用户使用时,其他用户以及之后才创建的用户没有包标识,新菜单里不会出现命令。
若要让全体用户都能使用,就要准备按用户注册的机制,例如应用首次启动时检查自己的包注册状态,未注册则进行注册。卸载时也要把“从已注册的各个用户处注销”纳入计划。
注册之后菜单上没有生效时
清单注册要生效,有时需要重启文件资源管理器(或者注销登录)。6
flowchart TB
accTitle: sparse package 注册与注销的顺序
accDescr: 安装时在放置文件之后注册 sparse package,卸载时在删除文件之前注销注册,但要注意注册只对执行它的用户有效
i1["安装"] --> i2["放置文件"]
i2 --> i3["注册 sparse package"]
u1["卸载"] --> u2["注销包注册"]
u2 --> u3["删除文件"]
i3 -.-> pu["注册只对执行用户有效"]
图15:注册放在文件放置之后,注销放在文件删除之前,并注意注册是按执行用户生效的。
7.4. 卸载时的清理——删什么、留什么
关于卸载时的清理,官方指南给出了明确的方针。1
- 要删的:自家的整套 ProgID 键、Capabilities/RegisteredApplications 注册、外壳扩展的 CLSID 注册、sparse package(Remove-AppxPackage)。
- 要留的:扩展名键(
.kmrpt)的默认值。即使它仍指向自家 ProgID 也不要删除,这是官方的推荐做法。因为很难判断安装之后是否有别的应用取走了默认值,而 Windows 在默认值里的 ProgID 未注册时只会直接忽略,留着并没有实际危害。 - 清理的最后同样要调用 SHChangeNotify(SHCNE_ASSOCCHANGED)。
“已经卸载了菜单里却还有残留”的问题,多数出在这套清理设计的疏漏上。
flowchart TB
accTitle: 卸载时清理的设计
accDescr: 卸载时要删除自家的 ProgID 键、CLSID 注册和 sparse package,扩展名键的默认值因为未注册时会被忽略所以保留,清理的最后用 SHChangeNotify 通知变更
un["卸载"] --> del["要删的"]
un --> keep["要留的"]
del --> d1["ProgID 和 CLSID 注册"]
del --> d2["sparse package"]
keep --> k1["扩展名键的默认值"]
k1 -.-> why["未注册的 ProgID 会被忽略"]
d1 --> fin["最后用 SHChangeNotify 通知"]
k1 --> fin
图16:删除自家的注册,保留扩展名键的默认值,并在清理的最后通知变更。
8. 故障排查——不出现、出两次、卡顿
故障按“不出现”“出现两次、消不掉”“卡顿、崩溃”分开来查。最后说明如何在干净的环境里验证注册和善后清理。
8.1. 菜单里不出现
首先确认自己看的是新菜单还是旧菜单。 在此基础上,按下面的顺序排查。
- 看的是哪个菜单:旧方式的注册只会出现在 Shift+F10 的旧菜单一侧。先把两边都看一遍。
- 位数:只有 32 位的外壳扩展 DLL 不会被 64 位文件资源管理器加载(4.3 节)。
- 注册位置:HKLM/HKCU、Wow6432Node 搞混了。用
reg query确认实际的键。 - 包注册:面向新菜单时,用
Get-AppxPackage确认是否已注册、签名证书是否受信任、-ExternalLocation的路径是否正确,然后重启文件资源管理器。6 - 漏发通知:如果是忘了 SHChangeNotify,可以通过重启文件资源管理器后是否生效来判别。
flowchart TB
accTitle: 菜单不出现时的排查顺序
accDescr: 先确认自己看的是哪个菜单,再依次排查 DLL 的位数、注册表的注册位置、包注册与签名,以及是否漏发了 SHChangeNotify 通知
c1["确认是新菜单还是旧菜单"] --> c2["确认 DLL 的位数"]
c2 --> c3["确认 HKLM 和 HKCU 的注册位置"]
c3 --> c4["确认包注册与签名"]
c4 --> c5["用重启判别是否漏发通知"]
图17:“不出现”时,按所看的菜单、位数、注册位置、包注册、漏发通知的顺序排查。
8.2. 出现两次、消不掉
先根据在哪一侧菜单里重复来判断大致方向。
- 只在旧菜单里出现两次: 怀疑卸载时的清理疏漏(7.4 节),或者旧版本 ProgID 的残留。
- 新旧菜单里都出现: 怀疑旧方式的注册表注册与 MSIX 清单注册同时存在。
这两条都只是对典型原因的区分,最终要查看实际的注册内容再下判断。
flowchart TB
accTitle: 重复显示的排查
accDescr: 只在旧菜单里出现两次就大致归为残留类原因,新旧菜单里都出现就大致归为旧方式注册表注册与清单注册并存类原因
q{"在哪一侧出现两次"} -->|只有旧菜单| zan["残留类"]
q -->|新旧都有| hei["并存类"]
zan -.-> z1["清理疏漏或旧 ProgID 残留"]
hei -.-> h1["旧注册表注册与新注册并存"]
图18:只在旧菜单里重复就归为残留类,新旧菜单都出现就归为并存类。
8.3. 文件资源管理器卡顿、崩溃
右键单击慢、在特定文件夹崩溃时,先盘点已安装的外壳扩展。
- 列出扩展。 用 NirSoft 的 ShellExView 这类工具,确认非 Microsoft 出品的扩展。
- 临时禁用以缩小范围。 对可疑项做二分查找,定位出问题的 DLL。如果是崩溃,事件查看器里的“出错模块名称”也是线索。
- 如果是自家扩展,就检查菜单构建路径。 怀疑同步 I/O 和网络访问(4.2 节、5.2 节)。
flowchart TB
accTitle: 卡顿崩溃时定位出问题的 DLL
accDescr: 用 ShellExView 列出非 Microsoft 出品的外壳扩展,一边临时禁用可疑项一边做二分查找来定位出问题的 DLL,崩溃时还可以把事件查看器里的出错模块作为线索
s1["盘点外壳扩展"] --> s2["列出非 Microsoft 出品的"]
s2 --> s3["临时禁用并二分查找"]
s3 --> s4["定位出问题的 DLL"]
crash["崩溃的情况"] -.-> ev["确认出错模块"]
ev -.-> s4
图19:一边临时禁用非 Microsoft 扩展一边做二分查找,崩溃时并用事件查看器。
8.4. 验证时 Windows 沙盒很好用
外壳集成的验证,基本是确认“在干净的环境里安装→运行→卸载→零残留”。这里好用的是 Windows 沙盒(Pro/Enterprise/Education),每次启动都能在几秒内拉起一个全新的一次性 Windows,安装程序的注册与清理测试可以反复跑。关掉就全部消失,因此也适合排查注册表残留。15
9. 小结
有人因为换到 Windows 11 而说“菜单被藏起来了”时,请先用第 6 章的决策表确认想实现的到底是什么。思考顺序分为下面三步。
1. 判断关联是否够用
如果只是“用这个应用打开”,现在依然靠关联和静态 verb 就够。底层是扩展名键→ProgID→verb,HKCR 是把 HKLM/HKCU 的 Classes 叠加起来、供查看用的视图。要明确写入位置,%1 必须用引号括起来。
不过,选择默认应用的是用户。安装程序不要去抢默认值,而要把自己正确注册为候选。
2. 决定把自定义命令放到哪一侧菜单
传统 IContextMenu 扩展运行在用“显示更多选项”打开的旧菜单一侧。要把自定义命令放到新菜单上,需要IExplorerCommand 以及在 MSIX 清单中注册。无法整体改成 MSIX 的应用,用 sparse package 取得包标识是务实的解法。
传统扩展的 DLL 运行在文件资源管理器等进程的内部。崩溃和变慢会波及整个宿主,64 位宿主需要 64 位 DLL。用托管代码编写进程内扩展不受支持,原则上要用本机 C++ 实现。
3. 把注册、变更通知、注销当作一组来验证
注册、修改、删除关联之后要用 SHChangeNotify 发出通知。卸载时删除自家的 ProgID 等,保留扩展名键的默认值。sparse package 是按用户注册的,因此在多用户环境中,还要把面向各个用户的注册与注销纳入计划。
用 Windows 沙盒反复执行在干净的环境里安装→运行→卸载→确认残留这一整套流程。
从最简单的手段开始选,只在必要时才推进到向新菜单添加自定义命令。按这个顺序,就能理清工作的规模,以及需要维护的注册和实现的范围。
相关文章
- COM / ActiveX / OCX 是什么 - 区别与关系一并梳理
- 什么是 Reg-Free COM - 无需注册使用 COM 的机制
- 注册表的 32 位/64 位重定向与虚拟化陷阱 —— Wow6432Node 与“写入的值找不到了”问题
- Windows 应用发布方式怎么选 - MSI / MSIX / ClickOnce / xcopy / 自定义 updater
- DLL 与 COM 接口的向后兼容性 —— 判断哪些改动会破坏调用方的对照表
- Windows 应用程序兼容如何运作 —— 用兼容模式、Shim 与 Compatibility Administrator 延续旧应用
相关咨询领域
小村软件有限公司承接业务应用的文件关联、右键菜单与外壳扩展的设计和实现,Windows 11 新上下文菜单的适配(改用 IExplorerCommand、引入 sparse package),现有安装程序注册与清理的梳理,以及文件资源管理器卡顿、崩溃问题的原因调查。从“被藏进显示更多选项里的菜单该怎么办”这一层的方针确定开始谈也可以。
参考链接
-
Microsoft Learn, File Types。关于扩展名键指向 ProgID 的结构、OpenWithProgIds、HKLM 与 HKCU\Software\Classes 注册位置的区分使用、更改关联之后应调用 SHChangeNotify(SHCNE_ASSOCCHANGED),以及卸载时应删除 ProgID 但保留扩展名键默认值的说明。 ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Choosing a Static or Dynamic Shortcut Menu Method。关于应选择能满足需求的最简单的静态 verb 方式、IContextMenu 最强大但也最复杂并被归入不推荐的一侧,以及 IExplorerCommand/IExplorerCommandState 才是推荐方式。 ↩ ↩2
-
Microsoft Learn, HKEY_CLASSES_ROOT Key。关于 HKEY_CLASSES_ROOT 是 HKLM\Software\Classes 与 HKCU\Software\Classes 合并而成的视图、用户一侧的定义优先于计算机一侧,以及写入时的分派规则。 ↩ ↩2
-
Microsoft Learn, Windows app defaults platform。关于默认应用的更改被设计为只能通过系统设置 UI 完成、用户设置数据经过混淆并由筛选器驱动程序(UCPD.sys)做写入保护、基于注册表的更改不受支持,以及受管环境中应使用组策略/MDM 策略。 ↩ ↩2
-
Windows Developer Blog, Extending the Context Menu and Share Dialog in Windows 11。关于 Windows 11 新上下文菜单的设计、用 IExplorerCommand 加应用标识做扩展、“打开”与“打开方式”被放到上部、多个命令被收进带应用名的浮出菜单,以及传统 IContextMenu 扩展作为“显示更多选项”(Shift+F10)的 Windows 10 菜单被加载。 ↩ ↩2 ↩3
-
Microsoft Learn, Add a File Explorer context menu command to a packaged desktop app。关于注册到 Windows 11 新上下文菜单是通过实现 IExplorerCommand 加上 windows.comServer 与 desktop4:FileExplorerContextMenus 的清单声明完成、ItemType 可指定 *、Directory、Directory\Background、DLL 的体系结构要一致、菜单构建方法要保持快速、用 sparse package 支持非打包应用、注册生效有时需要重启文件资源管理器,以及文件关联并不是通用的菜单扩展机制。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8
-
Microsoft Learn, Grant package identity by packaging with external location。关于不改动现有安装程序即可注册带外部位置的包(sparse package)来取得包标识、从 Windows 10 版本 2004 起可用,以及由此能使用必须具备标识的 Windows 功能(上下文菜单注册、通知等)。 ↩ ↩2 ↩3
-
Microsoft Learn, Working with Shell Extensions。关于外壳扩展处理程序的种类、扩展是被加载进文件资源管理器(以及承载外壳的进程)的进程内 COM DLL 因而崩溃或挂起会波及整个 Explorer、以 ThreadingModel=Apartment 注册,以及应先考虑比外壳扩展更简单的替代手段。 ↩ ↩2 ↩3
-
Microsoft Learn, Guidance for Implementing In-Process Extensions。关于 Microsoft 不推荐也不支持用托管代码实现进程内外壳扩展、CLR 版本冲突与重入与对象生存期非确定性等原因,以及进程外扩展(预览处理程序或从 shell\verb\command 启动)允许使用托管代码。 ↩ ↩2 ↩3
-
Microsoft Learn, SHChangeNotify function。关于发出 SHCNE_ASSOCCHANGED 事件以把文件关联的更改通知给系统的方法,以及让外壳识别到更改的用法。 ↩ ↩2
-
Microsoft Learn, Application Registration。关于推荐用 App Paths 子键注册可执行文件、Applications 子键的职责、通过 SystemFileAssociations 注册 verb,以及更改默认应用时 ProgID 与相关信息的优先顺序。 ↩
-
Microsoft Learn, Verbs and File Associations。关于 verb 也用于 ShellExecuteEx、命令字符串中可能含空格的组成部分必须用引号括起来、”%1” 应始终带引号书写,以及在 HKCR\Applications 下注册默认处理过程。 ↩
-
Microsoft Learn, IExplorerCommand interface。关于 GetTitle、GetIcon、GetState、Invoke、EnumSubCommands 等方法构成、方法在 UI 线程上被调用因而不得与网络资源通信,以及从 Windows Vista 起可用。 ↩
-
Microsoft Learn, Windows Sandbox。关于可在几秒内启动一次性的隔离 Windows 环境、关闭后所有更改都被丢弃、适合软件测试与安装程序验证,以及在 Pro/Enterprise/Education 上可用。 ↩
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
Windows 打印驱动程序停止提供 ── 业务应用的报表与标签打印如何应对
Microsoft 正在分阶段推进 v3/v4 打印驱动程序的停止提供,从 2026 年 7 月起会优先选择 IPP 类驱动程序。本文梳理 Windows protected print mode 下会消失什么,并用判断表整理业务应用程序的报表、标签打印中依赖点的盘点方法与...
WinRT 就是 COM —— IInspectable、.winmd、语言投影,以及 WinUI 至今仍立在二进制契约之上的原因
WinRT 不是托管运行时,而是在 COM 之上加了元数据(.winmd)与语言投影的 ABI。本文从 IUnknown 与 IInspectable 的关系,一直讲到桌面应用里 HWND 初始化与 package identity 的卡点。
什么是 OLE 对象 —— 嵌入与链接的机制以及业务文档中的陷阱
在 Word 中嵌入 Excel 表格的功能,本质就是 OLE 对象。本文从嵌入与链接的区别、复合文件与结构化存储、In-Place Activation 的机制,一直讲到链接断开、文件膨胀与安全对策,全部立足于实务视角。
剪贴板与拖放的工作原理——在业务应用中正确处理 OLE 数据传输
粘贴 Excel 表格会散架、关掉复制源就贴不上,根源都是剪贴板把同一内容放成多种格式的机制。本文讲解标准格式、延迟渲染、OLE 拖放,直到剪贴板历史与云同步的策略。
委托开发 Windows 应用程序前该梳理的事项
在委托外包开发 Windows 应用程序之前,梳理现有软件改造、设备联动、COM/ActiveX、发布与更新、维护等需要注意的要点。
相关主题
与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。
Windows 技术主题
汇整 KomuraSoft LLC 关于 Windows 开发、故障调查与既有资产活用文章的主题中心。
ActiveX 迁移
整理保留、包装或替换 COM / ActiveX / OCX 资产的阶段性判断的主题页面。
与本主题相关的服务
本文与以下服务页面相关联,欢迎从最接近的入口查看。
Windows 应用程序开发
支持包含常驻处理、设备联动、运行日志与可维护结构的 Windows 桌面应用程序。
Windows 软件维护 & 现代化
支持既有 Windows 软件的阶段性升级、功能追加、64 位就绪以及可维护性的重构。
常见问题
汇总了咨询这一主题时常见的问题。
- 为什么自家应用的右键菜单项在 Windows 11 上只出现在“显示更多选项”里面?
- 因为 Windows 11 把文件资源管理器的右键菜单拆成了新旧两层。能把项放到新菜单上的,只有实现了 IExplorerCommand 接口并在 MSIX 包清单中注册(即具备包标识)的命令,基于传统 IContextMenu 的外壳扩展被移到了用“显示更多选项”(Shift+F10)打开的旧菜单一侧。扩展本身并没有坏掉,所以暂时仍能照常工作;但如果想让它出现在新菜单上,就需要迁移到 IExplorerCommand,并通过改用 MSIX 或 sparse package 来获得标识。
- 可以由安装程序把自家应用设成文件的默认应用(双击时打开的那个应用)吗?
- 不能。默认应用的选择被设计为由用户完成,Windows 不支持从系统设置 UI 以外的地方更改默认应用。保存各用户选择的 UserChoice 信息经过混淆,并且由筛选器驱动程序(UCPD.sys)阻止应用写入。安装程序能做的,只到注册 ProgID 和 verb、把自己加入 OpenWithProgIds 以出现在“打开方式”的候选中,以及把用户引导到默认应用设置页面为止。正确的实现不是抢走默认值,而是把被选中的准备做好。
- 可以用 C# 这类托管代码编写外壳扩展吗?
- Microsoft 已明确表示,用托管代码编写进程内加载的外壳扩展(上下文菜单处理程序、图标处理程序等)既不推荐也不提供支持。因为扩展会被加载进文件资源管理器,以及任何打开了通用文件对话框的应用进程中,CLR 版本冲突、重入以及对象生存期的非确定性都会让宿主应用变得不稳定。实现上原则是用本机 C++。另一方面,如果是由 verb 的 command 启动的普通 EXE,或在另一个进程中运行的进程外扩展(例如预览处理程序),用托管代码就没有问题。
- 什么是 sparse package(带外部位置的 MSIX)?
- 它是一个不含应用本体文件、只带清单(标识信息)的小型 MSIX 包。对于用现有安装程序(MSI、Inno Setup 等)正常安装的应用,只要用 Add-AppxPackage 的 -ExternalLocation 指向安装目录来注册,该应用就能取得包标识,从而使用注册到 Windows 11 新上下文菜单、Toast 通知等必须具备标识的功能。它从 Windows 10 版本 2004 起可用,并且包需要用目标计算机信任的代码签名。当你不想把分发方式全面迁移到 MSIX 又想支持新菜单时,它是务实的选择。
- 右键菜单项出现两次、或者怎么也消不掉时该怎么办?
- 首先作为排查的第一步,确认它出现在新菜单还是旧菜单(显示更多选项)里。重复显示的典型原因,是旧方式的注册表注册与 MSIX 清单注册同时存在,或者卸载时没有清理掉 ProgID、扩展的 CLSID 注册而留下了残留。更改关联之后,还要怀疑是否漏发了 SHChangeNotify(SHCNE_ASSOCCHANGED);刚注册完包时,则要怀疑是否漏了重启文件资源管理器。如果仍未解决,可以用 ShellExView 临时禁用非 Microsoft 出品的扩展并做二分查找,就能定位出问题的 DLL。