Hướng dẫn lập trình android trần duy thanh năm 2024

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:

Hướng dẫn lập trình android trần duy thanh năm 2024

  • 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:

Hướng dẫn lập trình android trần duy thanh năm 2024

  • “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:

Hướng dẫn lập trình android trần duy thanh năm 2024

  • 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:

Hướng dẫn lập trình android trần duy thanh năm 2024

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à https và http
  • 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 Customer đượ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ướng dẫn lập trình android trần duy thanh năm 2024

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ự:

Hướng dẫn lập trình android trần duy thanh năm 2024

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:

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

0

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:

Hướng dẫn lập trình android trần duy thanh năm 2024

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

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

1

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

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

2

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

Hướng dẫn lập trình android trần duy thanh năm 2024

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

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:

Hướng dẫn lập trình android trần duy thanh năm 2024

  • 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:

Hướng dẫn lập trình android trần duy thanh năm 2024

  • “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:

Hướng dẫn lập trình android trần duy thanh năm 2024

  • 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:

Hướng dẫn lập trình android trần duy thanh năm 2024

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:

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

3

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

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

4

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“

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

5

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“

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

6

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

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

7

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

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

8

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 độ:

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

9

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:

from PyQt6.QtCore import QObject, pyqtSignal
from ItemDownload import ItemDownload
class WorkerSignals(QObject):
    runningSignal = pyqtSignal(ItemDownload)
    finishSignal = pyqtSignal(int)

0

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 QObject, pyqtSignal
from ItemDownload import ItemDownload
class WorkerSignals(QObject):
    runningSignal = pyqtSignal(ItemDownload)
    finishSignal = pyqtSignal(int)

1

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

from PyQt6.QtCore import QObject, pyqtSignal
from ItemDownload import ItemDownload
class WorkerSignals(QObject):
    runningSignal = pyqtSignal(ItemDownload)
    finishSignal = pyqtSignal(int)

2

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

Hướng dẫn lập trình android trần duy thanh năm 2024

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

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.

Hướng dẫn lập trình android trần duy thanh năm 2024

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:

Hướng dẫn lập trình android trần duy thanh năm 2024

  • “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:

Hướng dẫn lập trình android trần duy thanh năm 2024

  • 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:

Hướng dẫn lập trình android trần duy thanh năm 2024

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:

from PyQt6.QtCore import QObject, pyqtSignal
from ItemDownload import ItemDownload
class WorkerSignals(QObject):
    runningSignal = pyqtSignal(ItemDownload)
    finishSignal = pyqtSignal(int)

3

Bước 4: 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)

4

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“

from PyQt6.QtCore import QObject, pyqtSignal
from ItemDownload import ItemDownload
class WorkerSignals(QObject):
    runningSignal = pyqtSignal(ItemDownload)
    finishSignal = pyqtSignal(int)

5

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 PyQt6.QtCore import QObject, pyqtSignal
from ItemDownload import ItemDownload
class WorkerSignals(QObject):
    runningSignal = pyqtSignal(ItemDownload)
    finishSignal = pyqtSignal(int)

6

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

from PyQt6.QtCore import QObject, pyqtSignal
from ItemDownload import ItemDownload
class WorkerSignals(QObject):
    runningSignal = pyqtSignal(ItemDownload)
    finishSignal = pyqtSignal(int)

7

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:

from PyQt6.QtCore import QObject, pyqtSignal
from ItemDownload import ItemDownload
class WorkerSignals(QObject):
    runningSignal = pyqtSignal(ItemDownload)
    finishSignal = pyqtSignal(int)

8

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

from PyQt6.QtCore import QObject, pyqtSignal
from ItemDownload import ItemDownload
class WorkerSignals(QObject):
    runningSignal = pyqtSignal(ItemDownload)
    finishSignal = pyqtSignal(int)

9

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:

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)

0

Mã lệnh đầy đủ của MainWindowEx.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)

1

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

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)

2

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

Hướng dẫn lập trình android trần duy thanh năm 2024

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

Đây là phần cuối cùng về PyQtGraph trong chuỗi bài hướng dẫn, nó vẫn còn nhiều loại chart khác nhau rất đẹp và phong phú, các bạn tự tìm hiểu thêm. Đặc biệt PyQtGraph hỗ trợ cả 3D tương tác rất đẹp và chuyên nghiệp, nhưng vì thời gian có hạn nên Tui không thể giới thiệu tiếp, mà các bài học khác Tui sẽ chuyển qua Lập trình đa tiến trình để cập nhật giao diện thời gian thực được mượt nhất.

Bài học này ta sẽ làm Custom BarGraphItem bằng cách tạo ra một lớp kế thừa từ BarGraphItem. Mục đích của việc này là cung cấp khả năng tương tự sự kiện trực tiếp trên từng BarItem để hỗ trợ cho việc thống kê cũng như xem chi tiết được tốt nhất.

Hướng dẫn lập trình android trần duy thanh năm 2024

Bài này xử lý y chang như bài 37, nó chỉ khác ở chỗ ta làm CustomBarGraphItem và xử lý sự kiện cho các Bar item này, khi nhấn vào Bar Item nó sẽ tô nền vàng, khi nhấn qua bar item khác thì nó được phục hồi lại màu cũ.

Ta làm theo các bước dưới đây.

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

Hướng dẫn lập trình android trần duy thanh năm 2024

  • “dataset-revenue.xlsx” là file Excel mẫu của dự án
  • “CustomBarGraphItem.py” là lớp kế thừa từ BarGraphItem để làm Custom Bar Item, xử lý người dùng tương tác trực tiếp trên Bar Item
  • “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.

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

Hướng dẫn lập trình android trần duy thanh năm 2024

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.

Bạn lưu ý trong bài này Tui bổ sung nhóm QHBoxLayout ở bên trên để chọn Dataset là file Excel.

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

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)

3

Bước 4: Viết mã lệnh cho “CustomBarGraphItem.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)

4

Lớp CustomBarGraphItem kế thừa từ BarGraphItem.

Lớp này khai báo 2 global veriable:

  • previousBarGraph để lưu lại BarItem nào được click trước đó
  • previousBarGraphBrush để lưu lại màu của BarItem được click trước đó

2 biến này dùng để phục hồi lại trạng thái ban đầu khi người dùng chọn qua Bar item khác.

Và ta override phương thức mouseClickEvent, sự kiện này sẽ kiểm tra BarItem nào đang được click để đổi màu, ta dùng hàm _updateColors() để đổi màu

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

