Bài 39: Lập trình Đa Tiến Trình trong Python-PyQt6- Part 1

Trong quá trình triển khai phần mềm, đôi khi chúng ta muốn cập nhật giao diện thời gian thực, muốn kiểm soát tiến độ từng bước quá trình xử lý, đồng thời muốn giao diện luôn mượt mà, không bị treo khi chương trình cùng lúc thực hiện nhiều tác vụ đồng thời. Trường hợp này chính là cơ chế xử lý Đa tiến trình (Multi-Threading).

Trong phần Đa tiến trình này Tui sẽ hướng dẫn 3 ví dụ minh họa về cập nhật giao diện thời gian thực để bạn làm quen với cách thức sử dụng các thư viện hỗ trợ đa nhiệm trong Python- PyQt6.

Giao diện trên tui minh họa người dùng có thể vẽ các QPushButton thời gian thực, trong quá trình vẽ người sử dụng có thể vừa tương tác tới các QPushButton đó là nhấn vào nó thì đổi nền xanh và chữ đỏ, đồng thời cũng cho biết tiến độ thực hiện của quá trình xử lý, ví dụ ở trên là đang xử lý tác vụ được 62%. Lưu ý là chương trình vẫn cứ đang thực hiện tác vụ vẽ các QPushButton, người sử dụng cứ nhấn các Button thoải mái mà không bị treo phần mềm.

Vậy ta làm điều này như thế nào?

Bước 1: Tạo dự án “LearnMultithreadingPart1” có cấu trúc như dưới đây:

  • “WorkerSignals.py” là lớp kế thừa từ QObject, khai báo các Signal để thực hiện call back gửi dữ liệu trở về giao diện chính để cập nhật giao diện thời gian thực
  • “Worker.py” là lớp kế thừa từ QRunnable, nó dùng để tạo các đối tượng chạy đa tiến trình, trong quá trình xử lý nó sẽ thông qua WorkerSignals để gửi tín hiệu về cho màn hình chính cập nhật giao diện.
  • “MainWindow.ui” là giao diện thiết kế tương tác người dùng bằng Qt Designer
  • “MainWindow.py” là Generate Python code cho giao diện “MainWindow.ui”
  • “MainWindowEx.py” là file mã lệnh kế thừa từ Generate Python Code để xử lý: Nạp giao diện, hiển thị chart, gán sự kiện và không bị lệ thuộc vào giao diện bị thay đổi sau này khi Generate lại code
  • “MyApp.py” là file mã lệnh thực thi chương trình.

Dưới đây là Flow Chart xử lý đa tiến trình trong các mã lệnh ở các Lớp trong Project:

  • Flow Chart ở trên Tui vẽ ra 5 bước tổng quan, Tui giải thích sơ lược để các bạn nắm:
    1. Step 1: Trong lớp WorkerSignals ta khai báo các Signal để làm nhiệm vụ bắn các tín hiệu từ Background Thread (chạy ngầm) qua Main Thread (UI)
    2. Step 2: Trong lớp Worker ta sẽ sử dụng đối tượng WorkerSignals ở bước 1, đối tượng này có 2 signal: biến runningSignal để bắn tín hiệu cập nhật dữ liệu cũng như tiến độ về cho MainThread để cập nhật giao diện thời gian thực, biến finishSignal sẽ bắn tín hiệu về MainThread để báo rằng tiến trình đã hoàn tất.
    3. Step 3: Trong Main Thread khai báo các đối tượng Worker, QThreadPool để thực thi đa tiến trình, nó cần báo cho Background Thread biết là khi bắn tín hiệu (khi gọi hàm emit) về cho Main Thread thì slot nào sẽ lắng nghe
    4. Step 4: Trong quá trình thực thi Background Thread, chương trình sẽ bắn tín hiệu về cho Main Thread thông qua hàm emit
    5. Step 5: Bất cứ khi nào Background Thread gọi hàm emit thì ngay lập tức slot được khai báo trong Main Thread sẽ nhận được tín hiệu từ Back ground Thread gửi về. Tùy thuộc vào WorkerSignals ta khai báo các đối số như thế nào thì ta truyền dữ liệu tương ứng.

Bước 2: Thiết kế giao diện “MainWindow.ui” và đặt tên cho Widget/layout như hình dưới đây:

Bạn lần lượt kéo thả các Widget vào giao diện như trên, lưu ý việc lựa chọn các Layout cho phù hợp. và đặt tên các Widget như hình.

Bước 3: Generate Python Code cho “MainWindow.ui”, lúc này mã lệnh “MainWindow.py” tự động được tạo ra:

