Building a smart DocumentPaginator that is able to cut Compex UIElement in Pages for printing

I am currently working with DocumentPaginator which allows me to do:

PrintDialog printDialog = new PrintDialog();
if(printDialog.ShowDialog()== true)
{
    var cp = new ControlPaginator(myUIElement, new Size(printDialog.PrintableAreaWidth, printDialog.PrintableAreaHeight));
    printDialog.PrintDocument(cp,"Test");
}

      

because every time you want to print a visual code that takes multiple pages is a pain in the ass. So I decided to create a smart control that will do it for me 90% of the time.

but it seems that I am not smart enough ...

so here is my code

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;

namespace WpfUIPrinter
{
    public class ControlPaginator : DocumentPaginator
    {
        private List<ControlDocumentPage> Pages { get; set; }

        private Rect PageRect;

        private const double Pagebegin = 0;

        #region cTor

        public ControlPaginator(UIElement VisualElement, Size PageSize)
        {
            if (VisualElement == null)
                throw new ArgumentNullException("Das UIElement VisualElement darf nicht Null sein");
            if (PageSize == null)
                throw new ArgumentNullException("Die Size PageSize darf nicht Null sein ");
            if (PageSize == null || PageSize.Width < 1 || PageSize.Height < 1)
                throw new ArgumentException("Die Size PageSize muss mindestens  eine Width > 0 und Height > 0");

            this.Pages = new List<ControlDocumentPage>();

            this.PageSize = PageSize;
            this.VisualElement = VisualElement;

            this.PageRect = new Rect(Pagebegin, Pagebegin, Math.Floor(PageSize.Width), Math.Floor(PageSize.Height));

            createPages();
        }

        #region Pages

        /// <summary>
        /// 
        /// </summary>
        private void createPages()
        {
            // if the print visual is not displayed we have to measure and arrange it
            // if it is displayed only the parts that are visible will be rendered for print
            VisualElement.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
            VisualElement.Arrange(new Rect(new Point(0, 0), VisualElement.DesiredSize));

            var rightPageBegin = Pagebegin;
            var bottomPageBegin = Pagebegin;

            while ((double)VisualElement.GetValue(FrameworkElement.ActualHeightProperty) > bottomPageBegin)
            {
                while ((double)VisualElement.GetValue(FrameworkElement.ActualWidthProperty) > rightPageBegin)
                {
                    // just for debugging
                    Console.WriteLine(rightPageBegin + "|" + bottomPageBegin);

                    // reposition  of the Printable location
                    this.PageRect.Location = new Point(rightPageBegin, bottomPageBegin);
                    this.Pages.Add(createPage());

                    rightPageBegin = Pages.Count == 0 ? 0 : Pages.LastOrDefault().RightPageBegin.Value;
                }

                var sl = Pages.Where(page => page.BottomPageBegin > bottomPageBegin).ToList();
                bottomPageBegin = sl.Count == 0 ? 0 : sl.Min(page => page.BottomPageBegin).Value >= bottomPageBegin ? bottomPageBegin + PageRect.Height : sl.Min(page => page.BottomPageBegin).Value;
                rightPageBegin = 0;
            }
        }

        #endregion Pages

        private ControlDocumentPage createPage()
        {
            ////clonen des VisualElement
            var VisualtoPrint = VisualElement.Clone();

            // if the print visual is not displayed we have to measure and arrange it
            // if it is displayed only the parts that are visible will be rendered for print

            var w = (double)VisualElement.GetValue(FrameworkElement.ActualWidthProperty);
            var h = (double)VisualElement.GetValue(FrameworkElement.ActualHeightProperty);

            VisualtoPrint.Measure(new Size(w, h));
            VisualtoPrint.Arrange(new Rect(new Point(PageRect.Left * -1, PageRect.Top * -1), VisualElement.DesiredSize));
            VisualtoPrint.UpdateLayout();

            var list = ((DependencyObject)VisualtoPrint).getChilds().ToList();

            ForeachChildIn(VisualtoPrint, list);

            double RightPageBegin;
            double BottomPageBegin;
            FindPageBeginns(VisualtoPrint, out RightPageBegin, out BottomPageBegin);

            LookupDependencyObject.Clear();

            return new ControlDocumentPage(VisualtoPrint, RightPageBegin, BottomPageBegin);
        }

