Excel 支持 COM 加载项,可扩展其界面并以编程方式与工作簿交互。加载项可以添加功能区按钮、处理电子表格事件,并在任务窗格(位于数据旁边的侧面板)中显示自定义 UI。
借助 DotNetBrowser,任务窗格可以托管完整的 Chromium 浏览器,其中的网页可以直接读写电子表格数据。
本文将逐步讲解如何构建一个实现上述功能的 COM 加载项。完整源代码可在 GitHub 仓库 中获取。
如果您正在探索相关的 Office 集成场景,以下两篇教程可能对您有所帮助。VSTO 教程 介绍如何将 DotNetBrowser 嵌入 Microsoft Outlook 加载项,COM/ActiveX 教程 则说明了 COM 客户端和 ActiveX 容器的通用封装方法。
架构概览
我们要构建的加载项是一个 COM 类库,Excel 通过 Windows 注册表发现并在启动时加载它。它通过一个按钮扩展功能区;点击该按钮可打开任务窗格并在其中启动 DotNetBrowser 浏览器视图。

Excel 中使用 DotNetBrowser 的自定义窗格。
DotNetBrowser 提供了一个可视化的 BrowserView 控件,可嵌入 WinForms 应用程序中。我们将其包装在一个 COM 可见的 WinForms 容器中,并在任务窗格中显示该容器。浏览器加载与加载项捆绑的本地网页,并将一个特殊的 C# 对象注入页面的 JavaScript 环境。我们将使用该对象让页面与电子表格直接交换信息。
Excel
├── COM 加载项
│ ├── 自定义功能区选项卡和按钮
│ └── 自定义窗格
│ └── WinForms COM 可见控件
│ ├── DotNetBrowser
│ └── 本地网页
1. 项目设置
创建类库
在 Visual Studio 中,创建一个新的 Windows 窗体类库,目标框架为 .NET Framework 4.7.2 或更高版本。.NET Framework 是 Office COM 加载项的常规目标框架。将项目命名为 ExcelComAddin,输出类型必须为 Library。
安装 DotNetBrowser
安装 DotNetBrowser.WinForms NuGet 包。它会引入核心 DotNetBrowser 包以及适用于所有支持的 Windows 架构的 Chromium 二进制文件:
dotnet add package DotNetBrowser.WinForms
为程序集签名
使用强名称为程序集签名,可确保 regasm.exe 能够可靠地注册它,并且 Excel 能够一致地加载它。在 Visual Studio 中,依次进入 项目属性 → 签名,勾选为程序集签名,并创建一个新的密钥文件。.csproj 中将体现以下内容:
<PropertyGroup>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>test.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>
在生成时启用 COM 注册
在 项目属性 → 生成 中,为 Debug 配置启用为 COM 互操作注册。MSBuild 将在每次生成后运行 regasm.exe,并在清理时取消注册。
在 .csproj 中:
<PropertyGroup
Condition="'$(Configuration)|$(Platform)'
== 'Debug|AnyCPU'">
<RegisterForComInterop>true</RegisterForComInterop>
</PropertyGroup>
程序集注册会写入 Windows 注册表,需要管理员权限。请以管理员身份运行 Visual Studio,否则注册步骤将因访问被拒绝而失败,而这一错误在生成输出中很容易被忽视。
2. 加载项入口点
Connect 类是 Excel 在启动时加载的 COM 入口点。我们使用属性标记它,以向运行时标识自身:
[ComVisible(true)]
[Guid("...")]
[ProgId("ExcelComAddin.Connect")]
[ClassInterface(ClassInterfaceType.AutoDispatch)]
public class Connect : IDTExtensibility2,
IRibbonExtensibility,
ICustomTaskPaneConsumer
ClassInterfaceType.AutoDispatch 属性指示 COM 通过 IDispatch 公开所有公共成员,无需单独声明接口。
Connect 类实现了三个接口:IDTExtensibility2(加载项生命周期)、IRibbonExtensibility(功能区按钮,详见第 4 节)和 ICustomTaskPaneConsumer(Excel 通过该接口提供用于创建任务窗格的工厂对象)。
IDTExtensibility2 接口管理加载项的生命周期。当加载项连接时,我们捕获 Excel 的 Application 对象:稍后需要使用它来访问活动工作表:
public class Connect : IDTExtensibility2, ...
{
private Application _excelApplication;
public void OnConnection(
object application,
ext_ConnectMode connectMode,
object addInInstance,
ref Array custom)
{
_excelApplication = application as Application;
}
...
}
ICustomTaskPaneConsumer 接口提供 Excel 用于创建任务窗格的工厂。它有一个方法 CTPFactoryAvailable,Excel 在加载项启动期间调用该方法将工厂实例传递给我们。我们将其存储起来,稍后用于创建窗格:
public void CTPFactoryAvailable(ICTPFactory CTPFactoryInst)
{
_ctpFactory = CTPFactoryInst;
}
当用户打开面板时,我们使用此工厂创建任务窗格:
public class Connect : IDTExtensibility2, ...
{
public Connect()
{
_taskPaneManager = new TaskPaneManager(..., CreateTaskPane, ...);
}
private CustomTaskPane CreateTaskPane()
{
var ctp = _ctpFactory.CreateCTP(
"ExcelComAddin.BrowserHostControl",
"Demo Panel",
Type.Missing);
// ...
}
}
3. 加载项注册
仅有 COM 可见类还不足以让 Excel 发现该加载项。还需要在以下位置添加注册表项:
HKCU\Software\Microsoft\Office\Excel\Addins\<ProgId>
该项必须包含以下值:
| 值 | 内容 |
|---|---|
FriendlyName | 加载项管理器中的显示名称 |
Description | 简短描述 |
LoadBehavior | 3,在启动时加载并立即连接 |
ProgId | ExcelComAddin.Connect |
CommandLineSafe | 0,在非交互模式下不安全加载 |
要在 regasm.exe 注册过程中自动写入和删除此注册表项,需要用 [ComRegisterFunction] 和 [ComUnregisterFunction] 装饰以下两个方法:
public class Connect : IDTExtensibility2, ...
{
[ComRegisterFunction]
public static void Register(Type t) =>
ComRegistration.RegisterExcelAddInKeys();
[ComUnregisterFunction]
public static void Unregister(Type t) =>
ComRegistration.UnregisterExcelAddInKeys();
}
4. 功能区扩展
在 Connect 类中,我们实现 IRibbonExtensibility.GetCustomUI,它返回功能区扩展的 XML。该 XML 添加了一个带有按钮的选项卡,用于打开任务窗格:
<customUI xmlns='http://schemas.microsoft.com/office/2009/07/customui'>
<ribbon><tabs>
<tab id='excelComAddinTab' label='Sales Lead Add-in'
getVisible='GetTabVisible'>
<group id='excelComAddinGroup' label='Lead Tools'>
<button id='excelComAddinOpenPanelButton'
size='large' label='Open Panel'
imageMso='TableInsert'
onAction='OnOpenPanel'/>
</group>
</tab>
</tabs></ribbon>
</customUI>

