Search code examples
gdi+sendmessagemicrosoft-ui-automation

Can I get a bitmap of an arbitrary window in another application process?


I am trying to automate a third-party Win32 application where I want to capture the graphics content of a particular window at defined time intervals. I am in the early phases of this, and I'm currently trying to use the Microsoft UI Automation API via C# to do most of the interaction between my client app and the external app. I can now get the external app to do what I want it to do, but now I want to capture the graphics from a specific window that seems to be some third-party owner-drawn control. How can I do this? The window I want to capture is the one marked by the red rectangle in this image:

I need what's in the red rectangle

I have an implementation that sort of works, but it's dependent on the external app's UI being on top, and that's not guaranteed for me, so I'd prefer to find something more general.

var p = Process.Start("c:\myapp.exe");
var mainForm = AutomationElement.FromHandle(p.MainWindowHandle);
// "workspace" below is the window whose content I want to capture.
var workspace = mainForm.FindFirst(TreeScope.Descendents,
                    new PropertyCondition(AutomationElement.ClassNameProperty, "AfxFrameOrView70u"));
var rect = (Rect) workspace.GetCurrentPropertyValue(AutomationElement.BoundingRectangleProperty);
using (var bmp = new Bitmap((int)rect.Width, (int)rect.Height))
{
    using (var g = Graphics.FromImage(bmp))
    {
        g.CopyFromScreen((int)rect.Left, (int)rect.Top, 0, 0, new Size((int)rect.Width, (int)rect.Height));
        bmp.Save(@"c:\screenshot.png", ImageFormat.Png);
    }
}

The above works well enough when the automated app is on top, but it just blindly copies the screen in the rectangle, so my code is at the mercy of whatever happens to be running on the machine and might cover my app's window.

I have read some suggestions to send the WM_PRINT message to the window. This question/answer from a few months back seemed promising, but when I use this code, I just get a white rectangle with none of my control's actual contents.

var prop = (int)workspace.GetCurrentPropertyValue(AutomationElement.NativeWindowHandleProperty);
var hwnd = new IntPtr(prop);
using ( var bmp2 = new Bitmap((int)rect.Width, (int)rect.Height))
{
    using (Graphics g = Graphics.FromImage(bmp2))
    {
        g.FillRectangle(SystemBrushes.Control, 0, 0, (int)rect.Width, (int)rect.Height);
        try
        {
            SendMessage(hwnd, WM_PRINT, g.GetHdc().ToInt32(), (int)(DrawingOptions.PRF_CHILDREN | DrawingOptions.PRF_CLIENT | DrawingOptions.PRF_OWNED));
        }
        finally
        {
            g.ReleaseHdc();
        }
        bmp2.Save(@"c:\screenshot.bmp");
    }
}

So, first, is it even possible for me to reliably save a bitmap of a window's contents? If so, what is the best way, and what is wrong with my WM_PRINT with SendMessage attempt?


Solution

  • This modification of the PrintWindow API example on the pinvoke.net site seems to have done the trick.

    Bitmap bmp = new Bitmap((int)rect.Width, (int)rect.Height);
    Graphics memoryGraphics = Graphics.FromImage(bmp);
    IntPtr dc = memoryGraphics.GetHdc();
    bool success = PrintWindow(hwnd, dc, 0);
    memoryGraphics.ReleaseHdc(dc);
    bmp.Save(@"c:\screenshot.bmp");
    

    This works if the app is covered by another window, but it doesn't work if the app is minimized. I think I can live with that.