简体   繁体   English

Python:QML 布局内的 matplotlib 绘图

[英]Python: matplotlib plot inside QML layout

Consider the following python3 PyQt code to display an interactive matplotlib graph with toolbar考虑以下 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_()

Now I'd like to achieve the same by using PyQt with QML instead.现在我想通过使用 PyQt 和 QML 来实现同样的效果。 I have some experience with creating QML GUIs in C++ and I really like the fact that the layout code is nicely separated from the core logic of the code.我有一些用 C++ 创建 QML GUI 的经验,我真的很喜欢布局代码与代码的核心逻辑很好地分离的事实。 I have found several examples on how to show plots in PyQt and on how to use Python with QML, but nothing that combines the two.我找到了几个关于如何在 PyQt 中显示绘图以及如何将 Python 与 QML 结合使用的示例,但没有任何一个例子可以将两者结合起来。

To start off, my python and QML snippets look as follows:首先,我的 python 和 QML 片段如下所示:

Python: 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_())

Layout:布局:

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
    }

}

But I am quite lost on how to continue.但我对如何继续感到很迷茫。

More concretely, the question would be: Is there a way to display an interactive matplotlib plot in QML (by interactive I mean not just a figure that has been saved as an image, ideally with the standard toolbar for zoom etc.)更具体地说,问题是:有没有办法在 QML 中显示交互式 matplotlib 图(交互式我的意思不仅仅是一个已保存为图像的图形,理想情况下使用标准工具栏进行缩放等)

Can anyone help?任何人都可以帮忙吗? Or is the combination of QML and plots just simply discouraged ( this question suggests python and QML should work together quite well)?或者只是不鼓励 QML 和绘图的组合( 这个问题表明 python 和 QML 应该很好地协同工作)?

I don't have a full solution, but if you're OK with just displaying charts and the fact that you'll have to provide any interactive controls by yourself, then there's a reasonably simple way to do that.我没有完整的解决方案,但如果您只需要显示图表并且您必须自己提供任何交互式控件这一事实,那么有一种相当简单的方法可以做到这一点。

First of all, you will need to convert your matplotlib chart into a QImage.首先,您需要将 matplotlib 图表转换为 QImage。 Fortunately doing so is surprisingly easy.幸运的是,这样做非常容易。 The canonical backend (renderer) for matplotlib is *Agg`, and it allows you to render your Figure into a memory. matplotlib 的规范后端(渲染器)是 *Agg`,它允许您将图形渲染到内存中。 Just make a suitable Canvas object for you Figure, then call .draw().只需为您的图形制作一个合适的 Canvas 对象,然后调用 .draw()。 The QImage constructor will take generated data directly as inputs. QImage 构造函数将直接将生成的数据作为输入。

canvas = FigureCanvasAgg(figure)
canvas.draw()
    
img = QtGui.QImage(canvas.buffer_rgba(), *canvas.get_width_height(), QtGui.QImage.Format_RGBA8888).copy()

The Qt way to provide that image into QML is to use QQuickImageProvider.将该图像提供给 QML 的 Qt 方法是使用 QQuickImageProvider。 It will get "image name" as input from QML and should provide a suitable image as output.它将从 QML 获取“图像名称”作为输入,并应提供合适的图像作为输出。 This allows you to serve all matplotlib charts in your app with just one Image provider.这允许您仅使用一个图像提供程序来提供应用程序中的所有 matplotlib 图表。 When I was working on a small visualization app for internal use, I ended up with a code like this:当我在开发一个供内部使用的小型可视化应用程序时,我最终得到了这样的代码:

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()

Whenever I need to draw a plot in python code, I just create Figure using this addFigure to give it a name and let the Qt to know about it.每当我需要在 python 代码中绘制绘图时,我只需使用此 addFigure 创建 Figure 并为其命名并让 Qt 知道它。 Once you got Figure, rest of matplotlib drawing happens exactly as usual.一旦你得到了Figure,matplotlib 绘图的其余部分就像往常一样发生。 Make axes and plot.制作坐标轴和绘图。

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

Then in QML code I can simply refer matplotlib image by name ("eventStatisticsPlot")然后在 QML 代码中,我可以简单地按名称引用 matplotlib 图像(“eventStatisticsPlot”)

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

Note that URL is prefixed by "image://" to tell QML that we need to get image from QQuickImageProvider and includes name ("perflog") of a particular provider to use.请注意,URL 以“image://”为前缀,告诉 QML 我们需要从 QQuickImageProvider 获取图像,并包含要使用的特定提供程序的名称(“perflog”)。 For this stuff to work we need to register our provider during QML initialization with a call to addImageProvider.为了让这些东西正常工作,我们需要在 QML 初始化期间通过调用 addImageProvider 来注册我们的提供程序。 For example,例如,

engine = QtQml.QQmlApplicationEngine()
engine.addImageProvider('perflog', qt_model.imageProvider)
engine.load(QtCore.QUrl("PerflogViewer.qml"))

At this point you should be able to see static graphs shown, but they will not be updated properly because Image component in QML assumes that image that we provide does not change.此时您应该能够看到显示的静态图,但它们不会正确更新,因为 QML 中的图像组件假定我们提供的图像不会更改。 I found no good solution for it, but an ugly workaround is fairly simple.我没有找到好的解决方案,但是一个丑陋的解决方法相当简单。 I added a signal called eventStatisticsPlotChanged to my helper class that exposes Python app data to QML and .emit() it whenever the relevant plot is changed.我向我的帮助程序类添加了一个名为eventStatisticsPlotChanged的​​信号,该类将 Python 应用程序数据公开给 QML,并在相关绘图发生更改时对其进行.emit()处理。 Eg here's a chunk of code where I get data from QML on a time interval selected by user.例如,这是一段代码,我在用户选择的时间间隔内从 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()

