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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM