Excel 支持 COM 加载项,可扩展其界面并以编程方式与工作簿交互。加载项可以添加功能区按钮、处理电子表格事件,并在任务窗格(位于数据旁边的侧面板)中显示自定义 UI。

借助 DotNetBrowser,任务窗格可以托管完整的 Chromium 浏览器,其中的网页可以直接读写电子表格数据。

本文将逐步讲解如何构建一个实现上述功能的 COM 加载项。完整源代码可在 GitHub 仓库 中获取。

如果您正在探索相关的 Office 集成场景,以下两篇教程可能对您有所帮助。VSTO 教程 介绍如何将 DotNetBrowser 嵌入 Microsoft Outlook 加载项,COM/ActiveX 教程 则说明了 COM 客户端和 ActiveX 容器的通用封装方法。

架构概览 

我们要构建的加载项是一个 COM 类库,Excel 通过 Windows 注册表发现并在启动时加载它。它通过一个按钮扩展功能区;点击该按钮可打开任务窗格并在其中启动 DotNetBrowser 浏览器视图。

Excel 中使用 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简短描述
LoadBehavior3,在启动时加载并立即连接
ProgIdExcelComAddin.Connect
CommandLineSafe0,在非交互模式下不安全加载

要在 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 功能区中的自定义选项卡

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 的自定义任务窗格

内嵌 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# 直接访问电子表格数据。

完整源代码可在示例仓库中获取。

Spinner

发送中。。。

抱歉,发送中断

请再次尝试,如果问题仍然存在,请联系我们 info@teamdev.com.

阅读并同意条款以继续。

您的个人 DotNetBrowser 试用密钥和快速入门指南将在几分钟内发送至您的电子邮箱。