【问题标题】:How to use COM from python win32com or comtypes to access an IRTDServer?如何使用来自 python win32com 或 comtypes 的 COM 访问 IRTDServer?
【发布时间】:2018-06-07 17:33:03
【问题描述】:

环境

Windows 10 + python 3.6.3 64 位(也尝试过 32 位)。我是一名 Python 开发人员,尝试(几乎)第一次使用 COM,并遇到了这个巨大的障碍。

问题

尝试通过win32comcomtypes 使用在dll(不是我编写的)中实现的IRTDServer 时,我遇到了各种错误。使用win32com 变得更加困难。我为下面的两个库提供了一个示例单元测试。

从 Excel 2016 访问服务器按预期工作;这将返回预期值:

=RTD("foo.bar", , "STAT1", "METRIC1")

使用win32com库的代码

这是一个简单的测试用例,它应该连接到服务器,但没有。 (这只是一个版本,因为我已经更改了很多次尝试调试问题。)

from unittest import TestCase

class COMtest(TestCase):
    def test_win32com(self):
        import win32com.client
        from win32com.server.util import wrap

        class RTDclient:
            # are these only required when implementing the server?
            _com_interfaces_ = ["IRTDUpdateEvent"]
            _public_methods_ = ["Disconnect", "UpdateNotify"]
            _public_attrs_ = ["HeartbeatInterval"]

            def __init__(self, *args, **kwargs):
                self._comObj = win32com.client.Dispatch(*args, **kwargs)
            def connect(self):
                self._rtd = win32com.client.CastTo(self._comObj, 'IRtdServer')
                result = self._rtd.ServerStart(wrap(self))
                assert result > 0

            def UpdateNotify(self):
                print("UpdateNotify() callback")
            def Disconnect(self):
                print("Disconnect() called")
            HeartbeatInterval = -1

_rtd = RTDclient("foo.bar")
_rtd.connect()

结果:

Traceback (most recent call last):
  File "env\lib\site-packages\win32com\client\gencache.py", line 532, in EnsureDispatch
    ti = disp._oleobj_.GetTypeInfo()
pywintypes.com_error: (-2147467263, 'Not implemented', None, None)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "test\test.py", line 23, in test_win32com
    _rtd.connect()
  File "test\test.py", line 16, in connect
    self._rtd = win32com.client.CastTo(dispatch, 'IRtdServer')
  File "env\lib\site-packages\win32com\client\__init__.py", line 134, in CastTo
    ob = gencache.EnsureDispatch(ob)
  File "env\lib\site-packages\win32com\client\gencache.py", line 543, in EnsureDispatch
    raise TypeError("This COM object can not automate the makepy process - please run makepy manually for this object")
TypeError: This COM object can not automate the makepy process - please run makepy manually for this object

按照这些指示,我成功运行了makepy 脚本:

> env\Scripts\python.exe env\lib\site-packages\win32com\client\makepy.py "foo.bar"
Generating to C:\Users\user1\AppData\Local\Temp\gen_py\3.5\longuuid1x0x1x0.py
Building definitions from type library...
Generating...
Importing module

(为了隐私,我替换了 stackoverflow 上的 UUID。这个 UUID 与“foo.bar”的 typelib UUID 相同。)

生成的文件包含IRtdServerIRTDUpdateEvent 的各种函数和类型定义。但是在这个文件中,两个接口都是win32com.client.DispatchBaseClass的子类,而根据OleViewDotNet,它们应该是IUnknown的子类?

但是,当我尝试再次运行单元测试时,我收到了与以前完全相同的错误。好像查找机制没有找到生成的模块?

另外,GetTypeInfo 返回Not implemented 让我感到震惊。据我了解,win32com 使用该方法(IDispatch COM 接口的一部分)来确定其他接口中所有其他函数的参数和返回类型,包括IRtdServer。如果没有实现,将无法正确确定类型。然而,生成的文件似乎包含了这些信息,这也令人困惑。


使用comtypes库的代码

from unittest import TestCase

class COMtest(TestCase):
    def test_comtypes(self):
        import comtypes.client

        class RTDclient:
            # are these for win32com only?
            _com_interfaces_ = ["IRTDUpdateEvent"]
            _public_methods_ = ["Disconnect", "UpdateNotify"]
            _public_attrs_ = ["HeartbeatInterval"]

            def __init__(self, clsid):
                self._comObj = comtypes.client.CreateObject(clsid)
            def connect(self):
                self._rtd = self._comObj.IRtdServer()
                result = self._rtd.ServerStart(self)
                assert result > 0

            def UpdateNotify(self):
                print("UpdateNotify() callback")
            def Disconnect(self):
                print("Disconnect() called")
            HeartbeatInterval = -1

