如何传递要在 Pyside2 中调用的函数?

Posted

技术标签:

【中文标题】如何传递要在 Pyside2 中调用的函数?【英文标题】:How to pass function to be invoked in Pyside2? 【发布时间】:2020-10-04 15:00:37 【问题描述】:

我正在尝试使用 runjavascript 函数从 QWebEngineView 获取一些数据,但它显示以下错误消息时出错。

有没有办法解决这个问题?较早的主题表明这是 Pyside2 中的一个限制,因此不确定现在是否已解决。

from PySide2 import QtCore, QtWidgets, QtGui, QtWebEngineWidgets

def callbackfunction(html):
    print html

file = "myhtmlfile.html"
view = QtWebEngineWidgets.QWebEngineView()
view.load(QtCore.QUrl.fromLocalFile(file))
view.page().runJavaScript("document.getElementsByTagName('html')[0].innerHTML", callbackfunction)
TypeError: 'PySide2.QtWebEngineWidgets.QWebEnginePage.runJavaScript' called with wrong argument types:
 PySide2.QtWebEngineWidgets.QWebEnginePage.runJavaScript(str, function)
Supported signatures:
 PySide2.QtWebEngineWidgets.QWebEnginePage.runJavaScript(str)
 PySide2.QtWebEngineWidgets.QWebEnginePage.runJavaScript(str, int)

【问题讨论】:

【参考方案1】:

PySide2 不提供 runJavaScript 的所有重载方法,因此它不支持向其传递回调。一个可能的解决方法是使用QtWebChannel,通过websockets实现javascript和python之间的通信:

import sys
import os

from PySide2 import QtCore, QtWidgets, QtWebEngineWidgets, QtWebChannel

CURRENT_DIR = os.path.dirname(os.path.realpath(__file__))


class Backend(QtCore.QObject):
    htmlChanged = QtCore.Signal()

    def __init__(self, parent=None):
        super(Backend, self).__init__(parent)
        self._html = ""

    @QtCore.Slot(str)
    def toHtml(self, html):
        self._html = html
        self.htmlChanged.emit()

    @property
    def html(self):
        return self._html


class WebEnginePage(QtWebEngineWidgets.QWebEnginePage):
    def __init__(self, parent=None):
        super(WebEnginePage, self).__init__(parent)
        self.loadFinished.connect(self.onLoadFinished)
        self._backend = Backend()
        self.backend.htmlChanged.connect(self.handle_htmlChanged)

    @property
    def backend(self):
        return self._backend

    @QtCore.Slot(bool)
    def onLoadFinished(self, ok):
        if ok:
            self.load_qwebchannel()
            self.load_object()

    def load_qwebchannel(self):
        file = QtCore.QFile(":/qtwebchannel/qwebchannel.js")
        if file.open(QtCore.QIODevice.ReadOnly):
            content = file.readAll()
            file.close()
            self.runJavaScript(content.data().decode())
        if self.webChannel() is None:
            channel = QtWebChannel.QWebChannel(self)
            self.setWebChannel(channel)

    def load_object(self):
        if self.webChannel() is not None:
            self.webChannel().registerObject("backend", self.backend)
            script = r"""
            new QWebChannel(qt.webChannelTransport, function (channel) 
                var backend = channel.objects.backend;
                var html = document.getElementsByTagName('html')[0].innerHTML;
                backend.toHtml(html);
            );"""
            self.runJavaScript(script)

    def handle_htmlChanged(self):
        print(self.backend.html)


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    filename = os.path.join(CURRENT_DIR, "index.html")
    url = QtCore.QUrl.fromLocalFile(filename)
    page = WebEnginePage()
    view = QtWebEngineWidgets.QWebEngineView()
    page.load(url)
    view.setPage(page)
    view.resize(640, 480)
    view.show()
    sys.exit(app.exec_())

我之前的逻辑只关注获取 HTML,但在这部分答案中,我将尝试概括逻辑以便能够关联回调。想法是将响应发送到关联一个与回调相关的 uuid 的桥对象,消息必须以 json 格式发送,以便能够处理不同类型的数据。

import json
import os
import sys

from PySide2 import QtCore, QtWidgets, QtWebEngineWidgets, QtWebChannel
from jinja2 import Template

CURRENT_DIR = os.path.dirname(os.path.realpath(__file__))


