Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Getting the text from a dialog box that does not use a label control?

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?

like image 921
Scott Chamberlain Avatar asked Sep 24 '12 17:09

Scott Chamberlain


1 Answers

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);
like image 114
Scott Chamberlain Avatar answered Sep 24 '22 15:09

Scott Chamberlain