Matplotlib pyplot嵌入PYQT5的实战与反思

‘’‘第一次使用csdn的博客内容,写的有问题的地方以后再行更改’’’
更新 2019/4/26 20:56
更新 2019/5/28 01:11 更新刷新界面的方法,详见Axes3d部分。

最近因为毕业设计的原因接触到pyqt5的库,用了一段时间,就想把使用过程中发生的事情记录下来,以供日后学习使用。

0.参考书目
1.《PyQt5快速开发与实战》:pyqt5的内容几乎都是参考了这里。脚本之x下的盗版,这书的特点就是简单易懂,也是我选pyqt5不选pyqt4的原因之一,找到介绍pyqt4的那本书太难啦qwq,该介绍的地方大部分都有了。但是,也仅仅只能作为工具书使用(因为应用级别的复杂的地方很少),用win10自带的Edge打开查函数很方便。
2.《Python科学计算(第二版)》:numpy和分形的相关知识主要参考了这里。相较于第一版语言上更加简洁,内容上更加丰富,清晰详细的例程,csdn上都有下载。另外数学不太好的同学(比如我)也可以多研究一下里面的例程。
3.《PYTHON数据可视化编程实战》:matplot的内容(不包括嵌入到pyqt5)相当一部分的问题都是参考这里的内容。
4.《廖雪峰的Python2.7教程》:对于python刚刚入门的选手(比如我)推荐使用,补一下python的基础。值得一提的是,pyqt5仅仅支持python 3.x,但是其实都没有特别多的区别,并且3.x更加安全和规范。

1.前期准备与pyqt5安装
使用的主要IDE为:python 3.5+pyqt5(来自anacoda),pycharm community(也就是学生版除了不能和数据库共通没啥特别的区别),eric6+qtdesigner(前期使用)
在安装过程中,pyqt5显得不那么友好。参考书目1的相关内容。
值得一提的地方:
_1.贴出一个博客:https://blog.csdn.net/aaazz47/article/details/71302907 ,介绍的很详细,侵权删除。(一定要一步一步执行啊,跳步会死,编程的时候也是这样,拒绝跳跃性思维模块化一步一步来很重要!)
_2.一些误区:就我使用的情况来看,qtdesigner并不能解决一些根本性的问题,只是开发ui的话用eric6+qtdesigner挺好的,但是后期发现用处不大。因为你懂了那些框架的知识以后其实并不需要qtdesigner来帮助你构建简单的框架,更复杂的东西它也提供不了,说实话就是一个快速开发的工具。并且在import qtdesigner生成的py文件的时候你需要一些特殊的方法这里会在后面进行介绍。

2.框架
更新 2019/4/26 20:56
摸鱼一周,今天把写完的框架进行更新。
之前写到QtDesigner对于已经初步进阶的选手没有意义,这个是不对的,因为它对于框架来说有着很大的意义。
贴出一个博客,简单介绍了子窗口嵌入到主窗口的内容:
https://blog.csdn.net/hubz131/article/details/79357138
我一开始也是百般不解为什么子窗口嵌入的时候不能直接添加到Gridlayout中,断断续续摸鱼实验了两天,发现只有在使用QT得到的界面才能嵌入,这也是为什么那个博客下面两条评论的原因。
贴出代码:


from Ui_MainWindow import Ui_MainWindow
import Matplot
import Derivative_Culculate
import Lorenz_Culculate

class Main(QMainWindow,Ui_MainWindow):
    def __init__(self):
        super(Main,self).__init__()
        self.setupUi(self)

        self.actionMatplot_Mandelort.triggered.connect(self.Show1)
        self.actionDerivaive_Calculator.triggered.connect(self.Show2)
        self.actionAx3D_Lorenz.triggered.connect(self.Show3)

    def Show1(self):
        self.child_Close()
        self.resize(525,600)
        self.child1 = Matplot.Main_window()
        self.gridLayout.addWidget(self.child1)
        self.child1.show()

    def Show2(self):
        self.child_Close()
        self.resize(525,375)
        self.child2 = Derivative_Culculate.Main_window()
        self.gridLayout.addWidget(self.child2)
        self.child2.show()

    def Show3(self):
        self.child_Close()
        self.resize(525,600)
        self.child3 = Lorenz_Culculate.Main_window()
        self.gridLayout.addWidget(self.child3)
        self.child3.show()

    def child_Close(self):
        try:
            self.child1.close()
            self.child2.close()
            self.child3.close()
            self.gridLayout.removeWidget(self.child1)
            self.gridLayout.removeWidget(self.child2)
            self.gridLayout.removeWidget(self.child3)
        except:
            pass


if __name__ == '__main__':
    app = QApplication(sys.argv)
    MainWindow = Main()
    MainWindow.show()
    sys.exit(app.exec_())

ps:
try写的不规范,但是开发的话挺好用的,哈哈。
resize的所有参数都不是从原来窗口获取的,获取的话用size方法得到的是一个地址,用ctype好像也读不出来地址里面存的是啥,因为我不知道到底是什么数据类型的。
因为矩阵部分是分页面的暂时不添加到工具箱中,而且矩阵计算中尚且不能对向量的叉乘进行计算。
之后也会继续添加其他功能。