_rtd = RTDclient("foo.bar")
_rtd.connect()

结果:

  File "test\test.py", line 27, in test_comtypes
    _rtd.connect()
  File "test\test.py", line 16, in connect
    self._rtd = self._comObj.IRTDServer()
  File "env\lib\site-packages\comtypes\client\dynamic.py", line 110, in __getattr__
    dispid = self._comobj.GetIDsOfNames(name)[0]
  File "env\lib\site-packages\comtypes\automation.py", line 708, in GetIDsOfNames
    self.__com_GetIDsOfNames(riid_null, arr, len(names), lcid, ids)
_ctypes.COMError: (-2147352570, 'Unknown name.', (None, None, None, 0, None))

我尝试过的其他一些解决方案

(基于谷歌搜索和下面 cmets 中的答案)

  • (重新)注册 DLL
  • 注册了32位版本的DLL,尝试了python 32位
  • python.exe的兼容模式设置为Windows XP SP3
  • 试过不实例化IRtdServer,即替换这两行:

    self._rtd = self._comObj.IRtdServer()
    result = self._rtd.ServerStart(self)
    

    与:

    result = self._comObj.ServerStart(self)
    

    这次的错误是:

    TypeError: 'NoneType' object is not callable
    

    这似乎表明ServerStart 函数存在,但未定义? (看起来真的很奇怪。这个谜肯定还有更多。)

  • 尝试将interface="IRtdServer" 参数传递给CreateObject

    def __init__(self, clsid):
        self._comObj = comtypes.client.CreateObject(clsid, interface="IRtdServer")
    def connect(self):
        result = self._comObj.ServerStart(self)
        ...
    

    收到的错误是:

      File "test\test.py", line 13, in __init__
        self._comObj = comtypes.client.CreateObject(clsid, interface="IRtdServer")
      File "env\lib\site-packages\comtypes\client\__init__.py", line 238, in CreateObject
        obj = comtypes.CoCreateInstance(clsid, clsctx=clsctx, interface=interface)
      File "env\lib\site-packages\comtypes\__init__.py", line 1223, in CoCreateInstance
        p = POINTER(interface)()
    TypeError: Cannot create instance: has no _type_
    

    comtypes 库中的跟踪代码,这似乎表明接口参数需要一个接口类,而不是字符串。我发现comtypes库中定义了各种接口:IDispatchIPersistIServiceProvider。都是IUnknown 的子类。根据 OleViewDotNet,IRtdServer 也是IUnknown 的子类。这让我相信我需要在 python 中类似地编写一个 IRtdServer 类才能使用该接口,但我不知道该怎么做。

  • 我注意到CreateObjectdynamic 参数。代码表明这与interface参数是互斥的,所以我试了一下:

    def __init__(self, clsid):
        self._comObj = comtypes.client.CreateObject(clsid, dynamic=True)
    def connect(self):
        self._rtd = self._comObj.IRtdServer()
        result = self._rtd.ServerStart(self)
    

    但是错误和我原来的错误一样:IRtdServer has _ctypes.COMError: (-2147352570, 'Unknown name.', (None, None, None, 0, None))

任何帮助或线索将不胜感激。提前谢谢你。


(不知道自己在做什么,)我尝试使用 OleViewDotNet 来查看 DLL:


【问题讨论】:

  • 这里我不确定,但是self._comObj.IRtdServer(self) 你真的需要吗?你试过打电话self._comObj.ServerStart(self)吗?
  • 另一种选择是将您需要的接口放在 CreateObject 的第 4 个参数中:comtypes.client.CreateObject(clsid, None, None, IRtdServer)。它可能是 IRtdServer"IRtdServer" 或者它的 GUID — comtypes 文档在这里不是很清楚。
  • 在“我尝试过的其他一些解决方案”下,我尝试了您在最新编辑中所说的几种排列方式。还是行不通。我想我需要使用dynamic=True 来避免编写我自己的 IRtdServer 类。
  • 所以你问我们“为什么我不能在不创建类 ID 的情况下访问服务”(注册)。
  • 我最终对这个项目采取了不同的方法,使用 win32com 与 Excel 对话并用 =RTD(...) 公式填充单元格,然后从单元格中读取值。这是一个额外的步骤,但它有效。为此,我使用 win32com 没有任何问题。

标签: python com pywin32 win32com comtypes


【解决方案1】:

我遇到了同样的问题。

