# ⚙️ B28｜用 C# + Excel-DNA 打造专业 Excel 工具 — 配套资产

> **系列**：AI 办公实战：从小白到达人
> **文章号**：B28（Track 7 办公开发 / Level 4 硬核）
> **配套中文标题**：用 C# + Excel-DNA 打造专业 Excel 工具
> **配套英文标题**：Build Pro Excel Tools with C# + Excel-DNA

---

## 一、这是给谁的？解决什么问题？

写给**用 Excel 久了撞上三堵墙**的朋友：

- **VBA 不够专业**——跑大数据慢、调试难、发同事被宏策略拦；
- **内置函数不够用**——需要"按自家规则算"的函数，IF 嵌套到怀疑人生；
- **实时数据进不来**——需要"数据一变、单元格自动刷"的看板（RTD）。

主教程讲的是**完整开发路径**；本目录给你一套**真实可编译的最小工程骨架**——`MyExcelTools.csproj` + `MyFunctions.cs` + `MyRtdServer.cs` + `MyTools.dna` + `Properties\AssemblyInfo.cs`，拷到 Visual Studio 里按 `F6` 就能生成 `.xll`、加载进 Excel 用。

| 文件 | 干什么 |
|------|--------|
| `MyExcelTools_zh.csproj` | .NET Framework 4.7.2 工程文件（含 ExcelDna.AddIn 自动引用） |
| `MyFunctions_zh.cs` | 自定义函数：`MyAdd` / `MyConcat` / `GetLivePrice` |
| `MyRtdServer_zh.cs` | RTD 实时数据服务器（每 1 秒推一个新数） |
| `MyTools_zh.dna` | Excel-DNA 配置（ExternalLibrary Pack="true"） |
| `Properties_AssemblyInfo_zh.txt` | AssemblyInfo 标准模板 |
| （同名 `_en` 系列） | 英文注释版 |

跑通本目录，你就有了"双击 `.xll` → 加载进 Excel → 像 SUM 一样调自定义函数 + RTD 实时数据"的硬核工具雏形。

---

## 二、使用方法（5 步跑通示例）

### 第一步：环境准备

- **Visual Studio 2019+** 社区版（免费），安装时勾选"**.NET 桌面开发**"工作负载；
- **.NET Framework 4.7.2 开发者包**（或 4.8）。不确定？新建工程时如果目标框架下拉能看到 `.NET Framework 4.7.2`，就说明有了；
- **Excel 2016+**；
- 能访问 **NuGet**。

> ⚠️ **不要选 .NET (Core) / 5+ / 6+ / 7+ / 8+**：Excel-DNA 对 .NET Framework 4.x 的支持最成熟、踩坑最少。

### 第二步：新建工程、装 NuGet 包

1. VS → 创建新项目 → 选 **"类库(.NET Framework)"**（带 .NET Framework 字样的）；
2. 工程名填 `MyExcelAddIn`，目标框架选 **.NET Framework 4.7.2** → 创建；
3. 右键工程 → **管理 NuGet 程序包** → 浏览 → 搜 **`ExcelDna.AddIn`** → 安装；
4. 安装完 NuGet 会自动：① 在工程根目录生成 `.dna` 配置；② 接好"生成时输出 `.xll`"的 MSBuild 目标；③ 把 `MyFunctions.cs / MyRtdServer.cs / AssemblyInfo.cs` 的模板化框架建好。

> ⚠️ 一定要装 `ExcelDna.AddIn`（全家桶），别装成裸的 `ExcelDna`。

### 第三步：把本目录源码拷进工程

把 `MyFunctions_zh.cs` 和 `MyRtdServer_zh.cs` 拷到工程目录（覆盖 VS 自动生成的占位文件）。把 `Properties_AssemblyInfo_zh.txt` 重命名为 `Properties\AssemblyInfo.cs`（VS 默认已经有，可手改覆盖）。把 `MyTools_zh.dna` 重命名为 `MyExcelAddIn.dna`（与 .csproj 的 RootNamespace 同名）。

### 第四步：编译 + 加载

1. VS 里按 **`F6`**（生成解决方案）；
2. 打开 `bin\Debug\` → 找到 **`MyExcelAddIn.xll`**（以你工程名/AssemblyName 为准）；
3. Excel 里 **文件 → 选项 → 加载项** → 底部"管理"选 **"Excel 加载项"** → **转到** → **浏览** → 选中 `.xll` → 确定。
   - 旧版快捷键参考：`Alt + T + I` 打开加载项管理器（**注意不是** `Alt+F11`——那是 VBA 编辑器）。
   - 更省事：直接把 `.xll` 拖进 Excel 窗口。
4. 在任意单元格输入 `=MyAdd(10, 20)`，回车，得 **30**；
5. 输入 `=GetLivePrice()`，每隔约 1 秒数字会跳——RTD 实时推送生效。

### 第五步：让 AI 帮你扩展

跑通后，把下面的提示词发给 AI 就能快速加业务函数：

```
我在用 C# + Excel-DNA 写一个 Excel 加载项。已有 MyExcelAddIn 工程，
其中 MyFunctions.cs 里已经有 MyAdd / MyConcat / GetLivePrice 三个函数。