3.各个模块的实现
主要包括:QTableWidget实现矩阵计算,pyplot嵌入pyqt5实现mandelbrot分形图案及其局部放大,pyplot嵌入pyqt5实现微分方程计算与lateX格式显示,pyplot嵌入pyqt5实现微分方程的计算与3d图形显示
_1.矩阵计算
寒假的时候开始准备pyqt的相关知识,也没有上手就把书粗略过了一遍就开始摸鱼,之后来学校了开始了一开始有点痛苦的开发之路。一开始也比较年轻(现在也是这样),感觉qtdesigner好方便啊,于是就用qtdesigner制作了第一个ui界面。

可是残酷的事实是qtdesigner的代码虽然很格式化方便了阅读,但是在逻辑部分新手完全无法掌握它的美妙之处(使用类内定义方法就显得非常重要,也是在开发第二个部分的时候我才感受到这一点)。并且,因为格式化的很彻底,就显得代码很冗长,特别是retranslate有什么意义吗。。。并且,这里没有用到布局,算是很失败的地方。

from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import QApplication,QWidget,QTableWidgetItem,QTableWidget

import numpy as np

#from Ui_UI_mat_cal_option import Ui_calculate_option
#from Ui_UI_mat_cal_option_1 import Ui_calculate_option_1

class calculate_option(object):
	def setupUi(self, calculate_option):
		calculate_option.setObjectName("calculate_option")
		calculate_option.resize(347, 324)
		self.pushButton = QtWidgets.QPushButton(calculate_option)
		self.pushButton.setGeometry(QtCore.QRect(80, 70, 75, 23))
		self.pushButton.setObjectName("pushButton")
		self.pushButton_2 = QtWidgets.QPushButton(calculate_option)
		self.pushButton_2.setGeometry(QtCore.QRect(80, 120, 75, 23))
		self.pushButton_2.setObjectName("pushButton_2")
		self.pushButton_3 = QtWidgets.QPushButton(calculate_option)
		self.pushButton_3.setGeometry(QtCore.QRect(200, 70, 75, 23))
		self.pushButton_3.setObjectName("pushButton_3")
		self.pushButton_4 = QtWidgets.QPushButton(calculate_option)
		self.pushButton_4.setGeometry(QtCore.QRect(200, 120, 75, 23))
		self.pushButton_4.setObjectName("pushButton_4")
		self.textEdit = QtWidgets.QLineEdit(calculate_option)
		self.textEdit.setGeometry(QtCore.QRect(170, 180, 104, 31))
		self.textEdit.setObjectName("textEdit")

		self.textEdit.textChanged.connect(self.btn5_clk) #输入数值改变n的大小
	
		self.label = QtWidgets.QLabel(calculate_option)
		self.label.setGeometry(QtCore.QRect(90, 180, 51, 20))
		self.label.setObjectName("label")
		self.pushButton_5 = QtWidgets.QPushButton(calculate_option)
		self.pushButton_5.setGeometry(QtCore.QRect(200, 240, 75, 23))
		self.pushButton_5.setObjectName("pushButton_5")
		self.retranslateUi(calculate_option)
		QtCore.QMetaObject.connectSlotsByName(calculate_option)
		
		self.pushButton.clicked.connect(self.btn1_clk) #点击相应按钮改变flag值
		self.pushButton_2.clicked.connect(self.btn2_clk)
		self.pushButton_3.clicked.connect(self.btn3_clk)
		self.pushButton_4.clicked.connect(self.btn4_clk)

	def retranslateUi(self, calculate_option):
		_translate = QtCore.QCoreApplication.translate
		calculate_option.setWindowTitle(_translate("calculate_option", "Form"))
		self.pushButton.setText(_translate("calculate_option", "+"))
		self.pushButton_2.setText(_translate("calculate_option", "叉积"))
		self.pushButton_3.setText(_translate("calculate_option", "-"))
		self.pushButton_4.setText(_translate("calculate_option", "点积"))
		self.label.setText(_translate("calculate_option", "矩阵维度"))
		self.pushButton_5.setText(_translate("calculate_option", "下一步"))
		
	
	def btn1_clk(self):
		global flag
		flag =1
	
	def btn2_clk(self):
		global flag
		flag =2

	def btn3_clk(self):
		global flag
		flag =3

	def btn4_clk(self):
		global flag
		flag =4
	
	def btn5_clk(self, text):
		global n #此处一定要注意,在类和函数中的全局变量需要进行修改的时候一定要global!
		n = int(text) #不转换的话会产生错误,tablewidget是纯数据类型,tableview是全变量的类型
		print("window1:", n)
		print("flag:", flag)
	

简单的来说就是输入矩阵维度,点击按钮返回flag值,很c的写法不是吗,以前做单片机的计算器的时候就是这样的一个逻辑,按下按钮返回flag,循环内检测flag进行下一步计算。第一次的我也使用了这种相似的方法。

这里已经是简化以后的代码了,纯新手开发的时候容易和书上写的那样把ui和逻辑分离开来,不喜欢使用类内的方法而是参照c语言的习惯写main里面的函数,感觉重置一个flag就能完成的事情为什么要在类里面定义一个方法来实现呢?实际上这种习惯是不好的,首先是代码不易阅读,其次,ui界面开发实际上属于一个连续的过程,可以在类内实现的很简单的逻辑(比如点击进入下一个窗口)最好不要在main里面实现,因为实在没什么必要。但是在开发复杂的工程的时候,多个窗口进行交互,代码逻辑分离实际上是非常重要的。

接下来用qtablewidget实现一下输入矩阵和输出矩阵:

