Python:QML 布局内的 matplotlib 绘图

unt*_*gam 7 python matplotlib qml

考虑以下 python3 PyQt 代码来显示带有工具栏的交互式 matplotlib 图形

import sys, sip
import numpy as np
from PyQt5 import QtGui, QtWidgets
from PyQt5.Qt import *

from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
import matplotlib.pyplot as plt

app = QApplication(sys.argv)
top = QWidget()

fig = plt.figure()
ax = fig.gca()
x = np.linspace(0,5,100)
ax.plot(x,np.sin(x))
canvas = FigureCanvas(fig)
toolbar = NavigationToolbar(canvas, top)

def pick(event):
    if (event.xdata is None) or (event.ydata is None): return
    ax.plot([0,event.xdata],[0,event.ydata])
    canvas.draw()

canvas.mpl_connect('button_press_event', pick)

layout = QtWidgets.QVBoxLayout()
layout.addWidget(toolbar)
layout.addWidget(canvas)
top.setLayout(layout)
top.show()

app.exec_()
Run Code Online (Sandbox Code Playgroud)

现在我想通过使用 PyQt 和 QML 来实现相同的目的。我有一些用 C++ 创建 QML GUI 的经验,而且我真的很喜欢布局代码与代码核心逻辑很好地分离的事实。我找到了几个关于如何在 PyQt 中显示绘图以及如何将 Python 与 QML 结合使用的示例,但没有任何示例将两者结合起来。

首先,我的 python 和 QML 片段如下所示:

Python:

import sys, sip
import numpy as np
from PyQt5 import QtGui, QtWidgets
from PyQt5.Qt import *

from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
import matplotlib.pyplot as plt

app = QApplication(sys.argv)
engine = QQmlApplicationEngine()
engine.load(QUrl('layout.qml'))
root = engine.rootObjects()[0]
root.show()
sys.exit(app.exec_())
Run Code Online (Sandbox Code Playgroud)

布局:

import QtQuick 2.7
import QtQuick.Controls 1.4

ApplicationWindow {

    visible: true
    width: 400
    height: 400

    Canvas {
        // canvas may not be the right choice here
        id: mycanvas
        anchors.fill: parent
    }

}
Run Code Online (Sandbox Code Playgroud)

但我完全不知道如何继续。

更具体地说,问题是:有没有一种方法可以在 QML 中显示交互式 matplotlib 图(所谓交互式,我的意思不仅仅是保存为图像的图形,最好使用标准工具栏进行缩放等)

有人可以帮忙吗?或者只是不鼓励将 QML 和绘图结合起来(这个问题表明 python 和 QML 应该很好地协同工作)?

Ser*_*rov 4

我没有完整的解决方案,但如果您只需要显示图表并且您必须自己提供任何交互式控件,那么有一个相当简单的方法可以做到这一点。

