PyQt5 GUI接收UDP数据并动态绘制(多线程间信令)
1. QT的使用 pyqt5 是 qt 的 python 版本。它主要以物体的形式存在。在编程过程中无法可视化,带来很多不便。为了简化pyqt5的界面设计,我们可以使用qt Designer(C:\qt\5.12.11\mingw73)_32\bin\designer。可执行程序)。生成的图形界面通常保存在带有*的文件中。 ui 后缀。 pyqt5可以直接调用。 ui 文件,或转换设计的。通过py
1. QT的使用
pyqt5 是 qt 的 python 版本。它主要以物体的形式存在。在编程过程中无法可视化,带来很多不便。为了简化pyqt5的界面设计,我们可以使用qt Designer(C:\qt\5.12.11\mingw73)_32\bin\designer。可执行程序)。生成的图形界面通常保存在带有*的文件中。 ui 后缀。
pyqt5可以直接调用。 ui 文件,或转换设计的。通过pyqt5自带的pyuic.exe将ui文件转换成.py格式的pyqt5类,供其他模块调用。
CSDN论坛上有很多QT安装教程,这里不再赘述。推荐使用Qt5。目前,更高版本的 Qt6 尚未被 matplotlib 纳入后端支持。
2. Pychart设置
首先,安利一博在代码颜色主题、功能界面、python环境切换、终端打开、Jupiter notebook支持、变量查看、Markdown支持、控制台多开等方面具有很高的便利性,所以我主要使用pychrm进行相关代码开发并推荐它。
2.1 安装Pyqt5和pyinstaller包
打开pycharm底部的终端,输入以下代码安装pyqt5包和pyinstaller包。 pyuninstaller包是一个用来将pyqt5GUI设计打包成exe可执行文件的工具。使用此工具,您可以将程序复制到其他 Windows 计算机。
pip install pyqt5,pyinstaller
matplotlib 包需要以类似的方式安装。我不会重复它。安装pyqt5后,matplotlib会自动使用pyqt5作为后端。绘制的图像效果更好,工具栏更实用。建议日常使用。
2.2 pychar pyqt工具配置
使用Qt进行界面设计时,可以在pycharm中配置Qt软件的几个工具作为外部工具(这是pycharm的众多优点之一),方便随时调用。 Pycharm点击文件-设置-工具-外部工具(英文版自行参考)进入外部工具添加界面。
- Qt Designer工具(设计Qt界面)
程序路径:
C:\Qt\5.12.11\mingw73_32\bin\designer.exe
工作目录:
$项目文件目录$
- Qt Creator工具(设计Qt界面)
程序路径在对应环境的Script目录下:
C:\Anaconda3\envs\tensor37\Scripts\pyuic5.exe
参数设置如下:
$FileName$ -o $FileNameWithoutExtension$.py
工作目录:
$项目文件目录$
- PyUI工具(Qt UI界面转python代码)
程序路径:
C:\Qt\Tools\QtCreator\bin\qtcreator.exe
工作目录:
$项目文件目录$
完成以上设置后,即可在右键菜单中打开designer.exe和creator.exe GUI设计应用程序。选择。 ui 文件并右键单击 pyuic.exe 生成一个。 py 同名文件,其中包含可以生成相同 GUI 的 pyqt5 类。
3 UDP图形界面设计
3.1 图形界面设计
在pycharm的空白处右键,选择外部工具,打开设计器,新建一个主窗口。
根据需要,我设计了一个UDP网络编程接口。主要功能是接收UDP客户端发送的正弦数据,将数据保存为txt文件并绘制在底部的widget(form part)中。
! zoz100037](https://programming.vip/images/doc/eab9f8a0093c041c92ec8bad58725022.jpg)
目标操作界面如下:
3.2 将GUI文件转换为py文件
界面设计完成后,保存widget_recev.ui图形文件。在左侧的Project Explorer中,可以选择UI文件,右键使用外部pyuic工具将其转换为widgets_recev.py文件,供程序调用。这个操作在后续的调试中经常用到。随时更改GUI,随时生成新的py文件。新建的py文件会覆盖原来的内容,所以建议再建一个python模块来调用该模块,避免信息丢失。
3.3 widget形式提升与matplotlib功能集成
这里需要注意的是,matplotlib 中的图形画布和 GUI 中的小部件是 Qwidget 的子类。 matplotlib 不能直接在小部件中绘制。需要在 Designer 中将小部件升级为 Qwidget 类。在 GUI 中选择小部件,右键单击选择提升小部件,选择 Qwidget,并为提升的类起一个好记的名称。这里我使用mplwidget。
生成的 widget_recev.py 会在最后生成一句话:
从 mplwidget 导入 mplwidget
放在class文件的开头,否则会报错。
mplwidget.py 模块需要自己构建。在对应路径下创建一个mplwidget.py文件。它的主要作用是创建一个继承了FigureCanvas和QWidget的类,并根据上面的预定义将其命名为mplwidget类。这个操作让原来的widget表单有了matplotlib画布的功能,可以在上面画图了。 mplwidget.py文件内容如下:
# _*_coding: UTF-8_*_
开发者:TXH
#开发时间:2021-09-05 14:42
文件名:mplwidget.py
开发工具:Python 3.7 + pychar IDE
从 PyQt5 导入 QtGui,QtWidgets
来自 matplotlib.backends.backend_qt5agg \
将 FigureCanvasQTAgg 导入为 FigureCanvas
从 matplotlib.figure 导入图
从 PyQt5.QtCore 导入 QThread
类 MplCanvas(FigureCanvas,QThread):
定义__init__(自我):
self.fig u003d 图()
FigureCanvas.__init__(self, self.fig)
FigureCanvas.setSizePolicy(自我,
QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Expanding)
FigureCanvas.updateGeometry(self)
类 mplwidget(QtWidgets.QWidget):
def __init__(自我,父母u003d无):
QtWidgets.QWidget.__init__(自身,父级)
self.canvas u003d MplCanvas()
self.vbl u003d QtWidgets.QVBoxLayout()
self.vbl.addWidget(self.canvas)
self.setLayout(self.vbl)
3.4 GUI设计成果
生成的 pyqt5 UI(小部件)_ Recev. Py) 如下。这个文件是根据Qt ui文件自动生成的,所以你一般只需要知道里面有哪些组件即可。您无需关注尺寸和位置设置的细节,因为它在 GUI 设计中已经做得很好。
从 PyQt5 导入 QtCore、QtGui、QtWidgets
from PyQt_Learning.UDP.GUI.mplwidget import mplwidget #根据MPL小部件的位置变化
类 Ui_Widget(对象):
def setupUi(自我,小部件):
Widget.setObjectName("Widget")
Widget.resize(280, 165)
self.label_2 u003d QtWidgets.QLabel(Widget)
self.label_2.setGeometry(QtCore.QRect(110, 10, 55, 16))
self.label_2.setObjectName("label_2")
self.lineEdit_2 u003d QtWidgets.QLineEdit(Widget)
self.lineEdit_2.setGeometry(QtCore.QRect(110, 30, 61, 21))
self.lineEdit_2.setObjectName("lineEdit_2")
self.pushButton u003d QtWidgets.QPushButton(Widget)
self.pushButton.setGeometry(QtCore.QRect(10, 120, 71, 24))
self.pushButton.setObjectName("pushButton")
self.label u003d QtWidgets.QLabel(Widget)
self.label.setGeometry(QtCore.QRect(12, 10, 55, 16))
self.label.setObjectName("标签")
self.lineEdit u003d QtWidgets.QLineEdit(Widget)
self.lineEdit.setGeometry(QtCore.QRect(12, 30, 81, 21))
self.lineEdit.setObjectName("lineEdit")
self.pushButton_2 u003d QtWidgets.QPushButton(Widget)
self.pushButton_2.setGeometry(QtCore.QRect(180, 120, 75, 24))
self.pushButton_2.setObjectName("pushButton_2")
self.lineEdit_5 u003d QtWidgets.QLineEdit(Widget)
self.lineEdit_5.setGeometry(QtCore.QRect(190, 80, 61, 21))
self.lineEdit_5.setObjectName("lineEdit_5")
self.label_3 u003d QtWidgets.QLabel(Widget)
self.label_3.setGeometry(QtCore.QRect(190, 60, 71, 16))
self.label_3.setObjectName("label_3")
self.label_4 u003d QtWidgets.QLabel(Widget)
self.label_4.setGeometry(QtCore.QRect(10, 60, 71, 16))
self.label_4.setObjectName("label_4")
self.lineEdit_3 u003d QtWidgets.QLineEdit(Widget)
self.lineEdit_3.setGeometry(QtCore.QRect(10, 80, 51, 21))
self.lineEdit_3.setObjectName("lineEdit_3")
self.label_5 u003d QtWidgets.QLabel(Widget)
self.label_5.setGeometry(QtCore.QRect(110, 60, 71, 16))
self.label_5.setObjectName("label_5")
self.lineEdit_4 u003d QtWidgets.QLineEdit(Widget)
self.lineEdit_4.setGeometry(QtCore.QRect(110, 80, 51, 21))
self.lineEdit_4.setObjectName("lineEdit_4")
self.label_6 u003d QtWidgets.QLabel(Widget)
self.label_6.setGeometry(QtCore.QRect(70, 80, 21, 16))
self.label_6.setObjectName("label_6")
self.retranslateUi(小部件)
QtCore.QMetaObject.connectSlotsByName(Widget)
def retranslateUi(自我,小部件):
_translate u003d QtCore.QCoreApplication.translate
Widget.setWindowTitle(_translate("Widget", "Data sender"))
self.label_2.setText(_translate("Widget", "port"))
self.lineEdit_2.setText(_translate("Widget", "9999"))
self.pushButton.setText(_translate("Widget", "Transmit sine"))
self.label.setText(_translate("Widget", "IP address"))
self.lineEdit.setText(_translate("Widget", "127.0.0.1"))
self.pushButton_2.setText(_translate("Widget", "停止发送"))
self.lineEdit_5.setText(_translate("Widget", "8"))
self.label_3.setText(_translate("Widget", "正弦通道数"))
self.label_4.setText(_translate("Widget", "正弦频率"))
self.lineEdit_3.setText(_translate("Widget", "50"))
self.label_5.setText(_translate("Widget", "正弦振幅"))
self.lineEdit_4.setText(_translate("Widget", "1"))
self.label_6.setText(_translate("Widget", "Hz"))
4 多线程编程UDP通信
使用Qt进行界面设计非常方便。 pyqt编程的难点在于底层的信号槽函数机制和多线程编程。我们先抛开多线程UDP编程,简单举例说明信号槽函数的原理。
4.1 信号和槽函数
该信号相当于 GUI 主循环中的一个事件。一旦触发事件,就会运行相应的槽函数(对象方法)。
信号可以是内置的或用户定义的。内置信号一般直接与组件相关联,可以按照一定的规则构造相应的槽函数,例如:
def on_pushButtom_clicked(self):
...
def on_pushButtom_2_clicked(self):
...
def on_pushButtom_3_clicked(self):
...
分别对应pushbutton和pushbutton_2,pushButton_3。当 clicked() 事件被触发时,这三个按钮会自动与 slot 函数关联。事件触发后,立即运行对应的槽函数。类似地,复选框_ 5 被触发,默认情况下会自动关联以下槽函数以传输经过验证的布尔信号:
def on_checkBox_5_toggled(self,checked):
...
自定义信号更灵活。当事件被触发时,它们通过 emit() 函数发送数据。在pyqt5中,信号传输的数据类型可以是python支持的任意类型。目前的测试表明,numpy.array、list、str、int、float等数据类型可以通过signal作为slot函数的输入传递给slot函数。
在主线程(或 GUI 主循环)中,自定义简单的信号和槽函数对,如下所示。
从 PyQt5.QtCore 导入 QObject
从 PyQt5 导入 QtCore
类测试(QObject):
test_signal u003d QtCore.pyqtSignal(list) # 定义 test_signal 信号
def __init__(自我,父母u003d无):
超级()。__init__(父)
self.test_signal.connect(self.print_data) # 将信号与测试槽函数关联
def 切换(自我):
a u003d 列表([1, 2, 3, 4, 5])
self.test_signal.emit(a) # 向槽函数发送信号
@QtCore.pyqtSlot(列表)
def print_data(self, list_var): # 定义槽函数
一旦slot函数接收到test_对于signal发送的数据,立即执行后续内容
打印(列表_var)
测试 u003d 测试()
test.toggle()
[1, 2, 3, 4, 5]
信号一般在初始化方法之前定义。作为Qt类的成员,在定义信号时给出了传输信号的数据类型。以下示例中使用了列表类型。在初始化期间,信号与相应的槽函数相关联。然后根据需要以不同的方式向槽函数发送信号,槽函数接收到数据后立即执行函数的内容。上面的例子比较简单。主要在主线程中触发信号和槽函数。下面的论文将给出一个多线程信号和槽函数传输数据的案例。
4.2 多线程
pyqt的主界面使用主线程,可以看成是一个死循环。一旦主线程发生比较耗时的操作,主线程就会假死,这体现在GUI界面上就是无响应,无操作。
GUI编程一般遵循GUI界面和代码界面分开设计的原则。主线程只负责管理基本GUI的动作,耗时的操作通过子线程计算。
回到“多线程UDP通信”的话题,在创建GUI的基础上,UDP通信接收器的主要功能如下。代码实现了从主线程到子线程、从子线程到子线程和从子线程到主线程的三种数据传输情况。
当然,信号和槽函数之间的所有连接都必须在主线程中完成。
具体方法是在主线程中创建一个子线程实例,并将子线程作为主线程的成员。这样就可以实现子线程与子线程之间以及子线程与主线程之间的信号传输。
# _*_coding: UTF-8_*_
开发者:TXH
#开发时间:2021年8月26日18:24
文件名:Receiver.py
开发工具:Python 3.7 + pychar IDE
进口插座
导入系统
从 PyQt5.QtWidgets 导入 QApplication、QMainWindow
从 PyQt_Learning.UDP.GUI.widget_recev 导入 Ui_MainWindow
从 PyQt5 导入 QtCore,uic
从 PyQt5.QtCore 导入 QThread,pyqtSlot
class QmyDialog(QMainWindow): #主窗体本身占用一个主线程
UDP_para u003d QtCore.pyqtSignal(列表)
发件人_para u003d QtCore.pyqtSignal(列表)
def __init__(自我,父母u003d无):
超级()。__init__(父)
self.pauseu003d假
self.statusBar().showMessage('加载 UI...')
如果为 0:
self.ui u003d uic.loadUi('E:/Pywork/PyQt_Learning/UDP/GUI/widget_recev.ui',self) #
其他:
self.ui u003d Ui_MainWindow()
self.ui.setupUi(自我)
self.statusBar().showMessage('初始化画布...')
self.canvas u003d self.ui.widget.canvas #绘图设置
self.canvas.ax1 u003d self.canvas.fig.add_subplot(111)
self.canvas.ax1.get_yaxis().grid(True)
self.statusBar().showMessage('Init UDP...') # 状态栏更新
self.UDP u003d UDPThread(self.para(1)) # 创建子线程1
self.UDP_para.connect(self.UDP.UDP_para_update) # 主线程向UDP线程传递参数
self.statusBar().showMessage('Init plot sender...')
self.Plot_fig u003d Plot_Thread(self.para(2)) # 创建子线程2
self.UDP.send\data.connect(self.Plot\fig.send) # UDP子线程向绘图子线程发送数据
self.sender_para.connect(self.Plot_fig.Sender_para_update) # 主线程传参数给绘图子线程
self.Plot_fig.plot_data.connect(self.plot_fig) # 绘图子线程将数据发送给主线程plot_fig函数,让它绘制
self.statusBar().showMessage('准备好了!')
def on_pushButton_clicked(self): # 设置参数
self.update_udp_para()
self.update_sender_para()
self.statusBar().showMessage('参数改变...')
def on_pushButton_2_clicked(self): # 接收数据
self.update_udp_para()
self.update_sender_para()
self.UDP.pause u003d 假
self.Plot_fig.pauseu003dFalse
self.UDP.start()
self.ui.lineEdit.setReadOnly(True)
self.ui.lineEdit_2.setReadOnly(True)
self.Plot_fig.start()
self.statusBar().showMessage('正在接收数据...')
def on_pushButton_3_clicked(self): # 停止接收和绘制
self.pauseu003d真
self.update_udp_para()
self.update_sender_para()
self.statusBar().showMessage('接收暂停!')
def plot_fig(self,temp): # 绘图函数,不计算,避免主线程阻塞,收到数据后立即绘图
self.canvas.ax1.clear()
self.canvas.ax1.plot(温度)
self.canvas.fig.tight_layout()
self.canvas.draw()
def update_udp_para(self): #UPD子线程参数设置
self.UDP_para.emit(self.para(1))
def update_sender_para(self):#绘图计算子线程参数设置
self.sender_para.emit(self.para(2))
def 参数(自我,标志):
如果标志u003du003d1:
返回列表([self.ui.lineEdit.text(),int(self.ui.lineEdit_2.text()),self.pause])
其他:
返回列表([int(self.ui.lineEdit_3.text()),self.pause])
定义UDP接收线程类
类 UDPThread(QThread):
发送_data u003d QtCore.pyqtSignal(str)
def __init__(self,udp_para_list):
超级()。__init__()
self.IP、self.Port、self.pause u003d udp_para_list
self.s u003d socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 设置socket协议为UDP
self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
def run(self) -> None: # 无限循环接收UDP数据
尝试:
self.s.bind((self.IP, self.Port))
除了:通过
我 u003d 1
with open('out.txt', 'w') as f: # 将获取的UDP数据保存到本地txt
当真:
temp u003d self.s.recv(1024).decode('utf-8') # 接收socket数据
如果 i%11u003du003d1:
self.send_data.emit(temp)
f.writelines(temp + '\n')
iu003d(i+1)%2000
if self.pause: # 判断是否跳出循环
休息
def UDP_para_update(self,udp_para_list):
self.Ip,self.Port,self.pauseu003dudp_para_list
定义绘图计算子线程类
类绘图_Thread(QThread):
绘图_data u003d QtCore.pyqtSignal(列表)
def __init__(self,para_list):
超级()。__init__()
self.pause u003d 假
自我. 数据 u003d []
self.max_len u003d para_list[0]
自我.iu003d1
def change_Len(self,len): # 绘图长度设置
如果长度<1000:
self.max_lenu003d1000
其他:
self.max_len u003d len
接收UDP子线程发送的数据,转发给GUI中的绘图方法
@pyqtSlot(str)
定义发送(自我,数据):
self.data.append(浮点数(数据))
如果 len(self.data)>self.max_len:
self.datau003dself.data[(len(self.data)-self.max_len):]
自我.iu003d(自我.i+1)%(1000)
如果 self.iu003du003d0:
self.plot_data.emit(self.data) # 每收到 1000 个点就将数据发送到 GUI 进行绘制
def Sender_para_update(self,para_list): # 根据主线程的信号更新绘图长度
self.max_len,self.pauseu003dpara\list
如果 __name__ u003du003d "__main__":
app u003d QApplication(sys.argv) # 调用父构造函数创建表单
form u003d QmyDialog() # 创建 UI 对象
form.show() #
sys.exit(app.exec()) #
效果如下图:
单击记录并绘制。程序在保存UDP接收到的数据的同时,将一些数据发送到GUI界面进行绘制。
5 Pyinstaller打包成exe
当pyinstaller将代码打包成exe时,会面临生成的exe过大的情况。一个很小的功能的exe文件体积高达200M。归根结底,pyinstaller将一些相互关联的安装包打包成exe,但是大部分安装包在当前项目中并没有真正使用。
经过测试,您可以使用 pipenv 创建一个干净的虚拟环境并减小 exe 的大小。只在环境中安装需要的pyinstaller、pyqt5、numpy等。虚拟环境中生成的pyqt5 exe可执行文件只有几十兆。
如果想进一步压缩,可以下载upx.exe,放到pipenv虚拟环境的Script文件夹下。打包时会自动调用pyinstaller。压缩量虽小,但多少有些效果。毕竟,没有其他补救措施。
在 pipenv 虚拟环境中运行以下代码:
# Gen_EXE.py
导入我们
错误 u003d os.system('pyinstaller --clean -Fw E:\Pywork\PyQt_Learning\UDP\GUI\Receiver.py E:\Pywork\PyQt_Learning\UDP\GUI \widget_recev.py E:\Pywork\PyQt_Learning\UDP\GUI\mplwidget.py') # 添加所有相关的py文件
if not error: print('成功生成exe文件!')
最终大小约为44M,还可以。
自动调用。压缩量虽小,但多少有些效果。毕竟,没有其他补救措施。
在 pipenv 虚拟环境中运行以下代码:
# Gen_EXE.py
导入我们
错误 u003d os.system('pyinstaller --clean -Fw E:\Pywork\PyQt_Learning\UDP\GUI\Receiver.py E:\Pywork\PyQt_Learning\UDP\GUI \widget_recev.py E:\Pywork\PyQt_Learning\UDP\GUI\mplwidget.py') # 添加所有相关的py文件
if not error: print('成功生成exe文件!')
最终大小约为44M,还可以。
写在最后:由于兴趣,水平有限。欢迎大家交流。
更多推荐
所有评论(0)