Click here to Skip to main content
15,922,512 members
Articles / Desktop Programming / WPF

Printing large WPF UserControls

Rate me:
Please Sign up or sign in to vote.
4.82/5 (10 votes)
29 Aug 2012CPOL2 min read 62.9K   31   11
Printing a large WPF UserControl across multiple pages.

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.

C#
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
                {
                    // Change the layout of the UI Control to match the width of the printer page
                    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));
 
                    // Convert the UI control into a bitmap at 300 dpi
                    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);
 
                    // Convert the RenderTargetBitmap into a bitmap we can more readily use
                    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);
 
                    // break the bitmap down into pages
                    int pageBreak = 0;
                    int previousPageBreak = 0;
                    int pageHeight = 
                        Convert.ToInt32(capabilities.PageImageableArea.ExtentHeight * dpiScale); 
                    while (pageBreak < bmp2.Height - pageHeight)
                    {
                        pageBreak += pageHeight;  // Where we thing the end of the page should be

                        // Keep moving up a row until we find a good place to break the page
                        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;
                    }
 
                    // Last Page
                    PageContent lastPageContent = generatePageContent(bmp2, previousPageBreak, 
                      bmp2.Height, document.DocumentPaginator.PageSize.Width, 
                      document.DocumentPaginator.PageSize.Height, capabilities); 
                    document.Pages.Add(lastPageContent);
                }
                finally
                {
                    // Scale UI control back to the original so we don't effect what is on the screen 
                    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.

C#
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);

    // Create a new bitmap for the contents of this page
    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;

    // Place the bitmap on the page
    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.

C#
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.

C#
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;  // find starting pixel of the specified 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);

            // go to next pixel
            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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMy vote of 4 Pin
JoshuaLamusga4-Oct-17 22:01
JoshuaLamusga4-Oct-17 22:01 
SuggestionFaster, more accurate version with margins Pin
JoshuaLamusga3-Oct-17 10:04
JoshuaLamusga3-Oct-17 10:04 
This version is intended to be a free improvement over the original.

Improvements
1. FindRowBreakpoint is a faster version of RowPixelDeviation that performs as few redundant steps as possible.
2. If no breakable row is found, the printer breaks at the end of the line (instead of crashing).
3. Every page has margins; margin size is controlled by static members. 96 pix = 1 inch.
4. All resources are disposed as soon as they can be, most importantly the memory stream.
5. Original deviation was bizarre. Now it is r = p[1]; g = p[2]; b = p[3]; and p += 4; at end.
6. Code structure is in clean, commented blocks.

Notes
1. Don't forget to set the document name (Ctrl + F for "Print Document Name" and change it).
2. There's a catch block with a generic error message. Feel free to change or remove it.
3. This version still does not effectively handle being out of memory.

