WPF打印原理,自定義打印

一.基礎知識html

1.System.Printing命名空間

咱們能夠先看一下System.Printing命名空間,東西其實不少,功能也很是強大,能夠說可以控制打印的每個細節,曾經對PrintDialog失望的我看到了一絲曙光。

 

2.PrintDialog

能夠看到PrintDialog除了構造函數有三個方法和一堆屬性,PrintDocument接受一個分頁器(DocumentPaginator,稍後介紹),PrintVisual能夠打印Visual,也就是WPF中的大部分繼承自Visual類的UI對象均可以打印出來,最後一個是ShowDialog方法,其實就是顯示一個界面,能夠配置一下紙張選擇,橫向打印仍是縱向打印,可是其打印範圍頁的功能是沒有實現的,不管怎麼配置,都是所有打印出來,這個稍後會有解決辦法。git

至此,能夠看出若是咱們要爲所欲爲打印本身的東西那麼PrintDialog一個是不夠用的,要可以打印自定義的內容咱們須要使用到強大的DocumentPaginator。編程

 

3.DocumentPaginator

  DocumentPaginator是一個抽象類,咱們繼承其看須要重寫哪些東西windows

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實際上表明的就是一個打印機,因此咱們就可以獲取到本地計算機上已經配置的打印機,還可以獲取默認打印機哦app

        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。立刻演示如何在畫板上做畫ide

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章打印

歡迎轉載,轉載請註明出處

相關文章
相關標籤/搜索