Search code examples
vb.netwinformsgraphicsscrollbarpanel

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


I have a scrollable Panel: some of its child Controls are hidden and other are visible.

How can I print all the content on this Panel, including child Controls that are hidden or otherwise not visible without scrolling?

Private Sub PrintDocument1_PrintPage(sender As Object, e As Printing.PrintPageEventArgs) Handles PrintDocument1.PrintPage
    Panel1.AutoSize = True
    Dim b As New Bitmap(Panel1.DisplayRectangle.Width, Panel1.DisplayRectangle.Height)
    Panel1.DrawToBitmap(b, Panel1.ClientRectangle)
    e.Graphics.DrawImage(b, New Point(40, 40))
    Panel1.AutoSize = False
End Sub

Solution

  • 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.