文档模板处理器






4.14/5 (7投票s)
本文将介绍如何创建 RTF 模板文件,然后解析该文件并用运行时数据填充它。
引言
与一位潜在客户交谈时,我发现他们想要实现的功能之一是文档处理。基本想法是创建一个模板,并以重复的方式用数据填充它。我之前的一位客户使用了一个第三方工具,这个工具的花费可不菲。我没有那么多钱,所以我想看看 .NET 能做什么。
我发现了 FlowDocument
类,它包含了很多功能。本文将非常基础地介绍我发现的内容以及如何使用它来满足基于模板的文档需求。
本文的目的是让普通用户能够使用熟悉的应用程序(如 MS-Word)创建文档,添加一些特殊标记,然后将其导入 .NET 应用程序作为生成自定义文档的模板。
背景
本文使用了 FlowDocumentPageViewer
、FlowDocument
和支持类,它们是 WPF 库 PresentationFramework.dll 的一部分。
代码
Zip 文件中包含两个项目
- DocumentReader - 此项目将探索 Rich Text Files (RTF) 和 XAML 的加载和保存。我们将了解通过首先创建 Rich Text File 来创建 XPS 文档的简单方法。
- CrazyStory - 此项目建立在 DocumentReader 的基础上。它演示了如何查询文档中的标记进行替换,并提示用户输入。
项目信息
DocumentReader
XAML 文件包含一个 FlowDocument
的内容持有者:<FlowDocumentPageViewer Name="docViewer"/>
,代码隐藏文件包含两个方法:Load()
和 Save()
。这两个方法演示了如何加载 XAML 和 RTF 文档。
Load
方法的核心代码如下:
using (FileStream fs = File.Open(documentFile, FileMode.Open))
{
switch (extension)
{
case ".xaml":
flowDocument = XamlReader.Load(fs) as FlowDocument;
break;
case ".rtf":
flowDocument = new FlowDocument();
TextRange range = new TextRange(
flowDocument.ContentStart,
flowDocument.ContentEnd);
range.Load(fs, DataFormats.Rtf);
break;
}
docViewer.Document = flowDocument;
}
根据文档类型,有两种不同的方法可以加载文档。加载 RTF 文件需要使用 TextRange.Load()
方法,但对于 XAML,您也可以使用 XamlReader.Load()
方法。需要注意的是,如果您使用 TextRange.Save()
保存,那么您需要使用 TextRange.Load()
。对于 XamlWriter.Save()
和 XamlReader.Load()
也是如此。混合这两种不同的方法会导致问题。
using (FileStream fileStream = File.Open(documentFile, FileMode.Create))
{
switch (extension)
{
case ".xaml":
XamlWriter.Save(flowDocument, fileStream);
break;
case ".rtf":
TextRange range = new TextRange(
flowDocument.ContentStart,
flowDocument.ContentEnd);
range.Save(fileStream, DataFormats.Rtf);
break;
}
}
使用 XamlWriter.Save()
方法的好处是可以保存图形的信息,但不能保存实际的图形。图形将保留在文档中,直到应用程序运行结束。如果您关闭应用程序并重新打开 XAML 文档,您将看不到图形。
为了在加载 XAML 时显示图形,您需要加载图形。以下代码将展示如何做到这一点。打开 XAML,找到您想添加图形的段落,然后将 <floater>
标记放在其中。
<Paragraph>
<Floater Width="100" Padding="5,0,5,0" HorizontalAlignment="Right">
<BlockUIContainer>
<Image Source="c:\temp\julius-caesar.jpg" />
</BlockUIContainer>
</Floater>
</Paragraph>
我没有深入研究图形,因为它超出了我最初的意图,但我唯一能让图形显示的方法是使用完全限定路径。
Crazy Story
private void ParseDocument())
{
SortedList<string, >> markers = new SortedList<string, >>();
gridWords.Children.Clear();
foreach (Block block in _originalDocument.Blocks)
{
Paragraph paragraph = block as Paragraph;
if (paragraph == null)
continue;
foreach (Inline inline in paragraph.Inlines)
{
Span span = inline as Span;
if (span == null)
continue;
// This is where the text would be found to replace
Run run = span.Inlines.FirstInline as Run;
if (run == null)
continue;
FindMarker(markers, run);
}
}
}
在 Run
节点内,可以包含很多内容,其中一个可能是标记。如果我们第一次找到标记,就创建一个 Run
节点列表,将其分配并附加到一个 TextBox
以供以后使用。后续找到的标记只需添加到列表中。
private void FindMarker(SortedList<string,>> markers, Run run)
{
Regex regex = new Regex(@"(\<\#(?<name>[^#]+)\#\>)");
MatchCollection matches = regex.Matches(run.Text);
foreach (Match match in matches)
{
string text = match.Groups["Name"].Value.Trim();
if (markers.Keys.Contains(text))
{
markers[text].Add(run);
continue;
}
// Create list and assign the Run
List<run> runList = new List<run> {run};
markers.Add(text, runList);
BuildTextBox(runList, text);
}
}
private void BuildTextBox(List<run> runList, string text)
{
// Add a Row in the Grid for this Prompt
RowDefinition row = new RowDefinition();
gridWords.RowDefinitions.Add(row);
// Create Prompt Label
Label lbl = new Label();
lbl.Content = text + ":";
// Add Prompt Label to the Grid
Grid.SetColumn(lbl, 0);
Grid.SetRow(lbl, gridWords.RowDefinitions.Count - 1);
gridWords.Children.Add(lbl);
// Add the TextBox to enter value
TextBox txt = new TextBox();
Grid.SetColumn(txt, 1);
Grid.SetRow(txt, gridWords.RowDefinitions.Count - 1);
gridWords.Children.Add(txt);
// Attach the List Items to replace with the value in the TextBox
txt.Tag = runList;
}
用户填写完值并按下“生成”按钮后,以下代码将运行。
代码将查找网格中位于第一列的所有控件,并获取动态创建的 TextBox
。Run
节点的列表从标记中检索出来,然后进行迭代,并替换文本的值。
private void cmdGenerate_Click(object sender, RoutedEventArgs e)
{
// Go through each UI Element in the Grid
foreach (UIElement child in gridWords.Children)
{
// Get the TextBox out of Column 1
if (Grid.GetColumn(child) != 1)
continue;
TextBox txt = child as TextBox;
if (txt == null || txt.Text == "")
continue;
// Get the list of Items to Replace
List<run> runList = txt.Tag as List<run>;
if (runList == null)
continue;
// Replace the text
foreach (Run run in runList)
run.Text = txt.Text;
}
}
文档在打开和解析后可以重复使用。您只需提供新值并再次单击“生成”按钮即可。
打印
我想简要提一下打印。我添加了一个打印菜单项,可以打印文档。如果您单击它并打印文档,您会注意到文档显示为两列。如果您不希望这样,这会非常恼人。这与 RTF 文档的列数无关。它就是这样。为了解决这个问题,您可以将 FlowDocument
的 ColumnWidth
设置为一个更大的值,比如 600。这样文档就会以单列打印。
结论
随着我完成了我想要做的事情,我将本文结束。希望本文能为您提供一个起点,以实现更丰富的文档处理应用程序。