Jul-25-2021, 08:11 PM
I'm trying to implement a cross-platform python interface for a third-party API (Antidote), so that it can communicate with a PyQt application.
Firstly, for Linux here's the working D-Bus interface; https://gitlab.com/-/snippets/2151173
Simply put, we create a DBus server (class AdaptateurAntidote) which relay all calls to another class (ImplementationAntidote), which in turn communicates with the GUI widgets.
Then, below is a tentative for the COM interface (notice that the method differ between the two API);
For instance, if "parent" is declared in _public_attrs_, this;
Finally, instead of using Dispatch(), I've tried;
Firstly, for Linux here's the working D-Bus interface; https://gitlab.com/-/snippets/2151173
Simply put, we create a DBus server (class AdaptateurAntidote) which relay all calls to another class (ImplementationAntidote), which in turn communicates with the GUI widgets.
Then, below is a tentative for the COM interface (notice that the method differ between the two API);
#!/usr/bin/python3 import pythoncom import sys import winreg import win32com.client import win32com.server.register import win32com.server.util from win32com.server import localserver from pathlib import Path from PyQt5 import QtCore, QtWidgets class MainWindow(QtWidgets.QMainWindow): def __init__(self, app): super().__init__() self.body = QtWidgets.QTextEdit("Texte à corrigée.") self.setCentralWidget(self.body) self.antidote = ImplementationAntidote(self, self.body) def closeEvent(self, event): super().closeEvent(event) sys.exit(0) # Kills the hanging server in QThread class ImplementationAntidote(QtCore.QObject): def __init__(self, parent: QtCore.QObject, body: QtWidgets.QTextEdit): super().__init__() self.parent = parent self.body = body # self._register() self._launch() self.worker = ServerThread() self.serverThread = QtCore.QThread() self.serverThread.started.connect(self.worker.run) self.worker.moveToThread(self.serverThread) self.serverThread.start() # this should renew a timer until local com server and antidote are both ready # ## QtCore.QTimer.singleShot(1000, self._connect) @staticmethod def readRegistry(path: str, name: str): key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, path, 0, winreg.KEY_READ) value, regtype = winreg.QueryValueEx(key, name) winreg.CloseKey(key) return value def _connect(self): version = self.readRegistry(r"Software\Druide informatique inc.\Antidote", "VersionAPI") version = min([2.0, float(version)]) self.server = win32com.client.Dispatch("Correcteur.Antidote") self.antidote = win32com.client.Dispatch("Antidote.ApiOle") self.antidote.LanceOutilDispatch2(self.server, "C", "", str(version)) # Server, outil, langue, version API def _launch(self): antidoteFolder = self.readRegistry(r"Software\Druide informatique inc.\Antidote", "DossierAntidote") antidotePath = Path(antidoteFolder) / "Antidote.exe" QtCore.QProcess.startDetached(str(antidotePath), ["-activex"]) def _register(self): win32com.server.register.UseCommandLine(AdapteurAntidote) class EventHandler: pass class ServerThread(QtCore.QObject): def __init__(self): super().__init__() def run(self): pythoncom.CoInitialize() localserver.serve(["{D390AE78-D6A2-47CF-B462-E4F2DC9C70F5}"]) # ## Hangs on quit pythoncom.CoUninitialize() # ## Never called class AdapteurAntidote: _reg_clsctx_ = pythoncom.CLSCTX_LOCAL_SERVER _reg_clsid_ = "{D390AE78-D6A2-47CF-B462-E4F2DC9C70F5}" # pythoncom.CreateGuid() _reg_progid_ = "Correcteur.Antidote" _reg_verprogid_ = "Correcteur.Antidote.1" _reg_class_spec_ = "AntidoteCOM.AdapteurAntidote" _public_methods_ = ["ActiveApplication", "ActiveDocument", "DonneDebutSelection", "DonneFinSelection", "DonneIdDocumentCourant", "DonneIdZoneDeTexte", "DonneIdZoneDeTexteCourante", "DonneIntervalle", "DonneLongueurZoneDeTexte", "DonneNbZonesDeTexte", "DonnePolice", "DonneTitreDocCourant", "RemplaceIntervalle", "SelectionneIntervalle"] def DonneIdDocumentCourant(self): print("DonneIdDocumentCourant") def DonneIdZoneDeTexte(self): print("DonneIdZoneDeTexte") def DonneIdZoneDeTexteCourante(self): print("DonneIdZoneDeTexteCourante") def DonneIntervalle(self): print("DonneIntervalle") def DonneLongueurZoneDeTexte(self): print("DonneLongueurZoneDeTexte") def DonneNbZonesDeTexte(self): print("DonneNbZonesDeTexte") def DonnePolice(self): print("DonnePolice") def DonneTitreDocCourant(self): print("DonneTitreDocCourant") def RemplaceIntervalle(self): print("RemplaceIntervalle") def ActiveDocument(self): print("ActiveDocument") # self.parent.activeDocument() def ActiveApplication(self): print("ActiveApplication") # self.parent.activeDocument() def DonneDebutSelection(self) -> int: print("DonneDebutSelection") # return self.parent.donneDebutSelection() def DonneFinSelection(self) -> int: print("DonneFinSelection") # return self.parent.donneFinSelection() def SelectionneIntervalle(self, debut: int, fin: int): print("SelectionneIntervalle") # self.parent.selectionneIntervalle(debut, fin) if __name__ == '__main__': app = QtWidgets.QApplication([]) gui = MainWindow(app) gui.show() app.exec()Similarly a COM server is created (AdaptateurAntidote), which receives calls from Antidote's API. However, due to the nature of COM objects, the adapter class is never instantiated, and won't accept the ImplementationAntidote as its parent attribute.
For instance, if "parent" is declared in _public_attrs_, this;
self.server.parent = selfResults in;
Error:TypeError: must be real number, not ImplementationAntidote.
If "parent" is not declared;Error:AttributeError: 'Correcteur.Antidote.parent' can not be set.
Settings a global variable or setting a "parent" attribute into AdaptateurAntidote also does not work, as it always return its default value (None).Finally, instead of using Dispatch(), I've tried;
self.server = win32com.client.DispatchWithEvents("Correcteur.Antidote", EventHandler)So I could split the COM objects properties in a class, and handle the events in another. However, this gives;
Error:TypeError: This COM object can not automate the makepy process - please run makepy manually for this object
Documentation is scarce and I'm stuck !