PyQt5 GUI receives UDP data and dynamically draws (signaling between multiple threads)

1. Usage of QT

  pyqt5 is the python version of qt. It mainly exists in the form of objects. It cannot be visualized in the process of programming, which brings a lot of inconvenience. To simplify pyqt5's interface design, we can use qt Designer (C: \ qt \ 5.12.11 \ mingw73)_ 32 \ bin \ designer. Exe). The generated graphical interface is usually saved in the file with *. ui suffix.

  pyqt5 can directly call the. ui file, or convert the designed. ui file into pyqt5 class in. py format through pyuic.exe provided with pyqt5 for other modules to call.

  there are many QT installation tutorials on the CSDN forum, which will not be repeated here. It is recommended to use Qt5. At present, the higher version of Qt6 has not been included in the back-end support by matplotlib.

2. Pychart setting

  first of all, Amway Yibo has high convenience in code color theme, function interface, python environment switching, terminal opening, Jupiter notebook support, variable viewing, Markdown support, console multi opening, etc. Therefore, I mainly use pychrm for relevant code development and recommend it.

2.1 installing Pyqt5 and pyinstaller packages

  open the terminal at the bottom of pycharm and enter the following code to install pyqt5 package and pyinstaller package. Pyuninstaller package is a tool used to package pyqt5GUI design into exe executable file. With this tool, you can copy the program to other windows computers.

pip install pyqt5,pyinstaller


  the matplotlib package needs to be installed in a similar way. I won't repeat it. After installing pyqt5, matplotlib will automatically use pyqt5 as the back end. The drawn image effect is better and the toolbar is more practical. It is recommended for daily use.

2.2 pychar pyqt tool configuration

  when using Qt for interface design, you can configure several tools of Qt software as external tools in pycharm (this is one of pycharm's many advantages), which is convenient to call at any time. Pycharm and click file - Settings - tools - external tools (self reference in English version) to enter the external tool addition interface.

  • Qt Designer tool (design Qt interface)

Program path:

C:\Qt\5.12.11\mingw73_32\bin\designer.exe

Working directory:

$ProjectFileDir$

  • Qt Creator tool (design Qt interface)

  the program path is in the Script directory of the corresponding environment:

C:\Anaconda3\envs\tensor37\Scripts\pyuic5.exe

  parameter settings are as follows:

$FileName$ -o $FileNameWithoutExtension$.py

  working directory:

$ProjectFileDir$

  • PyUI tool (convert Qt UI interface to python code)

  program path:

C:\Qt\Tools\QtCreator\bin\qtcreator.exe

  working directory:

$ProjectFileDir$


  after completing the above settings, you can open designer.exe and creator.exe GUI design applications in the right-click menu. Select the. ui file and right-click pyuic.exe to generate a. py file with the same name, which contains pyqt5 classes that can generate the same GUI.

3 UDP graphical interface design

3.1 GUI design

  right click in the blank space of pycharm, select external tools, open designer, and create a new Main Window.

  as needed, I designed a UDP network programming interface. The main function is to receive sinusoidal data sent by UDP client, save the data to txt file and draw it in the widget (form part) at the bottom.

  the target operation interface is as follows:

3.2 converting GUI files to py files

  after designing the interface, save the widget_recev.ui graphic file. In the Project Explorer on the left, you can select the UI file and right-click to use external pyuic tools to convert it into widgets_ Recev.py file for program call. This operation is often used in subsequent debugging. Change the GUI at any time and generate new py files at any time. The new py file will overwrite the original content, so it is recommended to build another python module to call the module to avoid information loss.

3.3 widget form promotion and integration of matplotlib functions

  it should be noted here that figure canvas in matplotlib and widgets in GUI are subclasses of Qwidget. matplotlib cannot draw directly in widgets. It is necessary to promote widgets to Qwidget class in Designer. Select the widget in the GUI, right-click to select the promotion widget, select Qwidget, and give the promoted class a memorable name. Here I use mplwidget.

  generated widget_recev.py will generate a sentence at the end:

from mplwidget import mplwidget

  put it at the beginning of the class file, otherwise an error will be reported.

  the mplwidget.py module needs to be built by itself. Create a mplwidget.py file under the corresponding path. Its main function is to create a class that inherits both FigureCanvas and QWidget, and name it mplwidget class according to the predefined above. This operation makes the original widget form have the function of matplotlib canvas, and you can draw on it. The contents of mplwidget.py file are as follows:

# _*_coding: UTF-8_*_
# Developed by: TXH
# Development time: 2021-09-05 14:42
# File name: mplwidget.py
# Development tool: Python 3.7 + pychar IDE

from PyQt5 import QtGui,QtWidgets
from matplotlib.backends.backend_qt5agg \
 import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
from PyQt5.QtCore import QThread

class MplCanvas(FigureCanvas,QThread):
    def __init__(self):
        self.fig = Figure()
        FigureCanvas.__init__(self, self.fig)
        FigureCanvas.setSizePolicy(self,
        QtWidgets.QSizePolicy.Expanding,
        QtWidgets.QSizePolicy.Expanding)
        FigureCanvas.updateGeometry(self)

class mplwidget(QtWidgets.QWidget):
    def __init__(self, parent=None):
        QtWidgets.QWidget.__init__(self, parent)
        self.canvas = MplCanvas()
        self.vbl = QtWidgets.QVBoxLayout()
        self.vbl.addWidget(self.canvas)
        self.setLayout(self.vbl)

3.4 GUI design results

  generated pyqt5 UI (widget)_ Recev. Py) is as follows. This file is automatically generated according to the Qt ui file, so you generally only need to know which components are in it. You don't need to pay attention to the details of size and location settings, because it has been done well in GUI design.