using System;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace MyApplication
{
    /// <summary>
    /// A print mechanism that prints a large visual as bitmaps across
    /// multiple pages. Adapted from https://www.codeproject.com/Articles/339416/Printing-large-WPF-UserControls
    /// under the Code Project Open License (CPOL): https://www.codeproject.com/info/cpol10.aspx
    /// </summary>
    class VisualPrinter
    {
        #region Members
        /// <summary>
        /// The left and right-hand margins in pixels.
        /// </summary>
        public static int horzBorder;

        /// <summary>
        /// The top and bottom margins in pixels.
        /// </summary>
        public static int vertBorder;

        /// <summary>
        /// The expected horizontal DPI.
        /// </summary>
        private static double dpiX;

        /// <summary>
        /// The expected vertical DPI.
        /// </summary>
        private static double dpiY;
        #endregion

        #region Static Constructor
        /// <summary>
        /// Sets default member values.
        /// </summary>
        static VisualPrinter()
        {
            horzBorder = 48;
            vertBorder = 96;
            dpiX = 300;
            dpiY = 300;
        }
        #endregion

        #region Public Methods
        /// <summary>
        /// Prints a visual, breaking across pages. The user should've already
        /// accepted the print job. Returns success.
        /// </summary>
        public static bool PrintAcrossPages(PrintDialog dlg, FrameworkElement element)
        {
            FrameworkElement printable = element;
            System.Drawing.Bitmap bmp = null;

            if (dlg != null && printable != null)
            {
                Mouse.OverrideCursor = Cursors.Wait;

                System.Printing.PrintCapabilities capabilities =
                    dlg.PrintQueue.GetPrintCapabilities(dlg.PrintTicket);
                dlg.PrintTicket.PageBorderless = System.Printing.PageBorderless.None;

                double dpiScale = dpiY / 96.0;
                FixedDocument document = new FixedDocument();

                try
                {
                    //Sets width and waits for changes to settle.
                    printable.Width = capabilities.PageImageableArea.ExtentWidth;
                    printable.UpdateLayout();

                    //Recomputes the desired height.
                    printable.Measure(new Size(
                        double.PositiveInfinity,
                        double.PositiveInfinity));

                    //Sets the new desired size.
                    Size size = new Size(
                        capabilities.PageImageableArea.ExtentWidth,
                        printable.DesiredSize.Height);

                    //Measures and arranges to the desired size.
                    printable.Measure(size);
                    printable.Arrange(new Rect(size));

                    //Converts GUI to bitmap at 300 DPI
                    RenderTargetBitmap bmpTarget = new RenderTargetBitmap(
                        (int)(capabilities.PageImageableArea.ExtentWidth * dpiScale),
                        (int)(printable.ActualHeight * dpiScale),
                        dpiX, dpiY, PixelFormats.Pbgra32);
                    bmpTarget.Render(printable);

                    //Converts RenderTargetBitmap to bitmap.
                    PngBitmapEncoder png = new PngBitmapEncoder();
                    png.Frames.Add(BitmapFrame.Create(bmpTarget));

                    using (MemoryStream memStream = new MemoryStream())
                    {
                        png.Save(memStream);
                        bmp = new System.Drawing.Bitmap(memStream);
                        png = null;
                    }

                    using (bmp)
                    {
                        document.DocumentPaginator.PageSize =
                            new Size(dlg.PrintableAreaWidth, dlg.PrintableAreaHeight);

                        //Breaks bitmap to fit across pages.
                        int pageBreak = 0;
                        int lastPageBreak = 0;
                        int pageHeight = (int)
                            (capabilities.PageImageableArea.ExtentHeight * dpiScale);

                        //Adds each full page.
                        while (pageBreak < bmp.Height - pageHeight)
                        {
                            pageBreak += pageHeight;
                            
                            //Finds a page breakpoint from bottom, up.
                            pageBreak = FindRowBreakpoint(bmp, lastPageBreak, pageBreak);

                            //Adds the image segment to its own page.
                            PageContent pageContent = GeneratePageContent(
                                bmp, lastPageBreak, pageBreak,
                                document.DocumentPaginator.PageSize.Width,
                                document.DocumentPaginator.PageSize.Height,
                                capabilities);

                            document.Pages.Add(pageContent);
                            lastPageBreak = pageBreak;
                        }

                        //Adds remaining page contents.
                        PageContent lastPageContent = GeneratePageContent(
                            bmp, lastPageBreak,
                            bmp.Height, document.DocumentPaginator.PageSize.Width,
                            document.DocumentPaginator.PageSize.Height, capabilities);

                        document.Pages.Add(lastPageContent);
                    }
                }
                catch (Exception e)
                {
                    MessageBox.Show("An error occurred while trying to print.");
                }

                //Drops visual size adjustments.
                finally
                {
                    //Unsets width and waits for changes to settle.
                    printable.Width = double.NaN;
                    printable.UpdateLayout();

                    printable.LayoutTransform = new ScaleTransform(1, 1);

                    //Recomputes the desired height.
                    Size size = new Size(
                        capabilities.PageImageableArea.ExtentWidth,
                        capabilities.PageImageableArea.ExtentHeight);

                    //Measures and arranges to the desired size.
                    printable.Measure(size);
                    printable.Arrange(new Rect(new Point(
                        capabilities.PageImageableArea.OriginWidth,
                        capabilities.PageImageableArea.OriginHeight), size));

                    Mouse.OverrideCursor = null;
                }

                dlg.PrintDocument(document.DocumentPaginator, "Print Document Name");
                return true;
            }

            return false;
        }
        #endregion

        #region Private Methods
        /// <summary>
        /// Iterates from the bottom line upwards to the top (so as to trim as
        /// little as possible from the complete page) to determine where to
        /// separate a page. Returns the row to break, or last if none found.
        /// </summary>
        private static unsafe int FindRowBreakpoint(
            System.Drawing.Bitmap bmp,
            int topLine,
            int bottomLine)
        {
            //Any computed deviation above the threshold
            //is considered too detailed to break on.
            double deviationThreshold = 1627500;

            //Locks to read data.
            System.Drawing.Imaging.BitmapData bmpData = bmp.LockBits(
                new System.Drawing.Rectangle(0, 0, bmp.Width, bmp.Height),
                System.Drawing.Imaging.ImageLockMode.ReadOnly, bmp.PixelFormat);

            //Sets the initial row and position.
            int stride = bmpData.Stride;
            IntPtr topLeftPixel = bmpData.Scan0;
            byte* p = (byte*)(void*)topLeftPixel;

            //Iterates from bottom to top to find a breakable row.
            for (int i = bottomLine; i > topLine; i--)
            {
                int count = 0;
                double total = 0;
                double totalVariance = 0;

                //Sets pointer to this row.
                p = (byte*)(void*)topLeftPixel + stride * i;

                //Iterates through each consecutive pixel in the given row.
                for (int column = 0; column < bmp.Width; column++)
                {
                    count++;

                    byte red = p[1];
                    byte green = p[2];
                    byte blue = p[3];

                    //Faster than System.Drawing.Color.FromArgb(0, red, green, blue).ToArgb().
                    int pixelValue = (red << 16) | (green << 8) | blue;

                    total += pixelValue;
                    double average = total / count;
                    totalVariance += (pixelValue - average) * (pixelValue - average);

                    //Skips to next pixel.
                    p += 4;
                }

                //Breaks on this line if possible.
                double standardDeviation = Math.Sqrt(totalVariance / count);
                if (Math.Sqrt(totalVariance / count) < deviationThreshold)
                {
                    bmp.UnlockBits(bmpData);
                    return i;
                }
            }

            //Breaks on the last line given if no break row is found.
            bmp.UnlockBits(bmpData);
            return bottomLine;
        }

        /// <summary>
        /// Sizes the given bitmap to the page size and returns it as part
        /// of a printable page.
        /// </summary>
        private static PageContent GeneratePageContent(
            System.Drawing.Bitmap bmp,
            int top,
            int bottom,
            double pageWidth,
            double PageHeight,
            System.Printing.PrintCapabilities capabilities)
        {
            Image pageImage;
            BitmapSource bmpSource;

            //Creates a page with a specific width/height.
            FixedPage printPage = new FixedPage();
            printPage.Width = pageWidth;
            printPage.Height = PageHeight;

            //Cuts the given image at a reasonable boundary.
            int newImageHeight = bottom - top;

            //Creates a clone of the image.
            using (System.Drawing.Bitmap bmpCut =
                bmp.Clone(new System.Drawing.Rectangle(0, top, bmp.Width, newImageHeight),
                bmp.PixelFormat))
            {
                //Prepares the bitmap source.
                pageImage = new Image();
                bmpSource =
                    System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(
                        bmpCut.GetHbitmap(),
                        IntPtr.Zero,
                        Int32Rect.Empty,
                        BitmapSizeOptions.FromWidthAndHeight(bmpCut.Width, bmpCut.Height));
            }

            //Adds the bitmap to the page.
            pageImage.Source = bmpSource;
            pageImage.VerticalAlignment = VerticalAlignment.Top;
            printPage.Children.Add(pageImage);

            PageContent pageContent = new PageContent();
            ((System.Windows.Markup.IAddChild)pageContent).AddChild(printPage);

            //Adds a margin.
            printPage.Margin = new Thickness(
                horzBorder, vertBorder,
                horzBorder, vertBorder);

            FixedPage.SetLeft(pageImage, capabilities.PageImageableArea.OriginWidth);
            FixedPage.SetTop(pageImage, capabilities.PageImageableArea.OriginHeight);

            //Adjusts for the margins and to fit the page.
            pageImage.Width = capabilities.PageImageableArea.ExtentWidth - horzBorder*2;
            pageImage.Height = capabilities.PageImageableArea.ExtentHeight - vertBorder*2;
            return pageContent;
        }
        #endregion
    }
}


