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

RTF 文档构造库

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (56投票s)

2010 年 7 月 30 日

CPOL

4分钟阅读

viewsIcon

260903

downloadIcon

5207

以编程方式创建富文本格式文档。

介绍/背景

不久前,我需要构建富文本格式报告,但我找不到任何合适的解决方案,于是决定从头开始编写自己的库。

主要想法是创建一个易于扩展的库,以便开发人员可以添加RTF 规范中默认未实现的任何功能。有关详细信息,请参阅本文的第二部分

Using the Code

RtfDocument 类有一个构造函数,它以 RtfCodepage 枚举值为参数。我用 Windows-1251 俄语编码测试了这个库,而且我很有把握它与其他编码也能很好地工作。Unicode 也得到了支持。

RtfDocument rtf = new RtfDocument();

我们继续向指定的表格添加字体和颜色。稍后,我们将使用索引来引用它们(这无疑是最简单但不是最好的实现)。RtfDocument.DefaultFont 定义了未显式设置 FontIndex 的段落所使用的字体的索引。

rtf.FontTable.Add(new RtfFont("Calibri"));
rtf.FontTable.Add(new RtfFont("Constantia"));
rtf.ColorTable.AddRange(new RtfColor[] {
    new RtfColor(Color.Red),
    new RtfColor(0, 0, 255)
});

文档的内容包括段落、格式化段落、表格行和表格。让我们创建一个带有居中 16pt 文本的标题段落。

RtfFormattedParagraph header = 
  new RtfFormattedParagraph(new RtfParagraphFormatting(16, RtfTextAlign.Center));

在标题中添加一些文本、格式化文本和一个空段落

header.AppendText("Calibri");
header.AppendText(new RtfFormattedText(" Bold", RtfCharacterFormatting.Bold));
header.AppendParagraph();

添加另一个具有不同格式的段落。我们将 FontIndex 设置为 1,将 IndentLeft 设置为 6.05cm。大多数缩进和宽度都以缇(twip)为单位设置。TwipConverter 将毫米、厘米和磅转换为缇,反之亦然。

RtfFormattedParagraph p = 
  new RtfFormattedParagraph(new RtfParagraphFormatting(12, RtfTextAlign.Left));
p.Formatting.FontIndex = 1;
p.Formatting.IndentLeft = TwipConverter.ToTwip(6.05F, MetricUnit.Centimeter);
p.AppendText("Constantia");
p.AppendText(new RtfFormattedText("Superscript", RtfCharacterFormatting.Superscript));

这是一个演示内联字体大小更改的示例。字体索引 -1 表示忽略。

p.AppendParagraph(new RtfFormattedText("Inline", -1, 8));
p.AppendText(new RtfFormattedText(" font size ", -1, 14));
p.AppendText(new RtfFormattedText("change", -1, 8));

支持不同输出格式的图片。WordPad 无法读取 JPEG 和 PNG,因此为了兼容性,最好使用 WMF。WMF 转换是通过 P/Invoke 调用完成的,这部分的功劳归于David Bennett

RtfImage picture = new RtfImage(Properties.Resources.lemon, RtfImageFormat.Wmf);
picture.ScaleX = 50;
picture.ScaleY = 50;

p.AppendParagraph(picture);

带有通用格式的超链接

RtfFormattedText linkText = 
  new RtfFormattedText("View article", RtfCharacterFormatting.Underline, 2);
p.AppendParagraph(new RtfHyperlink("RtfConstructor.aspx", linkText));

一个带有 2 列 3 行的居中表格

RtfTable t1 = new RtfTable(RtfTableAlign.Center, 2, 3);

单元格可以水平和垂直合并

t1.MergeCellsVertically(1, 0, 2);

在单元格内使用的格式

RtfParagraphFormatting LeftAligned12 = new RtfParagraphFormatting(12, RtfTextAlign.Left);
RtfParagraphFormatting Centered10 = new RtfParagraphFormatting(10, RtfTextAlign.Center);

表格单元格类继承自格式化段落,并具有一些附加属性。

t1[0, 0].Definition.Style = 
  new RtfTableCellStyle(RtfBorderSetting.None, LeftAligned12, 
                        RtfTableCellVerticalAlign.Bottom);
