上文介绍了Excel中的UDF函数,本文介绍一下同样重要的RTD函数。从Excel 2002开始,Excel引入了一种新的查看和更新实时数据的机制,即real-time data简称RTD函数,他是一种Push-Pull的方式,及在需要更新数据的时候,RTD给Excel Push一个消息说要更新数据,Excel在收到消息后主动拉取Pull新的数据。RTD函数最开始的用途在于更新实时变化的数据,比如股票实时行情数据,实时天气预报数据,球队比赛得分数据等等。
在过去,要实现这些功能,需要依赖一些其他诸如Dynamic Data Exchange(DDE)技术来访问实时数据资源,但DDE和标准的Excel函数样式有很大的不同,并且并不是为Excel获取实时数据而设计的,缺乏健壮性,并且效率不高,RTD的引入解决了这些问题。
本文首先介绍RTD的一些常用的使用场景,RTD函数的基本结构,注意事项,最后演示如何通过RTD函数来实现从Google Financial API中获取实时行情数据。
RTD函数很有用,如果您遇到以下情况,那么您应当考虑使用RTD函数了:
Excel RTD函数是一个实现了IRtdServer接口的Com组件,Excel通过该Com组件与实时数据进行交互。要实现RTD 函数,必须要实现IRtdServer这一接口,该接口位于 Microsoft.Office.Interop.Excel命名空间中,跳转到定义,可以看到该接口的内部:
class="code">/// <summary> /// Represents an interface for a real-time data server. /// </summary> [Guid("EC0E6191-DB51-11D3-8F3E-00C04F3651B8")] [TypeLibType(4160)] public interface IRtdServer2 { /// <summary> /// Adds new topics from a real-time data server. The ConnectData method is called /// when a file is opened that contains real-time data functions or when a user /// types in a new formula which contains the RTD function. /// </summary> /// <param name="TopicID"> /// Required Integer. A unique value, assigned by Microsoft Excel, which identifies the topic.</param> /// <param name="Strings">Required Object. A single-dimensional array of strings identifying the topic.</param> /// <param name="GetNewValues"> Required Boolean. True to determine if new values are to be acquired.</param> /// <returns></returns> [DispId(11)] dynamic ConnectData(int TopicID, ref Array Strings, ref bool GetNewValues); /// <summary> /// Notifies a real-time data (RTD) server application that a topic is no longer in use. /// </summary> /// <param name="TopicID"> Required Integer. A unique value assigned to the topic assigned by Microsoft Excel.</param> [DispId(13)] void DisconnectData(int TopicID); [DispId(14)] int Heartbeat(); /// <summary> /// This method is called by Microsoft Excel to get new data. /// </summary> /// <param name="TopicCount">TopicCount: /// Required Integer. The RTD server must change the value of the TopicCount /// to the number of elements in the array returned.</param> /// <returns></returns> [DispId(12)] Array RefreshData(ref int TopicCount); /// <summary> /// The ServerStart method is called immediately after a real-time data server /// is instantiated. Negative value or zero indicates failure to start the server; /// positive value indicates success. /// </summary> /// <param name="CallbackObject">Required Microsoft.Office.Interop.Excel.IRTDUpdateEvent object. The callback object.</param> /// <returns></returns> [DispId(10)] int ServerStart(IRTDUpdateEvent CallbackObject); /// <summary> /// Terminates the connection to the real-time data server. /// </summary> [DispId(15)] void ServerTerminate(); }
其中ServerStart参数中有类型为IRTDUpdateEvent接口,该接口的实现为:
/// <summary> /// Represents real-time data update events. /// </summary> [Guid("A43788C1-D91B-11D3-8F39-00C04F3651B8")] [TypeLibType(4160)] public interface IRTDUpdateEvent { [DispId(11)] int HeartbeatInterval { get; set; } /// <summary> /// Instructs the real-time data server (RTD) to disconnect from the specified /// Microsoft.Office.Interop.Excel.IRTDUpdateEvent object. /// </summary> [DispId(12)] void Disconnect(); [DispId(10)] void UpdateNotify(); }
巨硬的接口注释写的很清楚,不过这里还是逐条解释一下,先看IRtdServer接口,这里依照一般的执行顺序讲解:
上面的接口介绍完了,现在介绍下IRTDUpdateEvent接口,该接口比较重要的一个方法是UpdateNotify方法。该方法想Excel发出一个通知,提示有新数据需要更新,这是Excel就会调用RefreshData方法,从中读取到更新后的数据。需要注意的是调用UpdateNofity方法必须要在Excel主线程中进行。
上面简单讲解了RTD函数的基本原理,接下来演示如何通过Excel RTD来实现从Google Fiancial API中获取实时行情并刷新数据,Google Financal API,提供了世界各大交易所的实时行情数据,其支持的市场及时效性在其官网上有说明,其使用方法可以参考这篇文章,这里不细谈。其主要思路如下,首先定义好请求的参数,我们的实时行情请求参数有两个,一个是股票代码,一个是指标的名称。因为我们请求的是http,而不是注册事件回调的方式,所以需要在RTD中使用timer控件去主动拉去请求,然后请求处理完成之后,将结果存储到对象中,然后调用UpdateNotify方法通知Excel来更新。RTD函数其实是一个注册为Com组件的类库,所以我们首先创建一个名为YYGoogleFinancialRTD的类库:
3.1 创建实体类对象
要将我们的请求的结果保存起来,必须创建一个实体类,来保存TopicID,请求的指标,股票代码,以及返回值的信息,这样再刷新的时候可以根据这些信息来刷新。所以添加一个RealStockData类。
class RealStockData { //该次请求的股票代码 public string StockCode{get;set;} //该次请求的股票名称 public string Index { get; set; } //该次请求Excel分配的TopicID public int TopicId { get; set; } //该次请求的返回值 public object Value{get;set;} }
3.2 创建RTD
如何创建RTD函数是本文的重点,这里我们先新建一个名为FinancialRtd的类,然后让他实现IRtdServer接口。利用VS的自动完成,实现其五个方法。和使用.NET 编写UDF 一样,我们需要在类名称上加一些自定义属性。这四个属性中,与UDF相比多了一个ProgID属性,该属性唯一标识改RTD函数。
[Guid("0A5F2820-C34E-4EF5-9110-F66B2A7796C0")] [ProgId("YYRTD.Stock")] [ClassInterface(ClassInterfaceType.AutoDual)] [ComVisible(true)] public class FinancialRtd : IRtdServer { private IRTDUpdateEvent xlRTDUpdate; private Timer tmrTimer; private List<RealStockData> stockDatas; private GoogleFinancial gfinancial = null; }
在实现五个方法之前,我们需要定义一些局部变量,第一个变量是xlRTDUpdate对象,该对象用来保存ServerStart中传进来的对象的引用,以方便后面调用该对象的UpdateNotify方法。Timer控件用来定时的从http接口中获取实时行情数据,List<RealStockData>对象用来保存所有的请求及其返回值。Gfinancial对象用来获取从Google Financial API中获取的实时行情。由于当前不再交易时间,所以我在收盘价格的基础上加了一个Random来演示实时变化。这个在下载的代码中您可以看到。这里不多讲。定义好这些变量之后我们就可以开始编写代码了。
第一个要实现的方法是 ServerStart方法。在该方法中,我们将CallbackObject对象保存到之前定义好的局部变量中,供日后调用该对象的UpdateNotify方法。然后,我们初始化了Timer对象,将其设置为2s去获取一次实时行情数据。最后返回1表示RTD服务正常开启。timer的Elapsed方法我们后面介绍。
public int ServerStart(IRTDUpdateEvent CallbackObject) { xlRTDUpdate = CallbackObject; gfinancial = new GoogleFinancial(); tmrTimer = new Timer(); tmrTimer.Interval = 2000; tmrTimer.Elapsed += tmrTimer_Elapsed; return 1; }
第二个要实现的重要的方法是ConnectData方法:
public dynamic ConnectData(int TopicID, ref Array Strings, ref bool GetNewValues) { //Retrive new values from RTD server when connected. GetNewValues = true; //Get field name from Excel. string strStockCode = Strings.GetValue(0).ToString().ToLower(); string strIndex = Strings.GetValue(1).ToString(); try { if (stockDatas == null) { stockDatas = new List<RealStockData>(); } //Get real-time data from data source. RealStockData temp = new RealStockData(); temp.StockCode = strStockCode; temp.Index = strIndex; gfinancial.GetRealStock(temp); if (temp.Value != null) { stockDatas.Add(temp); } } catch { return "ERROR IN QUOTE."; } //Make sure that the timer is started. if (!tmrTimer.Enabled) { tmrTimer.Start(); } for (int i = 0; i < stockDatas.Count; i++) { //Match the topic value if (stockDatas[i].StockCode.Equals(strStockCode, StringComparison.OrdinalIgnoreCase) && stockDatas[i].Index.ToString().Equals(strIndex, StringComparison.OrdinalIgnoreCase)) { if (stockDatas[i].TopicId == -1) { stockDatas[i].TopicId = TopicID; } return stockDatas[i].Value; } } return "Unrecognized requested"; }
在该方法中,我们首先解析传进来的参数,根据之前的约定,第一个参数为股票代码,第二个参数为指标名称。然后我们实例化了一个RealStockData对象,并给该对象的相关StockCode和Index赋值,然后去为该请求去请求一次实时行情,并将值存储到对象的Value属性中。然后将该请求加载到List集合对象中。最后循环判断是否已经存在,如果已经存在,则直接返回值,否则将该次请求的Excel为其分配的TopicID存到该次对象的TopicId对象中。以便后面刷新时使用。最后如果输入的参数条件不满足要求,提示用户请求格式不正确。每一个单元格的请求仅执行该方法一次。后面就通过刷新机制实现更新了。
为了连续性,现在介绍timer的Elapse方法,该方法的实现如下:
private void tmrTimer_Elapsed(object sender, ElapsedEventArgs e) { gfinancial.GetRealStock(stockDatas); xlRTDUpdate.UpdateNotify(); }
该方法很简单,第一句去讲我们之前保存的List集合的所有请求那过去请求实时行情,并将其返回值,保存到各元素的Value中,然后调用UpdateNotify方法通知Excel新的数据值已经获取到了,新数据的值就存在stockDatas的各元素的Value属性中。这时Excel收到UpdateNotify方法之后,就回去调用RefreshData方法,该方法的实现如下:
public Array RefreshData(ref int TopicCount) { object[,] rets = new object[2, stockDatas.Count]; int counter = 0; foreach (RealStockData data in stockDatas) { if (data.TopicId != -1) { rets[0, counter] = data.TopicId; rets[1, counter] = data.Value; } counter++; } TopicCount = stockDatas.Count; return rets; }
该方法很简单,首先我们创建了一个二维数组,第二维的大小即为所有有效请求的个数,这里几位stockDatas元素的个数。
然后遍历所有的请求,填充该二维数组,第一维值为TopicID,Excel会通过该ID去更新对应的单元格,第二维为单元格的值,即将该值填充到对应的TopicID中。另外我们还要通知Excel我们要请求刷新的单元格的个数,这个个数当然就是所有请求的个数啦。最后我们返回这个二位数组。
至此,最重要的几个方法介绍完了。
和ConnectData方法对应的DisconnectData方法在我们在Excel中删除我们之前输入的RTD函数时触发,方法实现如下,操作就是从我们的所有请求集合中移除该TopicID对应的请求。
public void DisconnectData(int TopicID) { for (int i = stockDatas.Count - 1; i > 0; i--) { if (stockDatas[i].TopicId == TopicID) { stockDatas.RemoveAt(i); } } if ((stockDatas == null || stockDatas.Count == 0) && tmrTimer.Enabled) { tmrTimer.Stop(); } }
Heartbeat方法仅仅返回1,表示我们的RTD还在运行中。
public int Heartbeat() { return 1; }
最后和ServerStart方法对应的方法ServerTerminate在关闭该RTD工作表时触发,该方法的主要用来释放资源,其实现如下:
public void ServerTerminate() { //Clear the RTDUpdateEvent reference. xlRTDUpdate = null; //Make sure the timer is stopped. if (tmrTimer.Enabled) { tmrTimer.Stop(); } tmrTimer.Elapsed -= new ElapsedEventHandler(tmrTimer_Elapsed); tmrTimer.Dispose(); }
至此,我们的函数编写完了。
完成之后,和UDF函数一样,我们需要将该类库注册为Com组件,在Visual Studio编译时勾选注册为Com组件即可,提醒您的是,在Windows 7 及以上系统上,注册Com组件涉及到要往注册表些东西,所以需要当前的Visual Studio以管理员权限运行。
编译通过之后,我们可以直接打开Excel输入RTD函数了。
在Excel 中使用RTD非常简单,Excel 提供了一个新的工作表函数 RTD,此函数允许通过调用组件对象模型 (COM) 自动化服务器来实现实时数据检索。RTD 工作表函数使用以下语法:
第一个变量 ProgID 表示Real-Time Data 服务器(RTD Server)的编程标识符 (ProgID),我们之前在RTD类的自定义属性中指定了ProgID,这里就可以用“YYRTD.Stock”了。Server 变量指示运行RTD Server的计算机的名称;如果RTD Server在本地运行,则可以将此变量设置为空字符串或将其忽略。后面的变量只表示发送到RTD Server的参数;这些参数的每个唯一组合都表示一个“主题”(topic),每个“主题”有一个关联的“主题 ID”(topic id)。这些参数区分大小写。例如,以下内容表示我们请求600000这只股票的最新价,最高价和最低价
我录制了一个动画,效果如下图,两秒钟刷新一次,由于当前不是交易时间,所以通过Google API获取的值是收盘时的行情,为了掩饰效果,我在原值的基础上加了一个随机变量以演示变化,如果在交易时间的话,您可以看到真实的股票行情变化的,相关代码您可以在附件中进行修改。
本文介绍了Excel中一类非常重要的函数,及RTD函数,他是Excel提供的一种Push-Pull机制的数据更新机制,在一些对数据实时性要求较高的场合,比如直播比赛得分,实时天气预报,交易所实时行情等方面用的很多。并且通过RTD的这种机制我们可以在此基础上实现异步的UDF函数,即当请求发进来的时候,我们暂不处理,记下TopicID,然后放到处理队列中,待处理完成后,调用UpdateNotify方法,然后刷新已经处理完的TopicID的单元格。下文将会介绍Excel中异步自定义函数,异步的UDF函数能够极大地提高Excel插件的用户体验,能够提高系统的可扩展性和稳定性。
本文所有的代码点击此处下载,希望本文对您了解Excel中的RTD函数有所帮助。