See that .emit() in the end?最后看到 .emit() 了吗? In QML this event forces image to reload URL like this:在 QML 中,此事件强制图像重新加载 URL,如下所示:

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

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

Connections {
    target: myDataSourceObjectExposedFromPython
    onEventStatisticsPlotChanged: eventStatisticsPlot.reload()
}

So whenever user moves a control, following happens:因此,每当用户移动控件时,都会发生以下情况:

  • QML sends updated time interval to my data source via selectTimeRange() call QML 通过 selectTimeRange() 调用向我的数据源发送更新的时间间隔
  • My code calls .set_xlim on appopriate matplotlib object and emit() a signal to notify QML that chart changed我的代码在适当的 matplotlib 对象上调用 .set_xlim 并发出一个信号以通知 QML 图表已更改
  • QML queries my imageProvider for updated chart image QML 查询我的 imageProvider 以获取更新的图表图像
  • My code renders matplotlib chart into new QImage with Agg and passes it to Qt我的代码使用 Agg 将 matplotlib 图表渲染为新的 QImage 并将其传递给 Qt
  • QML shows that image to user QML 向用户显示该图像

It might sound a bit complicated, but its actually easy to design and use.听起来可能有点复杂,但实际上很容易设计和使用。

Here's an example of how all this looks in our small visualization app.这是一个示例,说明所有这些在我们的小型可视化应用程序中的外观。 That's pure Python + QML, with pandas used to organize data and matplotlib to show it.那是纯 Python + QML,用 pandas 来组织数据和 matplotlib 来显示它。 Scroll-like element on bottom of the screen essentially redraws chart on every event and it happens so fast that it feels real-time.屏幕底部的类似滚动的元素本质上会在每个事件上重绘图表,并且它发生得如此之快以至于感觉是实时的。

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

I also tried to use SVG as a way to feed vector image into QML.我还尝试使用 SVG 作为将矢量图像输入 QML 的一种方式。 It's also possible and it also works.这也是可能的,它也有效。 Matplotlib offers SVG backend (matplotlib.backends.backend_svg) and Image QML component support inline SVG data as a Source. Matplotlib 提供 SVG 后端 (matplotlib.backends.backend_svg) 和 Image QML 组件支持将内联 SVG 数据作为 Source。 The SVG data is text so it can be easily passed around between python and QML. SVG 数据是文本,因此可以在 python 和 QML 之间轻松传递。 You can update (source) field with new data and image will redraw itself automatically, you can rely on data binding.您可以使用新数据更新(源)字段,图像将自动重绘,您可以依赖数据绑定。 It could've worked quite well, but sadly SVG support in Qt 4 and 5 is poor.它本来可以很好地工作,但遗憾的是 Qt 4 和 5 中的 SVG 支持很差。 Clipping is not supported (charts will go out of the axes);不支持剪裁(图表会超出坐标轴); resizing Image does not re-render SVG but resizes pixel image of it; resizing Image 不会重新渲染 SVG,但会调整其像素图像的大小; changing SVG causes image to blink;更改 SVG 会导致图像闪烁; performance is poor.性能很差。 Maybe this will change one day later, but for now stick to agg backend.也许这会在一天后改变,但现在坚持 agg 后端。

I really love design of both matlpotlib and Qt.我真的很喜欢 matlpotlib 和 Qt 的设计。 It's smart and it meshes well without too much effort or boilerplate code.它很聪明,无需太多努力或样板代码即可很好地啮合。

It's in a fairly basic state, but https://github.com/jmitrevs/matplotlib_backend_qtquick provides a workable model to start from.它处于相当基本的状态,但https://github.com/jmitrevs/matplotlib_backend_qtquick提供了一个可行的模型。

To quickly summarize the gist of the example provided with it:快速总结随附示例的要点:

  • The library provides the types FigureCanvasQtQuickAgg and NavigationToolbar2QtQuick .该库提供了FigureCanvasQtQuickAggNavigationToolbar2QtQuick类型。
  • The FigureCanvasQtQuickAgg type is registered with QML: FigureCanvasQtQuickAgg类型在 QML 中注册:
     QtQml.qmlRegisterType(FigureCanvasQtQuickAgg, "Backend", 1, 0, "FigureCanvas")
  • This allows you to use it from within QML:这允许您在 QML 中使用它:
     FigureCanvas { id: mplView objectName : "figure" dpi_ratio: Screen.devicePixelRatio anchors.fill: parent }
    The objectName property allows the canvas instance to be found from within the Python code. objectName属性允许从 Python 代码中找到画布实例。
  • The toolbar is made out of QML buttons,工具栏由 QML 按钮组成,
  • In the demo, they provide a DisplayBridge Python class that is linked to the canvas and is responsible for the actual plotting and for forwarding events from the various toolbar buttons在演示中,他们提供了一个DisplayBridge Python 类,该类链接到画布,负责实际绘图和从各种工具栏按钮转发事件

Internally, the FigureCanvasQtQuickAgg backend is a QQuickPaintedItem .在内部, FigureCanvasQtQuickAgg后端是QQuickPaintedItem In its paint function it copies data from the renderer attribute of the matplotlib FigureCanvasAgg base class into a QImage and the QImage is then painted.在其paint函数中,它将数据从 matplotlib FigureCanvasAgg基类的renderer属性复制到QImage中,然后绘制QImage This is a fairly similar design to how the QWidget version of matplotlib works.这与 matplotlib 的QWidget版本的工作方式非常相似。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM