4

I want to capture the OK Button's Click event on a MessageBox shown by another WinForms application.

I want to achieve this using UI Automation. After some research, I have found that IUIAutomation::AddAutomationEventHandler will do the work for me.

Though, I can capture the Click event of any other button, I'm unable to capture a Click event of the MessageBox.

My code is as follows:

var FindDialogButton = appElement.FindFirst(TreeScope.Descendants, new PropertyCondition(AutomationElement.NameProperty, "OK"));

if (FindDialogButton != null)
{
    if (FindDialogButton.GetSupportedPatterns().Any(p => p.Equals(InvokePattern.Pattern)))
    {
        Automation.AddAutomationEventHandler(InvokePattern.InvokedEvent, FindDialogButton, TreeScope.Element, new AutomationEventHandler(DialogHandler));
    }
}

private void DialogHandler(object sender, AutomationEventArgs e)
{
    MessageBox.Show("Dialog Button clicked at : " + DateTime.Now);
}

EDIT:

My Complete code is as follows:

private void DialogButtonHandle()
{
    AutomationElement rootElement = AutomationElement.RootElement;
    if (rootElement != null)
    {
        System.Windows.Automation.Condition condition = new PropertyCondition
     (AutomationElement.NameProperty, "Windows Application"); //This part gets the handle of the Windows application that has the MessageBox

        AutomationElement appElement = rootElement.FindFirst(TreeScope.Children, condition);

        var FindDialogButton = appElement.FindFirst(TreeScope.Descendants, new PropertyCondition(AutomationElement.NameProperty, "OK")); // This part gets the handle of the button inside the messagebox
        if (FindDialogButton != null)
        {
            if (FindDialogButton.GetSupportedPatterns().Any(p => p.Equals(InvokePattern.Pattern)))
            {
                Automation.AddAutomationEventHandler(InvokePattern.InvokedEvent, FindDialogButton, TreeScope.Element, new AutomationEventHandler(DialogHandler)); //Here I am trying to catch the click of "OK" button inside the MessageBox
            }
        }
    }
}

private void DialogHandler(object sender, AutomationEventArgs e)
{
    //On Button click I am trying to display a message that the button has been clicked
    MessageBox.Show("MessageBox Button Clicked");
}
Jimi
  • 29,621
  • 8
  • 43
  • 61
Faran Saleem
  • 404
  • 1
  • 7
  • 31
  • What is `appElement`? You need to identify the MessageBox before adding an handler to one of its Elements (if the message box is actually a [MessageBox](https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.messagebox)). See the answer that's using UI Automation here: [How to get the text of a MessageBox when it has an icon?](https://stackoverflow.com/a/55028688/7444103) to make it work correctly. – Jimi Oct 01 '19 at 16:47
  • appElement is another application inside which MessageBox opens. And I have already identified MessageBox inside FindDialogButton. Inside it I am getting the handle of MessageBox. – Faran Saleem Oct 02 '19 at 06:11
  • *appElement is another application inside which MessageBox opens*. UI Automation doesn't deal with *Application* elements, it deals with Controls. So, `appElement` should be a `Window` element. A `MessageBox` doesn't belong to another Window, it's a stand-alone Window. How did you detect the opening of this MessageBox? This part of the code is missing, thus I don't know what you're handling right now. – Jimi Oct 02 '19 at 08:08
  • Yes thats what I meant to say, appElement is a Window. I am detecting the opening of the MessageBox through this line , var FindDialogButton = appElement.FindFirst(TreeScope.Descendants, new PropertyCondition(AutomationElement.NameProperty, "OK")); I can also Find the Message box through ClassName that is not an issue, the issue here is I want to detect the click of the dialog button – Faran Saleem Oct 02 '19 at 08:21
  • That's not the way to detect a `WindowOpened` event. A MessageBox is not a descendant of another Window. See the link in my fist comment. – Jimi Oct 02 '19 at 08:23
  • I think you missed my point, I do not want to detect WindowOpened event. I want to detect the click of the MessageBox. – Faran Saleem Oct 02 '19 at 08:24
  • I am able to get the handle of MessageBox inside FindDialogButton – Faran Saleem Oct 02 '19 at 08:25
  • Post the complete code. The one that detects the opening of a MessageBox, created by a specific (third-pary) Application. – Jimi Oct 02 '19 at 08:26
  • As I already mentioned, you cannot find a MessageBox this way. A MessageBox is not a descendant (`TreeScope.Descendants`) of another Window (`appElement`). `var FindDialogButton = appElement.FindFirst(...)` won't find anything. See the link in my first comment. You need to use a [WindowOpenedEvent](https://learn.microsoft.com/en-us/dotnet/api/system.windows.automation.windowpattern.windowopenedevent) – Jimi Oct 02 '19 at 08:37
  • Okay once I find the MessageBox through WindowOpenedEvent then how can I catch the event where the button is clicked inside the messageBox? – Faran Saleem Oct 02 '19 at 08:44
  • Can you please post the correct code as an answer? – Faran Saleem Oct 03 '19 at 06:20
  • I can do that. Can you clarify whether your app is run after the application you're watching is already active or you may also need to run yours and wait until this application is run. This changes the code a bit, since, in the latter case, you need to pre-install a `WindowOpenedEvent` that detects when this application's Main Window is opened, store its Process ID and compare it to the MessageBox Process ID (it should be the same ID) when the MessageBox is opened. – Jimi Oct 03 '19 at 10:10
  • The dialog box will open afterwards. The application will be running and it will watch when the messageBox is opened and then as soon as the MessageBox is closed the first application will catch the event of dialog close – Faran Saleem Oct 04 '19 at 07:20

