PyQt5 “PyTuning“调试软件从0开发总结

PyQt5 "PyTuning"调试软件从0开发总结

北航3系大四要调小车在赛道上跑,小车单片机用的K60,老师提供的代码里还有串口收发的库,就想着用蓝牙模块再开发上位机调试软件进行远程调试,正好借此机会学习了一番PyQt。现在从头总结一下开发流程~想从0开发的可以参考。

代码功能:显示直观的调试界面,分两个线程,主线程负责处理用户事件以及显示,子线程处理串口通讯并向主线程上报数据。

放一下效果图~~
文末有全部代码链接
PyQt5 “PyTuning“调试软件从0开发总结_第1张图片

开发流程

  • PyQt5 "PyTuning"调试软件从0开发总结
  • 一、图形界面搭建(躯壳)
    • 1,designer的安装与打开
    • 2,designer使用
    • 3,ui文件转py文件
  • 二、功能实现(灵魂)
    • 1、继承ui转出的py躯壳
    • 2、按键回调函数
    • 3、内容显示与读取
      • (1) 文字读写
      • (2) 图片显示
      • (3) 几何绘图
    • 4、信号槽与多线程
      • (1) 信号槽理解与实例
      • (2) 多线程创建
  • 三、结语
    • 项目源码:

一、图形界面搭建(躯壳)

1,designer的安装与打开

这里博主用了designer进行图形界面开发,可以手拖控件,比纯手编直观多了,强烈推荐快速开发Qt简单图形界面的用这种方法。designer安装及打开方法:

pip install PyQt5
pip install pyqt-tools

如果用的是Anaconda,在Anaconda Prompt上运行上面两行后,找到anaconda3的安装目录并在下面找anaconda3 > Library > bin > designer,建议发送一个快捷方式到桌面上,之后更容易打开。
PyQt5 “PyTuning“调试软件从0开发总结_第2张图片

2,designer使用

下面简单说明一下designer的使用,由于设计的已经非常亲民了,就简略说下。基本你想干嘛,第一反应的操作就能实现。
PyQt5 “PyTuning“调试软件从0开发总结_第3张图片
① Widget Box
里面有各种各样的控件可以用,需要啥就把他往最中间要的位置拖就行了。
一些对控件选择的小建议:
1,大的框框用Group Box或 Frame. Group Box能方便的写小标题,Frame能显示明显的框框,虽然这些用最基础的Widget也能实现,但那两个更亲民直观好用。
2,文本提示用Label,好用
3,文本输入用Text Edit或Plain Text Edit,好用
4,大量文本输出用Text Browser,够大,能滚动(虽然其他也能)
5,!!!!想显示图片或视频,可以用Label!!!!很方便
6,尽量不要把所有东西都没有组织地堆到主界面上,不然后期调起来很麻烦,建议多创建几个框框,把同一个功能用到的控件放到这个框框里,再把框框在主界面上拖。

② Main Window
这里是你的创作画布,一个字:拖!!!
(除了拖还可以双击输入内容,反正操作非常直观)

③ 对象查看器
这里你可以看你所有控件的结构,跟文件浏览器似的,双击可以改对象名。
你并不想你的C盘所有文件全铺在表面,所以再次建议搭一个比较合理的结构,方便之后写代码。

④ 属性编辑器
这里列了好多控件常用的属性,点一个控件就会显示他的属性,从上到下依次是该控件对象的亲戚关系,越往下越是子对象。
在这里细调控件的位置很方便!!!

3,ui文件转py文件

designer保存的文件后缀.ui实际上是XML文件,想转成.py文件开始各种功能的编写很容易:在Anaconda Prompt里执行:

pyuic5 -o 文件路径\文件名.py 文件路径\文件名.ui

文件路径和文件名写自己的,.py文件就出来了,整个软件的图形界面骨架就能用了!!
建议把这段代码存记事本里!因为你肯定会再在designer里调整界面的hhhh,每次都得再执行一遍

二、功能实现(灵魂)

下面是PyQt界面开发的重头戏,上面designer造出来的只是个空壳子,现在需要注入灵魂让他动起来了。

1、继承ui转出的py躯壳

这步很重要!!不要直接在刚刚捏出来的py文件上编!!(他可能有几百行看起来很相似的代码,直接搞得你不想动他~)
以下博主就用自己的文件名做例子了。躯壳文件叫Racing_Tool.py,在同目录下创建新的文件,输入以下代码,跑,你会发现你已经继承了那具躯壳。(import了好多东西是我整个程序用的,先列在这里)