from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt_Learning.UDP.GUI.mplwidget import mplwidget  # Change according to the location of MPL widget

class Ui_Widget(object):
    def setupUi(self, Widget):
        Widget.setObjectName("Widget")
        Widget.resize(280, 165)
        self.label_2 = QtWidgets.QLabel(Widget)
        self.label_2.setGeometry(QtCore.QRect(110, 10, 55, 16))
        self.label_2.setObjectName("label_2")
        self.lineEdit_2 = QtWidgets.QLineEdit(Widget)
        self.lineEdit_2.setGeometry(QtCore.QRect(110, 30, 61, 21))
        self.lineEdit_2.setObjectName("lineEdit_2")
        self.pushButton = QtWidgets.QPushButton(Widget)
        self.pushButton.setGeometry(QtCore.QRect(10, 120, 71, 24))
        self.pushButton.setObjectName("pushButton")
        self.label = QtWidgets.QLabel(Widget)
        self.label.setGeometry(QtCore.QRect(12, 10, 55, 16))
        self.label.setObjectName("label")
        self.lineEdit = QtWidgets.QLineEdit(Widget)
        self.lineEdit.setGeometry(QtCore.QRect(12, 30, 81, 21))
        self.lineEdit.setObjectName("lineEdit")
        self.pushButton_2 = QtWidgets.QPushButton(Widget)
        self.pushButton_2.setGeometry(QtCore.QRect(180, 120, 75, 24))
        self.pushButton_2.setObjectName("pushButton_2")
        self.lineEdit_5 = QtWidgets.QLineEdit(Widget)
        self.lineEdit_5.setGeometry(QtCore.QRect(190, 80, 61, 21))
        self.lineEdit_5.setObjectName("lineEdit_5")
        self.label_3 = QtWidgets.QLabel(Widget)
        self.label_3.setGeometry(QtCore.QRect(190, 60, 71, 16))
        self.label_3.setObjectName("label_3")
        self.label_4 = QtWidgets.QLabel(Widget)
        self.label_4.setGeometry(QtCore.QRect(10, 60, 71, 16))
        self.label_4.setObjectName("label_4")
        self.lineEdit_3 = QtWidgets.QLineEdit(Widget)
        self.lineEdit_3.setGeometry(QtCore.QRect(10, 80, 51, 21))
        self.lineEdit_3.setObjectName("lineEdit_3")
        self.label_5 = QtWidgets.QLabel(Widget)
        self.label_5.setGeometry(QtCore.QRect(110, 60, 71, 16))
        self.label_5.setObjectName("label_5")
        self.lineEdit_4 = QtWidgets.QLineEdit(Widget)
        self.lineEdit_4.setGeometry(QtCore.QRect(110, 80, 51, 21))
        self.lineEdit_4.setObjectName("lineEdit_4")
        self.label_6 = QtWidgets.QLabel(Widget)
        self.label_6.setGeometry(QtCore.QRect(70, 80, 21, 16))
        self.label_6.setObjectName("label_6")

        self.retranslateUi(Widget)
        QtCore.QMetaObject.connectSlotsByName(Widget)

    def retranslateUi(self, Widget):
        _translate = 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", "Stop sending"))
        self.lineEdit_5.setText(_translate("Widget", "8"))
        self.label_3.setText(_translate("Widget", "Number of sinusoidal channels"))
        self.label_4.setText(_translate("Widget", "Sinusoidal frequency"))
        self.lineEdit_3.setText(_translate("Widget", "50"))
        self.label_5.setText(_translate("Widget", "Sinusoidal amplitude"))
        self.lineEdit_4.setText(_translate("Widget", "1"))
        self.label_6.setText(_translate("Widget", "Hz"))

