65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 WPF 进行邮件合并打印

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (5投票s)

2011 年 2 月 18 日

CPOL

4分钟阅读

viewsIcon

38237

downloadIcon

1537

本文探讨了如何使用 WPF 的文档查看和打印类来检查、缩放和输出由 XAML 模板生成的邮件合并信函。

引言

邮件合并涉及将数据库中的记录与文档模板合并,以生成一组相似的文档,其中文档的正文是固定的,但每个单独记录的细节会发生变化。本文探讨了如何使用 WPF 的文档查看和打印类来检查、缩放和输出由 XAML 模板生成的邮件合并信函。

演示应用程序

在演示中,模板采用的是写给自然保护区客户的信函形式。有一个设施可以预览信函,以检查其格式是否正确,并能够分批打印,这样在打印机出现问题时,整个打印作业就不会丢失。所需的所有功能都由 WPF 的文档查看和打印类提供;演示代码主要关注这些类的接口。

邮件合并信函模板

XAML 凭借其丰富的布局项,非常适合生成模板。演示使用了一个带有 DockPanelUserControl。信函的页眉和页脚 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;

查看文档

.jpg

查看 Document 仅需使用 DocumentViewer 控件,并将其 Document 属性绑定到邮件合并 Document。可以打印信函,但要控制信函的打印方式,需要获取 DocumentViewerOnPrint 命令的控制权。有两种主要方法可以做到这一点。一种是替换控件的模板,另一种是继承 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 的次数由 PaginatorPageCount 属性决定。可以通过将 PageCount 设置为适当的值,并将 pageNumber 与从 PrintDialog 控件的页面范围选择中获得的 StartIndex 相加来打印页面范围。但此技术存在潜在问题。一旦 Visual 成为 DocumentPages 集合的一部分,它就会与集合牢固地绑定在一起,并尝试将其关联到另一个元素可能会抛出异常,类似于“Specified element is already the logical child of another element. Disconnect it first”(指定的元素已经是另一个元素的逻辑子元素。请先断开其连接)——但您无法断开它,因为 Parent 属性是只读的。此异常有点特殊。它的出现似乎取决于操作系统和打印驱动程序。

幸运的是,还有一种替代的打印方法,它不使用 DocumentPaginator,而是创建一个 VisualsToXpsDocument,将 MailMerge Visuald 直接以批量模式写入 PrintQueue。这可以从 DocumentViewerOnPrint 方法调用,并且最好与页面尺寸预定义且 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 的常客们。他们教会了我大部分知识。本文中的任何不足之处都完全是由于我自己的疏忽,与他们无关。

© . All rights reserved.