How to use PrintDocument with a scrollable Panel?

These sets of methods allow to print the content of a ScrollableControl to a Bitmap.

A description of the procedure:

  1. The control is first scrolled back to the origin (control.AutoScrollPosition = new Point(0, 0); (an exception is raised otherwise: the Bitmap has a wrong size. You may want to store the current scroll position and restore it after).
  2. Verifies and stores the actual size of the Container, returned by the PreferredSize or DisplayRectangle properties (depending on the conditions set by the method arguments and the type of container printed). This property considers the full extent of a container.
    This will be the size of the Bitmap.
  3. Clears the Bitmap using the background color of the Container.
  4. Iterates the ScrollableControl.Controls collection and prints all first-level child controls in their relative position (a child Control’s Bounds rectangle is relative to the container ClientArea.)
  5. If a first-level Control has children, calls the DrawNestedControls recursive method, which will enumerate and draw all nested child Containers/Controls, preserving the internal clip bounds.

Includes support for RichTextBox controls.
The RichEditPrinter class contains the logic required to print the content of a RichTextBox/RichEdit control. The class sends an EM_FORMATRANGE message to the RichTextBox, using the Device context of the Bitmap where the control is being printed.
More details available in the MSDN Docs: How to Print the Contents of Rich Edit Controls.


The ScrollableControlToBitmap() method takes only a ScrollableControl type as argument: you cannot pass a TextBox control, even if it uses ScrollBars.

▶ Set the fullSize argument to true or false to include all child controls inside a Container or just those that are visible. If set to true, the Container’s ClientRectangle is expanded to include and print all its child Controls.

▶ Set the includeHidden argument to true or false to include or exclude the hidden control, if any.


Note: this code uses the Control.DeviceDpi property to evaluate the current Dpi of the container’s Device Context. This property requires .Net Framework 4.7+. If this version is not available, you can remove:

bitmap.SetResolution(canvas.DeviceDpi, canvas.DeviceDpi);

or derive the value with other means. See GetDeviceCaps.
Possibly, update the Project’s Framework version 🙂


// Prints the content of the current Form instance, 
// include all child controls and also those that are not visible
var bitmap = ControlPrinter.ScrollableControlToBitmap(this, true, true);

// Prints the content of a ScrollableControl inside a Form
// include all child controls except those that are not visible
var bitmap = ControlPrinter.ScrollableControlToBitmap(this.panel1, true, false);
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.Windows.Forms;

public class ControlPrinter
{
    public static Bitmap ScrollableControlToBitmap(ScrollableControl canvas, bool fullSize, bool includeHidden)
    {
        canvas.AutoScrollPosition = new Point(0, 0);
        if (includeHidden) {
            canvas.SuspendLayout();
            foreach (Control child in canvas.Controls) {
                child.Visible = true;
            }
            canvas.ResumeLayout(true);
        }

        canvas.PerformLayout();
        Size containerSize = canvas.DisplayRectangle.Size;
        if (fullSize) {
            containerSize.Width = Math.Max(containerSize.Width, canvas.ClientSize.Width);
            containerSize.Height = Math.Max(containerSize.Height, canvas.ClientSize.Height);
        }
        else {
            containerSize = canvas.ClientSize;;
        }
         
        var bitmap = new Bitmap(containerSize.Width, containerSize.Height, PixelFormat.Format32bppArgb);
        bitmap.SetResolution(canvas.DeviceDpi, canvas.DeviceDpi);

        var graphics = Graphics.FromImage(bitmap);
        if (canvas.BackgroundImage != null) {
            graphics.DrawImage(canvas.BackgroundImage, new Rectangle(Point.Empty, containerSize));
        }
        else {
            graphics.Clear(canvas.BackColor);
        }

        var rtfPrinter = new RichEditPrinter(graphics);

        try {
            DrawNestedControls(canvas, canvas, new Rectangle(Point.Empty, containerSize), bitmap, rtfPrinter);
            return bitmap;
        }
        finally {
            rtfPrinter.Dispose();
            graphics.Dispose();
        }
    }

    private static void DrawNestedControls(Control outerContainer, Control parent, Rectangle parentBounds, Bitmap bitmap, RichEditPrinter rtfPrinter)
    {
        for (int i = parent.Controls.Count - 1; i >= 0; i--) {
            var ctl = parent.Controls[i];
            if (!ctl.Visible || (ctl.Width < 1 || ctl.Height < 1)) continue;

            var clipBounds = Rectangle.Empty;
            if (parent.Equals(outerContainer)) { clipBounds = ctl.Bounds; }
            else {
                Size scrContainerSize = parentBounds.Size;
                if ((parent != ctl) && parent is ScrollableControl scrctl) {
                    if (scrctl.VerticalScroll.Visible) scrContainerSize.Width -= (SystemInformation.VerticalScrollBarWidth + 1);
                    if (scrctl.HorizontalScroll.Visible) scrContainerSize.Height -= (SystemInformation.HorizontalScrollBarHeight + 1);
                }
                clipBounds = Rectangle.Intersect(new Rectangle(Point.Empty, scrContainerSize), ctl.Bounds);
            }

            if (clipBounds.Width < 1 || clipBounds.Height < 1) continue;
            var bounds = outerContainer.RectangleToClient(parent.RectangleToScreen(clipBounds));

            if (ctl is RichTextBox rtb) {
                rtfPrinter.DrawRtf(rtb.Rtf, outerContainer.Bounds, bounds, ctl.BackColor);
            }
            else {
                ctl.DrawToBitmap(bitmap, bounds);
            }
            if (ctl.HasChildren) {
                DrawNestedControls(outerContainer, ctl, clipBounds, bitmap, rtfPrinter);
            }
        }
    }

    internal class RichEditPrinter : IDisposable
    {
        Graphics dc = null;
        RTBPrinter rtb = null;

        public RichEditPrinter(Graphics graphics)
        {
            this.dc = graphics;
            this.rtb = new RTBPrinter() { ScrollBars = RichTextBoxScrollBars.None };
        }

        public void DrawRtf(string rtf, Rectangle canvas, Rectangle layoutArea, Color color)
        {
            rtb.Rtf = rtf;
            rtb.Draw(dc, canvas, layoutArea, color);
            rtb.Clear();
        }

        public void Dispose() => this.rtb.Dispose();

        private class RTBPrinter : RichTextBox
        {
            public void Draw(Graphics g, Rectangle hdcArea, Rectangle layoutArea, Color color)
            {
                using (var brush = new SolidBrush(color)) {
                    g.FillRectangle(brush, layoutArea);
                };

                IntPtr hdc = g.GetHdc();
                var canvasAreaTwips = new RECT().ToInches(hdcArea);
                var layoutAreaTwips = new RECT().ToInches(layoutArea);

                var formatRange = new FORMATRANGE() {
                    charRange = new CHARRANGE() { cpMax = -1, cpMin = 0 },
                    hdc = hdc,
                    hdcTarget = hdc,
                    rect = layoutAreaTwips,
                    rectPage = canvasAreaTwips
                };

                IntPtr lParam = Marshal.AllocCoTaskMem(Marshal.SizeOf(formatRange));
                Marshal.StructureToPtr(formatRange, lParam, false);

                SendMessage(this.Handle, EM_FORMATRANGE, (IntPtr)1, lParam);
                Marshal.FreeCoTaskMem(lParam);
                g.ReleaseHdc(hdc);
            }

            [DllImport("User32.dll", CharSet = CharSet.Auto, SetLastError = true)]
            internal static extern int SendMessage(IntPtr hWnd, int uMsg, IntPtr wParam, IntPtr lParam);

            internal const int WM_USER = 0x0400;
            // https://docs.microsoft.com/en-us/windows/win32/controls/em-formatrange
            internal const int EM_FORMATRANGE = WM_USER + 57;

            [StructLayout(LayoutKind.Sequential)]
            internal struct RECT
            {
                public int Left;
                public int Top;
                public int Right;
                public int Bottom;

                public Rectangle ToRectangle() => Rectangle.FromLTRB(Left, Top, Right, Bottom);
                public RECT ToInches(Rectangle rectangle)
                {
                    float inch = 14.92f;
                    return new RECT() {
                        Left = (int)(rectangle.Left * inch),
                        Top = (int)(rectangle.Top * inch),
                        Right = (int)(rectangle.Right * inch),
                        Bottom = (int)(rectangle.Bottom * inch)
                    };
                }
            }

            // https://docs.microsoft.com/en-us/windows/win32/api/richedit/ns-richedit-formatrange?
            [StructLayout(LayoutKind.Sequential)]
            internal struct FORMATRANGE
            {
                public IntPtr hdcTarget;      // A HDC for the target device to format for
                public IntPtr hdc;            // A HDC for the device to render to, if EM_FORMATRANGE is being used to send the output to a device
                public RECT rect;             // The area within the rcPage rectangle to render to. Units are measured in twips.
                public RECT rectPage;         // The entire area of a page on the rendering device. Units are measured in twips.
                public CHARRANGE charRange;   // The range of characters to format (see CHARRANGE)
            }

            [StructLayout(LayoutKind.Sequential)]
            internal struct CHARRANGE
            {
                public int cpMin;           // First character of range (0 for start of doc)
                public int cpMax;           // Last character of range (-1 for end of doc)
            }
        }
    }
}

This is how it works:

Scrollable Control Draw To Bitmap

VB.Net version of the same procedure

Leave a Comment