Как передать функцию для вызова в Pyside2?

#python #python-2.7 #qt #pyside2 #qwebengineview

#python #python-2.7 #qt #pyside2 #qwebengineview

Вопрос:

Я пытаюсь получить некоторые данные из QWebEngineView с помощью функции runJavaScript, но она выдает ошибку, показывая приведенное ниже сообщение об ошибке.

Есть ли способ решить эту проблему? Старые темы предполагают, что это ограничение в 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_())
  

Комментарии:

1. Спасибо, это очень полезно. Но я не понимаю функцию load_object . Это только жестко запрограммировано для получения элемента по имени тега? Так что я могу изменить его для запуска любого javascript? Даже если мне не нужно передавать функцию обратного вызова?

2. @JoanVenge 1) Я рекомендую вам прочитать о QtWebChannel, 2) Простыми словами QtWebChannel реализуется как мост между python и javascript. QtWebChannel сопоставляет свойства QObject и отправляет их в js, чтобы он создавал объект с этими свойствами, поэтому QObject регистрируется как «серверный», а затем другой объект получается через «channel.objects.backend», но с отображенными свойствами, а затем при использовании любого метода отображенногообъект, подобный toHtml, отправляет информацию в метод QObject, и это делается через websockets.

3. @JoanVenge 3) Мой ответ отвечает конкретно на ваш вопрос и не выходит за его рамки, не преувеличивайте мой ответ. В вашем конкретном случае вы хотели получить результат document.getElementsByTagName('html')[0].innerHTML в python, и только это дает мой ответ. Я не пытаюсь реализовать обобщенную функцию.

4. @JoanVenge 4) Я думаю, вам следует подождать, пока я отвечу на ваш другой вопрос, поскольку там я покажу вам, как взаимодействовать с javascript для управления свойствами, но я все еще работаю над улучшением своего решения

5. Спасибо, но после выполнения вашего кода предполагается ли печатать innerHTML? Поскольку я не получаю никакой распечатки, я попробовал page.load_object() , но это то же самое. Вызывая page.handle_htmlChanged, я получаю объект, не имеющий атрибута _html.