Search code examples
c#winformsbitmappanelpicturebox

Print an image of the content of a Panel excluding any external overlapping Window


I have some problems.
I have a Panel and a PictureBox in a Form.
I would like to open a Windows application (for example Notepad) and parent it to the panel.
I then want to show an image of the content of the Panel in the PictureBox:

My code:

[DllImport("user32.dll", SetLastError = true)]
private static extern long SetParent(IntPtr hWndChild, IntPtr hWndNewParent);

[DllImport("user32.dll", SetLastError = true)]
private static extern long SetWindowPos(IntPtr hwnd, long hWndInsertAfter, long x, long y, long cx, long cy, long wFlags);

[DllImport("user32.dll", SetLastError = true)]
private static extern bool MoveWindow(IntPtr hwnd, int x, int y, int cx, int cy, bool repaint);

IntPtr appWin1;

private void Notepad_Button_Click(object sender, EventArgs e)
{
    ProcessStartInfo ps1 = new ProcessStartInfo(@"notepad.exe");
    ps1.WindowStyle = ProcessWindowStyle.Minimized;
    Process p1 = Process.Start(ps1);
    System.Threading.Thread.Sleep(5000); // Allow the process to open it's window
    appWin1 = p1.MainWindowHandle;
    // Put it into this form
    SetParent(appWin1, this.panel1.Handle);
}


private void timer1_Tick(object sender, EventArgs e)
{
    Bitmap bm = new Bitmap(panel1.Width, panel1.Height);
    Graphics g = Graphics.FromImage(bm);
    g.CopyFromScreen(0, 0, 0, 0, bm.Size);
    pictureBox1.Image = bm;
    
}

private void Form5_Load(object sender, EventArgs e)
{

}

private void panel1_Paint(object sender, PaintEventArgs e)
{

}

private void timer2_Tick(object sender, EventArgs e)
{
    MoveWindow(appWin1, 0, 0, this.panel1.Width, this.panel1.Height, true);
}

private void pictureBox1_Click(object sender, EventArgs e)
{

}

Running this code:

enter image description here

The problem is this: I only want to get the what's inside the Panel, but any other Window that moves in front of my Panel is included in the Bitmap shown in the PictureBox:

[enter image description here}3

As seen in the image, when an external page is placed on panel1, it also appears in pictureBox1 (red frame in the picture).
I want only Notepad to appear in pictureBox1, no matter what Window is hovering over panel1.


