从Excel 2002开始,微软提供了一种叫做Excel RTD(real-time data)的技术,使用该技术可以在Excel中实时查看和更新数据。RTD采用所谓的“推-拉”相结合的技术,使得其在实时获取和更新不断变化的数据(例如股票、汇率、天气)的性能方面,相比较之前的DDE更加稳健和快速。在MSDN的Real-Time Data FAQ上有其性能描述(http://msdn.microsoft.com/en-us/library/aa140060(v=office.10).aspx#odc_xlrtdfaq_whatisrtd),据说在一台配置为 Pentium III 500 MHz 的CPU和 128 MB 内存的电脑上,RTD可以在一秒内对20000个独立的主题(topic)更新三次,对于单独一个主题的更新频率可达1秒200次。
在Excel 中使用RTD非常简单,Excel 提供了一个新的工作表函数 RTD,此函数允许通过调用组件对象模型 (COM) 自动化服务器来实现实时数据检索。RTD 工作表函数使用以下语法:
"=RTD(ProgID, Server, String 1, String 2, ... String n)"
第一个变量 ProgID 表示Real-Time Data 服务器(RTD Server)的编程标识符 (ProgID)。Server 变量指示运行RTD Server的计算机的名称;如果RTD Server在本地运行,则可以将此变量设置为空字符串或将其忽略。其他变量只表示发送到RTD Server的参数;这些参数的每个唯一组合都表示一个“主题”(topic),每个“主题”有一个关联的“主题 ID”(topic id)。这些参数区分大小写。例如,以下内容演示将生成三个不同主题ID的RTD Server调用:
=RTD("ExcelRTD.RTDFunctions",,"AAA", "10")
=RTD("ExcelRTD.RTDFunctions",,"AAA", "5")
=RTD("ExcelRTD.RTDFunctions",,"aaa", "5")
要使用 Excel 的 RTD 函数,必须注册一个实现 IRTDServer 接口的COM组件。实现这个接口的COM组件就是所谓的RTD Server。IRTDServer具有以下成员:
ServerStart(CallbackObject)
CallbackObject 是一个IRTDUpdateEvent类型的参数,它有一个UpdateNotify方法,用于通知Excel有更新的数据可用(push)。这样Excel就会通过调用RefreshData方法来刷新所有的主题(pull)。当 Excel 请求RTD Server的第一个 RTD 主题时调用ServerStart方法,该方法会在成功时返回 1,并在失败时返回负值或 0。这个方法在随后应用其他RTD函数时不会再次被调用。
ConnectData(TopicID, Strings, GetNewValues)
其中,TopcID 唯一标识这个函数在Excel中的一个应用,即使复制多份到不同的单元格,对于Excel来讲,也只是对应一个主题。这个 topicID 由Excel返回,我们需要将其记录下来,以便为其提供更新的数据。Strings 是一个System.Array,用于接收RTD函数传入的参数(String 1...String n),这是一个引用类型的参数。GetNewValues 用于确定是否总是获取最新数据,如果这个参数传入true,则每次保存Excel文档以后,再次重新打开时,看到的不一定是上次保存时的数据,而是最新的实时数据,这也是一个引用类型的参数。
每当一个新的主题(Topic)被应用到Excel,ConnectData都会被调用。在这里,需要保存传入的新的TopicID和查询参数以供之后更新数据使用。为此,需要定义好自己的数据结构。
DisconnectData(TopicID)
与ConnectData一样,TopcID 唯一标识这个函数在Excel中的一个应用。当我们从Excel中移除一个主题(删除所有采用相同参数的RTD函数)之后,DisconnectData将被调用,在这里,可以释放对这个主题的监控,并不再为其获取新数据。
Heartbeat
确定RTD Server是不是依然可用,0和负数代表不可用,1代表可用。Excel会调用此方法确定服务是否断连。
RefreshData(TopicCount)
TopicCount表示要更新的主题数量,这是一个引用类型的参数,用于返回给Excel。我们可以定义一个时钟,用于定时向数据源获取数据,这样,在时钟的Elapsed事件中,获取最新数据,并调用xlRTDUpdate成员的UpdateNotify方法以通知Excel,新的数据准备完毕。这样Excel就会调用RefreshData方法,来对工作簿中的数据进行更新。
ServerTerminate
当Excel不再需要从RTD Server获取实时数据时被调用。在这里,可以执行一些清理,例如清除缓存,关闭时钟等等。至此,一个RTD Server的生命周期就结束了。
演练
创建一个项目ExcelRTD,添加Microsoft.Office.Interop.Excel引用。创建一个类MarketData.cs,这个类继承IRtdServer接口,以实现一个RTD Server。新建另一个类DataCollector.cs用于从数据源获取数据。DataCollector有一个方法public DataTable GetMarketData(string keyField, object keyValue)将会在MarketData.cs中被使用。为了图简便,我就贴出MarketData.cs的全部代码。
Code
1using System;
2using System.Collections.Generic;
3using System.Linq;
4using System.Text;
5
6using System.Collections;
7using System.Configuration;
8using System.Timers;
9using System.Runtime.InteropServices;
10using Microsoft.Office.Interop.Excel;
11
12namespace ExcelRTD
13{
14 [ComVisible(true),ProgId("ExcelRTD.MarketData")]
15 public class MarketDataServer : IRtdServer
16 {
17 private IRTDUpdateEvent xlRTDUpdate;
18 private Timer tmrTimer;
19 private List<MarketData> marketDatas;
20
21 //Lets Excel know that our RTD server is still "alive".
22 public int Heartbeat()
23 {
24 return 1;
25 }
26
27 //Initialize RTD server.
28 public int ServerStart(Microsoft.Office.Interop.Excel.IRTDUpdateEvent CallbackObject)
29 {
30 //Hold a reference to the callback object.
31 xlRTDUpdate = CallbackObject;
32
33 //2000 millisecond by default.
34 int intv = 2000;
35
36 //'Create the time with a 5000 millisecond interval.
37 tmrTimer = new Timer(intv);
38 tmrTimer.AutoReset = true;
39
40 tmrTimer.Elapsed += new ElapsedEventHandler(tmrTimer_Elapsed);
41
42 return 1;
43 }
44
45 //Terminate RTD server.
46 public void ServerTerminate()
47 {
48 //Clear the RTDUpdateEvent reference.
49 xlRTDUpdate = null;
50
51 //Make sure the timer is stopped.
52 if (tmrTimer.Enabled)
53 {
54 tmrTimer.Stop();
55 }
56
57 tmrTimer.Elapsed -= new ElapsedEventHandler(tmrTimer_Elapsed);
58 tmrTimer.Dispose();
59 }
60
61
62 public object ConnectData(int TopicID, ref System.Array Strings, ref bool GetNewValues)
63 {
64 //Retrive new values from RTD server when connected.
65 GetNewValues = true;
66
67 //Get field name from Excel.
68 string strFieldName = Strings.GetValue(0).ToString().ToLower();
69 string strKeyValue = Strings.GetValue(1).ToString();
70
71 try
72 {
73 if (marketDatas == null)
74 {
75 marketDatas = new List<MarketData>();
76 }
77
78 //Get real-time data from data source.
79 DataCollector dc = new DataCollector();
80 System.Data.DataTable dt = null;
81
82 MarketData temp = new MarketData();
83
84 temp.KeyField = "stockcode";
85 temp.KeyValue = strKeyValue;
86 temp.FieldName = strFieldName;
87
88 dt = dc.GetMarketData(temp.KeyField, temp.KeyValue);
89
90 if (dt != null && dt.Rows.Count > 0)
91 {
92 temp.FieldValue = dt.Rows[0][temp.FieldName];
93
94 marketDatas.Add(temp);
95 }
96
97 }
98 catch
99 {
100 return "ERROR IN QUOTE.";
101 }
102
103 //Make sure that the timer is started.
104 if (!tmrTimer.Enabled)
105 {
106 tmrTimer.Start();
107 }
108
109 for (int i = 0; i < marketDatas.Count; i++)
110 {
111 //Match the topic value
112 if (marketDatas[i].FieldName.Equals(strFieldName, StringComparison.OrdinalIgnoreCase) && marketDatas[i].KeyValue.ToString().Equals(strKeyValue, StringComparison.OrdinalIgnoreCase))
113 {
114 if (marketDatas[i].TopicID == -1)
115 {
116 marketDatas[i].TopicID = TopicID;
117 }
118
119 return marketDatas[i].FieldValue;
120 }
121 }
122
123 return "Unrecognized value requested";
124 }
125
126 public void DisconnectData(int TopicID)
127 {
128 for (int i = marketDatas.Count - 1; i > 0; i--)
129 {
130 if (marketDatas[i].TopicID == TopicID)
131 {
132 marketDatas.RemoveAt(i);
133 }
134 }
135
136 if ((marketDatas == null || marketDatas.Count == 0) && tmrTimer.Enabled)
137 {
138 tmrTimer.Stop();
139 }
140 }
141
142
143 public void tmrTimer_Elapsed(object sender, ElapsedEventArgs e)
144 {
145 try
146 {
147 for (int i = 0; i < marketDatas.Count; i++)
148 {
149 DataCollector dc = new DataCollector();
150 System.Data.DataTable dt = dc.GetMarketData(marketDatas[i].KeyField, marketDatas[i].KeyValue);
151
152 if (dt != null && dt.Rows.Count > 0)
153 {
154 marketDatas[i].FieldValue = dt.Rows[0][marketDatas[i].FieldName];
155 }
156 }
157
158 //Tell Excel that we have updates.
159 xlRTDUpdate.UpdateNotify();
160 }
161 catch(Exception ex)
162 {
163 string x = ex.ToString();
164 }
165 }
166
167 //Pull new values from the real-time data server to Excel.
168 public System.Array RefreshData(ref int TopicCount)
169 {
170 object[,] rets = new object[2, marketDatas.Count];
171 int counter = 0, sum = 0;
172
173 foreach (MarketData data in marketDatas)
174 {
175 if (data.TopicID != -1)
176 {
177 rets[0, counter] = data.TopicID;
178 rets[1, counter] = data.FieldValue;
179
180 sum++;
181 }
182
183 counter++;
184 }
185
186 TopicCount = marketDatas.Count;
187
188 return rets;
189 }
190
191
192 private class MarketData
193 {
194 private string fieldName;
195 private object fieldValue;
196 private string keyField;
197 private object keyValue;
198 private int topicID;
199
200 public MarketData()
201 {
202 topicID = -1;
203
204 keyValue = null;
205 fieldValue = null;
206 }
207
208 public string FieldName
209 {
210 get
211 {
212 return fieldName;
213 }
214 set
215 {
216 fieldName = value;
217 }
218 }
219
220 public object FieldValue
221 {
222 get
223 {
224 return fieldValue;
225 }
226 set
227 {
228 fieldValue = value;
229 }
230 }
231
232 public string KeyField
233 {
234 get
235 {
236 return keyField;
237 }
238 set
239 {
240 keyField = value;
241 }
242 }
243
244 public object KeyValue
245 {
246 get
247 {
248 return keyValue;
249 }
250 set
251 {
252 keyValue = value;
253 }
254 }
255
256 public int TopicID
257 {
258 get
259 {
260 return topicID;
261 }
262 set
263 {
264 topicID = value;
265 }
266 }
267 }
268 }
269}
270
使用时,需要注册COM。这只是一个简单的示例。
Step by step可以参见以下链接:
http://msdn.microsoft.com/en-us/library/aa140061(office.10).aspx
http://support.microsoft.com/kb/285339/zh-cn
MSDN上的文章虽然是针对Excel 2002,但是2003和2007同样支持。此外,从外部数据源获取Excel的方法有很多种,这只是其中一种而已,它并不能适应所有的情况。