Lưu ý lớp này mã lệnh y chang như bài 37, Tui chỉ thay thế chỗ sử dụng CustomBarrGraphItem mà thôi, vì vậy Tui không giải thích chi tiết từng thành phần nữa, mà Tui gửi đầy đủ coding ở đây, cuối coding Tui giải thích sơ lược cách dùng CustomBarGraphItem:

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)

5

Chúng ta quan sát dòng lệnh 83 thôi, đây là chỗ thay thế pg.BarGraphItem bằng:

bargraphItem = CustomBarGraphItem(x=x, height=revenues, width=self.width, brush=brush, name=name).

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

from PyQt6.QtCore import QObject, pyqtSignal
from ItemDownload import ItemDownload
class WorkerSignals(QObject):
    runningSignal = pyqtSignal(ItemDownload)
    finishSignal = pyqtSignal(int)

2

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

Hướng dẫn lập trình android trần duy thanh năm 2024

Khi nhấn BarItem nào thì nó tự động đổi qua nền vàng, bấm qua BarItem khác thì nó phục hồi lại màu như cũ.

Như vậy chúng ta đã làm xong CustomGraphBarItem, các bạn đã biết cách xử lý sự kiện riêng cho từng bar item, có thể áp dụng vào nhiều mục đích khác nhau.

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

https://www.mediafire.com/file/kdvzf1melwq37ty/LearnPyQtBarGraphPart8.rar/file

Bài học sau Tui sẽ hướng dẫn các bạn cách lập trình Đa tiến trình để giúp cập nhật giao diện thời gian thực được mượt mà.

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

Bài học này Tui sẽ nâng cấp bài 36, đó là dữ liệu thay vì hardcode thì Tui đọc tự động từ Excel:

  • Tự lấy tick trong cột file Excel (Hà Nội, Huế, TP.HCM, Cần Thơ, An Giang)
  • Tự lấy Legend cho Bar Item (Quarter 1, Quarter 2, Quarter 3, Quarter 4)
  • Màu các Bar Item được tạo ngẫu nhiên theo 3 thông số R, G, B
  • Tự động vẽ Chart mà không lệ thuộc vào tiêu đề Cột hay tiều đề dòng của dữ liệu trong File Excel
  • Các chức năng còn lại được kế thừa từ bài 36
    Hướng dẫn lập trình android trần duy thanh năm 2024

Ta tiến hành từng bước như sau:

Bước 0: Cài thư viện “xlrd“

Hướng dẫn lập trình android trần duy thanh năm 2024

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

Hướng dẫn lập trình android trần duy thanh năm 2024

  • “dataset-revenue.xlsx” là file Excel mẫu của dự á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.

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

Hướng dẫn lập trình android trần duy thanh năm 2024

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.

Bạn lưu ý trong bài này Tui bổ sung nhóm QHBoxLayout ở bên trên để chọn Dataset là file Excel.

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

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)

7

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

Khai báo lớp MainWindowEx kế thừa từ Generate Python code ở bước 3, dưới đây là hàm override setupUi, Tui chỉnh sửa chút xíu so với bài học trước, đó là trong hàm này nó sẽ gọi 2 hạm setupBarGraph() để cấu hình Graph trống ban đầu và setupSignal() để xủ lý sự kiến:

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)

8

Hàm setupBarGraph() được viết theo mã lệnh như dưới đây:

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)

9

Hàm setupBarGraph() ở trên thiết kế Graph trống như hình dưới đây (các mã lệnh Tui đã giải thích chi tiết ở các bài học trước, nếu chưa hiểu thì các bạn xem lại):

Hướng dẫn lập trình android trần duy thanh năm 2024

Hàm setupSignal() để gán các Signal để triệu gọi các hàm, slot:

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

0

Hàm setupSignal() Tui có bổ sung slot “processPickDataset”, slot này sẽ cung cấp chức năng lựa chọn file Excel cho người dùng và vẽ lên Graph.

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

1

Hàm trên, Tui dùng QFileDialog cho người dùng chọn File được filter .xlsx mặc định.

Chương trình sẽ hiển thị tên file và đường dẫn lên trên QLineEdit

Và gọi hàm drawChart() để vẽ các BarGraph Item.

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

2

Hàm ẩn hiển thị lưới processChangeGrid():

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

3

Hàm ẩn hiển thị legend processChangeLegend():

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

4

Hàm thay đổi màu nền của Chart processChangeChartBackground():

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

5

Hàm thay đổi màu của từng Baritem, lưu ý là người sử dụng sẽ chọn Bar item trong QComboBox trước, sau đó nhấn vào button “bar color” để đổi màu:

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

6

Hàm autoTick() sẽ thiết lập label cho các thành phố ở bottom Cities:

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

7

Hàm autolabel() sẽ thiết lập label cho các Bar Item:

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

8

Label cho từng Bar item giúp người sử dụng dễ dàng quan sát và so sánh cũng như đánh giá dữ liệu.

Dưới đây là coding đầy đủ của MainWindowEx.py:

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

9

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

from PyQt6.QtCore import QObject, pyqtSignal
from ItemDownload import ItemDownload
class WorkerSignals(QObject):
    runningSignal = pyqtSignal(ItemDownload)
    finishSignal = pyqtSignal(int)

2

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

Hướng dẫn lập trình android trần duy thanh năm 2024

Ta nhấn vào nút “Pick Dataset” rồi trỏ tới file Excel data mẫu:

Hướng dẫn lập trình android trần duy thanh năm 2024

Chọn File Excel rồi nhấn Open, Sau khi nhấn Open ta có kết quả:

Hướng dẫn lập trình android trần duy thanh năm 2024

Chương trình sẽ tự động nạp tên vào QComBoBox như bài trước để ta có thể chọn và đổi màu cho từng bar item:

Hướng dẫn lập trình android trần duy thanh năm 2024

Ví dụ như ta chọn “Quarter 3” Bar Item rồi chọn Bar Color:

Hướng dẫn lập trình android trần duy thanh năm 2024

Ta thử chọn màu đỏ rồi bấm OK:

Hướng dẫn lập trình android trần duy thanh năm 2024

Màu Bar Item cũng đổi như ta mong muốn, tương tự như bài trước.

