PyQt5 "PyTuning"除錯軟體從0開發總結
北航3系大四要調小車在賽道上跑,小車單片機用的K60,老師提供的代碼里還有串口收發的庫,就想著用藍牙模塊再開發上位機除錯軟體進行遠程除錯,正好借此機會學習了一番PyQt,現在從頭總結一下開發流程~想從0開發的可以參考,
代碼功能:顯示直觀的除錯界面,分兩個執行緒,主執行緒負責處理用戶事件以及顯示,子執行緒處理串口通訊并向主執行緒上報資料,
放一下效果圖~~
文末有全部代碼鏈接

開發流程
- 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,建議發送一個快捷方式到桌面上,之后更容易打開,

2,designer使用
下面簡單說明一下designer的使用,由于設計的已經非常親民了,就簡略說下,基本你想干嘛,第一反應的操作就能實作,

① 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]
可以這么想象一個色圖:

第一句:
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),類似這樣:

第三句:
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()
實作效果:

其實色圖實作還有好多引數可以設定,可以更靈活,博主只是從新手的角度出發,提取了最省心實用的使用方法,
4、信號槽與多執行緒
這一部分主要講怎么利用PyQt5的信號槽機制為多執行緒提供資訊互動的方法,并利用QThread創建多執行緒,
(1) 信號槽理解與實體
信號槽是PyQt里傳遞資訊的一個比較穩定實用的方式,邏輯清晰,需要呼叫pyqtsignal庫,信號回應直觀理解:

具體實體如下:
①創建信號物件
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
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/226135.html
標籤:AI
上一篇:非正交多址技術(NOMA)
