Search code examples
c#excelvbainteropmarshalling

Attach To Existing Excel Instance even if VBA is Open by the Excel Instance


This is in regards to the selected answer for this question:
How to iterate through instance of Excel c#

This code works great for getting all instances of Excel unless one of these instances has the VBA Editor open. The code breaks when it tries to find the class for EXCEL7. This is the class for the workbook subwindow. While debugging I determined that when enumerating the child windows, the EXCEL7 child window can not be located. Classes like VbaWindow instead show up. I even tried to get the Excel.Window with the window handle for this vba window class but it failed. How can I still get the method AccessibleObjectFromWindow to reference the Excel.Window, which I can then use to reference the application. Here is my modified method (I already have the Excel Process ID... all other declarations were omitted for readability):

internal static Excel.Application GetExcelInstance(int procID)
{
    EnumChildCallback cb;
    Process p = Process.GetProcessById(procID);

    if (p != null)
    {
        if ((int)p.MainWindowHandle > 0)
        {
            int childWindow = 0;
            cb = new EnumChildCallback(EnumChildProc);
            EnumChildWindows((int)p.MainWindowHandle, cb, ref childWindow);

            if (childWindow > 0)
            {
                const uint OBJID_NATIVEOM = 0xFFFFFFF0;
                // GUIDs used by the OLE Automation Protocol:
                // https://msdn.microsoft.com/en-us/library/cc237842.aspx
                Guid IID_IDispatch = new Guid("{00020400-0000-0000-C000-000000000046}");
                Excel.Window window = null;
                int res = AccessibleObjectFromWindow
                          (
                              childWindow,
                              OBJID_NATIVEOM,
                              IID_IDispatch.ToByteArray(),
                              ref window
                          );

                if (res >= 0)
                {
                    return window.Application;
                }
            }
        }
    }

    return null;
}

// If VBA is open this method will fail when enumerating
// all child windows of the excel process
// EXCEL7 will not be found in child windows but other windows
// will be found like the window for class "VbaWindow"
private static bool EnumChildProc(int hwndChild, ref int lParam)
{
    StringBuilder buf = new StringBuilder(128);
    GetClassName(hwndChild, buf, 128);

    // More info on excel classes:
    // http://www.mrexcel.com/forum/excel-questions/54007-worksheet-class-findwindow-api.html
    if (buf.ToString() == "EXCEL7")
    {
        lParam = hwndChild;
        return false;
    }

    return true;
}

Solution

  • I had exactly the same problem. I solved it by:

    1. Getting a list of all Excel process ids
    2. Looping over ALL top level windows and checking to see if its process id was in the list of Excel ids. If so, then add it to another list. (Use EnumWindows and GetWindowThreadProcessID)
    3. Then loop over this list of Excel hwnds and essentially do what you were previously doing, i.e. search through their child windows to find one where the class name is EXCEL7

    Here's some example code:

    public class ExcelInstances
    {
        HashSet<int> _ExcelProcessIDs;
        List<int> _ExcelTopLevelWindowHwnds;
        List<Excel.Application> _XLInstances;
    
        public Excel.Application[] GetExcelInstances()
        {
            _XLInstances = new List<Excel.Application>();
            _ExcelProcessIDs = new HashSet<int>();
            _ExcelTopLevelWindowHwnds = new List<int>();
    
            foreach (Process p in Process.GetProcessesByName("EXCEL")) _ExcelProcessIDs.Add(p.Id); //find all process ids related to Excel
    
            int hwnd = 0;
            var cb = new WinAPI.WindowEnumProc(GetAllExcelTopLevelWindowHwnds);
            WinAPI.EnumWindows(cb, ref hwnd);
    
            foreach (var hwnd2 in _ExcelTopLevelWindowHwnds)
            {
                var excelHwnd = 0;
                var cb2 = new WinAPI.WindowEnumProc(GetExcelWorkbooksFromExcelWindowHandles);
                WinAPI.EnumChildWindows(hwnd2, cb2, ref excelHwnd);
            }
    
            return _XLInstances.ToArray();
        }
    
        private bool GetAllExcelTopLevelWindowHwnds(int hwnd, ref int lParam)
        {
            int id = 0;
            WinAPI.GetWindowThreadProcessId(hwnd, ref id);
    
            if (_ExcelProcessIDs.Contains(id))
            {
                if (hwnd > 0)
                {
                    _ExcelTopLevelWindowHwnds.Add(hwnd);
                }
            }
    
            return true;
        }
    
        private bool GetExcelWorkbooksFromExcelWindowHandles(int hwndChild, ref int lParam)
        {
            int id = 0;
            WinAPI.GetWindowThreadProcessId(hwndChild, ref id);
    
            StringBuilder buf = new StringBuilder(128);
            WinAPI.GetClassName(hwndChild, buf, 128);
            string clsName = buf.ToString();
    
            if (clsName == "EXCEL7")
            {
                lParam = hwndChild;
                var wb = UsefulStaticMethods.GetActiveWorkbookFromExcelHandle(hwndChild);
                if (wb != null) _XLInstances.Add(wb.Parent);
            }
    
            return true;
        }
    }