我还尝试使用 win32com 让 excel 为我运行,说实话这有点不稳定......我什至无法触摸我的 Excel。

因此我花了一些时间研究这个。问题在于 CastTo。认为您(和我)加载的 COM 对象没有包含足够的信息进行转换(某些方法,如 GetTypeInfo 未实现等......)

因此,我创建了一个包装器,使这些 COM 对象的方法可调用……不明显。这似乎对我有用。

客户端代码是从一个名为 pyrtd 的项目中修改而来的,由于各种原因无法正常工作(认为是由于 RTD 模型的变化……RefreshData 的返回现在完全不同了)。

import functools

import pythoncom
import win32com.client
from win32com import universal
from win32com.client import gencache
from win32com.server.util import wrap


EXCEL_TLB_GUID = '{00020813-0000-0000-C000-000000000046}'
EXCEL_TLB_LCID = 0
EXCEL_TLB_MAJOR = 1
EXCEL_TLB_MINOR = 4

gencache.EnsureModule(EXCEL_TLB_GUID, EXCEL_TLB_LCID, EXCEL_TLB_MAJOR, EXCEL_TLB_MINOR)

universal.RegisterInterfaces(EXCEL_TLB_GUID,
                             EXCEL_TLB_LCID, EXCEL_TLB_MAJOR, EXCEL_TLB_MINOR,
                             ['IRtdServer', 'IRTDUpdateEvent'])


# noinspection PyProtectedMember
class ObjectWrapperCOM:
    """
    This object can act as a wrapper for an object dispatched using win32com.client.Dispatch
    Sometimes the object written by 3rd party is not well constructed that win32com will not be able to obtain
    type information etc in order to cast the object to a certain interface. win32com.client.CastTo will fail.

    This wrapper class will enable the object to call its methods in this case, even if we do not know what exactly
    the wrapped object is.
    """
    LCID = 0x0

    def __init__(self, obj):
        self._impl = obj  # type: win32com.client.CDispatch

    def __getattr__(self, item):
        flags, dispid = self._impl._find_dispatch_type_(item)
        if dispid is None:
            raise AttributeError("{} is not a valid property or method for this object.".format(item))
        return functools.partial(self._impl._oleobj_.Invoke, dispid, self.LCID, flags, True)


# noinspection PyPep8Naming
class RTDUpdateEvent:
    """
    Implements interface IRTDUpdateEvent from COM imports
    """
    _com_interfaces_ = ['IRTDUpdateEvent']
    _public_methods_ = ['Disconnect', 'UpdateNotify']
    _public_attrs_ = ['HeartbeatInterval']

    # Implementation of IRTDUpdateEvent.
    HeartbeatInterval = -1

    def __init__(self, event_driven=True):
        self.ready = False
        self._event_driven = event_driven

    def UpdateNotify(self):
        if self._event_driven:
            self.ready = True

    def Disconnect(self):
        pass


class RTDClient:
    """
    Implements a Real-Time-Data (RTD) client for accessing COM data sources that provide an IRtdServer interface.
    """

    MAX_REGISTERED_TOPICS = 1024

    def __init__(self, class_id):
        """
        :param classid: can either be class ID or program ID
        """
        self._class_id = class_id
        self._rtd = None
        self._update_event = None

        self._topic_to_id = {}
        self._id_to_topic = {}
        self._topic_values = {}
        self._last_topic_id = 0

    def connect(self, event_driven=True):
        """
        Connects to the RTD server.

        Set event_driven to false if you to disable update notifications.
        In this case you'll need to call refresh_data manually.
        """

        dispatch = win32com.client.Dispatch(self._class_id)
        self._update_event = RTDUpdateEvent(event_driven)
        try:
            self._rtd = win32com.client.CastTo(dispatch, 'IRtdServer')
        except TypeError:
            # Automated makepy failed...no detailed construction available for the class
            self._rtd = ObjectWrapperCOM(dispatch)

        self._rtd.ServerStart(wrap(self._update_event))

    def update(self):
        """
        Check if there is data waiting and call RefreshData if necessary. Returns True if new data has been received.
        Note that you should call this following a call to pythoncom.PumpWaitingMessages(). If you neglect to
        pump the message loop you'll never receive UpdateNotify callbacks.
        """
        # noinspection PyUnresolvedReferences
        pythoncom.PumpWaitingMessages()
        if self._update_event.ready:
            self._update_event.ready = False
            self.refresh_data()
            return True
        else:
            return False

    def refresh_data(self):
        """
        Grabs new data from the RTD server.
        """

        (ids, values) = self._rtd.RefreshData(self.MAX_REGISTERED_TOPICS)
        for id_, value in zip(ids, values):
            if id_ is None and value is None:
                # This is probably the end of message
                continue
            assert id_ in self._id_to_topic, "Topic ID {} is not registered.".format(id_)
            topic = self._id_to_topic[id_]
            self._topic_values[topic] = value

    def get(self, topic: tuple):
        """
        Gets the value of a registered topic. Returns None if no value is available. Throws an exception if
        the topic isn't registered.
        """
        assert topic in self._topic_to_id, 'Topic %s not registered.' % (topic,)
        return self._topic_values.get(topic)

    def register_topic(self, topic: tuple):
        """
        Registers a topic with the RTD server. The topic's value will be updated in subsequent data refreshes.
        """
        if topic not in self._topic_to_id:
            id_ = self._last_topic_id
            self._last_topic_id += 1

            self._topic_to_id[topic] = id_
            self._id_to_topic[id_] = topic

            self._rtd.ConnectData(id_, topic, True)

    def unregister_topic(self, topic: tuple):
        """
        Un-register topic so that it will not get updated.
        :param topic:
        :return:
        """
        assert topic in self._topic_to_id, 'Topic %s not registered.' % (topic,)
        self._rtd.DisconnectData(self._topic_to_id[topic])

    def disconnect(self):
        """
        Closes RTD server connection.
        :return:
        """
        self._rtd.ServerTerminate()