1 Answers1

3

I tried to keep this procedure as generic as possible, so that it will work whether the application you're watching is already running when your app is started or not.

You just need to provide the watched Application's Process Name or its Main Window Title to let the procedure identify this application.
Use one of these Fields and the corresponding Enumerator:

private string appProcessName = "theAppProcessName"; and 
FindWindowMethod.ProcessName
// Or
private string appWindowTitle = "theAppMainWindowTitle"; and 
FindWindowMethod.Caption

passing these values to the procedure that starts the watcher, e.g., :

StartAppWatcher(appProcessName, FindWindowMethod.ProcessName); 

As you can see - since you tagged your question as winforms - this is a complete Form (named frmWindowWatcher) that contains all the logic required to perform this task.

How does it work:

  • When you start frmWindowWatcher, the procedure verifies whether the watched application (here, identified using its Process name, but you can change the method, as already described), is already running.
    If it is, it initializes a support class, ElementWindow, which will contain some informations about the watched application.
    I added this support class in case you need to perform some actions if the watched application is already running (in this case, the ElementWindow windowElement Field won't be null when the StartAppWatcher() method is called). These informations may also be useful in other cases.
  • When a new Windows is opened in the System, the procedure verifies whether this Window belongs to the watched application. If it does, the Process ID will be the same. If the Windows is a MessageBox (identified using its standard ClassName: #32770) and it belongs to the watched Application, an AutomationEventHandler is attached to the child OK Button.
    Here, I'm using a Delegate: AutomationEventHandler DialogButtonHandler for the handler and an instance Field (AutomationElement msgBoxButton) for the Button Element, because these references are needed to remove the Button Click Handler when the MessageBox is closed.
  • When the MessageBox's OK Button is clicked, the MessageBoxButtonHandler method is called. Here, you can determine which action to take at this point.
  • When the frmWindowWatcher Form is closed, all Automation Handlers are removed, calling the Automation.RemoveAllEventHandlers() method, to provide a final clean up and prevent your app from leaking resources.


using System.Diagnostics;
using System.Linq;
using System.Windows.Automation;
using System.Windows.Forms;

public partial class frmWindowWatcher : Form
{
    AutomationEventHandler DialogButtonHandler = null;
    AutomationElement msgBoxButton = null;
    ElementWindow windowElement = null;
    int currentProcessId = 0;
    private string appProcessName = "theAppProcessName";
    //private string appWindowTitle = "theAppMainWindowTitle";

    public enum FindWindowMethod
    {
        ProcessName,
        Caption
    }

    public frmWindowWatcher()
    {
        InitializeComponent();
        using (var proc = Process.GetCurrentProcess()) {
            currentProcessId = proc.Id;
        }
        // Identify the application by its Process name...
        StartAppWatcher(appProcessName, FindWindowMethod.ProcessName);
        // ... or by its main Window Title
        //StartAppWatcher(appWindowTitle, FindWindowMethod.Caption);
    }

    protected override void OnFormClosed(FormClosedEventArgs e)
    {
        Automation.RemoveAllEventHandlers();
        base.OnFormClosed(e);
    }

    private void StartAppWatcher(string elementName, FindWindowMethod method)
    {
        windowElement = GetAppElement(elementName, method);
        // (...)
        // You may want to perform some actions if the watched application is already running when you start your app

        Automation.AddAutomationEventHandler(WindowPattern.WindowOpenedEvent, AutomationElement.RootElement,
            TreeScope.Subtree, (elm, e) => {
                AutomationElement element = elm as AutomationElement;

                try
                {
                    if (element == null || element.Current.ProcessId == currentProcessId) return;
                    if (windowElement == null) windowElement = GetAppElement(elementName, method);
                    if (windowElement == null || windowElement.ProcessId != element.Current.ProcessId) return;

                    // If the Window is a MessageBox generated by the watched app, attach the handler
                    if (element.Current.ClassName == "#32770")
                    {
                        msgBoxButton = element.FindFirst(TreeScope.Descendants, 
                            new PropertyCondition(AutomationElement.NameProperty, "OK"));
                        if (msgBoxButton != null && msgBoxButton.GetSupportedPatterns().Any(p => p.Equals(InvokePattern.Pattern)))
                        {
                            Automation.AddAutomationEventHandler(
                                InvokePattern.InvokedEvent, msgBoxButton, TreeScope.Element,
                                    DialogButtonHandler = new AutomationEventHandler(MessageBoxButtonHandler));
                        }
                    }
                }
                catch (ElementNotAvailableException) {
                    // Ignore: this exception may be raised if you show a modal dialog, 
                    // in your own app, that blocks the execution. When the dialog is closed, 
                    // AutomationElement element is no longer available
                }
            });

        Automation.AddAutomationEventHandler(WindowPattern.WindowClosedEvent, AutomationElement.RootElement,
            TreeScope.Subtree, (elm, e) => {
                AutomationElement element = elm as AutomationElement;

                if (element == null || element.Current.ProcessId == currentProcessId || windowElement == null) return;
                if (windowElement.ProcessId == element.Current.ProcessId) {
                    if (windowElement.MainWindowTitle == element.Current.Name) {
                        windowElement = null;
                    }
                }
            });
    }

    private void MessageBoxButtonHandler(object sender, AutomationEventArgs e)
    {
        Console.WriteLine("Dialog Button clicked at : " + DateTime.Now.ToString());
        // (...)
        // Remove the handler after, since the next MessageBox needs a new handler.
        Automation.RemoveAutomationEventHandler(e.EventId, msgBoxButton, DialogButtonHandler);
    }

    private ElementWindow GetAppElement(string elementName, FindWindowMethod method)
    {
        Process proc = null;

        try {
            switch (method) {
                case FindWindowMethod.ProcessName:
                    proc = Process.GetProcessesByName(elementName).FirstOrDefault();
                    break;
                case FindWindowMethod.Caption:
                    proc = Process.GetProcesses().FirstOrDefault(p => p.MainWindowTitle == elementName);
                    break;
            }
            return CreateElementWindow(proc);
        }
        finally {
            proc?.Dispose();
        }
    }

    private ElementWindow CreateElementWindow(Process process) => 
        process == null ? null : new ElementWindow(process.ProcessName) {
            MainWindowTitle = process.MainWindowTitle,
            MainWindowHandle = process.MainWindowHandle,
            ProcessId = process.Id
        };
}

Support class, used to store informations on the watched application:
It's initialized using the App's Process Name:

public ElementWindow(string processName)

but of course you can change it as required, using the Window Title as described before, or even remove the initialization's argument if you prefer (the class just need to not be null when the watched Application has been detected and identified).

using System.Collections.Generic;

public class ElementWindow
{
    public ElementWindow(string processName) => this.ProcessName = processName;

    public string ProcessName { get; set; }
    public string MainWindowTitle { get; set; }
    public int ProcessId { get; set; }
    public IntPtr MainWindowHandle { get; set; }
}
Jimi
  • 29,621
  • 8
  • 43
  • 61
  • This worked for me. Thank you so much for your help. One question do we require this ElementWindow class or is it not necessary to have this? – Faran Saleem Oct 04 '19 at 13:52
  • No, it's not necessary. It's only used to store some data about the watched application (it's ProcessID, MainWindowHandle etc.). If you don't need to perform any action that require these informations, you just need a Field to store the App's ProcessID, which is used to identify the watched Application's Process, so you can determine if a newly opened Window belongs to this app (as a note, some application may have more than one ProcessID, but that's another story :) and the `MainWindowTitle`, to determine whether the Main app window has been closed. – Jimi Oct 04 '19 at 15:03
  • Note that I've edited the code, since I left an unneeded Field, `buttonElement` (while trying to shorten the code, I forgot to remove it), which is now renamed to `msgBoxButton` which is the only AutomationElement required to remove the AutomationEventHandler. The notes have been updated accordingly. – Jimi Oct 04 '19 at 15:13
  • Bravo! Thank you soo much for your help. Much appreciated. :) – Faran Saleem Oct 07 '19 at 06:01
  • Hi @Jimi, I need some help, i am trying the same code on WPF application to get the handle of the button click but it is not working for WPF Application. It is only working for Winform application or any other dialog appeared in the window. Please help – Faran Saleem Dec 03 '19 at 09:47
  • WPF/UWP controls (elements) don't have handles. Only Windows elements have one. I never used the Button handle in this code, I just use the element reference: `msgBoxButton = element.FindFirst(...)`, where `element` is the parent Window element. – Jimi Dec 03 '19 at 09:54
  • The thing is it is finding the dialog box on WPF but it is not getting the handle on click of the dialog of WPF application – Faran Saleem Dec 03 '19 at 10:19
  • Is there a way I can make this thing work with WPF? Because my primary objective is with WPF application – Faran Saleem Dec 03 '19 at 10:20
  • I cannot test it right now, but I will look into it. – Jimi Dec 03 '19 at 12:02
  • Okay sure.. meanwhile can you just tell me the direction I should look this into? So that I can try different things at my end as well – Faran Saleem Dec 03 '19 at 12:04