How to get the text of a MessageBox when it has an icon?

I am working on trying to close a specific MessageBox if it shows up based on the caption and text. I have it working when the MessageBox doesn't have an icon.

IntPtr handle = FindWindowByCaption(IntPtr.Zero, "Caption");
if (handle == IntPtr.Zero)
    return;

//Get the Text window handle
IntPtr txtHandle = FindWindowEx(handle, IntPtr.Zero, "Static", null);
int len = GetWindowTextLength(txtHandle);

//Get the text
StringBuilder sb = new StringBuilder(len + 1);
GetWindowText(txtHandle, sb, len + 1);

//close the messagebox
if (sb.ToString() == "Original message")
{
    SendMessage(new HandleRef(null, handle), WM_CLOSE, IntPtr.Zero, IntPtr.Zero);
}

The above code works just fine when the MessageBox is shown without an icon like the following.

MessageBox.Show("Original message", "Caption");

However, if it includes an icon (from MessageBoxIcon) like the following, it doesn't work; GetWindowTextLength returns 0 and nothing happens.

MessageBox.Show("Original message", "Caption", MessageBoxButtons.OK, MessageBoxIcon.Information);

My best guess is that the 3rd and/or 4th paramters of FindWindowEx need to change but I'm not sure what to pass instead. Or maybe the 2nd parameter needs to change to skip the icon? I'm not really sure.


Solution 1:

It appears that when the MessageBox has an icon, FindWindowEx returns the text of the first child (which is the icon in this case) hence, the zero length. Now, with the help of this answer, I got the idea to iterate the children until finding one with a text. This should work:

IntPtr handle = FindWindowByCaption(IntPtr.Zero, "Caption");

if (handle == IntPtr.Zero)
    return;

//Get the Text window handle
IntPtr txtHandle = IntPtr.Zero;
int len;
do
{
    txtHandle = FindWindowEx(handle, txtHandle, "Static", null);
    len = GetWindowTextLength(txtHandle);
} while (len == 0 && txtHandle != IntPtr.Zero);

//Get the text
StringBuilder sb = new StringBuilder(len + 1);
GetWindowText(txtHandle, sb, len + 1);

//close the messagebox
if (sb.ToString() == "Original message")
{
    SendMessage(new HandleRef(null, handle), WM_CLOSE, IntPtr.Zero, IntPtr.Zero);
}

Obviously, you could adjust it to fit your particular situation (e.g., keep iterating until you find the actual text you're looking for) although I think the child with the text will probably always be the second one:

Messagebox in Spy++

Solution 2:

This is a UI Automation method that can detect a Window Opened event anywhere in the System, identify the Window using the Text of one its child elements and close the Window upon positive identification.

The detection is initialized using Automation.AddAutomationEventHandler with WindowPattern.WindowOpenedEvent and Automation Element argument set to AutomationElement.RootElement, which, having no other ancestors, identifies the whole Desktop (any Window).

The WindowWatcher class exposes a public method (WatchWindowBySubElementText) that allows to specify the Text contained in one of the sub elements of a Window that just opened. If the specified Text is found, the method closes the Window and notifies the operation using a custom event handler that a subscriber can use to determine that the watched Window has been detected and closed.

Sample usage, using the Text string as provided in the question:

WindowWatcher watcher = new WindowWatcher();
watcher.ElementFound += (obj, evt) => { MessageBox.Show("Found and Closed!"); };
watcher.WatchWindowBySubElementText("Original message");

WindowWatcher class:

This class requires a Project Reference to these assemblies:
UIAutomationClient
UIAutomationTypes

Note that, upon identification, the class event removes the Automation event handler before notifying the subscribers. This is just an example: it points out that the handlers need to be removed at some point. The class could implement IDisposable and remove the handler(s) when disposed of.

EDIT:
Changed the condition that doesn't consider a Window created in the current Process:

if (element is null || element.Current.ProcessId != Process.GetCurrentProcess().Id)  

As noted in the comments, it imposes a limitation that is probably not necessary: the Dialog could also belong to the current Process. I left there just the null check.

using System.Diagnostics;
using System.Windows.Automation;

public class WindowWatcher
{
    public delegate void ElementFoundEventHandler(object sender, EventArgs e);
    public event ElementFoundEventHandler ElementFound;

    public WindowWatcher() { }
    public void WatchWindowBySubElementText(string ElementText) => 
        Automation.AddAutomationEventHandler(WindowPattern.WindowOpenedEvent, 
            AutomationElement.RootElement, TreeScope.Subtree, (UIElm, evt) => {
                AutomationElement element = UIElm as AutomationElement;
                try {
                    if (element is null) return;

                    AutomationElement childElm = element.FindFirst(TreeScope.Children,
                        new PropertyCondition(AutomationElement.NameProperty, ElementText));
                    if (childElm != null) {
                        (element.GetCurrentPattern(WindowPattern.Pattern) as WindowPattern).Close();
                        OnElementFound(new EventArgs());
                    }
                }
                catch (ElementNotAvailableException) {
                    // Ignore: generated when a Window is closed. Its AutomationElement   
                    // is no longer available. Usually a modal dialog in the current process. 
                }
            });
    public void OnElementFound(EventArgs e)
    {
        // Automation.RemoveAllEventHandlers(); <= If single use. Add to IDisposable.Dispose()
        ElementFound?.Invoke(this, e);
    }
}