Như vậy tới đây Tui đã trình bày xong cách lập trình xử lý trực quan hóa dữ liệu với Multiple BarGraph đọc dữ liệu từ EXCEL. Ôn tập lại cách khai báo các mảng dữ liệu tương ứng với các trục, Ôn tập cách gọi các hàm liên quan tới BarGraphItem để hiển thị Chart. Cũng như ôn tập lại được toàn bộ kiến thức liên quan tới PlotWidget.

Ôn tập cách tương tác Chart thông qua các QCheckBox và QPushButton để tương tác lưới của chart, tương tác legend, đổi màu chart và màu bar.

Ôn tập cách lập trình để Auto Tick, và Auto Label cho Bar. Nó có ý nghĩa quan trọng để giúp Chart được rõ nghĩa, đọc dễ hiểu hơn.

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

https://www.mediafire.com/file/89e33vfnoiob6dl/LearnPyQtBarGraphPart7.rar/file

Bài học sau Tui sẽ hướng dẫn các bạn cách kế thừa BarGraphItem để lắng nghe và xử lý sự kiện click chuột ngay trên từng Baritem

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

Bài này chúng ta sẽ trực quan hóa dạng Multiple Bar Graph và nó được mở rộng từ bài trước.

Giả sử chúng ta có doanh số trung bình của công ty Lucy ở các chi nhánh Hà Nội, Huế, TP.HCM, Cần Thơ và An Giang theo 4 quý như sau:

Hà NộiHuếTP.HCMCần ThơAn GiangQuarter 1100200250190220Quarter 21207015080160Quarter 380270180200250Quarter 472230106210180

Sau khi viết các mã lệnh trực quan hóa thì ta có giao diện như dưới đây:

Hướng dẫn lập trình android trần duy thanh năm 2024

Sự khác biệt lớn của bài này là ở 2 chỗ:

  • Có nhiều Bar Item ở mỗi nhóm trực quan
  • Khi đổi Bar Color thì người dùng có thể lực chọn từng Bar Item để đổi màu, các Bar Item này được hiển thị trong QComboBox.

Chúng ta thực hiện các bước như sau:

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

Hướng dẫn lập trình android trần duy thanh năm 2024

  • “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.

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

Hướng dẫn lập trình android trần duy thanh năm 2024

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.

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

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)

1

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

Khai báo lớp MainWindowEx kết thừa từ Generate Python code ở bước 3, dưới đây là hàm override setupUi để định nghĩa các thành phần cho Bar Chart:

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)

2

Mã lệnh ở trên Tui định nghĩa các tiêu đề cho 5 thành phố (Hà Nội, Huế, TP.HCM, Cần Thơ, An Giang) và các doanh thu tương ứng theo các quý.

Đồng thời khai báo mảng các màu brushes (r, g, b, c) cho các Bar Item.

Trong setupUi() có gọi các hàm setupBarGraph(), autoTick(), và autoLabel()

Dưới đây là hàm setupBarGraph để vẽ Multiple Bar Graph:

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)

3

Các mã lệnh ở trên đã được giải thích chi tiết ở những bài học trước do đó các bạn cần xem lại. Chỉ có chỗ vòng lặp là Tui dùng để duyệt qua từng phần tử trong ma trận revenues, mỗi lần duyệt sẽ tạo ra một BarGraphItem và tiến hành định dạng cho nó.

Trong vòng lặp ngoài việc vẽ các Bar Item thì nó cũng đưa tên và bar item object vào bên trong QCombox để cho người sử dụng có thể lựa chọn từng Bar item trên giao diện để đổi màu theo mong muốn của họ.

Ở cuối hàm setupUi() cũng gọi các signal như hiển thị/ẩn lưới của chart, ẩn hiện Legend, đổi màu nền chart, đổi bar item color.

Hàm ẩn hiển thị lưới processChangeGrid():

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

3

Hàm ẩn hiển thị legend processChangeLegend():

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

4

Hàm thay đổi màu nền của Chart processChangeChartBackground():

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

5

Hàm thay đổi màu của từng Baritem, lưu ý là người sử dụng sẽ chọn Bar item trong QComboBox trước, sau đó nhấn vào button “bar color” để đổi màu:

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

6

Ví dụ chọn Quarter 2 trên giao diện:

Hướng dẫn lập trình android trần duy thanh năm 2024

Sau đó chọn Bar Color button:

Hướng dẫn lập trình android trần duy thanh năm 2024

Sau khi nhấn OK ta có màu vàng được thiết lập cho Bar Item Quarter 2:

Hướng dẫn lập trình android trần duy thanh năm 2024

Hàm autoTick() sẽ thiết lập label cho các thành phố ở bottom Cities:

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

7

Hàm autolabel() sẽ thiết lập label cho các Bar Item:

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

8

Label cho từng Bar item giúp người sử dụng dễ dàng quan sát và so sánh cũng như đánh giá dữ liệu.

Dưới đây là coding đầy đủ của MainWindowEx.py:

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("")

0

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

from PyQt6.QtCore import QObject, pyqtSignal
from ItemDownload import ItemDownload
class WorkerSignals(QObject):
    runningSignal = pyqtSignal(ItemDownload)
    finishSignal = pyqtSignal(int)

2

Thực thi MyApp.py ta có kết quả:

Hướng dẫn lập trình android trần duy thanh năm 2024

Như vậy tới đây Tui đã trình bày xong cách lập trình xử lý trực quan hóa dữ liệu với Multiple BarGraph. Ôn tập lại cách khai báo các mảng dữ liệu tương ứng với các trục, Ôn tập cách gọi các hàm liên quan tới BarGraphItem để hiển thị Chart. Cũng như ôn tập lại được toàn bộ kiến thức liên quan tới PlotWidget.

Ôn tập cách tương tác Chart thông qua các QCheckBox và QPushButton để tương tác lưới của chart, tương tác legend, đổi màu chart và màu bar.

Ôn tập cách lập trình để Auto Tick, và Auto Label cho Bar. Nó có ý nghĩa quan trọng để giúp Chart được rõ nghĩa, đọc dễ hiểu hơn.

Đặc biệt trong bài này chúng ta biết cách vẽ Multiple Bar Item lên 1 Chart, nạp được bar item vào QComboBox để người dùng có thể tùy ý chọn màu cho từng Bar item. Đây là bài học ứng dụng thực tế rất cao. Các bạn chú ý làm lại nhiều lần để thuần thục về kỹ thuật, cũng như biết cách áp dụng vào các bài multiple chart tương tự

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

