0

I need to get a list of all currently opened MSACCESS instances in the system (windows) to be able to close any of them from within my app. I have no problems with EXCEL and WINWORD but can't hook up with Access.

I use Office 2016 and I see that MSACCESS creates separate procss for each opened database file. So I think I have to get application instances from window handles. I've tried to adapt this code: How to iterate through instance of Excel c#

I'm able to get all MSACCESS processes but the Excel or Word code isn't working for MSACCESS. The Code line:

if (buf.ToString() == "EXCEL7")

Always gives me the MsoCommandBarDock value. Any thoughts on how I can achieve this?

Alexander Smirnov
  • 1,573
  • 12
  • 23
  • 1
    the class name of the MS Access main window happens to be `OMain` (CS_OWNDC style). why should it be `EXCEL7`? be aware that this will be different between versions and may change at any time when MS choose to update. MsoCommandBarDock is one or two levels too deep in the window hierarchy. – Cee McSharpface Oct 31 '18 at 11:53
  • 1
    In Access, you can directly get the application object from the hWnd of the top-level window. In Excel, you need to get a specific nested window, and can acquire the application object from it's hWnd. This means you should use a significantly more simple approach for Access. You can just directly call `AccessibleObjectFromWindow` with the hWnd acquired from `(int)p.MainWindowHandle`. You can scrap all code enumerating child windows. I'd write up a short answer, but my C# is beginner level at best and I don't have a test environment handy, and this should get you started. – Erik A Oct 31 '18 at 12:24
  • if you are interested in just closing the app why not just get the process by name and kill it? – Krish Oct 31 '18 at 16:38
  • @krishKM The app should save data and close gracefully. The simple kill is not enough :( – Alexander Smirnov Nov 01 '18 at 09:43
  • @ErikvonAsmuth This is it! I have to omit child finder routine to get the access to MSAccess Application class. Please post a new answer so I can accept it. – Alexander Smirnov Nov 01 '18 at 10:19
  • @dlatikay I've noted above that I've examined an EXCEL code as I haven't found anything for MSACCESS. And stumbled exactly on this logic as I had no idea what name to expect from MsAccess. – Alexander Smirnov Nov 01 '18 at 10:21
  • @AlexanderSmirnov I'd prefer if you write up an answer yourself, since, as said, my C# is beginner level at best, and you could actually write up an answer that includes the code required and be easy to implement for future visitors. That's more important than any rep gains I might receive. – Erik A Nov 01 '18 at 10:24
  • it seems to be more complicated than we think: https://social.msdn.microsoft.com/Forums/en-US/27e15a29-7f8b-4b8f-b7d7-85cce1dfa660/how-to-get-an-automation-reference-to-an-ms-access-instance-from-a-windows-handle?forum=vbinterop gist: it is not known which of the access application window handles provide an `IAccessible` via AccessibleObjectFromWindow (and more importantly, not documented) – Cee McSharpface Nov 01 '18 at 10:41
  • @dlatikay It may not be documented, but it's certainly known, I've verified it for both Access 2010 and Access 2016. It's the top-level window, which has the class OMain. While it may not be documented, these things rarely are officially documented. – Erik A Nov 01 '18 at 11:19
  • @dlatikay Btw, that post you've linked is going wrong with the third argument of `AccessibleObjectFromWindow`. That should be a pointer to a guid struct, but is passing the guid as bytes by value instead. It's locked so I can't answer it. – Erik A Nov 01 '18 at 11:26

3 Answers3

1

Based on the answer for Excel, the Access version is similar:

const uint OBJID_NATIVEOM = 0xFFFFFFF0;

var procs = new List<Process>(Process.GetProcessesByName("MSACCESS.EXE"));

foreach (var p in procs)
{
    var mainHandle = (int)p.MainWindowHandle;
    if (mainHandle > 0)
    {
        var IID_IDispatch = new Guid("{00020400-0000-0000-C000-000000000046}");
        Microsoft.Office.Interop.Access.Application app = null;
        int res = AccessibleObjectFromWindow(mainHandle, OBJID_NATIVEOM, IID_IDispatch.ToByteArray(), ref app);
        if (res >= 0)
        {
            Debug.Assert(app.hWndAccessApp == mainHandle);
            Console.WriteLine(app.Name);
        }
    }
}

I tested it with Access 2016 on Windows 10, en-us locale. The major difference is that the window hierarchy of access is not as convoluted as the one of Excel, therefore you can omit the iteration of child windows.

Disclaimer: This relies on the internal structure of a closed-source Windows application. Microsoft as its vendor discourages this kind of tricks for obvious reasons: they may ship and update or release a new version at any time where the inner structure (the window hierarchy) has changed, breaking code that relies on this. Also, MS Access used to have a single document view mode, which may present you with two versions of window hierarchy in the same release. Don't do this in commercial products / productive software.

Cee McSharpface
  • 8,493
  • 3
  • 36
  • 77
1

According to the answer from Cee McSharpface, in 2021 (Microsoft Access for Microsoft 365 MSO (16.0.14326.20504) 64-bit and Windows 10 20H2) I had to adapt the solution as follows:

[DllImport("oleacc.dll")]
private static extern int AccessibleObjectFromWindow(
int hwnd, uint dwObjectID, byte[] riid,
    ref Microsoft.Office.Interop.Access.Application ptr);  

const uint OBJID_NATIVEOM = 0xFFFFFFF0;

var procs = new List<Process>(Process.GetProcessesByName("MSACCESS"));

foreach (var p in procs)
{
    var mainHandle = (int)p.MainWindowHandle;
    if (mainHandle > 0)
    {
        var IID_IDispatch = new Guid("{00020400-0000-0000-C000-000000000046}");
        Microsoft.Office.Interop.Access.Application app = null;
        int res = AccessibleObjectFromWindow(mainHandle, OBJID_NATIVEOM, IID_IDispatch.ToByteArray(), ref app);
        if (res >= 0)
        {
            Debug.Assert(app.hWndAccessApp() == mainHandle);
            Console.WriteLine(app.Name);
        }
    }
}

Please notice the following changes:

GetProcessesByName uses "MSACESS" instead of "MSACCESS.EXE", according to this documentation:

The process name is a friendly name for the process, such as Outlook, that does not include the .exe extension or the path

AccessibleObjectFromWindow uses a ref Microsoft.Office.Interop.Access.Application ptr because there is no Window object like in the Excel interop.

Christian Junk
  • 1,000
  • 1
  • 8
  • 22
0

There are many ways of doing this inlcuding retrieve COM Objects from ROT (running object table). Since your need is "just" to be able to close apps, following code should work fine.

using System.Diagnostics;
using System.Linq;

Process.GetProcessesByName("MSACCESS").All(x => x.CloseMainWindow());

This sends a close message to all Access main windows, which is similar to user closing the app.

Krish
  • 5,917
  • 2
  • 14
  • 35