一.基礎知識html
能夠看到PrintDialog除了構造函數有三個方法和一堆屬性,PrintDocument接受一個分頁器(DocumentPaginator,稍後介紹),PrintVisual能夠打印Visual,也就是WPF中的大部分繼承自Visual類的UI對象均可以打印出來,最後一個是ShowDialog方法,其實就是顯示一個界面,能夠配置一下紙張選擇,橫向打印仍是縱向打印,可是其打印範圍頁的功能是沒有實現的,不管怎麼配置,都是所有打印出來,這個稍後會有解決辦法。git
至此,能夠看出若是咱們要爲所欲爲打印本身的東西那麼PrintDialog一個是不夠用的,要可以打印自定義的內容咱們須要使用到強大的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。如何實現自定義打印稍後介紹。網絡
PrintServer能夠獲取本地的打印機列表或網絡打印機,PrintQueue實際上表明的就是一個打印機,因此咱們就可以獲取到本地計算機上已經配置的打印機,還可以獲取默認打印機哦app
private void LoadPrinterList() { var printServer = new PrintServer(); //獲取所有打印機 PrinterList = printServer.GetPrintQueues(); //獲取默認打印機 DefaultPrintQueue = LocalPrintServer.GetDefaultPrintQueue(); }
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 }
爲何會想到使用異步打印呢?當要打印的頁面數量很是大的時候,好比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章打印
歡迎轉載,轉載請註明出處