modified 5-Oct-17 3:57am.

GeneralRe: Faster, more accurate version with margins Pin
S.B.10-Jul-18 14:25
S.B.10-Jul-18 14:25 
GeneralRe: Faster, more accurate version with margins Pin
Member 1470607618-May-20 22:09
Member 1470607618-May-20 22:09 
GeneralRe: Faster, more accurate version with margins Pin
Ivandagaint10-Jan-21 15:26
Ivandagaint10-Jan-21 15:26 
QuestionPrinting datagrid Pin
Member 1079259927-Jul-16 9:01
Member 1079259927-Jul-16 9:01 
QuestionPrinting Larege Wpf Usercontrol Pin
Member 117974658-Jul-15 6:06
Member 117974658-Jul-15 6:06 
Questioncan you convert the last part in to vb.net ? I try the conversion but is giving me an error. please help Pin
stelios19847-Feb-15 6:13
stelios19847-Feb-15 6:13 
SuggestionAnother way to print long ListView as Visual Pin
Jetteroh17-Sep-12 21:58
Jetteroh17-Sep-12 21:58 
QuestionAlso Pin
Sacha Barber29-Aug-12 22:10
Sacha Barber29-Aug-12 22:10 
QuestionThis looked cool, but then I tried it and got OutOfMemoryException Pin
Sacha Barber29-Aug-12 22:02
Sacha Barber29-Aug-12 22:02 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.