class inputTable_1(QWidget):
	def __init__(self, arg=None):
		super(inputTable_1, self).__init__(arg)
		self.setWindowTitle("QTableWdget表格_输入矩阵")
		self.resize(500,300)
		conLayout = QVBoxLayout()
		self.table=QTableWidget()
		self.table.setRowCount(2)
		self.table.setColumnCount(2)
		conLayout.addWidget(self.table)

		self.pushButton_1 = QtWidgets.QPushButton("确认")
		conLayout.addWidget(self.pushButton_1)
		self.setLayout(conLayout)

	def initTable(self):
		self.table.setRowCount(n)
		self.table.setColumnCount(n)
		for row in range(n):
			for column in range(n):
				self.table.setItem( row, column, QTableWidgetItem("row %s, column %s"%(row,column)))
	
	def read_mat_1(self):
		global mat_receive_1
		mat_receive_1 = np.zeros(n*n).reshape(n, n)
		for row in range(n):
			for column in range(n):
				new = self.table.item(row,  column).text() #注意读取使用text方法
				mat_receive_1[row, column] = int(new)
		print("this is mat_receive_1:", mat_receive_1)
		
	def read_mat_2(self):
		global mat_receive_2
		mat_receive_2 = np.zeros(n*n).reshape(n, n)
		for row in range(n):
			for column in range(n):
				new = self.table.item(row,  column).text() #注意读取使用text方法
				mat_receive_2[row, column] = int(new)
		print("this is mat_receive_2:", mat_receive_2)

	
	def cul_ans(self):
		global mat_ans
		mat_ans = np.zeros(n*n).reshape(n, n)
		if flag == 1:
			mat_ans = np.add(mat_receive_1, mat_receive_2)
		elif flag ==2:
			mat_ans = np.subtract(mat_receive_1, mat_receive_2)
		elif flag ==3:
			mat_ans = mat_receive_1*mat_receive_2
		elif flag == 4:
			mat_ans = np.multiply(mat_receive_1, mat_receive_2)
			
		for row in range(n):
			for column in range(n):
				newitem =str( mat_ans[row, column])
				self.table.setItem(row,  column, QTableWidgetItem(newitem))
		print("this is mat_receive_1:", mat_receive_1)
		print("this is mat_receive_2", mat_receive_2)
		print("this is mat_ans", mat_ans)

开发这个部分的时候经历了一段时间的挫折,因为没有搞明白内在逻辑和数据到底如何处理,但是可以看出来开发到最后我已经开始有意识地使用继承和类内部的方法了。说到数据的处理,qtablewidget使用的是自己定义的数据类,这里也就涉及到str和float的来回转换。
现在回头看的时候,我用的逻辑是界面-界面-按钮调用方法-界面显示结果的方式。但是实际上如果我重新来开发,我会把界面的数目尽量地简化,在一个界面上来布局qtablewidget,算法按钮,这样算完马上显示的话就可以不要多个界面,而用类内部的方法来实现运算。
最后是界面背后的逻辑部分:

if __name__ == "__main__":
	import sys
	app = QApplication(sys.argv)
############qt生成实例##############    
	window1 = QWidget()
	window1_ui = calculate_option()
	window1_ui.setupUi(window1)
############实例生成###############
	table1 = inputTable_1()
	table2 = inputTable_1()
	table3 = inputTable_1()
############逻辑部分###############
	window1.show()
	window1_ui.pushButton_5.clicked.connect(table1.initTable)
	window1_ui.pushButton_5.clicked.connect(table1.show)
	window1_ui.pushButton_5.clicked.connect(window1.close)


	table1.pushButton_1.clicked.connect(table2.initTable)
	table1.pushButton_1.clicked.connect(table2.show)
	table1.pushButton_1.clicked.connect(table1.read_mat_1)

	table1.pushButton_1.clicked.connect(table1.close)

	table2.pushButton_1.setText("计算")
	table2.pushButton_1.clicked.connect(table2.read_mat_2)
	
	table2.pushButton_1.clicked.connect(table3.initTable)
	table2.pushButton_1.clicked.connect(table3.cul_ans)
	table2.pushButton_1.clicked.connect(table3.show)
	table2.pushButton_1.clicked.connect(table2.close)

	table3.pushButton_1.setText("关闭")
	table3.pushButton_1.clicked.connect(table3.close)

	
	sys.exit(app.exec_())

这里需要注意的是继承qtdesigner生成的ui的时候的一些技巧。最开始的时候我在这里卡了很久可惜现在已经想不起来当时发生了什么问题了。但其实只需要记住一点诀窍或者说是思维:这些(指界面)都是类的继承并且生成实例,在更改的时候调用方法进行更改。
由于qtdesigner和多个界面的原因,这个部分总共使用了200多行代码,最简单的部分用了最多的代码开发用了最长的时间(心情不好+摸鱼一共花了一周),算是为自己交的学费。
最后贴一下界面:
在这里插入图片描述