4 multi thread programming UDP communication

  it is very convenient to use Qt for interface design. The difficulty of pyqt programming lies in the underlying signal slot function mechanism and multithreading programming. Let's put aside multithreaded UDP programming and give a simple example of the principle of signal slot function.

4.1 signal and slot functions

  the signal is equivalent to an event in the GUI main loop. Once an event is triggered, the corresponding slot function (object method) will run.

  the signal can be built-in or user-defined. Built in signals are generally directly associated with components, and corresponding slot functions can be constructed according to certain rules, such as:

def on_pushButtom_clicked(self): 
    ...
def on_pushButtom_2_clicked(self): 
    ...
def on_pushButtom_3_clicked(self): 
    ...

Corresponding to pushbutton and pushbutton respectively_ 2,pushButtom_3. The three buttons are automatically associated with the slot function when the clicked() event is triggered. After the event is triggered, the corresponding slot function is run immediately. Similarly, check box_ 5 is triggered, the following slot functions are automatically associated by default to transmit chencked Boolean signals:

def on_checkBox_5_toggled(self,checked):
    ...

  custom signals are more flexible. They send data through the emit() function when an event is triggered. In pyqt5, the data type of signal transmission can be any type supported by python. The current test shows that the data types such as numpy.array, list, str, int and float can be passed to the slot function through signal as the input of the slot function.

In the main thread (or GUI main loop), customize simple signal and slot function pairs as follows.

from PyQt5.QtCore import QObject
from PyQt5 import QtCore

class Test(QObject):
    test_signal = QtCore.pyqtSignal(list)  # Define test_signal signal
    def __init__(self, parent=None):
        super().__init__(parent)
        self.test_signal.connect(self.print_data)  # Associate the signal with the test slot function

    def toggle(self):
        a = list([1, 2, 3, 4, 5])
        self.test_signal.emit(a)  # Send signal to slot function

    @QtCore.pyqtSlot(list)
    def print_data(self, list_var):  # Define slot function
        # Once the slot function receives test_ For the data sent by signal, execute the subsequent content immediately
        print(list_var) 

test = Test()
test.toggle()

>>> [1, 2, 3, 4, 5]

  the signal is generally defined before the initialization method. As a member of Qt class, the data type of the transmitted signal is given when defining the signal. The list type is used in the following example. During initialization, the signal is associated with the corresponding slot function. Then, signals are sent to the slot function in different methods as needed, and the slot function executes the contents of the function immediately after receiving the data. The above example is relatively simple. It mainly triggers signals and slot functions in the main thread. The following paper will give a case of multi-threaded signal and slot function transmitting data.

