6

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.

James
  • 523
  • 1
  • 4
  • 20
  • 1
    If you're willing to try UI Automation, [Automation.AddAutomationEventHandler](https://learn.microsoft.com/en-us/dotnet/api/system.windows.automation.automation.addautomationeventhandler) with [WindowPattern.WindowOpenedEvent](https://learn.microsoft.com/en-us/dotnet/api/system.windows.automation.windowpattern.windowopenedevent) will notify you when the MessageBox is opened (or closed), no matter how. – Jimi Mar 06 '19 at 02:48
  • @Jimi I'd still need to see if the `MessageBox` that was just opened is the one I'm looking for. I don't want to close all `MessageBox`. Just the one with the message I'm looking for. To do that it still seems I need to use FindWindow. – James Mar 06 '19 at 02:53
  • 1
    UI Automation returns, in the event handler arguments, the Element that raised the event. The `Element.Current` object has all the properties you need to identify the MessageBox. For example the `Element.Current.Name` will be "Caption" , in this case. – Jimi Mar 06 '19 at 02:56
  • @Jimi, but how can I get the text of the message box. The "Caption" narrows it down some but is too generic for my purposes. I need to check the specific text of the message. That works with my code unless I have the icon showing in the message box. – James Mar 06 '19 at 03:05
  • 1
    When the Event handler returns the Element that raised the event (your MessageBox), you just need to find the child element that has the properties you know about. For example, to find an element that has the Text you're showing here (`Original message`), it would be `[Element].FindAll(TreeScope.Children, New PropertyCondition(AutomationElement.NameProperty, "Original message"));`. If the collection returned is empty, no matching elements were found. Not your MessageBox. – Jimi Mar 06 '19 at 03:16
  • @jimi I like this approach and it does seem to work with one exception. The first time the even fires after being the handler being added causes a very long delay. Like 15 seconds. – James Mar 06 '19 at 04:19
  • [Use accessibility to monitor the creation of windows](https://blogs.msdn.microsoft.com/oldnewthing/20130325-00/?p=4863/). Don't try and go spelunking afterwards. – Cody Gray - on strike Mar 06 '19 at 04:19
  • You did something wrong :) If you want, I can post a simple *Window Watcher* class that will raise an event when a child control Text matches some string pattern. – Jimi Mar 06 '19 at 04:27
  • @Jimi I'm not sure how I did something wrong since it does work. There is just a delay for the first firing of the event. – James Mar 06 '19 at 09:00
  • Test the class I posted, see whether you have the same delay. Note that you should test this with a Release build. Anyway, it doesn't have any delay in Debug mode either, in my experience. – Jimi Mar 06 '19 at 17:15
  • This sort of thing is made much easier with OO-based _Microsoft UI Automation_. Something I see Jimi has already mentioned –  Feb 07 '20 at 03:14

2 Answers2

4

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++

  • That worked. I was pretty close. I was thinking to pass IntPtr.Zero + IntPtr.Size thinking it might move the position over by one (skip the icon). Anyways, it's working now. Thanks – James Mar 06 '19 at 04:26
  • Where did you get that "Window Search" utility? That would have been helpful. – James Mar 06 '19 at 04:30
  • 1
    @James It's part of a utility called [Spy++](https://learn.microsoft.com/en-us/visualstudio/debugger/introducing-spy-increment?view=vs-2017). – 41686d6564 stands w. Palestine Mar 06 '19 at 04:31
3

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);
    }
}
Jimi
  • 29,621
  • 8
  • 43
  • 61
  • I tried this and it works just fine on my side, with no delays. Upvoted! – 41686d6564 stands w. Palestine Mar 07 '19 at 13:23
  • 1
    Hmm, rethinking my edit now, I'm not sure whether your intention (or even the OP's intention) was to actually catch a window that belongs to the current process or to other processes. Please review the edit and feel free to rollback if it doesn't reflect your original intention. – 41686d6564 stands w. Palestine Mar 07 '19 at 13:31
  • @Ahmed Abdelhameed Thanks. It was probably an unnecessary/limiting check. I edited the code and left a note about it. – Jimi Mar 07 '19 at 19:21
  • @Jimi I've been busy so I couldn't get back to this for awhile. It appears that this is working now. Thanks. I did have to tweak it a little bit since I don't want to remove the handler as soon as it closes the message box. I want to handle that somewhere else. To answer the question about current process, no I'm not looking for messages from other processes. – James Mar 20 '19 at 00:26
  • Sure. Removing the hanlder is not strictly necessary. In this case, it's just an example (also posted to point out that removing these hanlders is a requirement). Note that there's also a [WindowPattern.WindowClosedEvent](https://learn.microsoft.com/en-us/dotnet/api/system.windows.automation.windowpattern.windowclosedevent), which can be use to notify when a process is closing a Window. – Jimi Mar 20 '19 at 00:44