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; using xlApp = Microsoft.Office.Interop.Excel.Application; namespace ExcelExtensions { public partial class ExcelAppCollection : IEnumerable<xlApp> { #region Constructors public ExcelAppCollection (Int32? sessionID = null) { if (sessionID.HasValue && sessionID.Value < -1) throw new ArgumentOutOfRangeException("sessionID"); this.SessionID = sessionID ?? Process.GetCurrentProcess().SessionId; } #endregion #region Properties public Int32 SessionID { get; private set; } #endregion #region Accessors public xlApp FromProcess(Process process) { if (process == null) throw new ArgumentNullException("process"); return InnerFromProcess(process); } public xlApp FromProcessID(Int32 processID) { try { return FromProcess(Process.GetProcessById(processID)); } catch (ArgumentException) { return null; } } public xlApp FromMainWindowHandle(Int32 mainHandle) { return InnerFromHandle(ChildHandleFromMainHandle(mainHandle)); } public xlApp PrimaryInstance { get { try { return Marshal.GetActiveObject(MarshalName) as xlApp; } catch (COMException) { return null; } } } public xlApp TopMostInstance { get { var topMost = GetProcesses() .Select(p => p.MainWindowHandle) .Select(h => new { h = h, z = GetWindowZ(h) }) .Where(x => x.z > 0) .OrderBy(x => x.z) .First(); return FromMainWindowHandle(topMost.h.ToInt32()); } } #endregion #region Methods public IEnumerator<xlApp> GetEnumerator() { foreach (var p in GetProcesses()) yield return FromProcess(p); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } 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; 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; 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
I am a professional developer, but I really create software because it’s fun. I’ve always been interested in deconstructing complex systems, and software engineering has proven to be an inexhaustable supersystem of such systems. In the past I’ve also spent time focusing on music composition, audio engineering, electronics, game design, history, and philosophy.
My strongest languages are English and C#, in fact I’m a Microsoft Certified Professional for «Programming in C#». I do not have any certifications for English, so please trust me there. I’ve spent a lot of time working on Windows desktop applications, particularly for interacting with SQL Server or automating Microsoft Office programs, using technologies such as C#, VB.NET, VBA, T-SQL, WinForms, WPF, ADO.NET, the MS Office PIA’s, ExcelDNA, EPPlus, and Crystal Reports. I’ve also done some web development using JavaScript, HTML, CSS, TypeScript, ASP.NET, WCF, jQuery, and requirejs. I am very interested in functional programming (F#, Haskell, Clojure), and try to use C# and JavaScript in a «functional» way at times, but I haven’t had the opportunity to use a functional language for a serious project yet.
I use the following to check if two instances are running, and display a message. It could be altered to close other instance… This may be of help… I need code to return a specific instance, and return for use similar to GetObject(,»Excel.Application»)… I don’t think it possible though
If checkIfExcelRunningMoreThanOneInstance() Then Exit Function
In module (some of the declarations are possible used for other code):
Const MaxNumberOfWindows = 10
Const HWND_TOPMOST = -1
Const SWP_NOSIZE = &H1
Const SWP_NOMOVE = &H2
Type RECT
Left As Long
Top As Long
Right As Long
Bottom As Long
End Type
Public Declare Function ShowWindow Lib "user32" (ByVal hwnd As Long, ByVal nCmdShow As Long) As Long
Global ret As Integer
Declare Function GetWindow Lib "user32" (ByVal hwnd As Long, ByVal wCmd As Long) As Long
Private Declare Function SetWindowPos Lib "user32" (ByVal hwnd As Long, ByVal hWndInsertAfter As Long, ByVal x As Long, ByVal y As Long, ByVal cx As Long, ByVal cy As Long, ByVal wFlags As Long) As Long
Public Declare Function GetWindowRect Lib "user32" (ByVal hwnd As Long, lpRect As RECT) As Long
Declare Function GetKeyNameText Lib "user32" Alias "GetKeyNameTextA" (ByVal lParam As Long, ByVal lpBuffer As String, ByVal nSize As Long) As Long
Declare Function MapVirtualKey Lib "user32" Alias "MapVirtualKeyA" (ByVal wCode As Long, ByVal wMapType As Long) As Long
Declare Function GetDesktopWindow Lib "user32" () As Long
Public Declare Function GetWindowText Lib "user32" Alias "GetWindowTextA" (ByVal hwnd As Long, ByVal lpString As String, ByVal cch As Long) As Long
Declare Function GetWindowLong Lib "user32" Alias "GetWindowLongA" (ByVal hwnd As Long, ByVal nIndex As Long) As Long
Public Declare Function GetParent Lib "user32" (ByVal hwnd As Long) As Long
Private Declare Function FindWindow Lib "user32" _
Alias "FindWindowA" _
(ByVal lpClassName As String, _
ByVal lpWindowName As String) As Long
Private Const VK_CAPITAL = &H14
Private Declare Function GetKeyState Lib "user32" _
(ByVal nVirtKey As Long) As Integer
Private Declare Function OpenProcess Lib "kernel32" ( _
ByVal dwDesiredAccess As Long, ByVal bInheritHandle As Long, ByVal dwProcessId As Long) As Long
Private Declare Function CloseHandle Lib "kernel32" ( _
ByVal hObject As Long) As Long
Private Declare Function EnumProcesses Lib "PSAPI.DLL" ( _
lpidProcess As Long, ByVal cb As Long, cbNeeded As Long) As Long
Private Declare Function EnumProcessModules Lib "PSAPI.DLL" ( _
ByVal hProcess As Long, lphModule As Long, ByVal cb As Long, lpcbNeeded As Long) As Long
Private Declare Function GetModuleBaseName Lib "PSAPI.DLL" Alias "GetModuleBaseNameA" ( _
ByVal hProcess As Long, ByVal hModule As Long, ByVal lpFileName As String, ByVal nSize As Long) As Long
Private Const PROCESS_VM_READ = &H10
Private Const PROCESS_QUERY_INFORMATION = &H400
Global ExcelWindowName$ 'Used to switch back to later
Function checkIfExcelRunningMoreThanOneInstance()
'Check instance it is 1, else ask user to reboot excel, return TRUE to abort
ExcelWindowName = excel.Application.Caption 'Used to switch back to window later
If countProcessRunning("excel.exe") > 1 Then
Dim t$
t = "Two copies of 'Excel.exe' are running, which may stop in cell searching from working!" & vbCrLf & vbCrLf & "Please close all copies of Excel." & vbCrLf & _
" (1 Then press Alt+Ctrl+Del to go to task manager." & vbCrLf & _
" (2 Search the processes running to find 'Excel.exe'" & vbCrLf & _
" (3 Select it and press [End Task] button." & vbCrLf & _
" (4 Then reopen and use PostTrans"
MsgBox t, vbCritical, ApplicationName
End If
End Function
Private Function countProcessRunning(ByVal sProcess As String) As Long
Const MAX_PATH As Long = 260
Dim lProcesses() As Long, lModules() As Long, N As Long, lRet As Long, hProcess As Long
Dim sName As String
countProcessRunning = 0
sProcess = UCase$(sProcess)
ReDim lProcesses(1023) As Long
If EnumProcesses(lProcesses(0), 1024 * 4, lRet) Then
For N = 0 To (lRet 4) - 1
hProcess = OpenProcess(PROCESS_QUERY_INFORMATION Or PROCESS_VM_READ, 0, lProcesses(N))
If hProcess Then
ReDim lModules(1023)
If EnumProcessModules(hProcess, lModules(0), 1024 * 4, lRet) Then
sName = String$(MAX_PATH, vbNullChar)
GetModuleBaseName hProcess, lModules(0), sName, MAX_PATH
sName = Left$(sName, InStr(sName, vbNullChar) - 1)
If Len(sName) = Len(sProcess) Then
If sProcess = UCase$(sName) Then
countProcessRunning = countProcessRunning + 1
End If
End If
End If
End If
CloseHandle hProcess
Next N
End If
End Function
The I found:
Dim xlApp As Excel.Application
Set xlApp = GetObject("ExampleBook.xlsx").Application
Which gets the object if you know the name of the sheet currently active in Excel instance. I guess this could be got from the application title using the first bit of code. In my app I do know the filename.
The code you have provided is for getting the workbooks in the current application. I can have different Excel applications in my machine (each having one or many workbooks). I want to get all of them.
Try the code below. Not all my work as per the credit at the top. Suggest you use a new workbook and copy all of the code into a standard module and then run Sub GetAllWorkbookWindowNames()
Not the comments at the asterisk line re only output the workbook names or both workbook names and worksheet names.
‘Credit to:
‘http://stackoverflow.com/questions/2971473/can-vba-reach-across-instances-of-excel
‘(Modified by OssieMac to output to a worksheet in ThisWorkbook _
‘plus to output all workbooks open in each Excel Instance)
Declare Function FindWindowEx Lib «User32» Alias «FindWindowExA» _
(ByVal hWnd1 As Long, ByVal hWnd2 As Long, ByVal lpsz1 As String, _
ByVal lpsz2 As String) As Long
Declare Function GetClassName Lib «User32» Alias «GetClassNameA» _
(ByVal hWnd As Long, ByVal lpClassName As String, _
ByVal nMaxCount As Long) As Long
Declare Function IIDFromString Lib «ole32» _
(ByVal lpsz As Long, ByRef lpiid As UUID) As Long
Declare Function AccessibleObjectFromWindow Lib «oleacc» _
(ByVal hWnd As Long, ByVal dwId As Long, ByRef riid As UUID, _
ByRef ppvObject As Object) As Long
Type UUID ‘GUID
Data1 As Long
Data2 As Integer
Data3 As Integer
Data4(7) As Byte
End Type
Const IID_IDispatch As String = «{00020400-0000-0000-C000-000000000046}»
Const OBJID_NATIVEOM As Long = &HFFFFFFF0
Sub GetAllWorkbookWindowNames()
On Error GoTo MyErrorHandler
Dim hWndMain As Long
hWndMain = FindWindowEx(0&, 0&, «XLMAIN», vbNullString)
Do While hWndMain <> 0
GetWbkWindows hWndMain
hWndMain = FindWindowEx(0&, hWndMain, «XLMAIN», vbNullString)
Loop
Exit Sub
MyErrorHandler:
MsgBox «GetAllWorkbookWindowNames» & vbCrLf & vbCrLf & «Err = » _
& Err.Number & vbCrLf & «Description: » & Err.Description
End Sub
Private Sub GetWbkWindows(ByVal hWndMain As Long)
On Error GoTo MyErrorHandler
Dim hWndDesk As Long
hWndDesk = FindWindowEx(hWndMain, 0&, «XLDESK», vbNullString)
If hWndDesk <> 0 Then
Dim hWnd As Long
hWnd = FindWindowEx(hWndDesk, 0, vbNullString, vbNullString)
Dim strText As String
Dim lngRet As Long
Do While hWnd <> 0
strText = String$(100, Chr$(0))
lngRet = GetClassName(hWnd, strText, 100)
If Left$(strText, lngRet) = «EXCEL7» Then
GetExcelObjectFromHwnd hWnd
Exit Sub
End If
hWnd = FindWindowEx(hWndDesk, hWnd, vbNullString, vbNullString)
Loop
On Error Resume Next
End If
Exit Sub
MyErrorHandler:
MsgBox «GetWbkWindows» & vbCrLf & vbCrLf & «Err = » _
& Err.Number & vbCrLf & «Description: » & Err.Description
End Sub
Sub GetExcelObjectFromHwnd(ByVal hWnd As Long)
On Error GoTo MyErrorHandler
Dim fOk As Boolean
fOk = False
Dim iid As UUID
Call IIDFromString(StrPtr(IID_IDispatch), iid)
Dim obj As Object
If AccessibleObjectFromWindow(hWnd, OBJID_NATIVEOM, iid, obj) = 0 Then ‘S_OK
Dim objApp As Excel.Application
Set objApp = obj.Application
Dim wb As Workbook
For Each wb In objApp.Workbooks
With ThisWorkbook.Sheets(«Sheet1»)
‘Following line of code outputs workbook names only
‘Comment out the line and uncommment code between asterisk lines
‘to output the workbook name plus worksheet names
.Cells(.Rows.Count, 1).End(xlUp).Offset(1, 0) = wb.FullName
‘******************************************************************************
‘Dim myWorksheet As Worksheet
‘For Each myWorksheet In wb.Worksheets
‘ .Cells(.Rows.Count, 1).End(xlUp).Offset(1, 0) = wb.FullName
‘ .Cells(.Rows.Count, 1).End(xlUp).Offset(0, 1) = myWorksheet.Name
‘Next myWorksheet
‘***************************************************************************
End With
DoEvents
Next wb
fOk = True
End If
Exit Sub
MyErrorHandler:
MsgBox «GetExcelObjectFromHwnd» & vbCrLf & vbCrLf & «Err = » _
& Err.Number & vbCrLf & «Description: » & Err.Description
End Sub
Regards, OssieMac
Public Declare PtrSafe Function AccessibleObjectFromWindow Lib "oleacc" (ByVal hwnd As LongPtr, ByVal dwId As LongPtr, ByRef riid As Any, ByRef ppvObject As Object) As LongPtr Public Declare PtrSafe Function FindWindowEx Lib "USER32" Alias "FindWindowExA" (ByVal hWnd1 As LongPtr, ByVal Hwnd2 As LongPtr, ByVal lpsz1 As String, ByVal lpsz2 As String) As LongPtr Public Declare PtrSafe Function GetWindowThreadProcessId Lib "USER32" (ByVal hwnd As LongPtr, lpdwProcessId As Long) As Long
I feel quite strongly that API declares are rarely used to their full potential in VBA. People too often transliterate these declarations — rewriting C++ code in VBA, rather than translating them into idiomatic VBA. The Alias keyword and ByRef/ByVal modifiers can be used to achieve a higher standard of abstraction similar to what you would expect from other VBA code you write, and renaming variables to avoid Systems Hungarian notation which is not required in VBA can simplify the black box of WinApi calls as well. For example why do I need to know lpszFoo
is a long pointer to a zero terminated string when VBA literally defines native strings in that way? The code calling the API does not need to be aware of this low level information, get rid of it. I sometimes even declare the same function twice under different aliases to match the context in which it is called.
So I would modify the declares to something like this:
'Stolen from https://github.com/wine-mirror/wine/blob/08b01d8271fe15c7680a957778d506221d7d94eb/include/winuser.h#L3181-L3195
Private Enum ObjectIdentifier
OBJID_WINDOW = 0
OBJID_SYSMENU = -1
OBJID_TITLEBAR = -2
OBJID_MENU = -3
OBJID_CLIENT = -4
OBJID_VSCROLL = -5
OBJID_HSCROLL = -6
OBJID_SIZEGRIP = -7
OBJID_CARET = -8
OBJID_CURSOR = -9
OBJID_ALERT = -10
OBJID_SOUND = -11
OBJID_QUERYCLASSNAMEIDX = -12
OBJID_NATIVEOM = -16 'This is the one you use, &HFFFFFFF0
End Enum
Private Declare PtrSafe Function AccessibleObjectFromWindow Lib "oleacc" (ByVal hWnd As LongPtr, ByVal ID As ObjectIdentifier, ByRef interfaceRiid As Any, ByRef outInstance As Object) As LongPtr
Private Declare PtrSafe Function FindWindowEx Lib "USER32" Alias "FindWindowExA" (ByVal hWndParent As LongPtr, ByVal hWndChildAfter As LongPtr, ByVal className As String, Optional ByVal windowName As String) As LongPtr
Private Declare PtrSafe Function GetWindowThreadProcessId Lib "USER32" (ByVal hwnd As LongPtr, ByRef outProcessId As Long) As Long
- Everything
Private
because these are implementation details and don’t need to be exposed. - Introduce an
ObjectIdentifier
Enum to get rid of magic&HFFFFFFF0
later on. - In
AccessibleObjectFromWindow
, changeByVal dwId As LongPtr
toByVal ID As ObjectIdentifier
since dw stands for DWORD which is a 32-bit integer always, just like VBA’s Long (Enum is an alias for Long, so they can be used interchangeably). - Explicit
ByRef
keyword added to thelpdwProcessID
because it’s important it doesn’t become ByVal. - In fact if we say
ByRef
we can drop thelp
prefix since that’s whatByRef
means, and we can also dropdw
since that’s whatAs Long
means. - Use
outBlah
for variables which will be assigned by the API functions. - It’s fine to use Optional in these functions too if you always pass
vbNullString
I know you probably just copy-pasted from vb forums so moving on…
Function ProcIDFromHWnd(ByVal hwnd As Variant) As Variant Dim idProc As Variant GetWindowThreadProcessId hwnd, idProc ProcIDFromHWnd = idProc End Function
Instead, how about without variant:
Private Function ProcIDFromHWnd(ByVal hwnd As LongPtr) As Long
If GetWindowThreadProcessId(hwnd, outProcessId:=ProcIDFromHWnd) = 0 Then
Err.Raise 5, Description:="We didn't get a valid thread ID"
End If
End Function
Notice you can also use the return value to see if the function succeeded.
Dim hwnd As LongPtr, ChildHwnd As LongPtr, PID As LongPtr
I guess declare these closer to usage would be nice, although I see you use conditional compilation so it could get messy. Should be PID As Long
though, again DWORDs are always 32-bit.*
*Oversized variables still generally happen to work — because VBA is managing the memory so it gets allocated/ deallocated properly still, and integral types are little endian (or fill memory in reverse) meaning the first 32 bits of a Long or a LongLong with the same value will be the same.
On Error Resume Next PID = ProcIDFromHWnd(hwnd) Map.Add WindowObject.Application, CStr(PID) On Error GoTo 0
What error are you expecting? Better to remove so you can launch into the debugger when an error occurs.
Do hwnd = FindWindowEx(0, hwnd, CStr(ClassList(0)), vbNullString) If hwnd = 0 Then Exit Do ChildHwnd = hwnd For n = 1 To UBound(ClassList) ChildHwnd = FindWindowEx(ChildHwnd, 0, CStr(ClassList(n)), vbNullString) Next '[...] DoEvents Loop
This is kinda dangerous having an infinite loop with DoEvents
. What if that allows the Z-Order of windows to change, meaning hwnd = FindWindowEx(0, hwnd, CStr(ClassList(0)), vbNullString)
will never return 0 since you keep hopping between windows. Removing the DoEvents
would be good although the situation could still crop up where you have a circular reference because another application is misbehaving. Safer would be to keep a track of all hwnds you’ve seen and then exit the loop if one repeats.
Also the process of looping through top level windows with FindWindowEx(0, 0 |prevhwnd, specialClassName,"")
has a few too many re-assignments and could be cleaned up.
I would add maybe:
Private Function TryGetNextWindowHwnd(ByVal className As String, ByVal prevHWnd As LongPtr, ByRef outNextHWnd As LongPtr) As Boolean
outNextHWnd = FindWindowEx(0, prevHWnd, className)
TryGetNextWindowHwnd = outNextHWnd <> 0
End Function
… which you can use in the loop:
Do While TryGetNextWindowHwnd(TopLevelClassName, parentHWnd, outNextHWnd:=parentHWnd)
For n = 1 To UBound(ClassList) ChildHwnd = FindWindowEx(ChildHwnd, 0, CStr(ClassList(n)), vbNullString) Next
Maybe add a comment saying what this does, drilling down through window class names took me by surprise a little. Also CStr is redundant, and since ClassList(n) only goes from 1 to ubound, maybe ClassList(0) should be passed as its own argument:
Public Function InstanceMap(ByVal TopLevelClassName As String, ParamArray ClassList() As Variant) As Collection
Public Function ExcelInstanceMap() As Collection Set ExcelInstanceMap = InstanceMap("XLMAIN", "XLDESK", "EXCEL7") End Function Public Function WordInstanceMap() As Collection Set WordInstanceMap = InstanceMap("OpusApp", "_WwF", "_WwB", "_WwG") End Function Public Function PowerPointInstanceMap() As Collection Set PowerPointInstanceMap = InstanceMap("PPTFrameClass", "MDIClient", "mdiClass") End Function
Really nice simple interface. A shame it isn’t strongly typed as Excel.Application, Word.Application though. Still the iteration variable could be. Also why store PID as the key in a collection at all; since collection keys are not readable, the only reason I can think to use it would be to lookup the instance of the current process’ Application from the collection — but in that case you could just use the default Application
object.
Not sure it can be done with GetObject, but this is how you can do it with
the Windows API:Option Explicit
Public Declare Function GetDesktopWindow Lib «user32» () As Long
Public Declare Function GetWindow Lib «user32» _
(ByVal hwnd As Long, _
ByVal wCmd As Long) As Long
Public Declare Function GetWindowText Lib «user32» _
Alias «GetWindowTextA» _
(ByVal hwnd As Long, _
ByVal lpString As String, _
ByVal cch As Long) As Long
Public Declare Function GetClassName Lib «user32» _
Alias «GetClassNameA» _
(ByVal hwnd As Long, _
ByVal lpClassName As String, _
ByVal nMaxCount As Long) As Long
Public Const GW_HWNDFIRST = 0
Public Const GW_HWNDLAST = 1
Public Const GW_HWNDNEXT = 2
Public Const GW_HWNDPREV = 3
Public Const GW_OWNER = 4
Public Const GW_CHILD = 5Sub test()
Dim i As Long
Dim collWindows As CollectionSet collWindows = New Collection
FindWindowHwndLike 0, «XLDESK», «», 0, 0, collWindows
If collWindows.Count > 0 Then
For i = 1 To collWindows.Count
MsgBox collWindows(i)
Next
End IfEnd Sub
Function FindWindowHwndLike(hWndStart As Long, _
ClassName As String, _
WindowTitle As String, _
level As Long, _
lHolder As Long, _
collWindows As Collection) As LongDim hwnd As Long
Dim sWindowTitle As String
Dim sClassName As String
Dim r As LongIf level = 0 Then
If hWndStart = 0 Then
hWndStart = GetDesktopWindow()
End If
End If‘Increase recursion counter
‘—————————
level = level + 1‘Get first child window
‘———————-
hwnd = GetWindow(hWndStart, GW_CHILD)Do While hwnd > 0
‘Search children by recursion
‘—————————-
lHolder = FindWindowHwndLike(hwnd, _
ClassName, _
WindowTitle, _
level, _
lHolder, _
collWindows)‘Get the window text
‘——————-
sWindowTitle = Space$(255)
r = GetWindowText(hwnd, sWindowTitle, 255)
sWindowTitle = Left$(sWindowTitle, r)‘get the class name
‘——————
sClassName = Space$(255)
r = GetClassName(hwnd, sClassName, 255)
sClassName = Left$(sClassName, r)If (InStr(1, sWindowTitle, WindowTitle, vbBinaryCompare) > 0 Or _
sWindowTitle = WindowTitle) And _
(sClassName Like ClassName & «*» Or _
sClassName = ClassName) Then
FindWindowHwndLike = hwnd
lHolder = hwnd
collWindows.Add hwnd
End If‘Get next child window
‘———————
hwnd = GetWindow(hwnd, GW_HWNDNEXT)
LoopFindWindowHwndLike = lHolder
End Function
RBS
«Xav» <Xaviar.Goulay@uk.standardchartered.com> wrote in message
news:1135424972.276765.137810@g47g2000cwa.googlegroups.com…
> For my application I would like to retrieve all Excel instances
> runnning on a specific machine. Basically I want to use the GetObject
> method and stock it in an array or whatsoever. Does anybody have an
> idea?
> Thanks for helping.
>
INTELLIGENT WORK FORUMS
FOR COMPUTER PROFESSIONALS
Contact US
Thanks. We have received your request and will respond promptly.
Log In
Come Join Us!
Are you a
Computer / IT professional?
Join Tek-Tips Forums!
- Talk With Other Members
- Be Notified Of Responses
To Your Posts - Keyword Search
- One-Click Access To Your
Favorite Forums - Automated Signatures
On Your Posts - Best Of All, It’s Free!
*Tek-Tips’s functionality depends on members receiving e-mail. By joining you are opting in to receive e-mail.
Posting Guidelines
Promoting, selling, recruiting, coursework and thesis posting is forbidden.
Students Click Here
Find All Instances of excel and closeFind All Instances of excel and close(OP) 21 Nov 05 12:01 Please could someone point me in the right direction, I need to find all open excel documents and close them. If any are not saved, the user must be prompted and also the script must wait for all excel apps to be closed before proceeding with the rest of the code. This is a vague ‘stab in the dark’ at the code, but to be honest i’m just guessing from stuff i’ve found around the forum. This is my first day on vbScript !! (although I know vba if that’s any help) Any help much appreciated as usual. jimlad «There is more to life than simply increasing its speed.» Red Flag SubmittedThank you for helping keep Tek-Tips Forums free from inappropriate posts. |
Join Tek-Tips® Today!
Join your peers on the Internet’s largest technical computer professional community.
It’s easy to join and it’s free.
Here’s Why Members Love Tek-Tips Forums:
- Talk To Other Members
- Notification Of Responses To Questions
- Favorite Forums One Click Access
- Keyword Search Of All Posts, And More…
Register now while it’s still free!
Already a member? Close this window and log in.
Join Us Close
If you have an Excel workbook that has hundreds of worksheets, and now you want to get a list of all the worksheet names, you can refer to this article. Here we will share 3 simple methods with you.
Sometimes, you may be required to generate a list of all worksheet names in an Excel workbook. If there are only few sheets, you can just use the Method 1 to list the sheet names manually. However, in the case that the Excel workbook contains a great number of worksheets, you had better use the latter 2 methods, which are much more efficient.
Method 1: Get List Manually
- First off, open the specific Excel workbook.
- Then, double click on a sheet’s name in sheet list at the bottom.
- Next, press “Ctrl + C” to copy the name.
- Later, create a text file.
- Then, press “Ctrl + V” to paste the sheet name.
- Now, in this way, you can copy each sheet’s name to the text file one by one.
Method 2: List with Formula
- At the outset, turn to “Formulas” tab and click the “Name Manager” button.
- Next, in popup window, click “New”.
- In the subsequent dialog box, enter “ListSheets” in the “Name” field.
- Later, in the “Refers to” field, input the following formula:
=REPLACE(GET.WORKBOOK(1),1,FIND("]",GET.WORKBOOK(1)),"")
- After that, click “OK” and “Close” to save this formula.
- Next, create a new worksheet in the current workbook.
- Then, enter “1” in Cell A1 and “2” in Cell A2.
- Afterwards, select the two cells and drag them down to input 2,3,4,5, etc. in Column A.
- Later, put the following formula in Cell B1.
=INDEX(ListSheets,A1)
- At once, the first sheet name will be input in Cell B1.
- Finally, just copy the formula down until you see the “#REF!” error.
Method 3: List via Excel VBA
- For a start, trigger Excel VBA editor according to “How to Run VBA Code in Your Excel“.
- Then, put the following code into a module or project.
Sub ListSheetNamesInNewWorkbook() Dim objNewWorkbook As Workbook Dim objNewWorksheet As Worksheet Set objNewWorkbook = Excel.Application.Workbooks.Add Set objNewWorksheet = objNewWorkbook.Sheets(1) For i = 1 To ThisWorkbook.Sheets.Count objNewWorksheet.Cells(i, 1) = i objNewWorksheet.Cells(i, 2) = ThisWorkbook.Sheets(i).Name Next i With objNewWorksheet .Rows(1).Insert .Cells(1, 1) = "INDEX" .Cells(1, 1).Font.Bold = True .Cells(1, 2) = "NAME" .Cells(1, 2).Font.Bold = True .Columns("A:B").AutoFit End With End Sub
- Later, press “F5” to run this macro right now.
- At once, a new Excel workbook will show up, in which you can see the list of worksheet names of the source Excel workbook.
Comparison
Advantages | Disadvantages | |
Method 1 | Easy to operate | Too troublesome if there are a lot of worksheets |
Method 2 | Easy to operate | Demands you to type the index first |
Method 3 | Quick and convenient | Users should beware of the external malicious macros |
Easy even for VBA newbies |
Excel Gets Corrupted
MS Excel is known to crash from time to time, thereby damaging the current files on saving. Therefore, it’s highly recommended to get hold of an external powerful Excel repair tool, such as DataNumen Outlook Repair. It’s because that self-recovery feature in Excel is proven to fail frequently.
Author Introduction:
Shirley Zhang is a data recovery expert in DataNumen, Inc., which is the world leader in data recovery technologies, including sql fix and outlook repair software products. For more information visit www.datanumen.com
1 / 1 / 1 Регистрация: 29.05.2015 Сообщений: 13 |
|
1 |
|
21.09.2015, 23:48. Показов 5186. Ответов 3
Добрый день. Добавлено через 7 часов 5 минут
0 |
es_ 217 / 216 / 114 Регистрация: 14.01.2013 Сообщений: 459 |
||||
22.09.2015, 15:28 |
2 |
|||
Сообщение было отмечено юный_падаван как решение Решение Ну вот смотри, подсоединился ты к EXCEL процессу..
Если отрыты два файла екселя — это открыты не два процесса EXCEL, а две книги, и тебе остаётся выбрать нужную.
Если же у тебя действительно два процесса Excel, то нужно смотреть в сторону ID процесса, но интуиция подсказывает мне, что это не твой случай..
1 |
юный_падаван 1 / 1 / 1 Регистрация: 29.05.2015 Сообщений: 13 |
||||
22.09.2015, 15:48 [ТС] |
3 |
|||
Спасибо, теперь вкурил. Я был на полпути С помощью команды:
я подцепился в екселю, а вот пролистать все книги, чтобы найти нужную — не догадался.
0 |
NikitaF 15 / 1 / 2 Регистрация: 27.07.2015 Сообщений: 10 |
||||||||
06.10.2016, 11:08 |
4 |
|||||||
Сегодня столкнулся с почти такой же проблемой, только строчка:
или
Запускает новый экземпляр excel (отдельный процесс со своим PID) и возвращает на него ссылку.
0 |
-
#1
Hi, I have been trying to get a reference to Excel applications using oExcelApp = (Excel.Application)System.Runtime.InteropServices.Marshal.GetActiveObject(«Excel.Application»); in C#. This is so that I can use code to automatically save any open workbooks & close the first Excel application, and then do the same for the next Excel application, etc. However unlike the other Office apps (Word, etc) the reference to Excel seems to stay permanently (until the code is finished) ie I can’t get the reference to move on to the next Excel application. I have tried killing the Excel app after the first iteration, using threads, etc. Any ideas/thoughts to why this may be happenning?
Control Word Wrap
Press Alt+Enter to move to a new row in a cell. Lets you control where the words wrap.
-
#2
Why are there multiple instances of Excel open in the first place?
I’ve worked a little with automating Excel via C# and let’s just say it was a bit quirky.
It took me about a week to write a simple program to create a new worksheet in an existing workbook and list the shapes.
Legacy 98055
Guest
-
#3
I wonder if this is a garbage collection issue? Have you forced disposed?
-
#4
Hi Norie & Tom,
Thanks for getting back to me.
Norie — yes it seems quirky compared to other Office apps. The reason is in case someone has more than one Excel app open.
Tom — yes I have tried forcing disposal and releasing the com object, as follows:
oExcelApp.Quit();
System.Runtime.InteropServices.Marshal.ReleaseComObject(oExcelApp);
GC.GetTotalMemory(false);
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.GetTotalMemory(true);
-
#5
Stephen
I wasn’t referring to Excel being quirky, it was C# I meant.
I don’t think the behaviour you are seeing is down to Excel itself.
It could be for some other reason — automating any application isn’t always straightforward.
By the way I’m still wondering why you have multiple instances of Excel open.
One thing I’ve found in the past when automating is that if you don’t reference things properly you can find yourself with ‘ghost’ instances.
-
#6
Tom — yes I have tried forcing disposal and releasing the com object, as follows:
oExcelApp.Quit();
System.Runtime.InteropServices.Marshal.ReleaseComObject(oExcelApp);
GC.GetTotalMemory(false);
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.GetTotalMemory(true);
The prescribed cleanup I use is like this:
Rich (BB code):
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
oExcelApp.Quit();
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(oExcelApp);
Note that GC.Collect() and GC.WaitForPendingFinalizers() are called twice.
If you have a reference to any of the Excel workbooks (which you are saving) then additional cleanup will be required, ie Close workbook then call Marshal.FinalReleaseComObject(oExcelWkb) prior to closing the Excel application.
-
#7
Thanks Colin. I have tried as you suggested, but the instance of Excel is still showing in Task manager after all these GC and Quit commands. I also tried process.kill and this removed it, but got the error ‘The RPC server is unavailable’ when it tried to get the reference of the next Excel application. Any further ideas?
-
#8
Hi,
Please can you post the full code? Which version of C# are you using?
I don’t have VS to hand but I might spot something; if I can’t see anything I can have a play later when I get home from work.
Legacy 98055
Guest
-
#9
Excel does not register itself in the ROT. I wonder if this is causing the problem? Workbooks do register and I have found myself using the workbook object as the root object and simply referring to the parent when I need a reference to the application.
-
#10
Thanks Tom and Colin for your replies.
Colin — I have posted my code so far as below. Am using Visual C# 2008 and Visual Studio 2008.
Tom — yes this is something that I have come across before, and may well be the case. I have tried to get it to register the apps in the past ie once the first one is closed to move on to the next one, but wasn’t able to get it to move on. This was mainly because the first Excel app still existed in memory and showed up on task manager even when the application is quit. How would you recommend changing the code below / suggestions to refer to the workbooks in the first Excel application and then referring to workbooks on the next Excel application?
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Diagnostics;
using System.ComponentModel;
using Microsoft.Office.Interop.Excel;
using Excel = Microsoft.Office.Interop.Excel;
using System.Windows.Forms;
namespace Officedocs
{
class Excelproj
{
public static void ExcelClass()
{
Excel.
Application oExcelApp = null;
try
{
//To get reference to the Excel app
oExcelApp = (Excel.Application)System.Runtime
.InteropServices.Marshal.GetActiveObject(«Excel.Application»);
oExcelApp.DisplayAlerts =
false; //don’t display updates
Process[] processlist = Process.GetProcessesByName(«Excel»); //Shows number of running Excel apps
foreach (Process theprocess in processlist) //foreach Excel app running
{
if (oExcelApp.Workbooks.Count >= 0) //for worbooks in each Excel app
{
foreach (Excel.Workbook wkb in oExcelApp.Application.Workbooks)
{
//Save files using their own names in the specified folder
Object oSaveAsFileExcel1 = wkb.Name;
//Save each workbook
wkb.SaveAs(oSaveAsFileExcel1, Type.Missing, Type.Missing,
Type.Missing, Type.Missing, Type.Missing,
Excel.XlSaveAsAccessMode.xlNoChange, XlSaveConflictResolution.xlLocalSessionChanges,
true, Type.Missing, Type.Missing, Type.Missing);
wkb.Close(
true, null, null);
//Release the wbk object
Marshal.FinalReleaseComObject(wkb); //Release the Excel wkb object
}
//Close workbooks
oExcelApp.Workbooks.Close();
}GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
oExcelApp.Quit();
System.Runtime.InteropServices.
Marshal.ReleaseComObject(oExcelApp);
}
return;
}
catch //(Exception x)
{ }
}
}
}