4.2 multithreading

  the main interface of pyqt uses the main thread, which can be regarded as an endless loop. Once a more time-consuming operation occurs in the main thread, the main thread will fake death, which is reflected in the GUI interface is no response and no operation.

  GUI programming generally follows the principle of separate design of GUI interface and code interface. The main thread is only responsible for managing the actions of basic GUI, and the time-consuming operations are calculated through sub threads.

  back to the topic of "multi thread UDP communication", on the basis of creating GUI, the main functions of UDP communication receiver are as follows. The code realizes three cases of data transmission from the main thread to the sub thread, from the sub thread to the sub thread and from the sub thread to the main thread.

Of course, all connections between signals and slot functions must be completed in the main thread.
The specific method is to create a sub thread instance in the main thread and take the sub thread as a member of the main thread. In this way, the signal transmission between the sub thread and the sub thread and between the sub thread and the main thread can be realized.

# _*_coding: UTF-8_*_
# Developed by: TXH
# Development time: 18:24, August 26, 2021
# File name: Receiver.py
# Development tool: Python 3.7 + pychar IDE

import socket
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt_Learning.UDP.GUI.widget_recev import Ui_MainWindow
from PyQt5 import QtCore,uic
from PyQt5.QtCore import QThread,pyqtSlot

class QmyDialog(QMainWindow): # The main form itself occupies a main thread
    UDP_para = QtCore.pyqtSignal(list)
    sender_para = QtCore.pyqtSignal(list)
    def __init__(self, parent=None):
        super().__init__(parent)
        self.pause=False
        self.statusBar().showMessage('Load UI...')
        if 0:
            self.ui = uic.loadUi('E:/Pywork/PyQt_Learning/UDP/GUI/widget_recev.ui',self) #
        else:
            self.ui = Ui_MainWindow()
            self.ui.setupUi(self)
        self.statusBar().showMessage('Init Canvas...')
        self.canvas = self.ui.widget.canvas # Drawing settings
        self.canvas.ax1 = self.canvas.fig.add_subplot(111)
        self.canvas.ax1.get_yaxis().grid(True)

        self.statusBar().showMessage('Init UDP...') # Status bar update
        self.UDP = UDPThread(self.para(1)) # Create child thread 1
        self.UDP_para.connect(self.UDP.UDP_para_update) # The main thread passes parameters to the UDP thread

        self.statusBar().showMessage('Init plot sender...')
        self.Plot_fig = Plot_Thread(self.para(2)) # Create child thread 2
        self.UDP.send_data.connect(self.Plot_fig.send) # UDP sub thread sends data to drawing sub thread
        self.sender_para.connect(self.Plot_fig.Sender_para_update) # The main thread passes parameters to the drawing child thread
        self.Plot_fig.plot_data.connect(self.plot_fig) # The drawing sub thread sends the data to the main thread plot_fig function, let it draw
        self.statusBar().showMessage('Ready!')

    def on_pushButton_clicked(self):  # Set parameters
        self.update_udp_para()
        self.update_sender_para()
        self.statusBar().showMessage('Para changed...')

    def on_pushButton_2_clicked(self):  # receive data 
        self.update_udp_para()
        self.update_sender_para()
        self.UDP.pause = False
        self.Plot_fig.pause=False
        self.UDP.start()
        self.ui.lineEdit.setReadOnly(True)
        self.ui.lineEdit_2.setReadOnly(True)
        self.Plot_fig.start()
        self.statusBar().showMessage('Receiving data...')

    def on_pushButton_3_clicked(self):  # Stop receiving and drawing
        self.pause=True
        self.update_udp_para()
        self.update_sender_para()
        self.statusBar().showMessage('Receiving paused!')

    def plot_fig(self,temp): # Drawing function, not calculated, to avoid main thread blocking, drawing immediately after receiving data
        self.canvas.ax1.clear()
        self.canvas.ax1.plot(temp)
        self.canvas.fig.tight_layout()
        self.canvas.draw()

    def update_udp_para(self): # UPD sub thread parameter settings
        self.UDP_para.emit(self.para(1))

    def update_sender_para(self):# Drawing calculation sub thread parameter settings
        self.sender_para.emit(self.para(2))

    def para(self,flag):
        if flag==1:
            return list([self.ui.lineEdit.text(),int(self.ui.lineEdit_2.text()),self.pause])
        else:
            return list([int(self.ui.lineEdit_3.text()),self.pause])
            