Excel 功能区中的自定义选项卡。
5. 初始化 DotNetBrowser
DotNetBrowser 的 Engine 是该库的核心实体,负责管理 Chromium 主进程的生命周期。我们在任务窗格首次打开时创建引擎,并使用自定义 app:// URL 方案对其进行配置:
public class Connect : IDTExtensibility2, ...
{
public Connect()
{
_taskPaneManager = new TaskPaneManager(
..., new EngineManager());
}
}
public class EngineManager
{
public IEngine Start()
{
var builder = new EngineOptions.Builder
{
RenderingMode = RenderingMode.HardwareAccelerated
};
builder.Schemes[Scheme.Create("app")] =
new Handler<
InterceptRequestParameters,
InterceptRequestResponse>(HandleAppSchemeRequest);
return EngineFactory.Create(builder.Build());
}
}
加载本地页面
我们的网页以嵌入式资源的形式捆绑在程序集中。在 .csproj 中:
<ItemGroup>
<EmbeddedResource Include="..\Web\index.html">
<Link>Web\index.html</Link>
</EmbeddedResource>
</ItemGroup>
方案处理程序从清单中读取该文件并将其作为响应返回:
private const string IndexHtmlResource = "ExcelComAddin.Web.index.html";
private static InterceptRequestResponse HandleAppSchemeRequest(
InterceptRequestParameters parameters)
{
using (var stream = Assembly.GetExecutingAssembly()
.GetManifestResourceStream(IndexHtmlResource))
using (var ms = new MemoryStream())
{
stream.CopyTo(ms);
var options = new UrlRequestJobOptions
{
HttpStatusCode = HttpStatusCode.OK,
Headers = { new HttpHeader("Content-Type", "text/html") }
};
var job = parameters.Network.CreateUrlRequestJob(
parameters.UrlRequest, options);
job.Write(ms.ToArray());
job.Complete();
return InterceptRequestResponse.Intercept(job);
}
}
此处理程序仅提供单个 HTML 文件。如果需要提供更复杂的文件集(如 JavaScript、CSS、图片),并根据文件扩展名推断内容类型,可以扩展此模式。详情请参阅此实现。
如果您的用例需要加载远程托管的页面,可跳过此步骤并直接加载常规 URL。
6. 在任务窗格中显示浏览器
我们将 BrowserHostControl 创建为具有 COM 可见性的 UserControl,以便 Excel 的任务窗格 API 可以实例化它:
[ComVisible(true)]
[Guid("...")]
[ProgId("ExcelComAddin.BrowserHostControl")]
public class BrowserHostControl : UserControl
然后初始化浏览器,将其添加到 BrowserHostControl,并导航至页面:
public class BrowserHostControl : UserControl
{
private IBrowser _browser;
public void InitializeBrowser(IEngine browserEngine)
{
_browser = browserEngine.CreateBrowser();
var browserView = new BrowserView();
browserView.InitializeFrom(_browser);
browserView.Dock = DockStyle.Fill;
Controls.Add(browserView);
_browser.Navigation.LoadUrl(
"app://excelcomaddin/index.html");
}
}
电子表格与浏览器的通信
我们还配置浏览器,在每次页面加载时将一个 C# 对象注入 JavaScript 上下文。我们将使用该对象在页面与电子表格之间交换信息:
public class BrowserHostControl : UserControl
{
private Func<string> _read;
private Action<string> _write;
public string readFromExcel() => _read();
public void writeToExcel(string value) => _write(value);
public void SetJavaScriptCallbacks(Func<string> read, Action<string> write)
{
_read = read;
_write = write;
}
public void InitializeBrowser(IEngine browserEngine)
{
// ...
_browser.InjectJsHandler =
new Handler<InjectJsParameters>(args =>
{
IJsObject window = args.Frame
.ExecuteJavaScript<IJsObject>("window")
.Result;
window.Properties["excelBridge"] = this;
});
// ...
}
}
7. 任务窗格
TaskPane 是我们围绕 CustomTaskPane 定义的薄封装层。它公开 HostControl 属性(一个 BrowserHostControl)和 Visible setter,使 TaskPaneManager 代码免受 COM API 细节的影响。
当用户点击功能区按钮时,我们创建任务窗格、提供读写电子表格数据的回调,并启动浏览器:
public class TaskPaneManager
{
public TaskPaneManager(
Func<Application> getApplication,
Func<TaskPane> paneFactory,
EngineManager engineManager)
{ ... }
public void Show()
{
_pane = _paneFactory();
_pane.HostControl.SetJavaScriptCallbacks(
ReadCellA1, WriteCellA1);
_pane.HostControl.InitializeBrowser(
_engineManager.Start());
_pane.Visible = true;
}
private string ReadCellA1()
{
var sheet = _getApplication()?.ActiveSheet as Worksheet;
var cell = sheet?.Cells[1, 1] as Range;
return cell?.Value?.ToString() ?? "(empty)";
}
private void WriteCellA1(string value)
{
var sheet = _getApplication()?.ActiveSheet as Worksheet;
if (sheet != null)
sheet.Cells[1, 1] = value;
}
}

