zoukankan      html  css  js  c++  java
  • WPF打印原理,自定义打印

    一.基础知识

    1.System.Printing命名空间

    我们可以先看一下System.Printing命名空间,东西其实很多,功能也非常强大,可以说能够控制打印的每一个细节,曾经对PrintDialog失望的我看到了一丝曙光。

    2.PrintDialog

    可以看到PrintDialog除了构造函数有三个方法和一堆属性,PrintDocument接受一个分页器(DocumentPaginator,稍后介绍),PrintVisual可以打印Visual,也就是WPF中的大部分继承自Visual类的UI对象都可以打印出来,最后一个是ShowDialog方法,其实就是显示一个界面,可以配置一下纸张选择,横向打印还是纵向打印,但是其打印范围页的功能是没有实现的,无论怎么配置,都是全部打印出来,这个稍后会有解决办法。

    至此,可以看出如果我们要随心所欲打印自己的东西那么PrintDialog一个是不够用的,要能够打印自定义的内容我们需要使用到强大的DocumentPaginator。

    3.DocumentPaginator

      DocumentPaginator是一个抽象类,我们继承其看需要重写哪些东西

    class TestDocumentPaginator : DocumentPaginator
        {
            public override DocumentPage GetPage(int pageNumber)
            {
                throw new NotImplementedException();
            }
    
            public override bool IsPageCountValid
            {
                get
                {
                    return true;
                }
            }
    
            public override int PageCount
            {
                get
                {
                    throw new NotImplementedException();
                }
            }
    
            public override Size PageSize
            {
                get;
                set;
            }
    
            public override IDocumentPaginatorSource Source
            {
                get
                {
                    return null;
                }
            }
        }

    注意GetPage方法,这个很重要,这也是分页器的核心所在,我们根据传入的页码返回内容DocumentPage,IsPageCountValid直接设置为True即可,PageCount即总页数,这个需要我们根据需求来分页来计算,PageSize就是纸张的大小,至于Source是用在什么地方还真没研究过,直接返回null。如何实现自定义打印稍后介绍。

    3.PrintServer && PrintQueue

    PrintServer可以获取本地的打印机列表或网络打印机,PrintQueue实际上代表的就是一个打印机,所以我们就能够获取到本地计算机上已经配置的打印机,还能够获取默认打印机哦

            private void LoadPrinterList()
            {
               var printServer = new PrintServer();
    
                //获取全部打印机
                PrinterList = printServer.GetPrintQueues();
    
                //获取默认打印机
                DefaultPrintQueue = LocalPrintServer.GetDefaultPrintQueue();
            }

     

    4.PageMeidaSize && PageMediaSizeName

    PageMediaSize包含了纸张的宽和高以及名称,PageMediaSizeName是一个枚举,把所有纸张的名称都列举出来了,所以我们就能够获取到打印机支持的纸张类型集合了

    var pageSizeCollection = DefaultPrintQueue.GetPrintCapabilities().PageMediaSizeCapability;

    二.自定义打印原理

    我们看一下DocumentPage这个对象,构造函数需要传入一个Visual对象,打印的每一页其实就是打印每一页的Visual,这就好办了,WPF中有一个Visual的派生类DrawingVisual,DrawingVisual好比一个“画板”,我们可以在上面任意作画,有了画板我们还要拥有“画笔”DrawingContext。马上演示如何在画板上作画

    private void DrawSomething()
            {
                var visual = new DrawingVisual();
    
                using (DrawingContext dc = visual.RenderOpen())
                {
                    dc.DrawRectangle(Brushes.Black, new Pen(Brushes.Black, 1), new Rect(0, 0, 100, 100));
                }
            }

    这样我就在左上角绘制了一个宽100高100的矩形,DrawingContext的方法很多

     

    可以看到能够绘制许多基本的东西,如图片,文本,线段等。

    到这儿,大家都该清楚了,自定义打印的原理就是使用DrawingVisual绘制自己的内容,然后交给DocumentPage,让打印机来处理。

    下面演示一下打印5个页面,每个页面左上角显示页码

    TestDocumentPaginator.cs

    class TestDocumentPaginator : DocumentPaginator
        {
    
            #region 字段
             private int _pageCount;
            private Size _pageSize;
            #endregion
    
            #region 构造
            public TestDocumentPaginator()
            {
                //这个数据可以根据你要打印的内容来计算
                _pageCount = 5;
    
                //我们使用A3纸张大小
                var pageMediaSize = LocalPrintServer.GetDefaultPrintQueue()
                                  .GetPrintCapabilities()
                                  .PageMediaSizeCapability
                                  .FirstOrDefault(x => x.PageMediaSizeName == PageMediaSizeName.ISOA3);
    
                if (pageMediaSize != null)
                {
                    _pageSize = new Size((double)pageMediaSize.Width, (double)pageMediaSize.Height);
                }
            }
            #endregion
    
            #region 重写
            /// <summary>
            /// 
            /// </summary>
            /// <param name="pageNumber">打印页是从0开始的</param>
            /// <returns></returns>
            public override DocumentPage GetPage(int pageNumber)
            {
                var visual = new DrawingVisual();
    
                using (DrawingContext dc = visual.RenderOpen())
                {
                    //设置要绘制的文本,文本字体,大小,颜色等
                    FormattedText text = new FormattedText(string.Format("第{0}页", pageNumber + 1), 
                                                         CultureInfo.CurrentCulture, 
                                                         FlowDirection.LeftToRight, 
                                                         new Typeface("宋体"), 
                                                         30, 
                                                         Brushes.Black);
    
                    //文本的左上角位置
                    Point leftpoint = new Point(0, 0);
    
                    dc.DrawText(text, leftpoint);
                }
    
                return new DocumentPage(visual, _pageSize, new Rect(_pageSize), new Rect(_pageSize));
            }
    
            public override bool IsPageCountValid
            {
                get
                {
                    return true;
                }
            }
    
            public override int PageCount
            {
                get
                {
                    return _pageCount;
                }
            }
    
            public override Size PageSize
            {
                get
                {
                    return _pageSize;
                }
                set
                {
                    _pageSize = value;
                }
            }
    
            public override IDocumentPaginatorSource Source
            {
                get
                {
                    return null;
                }
            }
            #endregion
        }

     

    private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
            {
                PrintDialog p = new PrintDialog();
    
                TestDocumentPaginator docPaginator = new TestDocumentPaginator();
    
                p.PrintDocument(docPaginator, "测试");
            }

    注意,这里我使用了MicroSoft的虚拟打印机XPS,然后使用XPS查看器查看

    这样一共5页

    三.打印范围页

    我在使用PrintDialog的时候,尝试过打印范围页,就通过设置PrintDialog的几个参数,但都失败了,网上一搜,遇到此问题的少年还不少,于是网上有许多办法,比较容易搜到的是一个从PrintDialog派生类然后自己处理打印范围页,这个方法想法是好的,但是内部理解起来不容易,一定有更合适的方法,于是各种搜索(Google不能用了,只好用Bing),搜到这么一篇文章How to print a PageRange with WPF’s PrintDialog,文章没有讲得很清晰,其实原理很简单

    对,分页器的分页器…,我们使用第二个分页器,在页码上加上一个基数,取第一个分页器的页面,也不知大家看明白没有,算了,我还是上代码吧

    PageRangeDocumentPaginator.cs

    class PageRangeDocumentPaginator : DocumentPaginator
        {
            private int _startIndex;
            private int _endIndex;
            private DocumentPaginator _paginator;
    
            public PageRangeDocumentPaginator(int startIndex, int endIndex, DocumentPaginator paginator)
            {
                _startIndex = startIndex;
                _endIndex = endIndex;
                _paginator = paginator;
            }
    
            public override DocumentPage GetPage(int pageNumber)
            {
                return _paginator.GetPage(pageNumber + _startIndex);
            }
    
            public override bool IsPageCountValid
            {
                get
                {
                    return _paginator.IsPageCountValid;
                }
            }
    
            public override int PageCount
            {
                get
                {
                    return _endIndex - _startIndex + 1;
                }
            }
    
            public override Size PageSize
            {
                get
                {
                    return _paginator.PageSize;
                }
                set
                {
                    _paginator.PageSize = value;
                }
            }
    
            public override IDocumentPaginatorSource Source
            {
                get
                {
                    return null;
                }
            }
        }

    这个方法实现很简单,也很巧妙。

    四.打印预览

    我们有了分页器,并且能够从分页器中GetPage(int pageNumber),得到某一页的DocumentPage,DocumentPage中包含了我们绘制的Visual,这个时候就可以将Visual拿出来,用一个Canvas在窗口上显示出来,达到一个预览的效果,但Canvas需要特殊处理一下

    DrawingCanvas.cs

    class DrawingCanvas : Canvas
        {
            #region 字段
            private List<Visual> _visuals = new List<Visual>();
            #endregion
    
            #region 公有方法
    
            public void AddVisual(Visual visual)
            {
                _visuals.Add(visual);
    
                base.AddLogicalChild(visual);
                base.AddVisualChild(visual);
            }
    
            public void RemoveVisual(Visual visual)
            {
                _visuals.Remove(visual);
    
                base.RemoveLogicalChild(visual);
                base.RemoveVisualChild(visual);
            }
    
            public void RemoveAll()
            {
                while (_visuals.Count != 0)
                {
                    base.RemoveLogicalChild(_visuals[0]);
                    base.RemoveVisualChild(_visuals[0]);
    
                    _visuals.RemoveAt(0);
                }
            }
    
            #endregion
    
            #region 构造
    
            public DrawingCanvas()
            {
                Width = 200;
                Height = 200;
            }
            #endregion
    
            #region 重写
            protected override int VisualChildrenCount
            {
                get
                {
                    return _visuals.Count;
                }
            }
    
            protected override Visual GetVisualChild(int index)
            {
                return _visuals[index];
            }
            #endregion
        }

    这样就可以直接用Canvas直接Add我们的Visual了

     

    五.异步打印

    为什么会想到使用异步打印呢?当要打印的页面数量非常大的时候,比如400多页,在使用PrintDialog.PrintDocument的时候,会卡住界面很久,这不是我们所希望的。

    其实PrintDialog内部是使用了XpsDocumentWriter的,它有一个WriteAsync方法

    var doc = PrintQueue.CreateXpsDocumentWriter(queue);
    
    doc.WriteAsync(new PageRangeDocumentPaginator(startIndex, endIndex, p));

    但是啊,这么做还是不能完全解决界面卡住的问题,为什么呢?因为我们的分页器使用了DrawingVisual,Visual是DispatcherObject的派生类,那么对它的使用是要占用UI线程资源的。而我们的分页器是在主UI线程中创建的,异步方法其实是另开一个线程去处理,那么这个线程对Visual的访问还是会切换到主线程上,要不就会报错…,好吧,干脆开一个线程,重新创建分页器,创建XpsDocumentWriter,整个一套都在一个单独的线程中执行,于是

    Task.Factory.StartNew(() =>
                        {
                            try
                            {
                                var p = PaginatorFactory.GetDocumentPaginator(_config);
    
                                p.PageSize = new Size(_paginator.PageSize.Width, _paginator.PageSize.Height);
    
                                var server = new LocalPrintServer();
    
                                var queue = server.GetPrintQueue(queueName);
    
                                queue.UserPrintTicket.PageMediaSize = PageSize;
    
                                queue.UserPrintTicket.PageOrientation = PageOrientation;
    
                                var doc = PrintQueue.CreateXpsDocumentWriter(queue);
    
                              }
                            catch (Exception ex)
                            {
    
                            }
                            finally
                            {
                                _dispatcher.BeginInvoke(new Action(() => Close()));
                            }
                        });

    一试,界面完全不卡,因为这个时候已经不关UI线程的事了,需要注意一点就是,已经单独在一个线程中,那么就不需要使用异步打印方法了即WriteAsync,使用Writer即可,大家试一下就知道了。

    六.源码

    项目是一个实现打印预览的功能,目前我已经实现了DataTable的打印预览,BitmapImage和FrameworkElement的打印预览,后两者暂不支持完全异步的打印。

    源码托管在开源中国:https://git.oschina.net/HelloMyWorld/HappyPrint.git,第一次把自己的东西共享出来,希望大家支持和斧正。

    参考资料:《WPF编程宝典》第29章打印

    欢迎转载,转载请注明出处

  • 相关阅读:
    Python-快速入门
    Python-面向对象编程
    python-模块
    .net mvc onexception capture; redirectresult;
    a c lang in linux
    上海哪里有学陈氏太极拳?
    【Origin】 叹文
    【Origin】 碑铭
    【Origin】 偶题 之 抒意
    【Origin】答友朋关切书
  • 原文地址:https://www.cnblogs.com/HelloMyWorld/p/4149969.html
Copyright © 2011-2022 走看看