DocumentFormat.OpenXml + MiniWord:Word 文档合并与内容赋值的优雅组合

在项目开发中,遇到这样的需求:要把一堆数据快速整理成格式规范的Word文档。比如:

  • 人力资源要给新员工批量制作入职通知书
  • 学校老师要为全班同学生成期末成绩单
  • 销售部门要给不同客户发送个性化的报价单
  • 财务人员要定期生成标准格式的统计报表

这些场景都有一个共同特点:数据量大、格式固定、重复性高。查找资料后了解到两个.NET库可以很轻松的就能够解决上述问题:

  • MiniWord(0.9.2):可以根据模板加填充符合快速的填充数据
  • DocumentFormat.OpenXml(3.3.0):可以把多个文档合并成一个

🛠️ 技术栈介绍

MiniWord - 文档内容填充

创建一个Word模板,只需要在需要填数据的地方写上{{姓名}}{{成绩}}这样的标记,然后把数据扔给MiniWord,它就能自动生成完整的文档。同时还能够填充Image图片、字体颜色、超链接等功能,非常的方便!

DocumentFormat.OpenXml - 微软官方的文档操作工具

这个工具比较专业,能直接操作Word文档的内部结构。我主要用它来做文档合并------把多个小文档拼接成一个大文档,还能自动加上分页符,让每个部分都从新的一页开始。

💻 完整实现代码

c# 复制代码
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
using MiniSoftware;

// 生成 100 条随机数据
List<Person> persons = GenerateRandomData(100);

// 准备临时目录
string tempDir = "TempDocs";
if (!Directory.Exists(tempDir)) Directory.CreateDirectory(tempDir);

string tplPath = "template.docx";   // MiniWord 模板

// 每 10 人一组
int groupSize = 10;
int groupCount = (int)Math.Ceiling(persons.Count / (double)groupSize);
var groupFiles = new List<string>(groupCount);

for (int g = 0; g < groupCount; g++)
{
    // 当前 10 人
    var group = persons.GetRange(g * groupSize,
                 Math.Min(groupSize, persons.Count - g * groupSize));

    // 构建表格行
    var rows = new List<Dictionary<string, object>>();
    foreach (var p in group)
    {
        rows.Add(new Dictionary<string, object>
        {
            ["Id"] = p.Id,
            ["Name"] = p.Name,
            ["Age"] = p.Age,
            ["City"] = p.City,
            ["Email"] = p.Email,
            ["Score"] = p.Score
        });
    }

    // 模板变量
    var dict = new Dictionary<string, object>
    {
        ["GroupNo"] = g + 1,
        ["Persons"] = rows          // MiniWord 支持集合标签 {{Persons}}
    };

    string groupPath = Path.Combine(tempDir, $"Group_{g + 1}.docx");
    MiniWord.SaveAsByTemplate(groupPath, tplPath, dict);
    groupFiles.Add(groupPath);
}

// 4. 合并所有组文件
string finalPath = "成绩单_总.docx";
MergeWordDocuments(groupFiles.ToArray(), finalPath);

Console.WriteLine($"完成!共 {groupCount} 个组(每组 10 人),已合并为 {finalPath}");
Console.ReadKey();

// 生成 n 条随机数据
static List<Person> GenerateRandomData(int n)
{
    var list = new List<Person>(n);
    var rnd = new Random(Guid.NewGuid().GetHashCode());

    string[] cities = { "北京", "上海", "广州", "深圳", "成都", "杭州", "西安", "武汉", "南京", "重庆" };
    string[] firstNames = { "王", "李", "张", "刘", "陈", "杨", "赵", "黄", "周", "吴" };
    string[] lastNames = { "伟", "芳", "娜", "敏", "静", "丽", "强", "磊", "洋", "勇" };

    for (int i = 1; i <= n; i++)
    {
        string name = firstNames[rnd.Next(firstNames.Length)] +
                      lastNames[rnd.Next(lastNames.Length)] +
                      (rnd.Next(2) == 0 ? lastNames[rnd.Next(lastNames.Length)] : "");

        int age = rnd.Next(18, 66);
        string city = cities[rnd.Next(cities.Length)];
        string email = $"{name.ToLower()}@example.com";
        double score = Math.Round(rnd.NextDouble() * 100, 1);

        list.Add(new Person
        {
            Id = i,
            Name = name,
            Age = age,
            City = city,
            Email = email,
            Score = score
        });
    }
    return list;
}