内嵌 DotNetBrowser 的自定义任务窗格。
8. Web UI
C# 端准备就绪后,最后一步是与加载项捆绑的网页。页面主体内容:
<div>
<label>Read value from Excel cell A1:</label>
<button id="btn-read">Read from Excel</button>
</div>
<div>
<label>Write a value to Excel cell A1:</label>
<input id="txt-write" type="text" />
<button id="btn-write">Write to Excel</button>
</div>
<div id="output">Press a button to interact with Excel.</div>
底部的内联 <script> 获取 window.excelBridge 并绑定按钮事件:
(function () {
var bridge = window.excelBridge;
var output = document.getElementById('output');
document.getElementById('btn-read')
.addEventListener('click', function () {
output.textContent = `A1: ${bridge.readFromExcel()}`;
});
document.getElementById('btn-write')
.addEventListener('click', function () {
var value = document.getElementById('txt-write').value;
bridge.writeToExcel(value);
output.textContent = `Written "${value}" to A1.`;
});
})();
运行效果
以管理员身份运行 Visual Studio 并在其中构建项目后,打开 Excel,加载项会自动加载。功能区中会出现一个"Sales Lead Add-in"选项卡。点击"Open Panel",任务窗格将打开并加载浏览器 UI。
DotNetBrowser 嵌入 Excel 的运行效果。
点击"Read from Excel",单元格 A1 的当前值会显示在按钮下方的输出区域。输入一个值并点击"Write to Excel",该值将直接写入单元格。
结论
最终效果是一个完整的 Chromium 浏览器在 Excel 内运行,并与活动工作表保持实时连接。网页可以简单也可以复杂——数据可视化图表、带验证的表单、React 应用——同时仍可通过 C# 直接访问电子表格数据。
完整源代码可在示例仓库中获取。
发送中。。。
您的个人 DotNetBrowser 试用密钥和快速入门指南将在几分钟内发送至您的电子邮箱。