https://www.mediafire.com/file/gyv9hgo9ezgro0e/LearnPyQtBarGraphPart6.rar/file

Bài học sau Tui sẽ hướng dẫn các bạn cách nạp dữ liệu từ Excel vào phần mềm Python và vẽ lên Bar chart. Nó cũng là một bài áp dụng trong thức tế, Các bạn chú ý theo dõi

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

Chúng ta đã biết cách sử dụng BarGraph để trực quan hóa dữ liệu ở các bài học trước, Lập trình xử lý sự kiện được trên BarGraph như ẩn hiện Background Grid, ẩn hiện Legend, thay đổi màu nền của Chart, thay đổi màu nền của Bar bằng QColorDialog. Với các bài minh họa trước thì các bạn đã có thể ứng dụng vào việc trực quan hóa dữ liệu dạng Bar ở một số trường hợp trong thực tế.

Trong bài học này Tui bổ sung thêm 2 kỹ thuật: Viết mã lệnh để tự động gán label cho các Bar Item, và viết mã lệnh để tự động tạo các Tick cho trục X của Chart nhằm làm rõ nghĩa cho các cột này thay vì chỉ các con số.

Đặc biệt Tui sẽ lập trình để 2 chức năng này có thể tự động hiểu được các Bar Chart theo dữ liệu khác nhau, có thể hiển thị chính xác theo Bar Item đơn hay nhóm các Bar Item, và các label có khả năng khớp màu với màu của mỗi Bar Item tương ứng.

Hướng dẫn lập trình android trần duy thanh năm 2024

  • Ở trên ta thấy Trục X thay vì chỉ hiển thị các con số 1, 2, 3, 4 mà nó được hiển thị bằng chuỗi “Quarter 1”, “Quarter 2”, “Quarter 3”, và “Quarter 4”
  • Đồng thời trên mỗi Bar item ta có các giá trị được hiển thị, ta gọi nó là label hoặc notation. Ví dụ ta thấy cột đầu tiên có giá trị 100, cột thứ 2 có giá trị 200, vân vân.

Rõ ràng việc hiển thị các Tick và Label sẽ giúp Chart được rõ nghĩa và dễ quan sát hơn.

Như vậy bài này sẽ có giao diện giống như bài trước, và các mã lệnh của một số chức năng cũng y chang nên Tui sẽ không giải thích lại các hàm này. Mà Tui chỉ nói tới 2 giải thuật về tự động tick và tự động label thôi.

Dưới đây là chi tiết từng bước thực hiện dự án:

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

Hướng dẫn lập trình android trần duy thanh năm 2024

  • “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.

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

Hướng dẫn lập trình android trần duy thanh năm 2024

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.

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

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("")

2

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

Chú ý rằng các mã lệnh nó y chang như bài 34, còn bài này Ta chỉ bổ sung thêm 2 giải thuật cho Auto Tick và Auto Label. Do đó Tui sẽ gửi code đầy đủ ở đây, và ở bên dưới Tui sẽ giải thích 2 giải thuật này:

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("")

3

Trong hàm setupUi() ta khai báo và gọi 3 lệnh:

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("")

4

Lệnh trên khai báo mảng chuỗi để hiển thị Tick ở bottom của Chart, nó sẽ thay thế cho các giá trị số, việc hiển thị chuỗi như này sẽ giúp cho Chart được rõ nghĩa hơn.

Lệnh:

self.autoTick(self.graphWidget,self.xlab)

Để gán các Tick

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("")

5

Hàm autoTick nhận vào 2 đối số:

-Đối số 1 là graphWidget (đối tượng PlotWidget)

– Đối số 2 là xlab là mảng chuỗi để hiển thị Tick

Vòng lặp mã lệnh bên trong autoTick ta sẽ đưa các chuỗi tick vào mảng ticks.

Lệnh graphWidget.getAxis(‘bottom’) để truy suất trục bottom,

Sau đó ta gọi lệnh setTicks(ticks) lúc này chương trình sẽ gán các Tick lên giao diện

Lệnh self.autolabel([self.bargraphItem]) để gán label cho mỗi Bar Item:

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

8

Hàm autolabel sẽ nhận vào 1 mảng BarGraphItem, Tui thiết kế đối số là mảng để chương trình có thể tự động hiểu được tùy ý số nhóm và số bar item trong mỗi nhóm.

Hàm barItem.getData() sẽ trả về 2 dữ liệu: xs mảng trục bottom, heights mảng độ cao của Bar Item (mảng giá trị mà ta trực quan hóa). Dựa vào 2 dữ liệu này ta dễ dàng phân bổ các vị trí label phù hợp

Ta dùng lệnh: pg.TextItem() để tạo các TextItem tương ứng với từng nhóm Bar Item, và nó được vẽ chính xác vị trí thông qua hàm setX(), setY().

Lệnh setAnchor() giúp neo label theo tọa độ cho từng Bar Item.

Dưới đây là mã lệnh đầy đủ của “MainWindowEx.py“:

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("")

7

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

from PyQt6.QtCore import QObject, pyqtSignal
from ItemDownload import ItemDownload
class WorkerSignals(QObject):
    runningSignal = pyqtSignal(ItemDownload)
    finishSignal = pyqtSignal(int)

2

Chạy “MyApp.py” ta có kết quả như mong muốn:

Hướng dẫn lập trình android trần duy thanh năm 2024

Như vậy tới đây Tui đã trình bày xong cách lập trình xử lý trực quan hóa dữ liệu với BarGraph. Ôn tập lại cách khai báo các mảng dữ liệu tương ứng với các trục, Ôn tập cách gọi các hàm liên quan tới BarGraphItem để hiển thị Chart. Cũng như ôn tập lại được toàn bộ kiến thức liên quan tới PlotWidget. Đặc biệt các bạn biết cách tương tác Chart thông qua các QCheckBox và QPushButton để tương tác lưới của chart, tương tác legend, đổi màu chart và màu bar.

Đồng thời bài này Tui hướng dẫn các bạn cách lập trình để Auto Tick, và Auto Label cho Bar. Nó có ý nghĩa quan trọng để giúp Chart được rõ nghĩa, đọc dễ hiểu hơn.

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

https://www.mediafire.com/file/gypdzk2ajb61fni/LearnPyQtBarGraphPart5.rar/file

Bài học sau Tui sẽ hướng cách vẽ Multiple Bar Item trên cùng một Chart. Đây là tính năng rất quan trọng và phổ biến để trực quan hóa dữ liệu trong việc so sánh kết quả.