请帮我新增一个函数 =Tax(salary, region)，按以下规则返回应缴个税：
- salary 是应税收入（number）
- region 文本（"北京" / "上海" / "深圳"），决定税率
- 给出 [ExcelFunction] + [ExcelArgument] 的完整方法签名 + 函数体（中英文注释）
```

---

## 三、文件清单

| 文件名 | 类型 | 作用 | 行数 |
|--------|------|------|------|
| `B28_ExcelDNA_中文README.md` | Markdown | 本文件 | — |
| `B28_ExcelDNA_EnglishREADME.md` | Markdown | 英文版 README | — |
| `MyExcelTools_zh.csproj` | MSBuild XML | 工程文件（中文注释） | ~80 |
| `MyExcelTools_en.csproj` | MSBuild XML | 工程文件（英文注释） | ~80 |
| `MyFunctions_zh.cs` | C# | 自定义函数实现（中文） | ~85 |
| `MyFunctions_en.cs` | C# | 自定义函数实现（英文） | ~85 |
| `MyRtdServer_zh.cs` | C# | RTD 实时数据服务（中文） | ~95 |
| `MyRtdServer_en.cs` | C# | RTD 实时数据服务（英文） | ~95 |
| `MyTools_zh.dna` | XML | Excel-DNA 配置（中文注释） | ~40 |
| `MyTools_en.dna` | XML | Excel-DNA 配置（英文注释） | ~40 |
| `Properties_AssemblyInfo_zh.txt` | C# | 程序集信息模板（中文） | ~30 |
| `Properties_AssemblyInfo_en.txt` | C# | 程序集信息模板（英文） | ~30 |

---

## 四、实战代码（关键片段逐行解读）

### ① [ExcelFunction] 标记的自定义函数

```csharp
[ExcelFunction(
    Name        = "MyAdd",                              // Excel 公式名
    Description = "把两个数相加，返回结果。",               // 鼠标悬停提示
    Category    = "我的工具",                            // 函数向导分组
    IsVolatile  = false)]                               // 非易失（数据没变不重算）
public static double MyAdd(
    [ExcelArgument(Name = "数值1", Description = "第一个加数")] double x,
    [ExcelArgument(Name = "数值2", Description = "第二个加数")] double y)
{
    return x + y;
}
```

**四个铁律**：
- 类必须是 **`public static class`**；
- 方法必须是 **`public static`**（COM 反射的就是静态方法）；
- `[ExcelFunction]` 贴在方法上；
- 参数用 `[ExcelArgument]` 贴中文名 + 说明。

### ② 包装 RTD 的函数

```csharp
[ExcelFunction(Name = "GetLivePrice", Description = "获取实时价格。")]
public static object GetLivePrice()
{
    // XlCall.RTD(服务全名, 回调参数, 主题1, 主题2, ...)
    return XlCall.RTD("MyExcelAddIn.MyRtdServer", null, "PRICE");
}
```

**关键点**：`XlCall.RTD` 的第一个参数是 RTD 服务的"全名"，按"命名空间.类名"约定（`MyExcelAddIn.MyRtdServer`）。不同 Excel-DNA 版本注册机制可能略有差异；以你安装版本的官方文档为准。

### ③ RTD 实时数据服务器

```csharp
public class MyRtdServer : ExcelRtdServer
{
    private Timer _timer;
    private double _lastValue = 0;

    // 必须重写：服务启动（无参版本）
    protected override bool ServerStart()
    {
        _timer = new Timer(1000);
        _timer.Elapsed += (s, e) =>
        {
            _lastValue = new System.Random().NextDouble() * 100;
            // 把所有订阅者都更新一次
            foreach (Topic topic in GetActiveTopics())   // 注意：protected 实例方法，不是 override
            {
                topic.UpdateValue(_lastValue);
            }
        };
        _timer.Start();
        return true;
    }

    protected override void ServerTerminate() { _timer?.Stop(); _timer?.Dispose(); }

    protected override object ConnectData(Topic topic, IList<string> topicInfo, ref bool newValues)
    {
        newValues = true;
        return _lastValue;
    }