        /// <summary>
        ///  Tryed to find the beginn of the following pages
        /// </summary>
        /// <param name="VisualtoPrint"></param>
        /// <param name="RightPageBegin"></param>
        /// <param name="BottomPageBegin"></param>
        private void FindPageBeginns(UIElement VisualtoPrint, out double RightPageBegin, out double BottomPageBegin)
        {
            RightPageBegin = PageRect.Right;
            BottomPageBegin = PageRect.Bottom;

            bool gotright = false, gotbottom = false;

            //tryes to find the begin of the next page on the right
            foreach (var item in LookupDependencyObject.Where(kv => kv.Width <= PageRect.Width && kv.Start == Position.Center && kv.End == Position.MidRight).ToList())
            {
                if (PageRect.Left < item.StartPoint.X && RightPageBegin > item.StartPoint.X)
                {
                    RightPageBegin = item.StartPoint.X;
                    gotright = true;
                }
            }

            // tryes to fin the begin of the next page on the bottom
            foreach (var item in LookupDependencyObject.Where(kv => kv.Height <= PageRect.Height && kv.Start == Position.Center && kv.End == Position.BottomMid).ToList())
            {
                if (PageRect.Top < item.StartPoint.Y && BottomPageBegin > item.StartPoint.Y)
                {
                    BottomPageBegin = item.StartPoint.Y;
                    gotbottom = true;
                }
            }

            //tryes to find the begin of the next page on the right and on the bottom
            foreach (var item in LookupDependencyObject.Where(kv => kv.Width <= PageRect.Width && kv.Height <= PageRect.Height && kv.Start == Position.Center && kv.End == Position.BottomRight).ToList())
            {
                if (RightPageBegin > item.StartPoint.X)
                {
                    RightPageBegin = item.StartPoint.X;
                    gotright = true;
                }

                if (BottomPageBegin > item.StartPoint.Y)
                {
                    BottomPageBegin = item.StartPoint.Y;
                    gotbottom = true;
                }
            }

            //if no page to the right was set, last try to find the startpoint for the next Page on the right
            if (!gotright)
            {
                var where = LookupDependencyObject.Where(kv => kv.Start == Position.Center && kv.EndPoint.X > PageRect.Width);
                RightPageBegin = where.Min(kv => kv.EndPoint.X);
            }

            //if no page to the bottom was set, last try to find the startpoint for the next Page on the bottom
            if (!gotbottom)
            {
                var where = LookupDependencyObject.Where(kv => kv.Start == Position.Center && kv.EndPoint.Y > PageRect.Height);
                BottomPageBegin = where.Min(kv => kv.EndPoint.Y);
            }
        }


        /// <summary>
        /// Visual/LogicalTree Crawler which hides Elements if the are overlap and NOT bigger than the Page
        /// </summary>
        /// <param name="parent"></param>
        /// <param name="childList"></param>
        /// <returns></returns>
        private bool ForeachChildIn(DependencyObject parent, IList<DependencyObject> childList)
        {
            var result = false;
            foreach (var item in childList)
            {
                if (item is Visual && parent is Visual)
                {
                    if (VisualChild(parent, item))
                    {
                        result = true;
                        continue;
                    }
                }

                if (ForeachChildIn(item, item.getChilds().ToList()))
                    result = true;
                else if (!(LookupDependencyObject.Last().Height > PageRect.Height || LookupDependencyObject.Last().Width > PageRect.Width))
                    item.SetValue(FrameworkElement.VisibilityProperty, Visibility.Hidden);

            }
            return result;
        }

