Introduction
While coding an application that displays a detailed report in a ScrollViewer
, it was decided that it would be nice to print the report to a printer.
I found that WPF provides a PrintDialog.PrintVisual
method for printing any WPF control derived from the
Visual
class. PrintVisual
will only print
a single page so you have to scale your control to fit on the page. Unfortunately this would not work for me since the report was sometimes long enough
that it could not be read easily when scaled to fit on the page.
Another option for printing provided by WPF is to create a separate view in a
FlowDocument
. This is probably the best way to print documents,
but it was more work than I wished to put into it, not to mention the extra view that would have to be maintained for each control I wished to print.
What I ended up doing may be a bit unorthodox but works well for my purpose of printing a report that is already displayed in the application.
I take the control and convert it into a bitmap that will look good on a 300 dpi printer and then chop the bitmap up into pieces that will fit on a page,
add the pages to a FixedDocument
and send that to the printer using
PrintDialog.PrintDocument
.
Using the code
Below is a class that you can bind to that will print any control derived from the
FrameworkElement
class.
public class PrintCommand : ICommand
{
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
if (parameter is FrameworkElement)
{
FrameworkElement objectToPrint = parameter as FrameworkElement;
PrintDialog printDialog = new PrintDialog();
if ((bool)printDialog.ShowDialog().GetValueOrDefault())
{
Mouse.OverrideCursor = Cursors.Wait;
System.Printing.PrintCapabilities capabilities =
printDialog.PrintQueue.GetPrintCapabilities(printDialog.PrintTicket);
double dpiScale = 300.0 / 96.0;
FixedDocument document = new FixedDocument();
try
{
objectToPrint.Width = capabilities.PageImageableArea.ExtentWidth;
objectToPrint.UpdateLayout();
objectToPrint.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
Size size = new Size(capabilities.PageImageableArea.ExtentWidth,
objectToPrint.DesiredSize.Height);
objectToPrint.Measure(size);
size = new Size(capabilities.PageImageableArea.ExtentWidth,
objectToPrint.DesiredSize.Height);
objectToPrint.Measure(size);
objectToPrint.Arrange(new Rect(size));
double dpiX = 300;
double dpiY = 300;
RenderTargetBitmap bmp = new RenderTargetBitmap(Convert.ToInt32(
capabilities.PageImageableArea.ExtentWidth * dpiScale),
Convert.ToInt32(objectToPrint.ActualHeight * dpiScale),
dpiX, dpiY, PixelFormats.Pbgra32);
bmp.Render(objectToPrint);
PngBitmapEncoder png = new PngBitmapEncoder();
png.Frames.Add(BitmapFrame.Create(bmp));
System.Drawing.Bitmap bmp2;
using (MemoryStream memoryStream = new MemoryStream())
{
png.Save(memoryStream);
bmp2 = new System.Drawing.Bitmap(memoryStream);
}
document.DocumentPaginator.PageSize =
new Size(printDialog.PrintableAreaWidth, printDialog.PrintableAreaHeight);
int pageBreak = 0;
int previousPageBreak = 0;
int pageHeight =
Convert.ToInt32(capabilities.PageImageableArea.ExtentHeight * dpiScale);
while (pageBreak < bmp2.Height - pageHeight)
{
pageBreak += pageHeight;
while (!IsRowGoodBreakingPoint(bmp2, pageBreak))
pageBreak--;
PageContent pageContent = generatePageContent(bmp2, previousPageBreak,
pageBreak, document.DocumentPaginator.PageSize.Width,
document.DocumentPaginator.PageSize.Height, capabilities);
document.Pages.Add(pageContent);
previousPageBreak = pageBreak;
}
PageContent lastPageContent = generatePageContent(bmp2, previousPageBreak,
bmp2.Height, document.DocumentPaginator.PageSize.Width,
document.DocumentPaginator.PageSize.Height, capabilities);
document.Pages.Add(lastPageContent);
}
finally
{
objectToPrint.Width = double.NaN;
objectToPrint.UpdateLayout();
objectToPrint.LayoutTransform = new ScaleTransform(1, 1);
Size size = new Size(capabilities.PageImageableArea.ExtentWidth,
capabilities.PageImageableArea.ExtentHeight);
objectToPrint.Measure(size);
objectToPrint.Arrange(new Rect(new Point(capabilities.PageImageableArea.OriginWidth,
capabilities.PageImageableArea.OriginHeight), size));
Mouse.OverrideCursor = null;
}
printDialog.PrintDocument(document.DocumentPaginator, "Print Document Name");
}
}
}
The GeneratePageContent
method creates one page from a section of the bitmap of the UI control. The content on the page will show everything from
top
(the first row of the page) to bottom
( the last row of the page.) You could modify this method to add a header and/or footer to each page if desired.
private PageContent generatePageContent(System.Drawing.Bitmap bmp, int top,
int bottom, double pageWidth, double PageHeight,
System.Printing.PrintCapabilities capabilities)
{
FixedPage printDocumentPage = new FixedPage();
printDocumentPage.Width = pageWidth;
printDocumentPage.Height = PageHeight;
int newImageHeight = bottom - top;
System.Drawing.Bitmap bmpPage = bmp.Clone(new System.Drawing.Rectangle(0, top,
bmp.Width, newImageHeight), System.Drawing.Imaging.PixelFormat.Format32bppArgb);
Image pageImage = new Image();
BitmapSource bmpSource =
System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(
bmpPage.GetHbitmap(),
IntPtr.Zero,
System.Windows.Int32Rect.Empty,
BitmapSizeOptions.FromWidthAndHeight(bmp.Width, newImageHeight));
pageImage.Source = bmpSource;
pageImage.VerticalAlignment = VerticalAlignment.Top;
printDocumentPage.Children.Add(pageImage);
PageContent pageContent = new PageContent();
((System.Windows.Markup.IAddChild)pageContent).AddChild(printDocumentPage);
FixedPage.SetLeft(pageImage, capabilities.PageImageableArea.OriginWidth);
FixedPage.SetTop(pageImage, capabilities.PageImageableArea.OriginHeight);
pageImage.Width = capabilities.PageImageableArea.ExtentWidth;
pageImage.Height = capabilities.PageImageableArea.ExtentHeight;
return pageContent;
}
The IsRowGoodBreakingPoint
method evaluates a row of the bitmap to determine if it is a good place to start a new page.
This is a bit magical, but basically if the values of pixels in the row vary in color values to much, then there must be text or something else there
so we don't want to break to another page there. The maxDeviationForEmptyLine
variable is basically a tolerance value that will allow some deviation for table borders, etc.
private bool IsRowGoodBreakingPoint(System.Drawing.Bitmap bmp, int row)
{
double maxDeviationForEmptyLine = 1627500;
bool goodBreakingPoint = false;
if (rowPixelDeviation(bmp, row) < maxDeviationForEmptyLine)
goodBreakingPoint = true;
return goodBreakingPoint;
}
The rowPixelDeviation
method below is used to calculate how much difference there is in the colors of the pixels across one row of the bitmap.
This method uses pointers to quickly go through the bitmap, so you will have to set the Allow unsafe code property for the project.
private double rowPixelDeviation(System.Drawing.Bitmap bmp, int row)
{
int count = 0;
double total = 0;
double totalVariance = 0;
double standardDeviation = 0;
System.Drawing.Imaging.BitmapData bmpData = bmp.LockBits(new System.Drawing.Rectangle(0, 0,
bmp.Width, bmp.Height), System.Drawing.Imaging.ImageLockMode.ReadOnly, bmp.PixelFormat);
int stride = bmpData.Stride;
IntPtr firstPixelInImage = bmpData.Scan0;
unsafe
{
byte* p = (byte*)(void*)firstPixelInImage;
p += stride * row;
for (int column = 0; column < bmp.Width; column++)
{
count++; count the pixels
byte blue = p[0];
byte green = p[1];
byte red = p[3];
int pixelValue = System.Drawing.Color.FromArgb(0, red, green, blue).ToArgb();
total += pixelValue;
double average = total / count;
totalVariance += Math.Pow(pixelValue - average, 2);
standardDeviation = Math.Sqrt(totalVariance / count);
p += 3;
}
}
bmp.UnlockBits(bmpData);
return standardDeviation;
}
As mentioned at the beginning of the article, this was developed for printing UI controls that display some sort of report or details.
It is not going to work in its present state if the control contains an image as a background or contains an image that ends up being larger than what will fit on a page vertically.
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.