12

This is a continuation of my previous question How to supress a dialog box an Inproc COM Server displays.


Background:

A recap of my situation: I have a Inproc COM Server written in Delphi from a 3rd party. One of the functions I call will display a error message dialog box if it traps a specific type of error. The issue is I am trying to process data in bulk, and the data source I am using is causing that error dialog to pop up a lot (thanks to the answer of my previous question it now auto closes and I was able to run it to completion, it would have shown the dialog box and required someone to press OK 9923 times). The process blocks till the message box is closed.


Question:

I would like to have better logging of what the error dialog said. However any attempt to get get the body text of the dialog box has failed.

Image of Dialog Box

//Snip

private void StartWindowListener()
{
    //Queue the watcher on the message pump if we are not watching.
    if (_watcherRunning == false)
    {
        _watcherRunning = true;
        _dummyForm.BeginInvoke(new Action(() =>
        {
            _watcherRunning = false;

            //If we are not inside the com object don't enumerate.
            if (_insideCom == false) return;

            // Enumerate windows to find dialogs
            EnumThreadWndProc callback = new EnumThreadWndProc(CheckWindow);
            EnumThreadWindows(GetCurrentThreadId(), callback, IntPtr.Zero);
            GC.KeepAlive(callback);
        }));
    }
}

private bool CheckWindow(IntPtr hWnd, IntPtr lp)
{
    // Checks if hWnd is the expected dialog
    StringBuilder sb = new StringBuilder(260);
    GetClassName(hWnd, sb, sb.Capacity);
    if (sb.ToString() == "TMessageForm")
    {
        //This returns the dialog box's title
        GetWindowText(hWnd, sb, sb.Capacity);

        //This returns IntPtr.Zero
        var hDialogText = GetDlgItem(hWnd, 0xFFFF);
        if (hDialogText != IntPtr.Zero)
            GetWindowText(hDialogText, sb, sb.Capacity);

        //This returns a empty string
        GetDlgItemText(hWnd, 0xFFFF, sb, sb.Capacity);


        //Only sees the OK button.
        IntPtr hCtl = IntPtr.Zero;
        HashSet<IntPtr> seen = new HashSet<IntPtr>();
        while ((hCtl = GetNextDlgGroupItem(hWnd, hCtl, false)) != IntPtr.Zero)
        {
            //When we see the same control twice, break out of the loop.
            if (seen.Add(hCtl) == false)
                break;

            GetClassName(hCtl, sb, sb.Capacity);
            SendMessage(hCtl, WM_GETTEXT, sb.Capacity, sb)

            //Close the dialog by sending WM_CLOSE to the window
            SendMessage(hWnd, WM_CLOSE, IntPtr.Zero, IntPtr.Zero);
        }

         //Snip...
    }
    return true;
}

//Snip...

// P/Invoke declarations
const int WM_CLOSE = 0x0010;
private delegate bool EnumThreadWndProc(IntPtr hWnd, IntPtr lp);
[DllImport("user32.dll")]
private static extern bool EnumThreadWindows(int tid, EnumThreadWndProc callback, IntPtr lp);
[DllImport("user32.dll")]
private static extern int GetClassName(IntPtr hWnd, StringBuilder buffer, int buflen);
[DllImport("user32.dll")]
private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wp, IntPtr lp);
[DllImport("kernel32.dll")]
private static extern int GetCurrentThreadId();

I thought I may have been interrupting the dialog before it gets to add the text to it (it is not fully painted yet when I break in the above code). However putting a Application.DoEvents inside StartWindowListener before it starts the enumeration allows the dialog box to fully paint but I still get the same results as I posted with the above code.

Doing a Ctrl-C works correctly on the dialog box so I could use that in a pinch, but being that I have to repeat this 9923 times I would like to avoid using that programmatically.

Is there any other methods I can try to get the text from the Message box?

Community
  • 1
  • 1