首先,您需要将 matplotlib 图表转换为 QImage。幸运的是,这样做非常容易。matplotlib 的规范后端(渲染器)是 *Agg`,它允许您将图形渲染到内存中。只需为您的Figure创建一个合适的Canvas对象,然后调用.draw()即可。QImage 构造函数将直接将生成的数据作为输入。

canvas = FigureCanvasAgg(figure)
canvas.draw()
    
img = QtGui.QImage(canvas.buffer_rgba(), *canvas.get_width_height(), QtGui.QImage.Format_RGBA8888).copy()
Run Code Online (Sandbox Code Playgroud)

将该图像提供到 QML 中的 Qt 方法是使用 QQuickImageProvider。它将获取“图像名称”作为 QML 的输入,并应提供合适的图像作为输出。这允许您仅使用一个图像提供程序来提供应用程序中的所有 matplotlib 图表。当我开发一个供内部使用的小型可视化应用程序时,我最终得到了这样的代码:

import PyQt5.QtCore as QtCore
import PyQt5.QtGui as QtGui
import PyQt5.QtQuick as QtQuick
import PyQt5.QtQml as QtQml

from matplotlib.figure import Figure
from matplotlib.backends.backend_agg import FigureCanvasAgg

class MatplotlibImageProvider(QtQuick.QQuickImageProvider):
    figures = dict()

    def __init__(self):
        QtQuick.QQuickImageProvider.__init__(self, QtQml.QQmlImageProviderBase.Image)

    def addFigure(self, name, **kwargs):
        figure = Figure(**kwargs)
        self.figures[name] = figure
        return figure

    def getFigure(self, name):
        return self.figures.get(name, None)

    def requestImage(self, p_str, size):
        figure = self.getFigure(p_str)
        if figure is None:
            return QtQuick.QQuickImageProvider.requestImage(self, p_str, size)

        canvas = FigureCanvasAgg(figure)
        canvas.draw()

        w, h = canvas.get_width_height()
        img = QtGui.QImage(canvas.buffer_rgba(), w, h, QtGui.QImage.Format_RGBA8888).copy()

        return img, img.size()
Run Code Online (Sandbox Code Playgroud)

每当我需要在 python 代码中绘制绘图时,我只需使用此 addFigure 创建Figure 并为其命名并让 Qt 了解它。一旦你得到了Figure,matplotlib绘图的其余部分就会像平常一样发生。制作坐标轴并绘图。

self.imageProvider = MatplotlibImageProvider()
figure = self.imageProvider.addFigure("eventStatisticsPlot", figsize=(10,10))
ax = figure.add_subplot(111)
ax.plot(x,y)
Run Code Online (Sandbox Code Playgroud)

然后在 QML 代码中我可以简单地按名称引用 matplotlib 图像(“eventStatisticsPlot”)

Image {
    source: "image://perflog/eventStatisticsPlot"
}
Run Code Online (Sandbox Code Playgroud)

请注意,URL 以“image://”为前缀,告诉 QML 我们需要从 QQuickImageProvider 获取图像,并包含要使用的特定提供程序的名称(“perflog”)。为了让这些东西发挥作用,我们需要在 QML 初始化期间通过调用 addImageProvider 来注册我们的提供程序。例如,

engine = QtQml.QQmlApplicationEngine()
engine.addImageProvider('perflog', qt_model.imageProvider)
engine.load(QtCore.QUrl("PerflogViewer.qml"))
Run Code Online (Sandbox Code Playgroud)

此时,您应该能够看到显示的静态图,但它们不会正确更新,因为 QML 中的图像组件假设我们提供的图像不会更改。我没有找到好的解决方案,但一个丑陋的解决方法相当简单。我添加了一个调用到我的帮助程序类的信号eventStatisticsPlotChanged,该信号将 Python 应用程序数据公开给 QML,并且.emit()每当相关绘图发生更改时都会将其公开。例如,这是一段代码,我在其中按照用户选择的时间间隔从 QML 获取数据。

@QtCore.pyqtSlot(float, float)
def selectTimeRange(self, min_time, max_time):
    self.selectedTimeRange = (min_time, max_time)
    _, ax, _ = self.eventStatisticsPlotElements
    ax.set_xlim(*self.selectedTimeRange)
    self.eventStatisticsPlotChanged.emit()
Run Code Online (Sandbox Code Playgroud)

最后看到 .emit() 了吗?在 QML 中,此事件强制图像重新加载 URL,如下所示:

Image {
    source: "image://perflog/eventStatisticsPlot"

    cache: false
    function reload() { var t = source; source = ""; source = t; }
}

Connections {
    target: myDataSourceObjectExposedFromPython
    onEventStatisticsPlotChanged: eventStatisticsPlot.reload()
}
Run Code Online (Sandbox Code Playgroud)

因此,每当用户移动控件时,就会发生以下情况:

  • QML 通过 selectTimeRange() 调用将更新的时间间隔发送到我的数据源
  • 我的代码在适当的 matplotlib 对象上调用 .set_xlim 并发出()一个信号以通知 QML 图表已更改
  • QML 查询我的 imageProvider 以获取更新的图表图像
  • 我的代码使用 Agg 将 matplotlib 图表渲染成新的 QImage 并将其传递给 Qt
  • QML 向用户显示该图像

听起来可能有点复杂,但实际上很容易设计和使用。

这是一个示例,展示了这一切在我们的小型可视化应用程序中的外观。这是纯粹的 Python + QML,用 pandas 来组织数据,用 matplotlib 来显示数据。屏幕底部的类似滚动的元素本质上会重新绘制每个事件的图表,并且它发生得如此之快,以至于感觉是实时的。

使用 Matplotlib 可视化数据的示例 QML 应用程序

我还尝试使用 SVG 作为将矢量图像输入 QML 的方法。这也是可能的,而且也有效。Matplotlib 提供 SVG 后端 (matplotlib.backends.backend_svg) 和 Image QML 组件支持内联 SVG 数据作为源。SVG 数据是文本,因此可以在 python 和 QML 之间轻松传递。您可以使用新数据更新(源)字段,图像将自动重绘,您可以依赖数据绑定。它本来可以很好地工作,但遗憾的是 Qt 4 和 5 中的 SVG 支持很差。不支持剪切(图表将超出轴);调整图像大小不会重新渲染 SVG,而是调整其像素图像的大小;更改 SVG 会导致图像闪烁;性能很差。也许一天后这会改变,但现在坚持使用聚合后端。

我真的很喜欢 matlpotlib 和 Qt 的设计。它很智能,并且不需要太多的努力或样板代码就可以很好地配合。