// 合并Word文档
static void MergeWordDocuments(string[] sourcePaths, string outputPath)
{
    using (WordprocessingDocument mergedDoc =
        WordprocessingDocument.Create(outputPath, WordprocessingDocumentType.Document))
    {
        // 创建文档主体
        MainDocumentPart mainPart = mergedDoc.AddMainDocumentPart();
        mainPart.Document = new Document(new Body());

        for (int i = 0; i < sourcePaths.Length; i++)
        {
            string sourcePath = sourcePaths[i];

            if (!File.Exists(sourcePath))
            {
                throw new FileNotFoundException($"文件不存在: {sourcePath}");
            }

            // 复制源文档内容
            using (WordprocessingDocument sourceDoc =
                WordprocessingDocument.Open(sourcePath, false))
            {
                Body sourceBody = sourceDoc.MainDocumentPart.Document.Body;

                foreach (OpenXmlElement element in sourceBody.ChildElements)
                {
                    mainPart.Document.Body.AppendChild(element.CloneNode(true));
                }
            }

            // 如果不是最后一个文档,添加分页符
            if (i < sourcePaths.Length - 1)
            {
                mainPart.Document.Body.AppendChild(new Paragraph(
                    new Run(new Break() { Type = BreakValues.Page })));
            }
        }

        mainPart.Document.Save();
    }
}

class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
    public string City { get; set; }
    public string Email { get; set; }
    public double Score { get; set; }
}

📝 模板设计

要使用 MiniWord,需要创建一个模板文档 template.docx。在模板中,我们可以使用简单的标记语法:

第 {{GroupNo}} 组学生成绩单

Id 姓名 年龄 城市 邮箱 成绩
{{Persons.Id}} {{Persons.Name}} {{Persons.Age}} {{Persons.City}} {{Persons.Email}} {{Persons.Score}}

🔍 技术要点解析

MiniWord 数据绑定

c# 复制代码
// 构建表格数据
var rows = new List<Dictionary<string, object>>();
foreach (var p in group)
{
    rows.Add(new Dictionary<string, object>
    {
        ["Id"] = p.Id,
        ["Name"] = p.Name,
        // ... 其他属性
    });
}

// 使用模板生成文档
var dict = new Dictionary<string, object>
{
    ["GroupNo"] = g + 1,
    ["Persons"] = rows
};
// 主要部分
MiniWord.SaveAsByTemplate(groupPath, tplPath, dict);

DocumentFormat.OpenXml 文档合并

c# 复制代码
// 创建目标文档
using (WordprocessingDocument mergedDoc = 
    WordprocessingDocument.Create(outputPath, WordprocessingDocumentType.Document))
{
    MainDocumentPart mainPart = mergedDoc.AddMainDocumentPart();
    mainPart.Document = new Document(new Body());
    
    // ...省略
    
    // 逐个复制源文档内容
    foreach (OpenXmlElement element in sourceBody.ChildElements)
    {
        mainPart.Document.Body.AppendChild(element.CloneNode(true));
    }
    
    // ...省略
    
    // 添加分页符
    mainPart.Document.Body.AppendChild(new Paragraph(
        new Run(new Break() { Type = BreakValues.Page })));
}

相关资源

相关推荐
考虑考虑3 小时前
ScopedValue在JDK24以及JDK25的改动
java·后端·java ee
金融数据出海4 小时前
实时性、数据覆盖范围和易用性的优质金融数据源API推荐
后端·金融·区块链·ai编程
渣哥4 小时前
面试必问:Spring 框架的核心优势,你能说全吗?
javascript·后端·面试
canonical_entropy4 小时前
告别经验主义:DDD的数学基础与工程实现
后端·架构·领域驱动设计
canonical_entropy4 小时前
软件构造的新物理学: Gemini AI对(广义)可逆计算理论的深度报告
后端
canonical_entropy4 小时前
(广义)可逆计算理论速览-统一软件构造与演化的新范式
后端
Craaaayon4 小时前
【数据结构】二叉树-图解广度优先搜索
java·数据结构·后端·算法·宽度优先
IT_陈寒5 小时前
Python 3.11性能翻倍秘诀:7个你从未注意过的隐藏优化点!
前端·人工智能·后端
ezl1fe5 小时前
RAG 每日一技(十九):当文本遇上表格,如何拿下“半结构化”PDF
人工智能·后端·算法