Các bạn chú ý theo dõi

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

Bài học này chúng ta tiếp tục làm việc với BarGraph, Tui hướng dẫn các bạn cách bổ sung thiết kế giao diện trong Qt Designer để cung cấp các chức năng tương tác trong quá trình trực quan hóa dữ liệu với BarGraph:

  • Chức năng ẩn và hiện Background Grid
  • Chức năng ẩn và hiện Legend
  • Chức năng đổi màu nền của Chart dùng QColorDialog
  • Chức năng đổi màu nền của Bar dùng QColorDialog

Các chức năng này đều được thiết kế giao diện và cung cấp sự kiện tương tác người dùng. Dựa vào bài này mà các bạn có thể tùy chỉnh tương tác biểu đồ trong các trường hợp thực tế khác nhau.

Ngoài ra các bạn cũng được ôn tập lại các kiến thức liên quan tới QCheckBox, xử lý các sự kiện trên QCheckBox. Cũng như xử lý sự kiện trên QPushButton.

Hướng dẫn lập trình android trần duy thanh năm 2024

Bạn quan sát giao diện ở trên có 2 QCheckBox, 3 QPushButton được bổ sung vào giao diện để cho người dùng tương tác. Bài này mở rộng từ bài trước

Dưới đây là chi tiết từng bước thực hiện dự án:

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

Hướng dẫn lập trình android trần duy thanh năm 2024

  • “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.

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

Hướng dẫn lập trình android trần duy thanh năm 2024

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.

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

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("")

2

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

Lớp MainWindowEx kế thừa lớp Ui_MainWindow (lớp được Generate Python code ở bước trước)

Tui định nghĩa 1 constructor __init__() gọi lại constructor ở lớp cha, tạm thời trong bài tập này chưa xử lý gì khác.

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

0

Hàm setupUi() được override để nạp giao diện, lưu lại biến MainWindow để sử dụng cho quá trình xử lý trong tương lai. Đồng thời nó cũng gọi hàm setupBarGraph() để hiển thị Chart, Hàm này ta viết như sau:

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

1

  • Tất cả các kỹ thuật liên quan tới PlotWidget và cách xử lý BarGraphItem Tui đã trình bày chi tiết và cụ thể ở các bài trước rồi, nên bài này Tui không nhắc lại. Mà chúng ta chỉ quan tâm xử lý Signal liên quan tới các QCheckBox và QPushButton để cung cấp chức năng tương tác Chart cho người dùng.
  • Ta xem Signal statechanged của QCheckBox background grid:

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

2

Slot(hàm) chi tiết xử lý khi người dùng Checked vào QCheckBox background grid được khai báo trong hàm processChangeGrid():

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

3

Khi chạy chức năng này thì ta thấy như sau: Nếu người sử dụng Unchecked QCheckBox background Grid này thì các lưới sẽ bị biến mất, còn Checked thì các lưới sẽ được hiển thị trở lại:

Hướng dẫn lập trình android trần duy thanh năm 2024

  • Tiếp theo Ta xem Signal statechanged của QCheckBox Legend:

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

4

Slot(hàm) chi tiết xử lý khi người dùng Checked vào QCheckBox legend được khai báo trong hàm processChangeLegend():

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

4

Khi chạy chức năng này thì ta thấy như sau: Nếu người sử dụng Unchecked QCheckBox Legend này thì Legend bị biến mất, còn Checked thì Legend sẽ được hiển thị trở lại:

Hướng dẫn lập trình android trần duy thanh năm 2024

  • Tiếp theo ta qua Signal clicked cho QPushButton thay đổi màu nền của Chart:

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

6

Slot(hàm) chi tiết xử lý khi người dùng nhấn vào QPushButton Chart Background được khai báo trong hàm processChangeChartBackground():

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

5

Ta thấy, hàm trên Tui dùng QColorDialog() để hiển thị màn hình chọn màu, khi người dùng lựa chọn màu và nhần OK thì màu sẽ được lấy từ hàm currentColor(), sau đó ta truy suất tên của màu này để đổi màu cho Chart thông qua hàm: setBackground(color.name()).

Hướng dẫn lập trình android trần duy thanh năm 2024

Ví dụ người dùng chọn màu hồng rồi nhấn OK. Lúc này nền màu của Chart sẽ đổi thành màu hồng này:

Hướng dẫn lập trình android trần duy thanh năm 2024

  • Tiếp theo ta qua Signal clicked cho QPushButton thay đổi màu của Bar:

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

8

Slot(hàm) chi tiết xử lý khi người dùng nhấn vào QPushButton Chart Background được khai báo trong hàm processChangeBarColor():

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

9

Mã lệnh ở trên Tui cũng sử dụng QColorDialog để cho người dùng lựa chọn màu, sau khi lựa chọn màu thì tên màu sẽ được lưu với mảng opts[“brush”] của BarGraphItem.

sau đó nó sẽ được cập nhật màu thông qua hàm:

_updateColors(self.bargraphItem.opts) của BarGraphItem

Hướng dẫn lập trình android trần duy thanh năm 2024

Ví dụ người dùng chọn màu vàng rồi nhấn OK:

Hướng dẫn lập trình android trần duy thanh năm 2024

Dưới đây là mã lệnh đầy đủ của “MainWindowEx.py“:

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)

0

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

from PyQt6.QtCore import QObject, pyqtSignal
from ItemDownload import ItemDownload
class WorkerSignals(QObject):
    runningSignal = pyqtSignal(ItemDownload)
    finishSignal = pyqtSignal(int)

2

Chạy “MyApp.py” ta có kết quả như mong muốn, các biểu đồ cột, các nhãn của các cột, tiêu đề chart, các QCheckBox và QPushButton được hiển thị để người dùng tương tác:

Hướng dẫn lập trình android trần duy thanh năm 2024

Như vậy tới đây Tui đã trình bày xong cách lập trình xử lý trực quan hóa dữ liệu với BarGraph. Ôn tập lại cách khai báo các mảng dữ liệu tương ứng với các trục, Ôn tập cách gọi các hàm liên quan tới BarGraphItem để hiển thị Chart. Cũng như ôn tập lại được toàn bộ kiến thức liên quan tới PlotWidget. Đặc biệt các bạn biết cách tương tác Chart thông qua các QCheckBox và QPushButton để tương tác lưới của chart, tương tác legend, đổi màu chart và màu bar.

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

