首页 > 解决方案 > 使用 QWebEngineView 显示大于 2MB 的内容?

问题描述

我正在尝试使用 QWebEngineView 在 PyQt5 GUI 中显示一些 Plot.ly 或 Plot.ly Dash 图(我还没有决定使用其中一个,所以我现在正在尝试两者)。由于某些 Chromium 级别的硬编码限制,这不适用于任何大于 2MB 的图。

我发现了一个类似的问题,就我们的需求而言,它几乎相同。看起来 OP 实际上找到了答案,但对我来说不幸的是,他们没有发布工作代码的示例或解释他们为使其工作所做的工作。我对基本理论的理解不够,无法将答案与另一个问题中链接的资源拼凑在一起,而且我的 Stack 声誉还不够高,无法评论并询问 OP 究竟是什么工作。

这是一个显示嵌入在 GUI 中的图的最小可重现示例。这是对有关在 PyQt5 GUI 中嵌入 Plotly 图的问题的答案的修改

import numpy as np
import plotly.offline as po
import plotly.graph_objs as go

from PyQt5.QtWebEngineWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import sys


def show_qt(fig):
    raw_html = '<html><head><meta charset="utf-8" />'
    raw_html += '<script src="https://cdn.plot.ly/plotly-latest.min.js"></script></head>'
    raw_html += '<body>'
    raw_html += po.plot(fig, include_plotlyjs=False, output_type='div')
    raw_html += '</body></html>'

    fig_view = QWebEngineView()
    # setHtml has a 2MB size limit, need to switch to setUrl on tmp file
    # for large figures.
    fig_view.setHtml(raw_html)
#    fig_view.setUrl(QUrl('temp-plot.html'))
    fig_view.show()
    fig_view.raise_()
    return fig_view


if __name__ == '__main__':
    app = QApplication(sys.argv)

    # Working small plot:
    fig = go.Figure(data=[{'type': 'scattergl', 'y': [2, 1, 3, 1]}])
    # Not working large plot:
#    t = np.arange(0, 200000, 1)
#    y = np.sin(t/20000)
    fig = go.Figure(data=[{'type': 'scattergl', 'y': y}])
#    po.plot(fig)

    fig_view = show_qt(fig)
    sys.exit(app.exec_())

这是一个修改后的版本,它演示了如何无法以相同的方式显示大型数据集:

import numpy as np
import plotly.offline as po
import plotly.graph_objs as go

from PyQt5.QtWebEngineWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import sys


def show_qt(fig):
    raw_html = '<html><head><meta charset="utf-8" />'
    raw_html += '<script src="https://cdn.plot.ly/plotly-latest.min.js"></script></head>'
    raw_html += '<body>'
    raw_html += po.plot(fig, include_plotlyjs=False, output_type='div')
    raw_html += '</body></html>'

    fig_view = QWebEngineView()
    # setHtml has a 2MB size limit, need to switch to setUrl on tmp file
    # for large figures.
    fig_view.setHtml(raw_html)
#    fig_view.setUrl(QUrl('temp-plot.html'))
    fig_view.show()
    fig_view.raise_()
    return fig_view


if __name__ == '__main__':
    app = QApplication(sys.argv)

    # Working small plot:
#    fig = go.Figure(data=[{'type': 'scattergl', 'y': [2, 1, 3, 1]}])
    # Not working large plot:
    t = np.arange(0, 200000, 1)
    y = np.sin(t/20000)
    fig = go.Figure(data=[{'type': 'scattergl', 'y': y}])
#    po.plot(fig)

    fig_view = show_qt(fig)
    sys.exit(app.exec_())

最后,这是我试图通过 QUrl 指向磁盘上的本地 html 图来显示大图:

import numpy as np
import plotly.offline as po
import plotly.graph_objs as go

from PyQt5.QtWebEngineWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import sys


def show_qt(fig):
    raw_html = '<html><head><meta charset="utf-8" />'
    raw_html += '<script src="https://cdn.plot.ly/plotly-latest.min.js"></script></head>'
    raw_html += '<body>'
    raw_html += po.plot(fig, include_plotlyjs=False, output_type='div')
    raw_html += '</body></html>'

    fig_view = QWebEngineView()
    # setHtml has a 2MB size limit, need to switch to setUrl on tmp file
    # for large figures.
#    fig_view.setHtml(raw_html)
    fig_view.setUrl(QUrl('temp-plot.html'))
    fig_view.show()
    fig_view.raise_()
    return fig_view


if __name__ == '__main__':
    app = QApplication(sys.argv)

    # Working small plot:
#    fig = go.Figure(data=[{'type': 'scattergl', 'y': [2, 1, 3, 1]}])
    # Not working large plot:
    t = np.arange(0, 200000, 1)
    y = np.sin(t/20000)
    fig = go.Figure(data=[{'type': 'scattergl', 'y': y}])
#    po.plot(fig)

    fig_view = show_qt(fig)
    sys.exit(app.exec_())

该图是通过以下方式生成的:

import numpy as np
import plotly.offline as po
import plotly.graph_objs as go
t = np.arange(0, 200000, 1)
y = np.sin(t/20000)
fig = go.Figure(data=[{'type': 'scattergl', 'y': y}])
po.plot(fig)

标签: pythonpyqtpyqt5plotlyqwebengineview

解决方案


由于此答案表明可能的解决方案是使用 a QWebEngineUrlSchemeHandler,因此在下一节中,我创建了一个类,该类允许您注册通过自定义 url 调用的函数:

