2

How do I use PrintDocument with a scrollable panel`?

Here is some of my code:

MemoryImage = new Bitmap(pnl.Width, pnl.Height);
Rectangle rect = new Rectangle(0, 0, pnl.Width, pnl.Height);
pnl.DrawToBitmap(MemoryImage, new Rectangle(0, 0, pnl.Width, 
pnl.Height));

Rectangle pagearea = e.PageBounds;
e.Graphics.DrawImage(MemoryImage, (pagearea.Width / 2) - 
(pannel.Width / 2), pannel.Location.Y);
Jimi
  • 29,621
  • 8
  • 43
  • 61
Cyrille Con Morales
  • 918
  • 1
  • 6
  • 21
  • Possible duplicate of [c# panel for drawing graphics and scrolling](https://stackoverflow.com/questions/4305011/c-sharp-panel-for-drawing-graphics-and-scrolling) – Anant Dabhi Jul 29 '19 at 06:28
  • Welcome to Stack Overflow. Please read [ask] and [mcve], for advice on how to present your question in a clear, answerable way. Please [edit your question](https://stackoverflow.com/posts/57247750/edit) so that includes an MCVE, explain what you've tried so far, what the code you have does, how that's different from what you want it to do, and what _specifically_ you need help with. – Peter Duniho Jul 29 '19 at 06:28
  • @AnantDabhi: your proposed duplicate has nothing at all to do with _printing_, something that this question is clearly asking about. – Peter Duniho Jul 29 '19 at 06:29

1 Answers1

5

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://learn.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://learn.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

Jimi
  • 29,621
  • 8
  • 43
  • 61
  • 2
    Two small fixes: *[1]* Draw children based on z-index (first in Controls collection, last to draw.) *[2]* Check if child is visible. – Reza Aghaei Jul 31 '19 at 18:07
  • @Jimi it gives me error on `canvas.DeviceDpi` :( can not find DeviceDpi – Inside Man Sep 07 '20 at 14:36
  • @Inside Man [Control.DeviceDpi](https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.control.devicedpi) requires .Net Framework 4.7+. Time to update :) But I'll add note. You can remove `bitmap.SetResolution(canvas.DeviceDpi, canvas.DeviceDpi);` in the meanwhile, it's not *strictly* necessary. If you app is DpiAware, it is. – Jimi Sep 07 '20 at 14:51
  • How do we extend this in second page ? @Jimi – Cyrille Con Morales Oct 18 '22 at 05:59
  • does this create new page? – Cyrille Con Morales Oct 28 '22 at 06:07