_2:mandelbrot图形的显示和鼠标点击事件
这里是这次开发的主要难点所在:1.分形图形到底是什么,如何实现分形图形的局部放大。2.maplotlib如何嵌入到pyqt5。
__1分形图形到底是什么:
参考一下百度百科:https://baike.baidu.com/item/分形/85449 分形
说白了,我们小时候看见的那种只用三角形构成的地毯(谢尔宾斯基地毯)就是分形的基本应用,
谢尔宾斯基地毯
总的来说,分形图形就是一类可以经过无限次迭代(计算机绘图肯定是有限循环)绘制出来的图形。这次选用的mandlort分形图形是在复平面上的一种分形图案,它的特点是通过迭代计算出这个点的逃逸时间来确定这个点的颜色深浅。
简单地贴出实现方法,我写程序的时候其实并不是很明白它到底是怎么实现的,只是参考了《Python科学计算(第二版)》一书中相关的方法,现在回头又看了一下才想明白。

    def draw_mandlbrot(self, cx, cy, d, width=400, length=400): #生成.png图片颜色默认为黄和蓝两种
        if width >= length:
            dx = d; dy = d*width/length #比例缩放,使其看起来依然是一个长宽比为1:1的图形
        else:
            dy = d; dx = d*length/width
        x0, x1, y0, y1 = cx - dx, cx + dx, cy - dy, cy + dy
        x = np.arange(x0, x1, (x1 - x0) / length).reshape(1, length)
        y = np.arange(y0, y1, (y1 - y0) / width ).reshape( width, 1)
        c = x + y*1j

        def iter_point(c):
            z = c
            for i in range(1, 100):
                if abs(z) > 2: break
                z = z * z + c
            return i

        start = time.process_time()
        mandelbrot = np.frompyfunc(iter_point, 1, 1)(c).astype(np.float) #后两个参数是返回参数的数目
        print("time = ", time.process_time() - start)
        return mandelbrot

width和length是一开始我想用鼠标拖动选区会导致分辨率变化的对策,但是后来想了想这图都是圆形干嘛要用鼠标拖拽选区。。就用了点击放大点附近区域到两倍这种方式。
关于这个函数,返回的是一个400*400的二维数组,这个数组实际上就是一副png图了。
关于运行的效率问题,这个函数可以优化的地方其实特别多但是暂时还没有优化的打算。运行的时候我也发现了很多有趣的地方,因为每次点击都会绘制一张图片,而这个图片是基于坐标和范围大小的,所以每经过2-3次的点击,坐标的xy参数就会多一位,众所周知,float的运算效率之所以低于int就是因为它的位数和小数点,位数越长运算的速度越慢,这里坐标数据的位数的增加直接导致运算效率下降,一开始算一张图0.65s,经历10次左右的点击,算一张图大概需要3.5s。并且算到一定程度尾数太长了就算不下去了。但是理论上如果你对于数据进行进一步的处理,它将可以无限迭代下去。

__2.maplotlib如何嵌入到pyqt5
如果说这是一篇技术博客(大雾),那么主要需要介绍的其实就是个部分了。首先贴出代码,

from PyQt5.QtCore import *
from PyQt5.QtGui import *
import sys
import numpy as np
import time
from PyQt5.QtWidgets import *
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtCore import (QEvent, QTimer, Qt)
from PyQt5.QtGui import QPainter
import matplotlib
matplotlib.use("Qt5Agg")  # 声明使用QT5
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
import matplotlib.pyplot as plt

data_x = None; data_y = None; N_n = 0;
C_x = -0.5; C_y = 0; D_d = 1.5;

class Myfigure(FigureCanvas):
    def __init__(self):
        self.fig = plt.figure(facecolor='#FFD7C4')  # 可选参数,facecolor为背景颜色
        self.axes = self.fig.subplots() #也可以用add_subplot
        FigureCanvas.__init__(self, self.fig) #初始化激活widget中的plt部分

    def Draw(self):
        global C_x, C_y, D_d
        plt.imshow(self.draw_mandlbrot(C_x, C_y, D_d, 400, 400))
        self.draw()
        self.fig.canvas.mpl_connect('button_press_event', self.on_press) #如果不使用canvas.mpl_connect的话将不能激活event中的xdata和ydata以及inaxes,这里为相对axes的坐标
        # pos = plt.ginput(3)
        # print(pos)

    def draw_mandlbrot(self, cx, cy, d, width=400, length=400): #生成.png图片颜色暂时为黄和蓝两种
        if width >= length:
            dx = d; dy = d*width/length #比例缩放,使其看起来依然是一个长宽比为1:1的图形
        else:
            dy = d; dx = d*length/width
        x0, x1, y0, y1 = cx - dx, cx + dx, cy - dy, cy + dy
        x = np.arange(x0, x1, (x1 - x0) / length).reshape(1, length)
        y = np.arange(y0, y1, (y1 - y0) / width ).reshape( width, 1)
        c = x + y*1j

        def iter_point(c):
            z = c
            for i in range(1, 100):
                if abs(z) > 2: break
                z = z * z + c
            return i

        start = time.process_time()
        mandelbrot = np.frompyfunc(iter_point, 1, 1)(c).astype(np.float)
        print("time = ", time.process_time() - start)
        return mandelbrot

    # def mousePressEvent(self, event): #如果只重载这个方法那么无法获得xdata,ydata
    #     global data_x, data_y
    #     # data_x = event.pos().x()
    #     # data_y = event.pos().y()
    #     # print(data_x, ',', data_y)
    #

    def on_press(self,event):
        global data_x, data_y, N_n, C_x, C_y, D_d
        N_n += 1
        data_x = event.xdata
        data_y = event.ydata
        print('you pressed', N_n, 'times,', event.xdata, event.ydata)

        C_x = C_x + (data_x-200)/200 * D_d  #计算新的中心点坐标
        C_y = C_y + (data_y-200)/200 * D_d
        D_d = 0.5**(N_n) * D_d #计算新的范围,0.5为放大倍数

        plt.imshow(self.draw_mandlbrot(C_x, C_y, D_d, 400, 400))
        self.draw()
        self.fig.canvas.mpl_connect('button_press_event', self.on_press)


