15

I want to use something similar to GetObject(,"Excel.Application") to get back the application I created.

I call CreateObject("Excel.Application") to create Excel instances. Later if the VBA project resets, due to debugging and coding, the Application object variables are lost but the Excel instances are running in the background. Kind of a memory leak situation.

I want to re-attach to either re-use (preferred way) or close them.

Community
  • 1
  • 1
NathaneilCapital
  • 1,389
  • 5
  • 17
  • 23
  • best to avoid the problem in the first place. See http://www.tushar-mehta.com/excel/vba/xl_doesnt_quit/index.htm – brettdj May 21 '15 at 05:50
  • Did you find anything posted useful? Please post feedback, vote and/or accept according to what you found. – sancho.s ReinstateMonicaCellio Jun 03 '15 at 08:46
  • 1
    None of the 4 answers below properly answer the question. The closest is Florent's [answer](https://stackoverflow.com/a/35343847/8112776), which lists workbooks (even if in multiple instances) but doesn't identify whether there are actually multiple instances running, or allow user to get `Application` objects for each instance (at least as far as I can tell). I haven't been able to find a way to actually list the number of instances. To clarify, an ***instance*** is not just "another workbook"; it's actually running the process in a separate section of memory, etc . . . – ashleedawg Oct 03 '18 at 07:06
  • . . . For example, a new instance of Excel can be opened by [holding ALT while opening a workbook](https://www.microsoft.com/microsoft-365/blog/2013/06/03/opening-workbooks-by-running-separate-instances-of-excel/), or by starting Excel via the [command line](https://support.office.com/article/command-line-switches-for-microsoft-office-products-079164cd-4ef5-4178-b235-441737deb3a6), or can be [forced for all workbooks with a registry tweak](https://support.microsoft.com/help/3165211/how-to-force-excel-to-open-in-a-new-instance-by-default) from Microsoft. – ashleedawg Oct 03 '18 at 07:06
  • @ashleedawg If i understand, xl.ActiveWorkbook.Application could be used, another option is full path file string returned from code by Florent B. that could be used to get access of the Application with GetObject function as proposed here https://stackoverflow.com/a/46141767/6406135 – robertocm May 19 '21 at 19:57

8 Answers8

27

To list the running instances of Excel:

#If VBA7 Then
  Private Declare PtrSafe Function AccessibleObjectFromWindow Lib "oleacc" ( _
    ByVal hwnd As LongPtr, ByVal dwId As Long, riid As Any, ppvObject As Object) As Long

  Private Declare PtrSafe Function FindWindowExA Lib "user32" ( _
    ByVal hwndParent As LongPtr, ByVal hwndChildAfter As LongPtr, _
    ByVal lpszClass As String, ByVal lpszWindow As String) As LongPtr
#Else
  Private Declare Function AccessibleObjectFromWindow Lib "oleacc" ( _
    ByVal hwnd As Long, ByVal dwId As Long, riid As Any, ppvObject As Object) As Long

  Private Declare Function FindWindowExA Lib "user32" ( _
    ByVal hwndParent As Long, ByVal hwndChildAfter As Long, _
    ByVal lpszClass As String, ByVal lpszWindow As String) As Long
#End If

Sub Test()
  Dim xl As Application
  For Each xl In GetExcelInstances()
    Debug.Print "Handle: " & xl.ActiveWorkbook.FullName
  Next
End Sub

Public Function GetExcelInstances() As Collection
  Dim guid&(0 To 3), acc As Object, hwnd, hwnd2, hwnd3
  guid(0) = &H20400
  guid(1) = &H0
  guid(2) = &HC0
  guid(3) = &H46000000

  Set GetExcelInstances = New Collection
  Do
    hwnd = FindWindowExA(0, hwnd, "XLMAIN", vbNullString)
    If hwnd = 0 Then Exit Do
    hwnd2 = FindWindowExA(hwnd, 0, "XLDESK", vbNullString)
    hwnd3 = FindWindowExA(hwnd2, 0, "EXCEL7", vbNullString)
    If AccessibleObjectFromWindow(hwnd3, &HFFFFFFF0, guid(0), acc) = 0 Then
      GetExcelInstances.Add acc.Application
    End If
  Loop
End Function
Florent B.
  • 41,537
  • 7
  • 86
  • 101
  • 2
    useful stuff, thanks :) should have been accepted by the **PO** – Shai Rado Aug 13 '17 at 11:57
  • 2
    Very cool +1 but to clarify, this isn't listing Excel *instances* - it's listing Excel *windows*. For example, if I have have two instances of Excel, the first with 2 workbooks open, the second with 1 workbook open, this will list the 3 windows, with [I think] no way to differentiate which are in which instance. – ashleedawg Oct 03 '18 at 06:43
  • @ashleedawg, this example lists all the instances for all the windows. You will end up with duplicated instances if one instance has more than one window. If you wish to list all the opened workbooks then read `acc.Parent` (`Dim wb As WorkBook` `Set wb = acc.Parent`). – Florent B. Oct 03 '18 at 14:07
  • 1
    I've discovered that (at least for me) the Function will NOT include an Excel Application object if the instance has no open workbooks (note: a hidden Personal workbook counts as an open workbook, as does any open XLA add-in, so need to close test to test). It seems this If comparison returns False if the Instance has no open workbooks: AccessibleObjectFromWindow(hwnd3, &HFFFFFFF0, guid(0), acc) = 0 Can anyone help with a correction that would include those? – M1chael Feb 06 '21 at 18:34
