本文基本借鉴了用WPF实现打印及打印预览

WPF的文档使用基本使用文档模版Flow Document,但是单独的Flow Document(流文档)是没法预览的,你必须把它放在一个容器中才可以,流文档的容器有FlowDocumentScrollViewer,FlowDocumentPageViewer,FlowDocumentReader,另外还有DocumentViewer,这个只支持固定流文档(只读)。

本次博客就决定界面使用DocumentViewer,DocumentViewer有很好的分页功能,我们只需要生成固定文档(XPS),然后交给它,它就能很好的将内容预览出来。

<Table FontSize="16">
    <TableRowGroup>
        <TableRow>
            <TableCell>
                <Paragraph>
                    班级
                </Paragraph>
            </TableCell>
            <TableCell>
                <Paragraph>
                    <Run Text="{Binding ClassName}"></Run>
                </Paragraph>
            </TableCell>
        </TableRow>
    </TableRowGroup>
</Table>

但最大的问题来了:流文档的Table的行数是固定的,流文档上的对象是静态的,所以我们只能用后台代码来手工改变它了,这是相当不方便的地方……我定义了这么一个接口来做这种工作。

public interface IDocumentRenderer
{
    void Render(FlowDocument doc, Object data);
}

就比如我下面需要获取班级所有学生各门成绩

<Table>
    <Table.Columns>
        <TableColumn Width="200"/>
        <TableColumn Width="150"/>
        <TableColumn Width="150"/>
        <TableColumn Width="150"/>
    </Table.Columns>

    <TableRowGroup Name="Student" Background="LightGray" FontWeight="Bold">
        <TableRow>
            <TableCell style="{StaticResource CellStyle}"><Paragraph>Name</Paragraph></TableCell>
            <TableCell style="{StaticResource CellStyle}"><Paragraph>ID</Paragraph></TableCell>
            <TableCell style="{StaticResource CellStyle}"><Paragraph>Chinese</Paragraph></TableCell>
            <TableCell style="{StaticResource CellStyle}"><Paragraph>Math</Paragraph></TableCell>
        </TableRow>
    </TableRowGroup>
</Table>

定义一个继承IDocumentRenderer接口的类,实现Render方法

public class ClassDocumentRender  : IDocumentRenderer
{
	public void Render(FlowDocument doc, Object data)
	{
		TableRowGroup group = doc.FindName("Student") as TableRowGroup ;
		Style cellStyle = doc.Resource("CellStyle") as Style;
		foreach(Student student in ((ClassData)data).StudentList)
		{
			TableRow row = new TableRow();
			TableCell cell = new TableCell(new Paragraph(new Run(student.Name)));
			cell.Style = cellStyle;
			row.Cells.Add(cell);
			
			cell = new TableCell(new Paragraph(new Run(student.ID.ToString())));
			cell.Style = cellStyle;
			row.Cells.Add(cell);
			
			cell = new TableCell(new Paragraph(new Run(student.Chinese.ToString())));
			cell.Style = cellStyle;
			row.Cells.Add(cell);
			
			cell = new TableCell(new Paragraph(new Run(student.Math.ToString())));
			cell.Style = cellStyle;
			row.Cells.Add(cell);
			
			group.Rows.Add(row);
		}
	}
}

先定义一个能接受流文档与IDocumentRenderer接口的方法。这里的DataContext必须要有,因为流文档中的数据都是以binding形式写的,UI只是用来显示数据的,而不是存储数据的。

public FlowDocument LoadDocumentAndRender(string strTmplName, Object data, IDocumentRenderer renderer = null)
{
    FlowDocument doc = (FlowDocument)Application.LoadComponent(new Uri(strTmplName, UriKind.RelativeOrAbsolute));
    doc.PagePadding = new Thickness(50);
    doc.DataContext = data;
    if (renderer != null)
    {
        renderer.Render(doc, data);
    }
    return doc;
}

定义一个加载流文档的方法

public void LoadXps(FlowDocument flowDoc)
{
    //构造一个基于内存的xps document
    MemoryStream ms = new MemoryStream();
    Package package = Package.Open(ms, FileMode.Create, FileAccess.ReadWrite);
    Uri DocumentUri = new Uri("pack://InMemoryDocument.xps");
    PackageStore.RemovePackage(DocumentUri);
    PackageStore.AddPackage(DocumentUri, package);
    XpsDocument xpsDocument = new XpsDocument(package, CompressionOption.Fast, DocumentUri.AbsoluteUri);

    //将flow document写入基于内存的xps document中去
    XpsDocumentWriter writer = XpsDocument.CreateXpsDocumentWriter(xpsDocument);
    writer.Write(((IDocumentPaginatorSource)flowDoc).DocumentPaginator);

    //获取这个基于内存的xps document的fixed document
    docViewer.Document = xpsDocument.GetFixedDocumentSequence();

    //关闭基于内存的xps document
    xpsDocument.Close();
}

这是打印预览代码,然后你可以从界面上DocumentView上的打印按钮来打印

async Task ShowMyClass(MyClass myClass)
{
    ClassReportData crd = await GetClassReportData(myClass);
    FlowDocument flowDoc = LoadDocumentAndRender("../Print/ClassReportDocument.xaml", crd, new ClassReportDocumentRenderer());
    Dispatcher.BeginInvoke(new LoadXpsMethod(LoadXps), DispatcherPriority.ApplicationIdle, flowDoc);
}

private async Task<ClassReportData > GetClassReportData(MyClass myClass)
{
    ClassReportData erd = new ClassReportData ();
    await TaskEx.Delay(1000);
    return erd;
}

这是多选班级打印,直接跳到微软的打印界面,我当时其实想的是弄个队列,然后一个一个打印,后来检查了PrintDialog类之后发现不用了,它内部实现其实就有PrintQueue,本身内部就有个队列来维护先后顺序的,那么我们直接调用PrintDocument方法就可以了

private async void printBtn_Click(object sender, RoutedEventArgs e)
{
    PrintDialog pDialog = new PrintDialog();
    pDialog.PageRangeSelection = PageRangeSelection.AllPages;
    
    // Display the dialog. This returns true if the user presses the Print button.
    bool? print = pDialog.ShowDialog();
    if (print == true)
    {
       List<MyClass> result = (from a in ViewModel.ClassList
                      where a.IsSelected
                      select a).ToList();
        foreach (var result in result)
        {
            ClassReportData crd = await GetClassReportData(result);
            FlowDocument flowDoc = LoadDocumentAndRender("../Print/ClassReportDocument.xaml", crd, new ClassReportDocumentRenderer());
            Dispatcher.BeginInvoke(new Action(() =>
            {
                pDialog.PrintDocument(((IDocumentPaginatorSource)flowDoc).DocumentPaginator, "打印报表");
            }), DispatcherPriority.ApplicationIdle);
        }
    }
}

注意:上文必须使用BeginInvoke而且参数必须为DispatcherPriority.ApplicationIdle,否则binding的地方都是空白的,因为给Document的DataContext赋值的时候,Document的内容并不是马上改变的。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