Mar 23, 2011

Paginated Printing of WPF Visuals - II

Last time I discussed paginated printing of wpf visuals, I mentioned that the pagination didn't support a whole lot of options.

There was no support for margins or headers for example. The print was edge-to-edge which is OK when you're a developer and you're going to throw the paper out right after printing, but not so good if you are the end user and need to put the paper in a binder, or if you've dropped the whole mess on the floor and need to put it back in order.

The easy way to put more visuals with the visuals you have rendered is to use a ContainerVisual object and stack some visuals together. With my previous entry there was a problem because it transformed and rendered the original Visual directly to print. This meant that the Visual could not be part of ContainerVisual as it already had a visual parent.

To solve that problem, I rendered the Visual to bitmap before print. That way I could use ContainerVisual with the bitmap and stack up any number of visuals in the container. Here's the class.

class FrameworkVisualPrint : DocumentPaginator
{
    private readonly FrameworkElement Element;
    private const double XMargin = 1 * 96;
    private const double YMargin = 1 * 96;
    public String Header { set; get; }

    public FrameworkVisualPrint(FrameworkElement element)
    {
        Element = element;
    }

    private const double ScaleFactor = 0.5;

    private DrawingVisual GetHeader(int pageNumber)
    {
        var header = new DrawingVisual();

        using (var dc = header.RenderOpen())
        {
            var text = new FormattedText("Page " + (pageNumber + 1) + Environment.NewLine +
                Header, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, 
                new Typeface("Arial"), 8, Brushes.Black);
            dc.DrawText(text, new Point(XMargin, 0.5 * 96));
        }

        return header;
    }

    public override DocumentPage GetPage(int pageNumber)
    {
        var tgroup = new TransformGroup();
        tgroup.Children.Add(new ScaleTransform(ScaleFactor, ScaleFactor));
        Element.RenderTransform = tgroup;
        tgroup.Children.Add(new TranslateTransform(-PageSize.Width * (pageNumber % Columns) 
            + XMargin, -PageSize.Height * (pageNumber / Columns) + YMargin));
        Element.RenderTransform = tgroup;

        try
        {
            var area = new Rect(
                new Point((PageSize.Width * (pageNumber % Columns)) / ScaleFactor,
                          (PageSize.Height * (pageNumber / Columns)) / ScaleFactor),
                new Size(PageSize.Width / ScaleFactor,
                         pageSize.Height / ScaleFactor));

            Element.Clip = new RectangleGeometry(area);
            var elementSize = new Size(
                Element.ActualWidth,
                Element.ActualHeight);
            Element.Measure(elementSize);
            Element.Arrange(new Rect(new Point(0, 0), elementSize));

            // Make a bitmap from the transformed Element
            var bitmap = PngBitmap(Element, (int)area.Width, (int)area.Height);
            var vis = new DrawingVisual();

            // Draw the bitmap into a DrawingVisual object
            DrawingContext dc = vis.RenderOpen();
            dc.DrawImage(bitmap, new Rect(0, 0, area.Width, area.Height));
            dc.Close();

            // Grab a header to apply (with page numbers)
            var header = GetHeader(pageNumber);
            // Container for all the new stuff.
            var cVisual = new ContainerVisual();

            cVisual.Children.Add(header);
            cVisual.Children.Add(vis);
            return new DocumentPage((cVisual));

        }
        finally
        {
            Element.RenderTransform = null;
            Element.Clip = null;
        }

    }

    private RenderTargetBitmap PngBitmap(Visual visual, int width, int height)
    {
        // Render
        var rtb =
            new RenderTargetBitmap(
                (int)(width / ScaleFactor),
                (int)(height / ScaleFactor),
                96.0 / ScaleFactor,
                96.0 / ScaleFactor,
                PixelFormats.Default);
        rtb.Render(visual);


        return rtb;
    }

    private Size pageSize;
    public override Size PageSize
    {
        set
        {
            pageSize = value;
            pageSize.Width -= XMargin;
            pageSize.Height -= YMargin;
        }
        get { return pageSize; }
    }
}

The class isn't actually complete. See the previous article for the initial version. This is just new and changed methods.

The important bits are commented.
  • Make a bitmap from the Visual
  • Draw the bitmap to a new DrawingVisual Object
  • Stick the DrawingVisual and any other visuals needed in a ContainerVisual
  • Bob's your uncle!

No comments:

Post a Comment