# Form implementation generated from reading ui file 'MainWindow.ui'
#
# Created by: PyQt6 UI code generator 6.4.2
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again.  Do not edit this file unless you know what you are doing.


from PyQt6 import QtCore, QtGui, QtWidgets


class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(429, 419)
        self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)
        self.verticalLayout.setObjectName("verticalLayout")
        self.horizontalLayout = QtWidgets.QHBoxLayout()
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.label = QtWidgets.QLabel(parent=self.centralwidget)
        self.label.setObjectName("label")
        self.horizontalLayout.addWidget(self.label)
        self.lineEditNumberOfButton = QtWidgets.QLineEdit(parent=self.centralwidget)
        self.lineEditNumberOfButton.setObjectName("lineEditNumberOfButton")
        self.horizontalLayout.addWidget(self.lineEditNumberOfButton)
        self.pushButtonDraw = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonDraw.setObjectName("pushButtonDraw")
        self.horizontalLayout.addWidget(self.pushButtonDraw)
        self.verticalLayout.addLayout(self.horizontalLayout)
        self.progressBarPercent = QtWidgets.QProgressBar(parent=self.centralwidget)
        self.progressBarPercent.setProperty("value", 0)
        self.progressBarPercent.setObjectName("progressBarPercent")
        self.verticalLayout.addWidget(self.progressBarPercent)
        self.scrollArea = QtWidgets.QScrollArea(parent=self.centralwidget)
        self.scrollArea.setWidgetResizable(True)
        self.scrollArea.setObjectName("scrollArea")
        self.scrollAreaWidgetContents = QtWidgets.QWidget()
        self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 405, 276))
        self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents")
        self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents)
        self.verticalLayout_3.setObjectName("verticalLayout_3")
        self.verticalLayoutButton = QtWidgets.QVBoxLayout()
        self.verticalLayoutButton.setObjectName("verticalLayoutButton")
        self.verticalLayout_3.addLayout(self.verticalLayoutButton)
        self.scrollArea.setWidget(self.scrollAreaWidgetContents)
        self.verticalLayout.addWidget(self.scrollArea)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 429, 26))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(parent=MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

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

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "Trần Duy Thanh - MultiThreading"))
        self.label.setText(_translate("MainWindow", "Number of Button:"))
        self.pushButtonDraw.setText(_translate("MainWindow", "Draw"))

Bước 4: Viết mã lệnh cho “WorkerSignals.py

from PyQt6.QtCore import QObject, pyqtSignal

class WorkerSignals(QObject):
    runningSignal=pyqtSignal(str,int)
    finishSignal=pyqtSignal()

Lớp này Tui khai báo 2 biến đối tượng có kiểu pyqtSignal:

  • runningSignal là signal để truyền tín hiệu trong quá trình thực hiện cập nhật giao diện thời gian thực, nó truyền 2 đối số, đối số 1 là chuỗi giá trị hiển thị cho QPushButton, đối số 2 là percent thực hiện quá trình cập nhật giao diện thời gian thực
  • finishSignal là signal thông báo đã hoàn tất thực hiện chạy đa tiến trình

Bước 5: Viết mã lệnh cho “Worker.py

import random
import time

from PyQt6.QtCore import QRunnable

from WorkerSignals import WorkerSignals


class Worker(QRunnable):
    def __init__(self,n):
        super().__init__()
        self.n=n
        self.signals=WorkerSignals()

    def run(self) -> None:
        for i in range(self.n):
            x=random.randint(0,100)
            label="value-%s"%x
            percent=int(100*(i+1)/self.n)
            self.signals.runningSignal.emit(label,percent)
            time.sleep(0.01)
        self.signals.finishSignal.emit()

Lớp này Tui định nghĩa 2 hàm:

  • Constructor nhận vào đối số n là số lượng QPushButton mà người dùng muốn vẽ trên giao diện. Nó cũng khởi tạo đối tượng WorkerSignals để sử dụng cho việc truyền tin về màn hình chính (Main Thread), Chúng ta chỉ có thể cập nhật giao diện ở Main Thread
  • override hàm run, hàm này là chạy long time, Tui đang giả lập nó 1 vòng lặp để tạo ngẫu nhiên các nhãn cho QPushButton, và tính percent. Sau đó dùng biên singal runningSignal để truyền tín hiệu cùng với dữ liệu về cho Main Thread thông qua hàm emit. Mỗi lần lặp Tui cho nó nghỉ 0.01 giây. Cuối cùng khi kết thúc vòng lặp Tui gọi finishSignal để truyền tín hiệu là kết thúc tiến trình

Bước 6: Viết mã lệnh cho “MainWindowEx.py”