    protected override void DisconnectData(Topic topic) { /* no-op */ }
}
```

**四个必须实现**：
- `ServerStart()`：**无参**虚方法（不同版本签名可能不同，以你安装的版本为准）；
- `ServerTerminate()`：清理 Timer / 数据库连接等；
- `ConnectData(Topic, IList<string>, ref bool)`：返回初始值；
- `DisconnectData(Topic)`：取消订阅时清理。

**一个易错点**：`GetActiveTopics()` 是 **protected 实例方法**（不是 override），推送时用它遍历当前所有订阅者。

### ④ .dna 单文件部署配置

```xml
<DnaLibrary Name="MyExcelAddIn AddIn" RuntimeVersion="v4.0">
  <ExternalLibrary Path="MyExcelAddIn.dll" Pack="true" />
</DnaLibrary>
```

**一行核心**：`Pack="true"` 让 Excel-DNA 在生成 `.xll` 时把 `.dll` 打进去——分发只需要一个 `.xll` 文件，不用附带一堆 dll。

---

## 五、常见问题 / 避坑指南

1. **NuGet 装错包**
   装的是 `ExcelDna.AddIn`（全家桶），不是裸的 `ExcelDna`。装错会生成不出 `.xll` 或运行时找不到 `ExcelDna.Integration` 程序集。

2. **.NET Framework 与 .NET (Core) 选错**
   本文用 **.NET Framework 4.7.2**。如果选 .NET (Core) / 5+，`.csproj`、`.dna` 的 `RuntimeVersion`、RTD 注册细节都会不同；不确定就先按本文走。

3. **加载项加载了但函数不出现**
   三种最常见：① 代码没编译成功（VS 输出窗口有没有红字）；② `.dna` 里 `ExternalLibrary Path` 与实际生成的 dll 名对不上（默认一致，别手改）；③ Excel 64 位 vs 工程目标平台不匹配——工程属性 → 生成 → 目标平台改成 `Any CPU` 或与 Excel 位数一致。

4. **`=GetLivePrice()` 返回 `#VALUE!` / `#NAME?`**
   RTD 服务全名拼错（`MyExcelAddIn.MyRtdServer`），或者 RTD 没有成功注册。检查 `.dna` 里有没有 `ExternalLibrary` 指向正确 dll；同时核对 Excel 是否允许 RTD 加载项运行（默认允许，部分企业 IT 会禁用）。

5. **公司电脑加载被拦**
   `.xll` 本质是可执行加载项，企业安全软件 / 组策略可能拦截。这不是代码错——把 `.xll` 放到本地受信任位置，或由 IT 加入白名单；先在自己机器跑通再谈分发。

---

## 六、下一步

跑通本文档，你就有了一个"双击 .xll 就能用"的专业 Excel 工具雏形。下一步推荐：

- **真实数据接入**：把 `MyRtdServer._lastValue = new Random().NextDouble() * 100` 换成调自家 HTTP API / 数据库 / 消息队列，做出真正有用的实时看板；
- **自定义 Ribbon 按钮**：用 Excel-DNA 的 `ExcelDnaUtils` + `IRibbonExtensibility` 在 Excel 顶栏加自定义按钮，让工具不只是函数、还能点按钮触发；
- **单文件部署**：用 `ExcelDnaPack` 把所有依赖打进一个 `.xll`，发给全公司同事双击就能用；
- **异步支持**：给函数加 `async`（Excel-DNA 0.34+ 支持），调慢接口时不卡 Excel；
- **横向对照**：若你只想批量处理现成 Excel 文件、不想写加载项，回到 **B25（Python + openpyxl）** 那条更轻的路更顺手。

---

> 📌 **快速核查清单**（跑之前再确认一次）：
> - [ ] VS 装了 ".NET 桌面开发" 工作负载；
> - [ ] 工程是 "类库(.NET Framework)"，目标框架是 4.7.2；
> - [ ] NuGet 装的是 `ExcelDna.AddIn`（不是 `ExcelDna`）；
> - [ ] 工程里有 `MyFunctions.cs` / `MyRtdServer.cs` / `Properties\AssemblyInfo.cs` 三个源文件；
> - [ ] 根目录有 `<RootNamespace>.dna` 文件，且 `ExternalLibrary Path` 与 dll 名一致；
> - [ ] `F6` 编译成功，`bin\Debug\` 下看到 `.xll`；
> - [ ] Excel 通过 "文件 → 选项 → 加载项 → 转到 → 浏览" 加载成功；
> - [ ] `=MyAdd(10, 20)` 返回 30；
> - [ ] `=GetLivePrice()` 每秒跳一个新数。