如何从pythonwin32com或comtypes使用COM访问IRTDServer?

2024-06-28 20:21:57 发布

您现在位置:Python中文网/ 问答频道 /正文

环境

Windows10+Python3.6.3 64位(也尝试了32位)。我是一个python开发人员,尝试(几乎)第一次使用COM,并击中了这个巨大的拦截器。在

问题

当我试图通过win32comcomtypes使用dll中实现的IRTDServer时,我遇到了各种错误。结果发现使用win32com更加困难。下面两个库都包含了一个unittest示例。在

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

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

使用win32com库的代码

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

^{pr2}$

结果:

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与“”的typelib UUID相同美食酒吧“)

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

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

而且,GetTypeInfo返回{}也在提醒我。据我所知,win32com使用该方法(属于IDispatchCOM接口的一部分)来确定其他接口中所有其他函数的参数和返回类型,包括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))

我尝试过的其他解决方案

(基于谷歌搜索和以下评论中的答案)

  • (重新)注册了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类来使用接口,但是我不知道如何去做。

  • 我注意到了dynamic参数。代码表明这与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有{}

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


(不知道我在做什么,)我试着用OleViewDotNet查看DLL:

enter image description here

enter image description here

enter image description here

enter image description here



Tags: inpytestselfenvclientlibdef
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)

请参考客户端.py和例子/rtdtime.py以下来源。
pyrtd - default
pyrtd/电阻式温度检测器/客户端.py
pyrtd/示例/rtdtime.py

我也遇到了同样的问题。在

我还试着用win32com让excel运行,说实话,这有点不稳定…我甚至连我的excel都摸不着。在

所以我花了一些时间来研究这个问题。问题出在卡斯托身上。认为您(和我)加载的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()

相关问题 更多 >