https://www.mediafire.com/file/mrb3yyoajnirgfr/LearnPyQtBarGraphPart4.rar/file

Bài học sau Tui sẽ hướng dẫn các bạn cách lập trình để hiển thị ticks và cách tự động gán notation cho từng Bar, các bạn chú ý theo dõi

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

Trong chuỗi các bài học về BarGraph để trực quan hóa dữ liệu Tui sẽ trình bày 6 bài về các kỹ thuật liên quan tới BarGraph, để tùy từng tình huống hay nhu cầu sử dụng khác nhau mà các bạn có thể áp dụng.

Đối tượng PlotWidget cũng như các kỹ thuật liên quan chúng ta đã học rất chi tiết và đầy đủ ở phần 2, ở phần này Tui không nói lại PlotWidget mà Tui chỉ sử dụng lại PlotWidget để vẽ các BarGraph biểu độ dạng cột, một trong những loại trực quan quá phổ biến.

Mô tả tập dữ liệu cho bài này:

Công ty Lucy có dữ liệu doanh thu trung bình theo quý của năm 2023 như sau:

QuýTrung bình doanh thu1100220032504190

Hãy trực quan hóa dữ liệu bằng biểu đồ cột. Hình dưới đây minh họa kỹ thuật đầu tiên trong chuỗi 6 bài về BarGraphItem này:

Hướng dẫn lập trình android trần duy thanh năm 2024

Chúng ta lưu ý là toàn bộ các bài liên quan tới trực quan hóa dữ liệu, Chúng ta sẽ sử dụng PlotWidget, các hàm phổ biến liên quan đã được học ở các bài trước đều được tái sử dụng. Còn loại Chart hiển thị như thế nào thì tùy từng trường hợp mà ta sẽ gọi các hàm hiển thị khác nhau.

Ta từng bước thực hiện bài này như sau:

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

Hướng dẫn lập trình android trần duy thanh năm 2024

  • “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.

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

Hướng dẫn lập trình android trần duy thanh năm 2024

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

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)

2

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

Lớp MainWindowEx kế thừa lớp Ui_MainWindow (lớp được Generate Python code ở bước trước)

Tui định nghĩa 1 constructor __init__() gọi lại constructor ở lớp cha, tạm thời trong bài tập này chưa xử lý gì khác.

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)

3

Hàm setupUi() được override để nạp giao diện, lưu lại biến MainWindow để sử dụng cho quá trình xử lý trong tương lai. Đồng thời nó cũng gọi hàm setupBarGraph() để hiển thị Chart, Hàm này ta viết như sau:

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)

4

Ta thấy rằng tất cả các phương thức thường sử dụng của PlotWidget trình bày ở bài học trước đều được dùng lại ở đây, nên Tui không có trình bày lại các kiến thức cũ.

Mà các bạn hãy để ý các dòng lệnh mới liên quan tới BarGraphItem thôi:

  • Khai bao và khởi tạo giá trị cho biến width, biến này là độ rộng của cột Bar :
  • Khai báo mảng lưu 4 quý vào biến quarter:
  • Khai báo mảng doanh thu tương ứng với mảng quarter:

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)

5

  • Cuối cùng là hàm vẽ biểu đồ BarGraph bằng cách khai báo đối tượng BarGraphItem:

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)

6

Ý nghĩa của BarGraphItem là vẽ các biểu đồ cột, dưới đây là một số thuộc tính/parameter thường dùng của BarGraphItem:

Thuộc tínhÝ nghĩa, Chức năng xlà đối số lưu mảng các vị trí mà mỗi Cột biểu đồ sẽ được vẽheightlà đối số lưu mảng các giá trị (độ cao) của mỗi cột biểu đồ ở vị trí tương ứng trong mảng xwidthLà độ rộng của cột biểu đồ, mà ở trên ta khai báo mặc định là 0.3, ta có thể lựa chọn giá trị tùy thíchbrushlà màu của các cột trong biểu đồnameLà tên của biểu đồ, nó có ý nghĩa cho xử lý tương tác biểu đồ, legend trên biểu đồ….optsMảng lưu đầy đủ các thông số cấu hình của Chart, dựa vào đây ta có thể hiệu chỉnh trực tiếp biểu đồ.

Dưới đây là mã lệnh đầy đủ của “MainWindowEx.py“:

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)

7

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

from PyQt6.QtCore import QObject, pyqtSignal
from ItemDownload import ItemDownload
class WorkerSignals(QObject):
    runningSignal = pyqtSignal(ItemDownload)
    finishSignal = pyqtSignal(int)

2

Chạy “MyApp.py” ta có kết quả như mong muốn, các biểu đồ cột, các nhãn của các cột, tiêu đề chart… được hiển thị:

Hướng dẫn lập trình android trần duy thanh năm 2024

Như vậy tới đây Tui đã trình bày xong cách lập trình xử lý trực quan hóa dữ liệu với BarGraph. Các bạn đã biết cách khai báo các mảng tương ứng với các trục, biết cách gọi các hàm liên quan tới BarGraphItem để hiển thị Chart. Cũng như ôn tập lại được toàn bộ kiến thức liên quan tới PlotWidget, một trong các đối tượng quan trọng và thường sử dụng trong trực quan hóa dữ liệu.

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

https://www.mediafire.com/file/tpic3u6bkckjilt/LearnPyQtBarGraphPart3.rar/file

Bài học sau Tui sẽ nâng cấp bài học này bằng cách bổ sung thêm các Widget cho người dùng tương tác, chẳng hạn như:

  • Cung cấp chức năng ẩn hiện Background Grid
  • Cung cấp chức năng ẩn hiện Legend
  • Cung cấp chức năng đổi màu nền của Chart
  • Cung cấp chức năng đổi màu nền của Bar

Các bạn chú ý theo dõi

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

Bài học này Tui trình bày chi tiết về đối tượng PlotWidget được đề cập trong PyQtGraph Part 1 mà ta đã dùng để vẽ một Chart đơn giản.

Hướng dẫn lập trình android trần duy thanh năm 2024

Việc nắm được ý nghĩa và các kỹ thuật sử dụng các phương thức của đối tượng PlotWidget sẽ giúp chúng ta dễ dàng hiệu chỉnh Chart theo nhu cầu:

Tui tóm tắt sơ lược ý nghĩa chức năng của một số phương thức của PlotWidget

