在 Word 里把“首页不同”勾上只需要一秒,但要是让你用 C# 程序生成一份几十页的合同文档,要求第一页页眉只放“公司内部资料”,从第二页开始显示完整文档标题,页脚还要“第 X 页 / 共 Y 页”,你大概率会卡在“怎么用代码改页眉页脚”这一步。手动操作顺手的东西,一旦变成自动化批量生成,就完全不是那么回事了。
这篇文章就是围绕这个需求来写的。核心阵地是 Spire.Doc for .NET,用它解决两个问题:一是单个章节内“首页页眉页脚与后续页不同”,二是多章节文档从封面到正文的页眉页脚接力与独立页码。适合做办公自动化、报表生成、投标文件或内部系统导出的开发者阅读,尤其是那些不想碰 COM 组件、想把 Word 生成逻辑完全握在自己手里的人。
1. 首页不同到底在“不同”什么:先把文档模型拆开
很多人第一次写代码生成页眉页脚时,习惯直接往某个 HeaderFooter 对象里塞文字,但打开 Word 发现要么首页没生效,要么首页出现了不该出现的页眉。原因很简单:没有理解 Word 的页眉页脚模型是“按节存储,按类型匹配”的。
Word 的一个节(Section)里,页眉页脚并不是只有一个。它默认会维护三套“页面区域”:
- Default:普通页用的页眉/页脚。
- FirstPage:首页用的页眉/页脚。
- EvenPages:偶数页用的页眉/页脚,只有在开启“奇偶页不同”时才会启用。
Spire.Doc 的PageSetup上正好有两个开关管这件事:DifferentFirstPageHeaderFooter和OddAndEvenPagesHeaderFooter。这两个开关一旦打开,Word 就会按“首页优先、偶页优先、其他默认”的规则去选择到底显示哪一套页眉页脚。
我做了个对照表,方便你把它和 Word 界面里的菜单对应起来。
| 需求 | Word 操作 | Spire.Doc 属性 |
|---|---|---|
| 首页页眉页脚和后续页不同 | 页眉和页脚选项卡里勾选“首页不同” | section.PageSetup.DifferentFirstPageHeaderFooter = true; |
| 奇数页和偶数页使用不同页眉页脚 | 勾选“奇偶页不同” | section.PageSetup.OddAndEvenPagesHeaderFooter = true; |
| 当前节不再沿用上一节的页眉页脚 | 点击“链接到前一节”取消高亮 | 对应 HeaderFooter 对象的IsLinkedToPrevious = false; |
关键技术点是:DifferentFirstPageHeaderFooter只是“开关”,它本身不含内容。内容必须分别写进FirstPageHeader和DefaultHeader对象里。如果开关没打开,就算你在 FirstPageHeader 里塞满了内容,Word 也只会当没有看见,渲染出来的仍然是默认页眉。
很多帖子只告诉你“设置某个属性”,但没告诉你为什么首页内容不生效。实际排查顺序其实就两步:先看开关开没开,再看内容是不是写到正确类型的 HeaderFooter 对象里。这两个条件缺一个,结果都是错的。
如果你使用的 Spire.Doc 版本里看不到FirstPageHeader这种属性,也可以改用索引器写法,比如section.HeadersFooters[HeaderFooterType.HeaderFirst],取到的对象完全一样。下面所有例子里,我都会优先写属性名,这样读起来直观。
2. 环境准备与选型:为什么用 Spire.Doc 而不是 COM 或 Open XML
做 Word 自动化的方案其实不少,最传统的是用 Microsoft Office 的 COM 接口。这套方案在本地装了 Office 的机器上很灵,可以操作 Word 的一切功能,但它有两个硬伤:一是服务器环境往往不能装 Office,二是 COM 组件的线程模型、释放机制很容易让 Windows 服务挂死。我之前维护过一个每晚批量生成 200 份报价单的程序,每隔一段时间就会出现“进程还在但 Word 窗口被鬼影”的诡异状态,后来彻底换掉了。
Open XML SDK 是另一个选项。它不依赖 Office,直接操作文件里的 XML 包,可控性最高。但代价也很明显:你得手工管理w:headerReference、w:ftrReference一组节点,还要理解sectPr的继承关系。写一个简单页眉比较轻松,一旦涉及首页不同、奇偶不同、多节重新编号,代码量会迅速膨胀。
Spire.Doc 站在两者中间。它在 API 层面把 Word 的节、页眉、页脚、页码域都包成了对象,既不需要 Office,又不用碰 XML。对我们这些以“交付功能”为目标的人来说,它最大的价值是开发速度快。安装也简单,Visual Studio 里直接:
Install-Package Spire.Doc或者在 NuGet 包管理器界面搜索 Spire.Doc 安装即可。
起步代码非常短:
using Spire.Doc; using Spire.Doc.Documents; using Spire.Doc.Fields; using (Document doc = new Document()) { Section section = doc.AddSection(); section.PageSetup.DifferentFirstPageHeaderFooter = true; section.AddParagraph().AppendText("第一页正文"); section.AddParagraph().AppendBreak(BreakType.PageBreak); section.AddParagraph().AppendText("第二页正文"); doc.SaveToFile("BasicDemo.docx", FileFormat.Docx); }这份代码跑完,你会得到一个页面设置已经打开“首页不同”的文档。这里我要特别提醒一句:这时候 FirstPageHeader 和 DefaultHeader 都还是空对象,所以首页和第二页都不会显示页眉。这不代表设置失败,只是因为内容还没填。
关于授权,手里有商业授权或试用 key 的话,按官方文档在程序启动时注册 License 即可。没拿到 key 时,Spire.Doc 的免费模式对文档段落数等有限制,自己评估功能没问题,但正式生产环境一定要先确认授权边界,别把试用限制带到客户现场去。
3. 核心实战:把首页和后续页彻底分开
这里我按照三个最常见的需求来拆解:首页页眉内容不同、首页隐藏页码、在此基础上叠加奇偶页设置。每个场景我都会给出可直接用的代码和必要的说明。
3.1 首页页眉放 Logo 或标题,后续页放统一文档名
场景:封面首页上方只有一行“XX 项目内部资料”,从第二页开始页眉显示“XX 项目实施方案 V2.0”。
实现时先在 PageSetup 打开开关,然后分别往 FirstPageHeader 和 DefaultHeader 里写内容。
using Spire.Doc; using Spire.Doc.Documents; using (Document doc = new Document()) { Section section = doc.AddSection(); section.PageSetup.DifferentFirstPageHeaderFooter = true; HeaderFooter firstHeader = section.HeadersFooters.FirstPageHeader; Paragraph pFirst = firstHeader.AddParagraph(); pFirst.AppendText("XX 项目内部资料"); pFirst.Format.HorizontalAlignment = HorizontalAlignment.Right; HeaderFooter defaultHeader = section.HeadersFooters.DefaultHeader; Paragraph pDefault = defaultHeader.AddParagraph(); pDefault.AppendText("XX 项目实施方案 V2.0"); pDefault.Format.HorizontalAlignment = HorizontalAlignment.Right; section.AddParagraph().AppendText("这是第一页正文。"); section.AddParagraph().AppendBreak(BreakType.PageBreak); section.AddParagraph().AppendText("这是第二页正文。"); doc.SaveToFile("FirstPageHeaderDemo.docx", FileFormat.Docx); }如果你的 logo 是图片,思路一样,把“加文字”换成“加图片”即可。Spire.Doc 里DocPicture可以用来插入图片,设置高度和宽度后放进页眉段落。不过有一点要提防:页眉里的图片默认是嵌入式的,Word 里页面边距会影响图片在一行内能不能放得下。我一般会把 height 控制在 20 到 40 像素之间,宽度视页眉显示空间而定,避免图片挤到下一行变成自动换行。
3.2 首页不显示页码,后续页显示“第 X 页 / 共 Y 页”
这个需求的核心逻辑是:打开“首页不同”后,FirstPageFooter 保持空白即可;页码只写到 DefaultFooter 里。这样 Word 渲染时,首页因为没有 FirstPageFooter 的内容,自然就不显示页码,而从第二页开始应用 DefaultFooter。
页码本身要使用字段,而不是直接把“第 1 页”当普通文字写进去。普通文字不会跟着翻页自动变化,字段才是 Word 里的活引用。Spire.Doc 里用AppendField方法插入页码域。
using Spire.Doc; using Spire.Doc.Documents; using Spire.Doc.Fields; using (Document doc = new Document()) { Section section = doc.AddSection(); section.PageSetup.DifferentFirstPageHeaderFooter = true; HeaderFooter firstFooter = section.HeadersFooters.FirstPageFooter; // 故意不往 firstFooter 里写内容,首页就没有页码 HeaderFooter defaultFooter = section.HeadersFooters.DefaultFooter; Paragraph p = defaultFooter.AddParagraph(); p.Format.HorizontalAlignment = HorizontalAlignment.Center; p.AppendText("第 "); TextRange pageField = p.AppendField(FieldType.FieldPage); pageField.Text = "1"; pageField.CharacterFormat.FontName = "Arial"; pageField.CharacterFormat.FontSize = 9; p.AppendText(" 页 / 共 "); TextRange pageCountField = p.AppendField(FieldType.FieldNumPages); pageCountField.Text = "1"; pageCountField.CharacterFormat.FontName = "Arial"; pageCountField.CharacterFormat.FontSize = 9; p.AppendText(" 页"); section.AddParagraph().AppendText("首页内容"); section.AddParagraph().AppendBreak(BreakType.PageBreak); section.AddParagraph().AppendText("第二页内容"); doc.SaveToFile("PageNumberFooterDemo.docx", FileFormat.Docx); }为什么字段还需要Text?因为 Word 文档保存时,字段可以带一个缓存结果。Spire.Doc 插入字段后通常需要给一个默认占位文本,这样在打开文档但尚未刷新字段前,至少能看到一个“1”。真正显示正确结果,还需要刷新字段。关于字段更新的问题,第五部分单独讲。
3.3 奇偶页不同和首页不同叠加
有些正式合同喜欢做成:奇数页页眉是文档标题、偶数页页眉是公司名、首页只放落款信息。这种组合在 Word 界面里其实就是同时勾选“首页不同”和“奇偶页不同”,代码里也就多开一个开关。
section.PageSetup.DifferentFirstPageHeaderFooter = true; section.PageSetup.OddAndEvenPagesHeaderFooter = true;接下来需要访问奇偶页眉。大多数 Spire.Doc 版本会提供OddHeader、EvenHeader、OddFooter、EvenFooter这组属性;如果你的版本没看到这些名字,就去枚举集合里取,比如section.HeadersFooters[HeaderFooterType.HeaderOdd]。写法略有差异,取到的对象本质是一样的。
开启奇偶页不同之后,Word 的渲染顺序会变成:
- 第一页:FirstPage 页眉/页脚
- 第二页:EvenPages 页眉/页脚
- 第三页:Default 页眉/页脚
- 第四页:EvenPages 页眉/页脚
- 第五页:Default 页眉/页脚,以此类推
要注意,默认页眉实际上充当了“奇数页页眉”的角色。如果你打开奇偶页不同,DefaultHeader 和 OddHeader 之间通常有一个对应关系,具体哪个见实效取决于 SDK 版本和 Word 对 OpenXML 的解析方式。最简单的办法是:把内容分别写到 First、Even、Default 三套对象里,然后生成文件用 Word 渲染确认一遍,比对着文档规范猜更可靠。
4. 多节文档:封面、目录、正文的页眉页脚接力
单节文档解决的是“首页 vs 后续页”,但现实需求往往是“封面不要页眉、目录页页脚用罗马数字、正文页从第 1 页重新开始”。这就必须引入多节(Section)。
4.1 节的继承规则:为什么新节默认会沿用上一节的页眉页脚
Word 里节和节之间天然存在一种“链接到前一个节”的关系。当你新建一个节时,如果什么都不改,它的页眉页脚会自动继承上一节的默认内容。这个设计本意是方便连续文档,比如整本小说都是同一个页眉,只要设置一次就行。
但自动化生成时,继承关系常常变成坑。你在第一节设置了“XX 项目内部方案”页眉,新建第二节后没有手动切断链接,那么第二节的页眉也会显示“XX 项目内部方案”。要改变第二节的内容,必须先断开链接。Spire.Doc 里对应的是IsLinkedToPrevious = false。
HeaderFooter secondDefaultHeader = secondSection.HeadersFooters.DefaultHeader; secondDefaultHeader.IsLinkedToPrevious = false;断开链接后,再将当前节的页眉内容重新赋值。如果目标是从空白开始,则先把 Paragraphs 里的历史内容清掉,再加新内容。这一步类似于在 Word 界面里把“链接到前一节”高亮取消,再重新输入页眉。
4.2 从封面到正文:两节三段式页眉页脚的写法
下面我用一个完整示例演示:第一节是封面,不要页眉,页脚也不显示页码;第二节是正文,首页页眉显示“项目实施方案”,后续页页眉显示文档名称,页脚从 1 开始编页码。
using Spire.Doc; using Spire.Doc.Documents; using Spire.Doc.Fields; using (Document doc = new Document()) { Section coverSection = doc.AddSection(); coverSection.PageSetup.DifferentFirstPageHeaderFooter = false; coverSection.AddParagraph().AppendText("封面内容"); Section bodySection = doc.AddSection(); bodySection.PageSetup.DifferentFirstPageHeaderFooter = true; // 封面节保持空白页眉即可,关键是正文节不要继承封面节的内容 bodySection.HeadersFooters.DefaultHeader.IsLinkedToPrevious = false; bodySection.HeadersFooters.FirstPageHeader.IsLinkedToPrevious = false; bodySection.HeadersFooters.DefaultFooter.IsLinkedToPrevious = false; HeaderFooter firstHeader = bodySection.HeadersFooters.FirstPageHeader; firstHeader.AddParagraph().AppendText("项目实施方案"); HeaderFooter defaultHeader = bodySection.HeadersFooters.DefaultHeader; defaultHeader.AddParagraph().AppendText("XX 项目实施方案 V2.0"); HeaderFooter defaultFooter = bodySection.HeadersFooters.DefaultFooter; Paragraph fp = defaultFooter.AddParagraph(); fp.AppendText("第 "); TextRange pageField = fp.AppendField(FieldType.FieldPage); pageField.Text = "1"; fp.AppendText(" 页"); fp.Format.HorizontalAlignment = HorizontalAlignment.Center; bodySection.AddParagraph().AppendText("正文第一页"); bodySection.AddParagraph().AppendBreak(BreakType.PageBreak); bodySection.AddParagraph().AppendText("正文第二页"); doc.SaveToFile("MultiSectionDemo.docx", FileFormat.Docx); }这段代码的关键就是IsLinkedToPrevious = false。如果没有这一行,即使你把新节的页眉重新写了,也可能出现旧内容残留或者页眉交替显示的情况。我见过不止一次程序输出文件后页眉是对的,但第二页开始页码却突然跳到 3 或 4,就是页脚链接没有断开、旧节的页码格式被带过来了。
4.3 页码重新从 1 开始:节起始页码的常见处理
想让正文部分重新从“第 1 页”计起,除了断开页脚链接之外,还要考虑起始页码。在 Word 界面里,这个选项藏在“页码格式”对话框里的“页码编号”区域,选“起始编号”为 1。Spire.Doc 中,页面设置相关对象通常会提供PageNumberStart或同类属性。
实际项目中,我建议把它和第一节的“无页码”组合使用。封面节的页脚留空,正文节 Footer 里放页码域,并且把起始页码设置为 1。这样无论封面加了多少行,正文第一页始终显示“第 1 页”,不会变成“第 5 页”或者“第 8 页”。
如果 API 版本里找不到起始页码属性,还有一个曲线方案:给封面节单独用罗马数字,正文节用阿拉伯数字。Word 本身允许不同的节使用不同的页码样式,但要注意,节与节之间的页码连续性默认会保持,除非你手动设置起始页码。所以真正能解决需求的核心思路始终是:每个节独立设置 PageSetup,并在 Footer 里插入字段,而不是写死文本。
5. 避坑清单和验证方法
代码能跑通不代表文档正确。页眉页脚这类功能属于“视觉上很容易看出问题、逻辑上又很容易踩坑”的领域。下面这类问题我在实际项目里几乎都遇到过,列成清单帮你提前绕开。
5.1 开关没开,内容白写
最常见的错误:写了 FirstPageHeader,也写了 DefaultHeader,但忘了开DifferentFirstPageHeaderFooter。结果首页显示的是默认页眉,而不是你精心写的 FirstPageHeader。检查顺序应该是:开关属性 → 内容对象 → 字段刷新,一步都不能省。
5.2 新节页眉沿用了上一节内容
新建 Section 之后,默认会继承上一节的页眉页脚。如果你已经给上一个节设置了内容,新节请你务必主动断链接。很多人只在第二个节重新写页眉,却没设置IsLinkedToPrevious = false,打开文档后新建节的内容要么没生效,要么和上一节混着显示。这个问题的排查思路其实很清晰:先确认你使用的是第几节,再看这节对应的 HeaderFooter 的 IsLinkedToPrevious 是否已经置为 false。
5.3 页码显示不对:字段没有刷新
字段本身有“缓存结果”。如果你只插入 FieldPage 但不刷新,打开文档时看到的页码可能是你设置的占位文本“1”,一直没有变化。解决的思路有两种。
第一种是让 Word 在打开时自动更新文档字段。Spire.Doc 的 Document 对象有一个IsUpdateFields属性,把它设置为 true,保存出来的文档会在打开时自动刷新域结果。第二种是保存前显式更新,具体看 SDK 版本是否提供UpdateFields方法。不同版本命名有差异,但核心目的是一样的。
如果你生成的是 PDF,情况又不同,PDF 一旦输出就无法再“打开时刷新”,所以必须确保字段在输出前已经计算完成。因此相对稳妥的做法是:在代码里主动触发一次字段更新,再保存。
5.4 页眉下面多了条线,或少了条线
Word 插入页眉时通常会带一条底部边框线,视觉上是页眉和正文之间的分隔线。这条线其实不是独立图形,而是段落底边框。没这条线时,页眉内容会显得很飘;多了一条不想要的线,又会被客户挑刺。代码里可以通过段落格式的 Borders 设置。我的经验是:先设置文字和字体,再决定要不要底边框,不要打开 Word 看到默认线后找不到去掉的入口。
5.5 生成后如何快速验证
我自己的验证流程是三步:
- 把生成的 Docx 文件用 Word 打开,直接翻页检查首页、第二页、正文中间页、正文最后一页。重点看页眉是否重复、页码是否衔接。
- 另存一份 PDF,用 PDF 预览器检验字体是否变形、图片是否溢出页眉边界。
- 如果程序是批量生成模板,我还会专门构造一个 10 页左右的测试样本,包含封面、目录、正文三段,跑完再人工抽查。这一步能提前暴露字段更新和节继承的问题,比在正式数据上反复调试省时间得多。
这几招检查完,首页不同和多节页眉页脚基本就不会翻车了。实际项目里我都是先输出一个 10 页左右的测试样本,把封面、目录、正文三段的样式一次性验证通过,再套用正式数据生成。页眉页脚看着简单,但牵扯到节和字段,任何一个环节理解不到位,最后出来的文档都是错的。希望这份实战指南能帮你少走点弯路。