📎 配套资产 / Companion assets:本篇所有示例的可下载文件在仓库
配套资产/B28_ExcelDNA/——完整 VS 工程(.csproj)+ 自定义函数 + RTD 服务器 + .dna 配置,每个子目录都带中文 README 与 English README。 English: downloadable files for every example in this article live in配套资产/B28_ExcelDNA/— a complete VS project (.csproj) + custom functions + RTD server + .dna config, each with a bilingual README.
【办公开发】用 C# + Excel-DNA 打造专业 Excel 工具
摘要:用 AI 帮你用 C# + Excel-DNA 写一个能在 Excel 里直接当公式用的专业工具。
一、痛点引入
你用 Excel 久了,迟早会撞上这三堵墙:
- 第一堵墙:VBA 不够"专业"。 VBA 上手快,但跑大数据慢、调试难、代码还分散在各个工作簿里,想发给同事用,对方得先启用宏、还可能被安全策略拦下。它更像"个人小脚本",不像一个正经软件。
- 第二堵墙:内置函数(公式)不够用。 你需要一个"按自己规则算"的函数,比如"把一串中文地址拆成省/市/区",或者"调我们公司内部的接口算个价"。内置公式写不出来,嵌套 IF 会写到怀疑人生。
- 第三堵墙:实时数据进不来。 比如股价、设备传感器读数、线上订单数,Excel 公式只能你手动刷新,做不到"数据一变,单元格自动变"。这种活儿专业说法叫 RTD(Real-Time Data,实时数据)。
那有没有一种办法,能让你用一门正经编程语言,写出一个双击就能装进 Excel、像原生功能一样当公式用、还能推实时数据的工具?有。这就是本文要带你做的:用 C# + Excel-DNA 打造专业 Excel 工具。
先解释两个词,免得后面发懵:
- C#(读作 C Sharp):微软出的一门现代编程语言,写起来比 VBA 严谨、比 C++ 省心,是 .NET 平台的主力语言。你可以把它理解成"更强大、更工程化的 VBA 替代品"。
- Excel-DNA:一个开源框架(你可以把它当成一个"翻译官"),它干一件事——让你用 C# 写的代码,能被 Excel 直接识别成公式和加载项。没有它,C# 和 Excel 之间隔着一道墙;有它,你编译出来的东西就是一个
.xll文件(Excel 加载项),双击或一次加载,函数就出现了。
说明:本文是 L4 硬核篇,需要你有 Visual Studio 和基本的 C# 概念(看得懂"类""方法""using"即可)。它不神秘,但也不是"一键生成"——下面每一步都要你真实动手,才能跑通。
二、目标产出
照着本文做完,你会得到:
- 一台装好 Visual Studio(建议 2019 或更新版本)和 .NET Framework(本文以 .NET Framework 4.7.2 为例,原因见第四节与避坑指南)的电脑;
- 一个真实的 Visual Studio 工程,结构清晰、可在自己机器上编译;
- 一个能编译出
.xll的 Excel-DNA 加载项,里面包含两个东西:- 一个 custom function(自定义函数):像
=MyAdd(1,2)这样在单元格里直接当公式用; - 一个 RTD(实时数据) 函数:单元格能自动刷新、显示"实时"数值;
- 一个 custom function(自定义函数):像
- 一套"让 AI 帮你搭代码框架"的提示词模板,以及对应的 C# 代码框架(真实可运行结构,不是伪代码)。
最终效果:打开 Excel → 加载这个 .xll → 在任意单元格输入 =MyAdd(10,20),回车得到 30;再输入 =GetLivePrice(),单元格每隔约 1–2 秒自动跳一个新数。这就是"专业 Excel 工具"的雏形。
三、案例实战
环境前提(务必先确认)
- Visual Studio 2019+(社区版免费即可),安装时勾选".NET 桌面开发"工作负载。
- .NET Framework 4.7.2 开发者包(或 4.8,本文以 4.7.2 为例)。为什么不用更新的 .NET(也就是 .NET 5/6/7/8 那一支)?因为 Excel-DNA 对 .NET Framework 的支持最成熟、踩坑最少。用 .NET (Core) 也能做,但工程配置不同,本文先走最稳的路。(注:具体以你安装的 Excel-DNA 版本为准,见避坑指南第 2 条。)
- Excel 任意较新版本(2016+ 均可)。
- 能正常访问 NuGet(Visual Studio 的包管理器,相当于"代码版的软件应用商店",用来一键安装别人写好的库)。
不确定自己装没装 .NET Framework 4.7.2?在 VS 里新建工程时,如果"目标框架"下拉里能看到
.NET Framework 4.7.2,就说明有了。
第一步:新建工程、安装 Excel-DNA
- 打开 Visual Studio → "创建新项目" → 搜索并选择 "类库(.NET Framework)"(注意:是带 .NET Framework 字样的那一个,不是普通的"类库")→ 下一步。
- 工程名填
MyExcelAddIn,位置随便选一个你找得到的文件夹,框架选 .NET Framework 4.7.2 → 创建。 - 工程建好后,在右侧"解决方案资源管理器"里,右键点击工程名
MyExcelAddIn→ "管理 NuGet 程序包"。 - 在打开的窗口里切到"浏览"选项卡,搜索
ExcelDna.AddIn→ 选中 → 点"安装"。(这就是前面说的"软件应用商店"里那个叫 Excel-DNA 的库。) - 安装完,VS 会自动帮你做几件关键的事:在工程里生成一个
.dna配置文件,并把"生成时输出.xll"这条规则接好。你不用手动配,这点很重要。
小提醒:NuGet 装的是
ExcelDna.AddIn(带 .AddIn 后缀)。它是"全家桶版",装完即包含运行所需的一切。别装错成只剩核心的ExcelDna包,新手用.AddIn最省心。
第二步:看清楚 Visual Studio 工程结构
装完包后,你的工程在"解决方案资源管理器"里大致长这样(这是真实结构,不是示意):
MyExcelAddIn/ ← 你的工程根目录
├── MyExcelAddIn.csproj ← 工程文件(VS 自动维护,一般不用手改)
├── MyExcelAddIn.dna ← Excel-DNA 配置(装包时自动生成,见第三步)
├── Class1.cs ← 默认空类,可删,待会儿换成我们的代码文件
├── MyFunctions.cs ← 你自己建的:放自定义函数(custom function)
├── LivePriceServer.cs ← 你自己建的:放 RTD 实时数据服务
└── bin/
└── Debug/
└── MyExcelAddIn.xll ← 按 F6 生成后出现的文件,Excel 加载的就是它操作:在"解决方案资源管理器"里,右键工程 → "添加" → "类",分别新建两个文件,名字填 MyFunctions.cs 和 LivePriceServer.cs。默认的 Class1.cs 可以删掉。
关键点:Excel 最终认的不是
.cs源码,也不是.dll,而是编译后那个.xll文件。.xll是 Excel 加载项的专用格式,由 Excel-DNA 在生成时自动产出。
第三步:Excel-DNA 配置(.dna 文件)
装包时 VS 已经帮你生成了 MyExcelAddIn.dna。打开它,内容大致是这样的 XML(不同版本可能略有差异,以你机器上生成的为准):
xml<DnaLibrary Name="MyExcelAddIn" RuntimeVersion="v4.0"> <ExternalLibrary Path="MyExcelAddIn.dll" Pack="true" /> </DnaLibrary>
逐行用人话解释:
<DnaLibrary ...>:整个配置文件的根标签,Excel-DNA 就认这个。Name="MyExcelAddIn":加载项在 Excel 里显示的名字,随便起,别带空格和中文也行(中文可能在某些旧版 Excel 显示异常,稳妥起见用英文/拼音)。RuntimeVersion="v4.0":告诉 Excel-DNA 用 .NET Framework 4.x 运行时。因为我们选的就是 .NET Framework,所以写v4.0没问题。<ExternalLibrary Path="MyExcelAddIn.dll" Pack="true" />:这是核心一行。它说"我的 C# 代码编译成了MyExcelAddIn.dll,请把它打进加载项里"。Pack="true"表示打包——生成时会把.dll直接塞进.xll,这样你给同事时只要发一个.xll文件就够了,不用附带一堆.dll。(专业上这叫"单文件部署",最干净。)
一般情况下你不需要手改这个文件,装包时已经配好了。这里把它讲清楚,是为了让你知道"我的代码是怎么被 Excel 认识的"。如果你以后要引用别的库,才需要在这里加
<Reference>等配置——那是进阶玩法,本文不展开。
第四步:写第一个自定义函数(C# 框架)
打开 MyFunctions.cs,把下面这段代码整段粘进去(这是真实可编译的框架,AI 辅助生成时也大致长这样):
csharpusing ExcelDna.Integration; // Excel-DNA 的核心命名空间,自定义函数全靠它 namespace MyExcelAddIn { // 自定义函数所在的类通常写成静态类(函数方法本身必须是 public static,见下方逐行解释) public static class MyFunctions { // [ExcelFunction] 这个"特性"告诉 Excel-DNA:下面这个方法,请变成 Excel 里能用的公式 [ExcelFunction( Name = "MyAdd", // 在 Excel 里输入的公式名,=MyAdd(...) Description = "把两个数相加,返回结果", // 鼠标悬停时看到的说明 Category = "我的工具")] // 在函数向导里归到哪一栏 public static double MyAdd( [ExcelArgument(Name = "数值1", Description = "第一个加数")] double x, [ExcelArgument(Name = "数值2", Description = "第二个加数")] double y) { return x + y; // 真正的计算逻辑:这里只是相加,你可以换成任何 C# 能写的逻辑 } } }
逐行用人话解释:
using ExcelDna.Integration;:把 Excel-DNA 的工具箱搬进来,不写这行,[ExcelFunction]就不认识。public static class MyFunctions:自定义函数方法本身必须是public static——这是 Excel-DNA 的硬性要求(COM 反射调的就是静态方法)。所在类通常写成静态类(public static class),让"这个类只用来挂函数、不能被实例化"这件事从代码层面一眼看出来;如果你不想写静态类,写成普通类也可以,只要里面的函数方法是public static就行。[ExcelFunction(...)]:这是"魔法标签"。贴在方法上,Excel-DNA 就会在 Excel 里注册一个同名公式。Name是用户在单元格里敲的公式名;Description是悬停提示;Category是把你的函数归到函数向导的某个分类下,方便别人找。[ExcelArgument(...)]:贴在参数上,给每个参数起个中文名、写句说明,体验更友好。return x + y;:这是函数体,现在只做加法。你可以把它换成任何业务逻辑——拆地址、算内部价、查字典都行。这就是 custom function 的本质:把"你的 C# 逻辑"暴露成"Excel 公式"。
第五步:写一个 RTD 实时数据函数(C# 框架)
实时数据(RTD)要稍微复杂一点。Excel-DNA 提供了一个基类 ExcelRtdServer,你继承它,就能做一个"会自己定时推送新值"的数据源。
打开 LivePriceServer.cs,粘入下面框架(这是真实结构,定时器和推送逻辑都齐了,你只需把"模拟数据"换成真实数据源,比如调你们公司的接口):
csharpusing ExcelDna.Integration; using ExcelDna.Integration.Rtd; // RTD 实时数据相关类都在这里 using System.Timers; namespace MyExcelAddIn { // 继承 ExcelRtdServer,Excel-DNA 会把它当一个实时数据源来用 public class LivePriceServer : ExcelRtdServer { private Timer _timer; // .NET 自带的计时器,用来定时触发刷新 private double _lastValue = 0; // 当前最新值 // 加载项启动这个 RTD 服务时调用(ExcelRtdServer.ServerStart 是无参虚方法,没有带 ServerStartEventArgs 的重载) protected override bool ServerStart() { _timer = new Timer(1000); // 每 1000 毫秒(1 秒)触发一次 _timer.Elapsed += (s, e) => { // 下面这行是"模拟数据":真实场景请换成调用 API / 读传感器 / 查数据库 _lastValue = new Random().NextDouble() * 100; // 把所有已订阅的主题都更新成最新值(用 GetActiveTopics() 获取当前活跃的主题集合) foreach (var topic in GetActiveTopics()) { topic.UpdateValue(_lastValue); // 告诉 Excel:这个值变了,请刷新 } }; _timer.Start(); return true; // 返回 true 表示服务启动成功 } // 服务关闭时调用,做清理 protected override void ServerTerminate() { _timer?.Stop(); _timer?.Dispose(); } // Excel 第一次订阅某个主题时调用,返回初始值 protected override object ConnectData(Topic topic, System.Collections.Generic.IList<string> topicInfo, ref bool newValues) { newValues = true; return _lastValue; } // Excel 不再需要某个主题时调用,做清理 protected override void DisconnectData(Topic topic) { // 这里一般无需特殊处理,留空即可 } } }
然后,在 MyFunctions.cs 里补一个"包装函数",让用户在 Excel 里用 =GetLivePrice() 就能拿到实时值(而不是手敲 =RTD(...) 那种难记的写法):
csharp// 放到 MyFunctions 静态类里,和 MyAdd 并列 [ExcelFunction(Description = "获取实时价格(RTD 实时数据示例)")] public static object GetLivePrice() { // XlCall.RTD 是 Excel-DNA 提供的调用实时数据的方法 // 第一个参数是 RTD 服务的"全名"(命名空间.类名),后面是可变数量的主题参数 return XlCall.RTD("MyExcelAddIn.LivePriceServer", null, "PRICE"); }
关于
XlCall.RTD第一个参数(所谓 ProgId,即"程序标识符"):本文按"命名空间.类名"的约定填写(MyExcelAddIn.LivePriceServer)。不同 Excel-DNA 版本对 RTD 注册的具体机制可能略有差别,请确保你机器上该服务的全名保持一致,实在对不上时,以你安装的 Excel-DNA 官方文档为准(见文末"技术准确性风险点"与避坑指南第 4 条)。
第六步:生成并加载到 Excel
- 按
F6(或菜单"生成"→"生成解决方案")。VS 下方"输出"窗口若显示"生成成功",就说明编译通过了。 - 打开工程目录下的
bin\Debug\,你应该能看到 **MyExcelAddIn.xll** 文件——这就是成品。**该目录下出现哪个.xll就加载哪个**(如果工程名不同、或多配置并存,文件名会跟着变,别死记MyExcelAddIn.xll)。 - 打开 Excel。主路径(推荐,所有版本通用):文件 → 选项 → 加载项 → 底部"管理"选 "Excel 加载项" → 转到 → 浏览 → 选中
bin\Debug下那个.xll→ 确定。旧版快捷键参考:在很老的 Excel 里也能用Alt+T+I打开"加载项"管理器(仅作历史快捷键参考,不同版本/语言可能略有差异,不要死记)。注意Alt+F11打开的是 VBA 编辑器,跟.xll加载项没关系,别按错。- 更省事:直接把
.xll文件拖进 Excel 窗口,也能加载(Excel 会问是否加载,选是)。
- 更省事:直接把
- 加载成功后,在任意单元格输入
=MyAdd(10,20),回车,得到30;再输入=GetLivePrice(),每隔约 1–2 秒,单元格里的数字会自动跳变(Excel 对 RTD 更新默认有约 2 秒的节流间隔,不是每秒必跳,属正常)——说明 RTD 实时推送生效了。
如果 Excel 提示"宏已被禁用"或加载失败:先确认文件是从本地磁盘打开(不是从邮件/网盘直接点开),Excel 对本地
.xll通常放行;公司电脑若有限制,需要让 IT 把该加载项加入信任列表。这不是代码问题,是安全策略,见避坑指南。
附:让 AI 帮你写代码的提示词模板
你不必从零背 C#。把下面这段发给任意 AI 助手,它就能给你搭出上面的框架(然后你再按本文核对、跑通):
我在用 C# + Excel-DNA 写一个 Excel 加载项(.xll)。请给我可直接编译的代码框架,要求:
1. 一个自定义函数(custom function),用 [ExcelFunction] 标记,函数名 MyAdd,两个 double 参数,
带中文 Name / Description / Category,功能为两数相加;
2. 一个 RTD 实时数据服务,继承 ExcelRtdServer,用 Timer 每 1 秒推送一个随机数,
并提供一个包装函数 GetLivePrice() 通过 XlCall.RTD 调用它;
3. 附上对应的 .dna 配置示例(ExternalLibrary,Pack=true);
4. 用 .NET Framework 4.7.2,命名空间 MyExcelAddIn。
请注意:只给真实可运行的框架,注明哪些地方需要我替换成真实数据源。提示词要点:把"框架/版本/命名空间/要做什么"写死,再让 AI 注明"哪里要换真实数据"——这样它给的代码才贴近真实,而不是凭空编。拿到代码后,务必对照本文第四步、第五步逐行核对,别直接盲跑。
四、原理小结
一句话说清底层发生了什么:Excel-DNA 在你的 C# 工程生成时,把代码编译成 .dll,再按 .dna 配置把它打包进一个 .xll 加载项;当你在 Excel 里加载这个 .xll,Excel-DNA 运行时会扫描你贴了 [ExcelFunction] 的方法,把它们逐个注册成 Excel 能识别的公式,于是 =MyAdd(...) 就能像 SUM 一样用了;而 RTD 部分,则靠 ExcelRtdServer 在后台起一个定时器,数据一更新就通过 topic.UpdateValue(...) 主动推给 Excel,Excel 收到通知便自动刷新对应单元格——这正是"实时数据"和普通公式"你不动它就不动"的本质区别。
五、避坑指南
- 装错 NuGet 包:一定要装
ExcelDna.AddIn(全家桶),不是裸的ExcelDna。装错会导致生成不出.xll或运行时报错。新手认准带.AddIn后缀的那个。 - .NET 版本选错:本文用 .NET Framework 4.7.2,这是与 Excel-DNA 配合最稳的组合。若你选了 .NET (Core) / .NET 5+,工程模板、
.csproj写法、.dna里的RuntimeVersion都会不同,且对 COM/RTD 支持有额外要求。不确定就先用 .NET Framework,别一上来挑战新运行时。 - 加载项加载了但函数不出现:多半是代码没编译成功(先看 VS "输出"窗口有没有报错),或
.dna里ExternalLibrary的Path与实际生成的.dll名对不上(通常装包后自动一致,别手改错)。还有一种是 Excel 64 位 / 32 位与你编译的"目标平台"不匹配——工程属性里"生成→目标平台"建议设为Any CPU或与你 Excel 位数一致。 - RTD 注册/ProgId 对不上:
XlCall.RTD第一个参数用的是 RTD 服务的全名(命名空间.类名)。不同 Excel-DNA 版本在 RTD 自动注册上的细节可能有差异;若=GetLivePrice()返回#VALUE!或#NAME?,优先核对服务类名拼写,并查阅你所装版本的官方文档确认注册方式。(本文代码为通用框架,具体行为以你安装的版本为准。) - 公司电脑加载被拦:
.xll本质是可执行加载项,企业安全软件可能拦截。这不是代码错,需要把文件放到本地受信任位置,或由 IT 加入白名单。先用自己电脑跑通,再谈分发。
六、进阶延展
- 回指基础:如果你还没系统玩过 Excel 的"轻量进阶三件套",建议先补本系列的 B2/B26(Power Query 数据清洗)、B21/B23(Power BI 报表与数据模型)、B22(DAX 度量值)——它们是"不改代码也能大幅提效"的地基;本文的 C# + Excel-DNA 是往下一层走:当公式和数据透视表都够不着你的需求时,才自己写加载项。
- 往深一层:跑通本文后,下一步可以学:① 用 Excel-DNA 的 Ribbon(功能区) 自定义 Excel 菜单按钮,让工具不只是函数,还能点按钮触发;② 把"模拟随机数"换成真实数据源(HTTP 接口、数据库、消息队列),做出真正有用的实时看板;③ 用 ExcelDnaPack 把依赖全打包成单文件
.xll,方便发给全公司;④ 给你的函数加 异步支持(async/await),调慢接口时不卡 Excel。 - 横向对照:本文是"硬核开发"路线;若你只想批量处理现成 Excel 文件、不想写加载项,回到 B25(Python + openpyxl) 那条更轻的路会更顺手。
小结:今天你用一个真实可编译的 C# + Excel-DNA 工程,做出了"自定义函数 + RTD 实时数据"的专业 Excel 工具雏形。它不靠宏、不靠嵌套公式,而是一个正经的
.xll加载项——这就是从"Excel 用户"通往"Excel 工具开发者"的第一步。