Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

excelRTDServer.py 16KB

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