一.基礎知識
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章打印
歡迎轉載,轉載請注明出處