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:
- 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). - 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. - Clears the Bitmap using the background color of the Container.
- Iterates the
ScrollableControl.Controls
collection and prints all first-level child controls in their relative position (a child Control'sBounds
rectangle is relative to the container ClientArea). - 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:
C# version of the same procedure.