qtplotly.py

from PyQt5 import QtCore, QtWebEngineCore, QtWebEngineWidgets

import plotly.offline as po
import plotly.graph_objs as go


class PlotlySchemeHandler(QtWebEngineCore.QWebEngineUrlSchemeHandler):
    def __init__(self, app):
        super().__init__(app)
        self.m_app = app

    def requestStarted(self, request):
        url = request.requestUrl()
        name = url.host()
        if self.m_app.verify_name(name):
            fig = self.m_app.fig_by_name(name)
            if isinstance(fig, go.Figure):
                raw_html = '<html><head><meta charset="utf-8" />'
                raw_html += '<script src="https://cdn.plot.ly/plotly-latest.min.js"></script></head>'
                raw_html += "<body>"
                raw_html += po.plot(fig, include_plotlyjs=False, output_type="div")
                raw_html += "</body></html>"
                buf = QtCore.QBuffer(parent=self)
                request.destroyed.connect(buf.deleteLater)
                buf.open(QtCore.QIODevice.WriteOnly)
                buf.write(raw_html.encode())
                buf.seek(0)
                buf.close()
                request.reply(b"text/html", buf)
                return
        request.fail(QtWebEngineCore.QWebEngineUrlRequestJob.UrlNotFound)


class PlotlyApplication(QtCore.QObject):
    scheme = b"plotly"

    def __init__(self, parent=None):
        super().__init__(parent)
        scheme = QtWebEngineCore.QWebEngineUrlScheme(PlotlyApplication.scheme)
        QtWebEngineCore.QWebEngineUrlScheme.registerScheme(scheme)
        self.m_functions = dict()

    def init_handler(self, profile=None):
        if profile is None:
            profile = QtWebEngineWidgets.QWebEngineProfile.defaultProfile()
        handler = profile.urlSchemeHandler(PlotlyApplication.scheme)
        if handler is not None:
            profile.removeUrlSchemeHandler(handler)

        self.m_handler = PlotlySchemeHandler(self)
        profile.installUrlSchemeHandler(PlotlyApplication.scheme, self.m_handler)

    def verify_name(self, name):
        return name in self.m_functions

    def fig_by_name(self, name):
        return self.m_functions.get(name, lambda: None)()

    def register(self, name):
        def decorator(f):
            self.m_functions[name] = f
            return f

        return decorator

    def create_url(self, name):
        url = QtCore.QUrl()
        url.setScheme(PlotlyApplication.scheme.decode())
        url.setHost(name)
        return url

主文件

import numpy as np
import plotly.graph_objs as go

from PyQt5 import QtCore, QtWidgets, QtWebEngineWidgets

from qtplotly import PlotlyApplication

# PlotlyApplication must be created before the creation
# of QGuiApplication or QApplication
plotly_app = PlotlyApplication()


@plotly_app.register("scatter")
def scatter():
    t = np.arange(0, 200000, 1)
    y = np.sin(t / 20000)
    fig = go.Figure(data=[{"type": "scattergl", "y": y}])
    return fig


@plotly_app.register("scatter2")
def scatter2():
    N = 100000
    r = np.random.uniform(0, 1, N)
    theta = np.random.uniform(0, 2 * np.pi, N)

    fig = go.Figure(
        data=[
            {
                "type": "scattergl",
                "x": r * np.cos(theta),
                "y": r * np.sin(theta),
                "marker": dict(color=np.random.randn(N), colorscale="Viridis"),
            }
        ]
    )
    return fig


@plotly_app.register("scatter3")
def scatter3():
    x0 = np.random.normal(2, 0.45, 30000)
    y0 = np.random.normal(2, 0.45, 30000)

    x1 = np.random.normal(6, 0.4, 20000)
    y1 = np.random.normal(6, 0.4, 20000)

    x2 = np.random.normal(4, 0.3, 20000)
    y2 = np.random.normal(4, 0.3, 20000)

    traces = []
    for x, y in ((x0, y0), (x1, y1), (x2, y2)):
        trace = go.Scatter(x=x, y=y, mode="markers")
        traces.append(trace)

    fig = go.Figure(data=traces)
    return fig


class Widget(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.m_view = QtWebEngineWidgets.QWebEngineView()

        combobox = QtWidgets.QComboBox()
        combobox.currentIndexChanged[str].connect(self.onCurrentIndexChanged)
        combobox.addItems(["scatter", "scatter2", "scatter3"])

        vlay = QtWidgets.QVBoxLayout(self)
        hlay = QtWidgets.QHBoxLayout()
        hlay.addWidget(QtWidgets.QLabel("Select:"))
        hlay.addWidget(combobox)
        vlay.addLayout(hlay)
        vlay.addWidget(self.m_view)
        self.resize(640, 480)

    @QtCore.pyqtSlot(str)
    def onCurrentIndexChanged(self, name):
        self.m_view.load(plotly_app.create_url(name))


if __name__ == "__main__":
    import sys

    app = QtWidgets.QApplication(sys.argv)
    # Init_handler must be invoked before after the creation
    # of QGuiApplication or QApplication
    plotly_app.init_handler()
    w = Widget()
    w.show()
    sys.exit(app.exec_())

结构:

├── main.py
└── qtplotly.py

输出:

在此处输入图像描述 在此处输入图像描述 在此处输入图像描述


推荐阅读