class Main_window(QWidget):
    def __init__(self):
        super(Main_window, self).__init__()

        # QWidget
        self.figure = Myfigure()
        self.setWindowTitle('Mandlort_Calculater')
        self.fig_ntb = NavigationToolbar(self.figure, self) #注意,记得指向figure的FigureCanvas
        self.button_draw = QPushButton("绘图")
        self.button_draw.setFont(QFont( "Roman times" , 10 ,  QFont.Bold))
        self.label = QLabel('')
        self.label.setFont(QFont( "Roman times" , 8 ,  QFont.Bold))
        timer = QTimer(self)  #设置一个定时器用来刷新label显示的坐标
        timer.timeout.connect(self.time_Event)
        timer.start(1000)
        # 连接事件
        self.button_draw.clicked.connect(self.figure.Draw)

        # 设置布局
        layout = QVBoxLayout()
        layout.addWidget(self.figure)
        layout.addWidget(self.fig_ntb)
        layout.addWidget(self.button_draw)
        layout.addWidget(self.label)
        self.setLayout(layout)

    def time_Event(self):
        data_xy = str(C_x)+','+str(C_y)
        self.label.setText(data_xy)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    ui = Main_window()
    ui.show()
    sys.exit(app.exec_())

给出最终的效果图:

点了两下:

一开始,我想的是既然要去显示二维数组png图,那么可以选择的方式有很多,比如opencv的几个库,PIL,但是仔细思考了一下,既然要处理二维数组还是科学计算于是还是选择了兼容numpy的matplotlib中的pylot。然后和大多数接触这边的人一样(都是网上看到帖子的网友),我想的是和继承QWidget一样去继承plt。这时候其实我犯了一个错误,我认为plt就是一个类,但是实际上plt(pyplot)是一个很大的范围,它包括manager,可以说它是个一个很独立的类有着自己的core,而不是pyqt的一个组件。
但是既然matplotlib支持pyqt5,那plt为什么不支持呢?

当你直接用pyqt的窗口和布局去实例化和布局pyplot的时候会直接出错,当你直接在mainwindow继承这个类就会单独出现一个界面,标题默认是Figure1,并且你在mainwindow使用pyqt的布局定义的按钮都不能用了。
然后我看了一些网上的帖子,他们使用了一个类FigureCanvas,后来我才知道其实pyplot都是继承的这个类。
但是把mainwindow直接继承这个类的话将不能使用pyqt的布局和组件。
当时参考的是这个博客:https://blog.csdn.net/panrenlong/article/details/80183519 ,很感谢博主的启发。
后来我又看到matplot的官网,才知道有介绍plt嵌入到pyqt的例程:
https://matplotlib.org/examples/user_interfaces/embedding_in_qt5.html 所以说官方文档还是很齐全的。

这时候就差不多水落石出了,用两个类,一个类继承Figure,之后在写一个mainwindow,把之前的类实例化作为组件放入布局中。之后又发现,这样一来,虽然继承了Figure,但是mainwindow其实还是无法直接调用plt的plt.imshow()和plt.show()方法,当时的代码:

 def Draw(self):
        global C_x, C_y, D_d
        plt.imshow(self.draw_mandlbrot(C_x, C_y, D_d, 400, 400))
        self.show()      

这时候的效果是这样:可以看到虽然plt嵌入到了mainwindow,但是实际无法显示图片。

如果改成plt.show()那么就又会出现新的窗口:

然后到这里网上就没有相关的例程了,我也陷入了瓶颈,到这里总共花费了两天时间。于是我去查看了底层库里面是如何描述FigureCanvas的。发现了这样的线索:
class FigureCanvasQTAgg(FigureCanvasAgg, FigureCanvasQT):

好像和QT有关系啊,又进入FigureCanvasQT的declaration查找线索:
发现它有这样的一个方法:

    def draw(self):
        """Render the figure, and queue a request for a Qt draw.
        """
        # The renderer draw is done here; delaying causes problems with code
        # that uses the result of the draw() to update plot elements.
        if self._is_drawing:
            return
        self._is_drawing = True
        try:
            super(FigureCanvasQT, self).draw()
        finally:
            self._is_drawing = False
        self.update()

这不正是我需要的吗?于是改成了现在的那个版本,使用self.draw()方法。这也说明,一切的问题都是有迹可循的,需要你更加深入地去观察问题背后的东西。
实际上,在偶然间查到这个方法之前,我也想到了另一种解决的方法,就是一些博客提到过,直接在plt里面绘制按钮,这也是一种解决的方法,但是这种方法比较复杂,贴出相关的帖子:
https://blog.csdn.net/u010668907/article/details/51115431

之后是要用到鼠标事件了。我想既然已经解决了FigureCanvas继承的问题,那直接使用event来描述鼠标事件应该没问题吧?
于是在看了一个小时的例程之后我开始改程序,一开始方法写在mainwidow里面,改了好几个版本,都发现不行出现的问题是:我想用event读取关于Figure的坐标,但是只能读到关于QWidget的坐标。我觉得应该还是继承的问题,可能这几个object用的event重合了,于是我把方法写到Myfigure还是不行,可惜当时返回的error我忘记了。

