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

Bài đa tiến trình này Tui sẽ trình bày kỹ thuật xử lý Đa tiến trình phức tạp hơn, và nó được ứng dụng thực tế cho các bài tải đồng thời nhiều trang web trên mạng về máy tính. Sẽ có nhiều Background Thread được chạy đồng thời, và chúng ta sẽ quản lý các Background Thread này thông qua id. Giao diện phần mềm như dưới đây:

  • Chương trình cung cấp giao diện cho người dùng chọn Protocol trong QCombobox (https và https) cùng với domain website muốn tải.
  • Khi người dùng nhấn nút “Add”, các URL sẽ được đưa vào QTableWidget, widget này có 3 cột: Cột # là số thứ tự cũng coi như là Id của từng Background Thread, Cột URL là link Website mà người dùng muốn tải, và cột Progress cho biết tiến độ đang tải dữ liệu được bao nhiêu %.
  • Lưu ý rằng mỗi một link website tải sẽ là một Background Thread, các thread này sẽ chạy đồng thời, các dữ liệu và tiến độ sẽ được cập nhật liên tục lên Main Thread (UI) tương ứng với từng background thread.
  • Khi tải xong thì biểu tượng Icon màu xanh ở cột # sẽ tự động xuất hiện, ô URL sẽ đổi qua nền vàng, và Progress sẽ là 100%
  • Nút “Clear URLs” dùng để xóa các URL mà người dùng đã nhập trên giao diện
  • Nút “Start Downloading” để bắt đầu thực hiện các Background Thread chạy đồng thời.

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

  • “ItemDownload.py” là file chứa lớp đối tượng ItemDownload để chương trình mỗi lần thực thi sẽ lấy Id, domain, data line và percent thực hiện trong tiểu trình rồi gửi về cho Main Thread cập nhật lên giao diện.
  • “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.
  • “ProgressDelegate.py” là file chứa lớp đối tượng ProgressDelegate kế thừa từ QStyledItemDelegate nhằm hỗ trợ việc vẽ Progress Bar tiến độ downloading cho mỗi URL trong QTableWidget.
  • “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.
  • Thư mục “download” dùng để lưu nội dung HTML tải được của các URL trên mạng
  • Thư mục “images” chứa các icon để trang trí cho giao diện được đẹp

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. Ta chỉ có thể cập nhật giao diện ở Main Thread, không thể cập nhật giao diện ở Background Thread, đó là lý do vì sao ta phải bắn tín hiệu về cho Main Thread.

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à bạn đặt tên các Widget như hình.

  • QComboBox (comboBoxProtocol) dùng để lưu mặc định 2 Protocols là httpshttp
  • QLineEdit (lineEditURL) là ô nhập domain muốn tải
  • QPushButton (pushButtonAddURL): Người dùng nhấn vào nút lệnh này thì chương trình sẽ đưa dữ liệu URL vào giao diện QTableWidget
  • QPushButton (pushButtonClearURL): Xóa các dữ liệu trong QTableWidget
  • QPushButton (pushButtonStartDownloading): Khi nhấn vào nút lệnh này, chương trình sẽ tạo nhiều Background Thread để tải và cập nhật tiến độ giao diện thời gian thực
  • QTableWidget (tableWidgetURL): Lưu các URL mà người dùng muốn tải, đồng thời cập nhật tiến độ tỉ lệ % tải dữ liệu từ Internet cho mỗi Item.

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(524, 394)
        icon = QtGui.QIcon()
        icon.addPixmap(QtGui.QPixmap("images/ic_logo.jpg"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        MainWindow.setWindowIcon(icon)
        self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.pushButtonStartDownloading = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonStartDownloading.setGeometry(QtCore.QRect(160, 310, 151, 41))
        icon1 = QtGui.QIcon()
        icon1.addPixmap(QtGui.QPixmap("images/ic_download.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.pushButtonStartDownloading.setIcon(icon1)
        self.pushButtonStartDownloading.setIconSize(QtCore.QSize(24, 24))
        self.pushButtonStartDownloading.setObjectName("pushButtonStartDownloading")
        self.tableWidgetURL = QtWidgets.QTableWidget(parent=self.centralwidget)
        self.tableWidgetURL.setGeometry(QtCore.QRect(20, 50, 481, 251))
        self.tableWidgetURL.setObjectName("tableWidgetURL")
        self.tableWidgetURL.setColumnCount(3)
        self.tableWidgetURL.setRowCount(0)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetURL.setHorizontalHeaderItem(0, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetURL.setHorizontalHeaderItem(1, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetURL.setHorizontalHeaderItem(2, item)
        self.pushButtonClearURL = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonClearURL.setGeometry(QtCore.QRect(20, 310, 121, 41))
        icon2 = QtGui.QIcon()
        icon2.addPixmap(QtGui.QPixmap("images/ic_delete.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.pushButtonClearURL.setIcon(icon2)
        self.pushButtonClearURL.setIconSize(QtCore.QSize(24, 24))
        self.pushButtonClearURL.setObjectName("pushButtonClearURL")
        self.label = QtWidgets.QLabel(parent=self.centralwidget)
        self.label.setGeometry(QtCore.QRect(30, 10, 55, 16))
        self.label.setObjectName("label")
        self.lineEditURL = QtWidgets.QLineEdit(parent=self.centralwidget)
        self.lineEditURL.setGeometry(QtCore.QRect(130, 10, 261, 22))
        self.lineEditURL.setObjectName("lineEditURL")
        self.pushButtonAddURL = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonAddURL.setGeometry(QtCore.QRect(400, 7, 93, 31))
        icon3 = QtGui.QIcon()
        icon3.addPixmap(QtGui.QPixmap("images/ic_add.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.pushButtonAddURL.setIcon(icon3)
        self.pushButtonAddURL.setIconSize(QtCore.QSize(24, 24))
        self.pushButtonAddURL.setObjectName("pushButtonAddURL")
        self.comboBoxProtocol = QtWidgets.QComboBox(parent=self.centralwidget)
        self.comboBoxProtocol.setGeometry(QtCore.QRect(60, 10, 61, 22))
        self.comboBoxProtocol.setObjectName("comboBoxProtocol")
        self.comboBoxProtocol.addItem("")
        self.comboBoxProtocol.addItem("")
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 524, 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.pushButtonStartDownloading.setText(_translate("MainWindow", "Start Downloading"))
        item = self.tableWidgetURL.horizontalHeaderItem(0)
        item.setText(_translate("MainWindow", "#"))
        item = self.tableWidgetURL.horizontalHeaderItem(1)
        item.setText(_translate("MainWindow", "URL"))
        item = self.tableWidgetURL.horizontalHeaderItem(2)
        item.setText(_translate("MainWindow", "Progress"))
        self.pushButtonClearURL.setText(_translate("MainWindow", "Clear URLs"))
        self.label.setText(_translate("MainWindow", "URL:"))
        self.pushButtonAddURL.setText(_translate("MainWindow", "Add"))
        self.comboBoxProtocol.setItemText(0, _translate("MainWindow", "https"))
        self.comboBoxProtocol.setItemText(1, _translate("MainWindow", "http"))

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

class ItemDownload:
    def __init__(self,id,domain,data,percent):
        self.id=id
        self.domain=domain
        self.data=data
        self.percent = percent

Lớp ItemDownload được định nghĩa với 4 thuộc tính: id, domain, data, percent. Với constructor nhận vào 4 biến như trên.

  • Dựa vào thuộc tính id để biết được Background Thread nào đang gửi tín hiểu về Main Thread
  • domain là thuộc tính cho biết tên domain muốn tải, nó cũng dùng để lưu tên file tương ứng xuống ở cứng
  • data là từng dòng dữ liệu tại thời điểm tải
  • percent là thuộc tính cho biết gói tin này đang ở tiến độ bao nhiêu %

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

from PyQt6.QtCore import QObject, pyqtSignal

from ItemDownload import ItemDownload

class WorkerSignals(QObject):
    runningSignal = pyqtSignal(ItemDownload)
    finishSignal = pyqtSignal(int)

Lớp WorkerSignals được kế thừa từ QObject, 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. Signal này truyền 1 đối số là đối tượng ItemDownload nó sẽ được bắn về Main Thread để cập nhật giao diện thời gian thực trên QTableWidget, vì ItemDownload Tui đã thiết kế có thuộc tính percent rồi nên không cần dùng đối số 2 ở đây như bài trước (dĩ nhiên bạn có thể đổi)
  • finishSignal là signal thông báo đã hoàn tất thực hiện chạy đa tiến trình

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

import os
import time
from urllib.parse import urlparse

import requests
from PyQt6.QtCore import pyqtSlot, QRunnable

from ItemDownload import ItemDownload
from WorkerSignals import WorkerSignals


class Worker(QRunnable):
    """
    Worker thread

    Inherits from QRunnable to handle worker thread setup, signals
    and wrap-up.

    :param id: The id for this worker
    :param url: The url to retrieve
    """

    def __init__(self, id, url):
        super().__init__()
        self.id = id
        self.url = url
        self.domain = urlparse(self.url).netloc
        path="download/"+self.domain+".html"
        if os.path.isfile(path):
            os.remove(path)
        self.signals = WorkerSignals()

    @pyqtSlot()
    def run(self):
        r = requests.get(self.url)
        lists=r.text.splitlines()
        for i in range(len(lists)):
            data=lists[i]
            percent = int(100 * (i + 1) / len(lists))
            itemDownload=ItemDownload(self.id,self.domain,data,percent)
            self.signals.runningSignal.emit(itemDownload)
            time.sleep(0.01)
        self.signals.finishSignal.emit(self.id)

Lớp Worker được kế thừa từ lớp QRunnable, lớp này Tui định nghĩa 2 hàm:

  • Constructor nhận vào đối số id (là id để xác định Background Thread nào sẽ nắm giữ nó) và url (là link muốn download). 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, nó xử lý cho 1 Background Thread để tải 1 URL tương ứng với id được truyền vào. 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

Như vậy rõ ràng trong Main Thread Tui sẽ có 1 vòng lặp để tạo ra nhiều đối tượng Worker này với Id và Url khác nhau.

Bước 7: Viết mã lệnh cho “ProgressDelegate.py”. Lớp này là lớp để làm Custom ProgressBar xuất hiện trong từng Ô của QTableWidget.

from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QStyledItemDelegate, QStyleOptionProgressBar, QApplication, QStyle


class ProgressDelegate(QStyledItemDelegate):
    def paint(self, painter, option, index):
        try:
            progress = index.data(Qt.ItemDataRole.UserRole)

            opt = QStyleOptionProgressBar()
            opt.rect = option.rect
            opt.minimum = 0
            opt.maximum = 100
            opt.progress = progress
            opt.text = f"{progress}%"
            opt.textVisible = True
            opt.state |= QStyle.StateFlag.State_Horizontal
            style = (
                option.widget.style() if option.widget is not None else QApplication.style()
            )
            style.drawControl(
                QStyle.ControlElement.CE_ProgressBar, opt, painter, option.widget
            )
        except:
            pass

Bước 8: 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 các Button:

from urllib.parse import urlparse

from PyQt6.QtCore import Qt, QThreadPool
from PyQt6.QtWidgets import QTableWidgetItem
from pyqtgraph import QtGui

from ItemDownload import ItemDownload
from MainWindow import Ui_MainWindow
from ProgressDelegate import ProgressDelegate
from Worker import Worker


class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        super().__init__()
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow
        self.urls=[]
        self.delegate = ProgressDelegate(self.tableWidgetURL)
        self.tableWidgetURL.setItemDelegateForColumn(2, self.delegate)
        self.pushButtonAddURL.clicked.connect(self.processAddURL)
        self.pushButtonClearURL.clicked.connect(self.clearData)
        self.pushButtonStartDownloading.clicked.connect(self.processMultiThreading)

Dòng lệnh 20 Tui khởi tạo đối tượng ProgressDelegate (self.delegate)

Dòng lệnh 21 Tui thiết lập cột Delegate cho QTableWidget là cột số 2. Cột này chính là custom Progress Bar để hiển thị tiến độ thực hiện Background Thread tải dữ liệu

Hàm processAddURL() để thêm URL vào QTableWidget khi người dùng nhấn nút “Add“, đồng thời lưu url vào danh sách self.urls:

def processAddURL(self):
    i=self.tableWidgetURL.rowCount()
    self.tableWidgetURL.insertRow(self.tableWidgetURL.rowCount())
    it_index = QTableWidgetItem(str(i + 1))
    protocol=self.comboBoxProtocol.currentText()
    url=protocol+"://"+self.lineEditURL.text()
    self.urls.append(url)
    it_url = QTableWidgetItem(url)
    it_progress = QTableWidgetItem()
    it_progress.setData(Qt.ItemDataRole.UserRole, 0)
    self.tableWidgetURL.setItem(i, 0, it_index)
    self.tableWidgetURL.setItem(i, 1, it_url)
    self.tableWidgetURL.setItem(i, 2, it_progress)
    self.lineEditURL.setText("")

Khi người dùng nhấn nút lệnh “Add” thì các URL sẽ được hiển thị lên QTableWidget như hình dưới đây:

Hàm clearData() dùng để xóa dữ liệu trên QTableWidget khi người dùng nhấn vào nút lệnh “Clear URLs”:

def clearData(self):
    self.urls.clear()
    self.tableWidgetURL.setRowCount(0)

Hàm processMultiThreading() sẽ tạo đối tượng QThreadPool để kích hoạt tiến trình bằng hàm start(worker), tuy nhiên trong bài này là nhiều Workers:

def processMultiThreading(self):

    self.threadpool = QThreadPool()
    print(
        "Multithreading with maximum %d threads" % self.threadpool.maxThreadCount()
    )
    for n, url in enumerate(self.urls):
        worker = Worker(n, url)
        worker.signals.runningSignal.connect(self.downloadingHtml)
        worker.signals.finishSignal.connect(self.finishedDownloadHtml)

        # Execute
        self.threadpool.start(worker)

Vòng lặp for của hàm processMultiThreading() sẽ start nhiều Worker chạy Background Thread. Lưu ý rằng mỗi một ThreadPool nó có khả năng chạy tối đa bao nhiêu Threads, chứ không phải chạy bao nhiêu cũng được, nên trong trường hợp số Worker nhiều hơn maximum của ThreadPool thì cần bổ sung thêm kỹ thuật khác.

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

Hàm downloadingHtml () để hiển thị các ItemDownloading lên QTableWidget theo thời gian thực và cập nhật percent tiến độ:

def downloadingHtml(self, itemDownload):
    it_progress_update = self.tableWidgetURL.item(itemDownload.id, 2)
    it_progress_update.setData(Qt.ItemDataRole.UserRole, itemDownload.percent)
    fileName="download\\"+itemDownload.domain+".html"
    file = open(fileName, "a", encoding="utf-8")  # append mode
    line="%s\n"%itemDownload.data
    file.write(line)
    file.close()

Mỗi lần bên Worker thực hiện lệnh:

self.signals.runningSignal.emit(itemDownload)

Thì hàm downloadingHtml sẽ tự động được thực thi.

Khi hàm này chạy thì ta có giao diện tương tự:

Cột Progress sẽ hiển thị tỉ lệ % hoàn thành, và chương trình Tui để lưu append html text vào file. Nên khi hoàn thành thì bạn có thể vào thư mục download của dự án để mở HTML lên xem nội dung tải.

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

def finishedDownloadHtml(self,id):
    it_progress_url = self.tableWidgetURL.item(id, 1)
    it_progress_url.setBackground(Qt.GlobalColor.yellow)

    it_progress_id = self.tableWidgetURL.item(id, 0)
    it_progress_id.setIcon(QtGui.QIcon('images/ic_done.png'))

Khi bên Worker thực thi lệnh:

self.signals.finishSignal.emit(self.id)

Thì hàm finishedDownloadHtml sẽ được thực hiện

Những ItemDownload nào được hoàn thành 100% thì nó được bổ sung Icon màu xanh đánh dấu là xong, đồng thời tô nền vàng cột URL, trong trường hợp tải xong toàn bộ 100% thì ta có giao diện như bên dưới:

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

from urllib.parse import urlparse

from PyQt6.QtCore import Qt, QThreadPool
from PyQt6.QtWidgets import QTableWidgetItem
from pyqtgraph import QtGui

from ItemDownload import ItemDownload
from MainWindow import Ui_MainWindow
from ProgressDelegate import ProgressDelegate
from Worker import Worker


class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        super().__init__()
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow
        self.urls=[]
        self.delegate = ProgressDelegate(self.tableWidgetURL)
        self.tableWidgetURL.setItemDelegateForColumn(2, self.delegate)
        self.pushButtonAddURL.clicked.connect(self.processAddURL)
        self.pushButtonClearURL.clicked.connect(self.clearData)
        self.pushButtonStartDownloading.clicked.connect(self.processMultiThreading)

    def processAddURL(self):
        i=self.tableWidgetURL.rowCount()
        self.tableWidgetURL.insertRow(self.tableWidgetURL.rowCount())
        it_index = QTableWidgetItem(str(i + 1))
        protocol=self.comboBoxProtocol.currentText()
        url=protocol+"://"+self.lineEditURL.text()
        self.urls.append(url)
        it_url = QTableWidgetItem(url)
        it_progress = QTableWidgetItem()
        it_progress.setData(Qt.ItemDataRole.UserRole, 0)
        self.tableWidgetURL.setItem(i, 0, it_index)
        self.tableWidgetURL.setItem(i, 1, it_url)
        self.tableWidgetURL.setItem(i, 2, it_progress)
        self.lineEditURL.setText("")
    def clearData(self):
        self.urls.clear()
        self.tableWidgetURL.setRowCount(0)
    def processMultiThreading(self):

        self.threadpool = QThreadPool()
        print(
            "Multithreading with maximum %d threads" % self.threadpool.maxThreadCount()
        )
        for n, url in enumerate(self.urls):
            worker = Worker(n, url)
            worker.signals.runningSignal.connect(self.downloadingHtml)
            worker.signals.finishSignal.connect(self.finishedDownloadHtml)

            # Execute
            self.threadpool.start(worker)
    def downloadingHtml(self, itemDownload):
        it_progress_update = self.tableWidgetURL.item(itemDownload.id, 2)
        it_progress_update.setData(Qt.ItemDataRole.UserRole, itemDownload.percent)
        fileName="download\\"+itemDownload.domain+".html"
        file = open(fileName, "a", encoding="utf-8")  # append mode
        line="%s\n"%itemDownload.data
        file.write(line)
        file.close()
    def finishedDownloadHtml(self,id):
        it_progress_url = self.tableWidgetURL.item(id, 1)
        it_progress_url.setBackground(Qt.GlobalColor.yellow)

        it_progress_id = self.tableWidgetURL.item(id, 0)
        it_progress_id.setIcon(QtGui.QIcon('images/ic_done.png'))

    def show(self):
        self.MainWindow.show()

Bước 9: 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 ôn tập được cách khai báo WorkerSignals, Worker cũng như ôn tập cách sử dụng QThreadPool để kích hoạt Worker và ứng dụng nó vào cập nhật giao diện thời gian thực trên QTableWidget. Đặc biệt bổ sung được custom Progress Bar cho QTableWidget, xử lý được nhiều Background Threading để cập nhật giao diện đồng thời cho nhiều tiểu trình.

Soure code của bài này các bạn tải ở đây:

https://www.mediafire.com/file/85bi70lmhj3ejo1/LearnMultithreadingPart3.rar/file

Bài tập dành cho độc giả: Hãy bổ sung Button Cancel cho từng Background Thread (đang tải chưa xong thì không muốn tải nữa).

Từ bài học sau Tui sẽ làm hàng loạt các bài minh họa liên quan tới Mô hình máy học, tích hợp nó vào giao diện tương tác người dùng, ví dụ như mô hình máy học dự báo giá nhà, mô hình máy học dự báo kinh doanh, mô hình máy học phân tích cảm xúc… Các bạn chú ý theo dõi

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

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

Trong bài học 39 Tui đã minh họa cách xử lý đa tiến trình để vẽ giao diện và tương tác thời gian thực mà chương trình không bị treo. Trong bài học này Tui tiếp tục minh họa phần cập nhật giao diện QTableWidget thời gian thực bằng kỹ thuật xử lý đa tiến trình có giao diện tương tự như dưới đây:

  • Chương trình giả lập N số lượng Customer từ giao diện
  • Khi bấm nút “Create” chương trình sẽ sử dụng kỹ thuật đa tiến trình đã cập nhật giao diện QTableWidget, mỗi lần tiểu trình tạo một Customer ngẫu nhiên nó sẽ gửi về tiến trình chính (giao diện) hiển thị Customer lên giao diện, đồng thời cho biết tỉ lệ hoàn thành cập nhật giao diện
  • Chẳng hạn ở trên ta thấy 44% tiến độ đã hoàn thành. Và trong quá trình thực hiện cập nhật giao diện thì người sử dụng có thể thao tác trên giao diện mà không bị treo.
  • Bạn có thể áp dụng bài này trong việc tải dữ liệu từ các Restful API để nạp danh sách đối tượng lên giao diện theo thời gian thực. Ví dụ như nạp các danh sách Sản phẩm từ Restful API chẳng hạn.

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

  • “Customer.py” là file chứa lớp đối tượng Customer để chương trình mỗi lần thực thi sẽ tạo ra một Customer ngẫu nhiên trong tiểu trình rồi gửi về cho Main Thread cập nhật lên giao diện.
  • “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. Ta chỉ có thể cập nhật giao diện ở Main Thread, không thể cập nhật giao diện ở Background Thread, đó là lý do vì sao ta phải bắn tín hiệu về cho Main Thread.

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à bạn đặt tên các Widget như hình.

  • QLineEdit (lineEditN) là ô nhập liệu
  • QProgressBar (progressbarPercent) là thanh trạng thái đánh dấu quá trình xử lý được bao nhiêu %.
  • QPushButton (pushButtonCreate) là widget sẽ ra lệnh để thực thi đa tiến trình
  • QTableWidget (tableWidgetCustomer)

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(436, 378)
        self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.label = QtWidgets.QLabel(parent=self.centralwidget)
        self.label.setGeometry(QtCore.QRect(40, 10, 131, 21))
        self.label.setObjectName("label")
        self.lineEditN = QtWidgets.QLineEdit(parent=self.centralwidget)
        self.lineEditN.setGeometry(QtCore.QRect(180, 10, 113, 22))
        self.lineEditN.setObjectName("lineEditN")
        self.pushButtonCreate = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonCreate.setGeometry(QtCore.QRect(310, 10, 93, 28))
        self.pushButtonCreate.setObjectName("pushButtonCreate")
        self.progressBarPercent = QtWidgets.QProgressBar(parent=self.centralwidget)
        self.progressBarPercent.setGeometry(QtCore.QRect(50, 50, 371, 23))
        self.progressBarPercent.setProperty("value", 0)
        self.progressBarPercent.setObjectName("progressBarPercent")
        self.tableWidgetCustomer = QtWidgets.QTableWidget(parent=self.centralwidget)
        self.tableWidgetCustomer.setGeometry(QtCore.QRect(20, 90, 401, 231))
        self.tableWidgetCustomer.setObjectName("tableWidgetCustomer")
        self.tableWidgetCustomer.setColumnCount(3)
        self.tableWidgetCustomer.setRowCount(0)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetCustomer.setHorizontalHeaderItem(0, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetCustomer.setHorizontalHeaderItem(1, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetCustomer.setHorizontalHeaderItem(2, item)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 436, 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-Multi Threading"))
        self.label.setText(_translate("MainWindow", "Number of Customer:"))
        self.pushButtonCreate.setText(_translate("MainWindow", "Create"))
        item = self.tableWidgetCustomer.horizontalHeaderItem(0)
        item.setText(_translate("MainWindow", "Id"))
        item = self.tableWidgetCustomer.horizontalHeaderItem(1)
        item.setText(_translate("MainWindow", "Name"))
        item = self.tableWidgetCustomer.horizontalHeaderItem(2)
        item.setText(_translate("MainWindow", "Age"))

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

class Customer:
    def __init__(self,id=0,name=None,age=0):
        self.id=id
        self.name=name
        self.age=age

Lớp Customer được định nghĩa với 4 thuộc tính: id, name, age. Với constructor nhận vào 4 biến có giá trị mặc định như trên.

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

from PyQt6.QtCore import pyqtSignal, QObject

from Customer import Customer

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

Lớp WorkerSignals được kế thừa từ QObject, 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. Signal này truyền 2 đối số, đối số 1 là đối tượng Customer hiển thị lên QTableWidget, đố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 6: Viết mã lệnh cho “Worker.py

import random
import time

from PyQt6.QtCore import QRunnable

from Customer import Customer
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):
            percent=int(100*(i+1)/self.n)
            customer=Customer()
            customer.id=i
            customer.name="name "+str(i)
            customer.age=random.randint(18,60)
            self.signals.runningSignal.emit(customer,percent)
            time.sleep(0.01)
        self.signals.finishSignal.emit()

Lớp Worker được kế thừa từ lớp QRunnable, lớp này Tui định nghĩa 2 hàm:

  • Constructor nhận vào đối số n là số lượng Customer giả lập mà người dùng muốn hiển thị trên giao diện QTableWidget. 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 đối tượng, 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 7: 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 create

from PyQt6.QtCore import QThreadPool
from PyQt6.QtWidgets import QTableWidgetItem, 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.pushButtonCreate.clicked.connect(self.processUpdate)

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

def processUpdate(self):
    self.tableWidgetCustomer.setRowCount(0)
    n=int(self.lineEditN.text())
    self.threadPool = QThreadPool()
    worker=Worker(n)
    worker.signals.runningSignal.connect(self.updateUI)
    worker.signals.finishSignal.connect(self.finishthread)
    self.threadPool.start(worker)

Trong hàm processUpdate này ta khai báo đối tượng worker, truyền n là số lượng Customer mà người giả lập để hiển thị lên QTableWidget.

Đồng thời ta cần gán các signal: runningSignal, finishSignal thông qua các slot updateUI và finishthred; Lúc này khi bên Worker thực hiện gọi các 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 updateUI() để hiển thị các Customer lên QTableWidget theo thời gian thực và cập nhật percent tiến độ:

def updateUI(self,customer,percent):
    row=self.tableWidgetCustomer.rowCount()
    self.tableWidgetCustomer.insertRow(row)
    self.tableWidgetCustomer.setItem(row, 0, QTableWidgetItem(str(customer.id)))
    self.tableWidgetCustomer.setItem(row, 1, QTableWidgetItem(customer.name))
    self.tableWidgetCustomer.setItem(row, 2, QTableWidgetItem(str(customer.age)))
    self.progressBarPercent.setValue(percent)

Mỗi lần bên Worker thực hiện lệnh:

self.signals.runningSignal.emit(customer,percent)

Thì hàm updateUI sẽ tự động được thực thi.

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

def finishthread(self):
    msg=QMessageBox()
    msg.setText("finished!")
    msg.setWindowTitle("information")
    msg.exec()

Khi bên Worker thực thi lệnh:

self.signals.finishSignal.emit()

Thì hàm finishthread sẽ được thực hiện

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

from PyQt6.QtCore import QThreadPool
from PyQt6.QtWidgets import QTableWidgetItem, 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.pushButtonCreate.clicked.connect(self.processUpdate)

    def processUpdate(self):
        self.tableWidgetCustomer.setRowCount(0)
        n=int(self.lineEditN.text())
        self.threadPool = QThreadPool()
        worker=Worker(n)
        worker.signals.runningSignal.connect(self.updateUI)
        worker.signals.finishSignal.connect(self.finishthread)
        self.threadPool.start(worker)

    def updateUI(self,customer,percent):
        row=self.tableWidgetCustomer.rowCount()
        self.tableWidgetCustomer.insertRow(row)
        self.tableWidgetCustomer.setItem(row, 0, QTableWidgetItem(str(customer.id)))
        self.tableWidgetCustomer.setItem(row, 1, QTableWidgetItem(customer.name))
        self.tableWidgetCustomer.setItem(row, 2, QTableWidgetItem(str(customer.age)))
        self.progressBarPercent.setValue(percent)

    def finishthread(self):
        msg=QMessageBox()
        msg.setText("finished!")
        msg.setWindowTitle("information")
        msg.exec()

    def show(self):
        self.MainWindow.show()

Bước 8: 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([])
myWindow=MainWindowEx()
myWindow.setupUi(QMainWindow())
myWindow.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 ôn tập được cách khai báo WorkerSignals, Worker cũng như ôn tập cách sử dụng QThreadPool để kích hoạt Worker và ứng dụng nó vào cập nhật giao diện thời gian thực trên QTableWidget.

Soure code của bài này các bạn tải ở đây:

https://www.mediafire.com/file/oudzu7kgjil8n3k/LearnMultithreadingPart2.rar/file

Bài học sau Tui sẽ hướng dẫn các bạn xử lý đa tiến trình gồm nhiều Worker thực hiện đồng thời để tải dữ liệu Online nội dung các Website trên internet. Và cập nhật dữ liệu thời gian thực cho QProgressBar trong QTableWidget cùng với lưu nội dung tải được xuống ổ cứng. Các bạn chú ý theo dõi

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

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