【讨论】:

  • 嘿,请分享如何实现此代码。可能是一个显示示例的回购。谢谢
  • 你可以查看pyrtd项目,上面只是为了让那个项目再次工作。
  • 嘿,伙计,我试过你所说的,我收到以下错误TypeError: Objects for SAFEARRAYS must be sequences (of sequences), or a buffer object. pythoncom error: Failed to call the universal dispatcher register_topic() 方法发生错误
  • 我无法复制错误。它适用于我正在连接的 RTDServer。您是否将元组作为 register_topic() 的参数传递?而且它还真的取决于 RTDServer 的实现......
  • 不,我正在发送一个字符串,该字符串在方法内转换为元组。如果我需要发送一个元组,主题旁边的内容应该是什么
【解决方案2】:

似乎已经有 Excel 2002 的服务器/客户端。
pyrtd

看看那个源,一旦你创建了一个调度对象,那么它似乎被强制转换为 IRtdServer。
提取相关部分,如下图。

from win32com import client, universal
from win32com.server.util import wrap

def __init__(self, classid):
    self._classid = classid
    self._rtd = None

def connect(self, event_driven=True):
    dispatch = client.Dispatch(self._classid)
    self._rtd = client.CastTo(dispatch, 'IRtdServer')
    if event_driven:
        self._rtd.ServerStart(wrap(self))
    else:
        self._rtd.ServerStart(None)

请参考以下来源的client.py和examples/rtdtime.py。
pyrtd - default
pyrtd/rtd/client.py
pyrtd/examples/rtdtime.py

【讨论】:

  • 我也看过那个项目,但是那个代码会产生各种错误。我记得我在让Dispatch 函数工作时遇到了麻烦。我相信我在某处遇到过一篇文章,其中说 RTD 或 COM 模型在 Office 2003/2007 之间的某个地方发生了变化。这可能是原因,也可能是其他原因......我将很快在我的问题中包含 win32com 示例。
  • 更新了一个 win32com 的例子
  • 您的 win32com 示例与 pyrtd 源不匹配。调用Dispatch()的地方、要存储的变量(_comObj)、CastTo()指定的变量(dispatch)都不一样。
  • (A) 无论从__init__() 还是connect() 调用Dispatch() 都会出现相同的错误。 (如果 win32com 假设某些 COM 方法仅从某些 python 函数内部调用,那将是非常糟糕的做法。)(B)我最初直接指定传递给 Dispatch() 的 COM 库的名称,但后来将其更改为使用 * args 解包以允许更灵活的测试。它在功能上是等效的,并且无论哪种方式都给出了相同的错误。 (C) dispatch 变量是复制+粘贴错误 - 已更正。
  • 虽然这可能不是问题,但 Excel 2016 的 RTD 接口有问题,有一个article 可以通过 RTD 指定服务器名称来避免( )。
猜你喜欢
  • 2014-02-23
  • 1970-01-01
  • 2021-05-11
  • 1970-01-01
  • 1970-01-01
  • 2014-08-12
  • 1970-01-01
  • 2021-07-27
  • 2021-06-24
相关资源
最近更新 更多