import Racing_Tool
from Vision import Vision, WeightsTuner, RecordDot
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5.QtCore import QCoreApplication, QThread, pyqtSignal
from PyQt5.QtGui import QImage, QPixmapimport sys
import serial
import re
import cv2
import numpy as np
from time import perf_counter

racing_tool = Racing_Tool.Ui_Main_Window

class RacingMain(QMainWindow, racing_tool):
    def __init__(self):
        QMainWindow.__init__(self)
        racing_tool.__init__(self)

        self.setupUi(self)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = RacingMain()
    window.show()
    sys.exit(app.exec_())

2、按键回调函数

本程序博主只用了按键回调函数,还有按下enter的回调函数等,逻辑类似。

def __init__(self):
        QMainWindow.__init__(self)
        racing_tool.__init__(self)
		
        self.setupUi(self)
        self.button_functioning()

def button_functioning(self):
        self.send_steer.clicked.connect(self.on_send_steer)
        self.send_motor.clicked.connect(self.on_send_motor)
        self.send_track.clicked.connect(self.on_send_track)
        self.open_UART.clicked.connect(self.on_open_uart)
        self.data_query.clicked.connect(self.on_data_query)
        self.debug_clear.clicked.connect(self.on_clear_debug)
        self.quit_button.clicked.connect(QCoreApplication.instance().quit)
        self.weights_tuner_button.clicked.connect(self.on_set_weights)
        self.record_button.clicked.connect(self.on_record_tracks)

这样各种按键按下后就都会执行后面的对应的函数了。self.onXXX是函数名,需要之后自己编写具体功能。

3、内容显示与读取

(1) 文字读写

Q是各种文本控件的名字
写:
设置字符串:Q.setPlainText(“string”)
追加字符串:Q.insertPlainText(“string”)
读:
读取字符串:buffer = Q.toPlainText()

(2) 图片显示

以QLabel 对象为例:

// 图片显示
self.img = np.zeros((self.CAMERA_H, self.CAMERA_W, 3), np.uint8) //你的图片,注意是np.uint8!!!!不是float请用0~255不要0.0~1.0
resized_img = cv2.resize(self.img, None, fx=8, fy=8, interpolation=cv2.INTER_CUBIC) //更改图片大小
show_image = QImage(resized_img, resized_img.shape[1], resized_img.shape[0],resized_img.shape[1] * 3,QImage.Format_RGB888)
self.MyQLabel.drawPixmap(QRect(0, 0, 640, 480), QPixmap(show_image)) 

博主这里用的是uint8表示的像素的RGB值,float形式的会出问题,还没研究出来咋弄。

(3) 几何绘图

核心对象:QPainter
这里博主参考了一堆零零散散的文章才明白各个语句的逻辑关系,在这里统一总结细讲一下从0实现绘图,方便大家理解使用,以显示小红点为例,先上基础代码:
先建一个新的类,继承QLabel:

from PyQt5.QtWidgets import QLabel
from PyQt5.QtCore import Qt, QRect, QPoint
from PyQt5.QtGui import QPainter, QColor, QPen, QBrush
import pyqtgraph as pg
import numpy as np


class RecordDot(QLabel):	//继承QLabel
    def __init__(self, parent_widget):		//创建实例的时候,给出父对象
    										//功能就相当于designer里把这个控件拖到另一个控件上
        super(RecordDot, self).__init__(parent_widget)	//初始化父对象
        self.name = "RecordDot"		//给自己起个名字
        self.left = 300		//以下四行为定义自己的位置大小,是不是很像designer里的
        self.top = 0		//其实你可以先在designer里拖个QLabel出来看效果,记住几何信息
        self.width = 40
        self.height = 40

        self.show = False	//非必要,博主自己的小功能标志位

        self.initUI()	//调用UI初始化,其实就是下面这个函数

    def initUI(self):	//下面这些你会发现,designer转出来的py文件全是这个,照搬就行
        self.setGeometry(QRect(self.left, self.top, self.width, self.height))
        self.setText("")
        self.setObjectName(self.name)

    def paintEvent(self, qp):	//接下来的参考下文解释
        if not self.show:
            return
        qp = QPainter()
        qp.begin(self)
        brush = QBrush(Qt.red, Qt.SolidPattern)
        qp.setBrush(brush)
        qp.drawEllipse(QPoint(20, 20), 10, 10)
        qp.end()

接下来着重讲解一下这个paintEvent函数。首先定义的时候多给了一个qp参数,这个不用太纠结,名字随意,给就行了,不给会有小报错。
插播一个许多新手很关心的问题:paintEvent()是在外面对象实例化之后(记实例为Q),执行Q.update()系统自动调用一次的!!!!每次update画一次