这边又卡了一天的样子,也查了很多人解决的方法,网上就没有使用event做为plt的点击事件返回坐标的例子。有些用的是getinput来获取关于figure的坐标。直到看到这里:
https://blog.csdn.net/Big_Head_/article/details/78966363 当时就哭了T T。。
他很巧妙地用了fig.canvas.mpl_connect这个函数来代替鼠标点击事件。并且这里返回的是xdata,ydata而不是x,y所以不用再使用相对QWidget的坐标还要转化来转化去,直接是相对坐标轴的位置,很棒。
至此,所有的线索组成了最后的真相。当晚就完成了这个模块的整个编写。所以这个模块虽然写的过程中经历很复杂,但是实际完成只花了三四天(夏哥:一度摸鱼)。

最后再解释一下放大的原理,刚刚忘记解释了。由于生成mandelbrot图的函数是用x,y和范围d产生的,所以我设置了N_n记录点击的次数,对每次点击的坐标附近的D_d = 0.5**(N_n) * D_d为半径的范围进行绘图。

__3.常微分方程模块
因为挺开心的,所以又摸了几天鱼回头来写这个模块。说实话其实没啥好说的,因为不是特别复杂。经历了前面那个非人哉的部分的洗礼我感觉都没啥难度了。整个过程耗时两天多一点。主要的时间还是花费在读张若愚的书了介绍sympy那边的部分。
然后,这部分主要的问题有两个:1.如何读取需要求解的表达式,2.如何把读取的text转化成表达式express的形式
看了半天书以后,选用的是直接使用matpltlib显示latex格式的结果,一开始也有想调用lateX显示,太过复杂不了了之之后发现了pyplot完美支持latx格式。
贴出代码:

rom __future__ import division #整数除法 / 变为普通除法
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import sys
import numpy as np
from sympy import *
import time
from PyQt5.QtWidgets import *
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtCore import (QEvent, QTimer, Qt)
from PyQt5.QtGui import QPainter
import matplotlib
matplotlib.use("Qt5Agg")  # 声明使用QT5
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure
import matplotlib.pyplot as plt
x_data = 0;y_data = 0;

'''
注意:
1.仅可以求解微分方程
2.输入的时候使用英文输入法,当使用中文输入法的时候会出现不可预知的错误
3.这个demo中,text仅仅只能作为接收信息的工具,不要用来处理信息
ps:给出一个书写需要求解的微分方程的例子:
Derivative(f(x), x) - f(x) + 1等同于df(x)-f(x)+1=0
给出一个书写求解对象的例子:
f(x)
'''

x, y = symbols('x, y', real=True)
f = Function('f')
func = Eq(Derivative(f(x), x) - f(x) + 1, 0)
df = f(x)
func_tmp = ''
df_tmp = ''
ans = dsolve(func, df)
write = ''


class Myfigure(FigureCanvas):
    def __init__(self):
        self.fig = plt.figure(figsize=(4,1))  # 可选参数,facecolor为背景颜色facecolor='#FFD7C4',
        # self.axes = self.fig.subplots() #也可以用add_subplot
        self.axes = self.fig.add_axes([0,0,1,1])
        FigureCanvas.__init__(self, self.fig) #初始化激活widget中的plt部分
        self.fig.canvas.mpl_connect('button_press_event', self.on_press)

    def _print(self):
        global write
        left, width = .25, .5
        bottom, height = .25, .5
        right = left + width
        top = bottom + height
        self.axes.text(0.5 * (left + right), 0.5 * (bottom + top), write,
                       horizontalalignment='center',
                       verticalalignment='center',
                       fontsize=20, color='red',
                       )
        self.axes.set_axis_off()
        self.draw()

    def on_press(self,event):
        global x_data, y_data
        x_data = event.xdata
        y_data = event.ydata
        self.fig.canvas.mpl_connect('button_press_event', self.on_press)

class Main_window(QWidget):
    def __init__(self):
        super(Main_window, self).__init__()

        # QWidget
        self.figure = Myfigure()
        self.setWindowTitle('LaTex display')
        # self.fig_ntb = NavigationToolbar(self.figure, self) #注意,记得指向figure的FigureCanvas
        self.button_text = QPushButton("确认输入")
        self.button_text.setFont(QFont( "Roman times" , 10 ,  QFont.Bold))

        self.text1 = QLineEdit('')
        self.text1.setPlaceholderText('输入需要求解的表达式')
        self.text1.textChanged.connect(self.text_handler1) # 输入数值改变n的大小

        self.text2 = QLineEdit('')
        self.text2.setPlaceholderText('输入需要求解的对象')
        self.text2.textChanged.connect(self.text_handler2)  # 输入数值改变n的大小

        self.label = QLabel('')
        self.label.setFont(QFont( "Roman times" , 8 ,  QFont.Bold))

        timer = QTimer(self)  #设置一个定时器用来刷新label显示的坐标
        timer.timeout.connect(self.time_Event)
        timer.start(1000)
        # 连接事件
        # self.button_draw.clicked.connect(self.figure.Draw)
        self.button_text.clicked.connect(self.but_click)

        # 设置布局
        layout = QVBoxLayout()
        layout.addWidget(self.text1)
        layout.addWidget(self.text2)
        layout.addWidget(self.figure)
        # layout.addWidget(self.fig_ntb)
        layout.addWidget(self.button_text)
        layout.addWidget(self.label)
        self.setLayout(layout)


    def time_Event(self):
        data_xy = str(x_data)+','+str(y_data)
        self.label.setText(data_xy)

    def text_handler1(self,text):
        global func_tmp
        func_tmp = text

    def text_handler2(self,text):
        global df_tmp
        df_tmp = text

    def but_click(self):
        global x, y, f, func, df, ans, write
        df = eval(df_tmp)
        func = Eq(eval(func_tmp), 0)
        ans = dsolve(func, df)
        write = '$' + ''.join(latex(ans)) + '$'
        print(write)
        self.figure._print()


