The PageBounds rectangle property of the PrintPageEventArgs class represents the entire rectangle of the page, all the way to the edge. The MarginBounds rectangle represents the area inside the margins. Figure 7.7 shows the difference. Figure 7.7. PageBounds versus MarginBounds
Both PageBounds and MarginBounds are always scaled to units of 100 dpi, so a standard 8.5x11 inch piece of paper will always have a PageBounds rectangle {0, 0, 850, 1100}. With the default margin of 1 inch all the way around, the MarginBounds will be at {100, 100, 650, 900}. To match the bounds, by default the GraphicsUnit for the Graphics object will be 100 dpi, too, and will be scaled to whatever is appropriate for the printer resolution. For example, our laser printer is 600x600 dpi. The margin is useful not only because users often want some white space around their pages when they print, but also because many printers can't print to the edge of the page, so anything printed all the way to the edge is bound to be cut off to some degree. To avoid this, the Graphics object you get when you're printing starts at the top-left corner of the printable area of the page. That's useful for printing outside the margins, such as for headers or footers. However, because printers normally can't print to the edge of the page, the PageBounds rectangle will be too large. To get the actual size of the bounding rectangle, you can use the Graphics object's VisibleClipBounds rectangle: ' Get a page bounds with an accurate size Dim pageBounds As RectangleF = e.Graphics.VisibleClipBounds ' Draw a header g.DrawString("header", font, Brushes.Black, pageBounds) Unfortunately, for some reason VisibleClipBounds contains nonsense values when the page is previewed, so in that case, the PageBounds rectangle should be used. Also, if the Graphics object is using a nondefault PageUnit (as discussed in Chapter 6: Advanced Drawing), VisibleClipBounds will be in different units than PageBounds (which is always in units of 100 dpi). To handle these variables , it's useful to have a helper method to return the "real" page bounds in a consistent unit of measure: ' Get real page bounds based on printable area of the page Shared Function GetRealPageBounds(e As PrintPageEventArgs, _ preview As Boolean) As Rectangle ' Return in units of 1/100th of an inch If preview Then return e.PageBounds ' Translate to units 1/100th of an inch Dim vpb As RectangleF = e.Graphics.VisibleClipBounds Dim bottomRight() As PointF = { _ New PointF(vpb.Size.Width, vpb.Size.Height)} e.Graphics.TranformPoints( _ CoordinateSpace.Device, CoordinateSpace.Page, bottomRight) Dim dpiX As Single = e.Graphics.DpiX Dim dpiY As Single = e.Graphics.DpiY Return New Rectangle( _ 0, _ 0, _ CInt(bottomRight(0).X * 100 / dpiX), _ CInt(bottomRight(0).Y * 100 / dpiY)) End Function GetRealPageBounds returns the PageBounds rectangle if in preview mode [1] and always scales the returned Rectangle in the same units. This helper allows you to write your printing code to stay within the real bounds of the page:
' Get the real page bounds Dim pageBounds As Rectangle = GetRealPageBounds(e, Me.preview) ' Draw a header in the upper left g.DrawString("header", font, Brushes.Black, pageBounds) ' Draw a footer in the lower right Dim farFormat As StringFormat = New StringFormat() farFormat.Alignment = StringAlignment.Far farFormat.LineAlignment = StringAlignment.Far g.DrawString("footer", font, Brushes.Black, pageBounds, farFormat) For the bulk of the printed content, however, you should be printing inside the MarginBounds rectangle: Sub printDocument1_PrintPage(sender As Object, e As PrintPageEventArgs) Dim g As Graphics = e.Graphics g.DrawString(..., e.MarginBounds) End Sub Unfortunately, because MarginBounds is offset from PageBounds and because PageBounds is offset to stay inside the printable region of the page, MarginBounds is often lined up at offsets that don't match the user -specified margins along the edge of the page. For example, on our Hewlett-Packard LaserJet 2100, the left edge of the PageBounds rectangle is actually inch in from the left edge of the page, and the top edge is inch down from the top. This affects MarginBounds, lining up the 1-inch margin we expect at 1 inches from the left edge of the page. This poses a problem because neither PageBounds nor VisibleClipBounds nor any other information provided by WinForms actually tells you how much the PageBounds is offset from the edge of the paper. To get the physical offsets, you must turn to interoperability with Win32 and the GetDeviceCaps function. Using that, you can get the printer's physical X and Y offset from the top left and adjust the margins appropriately. However, the X and Y offset is in printer coordinates, which may not be the same units as MarginBounds, so you must convert those units as well. The following helper methods do all that work: <System.Runtime.InteropServices.DllImport("gdi32.dll", _ EntryPoint := "GetDeviceCaps")> _ Shared Function GetDeviceCaps(hdc As IntPtr, index As DeviceCapsIndex) _ As Integer End Function Enum DeviceCapsIndex PhysicalOffsetX = 112 PhysicalOffsetY = 113 End Enum ' Adjust MarginBounds rectangle when printing based ' on the physical characteristics of the printer Shared Function GetRealMarginBounds(e As PrintPageEventArgs, _ preview As Boolean) If preview Then Return e.MarginBounds Dim cx As Integer = 0 Dim cy As Integer = 0 Dim hdc As IntPtr = e.Graphics.GetHdc() Try ' Both of these come back as device units and are not ' scaled to 1/100th of an inch cx = GetDeviceCaps(hdc, DeviceCapsIndex.PhysicalOffsetX) cy = GetDeviceCaps(hdc, DeviceCapsIndex.PhysicalOffsetY) Finally e.Graphics.ReleaseHdc(hdc) End Try ' Create the real margin bounds by scaling the offset ' by the printer resolution and then rescaling it ' back to 1/100th of an inch Dim marginBounds As RectangleF = e.MarginBounds Dim dpiX As Integer = CInt(e.Graphics.DpiX) Dim dpiY As Integer = CInt(e.Graphics.DpiY) marginBounds.Offset(-cx * 100 / dpiX, -cy * 100 / dpiY) Return marginBounds End Sub The GetRealMarginBounds method takes preview mode into account and, when you use a real printer, adjusts MarginBounds using the physical offsets, always returning a rectangle in the same units. With this in place, you can safely print inside the margins based on the edges of the paper, as you'd expect: Sub printDocument1_PrintPage(sender As Object, e As PrintPageEventArgs) ... g.DrawString(..., GetRealMarginBounds(e)) End Sub As an alternative to using these helper functions, the .NET 1.1 Framework provides a property on PrintDocument called OriginAtMargins. This property defaults to false, but setting it to true sets the offset of the PageBounds rectangle to be at the margin offset from the physical edge of the page, letting you print at the appropriate margins using the PageBounds rectangle. However, this property doesn't have any effect in preview mode, doesn't adjust the PageBounds size, and keeps the MarginBounds as offset from the now further offset PageBounds. For these reasons, we don't find it particularly useful when compared with the GetRealPageBounds and GetRealMarginBounds helper methods. |