Phương thứcÝ nghĩa chức năngsetTitle(title)Thiết lập tiêu đề cho Chart Ví dụ: self.graphWidget.setTitle(“My Chart Title”)setTitle(title, color, size)Thiết lập tiêu đề cho Chart cùng với định dạng màu chữ và cỡ chữ. Ví dụ: self.graphWidget.setTitle(“My Chart Title”, color=”b”, size=”30pt”)setLabel(position,text)Thiết lập tiêu đề cho các trục. position có 4 giá trị: ‘left,’right’,’top’,’bottom’ Ví dụ: styles = {‘color’:’r’, ‘font-size’:’30pt’} self.graphWidget.setLabel(‘left’, ‘Temperature (°C)’, **styles) self.graphWidget.setLabel(‘bottom’, ‘Hour (H)’, **styles)setBackground(background)Phương thức này dùng để thiết lập màu nền cho Chart. Ví dụ: import pyqtgraph as pg self.graphWidget=pg.PlotWidget() self.graphWidget.setBackground(“y”)showGrid(x=True, y=True)Hiển thị lưới cho Chartplot(X value, Y value, name=”Plot 1″, pen=pen, symbol=”+”, symbolSize=30, symbolBrush=(“b”), )Hàm để vẽ Chart có sử dụng Pen để định dạng đường vẽ như màu đường kẻ, kiểu đường kẻ, độ dày đường kẻ, biểu tượng. Ví dụ: pen = pg.mkPen(color=(255, 0, 0)) self.graphWidget.plot(hour, temperature, pen=pen)addLegend()Hàm hiển thị Legend cho ChartsetXRange(5, 20, padding=0)Thiết lập giới hạn cho trục XsetYRange(30, 40, padding=0)Thiết lập giới hạn cho trục Yclear()Xóa các plot trên Chartdata_line= graphWidget.plot(x, y)

data_line.setData(new x, new y)

Cập nhật dữ liệu cho Plot

Tui trình bày chi tiết các chức năng dưới này, vừa kết hợp lý thuyết và thực hành, các bạn nhớ thực hiện theo.

Tạo dự án “LearnPyQtGraphPart2” thiết kế giao diện và dữ liệu mẫu giống như bài trước. Bạn có thể copy y chang toàn bộ các file .py, .ui của bài trước vào “LearnPyQtGraphPart2” để sử dụng luôn

Hướng dẫn lập trình android trần duy thanh năm 2024

  • “MainWindow.ui” là giao diện được thiết kế bằng Qt Designer
  • “MainWindow.py” là generate python code của “MainWindow.ui”
  • “MainWindowEx.py” là file mã lệnh kế thừa từ “MainWindow.py” để xử lý nạp giao diện, gán Chart và các sự kiện, lớp này sẽ không lệ thuộc vào sự thay đổi của giao diện cũng như generate code
  • “MyApp.py” là file mã lệnh thực thi chương trình

Giao diện “MainWindow.ui” và dữ liệu mẫu trong “MainWidowEx.py” là y chang như bài trước nên Tui không chụp hình lại, các bạn tự chuyển qua. Còn dưới đây Tui sẽ lần lượt hướng dẫn từng chức năng cụ thể, các bạn bổ sung vào “MainWindowEx.py“:

1. Thiết lập tiêu đề cho Chart

Ta thiết lập tiêu đề cho Chart bằng hàm setTitle(title)

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)

9

Ngoài ra ta có thể định dạng style cho title như màu tiêu đề, cỡ chữ tiêu đề, in đậm, in nghiêng.

StyleCách dùngcolor‘CCFF00’ , ‘b’size’10pt’boldTrue/FalseitalicTrue/False

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()

0

Dưới đây là chi tiết mã lệnh của MainWindowEx.py cho phần tiêu đề:

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()

1

Chạy “MyApp.py” ta có kết quả:

Hướng dẫn lập trình android trần duy thanh năm 2024

Ngoài ra ta cũng có thể thiết lập title bằng cấu trúc HTML như dưới đây:

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()

2

2. Thiết lập tiêu đề cho các Trục

Chúng ta dùng hàm setLabel(position,text,style) để thiết lập tiêu đề cho các trục. position có ‘left,’right’,’top’,’bottom’. Ví dụ:

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()

3

Coding đầy đủ của MainWindowEx.py cho phần thiết lập tiêu đề cho các Trục:

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()

4

Chạy “MyApp.py” ta có kết quả như dưới đây:

Hướng dẫn lập trình android trần duy thanh năm 2024

Ở Chart bên trên ta thấy, cả 4 trục đều có tiêu đề.

3. Thiết lập màu nền cho Chart

Bây giờ ta làm quen với phương thức setBackground(). Phương thức đổi thiết lập màu nền có thể nhận các dạng màu sau:

  • Dùng Letter code ColorLetter Codebluebgreengredrcyancmagentamyellowyblackkwhitew

Ví dụ lệnh dưới đây thiết lập nền trắng cho Chart:

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()

5

Hướng dẫn lập trình android trần duy thanh năm 2024

  • Dùng hex color

Danh sách Hex Color bạn có thể lấy nhiều nơi, có thể lấy ở đây:

https://www.color-hex.com/

Hướng dẫn lập trình android trần duy thanh năm 2024

Ví dụ ta thiết lập màu nền Hex Color

ff0000 màu đỏ:

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()

6

Hướng dẫn lập trình android trần duy thanh năm 2024

  • Sử dụng RGB (Red – Green-Blue) và RGBA (Red – Green – Blue – Alpha Opacity)

Khi dùng RGB thì ta dùng bộ 3:

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()

7

Hướng dẫn lập trình android trần duy thanh năm 2024

Khi dùng RGBA thì ta dùng bộ 4:

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()

8

Hướng dẫn lập trình android trần duy thanh năm 2024

  • Ngoài ra ta có thể dùng đối tượng QColor để thiết lập màu

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()

9

Hướng dẫn lập trình android trần duy thanh năm 2024

Mã lệnh đầy đủ của MainWindowEx.py để đổi màu nền của Chart qua màu trắng:

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

00

Chạy “MyApp.py” ta có kết quả:

Hướng dẫn lập trình android trần duy thanh năm 2024

4. Thiết lập Background Grid

Để hiển thị lưới cho Chart ta dùng hàm .showGrid()

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

01

Ta có thể tùy chỉnh trục nào sẽ xuất hiện lưới, mã lệnh ở trên là hiển thị cả trục tung và trục hoành, dưới đây là mã lệnh đầy đủ của MainWindowEx.py:

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

