使用 WPF 进行邮件合并打印






4.83/5 (5投票s)
本文探讨了如何使用 WPF 的文档查看和打印类来检查、缩放和输出由 XAML 模板生成的邮件合并信函。
引言
邮件合并涉及将数据库中的记录与文档模板合并,以生成一组相似的文档,其中文档的正文是固定的,但每个单独记录的细节会发生变化。本文探讨了如何使用 WPF 的文档查看和打印类来检查、缩放和输出由 XAML 模板生成的邮件合并信函。
演示应用程序
在演示中,模板采用的是写给自然保护区客户的信函形式。有一个设施可以预览信函,以检查其格式是否正确,并能够分批打印,这样在打印机出现问题时,整个打印作业就不会丢失。所需的所有功能都由 WPF 的文档查看和打印类提供;演示代码主要关注这些类的接口。
邮件合并信函模板
XAML 凭借其丰富的布局项,非常适合生成模板。演示使用了一个带有 DockPanel
的 UserControl
。信函的页眉和页脚 TextBlock
项分别停靠在顶部和底部,而信函正文则由堆叠在 StackPanel
中的更多 TextBlock
组成。数据绑定将相关的 TextBlock
绑定到 ViewModel 中的属性。ViewModel 又由数据集中的单个记录填充。
<UserControl x:Class="printing.MailMergeLetter"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
Height="1104" Width="792">
<UserControl.Resources>
<Style TargetType="TextBlock" x:Key="BodyText">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="FontFamily" Value="Times New Roman"/>
<Setter Property="FontSize" Value="15"/>
<Setter Property="TextWrapping" Value="Wrap"/>
</Style>
</UserControl.Resources>
<DockPanel>
<TextBlock
DockPanel.Dock="Top"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Height="30"
Margin="0,48,0,48"
FontFamily="Times New Roman" FontSize="32" >
<Bold>Hawk's Nature Reserve</Bold>
</TextBlock>
<TextBlock
Style="{StaticResource BodyText}"
DockPanel.Dock="Bottom"
HorizontalAlignment="Center"
Margin="0,0,0,60"
Name="footer"
>
<Bold>23 Park Meadows Pembroke PB44 7BN Tel 016583946</Bold>
</TextBlock>
<TextBlock
Style="{StaticResource BodyText}"
DockPanel.Dock="Bottom"
Margin="96,0,0,192"
Text="A. Sparrow-Hawk"
/>
<StackPanel>
<TextBlock
Style="{StaticResource BodyText}"
FontWeight="Bold"
Margin="96,20,96,45"
Text="{Binding Path=Address}"
/>
<TextBlock
Style="{StaticResource BodyText}"
Margin="96,25,96,45"
Text="{Binding Path=Date}"
/>
<TextBlock
Style="{StaticResource BodyText}"
Margin="96,10,96,0"
Text="{Binding Path=Salutation}"
/>
<TextBlock
Style="{StaticResource BodyText}"
Margin="96,10,96,0"
Text="{Binding Path=Body}"
/>
<TextBlock
Style="{StaticResource BodyText}"
Margin="96,10,96,0"
Name="validation"
Text="Your feathered friend,"
/>
</StackPanel>
</DockPanel>
</UserControl>
构造文档
在 WPF 中查看和打印各种类型的文档都围绕着 Document
类。通过将邮件合并信函添加到 Document
类型的对象中,其提供的所有功能都可用于邮件合并。在演示中,邮件合并信函被添加到 FixedDocument
对象中,并构成 Document
的各个页面。使用 FixedDocument
是因为它旨在容纳 Visual
(UI) 元素。另一种主要的文档类型 FlowDocument
并不太合适,因为它更倾向于渲染存储在 XML 格式中的文本块。
将邮件合并信函添加到 Document
中非常简单。
FixedDocument fixedDocument = new FixedDocument();
MailMergeLetter mailMergeLetter;
for (int i = 0; i < data.Length; i++)
{
mailMergeLetter = documentBuilder.BuildPage(data, i);
PageContent pageContent = new PageContent();
FixedPage fixedPage = documentBuilder.CreatePage();
fixedPage.Children.Add(mailMergeLetter);
((IAddChild)pageContent).AddChild(fixedPage);
fixedDocument.Pages.Add(pageContent);
}
this.Document = fixedDocument;
查看文档
查看 Document
仅需使用 DocumentViewer
控件,并将其 Document
属性绑定到邮件合并 Document
。可以打印信函,但要控制信函的打印方式,需要获取 DocumentViewer
的 OnPrint
命令的控制权。有两种主要方法可以做到这一点。一种是替换控件的模板,另一种是继承 DocumentViewer
并重写其 OnPrint
方法。演示中采用了后一种方法。
protected override void OnPrintCommand()
{
PrintDialog printDialog = new PrintDialog();
// set up initial values for printDialog
printDialog.UserPageRangeEnabled = true;
printDialog.PrintTicket = printDialog.PrintQueue.UserPrintTicket;
printDialog.PrintTicket.PageOrientation = PageOrientation.Portrait;
printDialog.PrintQueue = LocalPrintServer.GetDefaultPrintQueue();
if (printDialog.ShowDialog() == true)
{
this.PerformPrintRun(printDialog);
}
}
PrintDialog
控件允许用户配置对话框的 PrintTicket
类。PrintTicket
包含有关打印如何执行的信息。PrintTicket
可用的选项取决于所选打印机的性能。无需查询打印机以确定其属性,因为 PrintDialog
会很方便地完成这项工作。
打印邮件合并
FixedDocument
具有一个 DocumentPaginator
对象。DocumentPaginator
的任务之一是将 Document
的各个页面呈现给打印机进行打印。这是通过 paginator 的 GetPage
方法完成的。但是,目前 GetPage
方法无法打印选定范围的页面。因此,要使用 DocumentPaginator
打印页面范围,需要继承 DocumentPaginator
并重写 GetPage
方法。
public override DocumentPage GetPage(int pageNumber)
{
PageContent pageContent =
this.fixedDocument.Pages[pageNumber + this.startIndex];
return new DocumentPage(pageContent, this.paperSize,
new Rect(this.paperSize), new Rect(this.paperSize));
}
调用 GetPage
的次数由 Paginator
的 PageCount
属性决定。可以通过将 PageCount
设置为适当的值,并将 pageNumber
与从 PrintDialog
控件的页面范围选择中获得的 StartIndex
相加来打印页面范围。但此技术存在潜在问题。一旦 Visual
成为 Document
的 Pages
集合的一部分,它就会与集合牢固地绑定在一起,并尝试将其关联到另一个元素可能会抛出异常,类似于“Specified element is already the logical child of another element. Disconnect it first”(指定的元素已经是另一个元素的逻辑子元素。请先断开其连接)——但您无法断开它,因为 Parent
属性是只读的。此异常有点特殊。它的出现似乎取决于操作系统和打印驱动程序。
幸运的是,还有一种替代的打印方法,它不使用 DocumentPaginator
,而是创建一个 VisualsToXpsDocument
,将 MailMerge
Visual
d 直接以批量模式写入 PrintQueue
。这可以从 DocumentViewer
的 OnPrint
方法调用,并且最好与页面尺寸预定义且 Paginator
功能有限的 FixedDocument
一起使用。
XpsDocumentWriter writer =
PrintQueue.CreateXpsDocumentWriter(printDialog.PrintQueue);
VisualsToXpsDocument visualsToXpsDoc =
(VisualsToXpsDocument)writer.CreateVisualsCollator();
Size scaling = this.GetScalingFactor();
Size paperSize = this.GetPaperSize();
visualsToXpsDoc.BeginBatchWrite();
for (int i = startIndex; i <= endIndex; i++)
{
PageContent pageContent = this.fixedDocument.Pages[i];
pageContent.Child.LayoutTransform =
new ScaleTransform(scaling.Width, scaling.Height);
pageContent.Child.Measure(paperSize);
pageContent.Child.Arrange(new Rect(paperSize));
visualsToXpsDoc.Write(pageContent);
}
visualsToXpsDoc.EndBatchWrite();
PrintDialog
控件通常允许用户选择纸张尺寸,这意味着可能需要缩放 MailMerge
以适应纸张。为了适应缩放,将 pageContent.Child.LayoutTransform
设置为适当的缩放因子。然后,通过调用 Measure
来确定为每个元素请求的空间量,最后,通过调用 Arrange
方法来更新布局。
结论
这里描述的方法并不是打印文档的最有效方式。但它们确实允许缩放到不同的纸张尺寸并进行打印范围选择。
致谢
我要感谢 CodeProject 的常客们。他们教会了我大部分知识。本文中的任何不足之处都完全是由于我自己的疏忽,与他们无关。