如何使用 RtdServer 在 C# 中创建实时 Excel 自动化加载项?

Posted

技术标签:

【中文标题】如何使用 RtdServer 在 C# 中创建实时 Excel 自动化加载项?【英文标题】:How do I create a real-time Excel automation add-in in C# using RtdServer? 【发布时间】:2011-03-22 20:44:19 【问题描述】:

我的任务是使用 RtdServer 在 C# 中编写一个实时 Excel 自动化插件。我非常依赖在 Stack Overflow 中学到的知识。为了表达我的谢意,我决定写一个如何将我所学的知识联系在一起的文档。 Kenny Kerr 的Excel RTD Servers: Minimal C# Implementation 文章帮助我入门。我发现 Mike Rosenblum 和 Govert 的 cmets 特别有用。

【问题讨论】:

辛苦了,我记得我第一次尝试让其中一个工作的人没有任何例子。我相信这会对某人有所帮助:) 我会做两件事。 1) 您可以创建一个 xla,它将对 RTD 的调用封装在一个函数中,该函数将为您提供更清晰、更清晰的语法和 Excel 端的错误处理。其次,如果您执行任何 VSTO 操作,以及使用 .net 编码的 IRTDServer 进行较小扩展的操作,请使用 Excel 中的选项对话框来模拟 Excel 被阻止。你的代码必须处理它。 非常正确。来自 Excel (RefreshData) 的拉取请求可能会在您调用 UpdateNotify 后立即发出,但有很多事情可能会无限期地延迟它(对话框、输入公式等)。你不能永远排队更新。 我也同意,一般来说你不想强迫人类直接调用 RTD 函数。在 VBA 中创建包装函数很容易。我不确定如何在 C# 中创建包装函数,并发布了与 here 相关的问题 @Frank - 我不确定如何在 c# 中编写包装器,但如果您要在 c++ 中编写 UDF 包装器 - 您可以使用 xlfRtd(excel 2007+ c api 的一部分)来将调用包装到您的 rtd 服务器。 【参考方案1】:

(作为下面描述的方法的替代方法,您应该考虑使用Excel-DNA。Excel-DNA 允许您构建免注册 RTD 服务器。COM 注册需要管理权限,这可能会导致安装问题。话虽如此,下面的代码工作正常。)

使用 RtdServer 在 C# 中创建实时 Excel 自动化插件:

1)在Visual Studio中创建一个C#类库项目,输入以下内容:

using System;
using System.Threading;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Microsoft.Office.Interop.Excel;

namespace ***

    public class Countdown
    
        public int CurrentValue  get; set; 
    

    [Guid("EBD9B4A9-3E17-45F0-A1C9-E134043923D3")]
    [ProgId("***.RtdServer.ProgId")]
    public class RtdServer : IRtdServer
    
        private readonly Dictionary<int, Countdown> _topics = new Dictionary<int, Countdown>();
        private Timer _timer;

        public int ServerStart(IRTDUpdateEvent rtdUpdateEvent)
        
            _timer = new Timer(delegate  rtdUpdateEvent.UpdateNotify(); , null, TimeSpan.Zero, TimeSpan.FromSeconds(5));
            return 1;
        

        public object ConnectData(int topicId, ref Array strings, ref bool getNewValues)
        
            var start = Convert.ToInt32(strings.GetValue(0).ToString());
            getNewValues = true;

            _topics[topicId] = new Countdown  CurrentValue = start ;

            return start;
        

        public Array RefreshData(ref int topicCount)
        
            var data = new object[2, _topics.Count];
            var index = 0;

            foreach (var entry in _topics)
            
                --entry.Value.CurrentValue;
                data[0, index] = entry.Key;
                data[1, index] = entry.Value.CurrentValue;
                ++index;
            

            topicCount = _topics.Count;

            return data;
        

        public void DisconnectData(int topicId)
        
            _topics.Remove(topicId);
        

        public int Heartbeat()  return 1; 

        public void ServerTerminate()  _timer.Dispose(); 

        [ComRegisterFunctionAttribute]
        public static void RegisterFunction(Type t)
        
            Microsoft.Win32.Registry.ClassesRoot.CreateSubKey(@"CLSID\" + t.GUID.ToString().ToUpper() + @"\Programmable");
            var key = Microsoft.Win32.Registry.ClassesRoot.OpenSubKey(@"CLSID\" + t.GUID.ToString().ToUpper() + @"\InprocServer32", true);
            if (key != null)
                key.SetValue("", System.Environment.SystemDirectory + @"\mscoree.dll", Microsoft.Win32.RegistryValueKind.String);
        

        [ComUnregisterFunctionAttribute]
        public static void UnregisterFunction(Type t)
        
            Microsoft.Win32.Registry.ClassesRoot.DeleteSubKey(@"CLSID\" + t.GUID.ToString().ToUpper() + @"\Programmable");
        
    

2) 右键单击​​项目并添加 > 新项目... > 安装程序类。切换到代码视图并输入以下内容:

using System.Collections;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace ***

    [RunInstaller(true)]
    public partial class RtdServerInstaller : System.Configuration.Install.Installer
    
        public RtdServerInstaller()
        
            InitializeComponent();
        

        [System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.Demand)]
        public override void Commit(IDictionary savedState)
        
            base.Commit(savedState);

            var registrationServices = new RegistrationServices();
            if (registrationServices.RegisterAssembly(GetType().Assembly, AssemblyRegistrationFlags.SetCodeBase))
                Trace.TraceInformation("Types registered successfully");
            else
                Trace.TraceError("Unable to register types");
        

        [System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.Demand)]
        public override void Install(IDictionary stateSaver)
        
            base.Install(stateSaver);

            var registrationServices = new RegistrationServices();
            if (registrationServices.RegisterAssembly(GetType().Assembly, AssemblyRegistrationFlags.SetCodeBase))
                Trace.TraceInformation("Types registered successfully");
            else
                Trace.TraceError("Unable to register types");
        

        public override void Uninstall(IDictionary savedState)
        
            var registrationServices = new RegistrationServices();
            if (registrationServices.UnregisterAssembly(GetType().Assembly))
                Trace.TraceInformation("Types unregistered successfully");
            else
                Trace.TraceError("Unable to unregister types");

            base.Uninstall(savedState);
        
    