t1[0, 0].AppendText("Bottom");

t1[1, 0].Definition.Style = 
  new RtfTableCellStyle(RtfBorderSetting.Left, Centered10, 
      RtfTableCellVerticalAlign.Center, 
      RtfTableCellTextFlow.BottomToTopLeftToRight);
t1[1, 1].Definition.Style = t1[1, 0].Definition.Style;
t1[1, 0].AppendText("Vertical");

我们将单元格的 TextColorIndex 设置为 1,并添加具有不同颜色的 RtfFormattedText

t1[0, 1].Formatting = new RtfParagraphFormatting(10, RtfTextAlign.Center);
t1[0, 1].Formatting.TextColorIndex = 1;
t1[0, 1].AppendText(new RtfFormattedText("Black", 0));
t1[0, 1].AppendText(" Red ");
t1[0, 1].AppendText(new RtfFormattedText("Blue", 2));

这部分展示了一个 RtfCharacterFormatting 位运算的示例

t1[0, 2].AppendText("Normal");
t1[1, 2].AppendText(new RtfFormattedText("Italic", 
         RtfCharacterFormatting.Caps | RtfCharacterFormatting.Italic));
t1[1, 2].AppendParagraph("+");
t1[1, 2].AppendParagraph(new RtfFormattedText("Caps", 
         RtfCharacterFormatting.Caps | RtfCharacterFormatting.Italic));

将内容添加到文档

rtf.Contents.AddRange(new IRtfDocumentPart[] {
    header,
    t1,
    p,
});

文档完成后,我们使用 RtfWriter 将其转换为 RTF 代码

RtfWriter rtfWriter = new RtfWriter();
TextWriter writer = new StreamWriter("test.rtf");
rtfWriter.Write(writer, rtf);

瞧,这就是 Microsoft Word 中看到的最终文件

Screenshot

这就是 RTF 代码

