获取所有正在运行的 Excel 实例
获取所有正在运行的 Excel Interop Application 对象的集合(不仅仅是活动的那个)
更新
我创建了一篇新文章,描述了如何像本文一样完成相同的事情,但使用了可重用的程序集而不是将所有内容放在一个类中,并且还有一个演示项目和教程。 在此处查看。
引言
如果您编写了大量的 .NET Excel 自动化代码,特别是在服务器上运行,而服务器上可能同时运行着多个 Excel 实例,您可能会发现需要访问特定的 Excel 实例,而不仅仅是“活动的”实例。
我花了很多时间在论坛和问答网站上寻找一种方法来遍历所有正在运行的实例,并通过 Hwnd
或 ProcessID
选择特定实例,但尚未找到令人满意的文章。经过一段时间的拼凑不同的答案,我相信我有一个类可以为处于类似情况的任何人提供此功能。
在此非常感谢以下链接中的匿名文章,以及 StackOverflow 上一些用户的提示。
http://pastebin.com/F7gkrAST背景
对于这个项目,您需要对 C# 有一定的理解,并熟悉 Windows 进程和窗口句柄。 还会使用一些 LINQ 和 lambda 表达式,但只在少数地方。 实际上,您不需要太多了解 Excel 自动化,除了知道 Microsoft.Office.Interop.Excel.Application
类是什么。
该类的私有实现中的几个部分涉及调用 Win32 API 的 extern
方法,您不一定需要理解它们才能使用这个类。 我自己对 Win32 API 并不太熟悉,但在构建这个类时学到了很多。
如果代码违反了处理 Win32 的任何最佳实践,请告诉我。
重要提示:此代码尚未在所有 Excel 或 Windows 版本上进行测试。 (请帮助我全部测试。) 我认为此代码可能特别容易在不同的 Excel 和 Windows 版本之间出现问题。
测试环境
- Windows 7 64 位,Excel 2016 32 位
- Windows 7 64 位,同时运行 Excel 2010 32 位和 Excel 2013 32 位实例。
Using the Code
下面的类可以与 Microsoft Excel 的主要互操作程序集一起使用,以获取任何正在运行的 Excel 实例的 Microsoft.Office.Interop.Excel.Application
对象。
我将该类拆分为两个部分类文件,以分解一个原本会是 200 行的文件。第一部分是 public
接口,第二部分是 private
实现。
公共接口
公共接口非常简单,并具有以下成员:
- 构造函数 - 接受一个可空的
Int32
作为参数,默认为null
。 该值用于按 WindowssessionID
过滤 Excel 实例。 如果为null
,则类的SessionID
属性将设置为当前sessionID
。 SessionID
- 此属性用于按 WindowssessionID
过滤 Excel 实例。 当处理服务器时,其中多个用户可能同时使用 Excel,这一点非常重要。- 如果为
-1
,则集合将提供对所有会话中实例的访问。 - 如果是一个有效的
sessionID
,则集合将提供对该会话中所有正在运行的 Excel 实例的访问。 - 如果不是有效的
sessionID
,则集合将始终为空。不会抛出异常。
- 如果为
- 访问器
FromProcess
- 此方法接受一个Process
的引用,并返回该Process
的 Excel 实例,如果Process
不是 Excel 实例,则返回null
。FromProcessID
- 此方法接受一个processID
,并返回相应的Process
的 Excel 实例,如果 ID 无效或与 Excel 实例不匹配,则返回null
。FromMainWindowHandle
- 此方法接受 Excel 实例主窗口的Hwnd
值,并返回相应的 Excel 实例,如果Hwnd
无效或与 Excel 实例不匹配,则返回null
。PrimaryInstance
- 此属性返回第一个创建的 Excel 实例,如果不存在则返回null
。 如果用户双击 Excel 文件图标,文件将在该实例中打开。TopMostInstance
- 此属性返回具有最顶层可见窗口的 Excel 实例,如果不存在则返回null
。 这通常是用户最后选择的实例。
- 方法
GetEnumerator
- 此方法返回所有 Excel 实例的集合,按SessionID
过滤(如果SessionID
不是-1
)。GetProcesses
- 此方法返回所有 Excel 实例的Process
对象集合,按SessionID
过滤(如果SessionID
不是-1
)。
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
//Don't add the entire interop namespace, it will introduce some naming conflicts.
using xlApp = Microsoft.Office.Interop.Excel.Application;
namespace ExcelExtensions {
/// <summary>
/// Collection of currently running Excel instances.
/// </summary>
public partial class ExcelAppCollection : IEnumerable<xlApp> {
#region Constructors
/// <summary>Initializes a new instance of the
/// <see cref="ExcelAppCollection"/> class.</summary>
/// <param name="sessionID">Windows sessionID to filter instances by.
/// If not assigned, uses current session.</param>
public ExcelAppCollection (Int32? sessionID = null) {
if (sessionID.HasValue && sessionID.Value < -1)
throw new ArgumentOutOfRangeException("sessionID");
this.SessionID = sessionID
?? Process.GetCurrentProcess().SessionId;
}
#endregion
#region Properties
/// <summary>Gets the Windows sessionID used to filter instances.
/// If -1, uses instances from all sessions.</summary>
/// <value>The sessionID.</value>
public Int32 SessionID { get; private set; }
#endregion
#region Accessors
/// <summary>Gets the Application associated with a given process.</summary>
/// <param name="process">The process.</param>
/// <returns>Application associated with process.</returns>
/// <exception cref="System.ArgumentNullException">process</exception>
public xlApp FromProcess(Process process) {
if (process == null)
throw new ArgumentNullException("process");
return InnerFromProcess(process);
}
/// <summary>Gets the Application associated with a given processID.</summary>
/// <param name="processID">The process identifier.</param>
/// <returns>Application associated with processID.</returns>
public xlApp FromProcessID(Int32 processID) {
try {
return FromProcess(Process.GetProcessById(processID));
}
catch (ArgumentException) {
return null;
}
}
/// <summary>Get the Application associated with a given window handle.</summary>
/// <param name="mainHandle">The window handle.</param>
/// <returns>Application associated with window handle.</returns>
public xlApp FromMainWindowHandle(Int32 mainHandle) {
return InnerFromHandle(ChildHandleFromMainHandle(mainHandle));
}
/// <summary>Gets the main instance. </summary>
/// <remarks>This is the oldest running instance.
/// It will be used if an Excel file is double-clicked in Explorer, etc.</remarks>
public xlApp PrimaryInstance {
get {
try {
return Marshal.GetActiveObject(MarshalName) as xlApp;
}
catch (COMException) {
return null;
}
}
}
/// <summary>Gets the top most instance.</summary>
/// <value>The top most instance.</value>
public xlApp TopMostInstance {
get {
var topMost = GetProcesses() //All Excel processes
.Select(p => p.MainWindowHandle) //All Excel main window handles
.Select(h => new { h = h, z = GetWindowZ(h) }) //Get (handle, z) pair per instance
.Where(x => x.z > 0) //Filter hidden instances
.OrderBy(x => x.z) //Sort by z value
.First(); //Lowest z value
return FromMainWindowHandle(topMost.h.ToInt32());
}
}
#endregion
#region Methods
/// <summary>Returns an enumerator that iterates through the collection.</summary>
/// <returns>
/// A <see cref="T:System.Collections.Generic.IEnumerator`1" />
/// that can be used to iterate through the collection.
/// </returns>
public IEnumerator<xlApp> GetEnumerator() {
foreach (var p in GetProcesses())
yield return FromProcess(p);
}
IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); }
/// <summary>Gets all Excel processes in the current session.</summary>
/// <returns>Collection of all Excel processing in the current session.</returns>
public IEnumerable<Process> GetProcesses() {
IEnumerable<Process> result = Process.GetProcessesByName(ProcessName);
if (this.SessionID >= 0)
result = result.Where(p => p.SessionId == SessionID);
return result;
}
#endregion
}
}
私有实现
如引言中所述,我不是 Win32 API 的专家。 private
实现的某些部分对我来说仍然有些神秘,并且可能违反了使用它的最佳实践。 尽管如此,在我使用它的范围内,它一直很可靠。
- 方法
InnerFromProcess
- 此方法接受一个Process
的引用,并返回相应的Microsoft.Office.Interop.Excel.Application
对象。ChildHandleFromMainHandle
- 此方法接受Process
或Application
对象的Hwnd
,并返回子窗口的Hwnd
。InnerFromHandle
- 此方法接受Application
对象的子窗口的Hwnd
,并返回Application
。GetWindowZ
- 此方法接受窗口的Hwnd
并返回其z
值。EnumChildFunc
- 此方法由EnumChildWindows
方法使用,用于获取子窗口Hwnd
。
- 外部方法
AccessibleObjectFromWindow
- 此方法接受 Excel 窗口的Hwnd
以及下面的一些常量,并通过其ref
参数返回一个Window
对象的引用,然后可以使用该引用获取其父Application
对象。- 如果传递
Application
的Hwnd
属性的值,它将不起作用;它必须是一个特定工作簿窗口的Hwnd
。 这可能仅在 Excel 2013 或更高版本中是这样,因为这些版本没有主 Excel 窗口。
- 如果传递
EnumChildWindows
- 此方法接受 Excel 实例主窗口的Hwnd
和一个EnumChildCallback
委托作为参数,并通过其ref
参数返回子窗口的Hwnd
,该Hwnd
可由AccessibleObjectFromWindow
使用。GetClassName
- 此方法由传递给EnumChildWindows
的EnumChildCallback
委托使用。 我认为它在内部获取Window
类的详细信息,以便返回一个Hwnd
。GetWindow
- 此方法接受一个Hwnd
和一个常量作为参数。 使用的常量决定了如何根据提供的Hwnd
获取其他Hwnd
。 使用GW_HWNDPREV
返回给定Hwnd
正上方的窗口(z 轴位置)的Hwnd
。 这用于获取TopMostInstance
。
- 常量和委托
MarshalName
- 从System.Runtime.InteropServices.Marshal
类获取“活动”实例(PrimaryInstance
)需要此常量。ProcessName
- 从System.Diagnostics.Process
按名称获取 Excel 进程需要此常量。ComClassName
-EnumChildFunc
方法需要此常量,该方法由 Win32 API 的EnumChildWindow
方法使用。DW_OBJECTID
- Win32 API 的AccessibleObjectFromWindow
方法需要此常量。GW_HWNDPREV
- Win32 API 的GetWindow
方法获取窗口z
(深度)值需要此常量。 我在代码注释中复制了一些 Microsoft 文档。rrid
- Win32 API 的AccesibleObjectFromWindow
需要此伪常量。EnumChildCallback
- 此委托由EnumChildFunc
方法实现,是 Win32 API 的EnumChildWindow
方法必需的。
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
//Don't import the entire namespace, this will cause name conflicts.
using xlApp = Microsoft.Office.Interop.Excel.Application;
using xlWin = Microsoft.Office.Interop.Excel.Window;
namespace ExcelExtensions {
public partial class ExcelAppCollection {
#region Methods
private static xlApp InnerFromProcess(Process p) {
return InnerFromHandle(ChildHandleFromMainHandle(p.MainWindowHandle.ToInt32()));
}
private static Int32 ChildHandleFromMainHandle(Int32 mainHandle) {
Int32 handle = 0;
EnumChildWindows(mainHandle, EnumChildFunc, ref handle);
return handle;
}
private static xlApp InnerFromHandle(Int32 handle) {
xlWin win = null;
Int32 hr = AccessibleObjectFromWindow(handle, DW_OBJECTID, rrid.ToByteArray(), ref win);
return win.Application;
}
private static Int32 GetWindowZ(IntPtr handle) {
var z = 0;
for (IntPtr h = handle; h != IntPtr.Zero; h = GetWindow(h, GW_HWNDPREV))
z++;
return z;
}
private static Boolean EnumChildFunc(Int32 hwndChild, ref Int32 lParam) {
var buf = new StringBuilder(128);
GetClassName(hwndChild, buf, 128);
if (buf.ToString() == ComClassName) {
lParam = hwndChild;
return false;
}
return true;
}
#endregion
#region Extern Methods
[DllImport("Oleacc.dll")]
private static extern Int32 AccessibleObjectFromWindow(
Int32 hwnd, UInt32 dwObjectID, Byte[] riid, ref xlWin ptr);
[DllImport("User32.dll")]
private static extern Boolean EnumChildWindows(
Int32 hWndParent, EnumChildCallback lpEnumFunc, ref Int32 lParam);
[DllImport("User32.dll")]
private static extern Int32 GetClassName(
Int32 hWnd, StringBuilder lpClassName, Int32 nMaxCount);
[DllImport("User32.dll")]
private static extern IntPtr GetWindow(IntPtr hWnd, UInt32 uCmd);
#endregion
#region Constants & delegates
private const String MarshalName = "Excel.Application";
private const String ProcessName = "EXCEL";
private const String ComClassName = "EXCEL7";
private const UInt32 DW_OBJECTID = 0xFFFFFFF0;
private const UInt32 GW_HWNDPREV = 3;
//3 = GW_HWNDPREV
//The retrieved handle identifies the window above the specified window in the Z order.
//If the specified window is a topmost window, the handle identifies a topmost window.
//If the specified window is a top-level window, the handle identifies a top-level window.
//If the specified window is a child window, the handle identifies a sibling window.
private static Guid rrid = new Guid("{00020400-0000-0000-C000-000000000046}");
private delegate Boolean EnumChildCallback(Int32 hwnd, ref Int32 lParam);
#endregion
}
}
关注点
如果您发现这个类有用(或糟糕),请告诉我。 我特别关注旧版本 Excel(2013 之前)、同一台机器上的多个 Excel 版本或服务器上的多个用户出现的问题。 如果您对 Win32 API 在后台的工作方式有任何进一步的见解,我也想了解更多。 非常感谢任何反馈。
进一步开发
我最近开始开发一个名为 ExcelBrowser 的 WPF 应用程序,该应用程序允许用户轻松地浏览多个 Excel 实例、它们的 Excel 工作簿和工作表。 该应用程序的实现部分直接继承自本文描述的类。 在 github.com/JamesFaix/ExcelBrowser 查看。 另外,请注意解决方案使用了 C#6/.NET 4.6.1。 在撰写本文时,我还需要更新一些代码注释,请耐心等待。
与本文相关的部分位于解决方案的 ExcelBrowser.Interop 项目中。 所有 extern
方法都封装在 NativeMethods
类中,Session
类代表所有正在运行的 Applications
和所有名为“Excel”的正在运行的 Processes
的集合。AppFactory
提供了获取特定 Application
实例的方法。 此类的其他部分也位于 ApplicationExtensionMethods
和 ProcessExtensionMethods
类中。
历史
- 添加“进一步开发”部分 11/20/16
- 发布于 2016/2/23