class Bridge(QtCore.QObject):
    initialized = QtCore.Signal()

    def __init__(self, parent=None):
        super().__init__(parent)
        self._callbacks = dict()

    @property
    def callbacks(self):
        return self._callbacks

    @QtCore.Slot()
    def init(self):
        self.initialized.emit()

    @QtCore.Slot(str, str)
    def send(self, uuid, data):
        res = json.loads(data)
        callback = self.callbacks.pop(uuid, None)
        if callable(callable):
            callback(res)


class WebEnginePage(QtWebEngineWidgets.QWebEnginePage):
    def __init__(self, parent=None):
        super(WebEnginePage, self).__init__(parent)
        self.loadFinished.connect(self.onLoadFinished)
        self._bridge = Bridge()

    @property
    def bridge(self):
        return self._bridge

    @QtCore.Slot(bool)
    def onLoadFinished(self, ok):
        if ok:
            self.load_qwebchannel()
            self.load_object()

    def load_qwebchannel(self):
        file = QtCore.QFile(":/qtwebchannel/qwebchannel.js")
        if file.open(QtCore.QIODevice.ReadOnly):
            content = file.readAll()
            file.close()
            self.runJavaScript(content.data().decode())
        if self.webChannel() is None:
            channel = QtWebChannel.QWebChannel(self)
            self.setWebChannel(channel)

    def load_object(self):
        if self.webChannel() is not None:
            self.webChannel().registerObject("bridge", self.bridge)
            script = r"""
            var bridge = null;
            new QWebChannel(qt.webChannelTransport, function (channel) 
                bridge = channel.objects.bridge;
                bridge.init();
            );"""
            self.runJavaScript(script)

    def execute(self, code, callback, uuid=""):
        uuid = uuid or QtCore.QUuid.createUuid().toString()
        self.bridge.callbacks[uuid] = callback
        script = Template(code).render(uuid=uuid)
        self.runJavaScript(script)


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.page = WebEnginePage()
        self.view = QtWebEngineWidgets.QWebEngineView()
        self.view.setPage(self.page)

        self.page.bridge.initialized.connect(self.handle_initialized)

        self.setCentralWidget(self.view)

        filename = os.path.join(CURRENT_DIR, "index.html")
        url = QtCore.QUrl.fromLocalFile(filename)
        self.view.load(url)

    def handle_initialized(self):
        self.page.execute(
            """
            var value = document.getElementsByTagName('html')[0].innerHTML
            bridge.send('uuid', JSON.stringify(value));
        """,
            callbackfunction,
        )


def callbackfunction(html):
    print(html)


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    w = MainWindow()
    w.show()
    sys.exit(app.exec_())

【讨论】:

谢谢,这非常有用。但我不明白 load_object 函数。这只是硬编码以通过标签名称获取元素吗?所以我可以改变它来运行任何javascript?即使我不需要传递回调函数? @JoanVenge 1) 我建议您阅读有关 QtWebChannel 的内容,2) 简单来说,QtWebChannel 实现了 python 和 javascript 之间的桥梁。 QtWebChannel 映射 QObject 的属性并将它们发送到 js,以便它创建具有这些属性的对象,因此将 QObject 注册为“后端”,然后通过“channel.objects.backend”获取另一个对象,但具有映射的属性,然后当使用映射对象的任何方法(如 toHtml)时,它会将信息发送到 QObject 方法,这是通过 websockets 完成的。 @JoanVenge 3) 我的回答专门回答了你的问题并且没有超越它,不要夸大我的回答。在您的特定情况下,您想在 python 中获得document.getElementsByTagName('html')[0].innerHTML 的结果,仅此一项就是我的答案。我不是想实现一个通用的功能。 @JoanVenge 4) 我认为您应该等我回答您的其他问题,因为我将向您展示如何与 javascript 交互来操作属性,但我仍在努力完善我的解决方案 谢谢,但是在您的代码运行后,它应该打印 innerHtml 吗?因为我没有打印出来,所以我尝试了 page.load_object(),但它是一样的。调用 page.handle_htmlChanged 我得到的对象没有属性 _html。

以上是关于如何传递要在 Pyside2 中调用的函数?的主要内容,如果未能解决你的问题,请参考以下文章

如何打包 PySide2 应用程序以进行分发?

PySide2 使用 QProgressBar 作为信号参数

VB.NET - 传递要在函数中使用的表达式

如何使用 pyside2 插槽函数共享数据?

当 slot 函数具有默认参数=None 时,PySide2 的行为与 PySide 不同

ES6(类)