if __name__ == '__main__':
    app = QApplication(sys.argv)
    ui = Main_window()
    ui.show()
    sys.exit(app.exec_())

一开始也有走进一些误区,觉得直接在QLineEditor读text然后就在那处理,实际上这是不行的。因为QLineEditor读取文本是你改变一次text就读一次,换句话说,你写一个字进去它就会处理一次self.text_handler1方法。所以,不要在self.handler里面写处理数据的内容,只用局部变量传递参数就行了。
之后又遇到了怎样把str转化成表达式的问题,这个是真的坑,你搜索string转表达式搜了半天也没啥结果,后来搜到关于eval()和exec()的只言片语。exec是对函数括号内的部分进行运行,eval是将str转化成表达式进行运算。
eval不是将str转化成表达式啊!这一点要记住,是将str转化成表达式进行运算,是运算,是运算,是运算!要问我怎么知道的,这个到第四部分再介绍,反正很坑就是了。
其他的就没啥了,都是关于dsolve的东西,自己慢慢看书就行了。

要说这个部分的不足其实还是挺多的,首先可以优化的是表达式的输入,完全可以用正则的形式进行识别表达式,这样就可以不用输入Derivative了,可以直接输入D(f(x))-f(x)就会方便多了。

PS:我学了三年物理专业(虽然是个学渣),我到写完的那天早上才知道常微分方程和偏微分方程的区别。。数学建模的时候一直都叫微分方程也没深究过,我是真的有点晕。。。

__4.常微分方程组的数值解及绘制3d图形
这个模块做的比较艰难的地方只有一个地方,就是怎样绘制3d图形以及如何让pyplot绘制的3d图形在pyqt5的框架下旋转起来。
给出代码:

from __future__ import division #整数除法 / 变为普通除法
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import sys
import numpy as np
from sympy import *
import time
from PyQt5.QtWidgets import *
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtCore import (QEvent, QTimer, Qt)
from PyQt5.QtGui import QPainter
import matplotlib
matplotlib.use("Qt5Agg")  # 声明使用QT5
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from scipy.integrate import odeint
x_data = 0;y_data = 0;

'''
注意:
1.仅可以求解常微分方程
2.输入的时候使用英文输入法,当使用中文输入法的时候会出现不可预知的错误
3.这个demo中,text仅仅只能作为接收信息的工具,不要用来处理信息
eg.:
10*(y-x),x*(28-z),x*y-3*z
0,30,0.01
0,1,0
'''

x, y, z, t = symbols('x, y, z, t', real=True)
f = Function('f')
func_x_tmp = '10*(y-x)'
func_y_tmp = 'x*(28-z)'
func_z_tmp = 'x*y-3*z'
T_tmp = '0,30,0.01'
bound_tmp = '0,1,0'

class T:
    start = 0
    stop = 30
    step = 3000
    length = 0.01

    def data(self):
        return np.arange(self.start, self.stop, self.length)

T = T()

boundary = (0, 1., 0)



class Myfigure(FigureCanvas):
    def __init__(self):
        self.fig = plt.figure(figsize=(4,4))  # 可选参数,facecolor为背景颜色facecolor='#FFD7C4',
        # self.axes = self.fig.subplots() #也可以用add_subplot
        self.axes = Axes3D(self.fig)
        FigureCanvas.__init__(self, self.fig) #初始化激活widget中的plt部分
        self.fig.canvas.mpl_connect('button_press_event', self.on_press)

    def _print(self):
        self.axes.mouse_init()
        self.draw()

    def on_press(self,event):
        global x_data, y_data
        x_data = event.xdata
        y_data = event.ydata
        self.fig.canvas.mpl_connect('button_press_event', self.on_press)