02

Chạy “MyApp.py” ta có kết quả:

Hướng dẫn lập trình android trần duy thanh năm 2024

5. Thiết lập màu, độ rộng và kiểu dáng của đường kẻ

  • Bây giờ chúng ta làm quen với đối tượng QPen hiệu chỉnh màu đường kẻ, kiểu đường kẻ, độ dày đường kẻ:

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

03

Kiểu của đường kẻ ta dùng như dưới đây:

Enum StyleÝ nghĩa chức năngQt.PenStyle.SolidLineVẽ đường liên tụcQt.PenStyle.DashLineVẽ đường các gạch ngangQt.PenStyle.DotLineVẽ đường các chấmQt.PenStyle.DashDotLineVẽ đường: Gạch ngang – chấm – gạch ngangQt.PenStyle.DashDotDotLineVẽ đường: Gạch ngang – chấm- chấm – gạch ngang

Coding đầy đủ của MainWindowEx.py:

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

04

Chạy MyApp.py ta có kết quả:

Hướng dẫn lập trình android trần duy thanh năm 2024

6. Thiết lập Line Markers

Để thiết lập Markers cho các Line plot, ta dùng các thuộc tính sau cho hàm plot() của đối tương PlotWidget:

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

05

  • symbol: Thiết lập biểu tượng của Marker SymbolÝ nghĩaoCircularsSquaretTriangulardDiamond+Cross
  • symbolSize: Thiết lập độ lớn của Marker
  • symbolBrush: Thiết lập màu của Marker
  • symbolPen: Thiết lập màu đường viên của Marker

Dưới đây là coding minh họa phần Marker trong MainWindowEx.py:

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

06

Chạy MyApp.py ta có kết quả:

Hướng dẫn lập trình android trần duy thanh năm 2024

Ta thấy Marker có màu xanh (symbolBrush) và màu viền là xám (symbolPen), biểu tượng + (symbol), kích thước của marker (symbolSize). Ta thử thay thế các symbol khác nhau rồi quan sát so sánh kết quả.

7. Thiết lập Legends

Để thiết lập Legends cho Chart, trước tiên các Line/plot cần được đặt tên khi gọi phương thức plot() của PlotWidget.

Sau đó ta gọi phương thức addLegend() của PlotWidget.

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

07

Dưới đây là mã lệnh đầy đủ của MainWindowEx.py để hiển thị Legend:

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

08

Chạy “MyApp.py” ta có kết quả:

Hướng dẫn lập trình android trần duy thanh năm 2024

Quan sát Chart ta thấy Legen “Sensor X” được hiển thị mặc định ở góc trái bên trên của Chart, ta có thể dùng chuột để di chuyển.

8. Thiết lập giới hạn các trục

Đôi khi trong quá trình trực quan hóa dữ liệu, chúng ta cần thiết phải giới hạn hiển thị dữ liệu ở các trục.

Ta dùng hàm setXRange(min, max,padding) và setYRange(min,max,padding) để giới hạn

Ví dụ:

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

09

Dưới đây là mã lệnh đầy đủ của MainWindowEx.py:

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

10

Chạy MyApp.py ta có kết quả:

Hướng dẫn lập trình android trần duy thanh năm 2024

9. Thiết lập multiple plot trong một Chart

Thông thường khi vẽ chart ta hay kết hợp nhiều plot để hiển thị, so sánh…. PyQtGraph cũng hỗ trợ đặc tính này:

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

11

Mã lệnh ở trên các bạn quan sát Tui bổ sung thêm các biến:

  • temperature2: lưu trữ mảng nhiệt độ mới cho Sensor Y
  • pen2: thiết lập đường kẻ thứ 2
  • symbolPen2: thiết lập biểu tượng cho đường kẻ thứ 2

Và Ta gọi 2 lần hàm plot() của đối tượng PlotWidget (biến graphWidget)

Dưới đây là mã lệnh đẩy đủ của MainWindowEx.py:

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

12

Chạy “MyApp.py” ta có kết quả:

Hướng dẫn lập trình android trần duy thanh năm 2024

Ta có thể ẩn/hiển thị các Plot bằng cách nhấn vào Legend, ví dụ như muốn ẩn Sensor Y:

Hướng dẫn lập trình android trần duy thanh năm 2024

Ta có thể áp dụng kỹ thuật của Multiple Line để tự động nạp nhiều Plot cho 1 Chart. Ví dụ như hãy vẽ biểu đồ doanh thu từng tháng của 10 chi nhánh trong năm 2023.

10. Xóa và cập nhật Plot

Để xóa Plot trên Chart ta gọi lệnh:

Trong quá trình hiển thị Chart, đặc biệt là liên quan tới Realtime data thì rõ ràng ta muốn Chart được cập nhật tự động.

Bước 1: Khai báo đối tượng lưu trữ lại Plot, ví dụ ta khai báo plot 2 cho Sensor Y

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

13

Bước 2: Thay đổi giá trị trong mảng temperature2, ví dụ:

Ở bước 2 này tức là nếu trong quá trình vận hành trực quan hóa, mà bất cứ khi nào đó mảng dữ liệu bị thay đổi. Ở đây là Tui minh họa 1 trường hợp phần tử thứ 3 bị đổi dữ liệu

Bước 3: Gọi hàm setData() của plot2 để cập nhật Plot

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

14

Code đầy đủ của MainWindowEx.py cho phần cập nhật Plot:

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

15

Chạy MyApp.py ta thấy temperature2[3]=100 được update.

Hướng dẫn lập trình android trần duy thanh năm 2024

Dựa vào tính năng nay ta có thể viết code tự động update dữ liệu theo thời gian.

Như vậy tới Tui đã hướng dẫn đầy đủ và chi tiết các chức năng quan trọng và thường dùng của PlotWidget trong trực quan hóa dữ liệu. Các bạn chú ý làm lại nhiều lần và hiểu thật rõ, áp dụng thật tốt từng kỹ thuật để giúp cho trực quan hóa được tốt nhất.

Các bạn tải mã lệnh đầy đủ của dự án ở đây:

https://www.mediafire.com/file/7s7lkzsmsoyufy0/LearnPyQtGraphPart2.rar/file

Bài học sau Tui sẽ minh họa cách sử dụng PyQtGraph và OpenGL để hiển thị 3D Graph, các bạn chú ý theo dõi