excelRTDServer.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. """Excel IRTDServer implementation.
  2. This module is a functional example of how to implement the IRTDServer interface
  3. in python, using the pywin32 extensions. Further details, about this interface
  4. and it can be found at:
  5. http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnexcl2k2/html/odc_xlrtdfaq.asp
  6. """
  7. # Copyright (c) 2003-2004 by Chris Nilsson <chris@slort.org>
  8. #
  9. # By obtaining, using, and/or copying this software and/or its
  10. # associated documentation, you agree that you have read, understood,
  11. # and will comply with the following terms and conditions:
  12. #
  13. # Permission to use, copy, modify, and distribute this software and
  14. # its associated documentation for any purpose and without fee is
  15. # hereby granted, provided that the above copyright notice appears in
  16. # all copies, and that both that copyright notice and this permission
  17. # notice appear in supporting documentation, and that the name of
  18. # Christopher Nilsson (the author) not be used in advertising or publicity
  19. # pertaining to distribution of the software without specific, written
  20. # prior permission.
  21. #
  22. # THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
  23. # TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT-
  24. # ABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR
  25. # BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
  26. # DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
  27. # WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
  28. # ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
  29. # OF THIS SOFTWARE.
  30. import pythoncom
  31. import win32com.client
  32. from win32com import universal
  33. from win32com.client import gencache
  34. from win32com.server.exception import COMException
  35. import threading
  36. import datetime # For the example classes...
  37. # Typelib info for version 10 - aka Excel XP.
  38. # This is the minimum version of excel that we can work with as this is when
  39. # Microsoft introduced these interfaces.
  40. EXCEL_TLB_GUID = '{00020813-0000-0000-C000-000000000046}'
  41. EXCEL_TLB_LCID = 0
  42. EXCEL_TLB_MAJOR = 1
  43. EXCEL_TLB_MINOR = 4
  44. # Import the excel typelib to make sure we've got early-binding going on.
  45. # The "ByRef" parameters we use later won't work without this.
  46. gencache.EnsureModule(EXCEL_TLB_GUID, EXCEL_TLB_LCID, \
  47. EXCEL_TLB_MAJOR, EXCEL_TLB_MINOR)
  48. # Tell pywin to import these extra interfaces.
  49. # --
  50. # QUESTION: Why? The interfaces seem to descend from IDispatch, so
  51. # I'd have thought, for example, calling callback.UpdateNotify() (on the
  52. # IRTDUpdateEvent callback excel gives us) would work without molestation.
  53. # But the callback needs to be cast to a "real" IRTDUpdateEvent type. Hmm...
  54. # This is where my small knowledge of the pywin framework / COM gets hazy.
  55. # --
  56. # Again, we feed in the Excel typelib as the source of these interfaces.
  57. universal.RegisterInterfaces(EXCEL_TLB_GUID,
  58. EXCEL_TLB_LCID, EXCEL_TLB_MAJOR, EXCEL_TLB_MINOR,
  59. ['IRtdServer','IRTDUpdateEvent'])
  60. class ExcelRTDServer(object):
  61. """Base RTDServer class.
  62. Provides most of the features needed to implement the IRtdServer interface.
  63. Manages topic adding, removal, and packing up the values for excel.
  64. Shouldn't be instanciated directly.
  65. Instead, descendant classes should override the CreateTopic() method.
  66. Topic objects only need to provide a GetValue() function to play nice here.
  67. The values given need to be atomic (eg. string, int, float... etc).
  68. Also note: nothing has been done within this class to ensure that we get
  69. time to check our topics for updates. I've left that up to the subclass
  70. since the ways, and needs, of refreshing your topics will vary greatly. For
  71. example, the sample implementation uses a timer thread to wake itself up.
  72. Whichever way you choose to do it, your class needs to be able to wake up
  73. occaisionally, since excel will never call your class without being asked to
  74. first.
  75. Excel will communicate with our object in this order:
  76. 1. Excel instanciates our object and calls ServerStart, providing us with
  77. an IRTDUpdateEvent callback object.
  78. 2. Excel calls ConnectData when it wants to subscribe to a new "topic".
  79. 3. When we have new data to provide, we call the UpdateNotify method of the
  80. callback object we were given.
  81. 4. Excel calls our RefreshData method, and receives a 2d SafeArray (row-major)
  82. containing the Topic ids in the 1st dim, and the topic values in the
  83. 2nd dim.
  84. 5. When not needed anymore, Excel will call our DisconnectData to
  85. unsubscribe from a topic.
  86. 6. When there are no more topics left, Excel will call our ServerTerminate
  87. method to kill us.
  88. Throughout, at undetermined periods, Excel will call our Heartbeat
  89. method to see if we're still alive. It must return a non-zero value, or
  90. we'll be killed.
  91. NOTE: By default, excel will at most call RefreshData once every 2 seconds.
  92. This is a setting that needs to be changed excel-side. To change this,
  93. you can set the throttle interval like this in the excel VBA object model:
  94. Application.RTD.ThrottleInterval = 1000 ' milliseconds
  95. """
  96. _com_interfaces_ = ['IRtdServer']
  97. _public_methods_ = ['ConnectData','DisconnectData','Heartbeat',
  98. 'RefreshData','ServerStart','ServerTerminate']
  99. _reg_clsctx_ = pythoncom.CLSCTX_INPROC_SERVER
  100. #_reg_clsid_ = "# subclass must provide this class attribute"
  101. #_reg_desc_ = "# subclass should provide this description"
  102. #_reg_progid_ = "# subclass must provide this class attribute"
  103. ALIVE = 1
  104. NOT_ALIVE = 0
  105. def __init__(self):
  106. """Constructor"""
  107. super(ExcelRTDServer, self).__init__()
  108. self.IsAlive = self.ALIVE
  109. self.__callback = None
  110. self.topics = {}
  111. def SignalExcel(self):
  112. """Use the callback we were given to tell excel new data is available."""
  113. if self.__callback is None:
  114. raise COMException(desc="Callback excel provided is Null")
  115. self.__callback.UpdateNotify()
  116. def ConnectData(self, TopicID, Strings, GetNewValues):
  117. """Creates a new topic out of the Strings excel gives us."""
  118. try:
  119. self.topics[TopicID] = self.CreateTopic(Strings)
  120. except Exception as why:
  121. raise COMException(desc=str(why))
  122. GetNewValues = True
  123. result = self.topics[TopicID]
  124. if result is None:
  125. result = "# %s: Waiting for update" % self.__class__.__name__
  126. else:
  127. result = result.GetValue()
  128. # fire out internal event...
  129. self.OnConnectData(TopicID)
  130. # GetNewValues as per interface is ByRef, so we need to pass it back too.
  131. return result, GetNewValues
  132. def DisconnectData(self, TopicID):
  133. """Deletes the given topic."""
  134. self.OnDisconnectData(TopicID)
  135. if TopicID in self.topics:
  136. self.topics[TopicID] = None
  137. del self.topics[TopicID]
  138. def Heartbeat(self):
  139. """Called by excel to see if we're still here."""
  140. return self.IsAlive
  141. def RefreshData(self, TopicCount):
  142. """Packs up the topic values. Called by excel when it's ready for an update.
  143. Needs to:
  144. * Return the current number of topics, via the "ByRef" TopicCount
  145. * Return a 2d SafeArray of the topic data.
  146. - 1st dim: topic numbers
  147. - 2nd dim: topic values
  148. We could do some caching, instead of repacking everytime...
  149. But this works for demonstration purposes."""
  150. TopicCount = len(self.topics)
  151. self.OnRefreshData()
  152. # Grow the lists, so we don't need a heap of calls to append()
  153. results = [[None] * TopicCount, [None] * TopicCount]
  154. # Excel expects a 2-dimensional array. The first dim contains the
  155. # topic numbers, and the second contains the values for the topics.
  156. # In true VBA style (yuck), we need to pack the array in row-major format,
  157. # which looks like:
  158. # ( (topic_num1, topic_num2, ..., topic_numN), \
  159. # (topic_val1, topic_val2, ..., topic_valN) )
  160. for idx, topicdata in enumerate(self.topics.items()):
  161. topicNum, topic = topicdata
  162. results[0][idx] = topicNum
  163. results[1][idx] = topic.GetValue()
  164. # TopicCount is meant to be passed to us ByRef, so return it as well, as per
  165. # the way pywin32 handles ByRef arguments.
  166. return tuple(results), TopicCount
  167. def ServerStart(self, CallbackObject):
  168. """Excel has just created us... We take its callback for later, and set up shop."""
  169. self.IsAlive = self.ALIVE
  170. if CallbackObject is None:
  171. raise COMException(desc='Excel did not provide a callback')
  172. # Need to "cast" the raw PyIDispatch object to the IRTDUpdateEvent interface
  173. IRTDUpdateEventKlass = win32com.client.CLSIDToClass.GetClass('{A43788C1-D91B-11D3-8F39-00C04F3651B8}')
  174. self.__callback = IRTDUpdateEventKlass(CallbackObject)
  175. self.OnServerStart()
  176. return self.IsAlive
  177. def ServerTerminate(self):
  178. """Called when excel no longer wants us."""
  179. self.IsAlive = self.NOT_ALIVE # On next heartbeat, excel will free us
  180. self.OnServerTerminate()
  181. def CreateTopic(self, TopicStrings=None):
  182. """Topic factory method. Subclass must override.
  183. Topic objects need to provide:
  184. * GetValue() method which returns an atomic value.
  185. Will raise NotImplemented if not overridden.
  186. """
  187. raise NotImplemented('Subclass must implement')
  188. # Overridable class events...
  189. def OnConnectData(self, TopicID):
  190. """Called when a new topic has been created, at excel's request."""
  191. pass
  192. def OnDisconnectData(self, TopicID):
  193. """Called when a topic is about to be deleted, at excel's request."""
  194. pass
  195. def OnRefreshData(self):
  196. """Called when excel has requested all current topic data."""
  197. pass
  198. def OnServerStart(self):
  199. """Called when excel has instanciated us."""
  200. pass
  201. def OnServerTerminate(self):
  202. """Called when excel is about to destroy us."""
  203. pass
  204. class RTDTopic(object):
  205. """Base RTD Topic.
  206. Only method required by our RTDServer implementation is GetValue().
  207. The others are more for convenience."""
  208. def __init__(self, TopicStrings):
  209. super(RTDTopic, self).__init__()
  210. self.TopicStrings = TopicStrings
  211. self.__currentValue = None
  212. self.__dirty = False
  213. def Update(self, sender):
  214. """Called by the RTD Server.
  215. Gives us a chance to check if our topic data needs to be
  216. changed (eg. check a file, quiz a database, etc)."""
  217. raise NotImplemented('subclass must implement')
  218. def Reset(self):
  219. """Call when this topic isn't considered "dirty" anymore."""
  220. self.__dirty = False
  221. def GetValue(self):
  222. return self.__currentValue
  223. def SetValue(self, value):
  224. self.__dirty = True
  225. self.__currentValue = value
  226. def HasChanged(self):
  227. return self.__dirty
  228. # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  229. ######################################
  230. # Example classes
  231. ######################################
  232. class TimeServer(ExcelRTDServer):
  233. """Example Time RTD server.
  234. Sends time updates back to excel.
  235. example of use, in an excel sheet:
  236. =RTD("Python.RTD.TimeServer","","seconds","5")
  237. This will cause a timestamp string to fill the cell, and update its value
  238. every 5 seconds (or as close as possible depending on how busy excel is).
  239. The empty string parameter denotes the com server is running on the local
  240. machine. Otherwise, put in the hostname to look on. For more info
  241. on this, lookup the Excel help for its "RTD" worksheet function.
  242. Obviously, you'd want to wrap this kind of thing in a friendlier VBA
  243. function.
  244. Also, remember that the RTD function accepts a maximum of 28 arguments!
  245. If you want to pass more, you may need to concatenate arguments into one
  246. string, and have your topic parse them appropriately.
  247. """
  248. # win32com.server setup attributes...
  249. # Never copy the _reg_clsid_ value in your own classes!
  250. _reg_clsid_ = '{EA7F2CF1-11A2-45E4-B2D5-68E240DB8CB1}'
  251. _reg_progid_ = 'Python.RTD.TimeServer'
  252. _reg_desc_ = "Python class implementing Excel IRTDServer -- feeds time"
  253. # other class attributes...
  254. INTERVAL = 0.5 # secs. Threaded timer will wake us up at this interval.
  255. def __init__(self):
  256. super(TimeServer, self).__init__()
  257. # Simply timer thread to ensure we get to update our topics, and
  258. # tell excel about any changes. This is a pretty basic and dirty way to
  259. # do this. Ideally, there should be some sort of waitable (eg. either win32
  260. # event, socket data event...) and be kicked off by that event triggering.
  261. # As soon as we set up shop here, we _must_ return control back to excel.
  262. # (ie. we can't block and do our own thing...)
  263. self.ticker = threading.Timer(self.INTERVAL, self.Update)
  264. def OnServerStart(self):
  265. self.ticker.start()
  266. def OnServerTerminate(self):
  267. if not self.ticker.finished.isSet():
  268. self.ticker.cancel() # Cancel our wake-up thread. Excel has killed us.
  269. def Update(self):
  270. # Get our wake-up thread ready...
  271. self.ticker = threading.Timer(self.INTERVAL, self.Update)
  272. try:
  273. # Check if any of our topics have new info to pass on
  274. if len(self.topics):
  275. refresh = False
  276. for topic in self.topics.values():
  277. topic.Update(self)
  278. if topic.HasChanged():
  279. refresh = True
  280. topic.Reset()
  281. if refresh:
  282. self.SignalExcel()
  283. finally:
  284. self.ticker.start() # Make sure we get to run again
  285. def CreateTopic(self, TopicStrings=None):
  286. """Topic factory. Builds a TimeTopic object out of the given TopicStrings."""
  287. return TimeTopic(TopicStrings)
  288. class TimeTopic(RTDTopic):
  289. """Example topic for example RTD server.
  290. Will accept some simple commands to alter how long to delay value updates.
  291. Commands:
  292. * seconds, delay_in_seconds
  293. * minutes, delay_in_minutes
  294. * hours, delay_in_hours
  295. """
  296. def __init__(self, TopicStrings):
  297. super(TimeTopic, self).__init__(TopicStrings)
  298. try:
  299. self.cmd, self.delay = self.TopicStrings
  300. except Exception as E:
  301. # We could simply return a "# ERROR" type string as the
  302. # topic value, but explosions like this should be able to get handled by
  303. # the VBA-side "On Error" stuff.
  304. raise ValueError("Invalid topic strings: %s" % str(TopicStrings))
  305. #self.cmd = str(self.cmd)
  306. self.delay = float(self.delay)
  307. # setup our initial value
  308. self.checkpoint = self.timestamp()
  309. self.SetValue(str(self.checkpoint))
  310. def timestamp(self):
  311. return datetime.datetime.now()
  312. def Update(self, sender):
  313. now = self.timestamp()
  314. delta = now - self.checkpoint
  315. refresh = False
  316. if self.cmd == "seconds":
  317. if delta.seconds >= self.delay:
  318. refresh = True
  319. elif self.cmd == "minutes":
  320. if delta.minutes >= self.delay:
  321. refresh = True
  322. elif self.cmd == "hours":
  323. if delta.hours >= self.delay:
  324. refresh = True
  325. else:
  326. self.SetValue("#Unknown command: " + self.cmd)
  327. if refresh:
  328. self.SetValue(str(now))
  329. self.checkpoint = now
  330. if __name__ == "__main__":
  331. import win32com.server.register
  332. # Register/Unregister TimeServer example
  333. # eg. at the command line: excelrtd.py --register
  334. # Then type in an excel cell something like:
  335. # =RTD("Python.RTD.TimeServer","","seconds","5")
  336. win32com.server.register.UseCommandLine(TimeServer)