Scott Chamberlain
  • 124,994
  • 33
  • 282
  • 431
  • Use the `System.Windows.Automation` namespace. This sort of thing is exactly what it's for. – Raymond Chen Sep 24 '12 at 18:01
  • @RaymondChen Can you provide a example of how to do this? Everything I have done using the Automation namespace has required you to have a item listed in UISpy or Spy++. However those to do not report a label control I could attach to and read the text from so I do not know what to do. I will update the question with the output of UI Spy. – Scott Chamberlain Sep 24 '12 at 18:26
  • I cannot give an example for your specific program because I don't have your program. But `AutomationElement.RootElement.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.NameProperty, "Warning"));` will get you to the dialog. Then look for text children. – Raymond Chen Sep 24 '12 at 19:27
  • @RaymondChen As I said in my last comment and the screenshot of UISpy shows, ***there are no text children of Warning***, there are two children of "Warning", a button for "OK" and the Title Bar. The Title Bar has 2 childern, a menu bar and the close button. The menu bar has one child, a menu item "System" (the menu with move and close in it if you right click). That is it, 5 desendants of Warning in UISpy, and the same 5 if I do `parent.FindAll(TreeScope.Descendants, PropertyCondition.TrueCondition)` if I save the result from your query as `var parent = AutomationElement.RootElement.FindF...` – Scott Chamberlain Sep 24 '12 at 19:47
  • As I noted, I don't have your program so I don't know why it does not have a text node child. That may suggest that it won't have a dialog item either (since that's where UIAutomation gets Win32 children from), so either way you're stuck. – Raymond Chen Sep 24 '12 at 21:09
  • 4
    Unfortunately 'TMessageForm' shows the message text in what's called a 'TLabel' which is a non-windowed control. Basically, what you see is text drawn on the parent window with either 'DrawText' or 'DrawThemeTextEx'. – Sertac Akyuz Sep 25 '12 at 02:11
  • Sigh, I spend close to 50 hours getting this thing working. With the latest update to the software, they now throw an Exception. All of my hackish hooking with DrawText and running in a stalled STA thread is replaced with a simple try/catch block. – Scott Chamberlain May 05 '13 at 21:35

1 Answers1

7

Thanks to Sertac's comment I found out that the text in Delphi's message boxes are not window objects, they are drawn on with the 'DrawText' methods. I used EasyHook to intercept the Windows API calls and I am now able to grab the text I care about.

////It appears that DrawText always calls DrawTextEx so it is getting intercepted twice.
//// Only need to hook DrawTextEx
static EasyHook.LocalHook _drawTextExAHook;

//Snip...

public override void Run()
{
    //Snip...

    IntPtr drawTextExAPtr = EasyHook.LocalHook.GetProcAddress("user32", "DrawTextExA");
    _drawTextExAHook = EasyHook.LocalHook.Create(drawTextExAPtr, new DrawTextExDelegate(DrawTextEx_Hooked), null);

    //The COM stuff must be run in a STA Thread so we can intercept the message boxes that it throws up.
    var staThread = new Thread(() =>
        {
            try
            {
                var threadID = new[] { GetCurrentThreadId() };
                //Enable the hook on the current thread.
                _drawTextExAHook.ThreadACL.SetInclusiveACL(threadID);

                //Tell the dummy form to start ComThread
                _dummyForm = new DummyForm(ComThread);
                Application.Run(_dummyForm);
            }
            finally
            {
                if(_drawTextExAHook != null)
                    _drawTextExAHook.Dispose();
            }
        });
    staThread.SetApartmentState(ApartmentState.STA);
    staThread.Name = "Com Thread";
    staThread.Start();

    //Wait for the Com Thread to finish.
    staThread.Join();

}

//Snip...

private delegate int DrawTextExDelegate(IntPtr hdc, string lpchText, int cchText,
                ref Rect lprc, uint dwDTFormat, ref DRAWTEXTPARAMS lpDTParams);

private int DrawTextEx_Hooked(IntPtr hdc, string lpchText, int cchText, ref Rect lprc, 
                                     uint dwDTFormat, ref DRAWTEXTPARAMS lpDTParams)
{
    LogErrorText(lpchText);
    return DrawTextEx(hdc, lpchText, cchText, ref lprc, dwDTFormat, ref lpDTParams);
}

[DllImport("user32.dll")]
static extern int DrawTextEx(IntPtr hdc, string lpchText, int cchText,
                             ref Rect lprc, uint dwDTFormat, ref DRAWTEXTPARAMS lpDTParams);
Community
  • 1
  • 1
Scott Chamberlain
  • 124,994
  • 33
  • 282
  • 431