        private bool VisualChild(DependencyObject parent, DependencyObject child)
        {
            var visualChild = child as Visual;
            double ActualWidth,
                   ActualHeight;

            //relative Position des myVisual zu VisualElement
            Point relativeSartPoint = visualChild.TransformToAncestor((Visual)parent)
                                              .Transform(new Point(0, 0));

            Point relativeEndPoint = createRelativEndPoint(visualChild, relativeSartPoint, out ActualWidth, out ActualHeight);

            Position start = findDOPosition(PageRect, relativeSartPoint);
            Position end = findDOPosition(PageRect, relativeEndPoint);

            var DOContainer = new DependencyObjectContainer
            {
                DO = child,
                BeginVisbility = (Visibility)child.GetValue(FrameworkElement.VisibilityProperty),

                StartPoint = relativeSartPoint,
                EndPoint = relativeEndPoint,

                Start = start,
                End = end,

                Width = ActualWidth,
                Height = ActualHeight
            };

            LookupDependencyObject.Add(DOContainer);

            if (start == Position.Center && end == Position.Center)
                return true;
            return false;
        }

        /// <summary>
        /// Prüft die Positon des Punktes im verhältnis zum Rechteck
        /// </summary>
        /// <param name="PageRect"></param>
        /// <param name="relativePoint"></param>
        /// <returns></returns>
        private Position findDOPosition(Rect PageRect, Point relativePoint)
        {
            //Top
            if (PageRect.Top > relativePoint.Y)
                if (PageRect.Left > relativePoint.X)
                    return Position.TopLeft;
                else if (PageRect.Right < relativePoint.X)
                    return Position.TopRight;
                else
                    return Position.TopMid;
            //Bottom
            else if (PageRect.Bottom < relativePoint.Y)
                if (PageRect.Left > relativePoint.X)
                    return Position.BottomLeft;
                else if (PageRect.Right < relativePoint.X)
                    return Position.BottomRight;
                else
                    return Position.BottomMid;
            //Mid
            if (PageRect.Left > relativePoint.X)
                return Position.MidLeft;
            else if (PageRect.Right < relativePoint.X)
                return Position.MidRight;
            else
                return Position.Center;
        }

        /// <summary>
        /// Hier wierd der Endpunkt des Objektes berechnet 
        /// da hier für die weite und höhe des Visual ermittelt wird
        /// werden diese Werte zusätzlich via out zurückgegeben
        /// </summary>
        /// <param name="myVisual"></param>
        /// <param name="relativeSartPoint"></param>
        /// <param name="ActualWidth"></param>
        /// <param name="ActualHeight"></param>
        /// <returns></returns>
        private static Point createRelativEndPoint(Visual myVisual, Point relativeSartPoint, out double ActualWidth, out double ActualHeight)
        {
            double relativeEndHorizontal;
            double relativeEndVertikal;

            ActualWidth = (double)myVisual.GetValue(FrameworkElement.ActualWidthProperty);
            ActualHeight = (double)myVisual.GetValue(FrameworkElement.ActualHeightProperty);

            relativeEndHorizontal = relativeSartPoint.X + ActualWidth;
            relativeEndVertikal = relativeSartPoint.Y + ActualHeight;

            return new Point(relativeEndHorizontal, relativeEndVertikal);
        }

        private List<DependencyObjectContainer> LookupDependencyObject = new List<DependencyObjectContainer>();

        #endregion cTor

        public UIElement VisualElement
        { get; set; }

        #region DocumentPaginator

        #region Properties

        public override bool IsPageCountValid
        {
            get
            {
                return true;
                //throw new NotImplementedException();
            }
        }

        public override int PageCount
        {
            get { return Pages.Count; }
        }

        public override System.Windows.Size PageSize { get; set; }

        public override IDocumentPaginatorSource Source
        {
            get { return null; }
        }

        #endregion Properties

        public override DocumentPage GetPage(int pageNumber)
        {
            return Pages[pageNumber];
        }