3) 右键单击​​项目属性并检查以下内容:应用程序 > 程序集信息... > 使程序集 COM 可见并构建 > 注册 COM 互操作

3.1) 右键单击​​项目添加引用... > .NET 选项卡 > Microsoft.Office.Interop.Excel

4) 构建解决方案 (F6)

5) 运行 Excel。转到 Excel 选项 > 加载项 > 管理 Excel 加载项 > 自动化并选择“***.RtdServer”

6) 在单元格中输入“=RTD("***.RtdServer.ProgId",,200)"。

7) 交叉手指,希望它有效!

【讨论】:

Excel如何找到带有ProgId的文件所在的位置?我正在尝试将我的 Excel 插件移动到另一台机器上,但我无法在 Excel 上设置它 这个例子中Trace写到什么路径?我找不到它。 Sysytem.Diagnostics.Trace 与 System.Console 类似,但更灵活。它允许您执行诸如添加跟踪级别和添加多个侦听器之类的操作。 RtdServer的GUID属性有什么意义?我需要指定它吗?我从哪里得到它? COM 注册需要 GUID。您可以使用工具 > 在 Visual Studio 中创建 GUID 生成 GUID。【参考方案2】:

从计时器线程调用 UpdateNotify 最终会导致奇怪的错误或与 Excel 断开连接。

UpdateNotify() 方法只能从调用 ServerStart() 的同一线程中调用。 RTDServer帮助中没有记录,但它是COM的限制。

修复很简单。使用 DispatcherSynchronizationContext 捕获调用 ServerStart 的线程并使用它来调度对 UpdateNotify 的调用:

public class RtdServer : IRtdServer

    private IRTDUpdateEvent _rtdUpdateEvent;
    private SynchronizationContext synchronizationContext;

    public int ServerStart( IRTDUpdateEvent rtdUpdateEvent )
    
        this._rtdUpdateEvent = rtdUpdateEvent;
        synchronizationContext = new DispatcherSynchronizationContext();
        _timer = new Timer(delegate  PostUpdateNotify(); , null, TimeSpan.Zero, TimeSpan.FromSeconds(5));
        return 1;
    


    // Notify Excel of updated results
    private void PostUpdateNotify()
    
        // Must only call rtdUpdateEvent.UpdateNotify() from the thread that calls ServerStart.
        // Use synchronizationContext which captures the thread dispatcher.
        synchronizationContext.Post( delegate(object state)  _rtdUpdateEvent.UpdateNotify(); , null);
    

    // etc
 // end of class

【讨论】:

非常好(另一个选择是有一个线程安全的集合来读取/写入)。但是,从纯 C# 类库中,定位 DispatcherSynchronizationContext 需要哪些额外的引用?我找不到它(我怀疑这是 WPF 特有的东西)。【参考方案3】:

按照 RTD 服务器的前两个答案对我有用。但是,我在运行 Excel x64 的 x64 机器上遇到了问题。就我而言,在我将项目的“目标平台”切换到 x64 之前,Excel 始终显示#N/A。

【讨论】:

以上是关于如何使用 RtdServer 在 C# 中创建实时 Excel 自动化加载项?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 C# 在 Windows 窗体中创建 F1 帮助

如何在 C# 中创建 COM 可见类?

如何在 C# 中创建异步方法?

如何使用 C# 在 Access 中创建、更新、删除数据?

如何使用 C# 在 SQL Server 数据库中创建视图?

如何在 C# 中创建 Microsoft Access 数据库? [复制]