一.基础知识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章打印
欢迎转载,转载请注明出处