使用PyQt设计程序界面过程中不可避免地需要将数据通过图形化的方式显示出来,对于一些实时性较强的系统,还需要能够动态地更新这些图形。然而Matplotlab对于曲线图、柱形图、二维图、三维图的绘制和更新各有不同的方法。
我在工作中就遇到了这个问题,花了不少精力汇总了使用Matplotlib实现曲线、柱形图、二维图以及三维图在PyQt5界面中的绘制方法以及使其动态更新的方法。本系列博客分成两篇,第一篇介绍以上四种图形的静态显示方法,下一篇介绍各图形的动态更新方法。
下面我们来介绍静态图片的显示方法。
当然首先,我们要配置好工作的环境,我使用的是python3.6+PyCharm,安装好PyQt5、QtDesigner和matplotlib扩展包,安装方式可以参考我之前的博客。
使用PyQt5绘制Matplotlib图形通常可以使用QGroupBox、QGraphicsView等方式,各有不同的用法,我使用的是QGroupBox,个人感觉它使用起来最简单。
接下来,我们先使用QtDesigner创建一个主窗口,然后在其中创建四个QGroupBox分别用来显示曲线图、柱形图、二维图和三维图。创建后的效果如下图所示:
将其保存为DataDIsplayUI.ui,然后通过PyUIC将其转换成DataDIsplayUI.py文件,具体实现方法可以参考链接。转换后的py代码如下所示,当然我们其实不用关心它的具体内容,只要会使用它就可以了。
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'DataDisplayUI.ui'
#
# Created by: PyQt5 UI code generator 5.11.3
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(800, 600)
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.gridLayout = QtWidgets.QGridLayout(self.centralwidget)
self.gridLayout.setObjectName("gridLayout")
self.LineDisplayGB = QtWidgets.QGroupBox(self.centralwidget)
self.LineDisplayGB.setObjectName("LineDisplayGB")
self.gridLayout.addWidget(self.LineDisplayGB, 0, 0, 1, 1)
self.BarDisplayGB = QtWidgets.QGroupBox(self.centralwidget)
self.BarDisplayGB.setObjectName("BarDisplayGB")
self.gridLayout.addWidget(self.BarDisplayGB, 0, 1, 1, 1)
self.ImageDisplayGB = QtWidgets.QGroupBox(self.centralwidget)
self.ImageDisplayGB.setObjectName("ImageDisplayGB")
self.gridLayout.addWidget(self.ImageDisplayGB, 1, 0, 1, 1)
self.SurfaceDisplayGB = QtWidgets.QGroupBox(self.centralwidget)
self.SurfaceDisplayGB.setObjectName("SurfaceDisplayGB")
self.gridLayout.addWidget(self.SurfaceDisplayGB, 1, 1, 1, 1)
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 18))
self.menubar.setObjectName("menubar")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
self.LineDisplayGB.setTitle(_translate("MainWindow", "Line Display"))
self.BarDisplayGB.setTitle(_translate("MainWindow", "Bar Display"))
self.ImageDisplayGB.setTitle(_translate("MainWindow", "Image Display"))
self.SurfaceDisplayGB.setTitle(_translate("MainWindow", "3D Surface Display"))
接下来,创建一个新的py文件,输入以下代码:
from DataDisplayUI import Ui_MainWindow
from PyQt5.QtWidgets import QApplication,QMainWindow,QGridLayout
from PyQt5.QtCore import QTimer
import sys,time
import numpy as np
from mpl_toolkits.mplot3d import Axes3D
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
from matplotlib.lines import Line2D
import matplotlib
import matplotlib.cbook as cbook
class ImgDisp(QMainWindow,Ui_MainWindow):
def __init__(self,parent=None):
super(ImgDisp,self).__init__(parent)
self.setupUi(self)
if __name__=='__main__':
app=QApplication(sys.argv)
ui=ImgDisp()
ui.show()
sys.exit(app.exec_())
保存后运行,即可看到我们设计的程序界面,如下图所示。关于上面这段代码,需要进行一下说明,第一行导入的是我们刚使用PyUIC生成的py文件中的界面类,QTimer用来将来动态更新我们的图片,Matplotlib模块用来绘制图形。
接下来,仍然在这个文件中,我们定义一个新的类用来创建画板,内容如下:
class Figure_Canvas(FigureCanvas):
def __init__(self,parent=None,width=3.9,height=2.7,dpi=100):
self.fig=Figure(figsize=(width,height),dpi=100)
super(Figure_Canvas,self).__init__(self.fig)
self.ax=self.fig.add_subplot(111)
def test(self):
x=[1,2,3,4,5,6,7]
y=[2,1,3,5,6,4,3]
self.ax.plot(x,y)
这个类继承自FigureCanvas类,其中width和height分别定义了画板的宽和高,单位是100(dpi)像素。这里创建了一个图self.fig,并在其上添加了一个轴self.ax,最终的图形是显示在轴里的,一个图里可以添加多个轴,这些概念与Matlab中的figure和axis一样。self.test函数调用后会在轴上画一个曲线图,用来测试画板创建是否成功。
接下来,回到我们的界面类中,定义一些新的函数在四个QGroupBox中分别添加画板:
class ImgDisp(QMainWindow,Ui_MainWindow):
def __init__(self,parent=None):
super(ImgDisp,self).__init__(parent)
self.setupUi(self)
self.Init_Widgets()
def Init_Widgets(self):
self.PrepareSamples()
self.PrepareLineCanvas()
self.PrepareBarCanvas()
self.PrepareImgCanvas()
self.PrepareSurfaceCanvas()
def PrepareSamples(self):
self.x = np.arange(-4, 4, 0.02)
self.y = np.arange(-4, 4, 0.02)
self.X, self.Y = np.meshgrid(self.x, self.y)
self.z = np.sin(self.x)
self.R = np.sqrt(self.X ** 2 + self.Y ** 2)
self.Z = np.sin(self.R)
def PrepareLineCanvas(self):
self.LineFigure = Figure_Canvas()
self.LineFigureLayout = QGridLayout(self.LineDisplayGB)
self.LineFigureLayout.addWidget(self.LineFigure)
self.LineFigure.ax.set_xlim(-4, 4)
self.LineFigure.ax.set_ylim(-1, 1)
self.line = Line2D(self.x, self.z)
self.LineFigure.ax.add_line(self.line)
def PrepareBarCanvas(self):
self.BarFigure = Figure_Canvas()
self.BarFigureLayout = QGridLayout(self.BarDisplayGB)
self.BarFigureLayout.addWidget(self.BarFigure)
self.BarFigure.ax.set_xlim(-4, 4)
self.BarFigure.ax.set_ylim(-1, 1)
self.bar = self.BarFigure.ax.bar(np.arange(-4, 4, 0.5), np.sin(np.arange(-4, 4, 0.5)), width=0.4)
self.patches = self.bar.patches
def PrepareImgCanvas(self):
self.ImgFigure = Figure_Canvas()
self.ImgFigureLayout = QGridLayout(self.ImageDisplayGB)
self.ImgFigureLayout.addWidget(self.ImgFigure)
self.ImgFig = self.ImgFigure.ax.imshow(self.Z, cmap='bone')
self.ImgFig.set_clim(-0.8,0.8)
def PrepareSurfaceCanvas(self):
self.SurfFigure = Figure_Canvas()
self.SurfFigureLayout = QGridLayout(self.SurfaceDisplayGB)
self.SurfFigureLayout.addWidget(self.SurfFigure)
self.SurfFigure.ax.remove()
self.ax3d = self.SurfFigure.fig.gca(projection='3d')
self.Surf = self.ax3d.plot_surface(self.X, self.Y, self.Z, cmap='rainbow')
由于我们要创建四个不同的画板,而这些画板的创建方式都大同小异,因此定义一个self.Init_Widgets()函数,将所有的画板创建函数都放进去。其中self.PrepareSamples()函数可以生成一些数据用来绘制图形。
下面结合刚才的代码,详细介绍一下画板的创建和不同图形的绘制方法。
四个画板的创建方式大同小异:
都是先实例一个我们刚才定义的画板类:
self.LineFigure = Figure_Canvas()
然后在相应的QGroupBox中添加一个栅格布局:
self.LineFigureLayout = QGridLayout(self.LineDisplayGB)
然后,将画板添加到布局中:
self.LineFigureLayout.addWidget(self.LineFigure)
设置坐标轴的显示范围:
self.LineFigure.ax.set_xlim(-4, 4)
self.LineFigure.ax.set_ylim(-1, 1)
接下来将我们要显示的数据绘制到画板上,对不同过的图形就需要不同过的方法了。
self.line = Line2D(self.x, self.z)
self.LineFigure.ax.add_line(self.line)
需要注意的是如果只是显示静态的曲线,也可以使用plot函数:
self.LineFigure.ax.plot(self.x, self.z)
而为了便于后续对曲线的动态更新,我采用先定义好曲线,然后在将其添加到轴上的方式。
对于柱形图,显示方式为:
self.bar = self.BarFigure.ax.bar(np.arange(-4, 4, 0.5), np.sin(np.arange(-4, 4, 0.5)), width=0.4)
其中np.arange(-4, 4, 0.5)定义了16个x坐标,np.sin(np.arange(-4, 4, 0.5))为对应的数据柱的高度,width=0.4定义数据柱的宽度。需要注意的是,这里我们需要记录下bar函数的返回值,然后通过代码self.patches = self.bar.patches
获取柱形图的patches,用于对其数据进行更新。柱形图的patches是一个矩阵的列表,记录了组成柱形图的每一个矩形的参数。
对于二维图,显示方式如下所示:
self.ImgFig = self.ImgFigure.ax.imshow(self.Z, cmap='bone')
self.ImgFig.set_clim(-0.8,0.8)
使用imshow函数即可,同样地,我们也需要保留它的返回值,其中,cmap定义使用的colormap,set_clim函数定义显示的Z的大小范围。
对于三维图,我们需要重新定义一个三维的轴,因此需要先删除原来的轴。
self.SurfFigure.ax.remove()
self.ax3d = self.SurfFigure.fig.gca(projection='3d')
self.Surf = self.ax3d.plot_surface(self.X, self.Y, self.Z, cmap='rainbow')