if not self.show:
	return

paintEvent函数会在对象实例化的时候被系统自动执行一次,所以为了不让他瞎画,直接先让他return。当然这句话广义的功能是,如果我没选择让他显示,在调用paintEvent后,会清空这个对象上画的所有内容(每次调这个函数会清空之前内容)。

// An highlighted block
qp = QPainter()
qp.begin(self)
//具体绘图的代码
qp.end()

这三行首先创建了QPainter对象,再指定了开始与结束绘图的位置。必要!!

具体绘图方法:

① 选择自己想要的画笔

brush = QBrush(Qt.red, Qt.SolidPattern)
qp.setBrush(brush)

如果想画实心图形,用QBrush,如果想画线条或几何轮廓,用QPen。QBrush创建的时候建议用a = QBrush(QColor, style)方法。注意!!!!不要把画笔颜色RGB直接以列表或元胞形式贴到第一个参数上,要

brush = QBrush(QColor(color[0], color[1], color[2]), Qt.SolidPattern)

然后告诉QPainter使用这个画笔(qp.setBrush(brush) / qp.setPen(pen))
如果只设置了QBrush则轮廓(QPen)默认为黑色,宽度一像素

②选择自己想要绘制的图形

// 推荐绘图代码示例
qp.drawEllipse(QPoint(x, y), rx, ry)			//(椭)圆
qp.drawRect(QRect(left, top, width, height))	//矩形
qp.drawLine(QPoint(start_x, start_y), QPoint(end_x, end_y))		//直线段
qp.drawPixmap(QRect(left, top, width, height), QPixmap(image)) 		//图片显示

③绘图代码放到正确的地方

把所有要画的东西,包括图片显示!!!!!!!重点强调,都依次放进qp.begin(self)和qp.end()之间,这样每次这个对象被update,就会显示想要的东西了。

④主程序里调用绘图

class RacingMain(QMainWindow, racing_tool):
    def __init__(self):
        QMainWindow.__init__(self)
        racing_tool.__init__(self)
        self.start_recorded = False		//博主自己的标志位,非必要
        self.setupUi(self)
        self.button_functioning()
        self.RecordDot = RecordDot(self.record_panel)	//对象实例化,self.record_panel是他所在的框框
        
    def button_functioning(self):      
    	self.record_button.clicked.connect(self.on_record_tracks)
    	
    def on_record_tracks(self):
        if not self.start_recorded:
            self.record_button.setText("结束录制")
            self.RecordDot.show = True		//设为需要绘图
            self.RecordDot.update()			//这一行调用了paintEvent
            self.start_recorded = True
        else:
            self.record_button.setText("开始录制")
            self.RecordDot.show = False		//设为不用绘图
            self.RecordDot.update()			//调用paintEvent直接return清空绘图
            self.start_recorded = False

录像小红点就有了!!
在这里插入图片描述
⑤ 色图设置(实用功能,非必要)

博主为了直观显示某些量的大小,想采用类似Matlab里colormap的方法,这里提供一下核心代码和实例实现代码:

核心代码及理解:

self.colormap = pg.ColorMap([0, 0.5, 1], [[255, 0, 0], [200, 200, 0], [0, 255, 0]])
self.lookup_table = self.colormap.getLookupTable()
color = self.lookup_table[index]

可以这么想象一个色图:
PyQt5 “PyTuning“调试软件从0开发总结_第4张图片

第一句:

self.colormap = pg.ColorMap([0, 0.5, 1], [[255, 0, 0], [200, 200, 0], [0, 255, 0]])

作用就是生成这个图,先提供一组要插值得点横坐标[0, 0.5, 1],再在一个列表里列出对应的RGB值[[255, 0, 0], [200, 200, 0], [0, 255, 0]],pyqtgraph库的ColorMap方法就能插出来上面那样子的图。

第二句:

self.lookup_table = self.colormap.getLookupTable()

作用就是把图离散化,给每一个离散出来的颜色配一个索引值(默认0 ~ 511),类似这样:
PyQt5 “PyTuning“调试软件从0开发总结_第5张图片
第三句:

color = self.lookup_table[int(511 * self.weights[i])]

作用就是从离散色图取颜色,和取列表元素是一个道理,其实就是个列表。
self.lookup_table[index]就能取色,注意index是0~511的int。总之理解成列表就行了。

实例代码及效果:

from PyQt5.QtWidgets import QLabel
from PyQt5.QtCore import Qt, QRect, QPoint
from PyQt5.QtGui import QPainter, QColor, QPen, QBrush
import pyqtgraph as pg
import numpy as np
from numpy import sign


class WeightsTuner(QLabel):
    def __init__(self, parent_widget):
        super(WeightsTuner, self).__init__(parent_widget)
        self.name = "WeightsTuner"
        self.left = 0
        self.top = 0
        self.width = 100
        self.height = 480

        self.weights = []

        self.colormap = pg.ColorMap([0, 0.5, 1], [[255, 0, 0], [200, 200, 0], [0, 255, 0]])
        self.lookup_table = self.colormap.getLookupTable()

        self.initUI()

    def initUI(self):
        self.setGeometry(QRect(self.left, self.top, self.width, self.height))
        self.setText("")
        self.setObjectName(self.name)

    def set_weight_values(self, coef):
        self.weights = []
        for i in range(60):
            self.weights.append(np.float_power(i / 60, coef))

    def paintEvent(self, qp):
        if len(self.weights) == 0:
            return
        qp = QPainter()
        qp.begin(self)
        for i in range(60):
            color = self.lookup_table[int(511 * self.weights[i])]
            brush = QBrush(QColor(color[0], color[1], color[2]), Qt.SolidPattern)
            qp.setBrush(brush)
            width = int(100 * self.weights[i])
            qp.drawRect(QRect(0, i * 8, width, 8))
        qp.end()

实现效果:

PyQt5 “PyTuning“调试软件从0开发总结_第6张图片

其实色图实现还有好多参数可以设置,可以更灵活,博主只是从新手的角度出发,提取了最省心实用的使用方法。

4、信号槽与多线程

这一部分主要讲怎么利用PyQt5的信号槽机制为多线程提供信息交互的方法,并利用QThread创建多线程。

(1) 信号槽理解与实例

信号槽是PyQt里传递信息的一个比较稳定实用的方式,逻辑清晰。需要调用pyqtsignal库。信号响应直观理解:
PyQt5 “PyTuning“调试软件从0开发总结_第7张图片
具体实例如下:

①创建信号对象

class RacingMain(QMainWindow, racing_tool):
    # signal transmitting
    
    CMD_data_query = pyqtSignal(str)
    
    def __init__(self):
    ...

建议在class底下直接创建,不要用self。
pyqtSignal()里面的内容表达要传递的信号类型,如果信号类型比较复杂,可以拿type()看一下,或者直接pyqtSignal(object)

②绑定回调函数

def transmission_functioning(self):
    self.CMD_data_query.connect(self.serial_thread.handle_send_request)

博主把所有绑定放进一个函数里执行了,在各个对象创建完毕后,记得调用这个绑定初始化函数。
有没有觉得这个connect和按键回调函数里的差不多?没错,本质是一样的。
connect()里你可以接任意函数名,当信号发射之后就会触发这个函数,并把之前pyqtSignal(object)里的具体的object当做参数传递给这个回调函数。

③信号发送

self.CMD_send_steer.emit(args)

emit()里的内容类型要对应上之前的pyqtSignal(object)里的类型。

④回调函数编写

这部分就和PyQt关系不大了hhh,自由发挥就好~

(2) 多线程创建

PyQt多线程很方便,继承QThread类自己创建个类就好:

class SerialThread(QThread):
    transmit_img = pyqtSignal(object)
    transmit_tracks = pyqtSignal(object)
    transmit_data = pyqtSignal(str)
    handle_acknowledge = pyqtSignal()
    serial_bug_report = pyqtSignal(str)
    state_report = pyqtSignal(bool)

    def __init__(self, port, baud):
        super().__init__()

    def __del__(self):
        self.wait()

	def run(self):
		//你要在子线程里执行的代码

博主这里把我在子线程里用的一些信号槽列了一下,对多线程创建本身没啥用,就想推荐一下用这种方法和主线程沟通hhhh.

下面三个函数前两个好理解,第三个run(self):
这个函数是在多线程实例被创建出来的时候系统自动开始跑的!!所以不用再纠结怎么让子线程工作了,创建出来他就开始跑run里的代码了!

三、结语

至此,博主用的所有与PyQt5有关的操作就整理完了!再加上各种功能的实现,最后已经是个比较酷炫实用的调试界面了。

所有的源码,包括designer的ui文件我放在下面链接里了,如需自取~

项目源码:

Github: https://github.com/Apokli/PyTuning

百度网盘:https://pan.baidu.com/s/13e4BoRPIoGG53FnI2R_CPg
密码:8u5c

你可能感兴趣的:(python,pyqt5)