# Define UDP receive thread class
class UDPThread(QThread):
    send_data = QtCore.pyqtSignal(str)
    def __init__(self,udp_para_list):
        super().__init__()
        self.IP,self.Port,self.pause = udp_para_list
        self.s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # Set socket protocol to UDP
        self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    def run(self) -> None: # Receive UDP data in an endless loop
        try:
            self.s.bind((self.IP, self.Port))
        except:pass
        i = 1
        with open('out.txt', 'w') as f: # Save the obtained UDP data to the local txt
            while True:
                temp = self.s.recv(1024).decode('utf-8')  # Receive socket data
                if i%11==1:
                    self.send_data.emit(temp)
                f.writelines(temp + '\n')
                i=(i+1)%2000
                if self.pause: # Determine whether to jump out of the loop
                    break

    def UDP_para_update(self,udp_para_list):
        self.Ip,self.Port,self.pause=udp_para_list

# Define drawing calculation sub thread class
class Plot_Thread(QThread):
    plot_data = QtCore.pyqtSignal(list)
    def __init__(self,para_list):
        super().__init__()
        self.pause = False
        self.data = []
        self.max_len = para_list[0]
        self.i=1

    def change_Len(self,len): # Drawing length setting
        if len<1000:
            self.max_len=1000
        else:
            self.max_len = len

    # Receive the data sent by UDP sub thread and forward it to the drawing method in GUI
    @pyqtSlot(str)
    def send(self,data): 
        self.data.append(float(data))
        if len(self.data)>self.max_len:
            self.data=self.data[(len(self.data)-self.max_len):]
        self.i=(self.i+1)%(1000)
        if self.i==0:
            self.plot_data.emit(self.data)  # Send data to GUI for drawing every 1000 points received

    def Sender_para_update(self,para_list): # Update the drawing length according to the signal of the main thread
        self.max_len,self.pause=para_list

# if __name__ == "__main__":
app = QApplication(sys.argv)  # Call the parent constructor to create the form
form = QmyDialog()  # Create UI object
form.show()  #
sys.exit(app.exec())  #

  the effect is shown as follows:
Click record and draw. While saving the data received by UDP, the program sends some data to the GUI interface for drawing.

5 Pyinstaller packaged into exe

  when pyinstaller packages the code into exe, it will face the situation that the generated exe is too large. The volume of exe with a very small function is up to 200M. In the final analysis, pyinstaller packages some interrelated installation packages into exe, but most installation packages are not really used in the current project.

  after testing, you can use pipenv to create a clean virtual environment and reduce the size of exe. Only install the required pyinstaller, pyqt5, numpy, etc. in the environment. The pyqt5 exe executable file generated in the virtual environment is only tens of megabytes.

  if you want to further compress, you can download upx.exe and put it into the Script folder in the pipenv virtual environment. pyinstaller will be called automatically when packaging. The compression amount is small, but it is somewhat effective. After all, there are no other remedial measures.

  run the following code in the pipenv virtual environment:

# Gen_EXE.py
import os
error = 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') # Add all relevant py files
if not error: print('Successfully generated exe File!')

  the final size is about 44M, which is OK.

Automatically called. The compression amount is small, but it is somewhat effective. After all, there are no other remedial measures.

  run the following code in the pipenv virtual environment:

# Gen_EXE.py
import os
error = 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') # Add all relevant py files
if not error: print('Successfully generated exe File!')

  the final size is about 44M, which is OK.

  write at the end: due to interest, the level is limited. Welcome to communicate with each other.

Keywords: Pycharm Qt udp

Added by ricmetal on Wed, 08 Sep 2021 00:55:31 +0300