        private int CountPages()
        {
            if (VisualElement == null)
                return -1;

            return Pages.Count;
        }

        #endregion DocumentPaginator
    }
    /// <summary>
    /// Used as Information Container
    /// </summary>
    internal class DependencyObjectContainer
    {
        public DependencyObject DO { get; set; }

        public Visibility BeginVisbility { get; set; }

        public Point StartPoint { get; set; }

        public Point EndPoint { get; set; }

        public Position Start { get; set; }

        public Position End { get; set; }

        public double Width { get; set; }

        public double Height { get; set; }
    }
}

      

ControlDocumentPage

using System.Windows;
using System.Windows.Documents;
using System.Windows.Media;

namespace WpfUIPrinter
{
    public class ControlDocumentPage : DocumentPage
    {
        public double? RightPageBegin { get; set; }

        public double? BottomPageBegin { get; set; }

        public ControlDocumentPage(Visual visual, double? rightPageBegin, double? bottomPageBegin)
            : base(visual)
        {
            this.RightPageBegin = rightPageBegin;
            this.BottomPageBegin = bottomPageBegin;
        }

        public ControlDocumentPage(Visual visual, double? rightPageBegin, double? bottomPageBegin, Size pageSize, Rect bleedBox, Rect contentBox)
            : base(visual, pageSize, bleedBox, contentBox)
        {
            this.RightPageBegin = rightPageBegin;
            this.BottomPageBegin = bottomPageBegin;
        }
    }
}

      

enumeration position

namespace WpfUIPrinter
{
    public enum Position
    {
        TopLeft = 7,        TopMid = 8,        TopRight = 9,
        MidLeft = 4,        Center = 5,        MidRight = 6,
        BottomLeft = 1,     BottomMid = 2,     BottomRight = 3,
        FullOut = 0
    }
}

      

UIElementExtension

using System.IO;
using System.Windows;
using System.Windows.Markup;
using System.Xml;

namespace WpfUIPrinter
{
    public static class UIElementExtension
    {
        public static UIElement Clone(this UIElement uielement)
        {
            string xamlString = XamlWriter.Save(uielement);
            StringReader stringReader = new StringReader(xamlString);
            XmlReader xmlReader = XmlReader.Create(stringReader);
            return (UIElement)XamlReader.Load(xmlReader);
        }
    }
}

      

DependencyObjectExtension

///Basiert auf Josh Smith Tutorial
///Understanding the Visual Tree and Logical Tree in WPF
///http://www.codeproject.com/Articles/21495/Understanding-the-Visual-Tree-and-Logical-Tree-in
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Media3D;

namespace WpfUIPrinter
{
    public static class DependencyObjectExtension
    {
        /// <summary>
        /// This method is an alternative to WPF's
        /// <see cref="VisualTreeHelper.GetChild"/> method, which also
        /// supports content elements. Do note, that for content elements,
        /// this method falls back to the logical tree of the element.
        /// </summary>
        /// <param name="parent">The item to be processed.</param>
        /// <returns>The submitted item child elements, if available.</returns>
        public static IEnumerable<DependencyObject> getChilds(this DependencyObject parent)
        {
            if (parent == null) yield break;

            //use the logical tree for content / framework elements
            foreach (object obj in LogicalTreeHelper.GetChildren(parent))
            {
                var depObj = obj as DependencyObject;
                if (depObj != null)
                    yield return depObj;
            }

            //use the visual tree per default
            if (parent is Visual || parent is Visual3D)
            {
                int count = VisualTreeHelper.GetChildrenCount(parent);
                for (int i = 0; i < count; i++)
                {
                    yield return VisualTreeHelper.GetChild(parent, i);
                }
            }
        }
    }
}

      

Known Issues:

  • Cannot reprint the binding at the moment (there is something that can be done with the method i i Clone my UIElemnt)
  • Start page search (sometimes it cannot find the right point to start on a new page)
  • hidding Elements to be displayed (here is the part where I'm not smart enough)
+3


source to share





All Articles