Get a Collection of All Running Excel Instances
Get a collection of all running Excel interop Application objects (not just the active one)
Update
I've created a new article that describes how to do the same things as this article, but with a reusable assembly instead of everything in one class, and there is a demo proejct and walkthrough as well. Check it out here.
Introduction
If you write a lot of .NET Excel automation, particularly for use on servers where there may be multiple instances of Excel running at a time, you may find yourself needing to access a specific instance of Excel, not just the "active" one.
I have spent a lot of time on forums and Q&A sites trying to find a way to iterate over all running instances, and select specific instances by Hwnd
or ProcessID
, but have not yet found a satisfactory article. After a while of piecing different answers together, I believe I have a class that can provide this functionality for anyone in a similar situation.
A lot of credit goes to the anonymous article at the link below, as well as some hints from various users of StackOverflow.
http://pastebin.com/F7gkrASTBackground
You will need an intermediate grasp of C# for this project, and some familiarity with Windows processes and window handles. There is also some usage of LINQ and lambda expressions, but only in a few places. You actually don't need to know much about Excel automation, other than knowing what the Microsoft.Office.Interop.Excel.Application
class is.
Several parts of the private implementation of the class involve extern
calls to the Win32 API, which you don't necessarily need to understand to use this class. I am not very familiar with the Win32 API myself, but learned a good bit about it in putting this class together.
Please let me know if the code violates any best practices for dealing with Win32.
IMPORTANT: This code has not been tested on all versions of Excel or Windows. (Please help me test them all out.) I believe this code may be particularly prone to issues based on different Excel and Windows versions.
Tested environments:
- Windows 7 64-bit with Excel 2016 32-bit
- Windows 7 64-bit with parallel instances of Excel 2010 32-bit and Excel 2013 32-bit.
Using the Code
The class below can be used alongside the Microsoft primary interop assembly for Excel, to get a Microsoft.Office.Interop.Excel.Application
object for any running instance of Excel.
I've split the class into two partial class files, to breakup what would otherwise be a 200-line file. The first part is the public
interface, and the second is the private
implementation.
Public Interface
The publicly visible interface is pretty simple, and has the following members:
- Constructor - This takes a nullable
Int32
as a parameter, which defaults tonull
. The value is used to filter Excel instances by WindowssessionID
. Ifnull
, the class'sSessionID
property will be set to the currentsessionID
. SessionID
- This property is used to filter Excel instances by WindowssessionID
. This is very important when working with servers where multiple users may be using Excel at once.- If
-1
, the collection will give access to instances from all sessions. - If a valid
sessionID
, the collection will give access to all Excel instances running in that session. - If not a valid
sessionID
, the collection will always be empty. No exception is thrown.
- If
- Accessors
FromProcess
- This method takes a reference to aProcess
and returns the Excel instance of thatProcess
, ornull
if theProcess
is not an Excel instance.FromProcessID
- This method takes aprocessID
and returns the Excel instance of the correspondingProcess
, ornull
if the ID is invalid or does not correspond to an Excel instance.FromMainWindowHandle
- This method takes theHwnd
value of the main window of an Excel instance, and returns the corresponding Excel instance, ornull
if theHwnd
is invalid or does not correspond to an Excel instance.PrimaryInstance
- This property returns the first-created Excel instance, ornull
if there are none. If a user double-clicks an Excel file icon, this will be the instance the file opens in.TopMostInstance
- This property returns the Excel instance with the top-most visible window, ornull
if there are none. This will normally be the last instance selected by a user.
- Methods
GetEnumerator
- This method returns a collection of all Excel instances, filtering bySessionID
(ifSessionID
is not-1
).GetProcesses
- This method returns a collection of allProcess
objects of Excel instances, filtering bySessionID
(ifSessionID
is not-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
}
}
Private Implementation
As mentioned in the introduction, I am not an expert on the Win32 API. Parts of the private
implementation are still a bit mysterious to me and may violate best practices for using it. That being said, it has been reliable as far as I've used it.
- Methods
InnerFromProcess
- This method takes a reference to aProcess
and returns the correspondingMicrosoft.Office.Interop.Excel.Application
object.ChildHandleFromMainHandle
- This method takes theHwnd
of aProcess
orApplication
object and returns a child window'sHwnd
.InnerFromHandle
- This method takes theHwnd
of a child window of anApplication
object and returns theApplication
.GetWindowZ
- This method takes theHwnd
of a window and returns itsz
value.EnumChildFunc
- This method is used by theEnumChildWindows
method to get child windowHwnd
s.
- External Methods
AccessibleObjectFromWindow
- This method takes theHwnd
of an Excel window, as well as some of the constants below, and returns (through itsref
parameter) a reference to aWindow
object, which can then be used to get its parentApplication
object.- It does not work if you pass it the value of a
Application
'sHwnd
property; it must be a specific workbook's window'sHwnd
. This may only be the case on Excel 2013 or newer, where there is no main Excel window.
- It does not work if you pass it the value of a
EnumChildWindows
- This method takes theHwnd
of the main window of an Excel instance and anEnumChildCallback
delegate as parameters, and returns (through itsref
parameter) theHwnd
of a child window, which can be used byAccessibleObjectFromWindow
.GetClassName
- This method is used by theEnumChildCallback
delegate that is passed toEnumChildWindows
. I believe it gets the details of theWindow
class internally so that anHwnd
can be returned.GetWindow
- This method takes anHwnd
and a constant as parameters. The constant used determines how to get otherHwnd
s based on the providedHwnd
. UsingGW_HWNDPREV
returns theHwnd
of the window directly above (z position) the givenHwnd
. This is used to get theTopMostInstance
.
- Constants and Delegates
MarshalName
- This constant is required to get the "active" instance (PrimaryInstance
) from theSystem.Runtime.InteropServices.Marshal
class.ProcessName
- This constant is required to get Excel processes by name fromSystem.Diagnostics.Process
.ComClassName
- This constant is required forEnumChildFunc
method, which is used by theEnumChildWindow
method from the Win32 API.DW_OBJECTID
- This constant is required for theAccessibleObjectFromWindow
method from the Win32 API.GW_HWNDPREV
- This constant is required for getting windowz
(depth) values from theGetWindow
method from the Win32 API. I copied a bit of the Microsoft documentation into the code comments.rrid
- This pseudo-constant is required for theAccesibleObjectFromWindow
from the Win32 API.EnumChildCallback
- This delegate is implemented by theEnumChildFunc
method and is requied for theEnumChildWindow
method from the Win32 API.
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
}
}
Points of Interest
Please let me know if you find this class helpful (or terrible). I'm especially interested in issues with older version of Excel (pre-2013), multiple versions of Excel on one machine, or multiple users on one server. If you have any further insight into how the Win32 API is working behind the scenes, I would also like to know more about that. Any feedback is highly appreciated.
Further Developments
I have recently started working on a WPF application called ExcelBrowser that allows users to easily browse through multiple Excel instances, their workbooks, and sheets. Part of the implementation of this application is directly decended from the class described in this article. Check it out at github.com/JamesFaix/ExcelBrowser. Also, note that the solution uses C#6/.NET 4.6.1. As of writing this, I also need to catch up on some code comments, so bear with me.
The parts related to this article are in the ExcelBrowser.Interop project of the solution. All extern
methods are encapsulated in the NativeMethods
class, the Session
class represents a collection of all running Applications
and all running Processes
with the name "Excel". AppFactory
provides methods for getting specific Application
instances. Some other parts of this class are also in the ApplicationExtensionMethods
and ProcessExtensionMethods
classes.
History
- Added "Further Developments" section 11/20/16
- Posted 2/23/2016