Python:QML布局内的matplotlib图 [英] Python: matplotlib plot inside QML layout

查看:128
本文介绍了Python:QML布局内的matplotlib图的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

import sys, sip将numpy导入为np从PyQt5导入QtGui,QtWidgets从 PyQt5.Qt 导入 *从matplotlib.backends.backend_qt5agg导入FigureCanvasQTAgg作为FigureCanvas从matplotlib.backends.backend_qt5agg导入NavigationToolbar2QT作为NavigationToolbar导入matplotlib.pyplot作为plt应用= QApplication(sys.argv)顶部 = QWidget()无花果= plt.figure()斧= fig.gca()x = np.linspace(0,5,100)ax.plot(x,np.sin(x))canvas = FigureCanvas(图)工具栏 = 导航工具栏(画布,顶部)定义选择(事件):if (event.xdata is None) or (event.ydata is None): 返回ax.plot([0,event.xdata],[0,event.ydata])canvas.draw()canvas.mpl_connect('button_press_event',选择)布局 = QtWidgets.QVBoxLayout()layout.addWidget(工具栏)layout.addWidget(画布)top.setLayout(布局)顶部显示()app.exec_()

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

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

蟒蛇:

import sys, sip将numpy导入为np从 PyQt5 导入 QtGui、QtWidgets从PyQt5.Qt导入*从matplotlib.backends.backend_qt5agg导入FigureCanvasQTAgg作为FigureCanvas从matplotlib.backends.backend_qt5agg导入NavigationToolbar2QT作为NavigationToolbar导入matplotlib.pyplot作为plt应用= QApplication(sys.argv)引擎= QQmlApplicationEngine()engine.load(QUrl('layout.qml'))root = engine.rootObjects()[0]根显示()sys.exit(app.exec_())

布局:

 导入QtQuick 2.7导入QtQuick.Controls 1.4ApplicationWindow {可见:真实宽度:400高度:400帆布 {//这里的画布可能不是正确的选择编号:mycanvasanchors.fill:父级}}

但是我对如何继续感到迷茫.

更具体地说,问题是:有没有办法在 QML 中显示交互式 matplotlib 图(交互式我指的不仅仅是已保存为图像的图形,理想情况下还有用于缩放等的标准工具栏)

有人可以帮忙吗?还是只是不鼓励使用QML和图表的组合(

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

我真的很喜欢matlpotlib和Qt的设计.它很聪明,而且网格划分良好,无需花费太多精力或样板代码.

Consider the following python3 PyQt code to display an interactive matplotlib graph with toolbar

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. 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. 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.

To start off, my python and QML snippets look as follows:

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

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

解决方案

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. 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. Just make a suitable Canvas object for you Figure, then call .draw(). The QImage constructor will take generated data directly as inputs.

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. It will get "image name" as input from QML and should provide a suitable image as output. This allows you to serve all matplotlib charts in your app with just one Image provider. 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. Once you got Figure, rest of matplotlib drawing happens exactly as usual. 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")

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. For this stuff to work we need to register our provider during QML initialization with a call to 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. 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. E.g. here's a chunk of code where I get data from QML on a time interval selected by user.

@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? In QML this event forces image to reload URL like this:

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
  • My code calls .set_xlim on appopriate matplotlib object and emit() a signal to notify QML that chart changed
  • QML queries my imageProvider for updated chart image
  • My code renders matplotlib chart into new QImage with Agg and passes it to Qt
  • QML shows that image to user

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. Scroll-like element on bottom of the screen essentially redraws chart on every event and it happens so fast that it feels real-time.

I also tried to use SVG as a way to feed vector image into 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. The SVG data is text so it can be easily passed around between python and 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. Clipping is not supported (charts will go out of the axes); resizing Image does not re-render SVG but resizes pixel image of it; changing SVG causes image to blink; performance is poor. Maybe this will change one day later, but for now stick to agg backend.

I really love design of both matlpotlib and Qt. It's smart and it meshes well without too much effort or boilerplate code.

这篇关于Python:QML布局内的matplotlib图的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