How to print hidden and visible content of a Container with ScrollBars

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 ([ScrollableControl].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
Dim bitmap = ControlsPrinter.ScrollableControlToBitmap(Me, True, True)

' Prints the content of a ScrollableControl inside a Form
' include all child controls except those that are not visible
Dim bitmap = ControlsPrinter.ScrollableControlToBitmap(Me.Panel1, True, False)

Imports System.Drawing
Imports System.Drawing.Imaging
Imports System.Runtime.InteropServices
Imports System.Windows.Forms

Public Class ControlPrinter
    Public Shared Function ScrollableControlToBitmap(canvas As ScrollableControl, fullSize As Boolean, includeHidden As Boolean) As Bitmap
        canvas.AutoScrollPosition = New Point(0, 0)
        If includeHidden Then
            canvas.SuspendLayout()
            For Each child As Control In canvas.Controls
                child.Visible = True
            Next
            canvas.ResumeLayout(True)
        End If

        canvas.PerformLayout()
        Dim containerSize As Size = canvas.DisplayRectangle.Size
        If fullSize Then
            containerSize.Width = Math.Max(containerSize.Width, canvas.ClientSize.Width)
            containerSize.Height = Math.Max(containerSize.Height, canvas.ClientSize.Height)
        Else
            containerSize = If((TypeOf canvas Is Form), canvas.PreferredSize, canvas.ClientSize)
        End If

        Dim bmp = New Bitmap(containerSize.Width, containerSize.Height, PixelFormat.Format32bppArgb)
        bmp.SetResolution(canvas.DeviceDpi, canvas.DeviceDpi)

        Dim g = Graphics.FromImage(bmp)
        g.Clear(canvas.BackColor)
        Dim rtfPrinter = New RichEditPrinter(g)

        Try
            DrawNestedControls(canvas, canvas, New Rectangle(Point.Empty, containerSize), bmp, rtfPrinter)
            Return bmp
        Finally
            rtfPrinter.Dispose()
            g.Dispose()
        End Try
    End Function

    Private Shared Sub DrawNestedControls(outerContainer As Control, parent As Control, parentBounds As Rectangle, bmp As Bitmap, rtfPrinter As RichEditPrinter)
        For i As Integer = parent.Controls.Count - 1 To 0 Step -1
            Dim ctl = parent.Controls(i)
            If Not ctl.Visible OrElse (ctl.Width < 1 OrElse ctl.Height < 1) Then Continue For

            Dim clipBounds = Rectangle.Empty
            If parent.Equals(outerContainer) Then
                clipBounds = ctl.Bounds
            Else
                Dim scrContainerSize As Size = parentBounds.Size
                If TypeOf parent Is ScrollableControl Then
                    Dim scrCtrl = DirectCast(parent, ScrollableControl)
                    With scrCtrl
                        If .VerticalScroll.Visible Then scrContainerSize.Width -= (SystemInformation.VerticalScrollBarWidth + 1)
                        If .HorizontalScroll.Visible Then scrContainerSize.Height -= (SystemInformation.HorizontalScrollBarHeight + 1)
                    End With
                End If
                clipBounds = Rectangle.Intersect(New Rectangle(Point.Empty, scrContainerSize), ctl.Bounds)
            End If
            If clipBounds.Width < 1 OrElse clipBounds.Height < 1 Then Continue For

            Dim bounds = outerContainer.RectangleToClient(parent.RectangleToScreen(clipBounds))

            If TypeOf ctl Is RichTextBox Then
                Dim rtb = DirectCast(ctl, RichTextBox)
                rtfPrinter.DrawRtf(rtb.Rtf, outerContainer.Bounds, bounds, ctl.BackColor)
            Else
                ctl.DrawToBitmap(bmp, bounds)
            End If

            If ctl.HasChildren Then
                DrawNestedControls(outerContainer, ctl, clipBounds, bmp, rtfPrinter)
            End If
        Next
    End Sub

    Friend Class RichEditPrinter
        Implements IDisposable
        Private dc As Graphics = Nothing
        Private rtb As RTBPrinter = Nothing

        Public Sub New(graphics As Graphics)
            dc = graphics
            rtb = New RTBPrinter() With {
                .ScrollBars = RichTextBoxScrollBars.None
            }
        End Sub

        Public Sub DrawRtf(rtf As String, canvas As Rectangle, layoutArea As Rectangle, color As Color)
            rtb.Rtf = rtf
            rtb.Draw(dc, canvas, layoutArea, color)
            rtb.Clear()
        End Sub

        Public Sub Dispose() Implements IDisposable.Dispose
            rtb.Dispose()
        End Sub

        Private Class RTBPrinter
            Inherits RichTextBox
            Public Sub Draw(g As Graphics, hdcArea As Rectangle, layoutArea As Rectangle, color As Color)
                Using brush = New SolidBrush(color)
                    g.FillRectangle(brush, layoutArea)
                End Using

                Dim hdc As IntPtr = g.GetHdc()
                Dim canvasAreaTwips = New RECT().ToInches(hdcArea)
                Dim layoutAreaTwips = New RECT().ToInches(layoutArea)

                Dim formatRange = New FORMATRANGE() With {
                    .charRange = New CHARRANGE() With {
                        .cpMax = -1,
                        .cpMin = 0
                    },
                    .hdc = hdc,
                    .hdcTarget = hdc,
                    .rect = layoutAreaTwips,
                    .rectPage = canvasAreaTwips
                }

                Dim lParam As IntPtr = Marshal.AllocCoTaskMem(Marshal.SizeOf(formatRange))
                Marshal.StructureToPtr(formatRange, lParam, False)

                SendMessage(Me.Handle, EM_FORMATRANGE, CType(1, IntPtr), lParam)
                Marshal.FreeCoTaskMem(lParam)
                g.ReleaseHdc(hdc)
            End Sub

            <DllImport("User32.dll", CharSet:=CharSet.Auto, SetLastError:=True)>
            Friend Shared Function SendMessage(hWnd As IntPtr, uMsg As Integer, wParam As IntPtr, lParam As IntPtr) As Integer
            End Function

            Friend Const WM_USER As Integer = &H400
            Friend Const EM_FORMATRANGE As Integer = WM_USER + 57

            <StructLayout(LayoutKind.Sequential)>
            Friend Structure RECT
                Public Left As Integer
                Public Top As Integer
                Public Right As Integer
                Public Bottom As Integer

                Public Function ToRectangle() As Rectangle
                    Return Rectangle.FromLTRB(Left, Top, Right, Bottom)
                End Function

                Public Function ToInches(rectangle As Rectangle) As RECT
                    Dim inch As Single = 14.92F
                    Return New RECT() With {
                        .Left = CType(rectangle.Left * inch, Integer),
                        .Top = CType(rectangle.Top * inch, Integer),
                        .Right = CType(rectangle.Right * inch, Integer),
                        .Bottom = CType(rectangle.Bottom * inch, Integer)
                    }
                End Function
            End Structure

            <StructLayout(LayoutKind.Sequential)>
            Friend Structure FORMATRANGE
                Public hdcTarget As IntPtr      ' A HDC for the target device to format for
                Public hdc As IntPtr            ' A HDC for the device to render to, if EM_FORMATRANGE is being used to send the output to a device
                Public rect As RECT             ' The area within the rcPage rectangle to render to. Units are measured in twips.
                Public rectPage As RECT         ' The entire area of a page on the rendering device. Units are measured in twips.
                Public charRange As CHARRANGE   ' The range of characters to format (see CHARRANGE)
            End Structure

            <StructLayout(LayoutKind.Sequential)>
            Friend Structure CHARRANGE
                Public cpMin As Integer   ' First character of range (0 for start of doc)
                Public cpMax As Integer   ' Last character of range (-1 for end of doc)
            End Structure
        End Class
    End Class
End Class

This is how it works:

ScrollableControl to Bitmap

C# version of the same procedure.