Excel 2016中VSTO插件忙碌时如何让新实例正常启动?
我之前在做VSTO插件开发时,也碰到过Excel 2016这个让人头疼的实例互斥问题——尤其是用DispatcherFrame维持消息泵执行后台任务时,新启动的Excel死活不肯独立打开,非要粘到已有实例上。结合你的描述和我踩过的坑,给你拆解下问题原理和可行的解决办法:
问题根源分析
Excel的实例启动逻辑本来是这样的:当你从开始菜单、命令行或跳转列表启动新Excel时,系统会先检查**ROT(运行对象表)**里有没有已注册的Excel Application对象。如果有,就会通过OLE自动化机制请求已有实例处理这个启动请求——正常情况下,已有实例的消息泵能及时响应,然后根据用户操作(比如按住Ctrl启动会强制开新实例)决定是否打开独立实例。
但你的插件用DispatcherFrame接管了主线程的消息循环,这就出问题了:Excel原本用来处理跨实例启动请求的OLE消息/IPC调用,要么被你的自定义消息泵拦截、要么没被及时处理,导致新实例以为已有实例“无响应”,就一直卡在请求状态,而不是直接启动独立实例。
再说说你试过的方法为什么没用:
Application.IgnoreRemoteRequests:这个属性只对OLE自动化连接请求生效,而Excel 2016的实例启动检测逻辑已经不依赖这个属性了,所以设置了也白搭。- 子类化
Application.hWnd的WndProc:Excel 2016把实例间的通信从传统的窗口消息改成了更隐蔽的机制(比如WCF或者专用IPC窗口),所以你监听主窗口根本收不到相关消息。
可行的解决办法
方案1:临时从ROT移除当前实例(最稳妥)
既然新实例是通过ROT找到已有实例的,那我们在执行带消息泵的任务时,临时把当前Excel实例从ROT里移除,任务完成后再重新注册回去。这样新启动的Excel找不到已有实例,就会乖乖打开独立实例。
下面是C#的实现代码,你可以直接集成到插件里:
using System; using System.Runtime.InteropServices; using System.Windows.Threading; using Microsoft.Office.Interop.Excel; public static class RotManager { [DllImport("ole32.dll")] private static extern int GetRunningObjectTable(int reserved, out IRunningObjectTable prot); [DllImport("ole32.dll")] private static extern int CreateBindCtx(int reserved, out IBindCtx ppbc); [DllImport("ole32.dll")] private static extern int RegisterActiveObject([MarshalAs(UnmanagedType.IUnknown)] object punk, ref Guid rclsid, uint dwFlags, out int pdwRegister); [DllImport("ole32.dll")] private static extern int RevokeActiveObject(int dwRegister, IntPtr pvReserved); private static int _rotRegistrationCookie = -1; private static readonly Guid _excelAppClsid = typeof(Application).GUID; // 从ROT移除当前Excel实例 public static void UnregisterFromRot(Application excelApp) { if (_rotRegistrationCookie != -1) return; // 先注册拿到Cookie,再撤销注册 RegisterActiveObject(excelApp, ref _excelAppClsid, 0, out _rotRegistrationCookie); RevokeActiveObject(_rotRegistrationCookie, IntPtr.Zero); _rotRegistrationCookie = -1; } // 重新注册回ROT public static void RegisterBackToRot(Application excelApp) { if (_rotRegistrationCookie != -1) return; RegisterActiveObject(excelApp, ref _excelAppClsid, 0, out _rotRegistrationCookie); } } // 调用示例(在你的任务执行代码里) var excelApp = Globals.ThisAddIn.Application; RotManager.UnregisterFromRot(excelApp); try { // 执行带DispatcherFrame的任务 var dispatcherFrame = new DispatcherFrame(); _ = Task.Run(async () => { // 这里是你的异步任务逻辑 await YourLongRunningPluginTask(); // 任务完成后退出消息泵 dispatcherFrame.Continue = false; }); Dispatcher.PushFrame(dispatcherFrame); } finally { // 不管任务成功失败,都要把实例重新注册回ROT RotManager.RegisterBackToRot(excelApp); }
注意:一定要在finally块里重新注册回ROT,否则其他进程(比如Office其他组件、第三方工具)可能找不到当前Excel实例。
方案2:排查Excel 2016的WCF通信(适合深度排查)
如果你的场景需要更深入的根源解决,可以用Process Monitor跟踪新启动Excel进程的行为:看它是读取ROT,还是和已有实例建立WCF连接。如果是WCF,你可以用WCF跟踪工具(比如Microsoft Service Trace Viewer)捕获通信内容,然后在插件中临时禁用该WCF端点(不过这个方法风险较高,可能影响Excel的其他功能,谨慎使用)。
方案3:监听Excel的隐藏IPC窗口
你提到不知道怎么找其他窗口句柄,这里给你步骤:
- 打开Spy++(Visual Studio工具里自带),找到当前Excel进程的所有窗口,包括隐藏窗口。
- 在执行带消息泵的任务时,启动新Excel,用Spy++监听这些窗口的消息,看新实例的请求消息发送到了哪个窗口。
- 找到对应的窗口后,子类化该窗口的WndProc来处理实例启动请求。
Excel 2016大概率有一个专门处理IPC的隐藏窗口,而不是主窗口(Application.hWnd),所以你之前监听主窗口才没收到消息。
总结
最稳妥、影响最小的是方案1,临时移除ROT注册,直接切断新实例的检测路径。方案3适合你想彻底搞清楚问题根源时用,方案2风险较高,不推荐在生产环境使用。
内容的提问来源于stack exchange,提问作者tobriand