class Main_window(QWidget):
    def __init__(self):
        super(Main_window, self).__init__()

        # QWidget
        self.figure = Myfigure()
        self.setWindowTitle('Axes3d display')
        # self.fig_ntb = NavigationToolbar(self.figure, self) #注意,记得指向figure的FigureCanvas
        self.button_text = QPushButton("确认输入")
        self.button_text.setFont(QFont( "Roman times" , 10 ,  QFont.Bold))

        self.text1 = QLineEdit('')
        self.text1.setPlaceholderText('输入需要求解的表达式1')
        self.text1.textChanged.connect(self.text_handler1)
        self.text2 = QLineEdit('')
        self.text2.setPlaceholderText('输入需要求解的表达式2')
        self.text2.textChanged.connect(self.text_handler2)
        self.text3 = QLineEdit('')
        self.text3.setPlaceholderText('输入需要求解的表达式3')
        self.text3.textChanged.connect(self.text_handler3)
        self.text4 = QLineEdit('')
        self.text4.setPlaceholderText('输入需要求解的时域:start,stop,step')
        self.text4.textChanged.connect(self.text_handler4)
        self.text5 = QLineEdit('')
        self.text5.setPlaceholderText('输入需要求解的边界:x[0],y[0],z[0]')
        self.text5.textChanged.connect(self.text_handler5)


        self.label = QLabel('')
        self.label.setFont(QFont( "Roman times" , 8 ,  QFont.Bold))

        timer = QTimer(self)  #设置一个定时器用来刷新label显示的坐标
        timer.timeout.connect(self.time_Event)
        timer.start(1000)
        # 连接事件
        # self.button_draw.clicked.connect(self.figure.Draw)
        self.button_text.clicked.connect(self.but_click)

        # 设置布局
        layout = QVBoxLayout()
        layout.addWidget(self.text1)
        layout.addWidget(self.text2)
        layout.addWidget(self.text3)
        layout.addWidget(self.text4)
        layout.addWidget(self.text5)
        layout.addWidget(self.figure)
        # layout.addWidget(self.fig_ntb)
        layout.addWidget(self.button_text)
        layout.addWidget(self.label)
        self.setLayout(layout)


    def time_Event(self):
        data_xy = str(x_data)+','+str(y_data)
        self.label.setText(data_xy)

    def text_handler1(self,text):
        global func_x_tmp
        func_x_tmp = text

    def text_handler2(self,text):
        global func_y_tmp
        func_y_tmp = text

    def text_handler3(self,text):
        global func_z_tmp
        func_z_tmp = text

    def text_handler4(self,text):
        global T_tmp
        T_tmp = text

    def text_handler5(self,text):
        global bound_tmp
        bound_tmp = text

    def but_click(self):
        output = self.data_handler()
        self.figure.axes.plot(output[:,0],output[:,1],output[:,2])
        self.figure._print()
        self.button_text.setText('关闭窗口')
        self.button_text.clicked.disconnect(self.but_click)
        self.button_text.clicked.connect(self.close)

    def data_handler(self):
        global x, y, z, boundary, T_tmp, bound_tmp, T
        global data_x, data_y, data_z
        T_tmp = [float(i) for i in T_tmp.split(',')]

        boundary = tuple(float(i) for i in bound_tmp.split(','))

        T.start = T_tmp[0];T.stop = T_tmp[1];T.length = T_tmp[2]

        def func(w,t):
            global func_x_tmp, func_y_tmp, func_z_tmp, x, y, z
            x, y, z = w
            X = eval(func_x_tmp)
            Y = eval(func_y_tmp)
            Z = eval(func_z_tmp)

            return np.array([X, Y, Z])

        output = odeint(func, boundary, T.data())

        # data_x[0], data_y[0], data_z[0] = boundary #这边用于拓展输出x,y,z
        # data_x[1,:] = output[:,0]
        # data_y[1,:] = output[:,1]
        # data_z[1,:] = output[:,2]

        return output




if __name__ == '__main__':
    app = QApplication(sys.argv)
    ui = Main_window()
    ui.show()
    sys.exit(app.exec_())

备注:由于为了测试方便,就给定义了一个默认的微分方程组(洛伦兹星子,有兴趣的盆友可以去找文献看看,涉及混沌理论,今年美赛的A题我们也是做的混沌理论的延伸部分,是关于多种群竞争的模型)。

可以看到这边基本都是以上一个解微分方程作为基础的。
但是解微分方程组用的是Scipy这个库的odeint,看见ode就都明白啦。这个函数主要是用func对时域上进行计算,func计算返回xyz坐标带入进行下一次计算。但是这边要提到上个部分提到的问题了,微分方程那边我用的方法是用func_tmp传入text。如果你认为eval是将func_x_tmp转化成表达式func_x,那么你将会发现这个表达式不能直接调用,并且返回的error会提示你odeint使用的参数不能为str。所以,实际上eval是将str转化成表达式进行运算。其他的东西都是字符串处理的内容了,就不多赘述了,就提一点就是字符串转list后再和float,tuple转化需要注意一下。odeint的第二个参数boundary是tuple哦。

再来就是Axes3D嵌入到pyqt5了。这里主要的问题不是pyplot嵌入的问题而是Axes3D画图以后怎么旋转,一开始旋转不了,挺烦人的,也是各种查不到对策,后来在一个论坛找到了,这人估计也和我之前一样,是查底层库查到的。好像国外的技术支持比较多?以后是不是要多去国外的论坛查资料。他用的是一个官方资料没有提到的函数:axes.mouse_init()。我估计之前plt不支持event多半也和这个函数有关系,有实验过的朋友可以留言告诉我,现在,我要去摸鱼了!
帖子的地址:http://www.it1352.com/37872.html

更新:刷新Axes3d的方法
最开始是想实现一下在原图上画新图两者对比,后来发现这个可以直接画,就又想要做个刷新,防止有不想要图的时候。
一般大家都会想使用__init__直接刷新,但是实际上是不行的。查找资料发现有cla方法用于清除数轴。不能用clf(clear figure)这样会导致整个figure.fig被清掉,而重新把fig添加到框架中实在是一个乱七八糟的过程。使用cla方法后发现无法刷新图片,花了一个多小时找basis里面的内容也没找到,灵机一动,决定用self.figure.draw()直接就刷新了,真神奇。
贴一下更改的部分代码,框架改动就没写了直接新建一个按钮插到框架并连接到这个槽函数就行:

    def but_refresh(self):
        self.figure.axes.cla()
        self.figure.axes._init_axis()
        self.figure.draw()

贴一下效果图:

到这边就全部写完啦,有兴趣的朋友请留言指教,有空会来看看。我也是个小白,有机会就一起进步啊~

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值