{\rtf1\ansi\ansicpg1252\deffont0\deflang1033
{\fonttbl {\f0\fnil\fcharset1\fprq0 Calibri;}{\f1\fnil\fcharset1\fprq0 Constantia;}}
{\colortbl ;\red255\green0\blue0;\red0\green0\blue255;}
\pard\plain\qc\fi0\li0\ri0\sl0\sb0\sa240\fs32 Calibri 
    {\b Bold}\par\trowd\trrh1134\trqc\clvertalb\cltxlrtb\cellx1701\clvmgf
\clvertalc\clbrdrl\brdrw10\brdrs\cltxbtlr\cellx2835\pard\
    intbl\plain\ql\fi0\li0\ri0\sl0\sb0\sa0\fs24 Bottom\cell\pard\intbl
\plain\qc\fi0\li0\ri0\sl0\sb0\sa0\fs20 Vertical\cell\row\
    trowd\trrh1134\trqc\clvertalc\cltxlrtb\cellx1701\clvmrg\clvertalc
\clbrdrl\brdrw10\brdrs\cltxbtlr\cellx2835\pard\intbl\plain\
    qc\fi0\li0\ri0\sl0\sb0\sa0\cf1\fs20{\cf0 Black } Red {\cf2 Blue}\cell
\pard\intbl\plain\qc\fi0\li0\ri0\sl0\sb0\sa0\fs20\cell\row\
    trowd\trrh1134\trqc\clvertalc\cltxlrtb\cellx1701\clvertalc\cltxlrtb
\cellx2835\pard\intbl\plain\qc\fi0\li0\ri0\sl0\sb0\sa0\fs20 Normal\
   cell\pard\intbl\plain\qc\fi0\li0\ri0\sl0\sb0\sa0\fs20{\i\caps I
talic}\par +\par{\i\caps Caps}\cell\row\pard\plain\ql\fi0\li3430\
   ri0\sl0\sb120\sa0\f1\fs24 Constantia {\super Superscript}\par{
\fs16 Inline}{\fs28  font size }{\fs16 change}\par{\pict\picscalex50\
   picscaley50\picw7938\pich11509\picwgoal4500\pichgoal6525
\wmetafile8 

...

}
\par{\field{\fldinst HYPERLINK "https://codeproject.org.cn/KB/
       cs/RtfConstructor.aspx"}{\fldrslt{\cf2\cb1\ul View article}}}}

上面的代码已进行包装以防止滚动。

反射部分

RtfDocument 到 RTF 代码的转换是使用反射完成的。

代表控制字的每个类都有一个特定的属性,RtfWriter 会识别该属性。这就是为什么可以轻松扩展库并添加自己的类来支持更多控制字的原因。这在某种程度上类似于为序列化目的向类和成员添加 System.Xml.Serialization 属性。只是您需要研究 RTF 规范。

毫不奇怪,最常用的属性是 RtfControlWord。如果其 RtfControlWord.Name 属性未设置,RtfWriter 将使用成员名作为控制字。

[RtfControlWord]
public int Red { get; set; }

int 成员的值会附加到控制字。RtfWriter 会忽略标记有 RtfIndex 属性且值为 -1 的成员。

[RtfControlWord("cf"), RtfIndex]
public int TextColorIndex { get; set; }

枚举是特殊情况,它们需要 RtfEnumAsControlWord 属性,因为它们可以以不同的方式处理。

[RtfEnumAsControlWord(RtfEnumConversion.UseAttribute)]
public enum RtfTableAlign
{
    [RtfControlWord("trql")]
    Left,
    [RtfControlWord("trqc")]
    Center,
    [RtfControlWord("trqr")]
    Right
}

[RtfEnumAsControlWord(RtfEnumConversion.UseValue, Prefix = "fprq")]
public enum RtfFontPitch
{
    Default,     // as /fprq0
    Fixed,       // as /fprq1
    Variable     // as /fprq2
}

[RtfEnumAsControlWord(RtfEnumConversion.UseName)]
public enum RtfDocumentCharacterSet
{
    ANSI,        // as /ansi
    Mac,         // as /mac
    PC,          // as /pc
    PCa          // as /pca
}

一些未标记 RtfControlWord 的成员必须包含,RtfInclude 会告知 RtfWriter 执行此操作。

public class RtfTable
{
    [RtfInclude]
    public RtfTableRowCollection Rows
    {
        get { return _rows; }
    }
    //...
}

某些成员仅在满足某些条件时才应包含,这就是 RtfInclude.ConditionMember 属性发挥作用的地方。

[RtfControlWord("clbrdrt"), RtfInclude(ConditionMember = "IsTopBorderSet")]
public RtfBorder Top
{
    get { return top; }
}

public bool IsTopBorderSet
{
    get { return Top.Width > 0; }
}

标记有 RtfControlWordbool 成员仅在其值为 true 时才包含。

[RtfControlWord("b")]
public bool Bold = false;

有些控制字是成对的,例如用于开始表格行的 /trowd,以及用于结束表格行的 /rowRtfControlWordDenotingEnd 属性用于定义第二个。

[RtfControlWord("pard"), RtfControlWordDenotingEnd("cell")]
public class RtfTableCell {
    //...
}

某些成员必须被忽略,RtfIgnore 用于此目的。

public class RtfTableCell
{
    [RtfIgnore]
    public RtfTable Table
    {
        get { return RowInternal.Table; }
    }
    
    //...
}

RtfEnclosingBraces 属性及其 RtfEnclosingBraces.ClosingSemicolon 属性不言自明。

[RtfControlWord("rtf1"), RtfEnclosingBraces]
public class RtfDocument {
    //...
}

[RtfControlWord("f", IsIndexed = true), 
 RtfEnclosingBraces(ClosingSemicolon = true)]
public class RtfFont {
    //...
}

还有两个属性未被提及:RtfTextData 用于标记文本,RtfControlGroup 是出于某种原因添加的,我已经不记得了。只有字体和颜色表会用它标记。

关注点

编写带有大量内容的 RtfDocument 需要相当长的时间,但只需一次,因为类型属性和成员的信息存储在特定的类中。请查看 RtfDocumentInfoRtfTypeInfoRtfAttributeInfo 类。

历史

  • 16.08.10:代码已修订,增加了对制表符的支持(文章中未显示)。
  • 07.08.10:增加了对图像、超链接、内联颜色和字体格式的支持。
  • 05.08.10:添加了反射部分。
  • 02.08.10:文本已修订,添加了 RTF 代码。
  • 30.07.10:初始发布。
© . All rights reserved.