Khởi tạo Constructor như bên dưới, và override hàm setupUi để thiết lập giao diện cũng như gọi signal cho Button draw threading

from functools import partial

from PyQt6.QtCore import QThreadPool
from PyQt6.QtWidgets import QPushButton, QMessageBox

from MainWindow import Ui_MainWindow
from Worker import Worker


class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        super().__init__()
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow
        self.pushButtonDraw.clicked.connect(self.processDraw)

Hàm processDraw sẽ tạo đối tượng QThreadPool để kích hoạt tiến trình bằng hàm start(worker):

def processDraw(self):
    n=(int)(self.lineEditNumberOfButton.text())
    self.threadpool=QThreadPool()
    worker=Worker(n)
    worker.signals.runningSignal.connect(self.drawingThreading)
    worker.signals.finishSignal.connect(self.drawingFinished)
    self.threadpool.start(worker)

Trong hàm processDraw này ta khai báo đối tượng worker, truyền n là số lượng QPushButton mà người sử dụng muốn vẽ.

Đồng thời ta cần gán các signal: runningSignal, finishSignal thông qua các slot drawingThreading và drawingFinished; Lúc này khi bên Worker thực hiện gọi lệnh emit thì bên MainThread này sẽ tự động thực hiện chính xác các Slot.

Hàm drawingThreading() để vẽ các QPushButton thời gian thực và cập nhật percent tiến độ, cũng như cho gán Signal runtime cho phép người dùng đổi màu nền và màu chữ của QPushButton:

def drawingThreading(self,label,percent):
    self.progressBarPercent.setValue(percent)
    button=QPushButton(self.MainWindow)
    button.setText(label)
    button.clicked.connect(partial(self.changeColor,button))
    self.verticalLayoutButton.addWidget(button)

Hàm đổi màu nền và màu chữ của QPushButton:

def changeColor(self,button):
    button.setStyleSheet("background-color: cyan;color:red");

Hàm drawingFinished() để lắng nghe khi nào thì Worker truyền tín hiệu hoàn tất tiến trình:

def drawingFinished(self):
    msgBox=QMessageBox(self.MainWindow)
    msgBox.setText("Drawing multi threading finished")
    msgBox.exec()

Mã lệnh đầy đủ của MainWindowEx.py:

from functools import partial

from PyQt6.QtCore import QThreadPool
from PyQt6.QtWidgets import QPushButton, QMessageBox

from MainWindow import Ui_MainWindow
from Worker import Worker


class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        super().__init__()
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow
        self.pushButtonDraw.clicked.connect(self.processDraw)
    def processDraw(self):
        n=(int)(self.lineEditNumberOfButton.text())
        self.threadpool=QThreadPool()
        worker=Worker(n)
        worker.signals.runningSignal.connect(self.drawingThreading)
        worker.signals.finishSignal.connect(self.drawingFinished)
        self.threadpool.start(worker)
    def drawingThreading(self,label,percent):
        self.progressBarPercent.setValue(percent)
        button=QPushButton(self.MainWindow)
        button.setText(label)
        button.clicked.connect(partial(self.changeColor,button))
        self.verticalLayoutButton.addWidget(button)
    def changeColor(self,button):
        button.setStyleSheet("background-color: cyan;color:red");
    def drawingFinished(self):
        msgBox=QMessageBox(self.MainWindow)
        msgBox.setText("Drawing multi threading finished")
        msgBox.exec()
    def show(self):
        self.MainWindow.show()

Bước 7: Viết mã lệnh “MyApp.py” để thực thi chương trình:

from PyQt6.QtWidgets import QApplication, QMainWindow

from MainWindowEx import MainWindowEx

app=QApplication([])
myApp= MainWindowEx()
myApp.setupUi(QMainWindow())
myApp.show()
app.exec()

Thực thi MyApp.py ta có kết quả, Chạy phần mềm lên:

Như vậy chúng ta đã làm xong xử lý đa tiến trình để cập nhật giao diện thời gian thực, các bạn biết cách khai báo WorkerSignals, Worker cũng như sử dụng QThreadPool để kích hoạt Worker. Soure code của bài này các bạn tải ở đây:

https://www.mediafire.com/file/zq1tkdk1yqfyilh/LearnMultithreadingPart1.rar/file

Bài học sau Tui sẽ hướng dẫn các bạn xử lý đa tiến trình mà cập nhật dữ liệu thời gian thực cho QTableWidget. Các bạn chú ý theo dõi

Chúc các bạn thành công

One thought on “Bài 39: Lập trình Đa Tiến Trình trong Python-PyQt6- Part 1”

Leave a Reply