Solution

  • Since you want to render to Bitmap the content of a Panel - which is hosting an external application - you cannot use Control.DrawToBitmap(): the contents of the hosted application, parented calling SetParent(), won't be printed.
    Since you also want to exclude from the rendering any other Window that may be hovering over the Panel, you can use the PrintWindow function, passing the handle of the Panel's Parent Form.
    The Form will receive a WM_PRINT or WM_PRINTCLIENT message and will print itself to the Device Context specified: in this case, generated from a Bitmap.

    This is not a screenshot: the Window paints itself and its content to a DC, so it doesn't matter whether other Windows are hovering / partially of fully overlapping it, the result is the same.

    I'm using DwmGetWindowAttribute, setting DWMWA_EXTENDED_FRAME_BOUNDS, to get the bounds of the Window to print. It may seem redundant, but, if your app is not DpiAware and you have a High-Dpi screen, it will appear less redundant in that case. Better have it, IMO.
    This function is more reliable than, say, GetWindowRect or GetClientRect, it will return correct measures in variable DPI contexts.

    You cannot pass the handle of a child Window to this function, so I'm using the Handle of the Parent Form to generate the Bitmap, then select the section where the Panel is located. This is determined by:

    [ParentForm].RectangleToClient([Child].RectangleToScreen([Child].ClientRectangle))


    Start the Process and parent Notepad's Window to the Panel control:

    private void Notepad_Button_Click(object sender, EventArgs e)
    {
        var psi = new ProcessStartInfo(@"notepad.exe") { WindowStyle = ProcessWindowStyle.Minimized };
        using (var p = Process.Start(psi)) {
            p.WaitForInputIdle();
            var handle = p.MainWindowHandle;
            if (handle != IntPtr.Zero) {
                SetParent(handle, panel1.Handle);
                MoveWindow(handle, 0, 0, panel1.Width, panel1.Height, true);
            }
        }
    }
    

    Now, whenever you want to render to Bitmap the section of the Form where your the Panel is positioned, call RenderWindow and take the part you're interested in: the Panel's ClientRectangle.

    using (var image = RenderWindow(this.Handle, true, false)) {
        if (image is null) return;
        if (WindowState == FormWindowState.Minimized) return;
        var panelRect = RectangleToClient(panel1.RectangleToScreen(panel1.ClientRectangle));
        pictureBox1.Image?.Dispose();
        pictureBox1.Image = image.Clone(panelRect, PixelFormat.Format32bppArgb);
    }
    
    // [...]
    
    public static Bitmap RenderWindow(IntPtr hWnd, bool clientAreaOnly, bool tryGetFullContent = false)
    {
        var printOption = clientAreaOnly ? PrintWindowOptions.PW_CLIENTONLY : PrintWindowOptions.PW_DEFAULT;
        printOption = tryGetFullContent ? PrintWindowOptions.PW_RENDERFULLCONTENT : printOption;
        var hResult = DwmGetWindowAttribute(hWnd, DWMWINDOWATTRIBUTE.DWMWA_EXTENDED_FRAME_BOUNDS, out Rectangle dwmRect, Marshal.SizeOf<Rectangle>());
        if (hResult < 0) {
            Marshal.ThrowExceptionForHR(hResult);
            return null;
        }
    
        if (dwmRect.Width <= 0 || dwmRect.Height <= 0) return null;
    
        var bmp = new Bitmap(dwmRect.Width, dwmRect.Height);
        using (var g = Graphics.FromImage(bmp)) {
            var hDC = g.GetHdc();
            try {
                var success = PrintWindow(hWnd, hDC, printOption);
                if (!success) {
                    // Failed, see what happened
                    var win32Error = Marshal.GetLastWin32Error();
                    return null;
                }
                return bmp;
            }
            finally {
                g.ReleaseHdc(hDC);
            }
        }
    }
    

    Win32 declarations:

    [DllImport("user32.dll", SetLastError = true)]
    static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);
    
    [DllImport("user32.dll", SetLastError = true)]
    static extern bool MoveWindow(IntPtr hWnd, int x, int y, int nWidth, int nHeight, bool bRepaint);
    
    public enum PrintWindowOptions : uint
    {
        PW_DEFAULT = 0,
        PW_CLIENTONLY = 1,
        PW_RENDERFULLCONTENT = 2  // Undocumented. Use, e.g., with a WebBrowser
    }
    
    [DllImport("user32.dll", SetLastError = true)]
    static extern internal bool PrintWindow(IntPtr hwnd, IntPtr hDC, PrintWindowOptions nFlags);
    
    public enum DWMWINDOWATTRIBUTE : uint
    {
        DWMWA_NCRENDERING_ENABLED = 1,      // [get] Is non-client rendering enabled/disabled
        DWMWA_NCRENDERING_POLICY,           // [set] DWMNCRENDERINGPOLICY - Non-client rendering policy - Enable or disable non-client rendering
        DWMWA_TRANSITIONS_FORCEDISABLED,    // [set] Potentially enable/forcibly disable transitions
        DWMWA_ALLOW_NCPAINT,                // [set] Allow contents rendered In the non-client area To be visible On the DWM-drawn frame.
        DWMWA_CAPTION_BUTTON_BOUNDS,        // [get] Bounds Of the caption button area In window-relative space.
        DWMWA_NONCLIENT_RTL_LAYOUT,         // [set] Is non-client content RTL mirrored
        DWMWA_FORCE_ICONIC_REPRESENTATION,  // [set] Force this window To display iconic thumbnails.
        DWMWA_FLIP3D_POLICY,                // [set] Designates how Flip3D will treat the window.
        DWMWA_EXTENDED_FRAME_BOUNDS,        // [get] Gets the extended frame bounds rectangle In screen space
        DWMWA_HAS_ICONIC_BITMAP,            // [set] Indicates an available bitmap When there Is no better thumbnail representation.
        DWMWA_DISALLOW_PEEK,                // [set] Don't invoke Peek on the window.
        DWMWA_EXCLUDED_FROM_PEEK,           // [set] LivePreview exclusion information
        DWMWA_CLOAK,                        // [set] Cloak Or uncloak the window
        DWMWA_CLOAKED,                      // [get] Gets the cloaked state Of the window. Returns a DWMCLOACKEDREASON object
        DWMWA_FREEZE_REPRESENTATION,        // [set] BOOL, Force this window To freeze the thumbnail without live update
        PlaceHolder1,
        PlaceHolder2,
        PlaceHolder3,
        DWMWA_ACCENTPOLICY = 19
    }
    
    [DllImport("dwmapi.dll", SetLastError = true)]
    static extern internal int DwmGetWindowAttribute(IntPtr hwnd, DWMWINDOWATTRIBUTE dwAttribute, out Rectangle pvAttribute, int cbAttribute);