10

This would be best as a comment on Florent B.'s very useful function that returns a collection of the open Excel instances, but I don't have sufficient reputation to add comments. In my tests, the collection contained "repeats" of the same Excel instances i.e. GetExcelInstances().Count was larger than it should have been. A fix for that is the use of the AlreadyThere variable in the version below.

Private Function GetExcelInstances() As Collection
    Dim guid&(0 To 3), acc As Object, hwnd, hwnd2, hwnd3
    guid(0) = &H20400
    guid(1) = &H0
    guid(2) = &HC0
    guid(3) = &H46000000
    Dim AlreadyThere As Boolean
    Dim xl As Application
    Set GetExcelInstances = New Collection
    Do
        hwnd = FindWindowExA(0, hwnd, "XLMAIN", vbNullString)
        If hwnd = 0 Then Exit Do
        hwnd2 = FindWindowExA(hwnd, 0, "XLDESK", vbNullString)
        hwnd3 = FindWindowExA(hwnd2, 0, "EXCEL7", vbNullString)
        If AccessibleObjectFromWindow(hwnd3, &HFFFFFFF0, guid(0), acc) = 0 Then
            AlreadyThere = False
            For Each xl In GetExcelInstances
                If xl Is acc.Application Then
                    AlreadyThere = True
                    Exit For
                End If
            Next
            If Not AlreadyThere Then
                GetExcelInstances.Add acc.Application
            End If
        End If
    Loop
