在另一个应用程序的MessageBox中捕获按钮点击事件

4
我想要捕获由另一个WinForms应用程序显示的MessageBox上“OK”按钮的“Click”事件。我希望使用UI自动化实现此目的。经过一些研究,我发现IUIAutomation::AddAutomationEventHandler将为我完成这项工作。尽管我可以捕获任何其他按钮的“Click”事件,但我无法捕获MessageBox的“Click”事件。我的代码如下:
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);
}

编辑:

我的完整代码如下:

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");
}

什么是appElement?在向其元素之一添加处理程序之前,您需要确定MessageBox(如果MessageBox实际上是MessageBox)的身份。请参见此处使用UI自动化的答案:当MessageBox有图标时如何获取其文本?,以使其正常工作。 - Jimi
我认为你误解了我的意思,我不想检测WindowOpened事件。我想检测MessageBox的点击。 - Faran Saleem
发布完整的代码。该代码可检测由特定(第三方)应用程序创建的MessageBox的打开。 - Jimi
正如我之前提到的,您不能以这种方式找到 MessageBox。MessageBox 不是另一个 Window(appElement)的后代(TreeScope.Descendants)。 var FindDialogButton = appElement.FindFirst(...) 将什么也找不到。请参阅我第一条评论中的链接。您需要使用 WindowOpenedEvent - Jimi
我可以做到。您能否澄清一下,您的应用程序是在您正在观看的应用程序已经处于活动状态后运行,还是您也需要运行您的应用程序并等待此应用程序运行。这会稍微改变代码,因为在后一种情况下,您需要预先安装一个“WindowOpenedEvent”,以便在打开此应用程序的主窗口时检测其进程ID,并将其与MessageBox进程ID进行比较(它应该是相同的ID)。 - Jimi
显示剩余8条评论
1个回答

3

我尽可能地将这个过程保持为通用状态,因此当您的应用程序启动或未启动时,它都可以正常工作。

您只需要提供被监视应用程序的进程名称或其主窗口标题以让该过程识别该应用程序。
使用这些字段之一和相应的枚举器:

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

将这些值传递给启动观察器的过程,例如:
StartAppWatcher(appProcessName, FindWindowMethod.ProcessName); 

正如您所看到的 - 由于您将问题标记为 winforms - 这是一个完整的表单(名为 frmWindowWatcher ),其中包含执行此任务所需的所有逻辑。
它如何工作:
  • 当您启动 frmWindowWatcher 时,该过程会验证是否已经运行了被监视应用程序(在此处使用其进程名称标识,但您可以更改方法,如已描述)。如果是,则初始化一个支持类 ElementWindow ,其中将包含有关被监视应用程序的一些信息。
    我添加了这个支持类以防需要在被监视的应用程序已运行时执行某些操作(在这种情况下,当调用 StartAppWatcher() 方法时,ElementWindow windowElement 字段不会为空)。这些信息在其他情况下也可能有用。
  • 当在系统中打开新的窗口时,该过程会验证此窗口是否属于受监视的应用程序。如果是,则进程ID将相同。如果窗口是消息框(使用其标准 ClassName#32770 来标识),并且它属于受监视的应用程序,则会将 AutomationEventHandler 附加到子 OK 按钮。
    在此处,我使用一个委托:AutomationEventHandler DialogButtonHandler 作为处理程序,以及一个实例字段 (AutomationElement msgBoxButton) 用于按钮元素,因为需要这些引用来在关闭消息框时删除按钮点击处理程序。
  • 当单击消息框的 OK 按钮时,将调用 MessageBoxButtonHandler 方法。此时,您可以确定要采取哪些操作。
  • 当关闭 frmWindowWatcher 窗体时,所有自动化处理程序都将被删除,调用 Automation.RemoveAllEventHandlers() 方法,以提供最终的清理并防止您的应用程序泄漏资源。
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
        };
}

支持类,用于存储监视应用程序的信息:
它使用App的进程名称进行初始化:

public ElementWindow(string processName)

当然,您可以根据需要更改它,使用之前描述的窗口标题,甚至如果您愿意,可以删除初始化参数(只需在检测和识别所监视的应用程序时类不为空即可)。

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,我需要一些帮助,我正在尝试在WPF应用程序上使用相同的代码来获取按钮单击的句柄,但它在WPF应用程序中不起作用。 它只适用于Winform应用程序或窗口中出现的任何其他对话框。 请帮忙。 - Faran Saleem
WPF/UWP控件(元素)没有句柄。只有Windows元素才有句柄。我从未在此代码中使用Button句柄,我只是使用元素引用:msgBoxButton = element.FindFirst(...),其中element是父窗口元素。 - Jimi
问题在于它可以找到 WPF 上的对话框,但在单击 WPF 应用程序的对话框时却无法获取句柄。 - Faran Saleem
有没有办法让这个东西在WPF中工作?因为我的主要目标是WPF应用程序。 - Faran Saleem
好的,当然可以。同时,您能告诉我应该朝哪个方向去寻找解决方法吗?这样我也可以在我的端尝试不同的方法。 - Faran Saleem
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接