End Function
Philip Swannell
  • 895
  • 7
  • 17
  • Cool answer but this doesn't list all of the open instances. For example I tested with two instances open, one with 1 workbook, one with 2 workbooks, and your modification only listed 2 of the workbooks. I think it's not listing unsaved workbooks (like the original answer does - but it also doesn't differentiate between instances) – ashleedawg Oct 03 '18 at 06:47
  • 1
    Let's not confuse Application objects with Workbook objects. My version of FlorentB's function returns a collection of Application objects, as per the original question. So if there are two instances of Excel running (the second launched with the ALT key held down) then the function returns a two-element collection. The number of workbooks in each application's workbooks collection is not relevant. Of course it would be possible to write a nested loop that loops over the workbooks collection of each application object. – Philip Swannell Nov 07 '18 at 21:21
  • 1
    This is brilliant and does exactly what it should. No idea why someone else has downvoted it. Probably don't understand it! – M1chael Nov 20 '18 at 14:38
  • I've discovered that (at least for me) the Function will NOT include an Excel Application object if the instance has no open workbooks (note: a hidden Personal workbook counts as an open workbook, as does any open XLA add-in, so need to close test to test). It seems this If comparison returns False if the Instance has no open workbooks: AccessibleObjectFromWindow(hwnd3, &HFFFFFFF0, guid(0), acc) = 0 Can anyone help with a correction that would include those? – M1chael Feb 06 '21 at 18:26
1

@PGS62/@Philip Swannell has the correct answer for returning a Collection; I can iterate all instances; and it is brilliant, as @M1chael comment.

Let's not confuse Application objects with Workbook objects... ...Of course it would be possible to write a nested loop that loops over the workbooks collection of each application object

This is the nested loop implemented and fully functional:

Sub Test2XL()
  Dim xl As Excel.Application
  Dim i As Integer
  For Each xl In GetExcelInstances()
    Debug.Print "Handle: " & xl.Application.hwnd
    Debug.Print "# workbooks: " & xl.Application.Workbooks.Count
    For i = 1 To xl.Application.Workbooks.Count
        Debug.Print "Workbook: " & xl.Application.Workbooks(i).Name
        Debug.Print "Workbook path: " & xl.Application.Workbooks(i).path
    Next i
  Next
  Set xl = Nothing
End Sub

And, for Word instances, the nested loop:

Sub Test2Wd()
  Dim wd As Word.Application
  Dim i As Integer
  For Each wd In GetWordInstancesCol()
    Debug.Print "Version: " & wd.System.Version
    Debug.Print "# Documents: " & wd.Application.Documents.Count
    For i = 1 To wd.Application.Documents.Count
        Debug.Print "Document: " & wd.Application.Documents(i).Name
        Debug.Print "Document path: " & wd.Application.Documents(i).path
    Next i
  Next
  Set wd = Nothing
End Sub

For Word you have to use what is explained in the end of this thread

0

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.

Sam Smith
  • 13
  • 3
0

I always prefer using API functions only as a LAST resort. I have devised a method which will work as long as the format is similar to this reveal. Here is the full solution without using API commands:

It is quite simple actually. In any one of the workbooks expected to be loaded within each application instance, you must store a public subroutine which will serve a very basic purpose.

Each subroutine will exist solely as a link in an overall programmatic chain. Each "link" will add the instance of the current application to a collection object which is being passed between subroutines until the "chain" is complete.

Step 1. Programmatically create a new instance of excel.

Step 2. Assign a workbook variable to the workbooks open method for that new app.

Step 3. WBVariable.Application.Run "Subroutine", apps

You can see in step 3. that the apps collection is being passed as a variable to the workbook that has been loaded in a separate application instance. Once, the "catcher" subroutine receives this collection object, that subroutine can then add the current application object to the collection. Steps 2 and 3 can be repeated in each predetermined "link" until stopping at its final destination.

The final instance can theoretically even be sent to a "catcher" subroutine in the original workbook or perhaps recursively send the final collection object through an optional argument into the originating subroutine, at which point checks may permit the subroutine to now continue past the previous point.

It may sound complicated, but with a little bit of ingenuity, this is incredibly easy to achieve without API calls.

-1

Create an array of objects and store the newly created Excel.Application in the array. That way you can reference them as and when you need. Let's take a quick example:

In a module:

Dim ExcelApp(2) As Object

Sub Test()
    Set ExcelApp(1) = CreateObject("Excel.Application")
    ExcelApp(1).Visible = True

    Set ExcelApp(2) = CreateObject("Excel.Application")
    ExcelApp(2).Visible = True
End Sub

Sub AnotherTest()
    ExcelApp(1).Quit
    ExcelApp(2).Quit
End Sub

Run Test() macro and you should see two Excel Applications pop up. Then run AnotherTest() and the Excel Applications will quit. You can even set the array to Nothing after you are done.

You can get handle of running Excel applications using the script published on http://www.ozgrid.com/forum/showthread.php?t=182853. That should get you where you want to go.

zedfoxus
  • 35,121
  • 5
  • 64
  • 63
  • While I do store them in variables. But sometimes I need to change other part of the VBA program. The VBA project sometimes got reset and all variables are lost.But the launched Excel instances are still running in background. – NathaneilCapital May 21 '15 at 03:08
  • That is tricky. GetObject and finding Window Handle are a couple of options. sancho.s has a some links in that answer that you could use. – zedfoxus May 21 '15 at 03:47
-1

This can accomplish what you want. Determine if an instance of Excel is open:

Dim xlApp As Excel.Application
Set xlApp = GetObject(, "Excel.Application")

If an instance is running you can access it using the xlApp object. If an instance is not running you will get a run-time error (you might need/want an error handler). The GetObject function gets the first instance of Excel that had been loaded. You can do your job with it, and to get to others, you can close that one and then try GetObject again to get the next one, etc. So you will be attaining your ok-but-second-preferred objective (taken from http://excelribbon.tips.net/T009452_Finding_Other_Instances_of_Excel_in_a_Macro.html).

For attaining your preferred objective, I think that https://stackoverflow.com/a/3303016/2707864 shows you how.

Community
  • 1
  • 1
-1

You should use this code every time you need an Excel application object. This way, your code will only ever work with one application object or use a pre-existing one. The only way you could end up with more than one is if the user started more than one. This is both the code to open Excel and attach and reuse, like you want.

Public Function GetExcelApplication() As Object
    On Error GoTo openExcel
    
    Set GetExcelApplication = GetObject(, "Excel.Application")
    Exit Function
    
openExcel:
    If Err.Number = 429 Then
        Set GetExcelApplication = CreateObject("Excel.Application")
    Else
        Debug.Print "Unhandled exception: " & Err.Number & " " & Err.Description
    End If
End Function

If you wanted to close multiple instances you would need to call GetObject followed by .Close in a loop until it throws the error 429.

The details can be found in this Article

HackSlash
  • 4,944
  • 2
  • 18
  • 44