Bài 29: Kiến trúc Model View-PyQt6–Part 4

Bài này Tui nâng cấp bài 28, tiếp tục ứng dụng kiến trúc Model View và bổ sung các chức năng cho QTableView:

  • Nạp ma trận “data” lên QTableView cùng với các cột được lưu trong mảng “columns“? Đồng thời nếu giá <100 thì tô chữ đỏ, nếu tên rỗng thì tô nền vàng:
  • Thêm, Sửa, Xóa dữ liệu ngay trên giao diện QTableView thông qua việc cập nhật mã lệnh mới cho TableModel.py.
  • Xử lý signal item selection change
  • Cách khai báo và sử dụng Enum trong Python
  • Xử lý Context Menu cho Widget để Chèn Đầu, Chèn Cuối, Chèn Trước, Chèn Sau dữ liệu vào trong QTableView, cũng như thao tác xóa dữ liệu ra khỏi QTableView.
  • Dùng partial function để triệu gọi các signals/slots có parameter
  • Import và Export dữ liệu trong model ra JSON ARRAY

Bài này nhiều chức năng, khá khó và tuy nhiên nó có tính ứng dụng cao trong thực tế. Các bạn có gắng thực hành theo từng bước để sau náy có thể áp dụng nó và các tình huống khác nhau.

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

  • Thư mục “images” chứa các hình ảnh và biểu tượng cho các QAction và Window Icon
  • Mã lệnh “InsertBehavior.py” là enum để định nghĩa các kiểu insert trước, sau, đầu, cuối
  • Mã lệnh “TableModel.py” là lớp Model View, lớp trung tâm của dự án
  • MainWindow.ui” là giao diện thiết kế phần mềm
  • Mã lệnh “MainWindow.py” là Generate Python code từ giao diện
  • MainWindowEx.py” là mã lệnh kế thừa từ lớp Generate Python code để xử lý nạp giao diện cũng như xử lý sự kiện người dùng, gán Model.
  • MyApp.py” là mã lệnh thực thi chương trình

Bước 2: Tạo mã lệnh enum “InsertBehavior.py”

from enum import Enum

class InsertBehavior(Enum):
        INSERT_FIRST = 0
        INSERT_LAST = 1
        INSERT_ABOVE = 2
        INSERT_BELOW=3

Trong InsertBehavior Tui định nghĩa 4 enum, và nó sẽ được dùng cho xử lý Context Menu trên QTableView.

Context Menu trên QTableView (nhấn chuột phải vào QTableView), chương trình sẽ hiển thị lên Context Menu ở trên. Có chức năng tương ứng với enum:

  • Insert First (INSERT_FIRST = 0), chương trình sẽ thêm một dòng trống ở đầu QTableView
  • Insert Last (INSERT_LAST = 1), chương trình sẽ thêm một dòng trống ở cuối QTableView
  • Insert Above (INSERT_ABOVE = 2), chương trình sẽ thêm một dòng trống ở trước dòng đang lựa chọn
  • Insert Below (INSERT_BELOW=3), chương trình sẽ thêm một dòng trống ở sau dòng đang lựa chọn

Ngoài ra Tui cũng bổ sung thêm 1 Context Menu khi người dùng nhấn chuột phải vào QTableView nhưng không nhấn vào dòng nào:

Trong trường hợp này thì chỉ có duy nhất một QAction là “Insert New Record” và nó dùng chung enum (INSERT_FIRST = 0)

Bước 3: Viết mã lệnh “TableModel.py“, đây là lớp mã lệnh trung tâm của dự án liên quan tới kiến trúc Model View.

Lớp TableModel kế thừa từ QAbstractTableModel, Tui định nghĩa constructor có 2 đối số nhận vào đó là ma trận dữ liệu (data) và mảng tên cột (columns)

from PyQt6 import QtGui
from PyQt6.QtCore import Qt, QAbstractTableModel, QModelIndex

class TableModel(QAbstractTableModel):
    def __init__(self, data,columns):
        super().__init__()
        self.data = data
        self.columns=columns

Tiếp theo ta override hàm data() để hiển thị dữ liệu và định dạng dữ liệu như mong muốn:

def data(self, index, role):
    value = self.data[index.row()][index.column()]
    if role == Qt.ItemDataRole.DisplayRole:
        return value
    if role==Qt.ItemDataRole.EditRole:
        return value
    if role == Qt.ItemDataRole.BackgroundRole:
        if index.column() == 1 and value == "":
            return QtGui.QColor(Qt.GlobalColor.yellow)
    if role == Qt.ItemDataRole.ForegroundRole:
        if index.column() == 2 and  value!="" and float(value) < 100:
            return QtGui.QColor(Qt.GlobalColor.red)

Thông qua hàm data() này, chương trình sẽ tô chữ đỏ với dòng dữ liệu có giá <100, tô nền vàng với dòng dữ liệu có tên rỗng.

Tiếp theo ta cần override hàm rowCount() và columnCount() để Model nhận dạng được số dòng và số cột trong giao diện:

def rowCount(self, index):
    return len(self.data)

def columnCount(self, index):
    return len(self.columns)

Để tạo tiêu đề cột và tiêu đề dòng, ta cần override phương thức headerData():

def headerData(self, section, orientation, role):
    if role == Qt.ItemDataRole.DisplayRole:
        if orientation == Qt.Orientation.Horizontal:
            return str(self.columns[section])
        if orientation==Qt.Orientation.Vertical:
            return  str(section+1)
  • Qt.Orientation.Horizontal là enum dùng để định nghĩa tiêu đề cột
  • Qt.Orientation.Vertical là enum dùng để định nghĩa tiêu đề dòng

Tiếp theo ta override 2 hàm flags() và setData():

def flags(self, index):
    if not index.isValid():
        return Qt.ItemFlag.ItemIsEnabled

    return super().flags(index) | Qt.ItemFlag.ItemIsEditable  # add editable flag.

def setData(self, index, value, role):
    if role == Qt.ItemDataRole.EditRole:
        # Set the value into the frame.
        self.data[index.row()][index.column()] = value
        return True
    return False
  • Hàm flags() dùng để customize tính năng cho người dùng chỉnh sửa dữ liệu trực tiếp trên từng ô dữ liệu
  • Hàm setData() dùng để bổ sung cho tính năng flags() đó là khi nhấn vào ô nào thì hiển thị chức năng cập nhật (Ô textbox) đồng thời hiển thị lại dữ liệu hiện hành). Cũng như khi người dùng đổi dữ liệu mới thì dữ liệu này sẽ được cập nhật vào ma trận: self.data[index.row()][index.column()] = value

Tiếp theo ta override hàm insertRows() để hỗ trợ việc chèn mới dòng dữ liệu cho Model:

def insertRows(self, row, rows=1, index=QModelIndex()):
    print("Inserting at row: %s" % row)
    self.beginInsertRows(QModelIndex(), row, row + rows - 1)
    self.data.insert(row,["","",""])
    self.endInsertRows()
    return True

Mã lệnh bên trong hàm insertRows() chương trình sẽ tạo thêm 1 dòng dữ liệu trên QTableView đồng thời cũng chèn dữ liệu rỗng cho ma trận trong biến data.

Hàm insertRows() sẽ dùng cho các chức năng: Chèn đầu, chèn cuối, chèn trước và chèn sau.

Để thực hiện chức năng xóa dữ liệu ta override hàm removeRows():

def removeRows(self, row, rows=1, index=QModelIndex()):
    print("Removing at row: %s" % row)
    self.beginRemoveRows(QModelIndex(), row, row + rows - 1)
    #self.data = self.data[:row] + self.data[row + rows:]
    del self.data[row]
    self.endRemoveRows()
    return True

Mã lệnh trong removeRows() sẽ xóa dòng ở vị trí row, đồng thời ta cũng gọi lệnh để xóa dữ liệu trong ma trận ở dòng row: del self.data[row]

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

from PyQt6 import QtGui
from PyQt6.QtCore import Qt, QAbstractTableModel, QModelIndex

class TableModel(QAbstractTableModel):
    def __init__(self, data,columns):
        super().__init__()
        self.data = data
        self.columns=columns

    def data(self, index, role):
        value = self.data[index.row()][index.column()]
        if role == Qt.ItemDataRole.DisplayRole:
            return value
        if role==Qt.ItemDataRole.EditRole:
            return value
        if role == Qt.ItemDataRole.BackgroundRole:
            if index.column() == 1 and value == "":
                return QtGui.QColor(Qt.GlobalColor.yellow)
        if role == Qt.ItemDataRole.ForegroundRole:
            if index.column() == 2 and  value!="" and float(value) < 100:
                return QtGui.QColor(Qt.GlobalColor.red)

    def rowCount(self, index):
        return len(self.data)

    def columnCount(self, index):
        return len(self.columns)

    def headerData(self, section, orientation, role):
        if role == Qt.ItemDataRole.DisplayRole:
            if orientation == Qt.Orientation.Horizontal:
                return str(self.columns[section])
            if orientation==Qt.Orientation.Vertical:
                return  str(section+1)

    def flags(self, index):
        if not index.isValid():
            return Qt.ItemFlag.ItemIsEnabled

        return super().flags(index) | Qt.ItemFlag.ItemIsEditable  # add editable flag.

    def setData(self, index, value, role):
        if role == Qt.ItemDataRole.EditRole:
            # Set the value into the frame.
            self.data[index.row()][index.column()] = value
            return True
        return False

    def insertRows(self, row, rows=1, index=QModelIndex()):
        print("Inserting at row: %s" % row)
        self.beginInsertRows(QModelIndex(), row, row + rows - 1)
        self.data.insert(row,["","",""])
        self.endInsertRows()
        return True

    def removeRows(self, row, rows=1, index=QModelIndex()):
        print("Removing at row: %s" % row)
        self.beginRemoveRows(QModelIndex(), row, row + rows - 1)
        #self.data = self.data[:row] + self.data[row + rows:]
        del self.data[row]
        self.endRemoveRows()
        return True

Bước 4: Thiết kế giao diện “MainWindow.ui” bằng Qt Designer

Các bạn kéo thả các widget QLineEdit, và nhập menu cũng như tên các widget như trên.

Bước 5: Generate Python Code “MainWindow.py” cho “MainWindow.ui”

# Form implementation generated from reading ui file 'MainWindow.ui'
#
# Created by: PyQt6 UI code generator 6.5.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(330, 329)
        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.tableViewProduct = QtWidgets.QTableView(parent=self.centralwidget)
        self.tableViewProduct.setGeometry(QtCore.QRect(20, 10, 291, 181))
        self.tableViewProduct.setObjectName("tableViewProduct")
        self.label = QtWidgets.QLabel(parent=self.centralwidget)
        self.label.setGeometry(QtCore.QRect(20, 200, 71, 16))
        self.label.setObjectName("label")
        self.lineEditProductId = QtWidgets.QLineEdit(parent=self.centralwidget)
        self.lineEditProductId.setGeometry(QtCore.QRect(100, 200, 191, 20))
        self.lineEditProductId.setObjectName("lineEditProductId")
        self.lineEditProductName = QtWidgets.QLineEdit(parent=self.centralwidget)
        self.lineEditProductName.setGeometry(QtCore.QRect(100, 230, 191, 20))
        self.lineEditProductName.setObjectName("lineEditProductName")
        self.label_2 = QtWidgets.QLabel(parent=self.centralwidget)
        self.label_2.setGeometry(QtCore.QRect(20, 230, 71, 16))
        self.label_2.setObjectName("label_2")
        self.label_3 = QtWidgets.QLabel(parent=self.centralwidget)
        self.label_3.setGeometry(QtCore.QRect(20, 260, 71, 16))
        self.label_3.setObjectName("label_3")
        self.lineEditUnitPrice = QtWidgets.QLineEdit(parent=self.centralwidget)
        self.lineEditUnitPrice.setGeometry(QtCore.QRect(100, 260, 191, 20))
        self.lineEditUnitPrice.setObjectName("lineEditUnitPrice")
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 330, 22))
        self.menubar.setObjectName("menubar")
        self.menuSystem = QtWidgets.QMenu(parent=self.menubar)
        self.menuSystem.setObjectName("menuSystem")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(parent=MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)
        self.actionExport_to_JSon = QtGui.QAction(parent=MainWindow)
        icon1 = QtGui.QIcon()
        icon1.addPixmap(QtGui.QPixmap("images/ic_export.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.actionExport_to_JSon.setIcon(icon1)
        self.actionExport_to_JSon.setObjectName("actionExport_to_JSon")
        self.actionImport_from_JSon = QtGui.QAction(parent=MainWindow)
        icon2 = QtGui.QIcon()
        icon2.addPixmap(QtGui.QPixmap("images/ic_import.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.actionImport_from_JSon.setIcon(icon2)
        self.actionImport_from_JSon.setObjectName("actionImport_from_JSon")
        self.actionExit = QtGui.QAction(parent=MainWindow)
        icon3 = QtGui.QIcon()
        icon3.addPixmap(QtGui.QPixmap("images/ic_exit.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.actionExit.setIcon(icon3)
        self.actionExit.setObjectName("actionExit")
        self.menuSystem.addAction(self.actionExport_to_JSon)
        self.menuSystem.addSeparator()
        self.menuSystem.addAction(self.actionImport_from_JSon)
        self.menuSystem.addSeparator()
        self.menuSystem.addAction(self.actionExit)
        self.menubar.addAction(self.menuSystem.menuAction())

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

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "Trần Duy Thanh - Model View"))
        self.label.setText(_translate("MainWindow", "Product Id:"))
        self.label_2.setText(_translate("MainWindow", "Product Name:"))
        self.label_3.setText(_translate("MainWindow", "Unit Price:"))
        self.menuSystem.setTitle(_translate("MainWindow", "System"))
        self.actionExport_to_JSon.setText(_translate("MainWindow", "Export to JSon"))
        self.actionImport_from_JSon.setText(_translate("MainWindow", "Import from JSon"))
        self.actionExit.setText(_translate("MainWindow", "Exit"))

Bước 6: Viết mã lệnh “MainWindowEx.py” để xử lý sự kiện, nạp giao diện, gán model. Lớp này kế thừa từ Generate Python code

Ta định nghĩa Constructor cho MainWindowEx.py để khởi tạo ma trận dữ liệu mẫu và các tiêu đề cột:

import json
from functools import partial

from PyQt6.QtCore import QModelIndex, Qt
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import QMenu, QFileDialog
from InsertBehavior import InsertBehavior
from MainWindow import Ui_MainWindow
from TableModel import TableModel

class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        self.data = [["p1", "Coca", 100],
                ["p2", "Pepsi", 50],
                ["p3", "Sting", 300],
                ["p4", "Aqua", 70],
                ["p5", "Redbull", 200],
                ["p6", "", 120]]
        #self.data = []
        self.columns = ["ID", "Name", "Price"]

Nếu bạn muốn khởi tạo ma trận rỗng thì dùng lệnh: self.data=[]

Tiếp theo ta Override hàm setupUi() để nạp giao diện cũng như gán đối tượng TableModel cho QTableView:

def setupUi(self, MainWindow):
    super().setupUi(MainWindow)
    self.MainWindow=MainWindow

    self.model = TableModel(self.data, self.columns)
    self.tableViewProduct.setModel(self.model)

    self.tableViewProduct.selectionModel().selectionChanged.connect(self.processItemSelection)

    self.tableViewProduct.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
    self.tableViewProduct.customContextMenuRequested.connect(self.onCustomContextMenuRequested)

    self.tableViewProduct.verticalHeader().setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
    self.tableViewProduct.verticalHeader().customContextMenuRequested.connect(self.onCustomContextMenuRequested)

    self.actionExport_to_JSon.triggered.connect(self.processExportJson)
    self.actionImport_from_JSon.triggered.connect(self.processImportJson)

Đồng thời ta cũng khai báo các signal để xử lý sự kiện selectionChange và đăng ký các Context Menu trên Cho QTableView (khi nhấn chuột phải vào các ô bên trong QTableView hoặc nhấn chuột phải vào tiêu đề dòng).

Hàm processItemSelection() sẽ xử lý lấy dữ liệu đang chọn tại dòng hiện hành và hiển thị thông tin chi tiết lên QLineEdit:

def processItemSelection(self):
    index=self.tableViewProduct.currentIndex()
    if index.row()>-1:
        item=self.data[index.row()]
        self.lineEditProductId.setText(item[0])
        self.lineEditProductName.setText(item[1])
        self.lineEditUnitPrice.setText(str(item[2]))

Ta viết mã lệnh cho hàm onCustomContextMenuRequested() để khởi tạo QMenu context menu:

def onCustomContextMenuRequested(self, pos):
    index = self.tableViewProduct.indexAt(pos)
    menu = QMenu()
    if index.isValid():
        insertFirst = menu.addAction("Insert &First")
        insertFirst.setIcon(QIcon("images/ic_first.png"))
        insertLast = menu.addAction("Insert &Last")
        insertLast.setIcon(QIcon("images/ic_last.png"))
        insertAbove = menu.addAction("Insert &Above")
        insertAbove.setIcon(QIcon("images/ic_above.png"))
        insertBelow = menu.addAction("Insert &Below")
        insertBelow.setIcon(QIcon("images/ic_below.png"))
        removeSelected = menu.addAction("Remove selected row")
        removeSelected.setIcon(QIcon("images/ic_delete.png"))

        menu.addAction(insertFirst)
        menu.addAction(insertLast)
        menu.addAction(insertAbove)
        menu.addAction(insertBelow)
        menu.addSeparator()
        menu.addAction(removeSelected)

        insertFirst.triggered.connect(partial(self.processInsert, InsertBehavior.INSERT_FIRST))
        insertLast.triggered.connect(partial(self.processInsert, InsertBehavior.INSERT_LAST))
        insertAbove.triggered.connect(partial(self.processInsert, InsertBehavior.INSERT_ABOVE))
        insertBelow.triggered.connect(partial(self.processInsert, InsertBehavior.INSERT_BELOW))
        removeSelected.triggered.connect(self.processDelete)
        menu.exec(self.tableViewProduct.viewport().mapToGlobal(pos))
        menu.close()
    else:
        insertNew = menu.addAction("Insert New Record")
        insertNew.setIcon(QIcon("images/ic_insertnew.png"))
        menu.addAction(insertNew)
        insertNew.triggered.connect(partial(self.processInsert, InsertBehavior.INSERT_FIRST))
        menu.exec(self.tableViewProduct.viewport().mapToGlobal(pos))
        menu.close()

Lệnh trên sẽ giúp tạo 2 QMenu ở 2 case khác nhau như đã nói ở trên.

Hàm:

index = self.tableViewProduct.indexAt(pos)

Sẽ cho ta biết dòng nào đang được chọn

Nếu:

index.isValid()

là hợp lệ thì tạo Context menu:

Nếu:

index.isValid()

là không hợp lệ, thì tạo Context menu:

Bạn lưu ý quan trọng là Tui có viết kỹ thuật partial để các signal triệu gọi các slot mà có truyền parameter:

insertFirst.triggered.connect(partial(self.processInsert, InsertBehavior.INSERT_FIRST))
insertLast.triggered.connect(partial(self.processInsert, InsertBehavior.INSERT_LAST))
insertAbove.triggered.connect(partial(self.processInsert, InsertBehavior.INSERT_ABOVE))
insertBelow.triggered.connect(partial(self.processInsert, InsertBehavior.INSERT_BELOW))

Ta bổ sung hàm processInsert() để xử lý: Chèn đầu, chèn cuối, chèn trước, chèn sau:

def processInsert(self,behavior=InsertBehavior.INSERT_FIRST):
    indexes = self.tableViewProduct.selectionModel().selectedIndexes()
    if behavior==InsertBehavior.INSERT_FIRST:
        row=0
    elif behavior==InsertBehavior.INSERT_LAST:
        row = self.tableViewProduct.model().rowCount(QModelIndex())+1
    else:
        if indexes:
            index=indexes[0]
            row=index.row()
            if behavior==InsertBehavior.INSERT_ABOVE:
                row=max(row,0)
            else:
                size = self.tableViewProduct.model().rowCount(QModelIndex())
                row = min(row + 1, size)
    self.tableViewProduct.model().insertRows(row, 1, QModelIndex())

Dựa vào các enum trong InsertBehavior mà ta gọi các lệnh Chèn cho phù hợp với từng index.

Tương tự như vậy, ta viết mà lệnh cho hàm processDelete():

def processDelete(self):
    indexes = self.tableViewProduct.selectionModel().selectedIndexes()
    if indexes:
            index=indexes[0]
            row = index.row()
            self.tableViewProduct.model().removeRows(row, 1, QModelIndex())

Hàm processDelete() sẽ xóa dòng đang chọn.

Cuối cùng là tới nhóm hàm dành cho các Menu.

Hàm processExportJson() để xuất dữ liệu mà người dùng trên QTableView ra JSon Array:

def processExportJson(self):
    # setup for QFileDialog
    filters = "Dataset (*.json);;All files(*)"
    filename, selected_filter = QFileDialog.getSaveFileName(
        self.MainWindow,
        filter=filters,
    )
    #self.fileFactory.writeData(filename,self.model.data)
    with open(filename, 'w') as jf:
        jsonString = json.dumps(self.model.data)
        jsonFile = open(filename, "w")
        jsonFile.write(jsonString)
        jsonFile.close()

Hàm trên cũng dùng QFileDialog dạng Save File Dialog để cho người dùng chọn nơi lưu trữ bất kỳ.

Hàm processImportJson() để nạp dữ liệu từ file JSon đã lưu ở ổ cứng, khởi tạo dữ liệu cho model và hiển thị lại giao diện:

def processImportJson(self):
    # setup for QFileDialog
    filters = "Dataset (*.json);;All files(*)"
    filename, selected_filter = QFileDialog.getOpenFileName(
        self.MainWindow,
        filter=filters,
    )
    with open(filename, 'r') as jf:
        lines = jf.readline()
        print(lines)
        self.data= json.loads(lines)
        self.model.data= self.data
    self.model.layoutChanged.emit()

Hàm sẽ khởi tạo lại ma trậ ncho self.data và truyền lệnh cho QTableView cập nhật lại giao diện thông qua lệnh:

self.model.layoutChanged.emit()

Dưới đã là mã lệnh chi tiết của MainWindowEx.py:

import json
from functools import partial

from PyQt6.QtCore import QModelIndex, Qt
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import QMenu, QFileDialog
from InsertBehavior import InsertBehavior
from MainWindow import Ui_MainWindow
from TableModel import TableModel

class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        self.data = [["p1", "Coca", 100],
                ["p2", "Pepsi", 50],
                ["p3", "Sting", 300],
                ["p4", "Aqua", 70],
                ["p5", "Redbull", 200],
                ["p6", "", 120]]
        #self.data = []
        self.columns = ["ID", "Name", "Price"]
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow

        self.model = TableModel(self.data, self.columns)
        self.tableViewProduct.setModel(self.model)

        self.tableViewProduct.selectionModel().selectionChanged.connect(self.processItemSelection)

        self.tableViewProduct.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.tableViewProduct.customContextMenuRequested.connect(self.onCustomContextMenuRequested)

        self.tableViewProduct.verticalHeader().setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.tableViewProduct.verticalHeader().customContextMenuRequested.connect(self.onCustomContextMenuRequested)

        self.actionExport_to_JSon.triggered.connect(self.processExportJson)
        self.actionImport_from_JSon.triggered.connect(self.processImportJson)
    def processItemSelection(self):
        index=self.tableViewProduct.currentIndex()
        if index.row()>-1:
            item=self.data[index.row()]
            self.lineEditProductId.setText(item[0])
            self.lineEditProductName.setText(item[1])
            self.lineEditUnitPrice.setText(str(item[2]))

    def onCustomContextMenuRequested(self, pos):
        index = self.tableViewProduct.indexAt(pos)
        menu = QMenu()
        if index.isValid():
            insertFirst = menu.addAction("Insert &First")
            insertFirst.setIcon(QIcon("images/ic_first.png"))
            insertLast = menu.addAction("Insert &Last")
            insertLast.setIcon(QIcon("images/ic_last.png"))
            insertAbove = menu.addAction("Insert &Above")
            insertAbove.setIcon(QIcon("images/ic_above.png"))
            insertBelow = menu.addAction("Insert &Below")
            insertBelow.setIcon(QIcon("images/ic_below.png"))
            removeSelected = menu.addAction("Remove selected row")
            removeSelected.setIcon(QIcon("images/ic_delete.png"))

            menu.addAction(insertFirst)
            menu.addAction(insertLast)
            menu.addAction(insertAbove)
            menu.addAction(insertBelow)
            menu.addSeparator()
            menu.addAction(removeSelected)

            insertFirst.triggered.connect(partial(self.processInsert, InsertBehavior.INSERT_FIRST))
            insertLast.triggered.connect(partial(self.processInsert, InsertBehavior.INSERT_LAST))
            insertAbove.triggered.connect(partial(self.processInsert, InsertBehavior.INSERT_ABOVE))
            insertBelow.triggered.connect(partial(self.processInsert, InsertBehavior.INSERT_BELOW))
            removeSelected.triggered.connect(self.processDelete)
            menu.exec(self.tableViewProduct.viewport().mapToGlobal(pos))
            menu.close()
        else:
            insertNew = menu.addAction("Insert New Record")
            insertNew.setIcon(QIcon("images/ic_insertnew.png"))
            menu.addAction(insertNew)
            insertNew.triggered.connect(partial(self.processInsert, InsertBehavior.INSERT_FIRST))
            menu.exec(self.tableViewProduct.viewport().mapToGlobal(pos))
            menu.close()

    def processInsert(self,behavior=InsertBehavior.INSERT_FIRST):
        indexes = self.tableViewProduct.selectionModel().selectedIndexes()
        if behavior==InsertBehavior.INSERT_FIRST:
            row=0
        elif behavior==InsertBehavior.INSERT_LAST:
            row = self.tableViewProduct.model().rowCount(QModelIndex())+1
        else:
            if indexes:
                index=indexes[0]
                row=index.row()
                if behavior==InsertBehavior.INSERT_ABOVE:
                    row=max(row,0)
                else:
                    size = self.tableViewProduct.model().rowCount(QModelIndex())
                    row = min(row + 1, size)
        self.tableViewProduct.model().insertRows(row, 1, QModelIndex())
    def processDelete(self):
        indexes = self.tableViewProduct.selectionModel().selectedIndexes()
        if indexes:
                index=indexes[0]
                row = index.row()
                self.tableViewProduct.model().removeRows(row, 1, QModelIndex())

    def processExportJson(self):
        # setup for QFileDialog
        filters = "Dataset (*.json);;All files(*)"
        filename, selected_filter = QFileDialog.getSaveFileName(
            self.MainWindow,
            filter=filters,
        )
        #self.fileFactory.writeData(filename,self.model.data)
        with open(filename, 'w') as jf:
            jsonString = json.dumps(self.model.data)
            jsonFile = open(filename, "w")
            jsonFile.write(jsonString)
            jsonFile.close()
    def processImportJson(self):
        # setup for QFileDialog
        filters = "Dataset (*.json);;All files(*)"
        filename, selected_filter = QFileDialog.getOpenFileName(
            self.MainWindow,
            filter=filters,
        )
        with open(filename, 'r') as jf:
            lines = jf.readline()
            print(lines)
            self.data= json.loads(lines)
            self.model.data= self.data
        self.model.layoutChanged.emit()
    def show(self):
        self.MainWindow.show()

Bước 7: Cuối cùng ta tạo “MyApp.py” để thực thi chương trình

from PyQt6.QtWidgets import QApplication, QMainWindow

from MainWindowEx import MainWindowEx

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

Chạy chương trình lên ta có kết quả như mong muốn:

Như vậy là tới đây Tui đã hướng dẫn đầy đủ và chi tiết kiến trúc Model View và áp dụng cho QTableView trong quản lý Product. Với đầy đủ tính năng: Xem, thêm, sửa, xóa. Đặc biệt là các chức năng Context Menu để hỗ trợ việc chèn dữ liệu cũng như xóa dữ liệu khởi QTableView, và Menu để Export và Import dữ liệu với định dạng JSon Array.

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

https://www.mediafire.com/file/jxdipo7auiz7tdl/LearnModelViewPart4.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ừ SQLite vào QTableView dùng kiến trúc Model View.

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

Bài 28: Kiến trúc Model View-PyQt6–Part 3

Ở các bài trước chúng ta đã ứng dụng thành thạo kiến trúc Model View để tương tác dữ liệu Employee trên QListview. Trong bài này Tui tiếp tục hướng dẫn các bạn cách dùng Model View cho QTableView để hiển thị dữ liệu dạng bảng. Bạn có thể nạp Ma trận dữ liệu vào QTableView hoặc nạp Danh sách dữ liệu đối tượng dạng List vào QTableView. Sự khác biệt giữa QListView và QTableView là QListView không cần dùng các cột dữ liệu, còn QTableView cần dùng các cột dữ liệu để chi tiết hóa các thuộc tính của đối tượng.

Trong bài này Tui minh họa ví dụ đơn giản về nạp Ma trận Product lên QTableView dùng kiến trúc Model View. Giả sử ta có Ma trận dữ liệu như dưới đây:

data = [["p1", "Coca", 100],
        ["p2", "Pepsi", 50],
        ["p3", "Sting", 300],
        ["p4", "Aqua", 70],
        ["p5", "Redbull", 200],
        ["p6", "", 120]]
columns = ["ID", "Name", "Price"]

Làm thế nào để nạp ma trận “data” lên QTableView cùng với các cột được lưu trong mảng “columns“? Đồng thời nếu giá <100 thì tô chữ đỏ, nếu tên rỗng thì tô nền vàng:

Nếu hiểu và triển khai được bài này thì bạn có thể dễ dàng nạp ma trận dữ liệu cần phân tích lên giao diện, ví dụ ma trận về lịch sử kinh doanh.

Các bạn tiến hành làm từng bước như Tui hướng dẫn dưới đây.

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

  • Trung tâm của dự án chính là lớp “TableModel.py“. Lớp này kế thừa từ “QAbstractTableModel” để thực hiện kiến trúc Model View. Tui sẽ trình bày chi tiết ở bước sau
  • MainWindow.ui” là giao diện thiết kế của phần mềm bằng Qt Designer
  • MainWindow.py” là generate python code của giao diện “MainWindow.ui”
  • MainWindowEx.py” là lớp kế thừa từ generate python code “MainWindow.py” nó được dùng để nạp giao diện, xử lý sự kiện người dùng, gán model view mà không bị ảnh hưởng khi ta tiếp tục generate code từ giao diện khi thay đổi.
  • MyApp.py” là file mã lệnh thực thi chương trình

Bước 2: Tạo lớp mã lệnh “TableModel.py” như dưới đây:

from PyQt6 import QtGui
from PyQt6.QtCore import Qt, QAbstractTableModel


class TableModel(QAbstractTableModel):
    def __init__(self, data,columns):
        super().__init__()
        self.data = data
        self.columns=columns

    def data(self, index, role):
        value=self.data[index.row()][index.column()]
        if role == Qt.ItemDataRole.DisplayRole:
            return value
        if role==Qt.ItemDataRole.BackgroundRole:
            if index.column()==1 and value=="":
               return QtGui.QColor(Qt.GlobalColor.yellow)
        if role==Qt.ItemDataRole.ForegroundRole:
            if index.column() == 2 and value<100:
                return QtGui.QColor(Qt.GlobalColor.red)
    def rowCount(self, index):
        return len(self.data)

    def columnCount(self, index):
        return len(self.columns)

    def headerData(self, section, orientation, role):
        if role == Qt.ItemDataRole.DisplayRole:
            if orientation == Qt.Orientation.Horizontal:
                return str(self.columns[section])
            if orientation==Qt.Orientation.Vertical:
                return  str(section+1)

Tui giải thích chi tiết các hàm của TableModel.py như sau:

  • Constructor __init__(self, data,columns) nhận vào 2 đối số. Đối số data là ma trận dữ liệu Product đã mô tả ở trên, đối số columns là mảng các cột của Product.
  • Hàm data(self, index, role) được override để hiển thị dữ liệu cũng như các định dạng dữ liệu (màu nền, màu chữ). Trong hàm này Tui có sử dụng 3 Role:
    1. Qt.ItemDataRole.DisplayRole (dùng để hiển thị dữ liệu thông thường)
    2. Qt.ItemDataRole.BackgroundRole (để hiển thị màu nền, trong trường hợp này là nếu dữ liệu của cột tên Product là rỗng thì ta tô nền vàng)
    3. Qt.ItemDataRole.ForegroundRole (để hiển thị màu chữ, trong trường hợp này là nếu dữ liệu của cột giá Product nhỏ hơn 100 thì tô chữ đỏ)
  • Hàm rowCount(self, index) được override để trả về số dòng dữ liệu trong model
  • Hàm columnCount(self, index) được override để trả về số cột trong model
  • Hàm headerData(self, section, orientation, role) được override để vẽ tiêu đề của Cột nằm đứng và header của dòng nằm ngang.

5 Hàm bắt buộc cần được định nghĩa đối với TableModel ở trên. Tương tự cho các loại dữ liệu khác thì bạn cũng tạo 5 hàm này, và điều chỉnh mã lệnh theo nhu cầu sử dụng khác nhau. Còn những hàm nâng cao khác để hỗ trợ thêm mới, chỉnh sửa, cũng như xóa dữ liệu Tui sẽ trình bày chi tiết ở bài học sau.

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

Bài này thì ta chỉ cần kéo 1 QTableView ra mà thôi.

Bước 4: Generate python code “MainWindow.py” cho giao diện “MainWindow.ui”

# 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(335, 297)
        self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.tableViewProduct = QtWidgets.QTableView(parent=self.centralwidget)
        self.tableViewProduct.setGeometry(QtCore.QRect(10, 10, 301, 231))
        self.tableViewProduct.setObjectName("tableViewProduct")
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 335, 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 - QTableView-ModelView"))

Bước 5: Tạo “MainWindowEx.py” kế thừa từ Generate Python code ở trên để nạp giao diện và gán Model View

from MainWindow import Ui_MainWindow
from TableModel import TableModel


class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        super().__init__()
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow
        data = [["p1", "Coca", 100],
                ["p2", "Pepsi", 50],
                ["p3", "Sting", 300],
                ["p4", "Aqua", 70],
                ["p5", "Redbull", 200],
                ["p6", "", 120]]
        columns = ["ID", "Name", "Price"]
        self.model = TableModel(data, columns)
        self.tableViewProduct.setModel(self.model)
    def show(self):
        self.MainWindow.show()

Dòng lệnh 18:

self.model = TableModel(data, columns)

Dòng lệnh 18 này dùng để khởi tạo đối tượng tableModel, nó nhận vào Ma trận Product và mảng columns

Dòng lệnh 19:

self.tableViewProduct.setModel(self.model)

Dòng lệnh 19 gán model cho QTableView.

Lúc này các hàm trong TableModel sẽ tự động được thực hiện, rất chuyên nghiệp và ảo ma canada.

Bước 6: Tạo file lệnh “MyApp.py” để thực thi chương trình

from PyQt6.QtWidgets import QApplication, QMainWindow

from MainWindowEx import MainWindowEx

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

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

Bạn quan sát thấy cột Price sẽ tô màu đỏ những Price nào <100, như vậy giá 50 và 70 tự động được tô màu đỏ. Và cột Name có dữ liệu nào rỗng sẽ tô nền vàng.

Như vậy Tui đã trình bày xong cách ứng dụng kiến trúc Model View vào quản lý và hiển thị dữ liệu dạng Ma trận, đã trình bày kỹ cách tạo TableModel kế thừa từ QAbstractTableModel. Cũng như đã trình bày chi tiết ý nghĩa và cách lập trình các hàm quan trọng trong lớp này để ta có thể dễ dàng tùy chỉnh cách thức mà dữ liệu hiển thị trên giao diện như mong muốn.

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

https://www.mediafire.com/file/o3s1ytr0fi1hn0w/LearnModelViewPart3.rar/file

Bài học sau Tui sẽ mở rộng bài này bằng cách bổ sung các chức năng: Thêm, Sửa, Xóa dữ liệu ngay trên giao diện QTableView để các bạn củng cố hơn nữa cách ứng dụng kiến trúc Model View cũng như nhìn thấy được lợi ích của mô hình này. Các bạn chú ý theo dõi

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

Bài 27: Kiến trúc Model View-PyQt6–Part 2

Bài 26 Tui đã trình bày chi tiết về kiến trúc Model View trong PyQt6 cùng với một minh họa về hiển thị danh sách Employee lên QListView. Trong bài này Tui sẽ tiếp tục mở rộng bài minh họa về Employee, bằng cách bổ sung các chức năng: Thêm, Sửa, Xóa model cũng như viết mã lệnh hiệu chỉnh định dạng font chữ, màu chữ cho các item trên QListView bằng kiến trúc Model View. Đặc biệt Tui cung cấp thêm 3 menu item để serialize và deserialize dữ liệu JSon Array cho danh sách Employee, và menu item Exit.

Mô tả chi tiết chức năng của phần mềm:

  • Chương trình dùng để quản lý Employee, thông tin của Employee bao gồm: id, name, age. Chương trình sử dụng kiến trúc Model View
  • Dữ liệu hiển thị lên QListView có 2 dạng: Nếu Employee có age>=18 thì có biểu tượng icon màu xanh. Nếu age<18 thì có biểu tượng icon màu đỏ, chữ đỏ và nền vàng.
  • Nút “New”: Chương trình sẽ xóa dữ liệu trên QLineEdit và focus tới ô ID
  • Nút “Save”: Có 2 chức năng là lưu mới và lưu cập nhật, nếu Employee đang chọn trên QListView thì chương trình sẽ cập nhật, còn nếu không chọn Employee trên QListView thì chương trình sẽ lưu mới. Nếu lưu thành công dữ liệu sẽ hiển thị lên QListView
  • Nút “Delete”: Chương trình sẽ xóa Employee đang chọn trên QListView, có hiển thị cửa sổ xác nhận muốn xóa hay không.
  • Menu item “Export to Json”: Chương trình sẽ mở cửa sổ SaveFileDialog để người dùng xuất dữ liệu Employee có định dạng JSON ARRY ra ổ cứng bất kỳ
  • Menu item “Import from Json”: Chương trình sẽ mở cửa sổ OpenFileDialog để người dùng chọn file JSON ARRAY đã lưu và hiểu thị danh sách Employee trong file này lên QListView.
  • Menu item “Exit”: Chương trình sẽ thoát

Bây giờ chúng ta đi vào chi tiết từng bước:

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

  • Thư mục “images” lưu trữ các hình ảnh và icon của phần mềm
  • “Employee.py” là file mã lệnh python cho mô hình lớp Employee có các thuộc tính id, name age
  • “EmployeeModel.py” là lớp Model kế thừa từ QAbstractListModel để sử dụng trong mô hình Model View nhằm hiển thị dữ liệu danh sách Employee.
  • “FileFactory.py” là file mã lệnh Python để Serialize và Deserialize dữ liệu Employee với định dạng Json Array
  • “MainWindow.ui” là file giao diện của phần mềm, sử dụng Qt Designer tích hợp trong Pycharm để thiết kế.
  • “MainWindow.py” là file Generate Python code của giao diện MainWindow.ui
  • “MainWindowEx.py” là file mã lệnh python để xử lý sự kiện người dùng cũng như gán Model View
  • “MyApp.py” là file mã lệnh python để thực thi chương trình.

Bước 2: Viết mã lệnh cho Employee.py

class Employee:
    def __init__(self,id,name,age):
        self.id=id
        self.name=name
        self.age=age
    def __str__(self):
        return str(self.id)+"-"+self.name+"-"+str(self.age)

Lớp Employee ở trên có 3 thuộc tính: id, name và age

hàm __str__ để hiển thị chuỗi dữ liệu format cho đối tượng Employee, tùy nhu cầu mà bạn có thể chỉnh sửa hàm này.

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

import typing

from PyQt6 import QtGui
from PyQt6.QtCore import QAbstractListModel, Qt, QModelIndex
from PyQt6.QtGui import QImage


class EmployeeModel(QAbstractListModel):
    def __init__(self,employees=None):
        super().__init__()
        self.employees=employees
    def data(self, index: QModelIndex, role: int = ...) -> typing.Any:
        emp = self.employees[index.row()]
        if role == Qt.ItemDataRole.DisplayRole:
            return str(emp)
        if role==Qt.ItemDataRole.DecorationRole:
            if emp.age<18:
                return QImage("images/ic_notvalid.png")
            else:
                return QImage("images/ic_valid.png")
        if role==Qt.ItemDataRole.ForegroundRole:
            if emp.age < 18:
                return QtGui.QColor(Qt.GlobalColor.red)
        if role==Qt.ItemDataRole.BackgroundRole:
            if emp.age < 18:
                return QtGui.QColor(Qt.GlobalColor.yellow)
    def rowCount(self, parent: QModelIndex = ...) -> int:
        return len(self.employees)

Nếu Employee có age>=18 thì có biểu tượng icon màu xanh. Nếu age<18 thì có biểu tượng icon màu đỏ, chữ đỏ và nền vàng.

Ở đây Tui giải thích thêm hàm override data(), nó dùng để hiển thị dữ liệu theo từng trường hợp đặc biệt:

RoleValueÝ nghĩa chức năng
Qt.ItemDataRole.DisplayRole0Dùng để hiển thị dữ liệu, kiểu QString.
Qt.ItemDataRole.DecorationRole1Dùng để hiển thị định dạng trang trí, biểu tượng, kiểu QColor, QIcon hoặc QPixmap
Qt.ItemDataRole.EditRole2Dùng để hiển thị khi dữ liệu chỉnh sửa, kiểu QString
Qt.ItemDataRole.ToolTipRole3Dùng để hiển thị Tooltip, kiểu QString
Qt.ItemDataRole.StatusTipRole4Dùng để hiển thị cho staus bar, kiểu QString
Qt.ItemDataRole.WhatsThisRole5Dùng để hiển thị “What’s this”, kiểu QString
Qt.ItemDataRole.FontRole6Dùng để hiển thị Font chữ
Qt.ItemDataRole.TextAlignmentRole7Dùng để căn lề chữ
Qt.ItemDataRole.BackgroundRole8Dùng để hiển thị màu nền
Qt.ItemDataRole.ForegroundRole9Dùng để hiển thị màu chữ
Qt.ItemDataRole.CheckStateRole10Dùng để kiểm tra trạng thái
Qt.ItemDataRole.AccessibleTextRole11Dùng để truy suất text
Qt.ItemDataRole.AccessibleDescriptionRole12Dùng để truy suất mô tả
Qt.ItemDataRole.SizeHintRole13Dùng để thiết lập size hint cho item
Qt.ItemDataRole.InitialSortOrderRole14Dùng để khởi tạo sắp xếp
Qt.ItemDataRole.UserRole15Dùng để xử lý dữ liệu object trong item

Bước 3: Viết mã lệnh cho FileFactory.py

import json
import os

class FileFactory:
    #path: path to serialize array of Employee
    #arrData: array of Employee
    def writeData(self,path,arrData):
        jsonString = json.dumps([item.__dict__ for item in arrData],default=str)
        jsonFile = open(path, "w")
        jsonFile.write(jsonString)
        jsonFile.close()
    #path: path to deserialize array of Employee
    #ClassName: Employee
    def readData(self,path,ClassName):
        if os.path.isfile(path) == False:
            return []
        file = open(path, "r")
        # Reading from file
        self.arrData = json.loads(file.read(), object_hook=lambda d: ClassName(**d))
        file.close()
        return self.arrData

Lớp FileFactory này các bạn đã làm quen nhiều lần ở các bài học trước, nhiệm vụ của writeData là Serialize danh sách Employee xuống ổ cứng với định dạng JSON Array. Hàm readData để Deserialize chuỗi JSON Array dưới ổ cứng lên bộ nhớ bằng mô hình hóa hướng đối tượng các Employee.

Bước 4: Thiết kế giao diện MainWindow.ui bằng Qt Designer (Hoặc Qt Creator)

Ta kéo thả các Widget và đặt tên cho chúng như hình tổng quan dưới đây:

Các widget QLineEdit, QPushButton, QListView các bạn đã làm nhiều lần rồi nên Tui không trình bày lại, các bạn chỉ cần kéo thả ra giao diện và đặt tên như trên là được.

Tui sẽ trình bày Menu vì bạn chưa được học. Cấu trúc Menu của PyQt6 gồm:

  • Thanh nằm ngang chứa các menu gọi là QMenuBar
  • Bên trong QMenu chứa các Menu được gọi là QMenu
  • Bên trong QMenu khi nhấn vào nó xổ danh sách xuống gọi là các QAction

Tức với giao diện ở trên thì:

  • Thanh nằm ngang ở trên cùng là QMenuBar
  • System” là QMenu
  • Export to JSon” là QAction
  • Import from JSon” là QAction
  • Exit” là QAction

Và signal ta dùng cho các QAction là triggered

Ta tiếp tục thiết kế QMenuBar nhé. Nhìn vào nhóm Menu Bar ở bên trên giao diện, ngay chỗ “Type Here” bạn gõ vào System và nhấn Enter thì ta có kết quả như hình bên dưới:

Tiếp tục chỗ “Type Here” ở hình trên ta gõ vào “Export to JSon” rồi nhấn Enter để tạo QAction, ta có kết quả:

Để tạo thanh nằm ngang ngăn cách các QAction ta nhấn “Add Separator”, kết quả sẽ xuất hiện đường nằm ngang:

Tiếp tục, chỗ “Type Here” ta gõ “Import from JSon” để tạo QAction rồi nhấn Enter, kết quả:

Để tạo thêm đường ngang giữa các QAction ta nhấn “Add Separator”, kết quả:

Cuối cùng ta gõ “Exit” vào mục Type Here, kết quả:

Sau khi tạo xong các QAction cho QMenu ta tiến hành gán các Icon cho các QAction để nó thẩm mỹ hơn, để tạo Icon thì ta cứ chọn QAction trước, sau đó trong thuộc tính Icon ta chọn “Choose File…” để trỏ tới hình mình mong muốn:

Cứ như vậy, các bạn lặp lại thao tác gán Icon cho các QAction còn lại, kết quả cuối cùng:

Bước 5: Generate Python code “MainWindow.py” cho giao diện “MainWindow.ui“. Cách Generate code đã được học kỹ ở các bài đầu tiên

# 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(361, 349)
        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.listViewEmployee = QtWidgets.QListView(parent=self.centralwidget)
        self.listViewEmployee.setGeometry(QtCore.QRect(10, 10, 331, 161))
        self.listViewEmployee.setObjectName("listViewEmployee")
        self.label = QtWidgets.QLabel(parent=self.centralwidget)
        self.label.setGeometry(QtCore.QRect(20, 180, 81, 16))
        self.label.setObjectName("label")
        self.lineEditId = QtWidgets.QLineEdit(parent=self.centralwidget)
        self.lineEditId.setGeometry(QtCore.QRect(130, 180, 201, 22))
        self.lineEditId.setObjectName("lineEditId")
        self.lineEditName = QtWidgets.QLineEdit(parent=self.centralwidget)
        self.lineEditName.setGeometry(QtCore.QRect(130, 210, 201, 22))
        self.lineEditName.setObjectName("lineEditName")
        self.label_2 = QtWidgets.QLabel(parent=self.centralwidget)
        self.label_2.setGeometry(QtCore.QRect(20, 210, 101, 16))
        self.label_2.setObjectName("label_2")
        self.label_3 = QtWidgets.QLabel(parent=self.centralwidget)
        self.label_3.setGeometry(QtCore.QRect(20, 240, 101, 16))
        self.label_3.setObjectName("label_3")
        self.lineEditAge = QtWidgets.QLineEdit(parent=self.centralwidget)
        self.lineEditAge.setGeometry(QtCore.QRect(130, 240, 201, 22))
        self.lineEditAge.setObjectName("lineEditAge")
        self.pushButtonNew = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonNew.setGeometry(QtCore.QRect(40, 270, 71, 28))
        icon1 = QtGui.QIcon()
        icon1.addPixmap(QtGui.QPixmap("images/ic_new.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.pushButtonNew.setIcon(icon1)
        self.pushButtonNew.setIconSize(QtCore.QSize(24, 24))
        self.pushButtonNew.setObjectName("pushButtonNew")
        self.pushButtonSave = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonSave.setGeometry(QtCore.QRect(150, 270, 71, 28))
        icon2 = QtGui.QIcon()
        icon2.addPixmap(QtGui.QPixmap("images/ic_save.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.pushButtonSave.setIcon(icon2)
        self.pushButtonSave.setIconSize(QtCore.QSize(24, 24))
        self.pushButtonSave.setObjectName("pushButtonSave")
        self.pushButtonDelete = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonDelete.setGeometry(QtCore.QRect(260, 270, 71, 28))
        icon3 = QtGui.QIcon()
        icon3.addPixmap(QtGui.QPixmap("images/ic_delete.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.pushButtonDelete.setIcon(icon3)
        self.pushButtonDelete.setObjectName("pushButtonDelete")
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 361, 26))
        self.menubar.setObjectName("menubar")
        self.menuSystem = QtWidgets.QMenu(parent=self.menubar)
        self.menuSystem.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight)
        self.menuSystem.setTearOffEnabled(False)
        self.menuSystem.setSeparatorsCollapsible(False)
        self.menuSystem.setToolTipsVisible(False)
        self.menuSystem.setObjectName("menuSystem")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(parent=MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)
        self.actionExport_to_JSon = QtGui.QAction(parent=MainWindow)
        icon4 = QtGui.QIcon()
        icon4.addPixmap(QtGui.QPixmap("images/ic_export.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.actionExport_to_JSon.setIcon(icon4)
        self.actionExport_to_JSon.setObjectName("actionExport_to_JSon")
        self.actionImport_from_JSon = QtGui.QAction(parent=MainWindow)
        icon5 = QtGui.QIcon()
        icon5.addPixmap(QtGui.QPixmap("images/ic_import.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.actionImport_from_JSon.setIcon(icon5)
        self.actionImport_from_JSon.setObjectName("actionImport_from_JSon")
        self.actionExit = QtGui.QAction(parent=MainWindow)
        icon6 = QtGui.QIcon()
        icon6.addPixmap(QtGui.QPixmap("images/ic_exit.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.actionExit.setIcon(icon6)
        self.actionExit.setObjectName("actionExit")
        self.menuSystem.addAction(self.actionExport_to_JSon)
        self.menuSystem.addSeparator()
        self.menuSystem.addAction(self.actionImport_from_JSon)
        self.menuSystem.addSeparator()
        self.menuSystem.addAction(self.actionExit)
        self.menubar.addAction(self.menuSystem.menuAction())

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

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "Trần Duy Thanh - Model View"))
        self.label.setText(_translate("MainWindow", "Employee ID:"))
        self.label_2.setText(_translate("MainWindow", "Employee Name:"))
        self.label_3.setText(_translate("MainWindow", "Age of Employee:"))
        self.pushButtonNew.setText(_translate("MainWindow", "New"))
        self.pushButtonSave.setText(_translate("MainWindow", "Save"))
        self.pushButtonDelete.setText(_translate("MainWindow", "Delete"))
        self.menuSystem.setTitle(_translate("MainWindow", "System"))
        self.actionExport_to_JSon.setText(_translate("MainWindow", "Export to JSon"))
        self.actionImport_from_JSon.setText(_translate("MainWindow", "Import from JSon"))
        self.actionExit.setText(_translate("MainWindow", "Exit"))

Bước 6: Viết mã lệnh “MainWindowEx.py“, mã lệnh trong này kế thừa từ lớp được Generate trong bước trước để xử lý sự kiện người dùng và nó giúp ta không bị ảnh hưởng khi trong tương lai Generate lại python code.

Constructor của lớp này Tui định nghĩa 2 biến đối tượng như dưới đây:

from PyQt6.QtWidgets import QFileDialog, QMessageBox
from Employee import Employee
from EmployeeModel import EmployeeModel
from FileFactory import FileFactory
from MainWindow import Ui_MainWindow

class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        self.employees=[]
        self.selectedEmployee=None
        self.fileFactory=FileFactory()
  • Biến employees để lưu danh sách đối tượng Employee
  • Biến selectedEmployee để lưu Employee hiện tại đang lựa chọn trên giao diện QListView, nó được dùng để xử lý lưu mới hay lưu cập nhật Employee
  • Biến fileFactory là đối tượng để Serialize và Deserialize danh sách Employee với định dạng JSON ARRAY

Tiếp theo là hàm setupUi() được override để xử lý nạp giao diện cũng như xử lý signal mà người dùng tương tác:

def setupUi(self, MainWindow):
    super().setupUi(MainWindow)
    self.MainWindow=MainWindow
    self.model=EmployeeModel(self.employees)
    self.listViewEmployee.setModel(self.model)

    self.pushButtonNew.clicked.connect(self.processNew)
    self.pushButtonSave.clicked.connect(self.processSave)
    #self.listViewEmployee.clicked.connect(self.processClicked)
    self.listViewEmployee.selectionModel().selectionChanged.connect(self.processSelection)
    self.pushButtonDelete.clicked.connect(self.processDelete)
    self.actionExport_to_JSon.triggered.connect(self.processExportJson)
    self.actionImport_from_JSon.triggered.connect(self.processImportJson)

Mã lệnh xử lý các Signal ở trên có 2 cái mới:

  • Xử lý sự kiện người dùng lựa chọn trên QListView (Bao gồm cả click chuột và di chuyển phím) ta dùng singal selectionChanged nằm trong selectionModel()
  • Xử lý sự kiện người dùng lựa chọn QAction ta dùng singal triggered

Hàm processNew() sẽ xóa các dữ liệu trên QLineEdit và focus vào ô ID để giúp người dùng nhập liệu nhanh chóng, đồng thời gán selectedEmployee=None để đánh dấu đây là khởi đầu cho thao tác lưu mới một Employee.

def processNew(self):
    self.lineEditId.setText("")
    self.lineEditName.setText("")
    self.lineEditAge.setText("")
    self.lineEditId.setFocus()
    self.selectedEmployee=None

Tiếp theo là hàm processSave().

def processSave(self):
    id=self.lineEditId.text()
    name=self.lineEditName.text()
    age=int(self.lineEditAge.text())
    emp=Employee(id,name,age)
    if self.selectedEmployee==None:
        self.employees.append(emp)
        self.selectedEmployee=emp
    else:
        index=self.employees.index(self.selectedEmployee)
        self.selectedEmployee=emp
        self.employees[index]=self.selectedEmployee
    self.model.layoutChanged.emit()

Hàm Save sẽ có 2 chức năng là lưu mới và lưu cập nhật Employee.

Nếu selectedEmployee = None thì chương trình lưu mới, còn khác None thì chương trình lưu cập nhật.

Sau khi dữ liệu được cập nhật trong biến employees, lúc này ta sẽ nói ModelView cập nhật lại giao diện bằng hàm layoutChanged.emit()

Tiếp theo là hàm processSelection(). Hàm này sẽ lấy dòng dữ liệu mà người dùng chọn trên QListView bằng hàm selectedIndexes(). Hàm selectedIndexes() trả về danh sách các QIndex được lựa chọn, ở đây ta chỉ xử lý chọn 1 dòng dữ liệu, do đó ta có indexes[0].row() là trả về vị trí kiểu integer để lấy chính xác đối tượng Employee tại vị trí đang chọn:

def processSelection(self):
    indexes=self.listViewEmployee.selectedIndexes()
    if indexes:
        row=indexes[0].row()
        emp=self.employees[row]
        self.lineEditId.setText(str(emp.id))
        self.lineEditName.setText(emp.name)
        self.lineEditAge.setText(str(emp.age))
        self.selectedEmployee=emp

Sau khi có đối tượng Employee đang chọn, ta hiển thị lên các QLineEdit đồng thời gán biến selectedEmployee cho biến đối tượng này, mục đích để hỗ trợ cho thao tác lưu cập nhật, ví dụ dưới đây là người dùng chọn Vitor:

Tiếp tới nữa là hàm “processDelete()“, hàm này tiến hành xóa Employee đang chọn trên QListView:

def processDelete(self):
    dlg = QMessageBox(self.MainWindow)
    if self.selectedEmployee == None:
        dlg.setWindowTitle("Deleteing error")
        dlg.setIcon(QMessageBox.Icon.Critical)
        dlg.setText("You have to select a Product to delete")
        dlg.exec()
        return
    dlg.setWindowTitle("Confirmation Deleting")
    dlg.setText("Are you sure you want to delete?")
    buttons = QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
    dlg.setStandardButtons(buttons)
    button = dlg.exec()
    if button == QMessageBox.StandardButton.Yes:
        self.employees.remove(self.selectedEmployee)
        self.model.layoutChanged.emit()
        self.selectedEmployee = None
        self.processNew()

Lệnh trên Tui tiếp tục dùng QMessage dialog để xác thực xem người dùng có thực sự muốn xóa hay không, chọn Yes thì xóa:

tương tự như thao tác lưu mới hay cập nhật, thì thao tác xóa cũng gọi hàm emit() để model view cập nhật lại giao diện.

Để Serialize và Deserialize dữ liệu ra JSON ARRAY ta có 2 hàm được Tui viết dưới đây.

Thứ nhất là hàm processExportJson() dùng để xuất toàn bộ dữ liệu Employee ra JSON ARRAY:

def processExportJson(self):
    # setup for QFileDialog
    filters = "Dataset (*.json);;All files(*)"
    filename, selected_filter = QFileDialog.getSaveFileName(
        self.MainWindow,
        filter=filters,
    )
    self.fileFactory.writeData(filename,self.employees)

Mã lệnh trên dùng QFileDialog mà dạng SaveFile, lúc này chương trình sẽ hiển thị cửa sổ lưu file cho ta lựa chọn nơi lưu trữ tùy ý.

Ta vào menu System rồi chọn “Export to JSon”:

Sau đó chọn nơi lưu trữ, đặt tên file và nhấn nút Save:

Sau khi lưu thành công ta có cấu dữ liệu của database.json như sau:

[{"id": "1", "name": "Peter", "age": 25}, {"id": "2", "name": "John", "age": 20}, {"id": "3", "name": "Tom", "age": 17}, {"id": "4", "name": "Bjorn", "age": 24}, {"id": "5", "name": "Vitor", "age": 27}, {"id": "6", "name": "Manuel", "age": 16}, {"id": "7", "name": "Daisy", "age": 20}]

Cuối cùng là hàm “processImportJson()” dùng để đọc dữ liệu từ JSON và phục hồi lên bộ nhớ đồng thời đưa về mô hình hóa hướng đối tượng và hiển thị lên QListView:

def processImportJson(self):
    # setup for QFileDialog
    filters = "Dataset (*.json);;All files(*)"
    filename, selected_filter = QFileDialog.getOpenFileName(
        self.MainWindow,
        filter=filters,
    )
    arr=self.fileFactory.readData(filename,Employee)

    self.employees.clear()

    for i in range(len(arr)):
        self.employees.append(arr[i])
    self.model.layoutChanged.emit()

Mã lệnh ở trên sẽ dùng QFileDialog.getOpenFileName để mở cửa sổ File Dialog, cho người dùng lựa chọn file dữ liệu để nạp lên giao diện.

Sau khi nạp xong dữ liệu lên bộ nhớ, ta gọi hàm self.model.layoutChanged.emit() để hiển thị dữ liệu lên giao diện:

Từ giao diện trống, ta chọn “Import from JSon”, sau đó chọn file “database.json” rồi bấm “Open”, ta sẽ có kết quả như mong muốn, các Emloyee được nạp lên QListView.

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

from PyQt6.QtWidgets import QFileDialog, QMessageBox
from Employee import Employee
from EmployeeModel import EmployeeModel
from FileFactory import FileFactory
from MainWindow import Ui_MainWindow

class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        self.employees=[]
        self.selectedEmployee=None
        self.fileFactory=FileFactory()
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow
        self.model=EmployeeModel(self.employees)
        self.listViewEmployee.setModel(self.model)

        self.pushButtonNew.clicked.connect(self.processNew)
        self.pushButtonSave.clicked.connect(self.processSave)
        #self.listViewEmployee.clicked.connect(self.processClicked)
        self.listViewEmployee.selectionModel().selectionChanged.connect(self.processSelection)
        self.pushButtonDelete.clicked.connect(self.processDelete)
        self.actionExport_to_JSon.triggered.connect(self.processExportJson)
        self.actionImport_from_JSon.triggered.connect(self.processImportJson)

    def processNew(self):
        self.lineEditId.setText("")
        self.lineEditName.setText("")
        self.lineEditAge.setText("")
        self.lineEditId.setFocus()
        self.selectedEmployee=None
    def processSave(self):
        id=self.lineEditId.text()
        name=self.lineEditName.text()
        age=int(self.lineEditAge.text())
        emp=Employee(id,name,age)
        if self.selectedEmployee==None:
            self.employees.append(emp)
            self.selectedEmployee=emp
        else:
            index=self.employees.index(self.selectedEmployee)
            self.selectedEmployee=emp
            self.employees[index]=self.selectedEmployee
        self.model.layoutChanged.emit()
    def processSelection(self):
        indexes=self.listViewEmployee.selectedIndexes()
        if indexes:
            row=indexes[0].row()
            emp=self.employees[row]
            self.lineEditId.setText(str(emp.id))
            self.lineEditName.setText(emp.name)
            self.lineEditAge.setText(str(emp.age))
            self.selectedEmployee=emp
    def processDelete(self):
        dlg = QMessageBox(self.MainWindow)
        if self.selectedEmployee == None:
            dlg.setWindowTitle("Deleteing error")
            dlg.setIcon(QMessageBox.Icon.Critical)
            dlg.setText("You have to select a Product to delete")
            dlg.exec()
            return
        dlg.setWindowTitle("Confirmation Deleting")
        dlg.setText("Are you sure you want to delete?")
        buttons = QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
        dlg.setStandardButtons(buttons)
        button = dlg.exec()
        if button == QMessageBox.StandardButton.Yes:
            self.employees.remove(self.selectedEmployee)
            self.model.layoutChanged.emit()
            self.selectedEmployee = None
            self.processNew()
    def processExportJson(self):
        # setup for QFileDialog
        filters = "Dataset (*.json);;All files(*)"
        filename, selected_filter = QFileDialog.getSaveFileName(
            self.MainWindow,
            filter=filters,
        )
        self.fileFactory.writeData(filename,self.employees)
    def processImportJson(self):
        # setup for QFileDialog
        filters = "Dataset (*.json);;All files(*)"
        filename, selected_filter = QFileDialog.getOpenFileName(
            self.MainWindow,
            filter=filters,
        )
        arr=self.fileFactory.readData(filename,Employee)

        self.employees.clear()

        for i in range(len(arr)):
            self.employees.append(arr[i])
        self.model.layoutChanged.emit()
    def show(self):
        self.MainWindow.show()

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

from PyQt6.QtWidgets import QApplication, QMainWindow

from MainWindowEx import MainWindowEx

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

Thực thi chương trình ta có kết quả như mong muốn:

Như vậy Tui đã trình bày chi tiết và kỹ lưỡng xong phần ứng dụng kiến trúc Model View để xử lý dữ liệu với QListView. Hướng dẫn các bạn cách dùng QMenuBar, QMenu, QAction cũng như các signal triggered tương ứng, đã minh họa đầy đủ và chi tiết các chức năng xem, thêm, sửa, xóa dữ liệu. Các bạn chú ý lập trình lại bài này nhiều lần để ứng dụng vào triển khai dự án trong thực tế.

Mã nguồn của dự án các bạn tải ở đây:

https://www.mediafire.com/file/e40x1sw76s71jxl/LearnModelViewPart2.rar/file

Bài học sau Tui sẽ tiếp tục trình bày ứng dụng Model View vào QTableView để hiển thị và tương tác dữ liệu dạng bảng, một trong những kỹ thuật rất quan trọng và hữu ích. Các bạn chú ý theo dõi

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

Bài 26: Kiến trúc Model View-PyQt6–Part 1

Trong lập trình xử lý tương tác dữ liệu có rất nhiều kỹ thuật, các bài học trước ta dùng các kỹ thuật thông thường và trực tiếp tương tác các đối tượng trên các Widget. Ngoài ra các kỹ sư đã phát triển kiến trúc liên quan tới Model View Controller, hay thường được gọi tắt là mô hình MVC để việc tương tác giao diện người dùng với dữ liệu cũng như các xử lý nghiệp vụ được dễ dàng hơn, dễ tái sử dụng hơn. PyQt6 cũng tích hợp mô hình MVC này và nó được gọi là Qt’s Model/Views Architecture. Chúng ta cùng điểm qua mô hình này:

Các bạn có thể hiểu nôm na mô hình Model View Controller nó hoạt động như sau:

  • Model: Nắm giữ cấu trúc dữ liệu trong ứng dụng mà nó đang làm việc. Ví dụ danh sách dữ liệu hướng đối tượng Product, Employee, Customer, Order, Provider… Khi người sử dụng thao tác trên View thì nó sẽ thông qua Controller để truy suất tới các Model tương ứng, các Model này sẽ hiển thị lên View thông qua Controller, có thể hiểu Controller là trung gian. Ví dụ trên Giao diện bạn muốn xem Danh sách PRODUCT, thì nó sẽ truyền tín hiện cho Controller PRODUCT, và controller này sẽ chọn đúng mô hình đối tượng PRODUCT, Model sẽ tạo ra danh sách Product và trả về cho Controller, sau đó Controller đẩy danh sách Product này lên View.
  • View: Là thành phần để hiển thị giao diện người dùng, nó cũng có nhiệm vụ nhận tương tác của người dùng và gọi Controller tương ứng. Nhiều View khác nhau có thể cùng hiển thị một loại dữ liệu, ví dụ bạn có 1 QTableWidget, 1 QListWidget, 1 QComboBox…các Widgets này có thể cùng hiển thị 1 danh sách model Product. Do đó ở đây bạn thấy tính tái sử dụng model rất cao.
  • Controller: Là thành phần trung gian, nó sẽ nhận các tương tác người dùng trên View, sau đó truyền đổi các tương tác này thành các lệnh để tương tác với Model để nhận các model tương ứng rồi nó sẽ hiển thị kết quả lên View hoặc tương tác với Model khác.

Với PyQt thì các kỹ sư có sự cải biên một xíu, đặc biệt là sự phân biệt giữa Model và View đôi khi nó khá mờ nhạt, khó nhận ra. Đồng thời View và Controller được hợp nhất lại với nhau để tao ra kiến trúc Model/ViewController hay nói tắt là Model View. PyQt chấp nhận các sự kiện từ người dùng thông qua hệ đều hành và ủy quyền những sự kiện này cho các widget để xử lý. Cái hay của Model View đó là giảm bớt sự cồng kềnh:

  • Model nắm giữ dữ liệu hoặc tham chiếu dữ liệu và trả về đối tượng đơn hoặc danh sách đối tượng. Khi model được thanh đổi dữ liệu nó sẽ ngay lập tức cập nhật lên giao diện
  • View sẽ yêu cầu dữ liệu từ Model và hiển thị lên các Widget tương ứng
  • Cơ chế Delegate giúp chương trình cập nhật dữ liệu mô hình tương ứng lên View một cách tự động.

Bài minh họa dưới đây, Chúng Ta sẽ làm một dự án hiển thị danh sách Employee đơn giản lên QListView sử dụng kiến trúc Model View của PyQt:

Ở hình trên Tui có minh họa là Chúng ta tạo một mô hình lớp Employee, và dùng kiến trúc Model View để hiển thị danh sách Employee lên QListView.

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

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

  • Employee.py là mô hình lớp hướng đối tượng định nghĩa cấu trúc dữ liệu cho Employee gồm id, name, age
  • EmployeeModel.py là lớp model view cho Employee, nó kế thừa từ “QAbstractListModel” lớp này sẽ lưu trữ danh sách dữ liệu mô hình Employee và hiển thị lên QListView
  • MainWindow.ui là màn hình giao diện được thiết kế bằng Qt Designer
  • MainWindow.py là lớp Generate Python của MainWindow.ui
  • MainWindowEx.py là lớp mã lệnh xử lý sự kiện người dùng cũng như lắp ráp dữ liệu trong mô hình Model View
  • MyApp.py là lớp thực thi chương trình

Bước 2: Tạo lớp Employee.py có mã lệnh như dưới đây:

class Employee:
    def __init__(self,id,name,age):
        self.id=id
        self.name=name
        self.age=age
    def __str__(self):
        return str(self.id)+"-"+self.name+"-"+str(self.age)

Lớp Employee có constructor nhận các đối số id, name, age và nó là các thuộc tính của Employee.

đồng thời hàm __str__ Tui thiết kế để nó tự động hiển thị lên giao diện như mong muốn, bạn có thể sửa lại.

Bước 3: Tạo lớp mô hình “EmployeeModel.py” và có mã lệnh như dưới đây:

from PyQt6.QtCore import QAbstractListModel, Qt

class EmployeeModel(QAbstractListModel):
    def __init__(self,employees=None):
        super().__init__()
        self.employees=employees
    def data(self, index, role):
        if role == Qt.ItemDataRole.DisplayRole:
            emp = self.employees[index.row()]
            return str(emp)
    def rowCount(self, index):
        return len(self.employees)

Lớp EmployeeModel sẽ kế thừa từ lớp QAbstractListModel, chúng ta cần overridfe 3 hàm (bắt buộc):

  • hàm __init__ để khởi tạo các biến ban đầu cho model, ở đây Tui thiết kế cho nó nhận vào một danh sách Employee
  • hàm data có 2 đối số chính, đối số index (Có kiểu QIndex) là vị trí của dòng dữ liệu sẽ hiển thị lên QListView, do đó ra sẽ lấy index.row() để trả về integer để truy suất chính xác vị trí của dữ liệu trong danh sách employees. Ở đây chúng ta sử dụng Enum Qt.ItemDataRole để điều hướng cách thức hiển thị dữ liệu, Tui sẽ nó chi tiết ở bài học tiếp theo, trong bài này Chúng ta quan tâm tới DisplayRole có nghĩa là enum dùng để hiển thị dữ liệu. Hàm này dùng str(emp) để nó tự động gọi hàm __str__ mà ta thiết kế trong lớp Employee.py
  • hàm rowCount trả về số phần tử trong danh sách, bắt buộc chúng ta phải Override hàm này.

Bước 4: Thiết kế giao diện MainWindow.ui bằng Qt Designer

Ta chỉ cần kéo thả ListView trong nhóm Model-Based vào giao diện là xong.

Bước 5: Generate Python code cho MainWindow.ui

# Form implementation generated from reading ui file 'MainWindow.ui'
#
# Created by: PyQt6 UI code generator 6.5.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(351, 251)
        self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.listView = QtWidgets.QListView(parent=self.centralwidget)
        self.listView.setGeometry(QtCore.QRect(10, 10, 311, 201))
        self.listView.setObjectName("listView")
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 351, 22))
        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 - Model View"))

Bước 6: Tạo MainWindowEx để xử lý sự kiện, gán model cho widget

from Employee import Employee
from EmployeeModel import EmployeeModel
from MainWindow import Ui_MainWindow


class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        pass
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow
        employees=[]
        employees.append(Employee(1,"John",37))
        employees.append(Employee(2, "Peter", 24))
        employees.append(Employee(3, "Tom", 25))
        self.model=EmployeeModel(employees)
        self.listView.setModel(self.model)
    def show(self):
        self.MainWindow.show()

Ta thấy trong hàm override setupUi, Tui tạo ra 3 đối tượng employee và lưu trữ vào danh sách employees, sau đó nó được đưa vào EmployeeModel(employees)

cuối cùng ta gán model này cho QListVIew bằng hàm: setModel(model)

Bước 7: Viết MyApp.py để thực thi chương trình

from PyQt6.QtWidgets import QApplication, QMainWindow

from MainWindowEx import MainWindowEx

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

Thực thi MyApp.py ta có kết quả như mong muốn.

Như vậy là tới đây Tui đã trình bày xong mô hình MVC, Qt Model/ViewController. Các bạn biết được lợi ích của mô hình và đặc biệt triển khai được một ví dụ minh họa cụ thể bằng Model View Architecture để hiển thị danh sách Employee lên QListView.

Sourcode đầy đủ của bài Model View này các bạn tải ở đây:

https://www.mediafire.com/file/7p75di7gwokzpa6/LearnModelViewPart1.rar/file

Bài học sau Tui tiếp tục trình bày chi tiết và chuyên sâu về model trong QListView, cung cấp thêm các chức năng: Thêm, Sửa, Xóa trên model. Các bạn chú ý theo dõi

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

Bài 25: Xử lý dữ liệu dạng bảng- QTableWidget và SQLite database–Part 4

Trong bài này Tui sẽ tiếp tục trình bày chi tiết về SQLite database trong Python với framework PyQt6. Các bạn sẽ tự tay tạo ra một cơ sở dữ liệu SQLite database bằng DB Browswer, rồi từ PyQt6 viết các mã lệnh Python để: Xem, thêm, sửa, xóa dữ liệu. Giao diện phần mềm của bài này mà chúng ta xây dựng sẽ như dưới đây:

  • Tạo Cơ sở dữ liệu SQLite bằng DB Browser, thiết kế các bảng dữ liệu có cột Id Primary Key là Auto Increment.
  • Viết chức năng đọc toàn bộ dữ liệu trong CSDL SQLite lên giao diện QTableWidget trong mục List Products
  • Xử lý sự kiện người dùng chọn các dòng dữ liệu trên QTableWidget và hiển thị thông tin chi tiết xuống phần Product Details
  • Chức năng “New” sẽ xóa dữ liệu đang nhập ở các ô QLineEdit và focus tới ô Product code
  • Chức năng “Save”, viết mã lệnh để chương trình tự xử lý 2 trường hợp là lưu mới dữ liệu xuống SQLite hoặc là lưu cập nhập xuống SQLite
  • Chức năng “Remove”, viết mã lệnh để xóa dòng dữ liệu đang chọn, cho người dùng xác thực trước khi xóa.

Chúng ta thực hiện chi tiết từng bước như dưới đây:

Bước 1: Tạo một dự án tên “LearnQTableWidgetPart4

  • Thư mục “database” sẽ lưu trữ SQLite “MyDatabase.sqlite” mà ta sẽ làm chi tiết ở bước sau.
  • Thư mục “images” lưu trữ các hình ảnh, icon của phần mềm
  • File “MainWindow.ui” là file giao diện thiết kế bằng Qt Designer
  • File “MainWindow.py” là file Generate Python code
  • File “MainWindowEx.py” là file mã lệnh kế thừa từ MainWindow.py để xử lý các nghiệp vụ phần mềm mà không lệ thuộc vào giao diện có thay đổi hay không trong tương lai
  • File “MyApp.py” là file thực thi phần mềm

Bước 2: Thiết kế cơ sở dữ liệu SQLite, đặt tên “MyDatabase.sqlite”

Từ phần mềm DB Browser (đã học ở bài 24), Các bạn chọn “New Database

Sau đó chọn nơi lưu trữ, ta lưu vào thư mục “database” trong bước 1.

Đặt tên “MyDatabase.sqlite” rồi bấm “Save” lúc này màn hình tạo Table sẽ hiển thị ra như dưới đây:

Trong mục Table ta đặt tên, rồi nhấn vào nút “Add” để thêm các thuộc tính. Ví dụ ta tạo bảng User như dưới đây:

  • Mỗi thuộc tính nó sẽ có kiểu dữ liệu tương ứng, ở trên ta thấy thuộc tính Id Tui chọn Type là INTEGER và tick vào PK(Primary Key) và AI (Auto Increment) là khóa chính tự động tăng.
  • Thuộc tính UserName, Password có type là TEXT

Sau khi tạo xong các thuộc tính ta nhấn nút OK và xem kết quả:

Tiếp theo ta nhập một vài dữ liệu mẫu cho bảng User này bằng cách nhấn vào thẻ “Browse Data”:

Để thêm dữ liệu cho bảng thì bấm vào biểu tượng New Record mà Tui tô khung đỏ ở trên, sau đó nhập dữ liệu và các dòng và cột tương ứng ở chỗ mũi tên màu đỏ.

Tương tự như thế, ta tạo bảng tiếp theo có tên “Product” bằng cách bấm vào biểu tượng “Create Table” trong thẻ “Database Structure”:

Ta thiết kế bảng Product như dưới đây:

Tương tự như bảng User, bảng Product Tui cũng cấu hình Id là cột khóa chính (PK) và tự động tăng (AI – Auto Increment)

  • cột ProductCode để lưu mã Product có kiểu TEXT
  • cột productName để lưu tên Product có kiểu TEXT
  • và cuối cùng là cột UnitPrice để lưu giá Product có kểu REAL

sau khi cấu hình xong thì bấm OK , ta có kết quả:

  • Tương tự như bảng User ta nhập một số dữ liệu mẫu ban đầu cho Product:

Ta nhấn CTRL+S để lưu sự thay đổi của SQLITE mà ta mới cấu hình.

  • Bảng User Tui sẽ không hướng dẫn code truy vấn. Bài này là bài tập các bạn cần thiết kết màn hình đăng nhập, nếu đăng nhập thành công thì vào màn hình quản lý sản phẩm
  • Bảng Product Tui sẽ hướng dẫn chi tiết để quản lý sản phẩm: Xem, thêm, sửa, xóa….

Bước 3: Thiết kế giao diện “MainWindow.ui” bằng Qt Designer được tích hợp trong Pycharm mà Tui đã hướng dẫn ở những bài đầu tiên của chuỗi bài học.

Các bạn kéo thả các Widget và cấu hình cũng như đặt tên cho các Widget như hình trên.

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

# 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(491, 481)
        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.groupBox = QtWidgets.QGroupBox(parent=self.centralwidget)
        self.groupBox.setGeometry(QtCore.QRect(20, 360, 461, 71))
        self.groupBox.setStyleSheet("background-color: rgb(241, 255, 202);")
        self.groupBox.setObjectName("groupBox")
        self.pushButtonRemove = QtWidgets.QPushButton(parent=self.groupBox)
        self.pushButtonRemove.setGeometry(QtCore.QRect(340, 20, 101, 41))
        self.pushButtonRemove.setStyleSheet("background-color: rgb(255, 170, 255);")
        icon1 = QtGui.QIcon()
        icon1.addPixmap(QtGui.QPixmap("images/ic_delete.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.pushButtonRemove.setIcon(icon1)
        self.pushButtonRemove.setIconSize(QtCore.QSize(32, 32))
        self.pushButtonRemove.setObjectName("pushButtonRemove")
        self.pushButtonNew = QtWidgets.QPushButton(parent=self.groupBox)
        self.pushButtonNew.setGeometry(QtCore.QRect(40, 20, 93, 41))
        self.pushButtonNew.setStyleSheet("background-color: rgb(255, 170, 255);")
        icon2 = QtGui.QIcon()
        icon2.addPixmap(QtGui.QPixmap("images/ic_new.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.pushButtonNew.setIcon(icon2)
        self.pushButtonNew.setIconSize(QtCore.QSize(32, 32))
        self.pushButtonNew.setObjectName("pushButtonNew")
        self.pushButtonSave = QtWidgets.QPushButton(parent=self.groupBox)
        self.pushButtonSave.setGeometry(QtCore.QRect(190, 20, 101, 41))
        self.pushButtonSave.setStyleSheet("background-color: rgb(255, 170, 255);")
        icon3 = QtGui.QIcon()
        icon3.addPixmap(QtGui.QPixmap("images/ic_save.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.pushButtonSave.setIcon(icon3)
        self.pushButtonSave.setIconSize(QtCore.QSize(32, 32))
        self.pushButtonSave.setObjectName("pushButtonSave")
        self.groupBox_2 = QtWidgets.QGroupBox(parent=self.centralwidget)
        self.groupBox_2.setGeometry(QtCore.QRect(20, 240, 461, 111))
        self.groupBox_2.setStyleSheet("background-color: rgb(234, 254, 255);")
        self.groupBox_2.setObjectName("groupBox_2")
        self.lineEditUnitPrice = QtWidgets.QLineEdit(parent=self.groupBox_2)
        self.lineEditUnitPrice.setGeometry(QtCore.QRect(110, 80, 341, 22))
        self.lineEditUnitPrice.setStyleSheet("background-color: rgb(241, 255, 202);")
        self.lineEditUnitPrice.setObjectName("lineEditUnitPrice")
        self.lineEditProductCode = QtWidgets.QLineEdit(parent=self.groupBox_2)
        self.lineEditProductCode.setGeometry(QtCore.QRect(110, 20, 341, 22))
        self.lineEditProductCode.setStyleSheet("background-color: rgb(241, 255, 202);")
        self.lineEditProductCode.setObjectName("lineEditProductCode")
        self.lineEditProductName = QtWidgets.QLineEdit(parent=self.groupBox_2)
        self.lineEditProductName.setGeometry(QtCore.QRect(110, 50, 341, 22))
        self.lineEditProductName.setStyleSheet("background-color: rgb(241, 255, 202);")
        self.lineEditProductName.setObjectName("lineEditProductName")
        self.label_2 = QtWidgets.QLabel(parent=self.groupBox_2)
        self.label_2.setGeometry(QtCore.QRect(10, 50, 91, 16))
        self.label_2.setObjectName("label_2")
        self.label = QtWidgets.QLabel(parent=self.groupBox_2)
        self.label.setGeometry(QtCore.QRect(10, 20, 81, 16))
        self.label.setObjectName("label")
        self.label_3 = QtWidgets.QLabel(parent=self.groupBox_2)
        self.label_3.setGeometry(QtCore.QRect(10, 80, 91, 16))
        self.label_3.setObjectName("label_3")
        self.groupBox_3 = QtWidgets.QGroupBox(parent=self.centralwidget)
        self.groupBox_3.setGeometry(QtCore.QRect(20, 30, 461, 201))
        self.groupBox_3.setStyleSheet("background-color: rgb(241, 255, 202);")
        self.groupBox_3.setObjectName("groupBox_3")
        self.verticalLayout = QtWidgets.QVBoxLayout(self.groupBox_3)
        self.verticalLayout.setObjectName("verticalLayout")
        self.tableWidgetProduct = QtWidgets.QTableWidget(parent=self.groupBox_3)
        self.tableWidgetProduct.setStyleSheet("background-color: rgb(255, 255, 255);")
        self.tableWidgetProduct.setObjectName("tableWidgetProduct")
        self.tableWidgetProduct.setColumnCount(4)
        self.tableWidgetProduct.setRowCount(0)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetProduct.setHorizontalHeaderItem(0, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetProduct.setHorizontalHeaderItem(1, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetProduct.setHorizontalHeaderItem(2, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetProduct.setHorizontalHeaderItem(3, item)
        self.verticalLayout.addWidget(self.tableWidgetProduct)
        self.label_4 = QtWidgets.QLabel(parent=self.centralwidget)
        self.label_4.setGeometry(QtCore.QRect(70, 0, 341, 31))
        font = QtGui.QFont()
        font.setPointSize(15)
        font.setBold(True)
        font.setItalic(True)
        font.setWeight(75)
        self.label_4.setFont(font)
        self.label_4.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
        self.label_4.setObjectName("label_4")
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 491, 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 - SQLite"))
        self.groupBox.setTitle(_translate("MainWindow", "Action"))
        self.pushButtonRemove.setText(_translate("MainWindow", "Remove"))
        self.pushButtonNew.setText(_translate("MainWindow", "New"))
        self.pushButtonSave.setText(_translate("MainWindow", "Save"))
        self.groupBox_2.setTitle(_translate("MainWindow", "Product Detail:"))
        self.label_2.setText(_translate("MainWindow", "Product Name:"))
        self.label.setText(_translate("MainWindow", "Product Code:"))
        self.label_3.setText(_translate("MainWindow", "Unit Price:"))
        self.groupBox_3.setTitle(_translate("MainWindow", "List Products:"))
        item = self.tableWidgetProduct.horizontalHeaderItem(0)
        item.setText(_translate("MainWindow", "Id"))
        item = self.tableWidgetProduct.horizontalHeaderItem(1)
        item.setText(_translate("MainWindow", "Product Code"))
        item = self.tableWidgetProduct.horizontalHeaderItem(2)
        item.setText(_translate("MainWindow", "Product Name"))
        item = self.tableWidgetProduct.horizontalHeaderItem(3)
        item.setText(_translate("MainWindow", "Unit Price"))
        self.label_4.setText(_translate("MainWindow", "Product - SQLite"))

Bước 5: Viết file mã lệnh “MainWindowEx.py” kế thừa từ “MainWindow.py” để xử lý sự kiện người dùng, cũng như không bị lệ thuộc vào giao diện trong tương lai thay đổi mà phải generate lại mã nguồn giao diện.

from PyQt6.QtSql import QSqlDatabase, QSqlTableModel, QSqlRecord
from PyQt6.QtWidgets import QTableWidgetItem, QMessageBox
from MainWindow import Ui_MainWindow

class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        self.databasePath="database/MyDatabase.sqlite"
        self.selectedRecord=None
        self.selectedRow=None

Trong MainWindowEx, Tui định nghĩa 3 biến:

  • databasePath để lưu trữ đường dẫn tới SQLite mà ta thiết kế
  • selectedRecord là biến lưu trữ đối tượng QSqlRecord đang chọn để hỗ trợ cho việc Lưu mới hay cập nhật dữ liệu tiện lợi nhất
  • selectedRow là biến lưu trữ dòng hiện tại đang chọn (index) để hỗ trợ cho việc Lưu mới hay cập nhật dữ liệu tiện lợi nhất

Hàm setupUi được override để định mặc định gọi các kết nối và hiển thị dữ liệu danh sách Product lên QTableWidget, cũng như gán các signal để xử lý sử kiện người dùng:

def setupUi(self, MainWindow):
    super().setupUi(MainWindow)
    self.MainWindow=MainWindow
    self.connectDatabase()
    self.loadProduct()
    self.pushButtonNew.clicked.connect(self.processNew)
    self.tableWidgetProduct.itemSelectionChanged.connect(self.processItemSelection)
    self.pushButtonSave.clicked.connect(self.processSave)
    self.pushButtonRemove.clicked.connect(self.processRemove)

Hàm connectDatabase sẽ gọi các kết nối tới cơ sở dữ liệu SQLite:

def connectDatabase(self):
    # create QSqlDatabase object
    self.db = QSqlDatabase("QSQLITE")
    # set the database selected path
    self.db.setDatabaseName(self.databasePath)
    # Open the SQLite database
    self.db.open()
    # Create QSqlTableModel object, and self.db is assigned
    self.model = QSqlTableModel(db=self.db)

Đối tượng QSqlitableModel được kích hoạt và được giao cho biến model quản lý

biến model này sẽ trỏ tới bất kỳ bảng dữ liệu nào mà ta mong muốn truy suất.

Hàm loadProduct để truy vấn toàn bộ dữ liệu trong bảng Product và hiển thị lên QTableWidget:

def loadProduct(self):
    # select table name to invoke data
    tableName = "Product"
    self.model.setTable(tableName)
    # active for selecting data
    self.model.select()
    # reset QTableWidget to 0 row
    self.tableWidgetProduct.setRowCount(0)
    # loop for insert new row:
    for i in range(self.model.rowCount()):
        # insert new row:
        self.tableWidgetProduct.insertRow(i)
        # get a record with i index:
        record = self.model.record(i)
        itemId = QTableWidgetItem(str(record.value(0)))
        itemProductCode = QTableWidgetItem(str(record.value(1)))
        itemProductName = QTableWidgetItem(str(record.value(2)))
        itemUnitPrice = QTableWidgetItem(str(record.value(3)))
        self.tableWidgetProduct.setItem(i, 0, itemId)
        self.tableWidgetProduct.setItem(i, 1, itemProductCode)
        self.tableWidgetProduct.setItem(i, 2, itemProductName)
        self.tableWidgetProduct.setItem(i, 3, itemUnitPrice)

hàm processNew để xóa toàn bộ dữ liệu trong QLineEdit và focus tới ô Code để hỗ trợ nhập liệu nhanh chóng. Đồng thời các biến selectedRecord và selectedRow cũng được reset về None để đánh dấu rằng khi nhấn “Save” là lưu mới 1 record:

def processNew(self):
    self.lineEditProductCode.setText("")
    self.lineEditProductName.setText("")
    self.lineEditUnitPrice.setText("")
    self.lineEditProductCode.setFocus()
    self.selectedRecord=None
    self.selectedRow=None

Hàm processItemSelection sẽ xử lý sự kiện người dùng chọn từng dòng trên QTableWidget, nó truy vấn dữ liệu trong model và hiển thị lên phần Product Details:

def processItemSelection(self):
    #Get current row index on the QTableWidget
    self.selectedRow=self.tableWidgetProduct.currentRow()
    if self.selectedRow==-1:
        return
    #call record(index) method from model
    self.selectedRecord=self.model.record(self.selectedRow)
    #Get detail information from QSqlRecord
    #id=self.selectedRecord.value(0)
    productCode=self.selectedRecord.value(1)
    productName=self.selectedRecord.value(2)
    unitPrice=self.selectedRecord.value(3)
    # show detail information into the QLineEdit
    self.lineEditProductCode.setText(productCode)
    self.lineEditProductName.setText(productName)
    self.lineEditUnitPrice.setText(str(unitPrice))

Hàm processSave sẽ thực hiện 2 tác vụ: Lưu mới và lưu cập nhật, nếu selectedRecord là None thì lưu mới, còn selectedRecord là khác None thì lưu cập nhật:

def processSave(self):
    #Get lasted row
    row = self.model.rowCount()
    if self.selectedRecord==None:#if new product
        #Get the QSqlRecord from record(row)
        record=self.model.record(row)
        #assign the value for QSqlRecord
        #record.setValue(0, None)
        record.setValue(1,self.lineEditProductCode.text())
        record.setValue(2, self.lineEditProductName.text())
        record.setValue(3, float(self.lineEditUnitPrice.text()))
        #call the insertRecord for storing a new record into SQLite
        result=self.model.insertRecord(row,record)
        #if saving successful then result =True
        if result==True:
            #save the lasted record and reload products
            self.selectedRecord=record
            self.selectedRow=row
            self.loadProduct()
    else:#if updating the QSqlRecord
        # assign the value for QSqlRecord
        self.selectedRecord.setValue(1, self.lineEditProductCode.text())
        self.selectedRecord.setValue(2, self.lineEditProductName.text())
        self.selectedRecord.setValue(3, float(self.lineEditUnitPrice.text()))
        # call the updateRowInTable for updating selected record into SQLite
        result=self.model.updateRowInTable(self.selectedRow,self.selectedRecord)
        # if saving successful then result =True
        if result == True:
            #reload products
            self.loadProduct()
  • hàm insertRecord(row,record) để lưu mới, nếu lưu thành công thì nó trả kết quả về là True
  • hàm updateRowInTable(row,record) để lưu cập nhật, nếu lưu thành công thì nó trả kết quả về là True

Khi lưu thành công thì chương trình sẽ nạp lại dữ liệu lên QTableWidget.

Hàm processRemove dùng để xóa QSqlRecord đang chọn trên QTableWidget:

def processRemove(self):
    dlg = QMessageBox(self.MainWindow)
    if self.selectedRecord == None:
        dlg.setWindowTitle("Deleteing error")
        dlg.setIcon(QMessageBox.Icon.Critical)
        dlg.setText("You have to select a Product to delete")
        dlg.exec()
        return
    dlg.setWindowTitle("Confirmation Deleting")
    dlg.setText("Are you sure you want to delete?")
    buttons = QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
    dlg.setStandardButtons(buttons)
    button = dlg.exec()
    if button == QMessageBox.StandardButton.Yes:
        #call removeRow method to remove QSqlRecord from the SQLite
        result=self.model.removeRow(self.selectedRow)
        # if saving successful then result =True
        if result == True:
            # save the lasted record and reload products
            self.loadProduct()
            self.processNew()
  • Tui coding dùng QMessageBox để hiển thị cửa sổ xác nhận có muốn xóa hay không
  • Hàm removeRow(row) dùng để xóa dòng dữ liệu đang chọn ra khỏi bảng. Nếu xóa thành công thì kết quả trả về là True, lúc này ta nạp lại dữ liệu lên giao diện QTableWidget, đồng thời gọi hàm processNew để xóa dữ liệu trong QLineEdit đi

Dưới đây là coding đầy đủ của chương trình:

from PyQt6.QtSql import QSqlDatabase, QSqlTableModel, QSqlRecord
from PyQt6.QtWidgets import QTableWidgetItem, QMessageBox
from MainWindow import Ui_MainWindow

class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        self.databasePath="database/MyDatabase.sqlite"
        self.selectedRecord=None
        self.selectedRow=None
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow
        self.connectDatabase()
        self.loadProduct()
        self.pushButtonNew.clicked.connect(self.processNew)
        self.tableWidgetProduct.itemSelectionChanged.connect(self.processItemSelection)
        self.pushButtonSave.clicked.connect(self.processSave)
        self.pushButtonRemove.clicked.connect(self.processRemove)

    def connectDatabase(self):
        # create QSqlDatabase object
        self.db = QSqlDatabase("QSQLITE")
        # set the database selected path
        self.db.setDatabaseName(self.databasePath)
        # Open the SQLite database
        self.db.open()
        # Create QSqlTableModel object, and self.db is assigned
        self.model = QSqlTableModel(db=self.db)

    def loadProduct(self):
        # select table name to invoke data
        tableName = "Product"
        self.model.setTable(tableName)
        # active for selecting data
        self.model.select()
        # reset QTableWidget to 0 row
        self.tableWidgetProduct.setRowCount(0)
        # loop for insert new row:
        for i in range(self.model.rowCount()):
            # insert new row:
            self.tableWidgetProduct.insertRow(i)
            # get a record with i index:
            record = self.model.record(i)
            itemId = QTableWidgetItem(str(record.value(0)))
            itemProductCode = QTableWidgetItem(str(record.value(1)))
            itemProductName = QTableWidgetItem(str(record.value(2)))
            itemUnitPrice = QTableWidgetItem(str(record.value(3)))
            self.tableWidgetProduct.setItem(i, 0, itemId)
            self.tableWidgetProduct.setItem(i, 1, itemProductCode)
            self.tableWidgetProduct.setItem(i, 2, itemProductName)
            self.tableWidgetProduct.setItem(i, 3, itemUnitPrice)

    def processNew(self):
        self.lineEditProductCode.setText("")
        self.lineEditProductName.setText("")
        self.lineEditUnitPrice.setText("")
        self.lineEditProductCode.setFocus()
        self.selectedRecord=None
        self.selectedRow=None

    def processItemSelection(self):
        #Get current row index on the QTableWidget
        self.selectedRow=self.tableWidgetProduct.currentRow()
        if self.selectedRow==-1:
            return
        #call record(index) method from model
        self.selectedRecord=self.model.record(self.selectedRow)
        #Get detail information from QSqlRecord
        #id=self.selectedRecord.value(0)
        productCode=self.selectedRecord.value(1)
        productName=self.selectedRecord.value(2)
        unitPrice=self.selectedRecord.value(3)
        # show detail information into the QLineEdit
        self.lineEditProductCode.setText(productCode)
        self.lineEditProductName.setText(productName)
        self.lineEditUnitPrice.setText(str(unitPrice))

    def processSave(self):
        #Get lasted row
        row = self.model.rowCount()
        if self.selectedRecord==None:#if new product
            #Get the QSqlRecord from record(row)
            record=self.model.record(row)
            #assign the value for QSqlRecord
            #record.setValue(0, None)
            record.setValue(1,self.lineEditProductCode.text())
            record.setValue(2, self.lineEditProductName.text())
            record.setValue(3, float(self.lineEditUnitPrice.text()))
            #call the insertRecord for storing a new record into SQLite
            result=self.model.insertRecord(row,record)
            #if saving successful then result =True
            if result==True:
                #save the lasted record and reload products
                self.selectedRecord=record
                self.selectedRow=row
                self.loadProduct()
        else:#if updating the QSqlRecord
            # assign the value for QSqlRecord
            self.selectedRecord.setValue(1, self.lineEditProductCode.text())
            self.selectedRecord.setValue(2, self.lineEditProductName.text())
            self.selectedRecord.setValue(3, float(self.lineEditUnitPrice.text()))
            # call the updateRowInTable for updating selected record into SQLite
            result=self.model.updateRowInTable(self.selectedRow,self.selectedRecord)
            # if saving successful then result =True
            if result == True:
                #reload products
                self.loadProduct()
    def processRemove(self):
        dlg = QMessageBox(self.MainWindow)
        if self.selectedRecord == None:
            dlg.setWindowTitle("Deleteing error")
            dlg.setIcon(QMessageBox.Icon.Critical)
            dlg.setText("You have to select a Product to delete")
            dlg.exec()
            return
        dlg.setWindowTitle("Confirmation Deleting")
        dlg.setText("Are you sure you want to delete?")
        buttons = QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
        dlg.setStandardButtons(buttons)
        button = dlg.exec()
        if button == QMessageBox.StandardButton.Yes:
            #call removeRow method to remove QSqlRecord from the SQLite
            result=self.model.removeRow(self.selectedRow)
            # if saving successful then result =True
            if result == True:
                # save the lasted record and reload products
                self.loadProduct()
                self.processNew()
    def show(self):
        self.MainWindow.show()

Bước 6: Cuối cùng ta tạo “MyApp.py” để thực thi chương trình:

from PyQt6.QtWidgets import QApplication, QMainWindow

from MainWindowEx import MainWindowEx

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

Chạy chương trình ta có kết quả như mong muốn:

Như vậy là tới đây các Bạn đã biết cách xây dựng một phần mềm hoàn chỉnh có tương tác SQLite từ khâu: Xem, thêm, sửa xóa. Cũng như ôn tập lại các signal, xử lý sự kiện QMessageBox…

Các bạn có thể áp dụng bài này vào các bài quản lý khác như quản lý Nhân viên, quản lý kho….

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

https://www.mediafire.com/file/mcvu25j1fibpliu/LearnQTableWidgetPart4.rar/file

Bài học tiếp theo Tui trình bày về ViewModel để hiển thị dữ liệu dạng Bảng nhưng nó cao cấp hơn. Các bạn chú ý theo dõi:

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

Bài 24: Xử lý dữ liệu dạng bảng- QTableWidget và SQLite database–Part 3

Trong bài 23 chúng ta đã thực hiện chi tiết các tác vụ trên QTableWidget như: Xem, thêm, sửa, xóa, mô hình hóa dữ liệu, serialize và deserialize dữ liệu với JSON ARRAY. Tuy nhiên, về vấn đề lưu trữ dữ liệu thường nó phức tạp, nó đòi hỏi rất nhiều bảng dữ liệu, mỗi bảng có rất nhiều trường dữ liệu và chúng thường có mối quan hệ khá chặt chẽ. Ví dụ như bạn muốn phát triển một phần mềm quản lý bán hàng thì nó cần các bảng dữ liệu có mối quan hệ như: Danh mục sản phẩm, sản phẩm, hóa đơn, chi tiết hóa đơn, khách hàng, nhân viên, nhà cung cấp, nhà vận chuyển… Do đó khi phát triển phần mềm thường chúng ta nghĩ tới cơ sở dữ liệu có thể đáp ứng nhu cầu lưu trữ phức tạp này. Chẳng hạn như Microsoft SQL Server, MY SQL, MongoDB, SQLite…

Trong bài này Tui sẽ hướng dẫn cách sử dụng SQLite để lưu trữ và xử dữ liệu, cũng như trình bày một số kỹ thuật để kết nối và truy vấn dữ liệu từ SQLite lên QTableWidget. Tui sẽ cung cấp một số cơ sở dữ liệu SQLite mẫu, và viết các mã lệnh để tự động kết nối các cơ sở dữ liệu này cũng như tự động đọc danh sách các bảng trong cơ sở dữ liệu và truy vấn danh sách dữ liệu trong bảng lên giao diện QTableWidget. Bài học kế tiếp Tui sẽ trình bày các thao tác Thêm, Sửa, Xóa vào SQLite Database.

Các chức năng chính của phần mềm bao gồm:

  • Cho người dùng lựa chọn một cơ sở dữ liệu SQLite bất kỳ để kết nối
  • Chương trình sẽ tự động đọc danh sách các Bảng dữ liệu nằm bên trong SQLite
  • Người dùng chọn Bảng dữ liệu nào thì chương trình sẽ truy vấn các dữ liệu ở bên trong Bảng này lên giao diện QTableWidget
  • Cung cấp chức năng Fetch More để tiếp tục đọc các dữ liệu trong Bảng trong trường hợp bảng có nhiều dòng dữ liệu (Ví dụ lớn hơn 256 dòng dữ liệu)

Ta tiến hành thực hiện chương trình nhé:

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

  • Thư mục “databases” chứa một số SQLite mẫu, người sử dụng sẽ lựa chọn tùy ý các SQLite để hiển thị lên QTableWidget
  • Thư mục “images” chưa hình ảnh, icon
  • “MainWindow.ui” là file giao diện thiết kế sử dụng Qt Designer
  • “MainWindow.py” là file generate python code từ giao diện MainWindow.ui
  • “MyApp.py” là file mã lệnh để thực thi chương trình

Bước 2: Làm quen với cơ sở dữ liệu SQLite.

Trong dự án có thư mục “databases”, Tui upload ở đây các bạn tải về sử dụng:

https://www.mediafire.com/file/8pd05w41bs2bbpl/databases.rar/file

Trong thư mục này có nhiều cơ sở dữ liệu mẫu, các bạn có thể ứng dụng để triển khai các phần mềm như: Karaoke, từ điển Anh Việt, quản lý bán hàng âm nhạc….

Có nhiều công cụ để mở các Cơ sở dữ liệu SQLite để thao tác, trong đó có SQLite DB Browser, các bạn tải ở link https://sqlitebrowser.org/dl/

Sau khi cài đặt DB Browser thành công, các bạn chạy phần mềm này lên và mở một cơ sở dữ liệu SQLite bất kỳ, ví dụ “Chinook_Sqlite.sqlite”:

Trong phần mềm DB Browser, bạn bấm chọn “Open Database” và trỏ tới cơ sở dữ liệu “Chinook_Sqlite.sqlite”.

Thẻ “Database Structure” sẽ hiển thị danh sách các bản trong cơ sở dữ liệu, ví dụ trong trường hợp này ta thấy có 11 bảng bao gồm: Album, Artist, Customer, Employee, Genre, Invoice, InvoiceLine, MediaType, Playlist, PlaylistTrack, Track

Bạn có thể bấm vào từng bảng để xem cấu trúc chi tiết:

  • Thẻ “Browse Data” để xem dữ liệu của từng bảng:

Trong thẻ “Browse Data” có combobox Table, ta có thể nhấn vào để chọn các Bảng để xem dữ liệu tương ứng của nó.

Bước 3: Thiết kế giao diện “MainWindow.ui” bằng Qt Designer, dĩ nhiên các bước này Tui đã trình bày rất kỹ lưỡng ở những bài học trước do đó Tui không có trình bày lại, mà các bạn cần phải học tuần tự để có khả năng tự thiết kế giao diện theo mục đích sử dụng riêng của mình:

Bạn kéo thả các Widget vào giao diện như hình minh họa, rồi đặt tên cho các Widget tương ứng như trong màn hình Object Inspector.

Sau đó lưu giao diện này lại vào dự án “LearnQTableWidgetPart3” với tên MainWindow.ui.

Bước 4: Dùng chức năng Generate Python code cho giao diện MainWindow.ui để tạo file mã nguồn “MainWindow.py”:

# 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(560, 511)
        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.label = QtWidgets.QLabel(parent=self.centralwidget)
        self.label.setGeometry(QtCore.QRect(30, 20, 101, 21))
        self.label.setObjectName("label")
        self.lineEditSQLite = QtWidgets.QLineEdit(parent=self.centralwidget)
        self.lineEditSQLite.setGeometry(QtCore.QRect(140, 20, 301, 22))
        self.lineEditSQLite.setObjectName("lineEditSQLite")
        self.pushButtonPickSQLite = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonPickSQLite.setGeometry(QtCore.QRect(450, 20, 93, 28))
        icon1 = QtGui.QIcon()
        icon1.addPixmap(QtGui.QPixmap("images/ic_pickdatabase.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.pushButtonPickSQLite.setIcon(icon1)
        self.pushButtonPickSQLite.setObjectName("pushButtonPickSQLite")
        self.label_2 = QtWidgets.QLabel(parent=self.centralwidget)
        self.label_2.setGeometry(QtCore.QRect(30, 60, 101, 21))
        self.label_2.setObjectName("label_2")
        self.cboTable = QtWidgets.QComboBox(parent=self.centralwidget)
        self.cboTable.setGeometry(QtCore.QRect(140, 60, 301, 22))
        self.cboTable.setObjectName("cboTable")
        self.pushButtonFetchMore = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonFetchMore.setGeometry(QtCore.QRect(30, 430, 111, 31))
        icon2 = QtGui.QIcon()
        icon2.addPixmap(QtGui.QPixmap("images/ic_fetchmore.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.pushButtonFetchMore.setIcon(icon2)
        self.pushButtonFetchMore.setObjectName("pushButtonFetchMore")
        self.tableWidget = QtWidgets.QTableWidget(parent=self.centralwidget)
        self.tableWidget.setGeometry(QtCore.QRect(30, 100, 511, 311))
        self.tableWidget.setObjectName("tableWidget")
        self.tableWidget.setColumnCount(0)
        self.tableWidget.setRowCount(0)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 560, 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 - QTableWidget - SQLite"))
        self.label.setText(_translate("MainWindow", "Choose SQLite:"))
        self.pushButtonPickSQLite.setText(_translate("MainWindow", "..."))
        self.label_2.setText(_translate("MainWindow", "Choose table:"))
        self.pushButtonFetchMore.setText(_translate("MainWindow", "Fetch More"))

Bước 5: Tạo file mã nguồn “MainWindowEx.py”, lớp này kế thừa từ lớp được Generate Python Code ở bước trước để xử lý các sự kiện người dùng, cũng như không bị ảnh hưởng mã lệnh khi trong tương lai giao diện thay đổi.

import os.path

from PyQt6.QtSql import QSqlDatabase, QSqlTableModel
from PyQt6.QtWidgets import QFileDialog, QTableWidgetItem, QMessageBox
from MainWindow import Ui_MainWindow

class MainWindowEx(Ui_MainWindow):
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow
        self.pushButtonPickSQLite.clicked.connect(self.processPickSQLite)
        self.cboTable.activated.connect(self.processSelectedTable)
        self.pushButtonFetchMore.clicked.connect(self.processFetchMore)

Hàm setupUi được override, và Ta tiến hành gán 3 signal tương ứng cho 3 Widget trên giao diện:

  • Signal “clicked” cho widget pushButtonPickSQLite với slot là “processPickSQLite” để chọn một cơ sở dữ liệu SQLite bất kỳ và hiển thị toàn bộ tên Bảng trong SQLite vừa chọn lên QComboBox.
  • Signal “activated” cho widget cboTable để chọn một bảng bất kỳ trong cơ sở dữ liệu và nạp dữ liệu của bảng lên QTableWidget
  • Signal “clicked” cho widget pushButtonFetchMore để đọc tiếp các dữ liệu còn trong Table (nếu dữ liệu nhiều hơn 256 dòng)

Dưới đây là chi tiết của từng slot(HÀM):

  • Hàm “processPickSQLite” hàm này sẽ hiển thị QFileDialog để người dùng chọn lựa Cơ sở dữ liệu SQLite bất kỳ:
def processPickSQLite(self):
    #setup for QFileDialog
    filters = "SQLite database (*.sqlite);;All files(*)"
    filename, selected_filter = QFileDialog.getOpenFileName(
        self.MainWindow,
        filter=filters,
    )
    #get selected file name and showing on the QLineEdit
    self.lineEditSQLite.setText(filename)
    #create base dir
    baseDir = os.path.dirname(__file__)
    #set the database path
    databasePath = os.path.join(baseDir, filename)
    #create QSqlDatabase object
    self.db = QSqlDatabase("QSQLITE")
    #set the database selected path
    self.db.setDatabaseName(databasePath)
    #Open the SQLite database
    self.db.open()
    #get all tables in the selected SQLite
    tables= self.db.tables()
    self.cboTable.clear()
    #show all the table names into the QCombobox:
    for i in range(len(tables)):
        tableName=tables[i]
        self.cboTable.addItem(tableName)

Sau khi kết nối cơ sở dữ liệu thành công, Tui có viết mã lệnh vòng lặp ở bên dưới cuối của hàm để nạp toàn bộ các tên bảng của cơ sở dữ liệu vừa chọn lựa lên QComboBox. Cách viết mã lệnh ở trên thì chương trình sẽ tự động đọc được các bảng của 1 cơ sở dữ liệu SQLite bất kỳ nên rất linh động.

  • Hàm “processSelectedTable” sẽ lắng nghe xem người sử dụng chọn Table nào trong QComboBox và sau đó chương trình sẽ nạp dữ liệu của Table này lên QTableWidget:
def processSelectedTable(self):
    #Get the current Table Name in QCombobox
    tableName=self.cboTable.currentText()
    #Create QSqlTableModel object, and self.db is assigned
    self.model = QSqlTableModel(db=self.db)
    #select table name to invoke data
    self.model.setTable(tableName)
    #active for selecting data
    self.model.select()
    #reset QTableWidget to 0 row
    self.tableWidget.setRowCount(0)
    #get the column count for selected Table as automatic
    self.columns=self.model.record().count()
    #set columns count for QTableWidget
    self.tableWidget.setColumnCount(self.columns)
    #create labels array for Columns Headers
    labels=[]
    for i in range(self.columns):
        #get column name:
        fieldName=self.model.record().fieldName(i)
        #store the column name
        labels.append(fieldName)
    #set the columns header with labels
    self.tableWidget.setHorizontalHeaderLabels(labels)
    #loop for insert new row:
    for i in range(self.model.rowCount()):
        #insert new row:
        self.tableWidget.insertRow(i)
        #get a record with i index:
        record=self.model.record(i)
        #loop column to get value for each cell:
        for j in range(self.columns):
            #create QTableWidgetItem object
            item=QTableWidgetItem(str(record.value(j)))
            #set value for each CELL:
            self.tableWidget.setItem(i,j,item)

Chương trình sẽ tự động đọc tất cả các Columns (attributes) của Bảng vừa chọn và tiến hành tạo các Columns Header cho QTableWidget. Sau đó nó sẽ đọc dữ liệu và nạp vào QTableWidget tương ứng với các cột mà nó đã khởi tạo.

Mã lệnh của hàm này hơi phức tạp, các bạn cố gắng đọc các comment mà Tui đã viết cho từng dòng lệnh ở trên.

Mặc định thì nó sẽ tải 256 dòng trước, vì vậy Tui bổ sung thêm hàm Fetch More để đọc tiếp các batch 256 tiếp theo:

  • Hàm “processFetchMore”:
def processFetchMore(self):
    #check if the model can fetch more:
    if self.model.canFetchMore():
        #set the i index for last rowcount:
        i=self.model.rowCount()
        #call fetchmore method:
        self.model.fetchMore()
        #loop for new batch data:
        for i in range(i,self.model.rowCount()):
            # insert new row:
            self.tableWidget.insertRow(i)
            # get a record with i index:
            record = self.model.record(i)
            # loop column to get value for each cell:
            for j in range(self.columns):
                # create QTableWidgetItem object
                item = QTableWidgetItem(str(record.value(j)))
                # set value for each CELL:
                self.tableWidget.setItem(i, j, item)
    else:
        msg=QMessageBox()
        msg.setText("No more records to fetch")
        msg.exec()

Mã lệnh trên Tui sẽ kiểm tra nếu còn dữ liệu trong model thì tiếp tục Fetch, fetch tới khi nào hết thì sẽ dùng QMessageBox để thông báo “No more recors to fetch”.

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

import os.path

from PyQt6.QtSql import QSqlDatabase, QSqlTableModel
from PyQt6.QtWidgets import QFileDialog, QTableWidgetItem, QMessageBox
from MainWindow import Ui_MainWindow

class MainWindowEx(Ui_MainWindow):
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow
        self.pushButtonPickSQLite.clicked.connect(self.processPickSQLite)
        self.cboTable.activated.connect(self.processSelectedTable)
        self.pushButtonFetchMore.clicked.connect(self.processFetchMore)
    def processPickSQLite(self):
        #setup for QFileDialog
        filters = "SQLite database (*.sqlite);;All files(*)"
        filename, selected_filter = QFileDialog.getOpenFileName(
            self.MainWindow,
            filter=filters,
        )
        #get selected file name and showing on the QLineEdit
        self.lineEditSQLite.setText(filename)
        #create base dir
        baseDir = os.path.dirname(__file__)
        #set the database path
        databasePath = os.path.join(baseDir, filename)
        #create QSqlDatabase object
        self.db = QSqlDatabase("QSQLITE")
        #set the database selected path
        self.db.setDatabaseName(databasePath)
        #Open the SQLite database
        self.db.open()
        #get all tables in the selected SQLite
        tables= self.db.tables()
        self.cboTable.clear()
        #show all the table names into the QCombobox:
        for i in range(len(tables)):
            tableName=tables[i]
            self.cboTable.addItem(tableName)
    def processSelectedTable(self):
        #Get the current Table Name in QCombobox
        tableName=self.cboTable.currentText()
        #Create QSqlTableModel object, and self.db is assigned
        self.model = QSqlTableModel(db=self.db)
        #select table name to invoke data
        self.model.setTable(tableName)
        #active for selecting data
        self.model.select()
        #reset QTableWidget to 0 row
        self.tableWidget.setRowCount(0)
        #get the column count for selected Table as automatic
        self.columns=self.model.record().count()
        #set columns count for QTableWidget
        self.tableWidget.setColumnCount(self.columns)
        #create labels array for Columns Headers
        labels=[]
        for i in range(self.columns):
            #get column name:
            fieldName=self.model.record().fieldName(i)
            #store the column name
            labels.append(fieldName)
        #set the columns header with labels
        self.tableWidget.setHorizontalHeaderLabels(labels)
        #loop for insert new row:
        for i in range(self.model.rowCount()):
            #insert new row:
            self.tableWidget.insertRow(i)
            #get a record with i index:
            record=self.model.record(i)
            #loop column to get value for each cell:
            for j in range(self.columns):
                #create QTableWidgetItem object
                item=QTableWidgetItem(str(record.value(j)))
                #set value for each CELL:
                self.tableWidget.setItem(i,j,item)
    def processFetchMore(self):
        #check if the model can fetch more:
        if self.model.canFetchMore():
            #set the i index for last rowcount:
            i=self.model.rowCount()
            #call fetchmore method:
            self.model.fetchMore()
            #loop for new batch data:
            for i in range(i,self.model.rowCount()):
                # insert new row:
                self.tableWidget.insertRow(i)
                # get a record with i index:
                record = self.model.record(i)
                # loop column to get value for each cell:
                for j in range(self.columns):
                    # create QTableWidgetItem object
                    item = QTableWidgetItem(str(record.value(j)))
                    # set value for each CELL:
                    self.tableWidget.setItem(i, j, item)
        else:
            msg=QMessageBox()
            msg.setText("No more records to fetch")
            msg.exec()
    def show(self):
        self.MainWindow.show()

Bước 6: Tạo “MyApp.py” để thực thi chương trình

from PyQt6.QtWidgets import QApplication, QMainWindow

from MainWindowEx import MainWindowEx

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

Ta chạy “MyApp.py” để thực thi chương trình như đã thiết kế:

  • Bước 1: Chọn Cơ sở dữ liệu SQLite tùy ý, lúc này toàn bộ bảng của CSDL sẽ được nạp vào QComboBox một cách tự động
  • Bước 2: Chọn tên bảng bất kỳ trong QComboBox, lúc này dữ liệu của bảng sẽ được nạp vào QTableWidget
  • Bước 3: Bấm Fetch More để nạp tiếp dữ liệu cho tới hết, mỗi lần nạp lấy thêm 256 records.

Như vậy tới đây Tui đã trình bày xong chi tiết cách sử dụng SQLite, DB Browser, cách dùng các thư viện để nạp dữ liệu từ SQLite lên QTableWidget, QComboBox, cũng như cách Fetch More. Các bạn cố gắng thực hành lại nhiều lần để rành hơn về kỹ thuật xử lý, cũng như áp dụng nó vào các bài toán trong thực tế của mình.

Mã lệnh đầy đủ cùng với các material của dự án các bạn tải ở đây:

https://www.mediafire.com/file/x87mmgvq8dh7vi1/LearnQTableWidgetPart3.rar/file

Bài học sau Tui sẽ hướng dẫn các bạn cách lập trình để Xem, Thêm, Sửa, Xóa dữ liệu trong SQLite.

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

Bài 23: Xử lý dữ liệu dạng bảng- QTableWidget–Part 2

Trong bài 22 các bạn đã biết cách thiết kế, lưu trữ và hiển thị dữ liệu trên QTableWidget, cũng như biết cách xử lý sự kiện người dùng chọn từng item trong Widget này. Tuy nhiên các dữ liệu đang được hardcode, còn trong bài học này Tui sẽ hướng dẫn các bạn cách thức tạo dữ liệu động lúc runtime cho QTableWidget, đây là trường hợp mà chúng ta thường xuyên sử dụng trong quá trình triển khai dự án trong thực tế.

Lý thuyết về QTableWidget đã được trình bày chi tiết ở bài học trước, nên bài học này Tui đi thẳng vào việc ứng dụng chúng để xây dựng một phần mềm hoàn chỉnh, các chức năng bao gồm:

  • Mô hình hóa hướng đối tượng trong Python
  • Chức năng thêm, sửa, xóa dữ liệu
  • Chức năng hiển thị dữ liệu, những Sản phẩm nào có giá <10 triệu thì tô nền vàng chữ đỏ.
  • Chức năng lưu dữ liệu từ QTableWidget xuống JSON
  • Chức năng đọc dữ liệu từ JSON lên QTableWidget
  • Chức năng xử lý sự kiện người dùng chọn các Item trên QTableWidget
  • Cũng như cách dùng QMessageBox để điều hướng các lựa chọn của người dùng.

Bước 1: Chúng ta tạo một dự án tên “LearnQTableWidgetPart2” có cấu trúc như dưới đây:

  • “images” thư mục chứa các hình ảnh của ứng dụng như: Icon của cửa sổ, icon thêm mới, icon lưu, icon xóa
  • “database.json” là file dữ liệu khi người dùng thêm mới, thay đổi Product thì danh sách dữ liệu sẽ được cập nhật trong file này
  • “Product.py” là lớp để mô hình hóa hướng đối tượng cho dữ liệu liên quan tới Sản phẩm, các thuộc tính báo gồm: mã, tên giá
  • “FileFactory.py” File dùng để Serilize danh sách Product xuống ổ cứng và Deserialize phục hồi lại danh sách đối tượng Product lên bộ nhớ để xử lý
  • “MainWindow.ui” là file thiết kế giao diện của ứng dụng, thiết kế bằng Qt Designer
  • “MainWindow.py” là file generate python code của MainWindow.ui
  • “MainWindowEx.py” là file mã lệnh thiết kế class kế thừa từ lớp generate python để dễ dàng xử lý các sự kiện, bổ sung các mã lệnh mà không bị lệ thuộc vào giao diện khi nó bị thay đổi
  • “MyApp.py” là file mã lệnh để thực thi chương trình

Bây giờ chúng ta đi vào chi tiết từng bước thiết kế và lập trình phần mềm Quản lý sản phẩm này.

Bước 2: Tạo lớp đối tượng Product trong file mã lệnh “Product.py”

class Product:
    def __init__(self,ProductId,ProductName,Price):
        self.ProductId=ProductId
        self.ProductName=ProductName
        self.Price=Price
    def __str__(self):
        return str(self.ProductId) +" - "+self.ProductName +"-"+str(self.Price)

Lớp Product ở trên được định nghĩa constructor mặc định có 3 đối số: ProductId, ProductName, Price

Các bạn cần thiết kế đúng như trên để sau này ta Serialize và Deserialize được nhanh chóng và chính xác.

Bước 3: Tạo lớp đối tượng FileFactory trong file mã lệnh “FileFactory.py”

import json
import os

class FileFactory:
    #path: path to serialize array of product
    #arrData: array of Product
    def writeData(self,path,arrData):
        jsonString = json.dumps([item.__dict__ for item in arrData],default=str)
        jsonFile = open(path, "w")
        jsonFile.write(jsonString)
        jsonFile.close()
    #path: path to deserialize array of Product
    #ClassName: Product
    def readData(self,path,ClassName):
        if os.path.isfile(path) == False:
            return []
        file = open(path, "r")
        # Reading from file
        self.arrData = json.loads(file.read(), object_hook=lambda d: ClassName(**d))
        file.close()
        return self.arrData

Lớp FileFactory này các bạn đã làm quen nhiều lần ở các bài học trước, nhiệm vụ của writeData là Serialize danh sách Product xuống ổ cứng với định dạng JSON Array. Hàm readData để Deserialize chuỗi JSON Array dưới ổ cứng lên bộ nhớ bằng mô hình hóa hướng đối tượng các Products.

Bước 4: Thiết kế giao diện cho MainWindow.ui và đặt tên cho các Widget như hình dưới đây (xem lại các bài học trước để kéo thả các widget cho phù hợp):

Bước 5: Tiến hành Generate Python code cho MainWindow.ui, lúc này MainWindow.py sẽ được tự động tạo ra như dưới đây:

# 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(456, 485)
        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.label = QtWidgets.QLabel(parent=self.centralwidget)
        self.label.setGeometry(QtCore.QRect(20, 0, 371, 41))
        font = QtGui.QFont()
        font.setPointSize(12)
        font.setBold(True)
        font.setWeight(75)
        self.label.setFont(font)
        self.label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
        self.label.setObjectName("label")
        self.groupBox = QtWidgets.QGroupBox(parent=self.centralwidget)
        self.groupBox.setGeometry(QtCore.QRect(20, 370, 411, 61))
        self.groupBox.setStyleSheet("background-color: rgb(236, 255, 199);")
        self.groupBox.setObjectName("groupBox")
        self.pushButtonRemove = QtWidgets.QPushButton(parent=self.groupBox)
        self.pushButtonRemove.setGeometry(QtCore.QRect(290, 20, 91, 28))
        self.pushButtonRemove.setStyleSheet("background-color: rgb(255, 170, 255);")
        icon1 = QtGui.QIcon()
        icon1.addPixmap(QtGui.QPixmap("images/ic_delete.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.pushButtonRemove.setIcon(icon1)
        self.pushButtonRemove.setIconSize(QtCore.QSize(24, 24))
        self.pushButtonRemove.setObjectName("pushButtonRemove")
        self.pushButtonSave = QtWidgets.QPushButton(parent=self.groupBox)
        self.pushButtonSave.setGeometry(QtCore.QRect(160, 20, 81, 28))
        self.pushButtonSave.setStyleSheet("background-color: rgb(255, 170, 255);")
        icon2 = QtGui.QIcon()
        icon2.addPixmap(QtGui.QPixmap("images/ic_save.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.pushButtonSave.setIcon(icon2)
        self.pushButtonSave.setIconSize(QtCore.QSize(24, 24))
        self.pushButtonSave.setObjectName("pushButtonSave")
        self.pushButtonNew = QtWidgets.QPushButton(parent=self.groupBox)
        self.pushButtonNew.setGeometry(QtCore.QRect(40, 20, 71, 28))
        self.pushButtonNew.setStyleSheet("background-color: rgb(255, 170, 255);")
        icon3 = QtGui.QIcon()
        icon3.addPixmap(QtGui.QPixmap("images/ic_new.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.pushButtonNew.setIcon(icon3)
        self.pushButtonNew.setIconSize(QtCore.QSize(24, 24))
        self.pushButtonNew.setObjectName("pushButtonNew")
        self.groupBox_2 = QtWidgets.QGroupBox(parent=self.centralwidget)
        self.groupBox_2.setGeometry(QtCore.QRect(20, 250, 411, 111))
        self.groupBox_2.setStyleSheet("background-color: rgb(255, 201, 243);")
        self.groupBox_2.setObjectName("groupBox_2")
        self.label_4 = QtWidgets.QLabel(parent=self.groupBox_2)
        self.label_4.setGeometry(QtCore.QRect(20, 80, 101, 16))
        self.label_4.setObjectName("label_4")
        self.label_3 = QtWidgets.QLabel(parent=self.groupBox_2)
        self.label_3.setGeometry(QtCore.QRect(20, 50, 101, 16))
        self.label_3.setObjectName("label_3")
        self.lineEditProductName = QtWidgets.QLineEdit(parent=self.groupBox_2)
        self.lineEditProductName.setGeometry(QtCore.QRect(130, 50, 261, 22))
        self.lineEditProductName.setStyleSheet("background-color: rgb(251, 255, 206);")
        self.lineEditProductName.setObjectName("lineEditProductName")
        self.lineEditUnitPrice = QtWidgets.QLineEdit(parent=self.groupBox_2)
        self.lineEditUnitPrice.setGeometry(QtCore.QRect(130, 80, 261, 22))
        self.lineEditUnitPrice.setStyleSheet("background-color: rgb(251, 255, 206);")
        self.lineEditUnitPrice.setObjectName("lineEditUnitPrice")
        self.label_2 = QtWidgets.QLabel(parent=self.groupBox_2)
        self.label_2.setGeometry(QtCore.QRect(20, 20, 101, 16))
        self.label_2.setObjectName("label_2")
        self.lineEditProductId = QtWidgets.QLineEdit(parent=self.groupBox_2)
        self.lineEditProductId.setGeometry(QtCore.QRect(130, 20, 261, 22))
        self.lineEditProductId.setStyleSheet("background-color: rgb(251, 255, 206);")
        self.lineEditProductId.setObjectName("lineEditProductId")
        self.groupBox_3 = QtWidgets.QGroupBox(parent=self.centralwidget)
        self.groupBox_3.setGeometry(QtCore.QRect(20, 40, 411, 201))
        self.groupBox_3.setStyleSheet("background-color: rgb(251, 255, 206);")
        self.groupBox_3.setObjectName("groupBox_3")
        self.verticalLayout = QtWidgets.QVBoxLayout(self.groupBox_3)
        self.verticalLayout.setObjectName("verticalLayout")
        self.tableWidgetProduct = QtWidgets.QTableWidget(parent=self.groupBox_3)
        self.tableWidgetProduct.setStyleSheet("background-color: rgb(207, 255, 235);")
        self.tableWidgetProduct.setObjectName("tableWidgetProduct")
        self.tableWidgetProduct.setColumnCount(3)
        self.tableWidgetProduct.setRowCount(0)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetProduct.setHorizontalHeaderItem(0, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetProduct.setHorizontalHeaderItem(1, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetProduct.setHorizontalHeaderItem(2, item)
        self.verticalLayout.addWidget(self.tableWidgetProduct)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 456, 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)
        MainWindow.setTabOrder(self.tableWidgetProduct, self.lineEditProductId)
        MainWindow.setTabOrder(self.lineEditProductId, self.lineEditProductName)
        MainWindow.setTabOrder(self.lineEditProductName, self.lineEditUnitPrice)
        MainWindow.setTabOrder(self.lineEditUnitPrice, self.pushButtonNew)
        MainWindow.setTabOrder(self.pushButtonNew, self.pushButtonSave)
        MainWindow.setTabOrder(self.pushButtonSave, self.pushButtonRemove)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "Trần Duy Thanh - QTableWidget"))
        self.label.setText(_translate("MainWindow", "Product Management"))
        self.groupBox.setTitle(_translate("MainWindow", "Actions:"))
        self.pushButtonRemove.setText(_translate("MainWindow", "Remove"))
        self.pushButtonSave.setText(_translate("MainWindow", "Save"))
        self.pushButtonNew.setText(_translate("MainWindow", "New"))
        self.groupBox_2.setTitle(_translate("MainWindow", "Product Details:"))
        self.label_4.setText(_translate("MainWindow", "Unit Price:"))
        self.label_3.setText(_translate("MainWindow", "Product Name:"))
        self.label_2.setText(_translate("MainWindow", "Product Id:"))
        self.groupBox_3.setTitle(_translate("MainWindow", "List of Products:"))
        item = self.tableWidgetProduct.horizontalHeaderItem(0)
        item.setText(_translate("MainWindow", "Product Id"))
        item = self.tableWidgetProduct.horizontalHeaderItem(1)
        item.setText(_translate("MainWindow", "Product Name"))
        item = self.tableWidgetProduct.horizontalHeaderItem(2)
        item.setText(_translate("MainWindow", "Unit Price"))

Bước 6: Tạo “MainWindowEx.py” để viết lớp kế thừa từ Generate Python code ở bước trước nhằm xử lý các sự kiện người dùng như: Thêm, Lưu, Xóa, Xem dữ liệu…. mà không bị lệ thuộc khi trong tương lai giao diện có thay đổi

from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QTableWidgetItem, QMessageBox

from FileFactory import FileFactory
from MainWindow import Ui_MainWindow
from Product import Product


class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        self.products=[]
        self.selectedProduct=None
        self.fileFactory=FileFactory()

“MainWindowEx.py” được thiết kế constructor khởi tạo 3 biến đối tượng:

  • products là biến lưu danh sách Product
  • selectedProduct là biến lưu Product đang được chọn trên giao diện hiện hành, dựa vào biến này để ta biết đang lưu mới Product hay cập nhật dữ liệu cho Product hiện tại
  • fileFactory là biến đối tượng để Serialize và Deserialize danh sách Product

“MainWindowEx.py” sẽ Override hàm setupUI để nạp giao diện cũng như gán các signal và các hàm mặc định khi thực thi ứng dụng:

def setupUi(self, MainWindow):
    super().setupUi(MainWindow)
    self.MainWindow=MainWindow
    self.products=self.fileFactory.readData("database.json",Product)
    self.loadDataIntoTableWidget()
    self.pushButtonNew.clicked.connect(self.processNew)
    self.pushButtonSave.clicked.connect(self.processSave)
    self.tableWidgetProduct.itemSelectionChanged.connect(self.processItemSelection)
    self.pushButtonRemove.clicked.connect(self.processDelete)

Mặc định khi khởi tạo xong giao diện, chương trình sẽ đọc danh sách dữ liệu từ JSON thông qua đối tượng FileFactory, sau đó gọi hàm loadDataIntoTableWidget để hiển thị danh sách Product nếu đọc dữ liệu thành công:

def loadDataIntoTableWidget(self):
    self.tableWidgetProduct.setRowCount(0)
    for i in range(len(self.products)):
        product=self.products[i]
        row = self.tableWidgetProduct.rowCount()
        self.tableWidgetProduct.insertRow(row)
        self.tableWidgetProduct.setItem(row, 0, QTableWidgetItem(str(product.ProductId)))
        self.tableWidgetProduct.setItem(row, 1, QTableWidgetItem(product.ProductName))
        itemPrice = QTableWidgetItem()
        itemPrice.setText(str(product.Price))
        if product.Price < 10000000:
            itemPrice.setForeground(Qt.GlobalColor.red)
            itemPrice.setBackground(Qt.GlobalColor.yellow)
        self.tableWidgetProduct.setItem(row, 2, itemPrice)

Hàm trên sẽ duyệt qua toàn bộ danh sách các Product và đưa nó lên giao diện QTableWidget, mỗi một thuộc tính sẽ được đưa vào ô dữ liệu tương ứng là QTableWidgetItem. Nếu giá của sản phẩm nào <10 triệu thì sẽ tô nền vàng và chữ đỏ.

row = self.tableWidgetProduct.rowCount() sẽ trả về số dòng trong QTableWidget, tương đương với dòng cuối cùng

Hà insertRow(row) sẽ chèn dòng mới vào dòng cuối cùng của QTableWidget

Tiếp tới là các hàm setItem để thay đổi giá trị tại các ô của dòng đó.

Ngoài ra Tui cố tình viết 2 cách. Nếu khi ta cần dòng phức tạp thì tách riêng ra QTableWidgetItem để dễ định dạng (ví dụ định dạng cho price)

Hàm ProcessNew sẽ xóa dữ liệu hiện tại để cho người dùng nhập liệu:

def processNew(self):
    self.lineEditProductId.setText("")
    self.lineEditProductName.setText("")
    self.lineEditUnitPrice.setText("")
    self.lineEditProductId.setFocus()
    self.selectedProduct=None

Khi người dùng nhấn vào nút “New” chương trình sẽ xóa dữ liệu trên giao diện QLineEdit và focus tới ô ID, cũng như gán selectedProduct=None để đánh dấu là thêm mới.

Hàm processSave sẽ xử lý 2 tình huống: Lưu mới và lưu cập nhật Product tùy thuộc vào selectedProduct:

def processSave(self):
    product=Product(self.lineEditProductId.text(),self.lineEditProductName.text(),float(self.lineEditUnitPrice.text()))
    if self.selectedProduct==None:
        self.products.append(product)
        row = self.tableWidgetProduct.rowCount()
        self.tableWidgetProduct.insertRow(row)
    else:
        row=self.products.index(self.selectedProduct)
    self.selectedProduct = product
    self.products[row] = self.selectedProduct
    self.tableWidgetProduct.setItem(row,0,QTableWidgetItem(str(product.ProductId)))
    self.tableWidgetProduct.setItem(row,1,QTableWidgetItem(product.ProductName))
    itemPrice=QTableWidgetItem()
    itemPrice.setText(str(product.Price))
    if product.Price<10000000:
        itemPrice.setForeground(Qt.GlobalColor.red)
        itemPrice.setBackground(Qt.GlobalColor.yellow)
    self.tableWidgetProduct.setItem(row, 2, itemPrice)
    self.fileFactory.writeData("database.json", self.products)

Hàm processSave ở trên sẽ kiểm tra biến selectedProduct để điều hướng là lưu mới hay lưu Cập nhật. Sau khi xử lý thành công thì chương trình sẽ hiển thị dữ liệu lên QTableWidget đồng thời lưu dữ liệu xuống ổ cứng bằng hàm writeData. Dữ liệu là mảng JSON sẽ được Serialize xuống ổ cứng.

Hàm processItemSelection sẽ xử lý người dùng lựa chọn dữ liệu trên các dòng của QTableWidget:

def processItemSelection(self):
    row=self.tableWidgetProduct.currentRow()
    if row ==-1 or row>=len(self.products):
        return
    #id=self.tableWidgetProduct.item(row,0).text()
    #name=self.tableWidgetProduct.item(row,1).text()
    #price = self.tableWidgetProduct.item(row, 2).text()
    product=self.products[row]
    self.selectedProduct=product
    id=product.ProductId
    name=product.ProductName
    price=product.Price
    self.lineEditProductId.setText(str(id))
    self.lineEditProductName.setText(name)
    self.lineEditUnitPrice.setText(str(price))

Dòng nào được lựa chọn thì thông tin chi tiết sẽ được hiển thị vào các ô QLineEdit.

Cuối cùng là hàm processDelete để xóa Product đang chọn trên QTableWidget:

def processDelete(self):
    dlg = QMessageBox(self.MainWindow)
    if self.selectedProduct == None:
        dlg.setWindowTitle("Deleteing error")
        dlg.setIcon(QMessageBox.Icon.Critical)
        dlg.setText("You have to select a Product to delete")
        dlg.exec()
        return
    dlg.setWindowTitle("Confirmation Deleting")
    dlg.setText("Are you sure you want to delete?")
    buttons = QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
    dlg.setStandardButtons(buttons)
    button = dlg.exec()
    if button == QMessageBox.StandardButton.Yes:
        row=self.products.index(self.selectedProduct)
        self.products.remove(self.selectedProduct)
        self.selectedProduct=None
        self.tableWidgetProduct.removeRow(row)
        self.processNew()
        self.fileFactory.writeData("database.json", self.products)

Chương trình sẽ dùng QMessageBox để xác nhận xem người sử dụng có muốn xóa hay không. Nếu xác nhận xóa thì chương trình sẽ xóa đồng thời lưu lại dữ liệu xuống ổ cứng.

Dưới đây là Full code của MainWindowEx:

from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QTableWidgetItem, QMessageBox

from FileFactory import FileFactory
from MainWindow import Ui_MainWindow
from Product import Product


class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        self.products=[]
        self.selectedProduct=None
        self.fileFactory=FileFactory()
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow
        self.products=self.fileFactory.readData("database.json",Product)
        self.loadDataIntoTableWidget()
        self.pushButtonNew.clicked.connect(self.processNew)
        self.pushButtonSave.clicked.connect(self.processSave)
        self.tableWidgetProduct.itemSelectionChanged.connect(self.processItemSelection)
        self.pushButtonRemove.clicked.connect(self.processDelete)
    def loadDataIntoTableWidget(self):
        self.tableWidgetProduct.setRowCount(0)
        for i in range(len(self.products)):
            product=self.products[i]
            row = self.tableWidgetProduct.rowCount()
            self.tableWidgetProduct.insertRow(row)
            self.tableWidgetProduct.setItem(row, 0, QTableWidgetItem(str(product.ProductId)))
            self.tableWidgetProduct.setItem(row, 1, QTableWidgetItem(product.ProductName))
            itemPrice = QTableWidgetItem()
            itemPrice.setText(str(product.Price))
            if product.Price < 10000000:
                itemPrice.setForeground(Qt.GlobalColor.red)
                itemPrice.setBackground(Qt.GlobalColor.yellow)
            self.tableWidgetProduct.setItem(row, 2, itemPrice)
    def processNew(self):
        self.lineEditProductId.setText("")
        self.lineEditProductName.setText("")
        self.lineEditUnitPrice.setText("")
        self.lineEditProductId.setFocus()
        self.selectedProduct=None
    def processSave(self):
        product=Product(self.lineEditProductId.text(),self.lineEditProductName.text(),float(self.lineEditUnitPrice.text()))
        if self.selectedProduct==None:
            self.products.append(product)
            row = self.tableWidgetProduct.rowCount()
            self.tableWidgetProduct.insertRow(row)
        else:
            row=self.products.index(self.selectedProduct)
        self.selectedProduct = product
        self.products[row] = self.selectedProduct
        self.tableWidgetProduct.setItem(row,0,QTableWidgetItem(str(product.ProductId)))
        self.tableWidgetProduct.setItem(row,1,QTableWidgetItem(product.ProductName))
        itemPrice=QTableWidgetItem()
        itemPrice.setText(str(product.Price))
        if product.Price<10000000:
            itemPrice.setForeground(Qt.GlobalColor.red)
            itemPrice.setBackground(Qt.GlobalColor.yellow)
        self.tableWidgetProduct.setItem(row, 2, itemPrice)
        self.fileFactory.writeData("database.json", self.products)
    def processItemSelection(self):
        row=self.tableWidgetProduct.currentRow()
        if row ==-1 or row>=len(self.products):
            return
        #id=self.tableWidgetProduct.item(row,0).text()
        #name=self.tableWidgetProduct.item(row,1).text()
        #price = self.tableWidgetProduct.item(row, 2).text()
        product=self.products[row]
        self.selectedProduct=product
        id=product.ProductId
        name=product.ProductName
        price=product.Price
        self.lineEditProductId.setText(str(id))
        self.lineEditProductName.setText(name)
        self.lineEditUnitPrice.setText(str(price))
    def processDelete(self):
        dlg = QMessageBox(self.MainWindow)
        if self.selectedProduct == None:
            dlg.setWindowTitle("Deleteing error")
            dlg.setIcon(QMessageBox.Icon.Critical)
            dlg.setText("You have to select a Product to delete")
            dlg.exec()
            return
        dlg.setWindowTitle("Confirmation Deleting")
        dlg.setText("Are you sure you want to delete?")
        buttons = QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
        dlg.setStandardButtons(buttons)
        button = dlg.exec()
        if button == QMessageBox.StandardButton.Yes:
            row=self.products.index(self.selectedProduct)
            self.products.remove(self.selectedProduct)
            self.selectedProduct=None
            self.tableWidgetProduct.removeRow(row)
            self.processNew()
            self.fileFactory.writeData("database.json", self.products)
    def show(self):
        self.MainWindow.show()

Bước 7: Tạo “MyApp.py” để thực thi chương trình

from PyQt6.QtWidgets import QApplication, QMainWindow

from MainWindowEx import MainWindowEx

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

Chạy phần mềm lên ta sẽ có kết quả như mong muốn:

Như vậy là Tui đã hướng dẫn xong cách ứng dụng QTableWidget để lưu trữ và xử lý dữ liệu dạng lưới, cụ thể là bài quản lý sản phẩm. Chương trình kết hợp nhiều kỹ thuật như mô hình hóa đối tượng, thêm mới, sửa, xóa…

Các bạn thực hiện lại bài này nhiều lần để ứng dụng được QTableWidget vào các trường hợp cụ thể của mình.

Source code bài này các bạn tải ở đây:

https://www.mediafire.com/file/acl70y1850nfl37/LearnQTableWidgetPart2.rar/file

Bài học sau Tui sẽ mình họa QTableWidget với cơ sở dữ liệu SQLite các bạn chú ý theo dõi

Bài 22: Hiển thị dữ liệu dạng bảng- QTableWidget–Part 1

Ở các bài học trước, Chúng ta đã dùng QComboBox, QListWidget để hiển thị dữ liệu dạng danh sách, tuy nhiên nó chưa được chi tiết hóa cho từng thuộc tính. QTableWidget là một widget dùng để hiển thị dữ liệu dạng bảng (lưới) giúp cho việc quan sát dữ liệu được chi tiết, rõ ràng hơn. Và trong thực tế các dữ liệu của chúng ta cũng thường có nhiều thuộc tính để hiển thị, do đó QTableWidget là một trong các Widget quan trọng thường dùng để hiển thị dữ liệu.

Bài học này Tui sẽ trình bày cách thức kéo thả QTableWidget, tạo các cột, dòng dữ liệu bằng Qt Designer để các bạn có cảm giác trước, các bài học sau Tui sẽ trình bày các kỹ thuật nâng cao về TableWidget chẳng hạn như cách nạp dữ liệu runtime, kết hợp mô hình hướng đối tượng, xử lý MVC Model cho QTable Widget, tương tác dữ liệu với SQLite….

Trước tiên chúng ta cần biết sơ qua Các thuộc tính, phương thức và signal thường dùng của QTableWidget:

Thuộc tính, phương thức, signalÝ nghĩa chức năng
QTableWidget()Constructor để tạo đối tượng QTableWidget
QTableWidget(rows, columns)Constructor để tạo đối tượng QTableWidget, mặc định có rows dòng và columns cột
setRowCount(rows)Hàm thiết lập số dòng cho QTableWidget
setColumnCount(columns)Hàm thiết lập số cột cho QTableWidget
setHorizontalHeaderLabels(labels)Hàm thiết lập tiêu đề cột
setColumnWidth(column, width)Hàm thiết lập độ rộng của cột
rowCount()Hàm trả về số dòng trong QTableWidget
insertRow(row)Hàm chèn một item mới vào vị trí row trong QTableWidget
setItem(row, column, item)Hàm thiết lập các giá trị cho CELL của Item.
removeRow(current_row)Hàm xóa dòng khỏi QTableWidget
itemSelectionChangedsignal để lắng nghe người sử dụng đang chọn dòng dữ liệu nào trong QTableWidget
currentRow()Hàm trả về vị trí của dòng đang lựa chọn trên giao diện
item(row,column)Trả về ô giao nhau giữa row và column

Ví dụ dưới đây Tui sẽ hướng dẫn các bạn cách dùng Qt Designer để kéo thả, thiết kế giao diện cho QTableWidget.

Bước 1: Tạo một dự án “LearnQTableWidget” trong Pycharm

Bước 2: Dùng Qt Designer để thiết kế giao diện cho MainWindow.ui

Kéo thả TableWidget vào MainWindow như hình dưới đây:

Sau đó bổ sung các Columns, Rows, Items cho QTableWidget cũng như các Widget cơ bản như hình dưới:

Các QLineEdit, QPushButton các bạn đã được học rất kỹ ở các bài trước rồi nên Tui không nhắc lại.

Bây giờ chúng ta sẽ cấu hình Cột và Dòng cũng như Items cho QTableWidget:

Chúng ta chỉ cần Double Click chuột vào QTableWidget hoặc bấm chuột phải vào nó rồi chọn “Edit Items”:

Lúc này màn hình Edit Table Widget sẽ hiển thị ra như bên dưới:

  • Thẻ “Columns” dùng để định nghĩa tiêu đề cột: Ở đây các bạn gõ Song ID, Song Name, và Singer. Có thể thêm Columns bằng cách nhấn vào biểu tưởng dấu +, Có thể xóa bằng biểu tượng dấu trừ, thay đổi vị trí xuất hiện bằng các mũi tên lên xuống

Tương tự như vậy ta vào thẻ “Rows” để tạo các tiêu đề dòng cho QTableWidget:

  • Thẻ “Rows” dùng để định nghĩa tiêu đề dòng: Ở đây các bạn thêm các tiêu đề dòng từ 1 tới 5. Có thể thêm Rows bằng cách nhấn vào biểu tưởng dấu +, Có thể xóa bằng biểu tượng dấu trừ, thay đổi vị trí xuất hiện bằng các mũi tên lên xuống.

Cuối cùng là thẻ “Items” dùng để nhập dữ liệu cho QTableWidget:

Ngoài ra QTableWidget cũng cung cấp chức năng định dạng nâng cao cho cả “Columns”, “Rows” và “Items” bằng cách nhấn vào nút “Properties“:

Tương ứng với mỗi thẻ “Columns”, “Rows” hay “Items” mà QTableWidget sẽ cung cấp các thuộc tính khác nhau trong “Properties”. Tùy vào nhu cầu sử dụng mà các bạn có thể cấu hình chẳng hạn như: Chuỗi hiểu thị, Icon, font chữ, màu nền, màu chữ…

Sau khi kéo thả đầy đủ các Widget như giao diện thiết kế, các bạn tiến hành đặt tên cho chúng để ta lập trình:

Bước 3: Dùng Công cụ Generate Python code tự động cho giao diện cho MainWindow.ui để tạo file mã lệnh Python “MainWindow.py“:

# Form implementation generated from reading ui file 'MainWindow.ui'
#
# Created by: PyQt6 UI code generator 6.5.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(476, 376)
        self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.tableWidgetSong = QtWidgets.QTableWidget(parent=self.centralwidget)
        self.tableWidgetSong.setGeometry(QtCore.QRect(10, 10, 451, 191))
        self.tableWidgetSong.setObjectName("tableWidgetSong")
        self.tableWidgetSong.setColumnCount(3)
        self.tableWidgetSong.setRowCount(5)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetSong.setVerticalHeaderItem(0, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetSong.setVerticalHeaderItem(1, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetSong.setVerticalHeaderItem(2, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetSong.setVerticalHeaderItem(3, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetSong.setVerticalHeaderItem(4, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetSong.setHorizontalHeaderItem(0, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetSong.setHorizontalHeaderItem(1, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetSong.setHorizontalHeaderItem(2, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetSong.setItem(0, 0, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetSong.setItem(0, 1, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetSong.setItem(0, 2, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetSong.setItem(1, 0, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetSong.setItem(1, 1, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetSong.setItem(1, 2, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetSong.setItem(2, 0, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetSong.setItem(2, 1, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetSong.setItem(2, 2, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetSong.setItem(3, 0, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetSong.setItem(3, 1, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetSong.setItem(3, 2, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetSong.setItem(4, 0, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetSong.setItem(4, 1, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetSong.setItem(4, 2, item)
        self.label = QtWidgets.QLabel(parent=self.centralwidget)
        self.label.setGeometry(QtCore.QRect(20, 220, 71, 16))
        self.label.setObjectName("label")
        self.lineEditSongID = QtWidgets.QLineEdit(parent=self.centralwidget)
        self.lineEditSongID.setGeometry(QtCore.QRect(110, 220, 351, 20))
        self.lineEditSongID.setObjectName("lineEditSongID")
        self.lineEditSongName = QtWidgets.QLineEdit(parent=self.centralwidget)
        self.lineEditSongName.setGeometry(QtCore.QRect(110, 250, 351, 20))
        self.lineEditSongName.setObjectName("lineEditSongName")
        self.label_2 = QtWidgets.QLabel(parent=self.centralwidget)
        self.label_2.setGeometry(QtCore.QRect(20, 250, 81, 16))
        self.label_2.setObjectName("label_2")
        self.label_3 = QtWidgets.QLabel(parent=self.centralwidget)
        self.label_3.setGeometry(QtCore.QRect(20, 280, 81, 16))
        self.label_3.setObjectName("label_3")
        self.lineEditSinger = QtWidgets.QLineEdit(parent=self.centralwidget)
        self.lineEditSinger.setGeometry(QtCore.QRect(110, 280, 351, 20))
        self.lineEditSinger.setObjectName("lineEditSinger")
        self.pushButtonClose = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonClose.setGeometry(QtCore.QRect(110, 310, 75, 23))
        self.pushButtonClose.setObjectName("pushButtonClose")
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 476, 22))
        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)
        self.pushButtonClose.clicked.connect(MainWindow.close) # type: ignore
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "Trần Duy Thanh-QTableWidget"))
        item = self.tableWidgetSong.verticalHeaderItem(0)
        item.setText(_translate("MainWindow", "1"))
        item = self.tableWidgetSong.verticalHeaderItem(1)
        item.setText(_translate("MainWindow", "2"))
        item = self.tableWidgetSong.verticalHeaderItem(2)
        item.setText(_translate("MainWindow", "3"))
        item = self.tableWidgetSong.verticalHeaderItem(3)
        item.setText(_translate("MainWindow", "4"))
        item = self.tableWidgetSong.verticalHeaderItem(4)
        item.setText(_translate("MainWindow", "5"))
        item = self.tableWidgetSong.horizontalHeaderItem(0)
        item.setText(_translate("MainWindow", "Song ID"))
        item = self.tableWidgetSong.horizontalHeaderItem(1)
        item.setText(_translate("MainWindow", "Song Name"))
        item = self.tableWidgetSong.horizontalHeaderItem(2)
        item.setText(_translate("MainWindow", "Singer"))
        __sortingEnabled = self.tableWidgetSong.isSortingEnabled()
        self.tableWidgetSong.setSortingEnabled(False)
        item = self.tableWidgetSong.item(0, 0)
        item.setText(_translate("MainWindow", "S1"))
        item = self.tableWidgetSong.item(0, 1)
        item.setText(_translate("MainWindow", "Không yêu đừng nói lời cay đắng"))
        item = self.tableWidgetSong.item(0, 2)
        item.setText(_translate("MainWindow", "Tèo Mộng Mơ"))
        item = self.tableWidgetSong.item(1, 0)
        item.setText(_translate("MainWindow", "P2"))
        item = self.tableWidgetSong.item(1, 1)
        item.setText(_translate("MainWindow", "Người ấy và Tui Em phải chọn"))
        item = self.tableWidgetSong.item(1, 2)
        item.setText(_translate("MainWindow", "Tý Khốn Khổ"))
        item = self.tableWidgetSong.item(2, 0)
        item.setText(_translate("MainWindow", "P3"))
        item = self.tableWidgetSong.item(2, 1)
        item.setText(_translate("MainWindow", "Yêu Em mà không dám nói"))
        item = self.tableWidgetSong.item(2, 2)
        item.setText(_translate("MainWindow", "Bin Nhút Nhát"))
        item = self.tableWidgetSong.item(3, 0)
        item.setText(_translate("MainWindow", "P4"))
        item = self.tableWidgetSong.item(3, 1)
        item.setText(_translate("MainWindow", "Đập Vỡ Cây Đàn"))
        item = self.tableWidgetSong.item(3, 2)
        item.setText(_translate("MainWindow", "Tin Nóng Tánh"))
        item = self.tableWidgetSong.item(4, 0)
        item.setText(_translate("MainWindow", "P5"))
        item = self.tableWidgetSong.item(4, 1)
        item.setText(_translate("MainWindow", "Áo Em Chưa Mặc 1 Lần"))
        item = self.tableWidgetSong.item(4, 2)
        item.setText(_translate("MainWindow", "Tủn Thợ May"))
        self.tableWidgetSong.setSortingEnabled(__sortingEnabled)
        self.label.setText(_translate("MainWindow", "Song ID:"))
        self.label_2.setText(_translate("MainWindow", "Song Name:"))
        self.label_3.setText(_translate("MainWindow", "Singer:"))
        self.pushButtonClose.setText(_translate("MainWindow", "Close"))

Bước 4: Tạo một lớp kế thừa lớp Generate từ bước 4, đặt tên “MainWindowEx.py“:

from MainWindow import Ui_MainWindow

class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        super().__init__()
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow
        self.tableWidgetSong.itemSelectionChanged.connect(self.processSelectedItem)
    def processSelectedItem(self):
        row=self.tableWidgetSong.currentRow()
        songId=self.tableWidgetSong.item(row,0)
        songName=self.tableWidgetSong.item(row,1)
        singer=self.tableWidgetSong.item(row,2)
        self.lineEditSongID.setText(songId.text())
        self.lineEditSongName.setText(songName.text())
        self.lineEditSinger.setText(singer.text())
    def show(self):
        self.MainWindow.show()

Mã lệnh trong “MainWindowEx” Tui có lập trình gọi signal itemSelectionChanged để xử lý dòng dữ liệu mà người dùng chọn trên QTableWidget. Tui tạo slot processSelectedItem tương ứng để xử lý cho signal này. Slot này sẽ lấy dữ liệu của các ô trong dòng đang chọn của QTableWidget và hiển thị lên các QLineEdit.

Bước 5: Tạo “MyApp.py” để thực thi chương trình:

from PyQt6.QtWidgets import QApplication, QMainWindow

from MainWindowEx import MainWindowEx

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

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

Các bạn thấy giao diện của chương trình xuất hiện như trên, các bạn chọn dòng nào thì chi tiết dữ liệu của dòng đó sẽ hiển thị vào QLineEdit ở bên dưới.

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

https://www.mediafire.com/file/lc84wnz3ikq7mhy/LearnQTableWidget.rar/file

Như vậy là tới đây các bạn đã biết cách sử dụng QTableWidget bằng cách kéo thả vào giao diện, biết cách tạo các Columns, Rows và Item cho QTableWidget. Đồng thời cũng biết cách cấu hình các thuộc tính nâng cao trong nhóm “Properties”. Và cũng biết sử dụng signal itemSelectionChanged để xử lý sự kiện người dùng lựa chọn các dòng trong giao diện QTableWidget.

Bài học sau Tui sẽ tiếp tục hướng dẫn các bạn cách sử dụng và lập trình QTableWidget kéo thả nhưng ở mức nâng cao hơn. Đó là các dữ liệu sẽ được thêm vào QTableWidget lúc Run time, cũng như hướng dẫn các bạn cách xử lý Sửa, Xóa các dòng dữ liệu trong QTableWidget bằng mã lệnh.

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

Bài 21: QDateTimeEdit – Basic Widgets – PyQt6

Bài học QDateEdit chúng ta đã được học về widget xử lý Ngày-Tháng-Năm. Còn bài học QTimeEdit chúng ta đã được học về widget xử lý Giờ-Phút-Giây. Các bài học đều trình bày kỹ lưỡng về lý thuyết, cách lập trình và áp dụng các widget vào thực tế. Trong bài học này, Tui sẽ tiếp tục trình bày về QDateTimeEdit, nó là một widget kết hợp giữa QDateEdit và QTimeEdit.

Ví dụ dưới đây là phần mềm quản lý sản phẩm hơi phức tạp một xíu, phần mềm có sử dụng QDateTimeEdit , cuối bài học này Tui sẽ trình bày chi tiết cách thiết kế và lập trình phần mềm này:

Trước khi vào coding ta xem một số thuộc tính, phương thức, signal thường dùng của widget này:

Thuộc tính, phương thức, signalÝ nghĩa chức năng
QDateTimeEdit(self)Constructor để tạo đối tượng QDateTimeEdit. 
QDateTimeEdit(self,calendarPopup=True)Constructor để tạo đối tượng QDateTimeEdit. Nếu dùng constructor này thì người sử dụng sẽ dùng Calendar popup để lựa chọn
date()Hàm trả về ngày tháng năm mà người sử dụng nhập trên giao diện, nó có kiểu QDate, do đó muốn chuyển về date trong python thuần túy thì ta gọi thêm hàm toPyDate()
time()Hàm trả về đối tượng QTime. Do đó để chuyển về Python datetime.time ta dùng hàm toPyTime()
dateTime()Hàm trả về đối tượng QDateTime. Do đó để chuyển về Python datetime.datetime ta dùng hàm toPyDateTime()
setDate(date)Hàm thiết lập Ngày – Tháng – Năm cho widget
setTime(time)Hàm thiết lập Giờ-Phút-Giây cho widget
setDateTime(dateTime)Hàm thiết lập Ngày – Tháng – Năm và Giờ-Phút-Giây cho widget
minimumDateThuộc tính thiết lập ngày nhỏ nhất
maximumDateThuộc tính thiết lập ngày lớn nhất
minimumTimeThuộc tính thiết lập time nhỏ nhất
maximumTimeThuộc tính thiết lập time lớn nhất
minimumDateTimeThuộc tính thiết lập date time nhỏ nhất
maximumDateTimeThuộc tính thiết lập date time lớn nhất
setDisplayFormat(format)Phương thức xác định cách thức hiện thị dữ liệu Ngày – Tháng – Năm, giờ phút giây lên giao diện
Ví dụ:
setDisplayFormat(“dd/MM/yyyy HH:mm:ss AP”)
editingFinishedSignal để xử lý khi người dùng hoàn tất việc nhập liệu Ngày – Tháng – Năm – Giờ- Phút- Giây (đã chọn và nhấn phím Enter)
dateTimeChangedSignal để xử lý khi người dùng chọn lựa Ngày – Tháng – Năm-Giờ-Phút-Giây trên Widget

Dưới đây là các bước khai báo và sử dụng QDateTimeEdit:

Bước 1: Khai báo và khởi tạo đối tượng QDateTimeEdit

self.datetime_edit = QDateTimeEdit(self, calendarPopup=True)

Bước 2: Thiết lập các giá trị cho các thuộc tính

time = QTime(10, 15, 35)
date = QDate(2024, 1, 1)
self.datetime_edit.setTime(time)
self.datetime_edit.setDate(date)
self.datetime_edit.setDisplayFormat("dd/MM/yyyy HH:mm:ss AP")

Mã lệnh ở trên thiết lập QTime (giờ , phút, giây) và QDate(Năm, tháng, ngày) cho QDateTimeEdit thông qua các hàm setTime(time) và setDate(date).

Hoặc ta thiết lập QDateTime (năm, tháng, ngày, giờ , phút, giây):

date_time=QDateTime(2024,1,1,10,15,35)
self.datetime_edit.setDateTime(date_time)

Chi tiết format “dd/MM/yyyy HH:mm:ss AP” đã được trình bày ở bài 19 bài 20.

Bước 3: Thiết lập signal cho QDateTimeEdit khi người sử dụng lựa chọn xong

self.datetime_edit.editingFinished.connect(self.update)
def update(self):
    value = self.datetime_edit.dateTime()
    print(str(value.toPyDateTime()))

Hàm dateTime() sẽ trả về dữ liệu có kiểu QDateTime, nên muốn đưa về Python datetime thì ta gọi thêm hàm toPyDateTime()

Tuy nhiên editingFinished signal thì người dùng cần nhấn phím Enter để xác nhận. Do đó để tiện lại ta có thể dùng dateTimeChanged signal, bất cứ khi nào người dùng lựa chọn date-time trên giao diện thì ta sẽ lấy được dữ liệu này:

self.datetime_edit.dateTimeChanged.connect(self.update)
def update(self):
    value = self.datetime_edit.dateTime()
    print(str(value.toPyDateTime()))

Lưu ý nhiều signal có thể triệu gọi cùng 1 hàm (ở trên Tui ví dụ signal editingFinished dateTimeChanged cùng triệu gọi hàm update).

Dưới đây là Full code minh họa:

from PyQt6.QtCore import QDateTime, QTime, QDate
from PyQt6.QtWidgets import QApplication, QWidget, QDateTimeEdit, QLabel, QFormLayout


class MainWindow(QWidget):
    def __init__(self):
        super().__init__()

        self.setWindowTitle('PyQt QDateTimeEdit')
        self.setMinimumWidth(200)

        layout = QFormLayout()
        self.setLayout(layout)

        self.datetime_edit = QDateTimeEdit(self, calendarPopup=True)
        time = QTime(10, 15, 35)
        date = QDate(2024, 1, 1)
        self.datetime_edit.setTime(time)
        self.datetime_edit.setDate(date)
        date_time=QDateTime(2024,1,1,10,15,35)
        self.datetime_edit.setDateTime(date_time)
        self.datetime_edit.setDisplayFormat("dd/MM/yyyy HH:mm:ss AP")

        self.datetime_edit.editingFinished.connect(self.update)

        self.datetime_edit.dateTimeChanged.connect(self.update)

        self.result_label = QLabel('', self)

        layout.addRow('Date:', self.datetime_edit)
        layout.addRow(self.result_label)

        self.show()

    def update(self):
        value = self.datetime_edit.dateTime()
        print(str(value.toPyDateTime()))
        self.result_label.setText(value.toString("yyyy-MM-dd HH:mm"))

if __name__ == '__main__':
    app = QApplication([])
    window = MainWindow()
    app.exec()

Chạy mã lệnh trên ta có kết quả:

Ta chọn ngày tháng năm trong QDateTimeEdit thì ngay lập tức nó sẽ được hiển thị vào QLabel ở bên dưới.

Bây giờ Tui sẽ hướng dẫn các bạn cách thức áp dụng QDateTimeEdit vào việc phát triển phần mềm quản lý sản phẩm như dưới đây:

Mô tả các chức năng của phần mềm:

  • Chương trình gồm có DANH MỤC và SẢN PHẨM. 1 Danh Mục có nhiều Sản phẩm, và một sản phẩm chỉ thuộc về một danh mục.
  • Thông tin chi tiết của một Danh Mục bao gồm: Mã Danh Mục, Tên Danh Mục
  • Thông tin chi tiết của một Sản Phẩm bao gồm: Mã sản phẩm, tên sản phẩm, giá, thời gian tracking (cứ hiểu đại là thời gian muốn theo làm mốc theo dõi, bạn có thể tạo đại một thuộc tính nào đó chứa ngày tháng năm giờ phút giây).
  • Chương trình sẽ nạp dữ liệu có sẵn trong “database.json”, mô hình hóa vào các lớp đối tượng Dataset (là đối tượng chứa nhiều Category), Category (Là đối tượng chứa nhiều Product) và Product.
  • Danh sách các Category sẽ được nạp vào QCombobox.
  • Khi chọn Category nào trong QCombobox thì danh sách Product của Category này sẽ được hiển thị vào QListWidget
  • Khi chọn Product trong QListWidget thì thông tin chi tiết của nó sẽ được hiển thị vào nhóm chi tiết bên màn hình “Product Details”.
  • Nút “New” sẽ xóa trắng các ô nhập liệu và focus tới ô ID.
  • Nút “Save” sẽ lưu Product vào các selected Category tương ứng. Đặc biệt nút “Save” sẽ xử lý 2 tác vụ: Lưu thêm mới Product và lưu Cập nhật Product. Đồng thời cập nhật lại cơ sở dữ liệu
  • Nút “Delete” sẽ xóa Product đang chọn, đồng thời cập nhật lại cơ sở dữ liệu.

Ta tạo một dự án tên “LearnQDateTimeEdit” trong Pycharm, có cấu trúc tập tin và thư mục như dưới đây:

  • Thư mục “images” lưu trữ các icon của phần mềm như Window Icon, New Icon, Save Icon, Delete Icon.
  • Product.py Là file lưu mã lệnh của lớp Product để định nghĩa một Product bao gồm: id,name,price,timetracking
  • Category.py Là file lưu mã lệnh của lớp Category gồm các thuộc tính: id, name. Lớp này sẽ cung cấp List và các phương thức để lưu trữ và xử lý danh sách Product, chẳng hạn các chức năng: thêm, truy suất, sửa, xóa Product
  • Dataset.py là file lưu mã lệnh của lớp Dataset, lớp này cung cấp list và các phương thức để lưu trữ và xử lý danh sách Category, chẳng hạn các chức năng: thêm, truy suất, sửa, xóa Category. Vì đặc thù khi mô hình hóa lại dữ liệu chưa triệt để do nó lưu dạng Dictionary nên Tui có bổ sung thêm hàm reModel() để mô hình hóa toàn bộ lại hướng đối tượng.
  • FileFactory.py” là mã lệnh dùng để ghi toàn bộ dữ liệu xuống ổ cứng với định dạng JSonArray, cũng như phục hồi và mô hình hóa lại dữ liệu hướng đối tượng
  • MainWindow.ui” là file thiết kế giao diện bằng Qt Designer
  • MainWindow.py” là file python code được generate tự động từ “MainWindow.ui”. Cách thiết kế và tích hợp công cụ tự động Generate đã được hướng dẫn rất chi tiết ở những bài học đầu tiên, các bạn cần xem lại
  • MainWindowEx.py” là mã lệnh ta bổ sung, kế thừ từ lớp được generate trong MainWindow.py để xử lý các sự kiện người dùng
  • MyApp.py” là mã lệnh để thực thi chương trình
  • database.json” là file JSonArray để lưu trữ các dữ liệu mà người dùng thao tác trên phần mềm.

Bây giờ chúng ta đi vào chi tiết của từng thành phần:

Bước 0: Cấu trúc dữ liệu của “database.json” để chúng ta sử dụng thông qua mô hình hóa:

{
    "categories": [
        {
            "id": 1,
            "name": "Drinking",
            "products": [
                {
                    "id": 1,
                    "name": "Coca",
                    "price": 15,
                    "timetracking": "2023-12-25 14:30:00"
                },
                {
                    "id": 2,
                    "name": "Pepsi",
                    "price": 30,
                    "timetracking": "2023-11-18 4:3:5"
                },
                {
                    "id": 3,
                    "name": "Sting",
                    "price": 20,
                    "timetracking": "2023-12-27 2:4:5"
                }
            ]
        },
        {
            "id": 2,
            "name": "Fast Food",
            "products": [
                {
                    "id": 4,
                    "name": "Hamburger",
                    "price": 70,
                    "timetracking": "2023-12-15 14:23:46"
                },
                {
                    "id": 5,
                    "name": "Sandwich",
                    "price": 80,
                    "timetracking": "2023-12-18 14:13:45"
                },
                {
                    "id": 6,
                    "name": "Cheeseburger",
                    "price": 25,
                    "timetracking": "2023-12-14 15:43:5"
                },
                {
                    "id": 7,
                    "name": "Hot dog",
                    "price": 35,
                    "timetracking": "2023-12-12 12:30:5"
                },
                {
                    "id": 8,
                    "name": "Fried chicken",
                    "price": 68,
                    "timetracking": "2023-12-22 14:13:15"
                },
                {
                    "id": 9,
                    "name": "Baguette",
                    "price": 20,
                    "timetracking": "2023-12-25 8:7:20"
                },
                {
                    "id": 10,
                    "name": "Pretzel",
                    "price": 17,
                    "timetracking": "2023-12-27 9:13:15"
                },
                {
                    "id": 11,
                    "name": "Pizza",
                    "price": 70,
                    "timetracking": "2023-12-26 12:17:15"
                },
                {
                    "id": 12,
                    "name": "Sausage",
                    "price": 65,
                    "timetracking": "2023-12-26 10:08:15"
                },
                {
                    "id": 13,
                    "name": "Bacon",
                    "price": 100,
                    "timetracking": "2023-12-28 12:12:10"
                },
                {
                    "id": 14,
                    "name": "ANIMAL STYLE BURGER",
                    "price": 100,
                    "timetracking": "2023-12-28 12:12:10"
                },
                {
                    "id": 15,
                    "name": "SPICY CHICKEN SANDWICH",
                    "price": 100,
                    "timetracking": "2023-12-28 12:12:10"
                },
                {
                    "id": 16,
                    "name": "BISCUITS POPEYES",
                    "price": 100,
                    "timetracking": "2023-12-28 12:12:10"
                }
            ]
        },
        {
            "id": 3,
            "name": "Fruits",
            "products": [
                {
                    "id": 14,
                    "name": "Orange",
                    "price": 26,
                    "timetracking": "2023-12-25 9:30:0"
                },
                {
                    "id": 15,
                    "name": "Strawberry",
                    "price": 30,
                    "timetracking": "2023-11-18 4:3:5"
                },
                {
                    "id": 16,
                    "name": "Water Melon",
                    "price": 28,
                    "timetracking": "2023-11-15 14:3:5"
                }
            ]
        }
    ]
}

Bước 1: Viết mã lệnh cho “Product.py”, mã lệnh được thể hiện như dưới đây:

class Product:
    def __init__(self,id,name,price,timetracking):
        self.id=id
        self.name=name
        self.price=price
        self.timetracking=timetracking
    def __str__(self):
        return str(self.id)+"-"+self.name+"("+ str(self.price)+" $)"

Ta khai báo lớp Product cùng với constructor và các đối số chính xác như trên để việc serialize và deserialize JSon data được chính xác, Từng Product sẽ là 1 JSonObject, và mỗi một Category sẽ có 1 danh sách JSonArray các Product.

hàm __str__() để truy suất dữ liệu đối tượng và đưa về chuỗi hiển thị.

Bước 2: Viết mã lệnh cho “Category.py” để lưu trữ và xử lý danh sách các Product, mã lệnh được thể hiện như dưới đây:

from Product import Product

class Category:
    def __init__(self,id,name,products=None):
        self.id=id
        self.name=name
        self.products=products
        if self.products==None:
            self.products=[]
    def item(self,index)->Product:
        return self.products[index]
    def add(self,p):
        self.products.append(p)
    def addAll(self,products):
        for i in range(len(products)):
            p=products[i]
            self.add(p)
    def index(self,p):
        i=self.products.index(p)
        return i
    def update(self,index,p):
        self.products[index]=p
        return self.products[index]
    def removeByIndex(self,index):
        return self.products.pop(index)
    def removeByItem(self,item):
        self.products.remove(item)
    def clear(self):
        self.products.clear()
    def size(self):
        return len(self.products)
    def __str__(self):
        return  str(self.id)+"-"+self.name

Lớp Category ở trên có id và name, đồng thời Tui thiết kế các phương thức để ta có thể sử dụng lưu danh sách Product, cũng như tương tác các danh sách chẳng hạn:

  • Hàm item(self,index) là hàm trả về Product theo index
  • Hàm index(self,p) là hàm trả về vị trí của Product trong Catalog
  • Hàm add(self,p) để thêm mới một Product vào Catalog
  • Hàm addAll(self,products) để thêm mới nhiều Product vào Catalog
  • Hàm update(self,index,p) để cập nhật dữ liệu cho Product theo index
  • Hàm update(self,p) để cập nhật dữ liệu cho Product theo đối tượng Product
  • Hàm removeByIndex(self,index) để xóa 1 Product ra khỏi Catalog tại ví trí index
  • Hàm removeByItem(self,item) để xóa 1 Product ra khỏi danh sách tại ví trí item
  • Hàm clear(self) xóa toàn bộ Product ra khỏi Catalog
  • Hàm size(self) trả về kích thước (số lượng) Product trong Catalog

Bước 3: Viết mã lệnh cho “Dataset.py” để lưu trữ và xử lý danh sách các Category, mã lệnh được thể hiện như dưới đây:

from Category import Category
from Product import Product

class Dataset:
    def __init__(self,categories=None):
        if categories==None:
            self.categories=[]
        else:
            self.categories = categories
    def item(self,index)->Category:
        return self.categories[index]
    def add(self,c):
        self.categories.append(c)
    def addAll(self,categories):
        for i in range(len(categories)):
            c=categories[i]
            self.add(c)
    def index(self,c):
        i=self.categories.index(c)
        return i
    def update(self,index,c):
        self.categories[index]=c
        return self.categories[index]
    def removeByIndex(self,index):
        return self.categories.pop(index)
    def removeByItem(self,item):
        self.categories.remove(item)
    def clear(self):
        self.categories.clear()
    def size(self):
        return len(self.categories)
    def printAll(self):
        for i in range(self.size()):
            c=self.item(i)
            print(c)
            for j in range(len(c.products)):
                p=c.products[j]
                print(p)
    def reModel(self):
        categories = []
        for i in range(self.size()):
            dict_c = self.item(i)
            products = []
            for j in range(len(dict_c["products"])):
                dict_p = dict_c["products"][j]
                p = Product(dict_p["id"], dict_p["name"], dict_p["price"],dict_p["timetracking"])
                products.append(p)
            c = Category(dict_c["id"], dict_c["name"], products)
            categories.append(c)
        self.categories=categories

Lớp Dataset gồm có các phương thức để lưu danh sách Category, cũng như tương tác các danh sách chẳng hạn:

  • Hàm item(self,index) là hàm trả về Catalog theo index
  • Hàm index(self,c) là hàm trả về vị trí của Catalog trong Dataset
  • Hàm add(self,c) để thêm mới một Catalog vào Dataset
  • Hàm addAll(self,categories) để thêm mới nhiều Catalog vào Dataset
  • Hàm update(self,index,c) để cập nhật dữ liệu cho Catalog theo index
  • Hàm removeByIndex(self,index) để xóa 1 Catalog ra khỏi Dataset tại ví trí index
  • Hàm removeByItem(self,item) để xóa 1 Catalog ra khỏi danh sách tại ví trí item
  • Hàm clear(self) xóa toàn bộ Catalog ra khỏi Dataset
  • Hàm size(self) trả về kích thước (số lượng) Catalog trong Dataset
  • Hàm reModel(self) là hàm để mô hình hóa toàn bộ dữ liệu trong Json thành mô hình hướng đối tượng. Vì khi đọc ra, tuy Deserialize rồi nhưng các đối tượng lại mới chỉ ở dạng Dictionary.

Bước 4: Viết mã lệnh cho “FileFactory.py“, class này dùng để serialize và deserialize dữ liệu với định dạng JSonArray:

import json
import os

class FileFactory:
    #path: path to serialize array of product
    #arrData: array of Product
    def writeData(self,path,dataObject):
        jsonString = json.dumps(dataObject,default=lambda o: o.__dict__, indent=4)
        jsonFile = open(path, "w")
        jsonFile.write(jsonString)
        jsonFile.close()
    #path: path to deserialize array of Product
    #ClassName: Product
    def readData(self,path,ClassName):
        if os.path.isfile(path) == False:
            return []
        file = open(path, "r")
        jsonstring=file.read()
        # Reading from file
        ds =  ClassName(**json.loads(jsonstring))
        file.close()
        return ds

Tui có chỉnh sửa lại mã lệnh của FileFactory.py để nó có thể lưu được mảng dữ liệu JSonArray lồng nhau

Bước 5: Dùng Qt Designer để thiết kế giao diện “MainWindow.ui” cho phần mềm.

Các bạn kéo thả các Widget và đặt tên giống như hình minh họa ở trên.

Bước 6: Dùng công cụ để Generate Python code cho “MainWindow.ui” ta có file “MainWindow.py”:

# 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(650, 437)
        font = QtGui.QFont()
        font.setPointSize(12)
        MainWindow.setFont(font)
        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.groupBox = QtWidgets.QGroupBox(parent=self.centralwidget)
        self.groupBox.setGeometry(QtCore.QRect(270, 30, 351, 271))
        self.groupBox.setStyleSheet("background-color: rgb(255, 206, 241);")
        self.groupBox.setObjectName("groupBox")
        self.label_3 = QtWidgets.QLabel(parent=self.groupBox)
        self.label_3.setGeometry(QtCore.QRect(10, 30, 111, 20))
        self.label_3.setObjectName("label_3")
        self.lineEditProductId = QtWidgets.QLineEdit(parent=self.groupBox)
        self.lineEditProductId.setGeometry(QtCore.QRect(80, 60, 211, 22))
        self.lineEditProductId.setObjectName("lineEditProductId")
        self.label_4 = QtWidgets.QLabel(parent=self.groupBox)
        self.label_4.setGeometry(QtCore.QRect(10, 90, 151, 16))
        self.label_4.setObjectName("label_4")
        self.lineEditProductName = QtWidgets.QLineEdit(parent=self.groupBox)
        self.lineEditProductName.setGeometry(QtCore.QRect(80, 120, 211, 22))
        self.lineEditProductName.setObjectName("lineEditProductName")
        self.lineEditUnitPrice = QtWidgets.QLineEdit(parent=self.groupBox)
        self.lineEditUnitPrice.setGeometry(QtCore.QRect(80, 170, 211, 22))
        self.lineEditUnitPrice.setObjectName("lineEditUnitPrice")
        self.label_5 = QtWidgets.QLabel(parent=self.groupBox)
        self.label_5.setGeometry(QtCore.QRect(10, 150, 111, 16))
        self.label_5.setObjectName("label_5")
        self.label_6 = QtWidgets.QLabel(parent=self.groupBox)
        self.label_6.setGeometry(QtCore.QRect(10, 200, 141, 31))
        self.label_6.setObjectName("label_6")
        self.dateTimeEditTracking = QtWidgets.QDateTimeEdit(parent=self.groupBox)
        self.dateTimeEditTracking.setGeometry(QtCore.QRect(80, 230, 221, 31))
        self.dateTimeEditTracking.setDateTime(QtCore.QDateTime(QtCore.QDate(2023, 12, 29), QtCore.QTime(0, 0, 0)))
        self.dateTimeEditTracking.setCalendarPopup(True)
        self.dateTimeEditTracking.setObjectName("dateTimeEditTracking")
        self.groupBox_2 = QtWidgets.QGroupBox(parent=self.centralwidget)
        self.groupBox_2.setGeometry(QtCore.QRect(9, 39, 251, 341))
        self.groupBox_2.setStyleSheet("background-color: rgb(212, 253, 255);")
        self.groupBox_2.setTitle("")
        self.groupBox_2.setObjectName("groupBox_2")
        self.listWidgetProduct = QtWidgets.QListWidget(parent=self.groupBox_2)
        self.listWidgetProduct.setGeometry(QtCore.QRect(10, 100, 231, 221))
        self.listWidgetProduct.setStyleSheet("background-color: rgb(237, 255, 202);")
        self.listWidgetProduct.setObjectName("listWidgetProduct")
        self.label = QtWidgets.QLabel(parent=self.groupBox_2)
        self.label.setGeometry(QtCore.QRect(10, 10, 161, 21))
        self.label.setObjectName("label")
        self.cboCatalog = QtWidgets.QComboBox(parent=self.groupBox_2)
        self.cboCatalog.setGeometry(QtCore.QRect(10, 40, 221, 22))
        self.cboCatalog.setStyleSheet("background-color: rgb(237, 255, 202);")
        self.cboCatalog.setObjectName("cboCatalog")
        self.label_2 = QtWidgets.QLabel(parent=self.groupBox_2)
        self.label_2.setGeometry(QtCore.QRect(10, 70, 161, 21))
        self.label_2.setObjectName("label_2")
        self.groupBox_3 = QtWidgets.QGroupBox(parent=self.centralwidget)
        self.groupBox_3.setGeometry(QtCore.QRect(270, 300, 351, 81))
        self.groupBox_3.setStyleSheet("background-color: rgb(249, 255, 210);")
        self.groupBox_3.setObjectName("groupBox_3")
        self.pushButtonNew = QtWidgets.QPushButton(parent=self.groupBox_3)
        self.pushButtonNew.setGeometry(QtCore.QRect(10, 30, 93, 41))
        self.pushButtonNew.setStyleSheet("background-color: rgb(170, 255, 127);")
        icon1 = QtGui.QIcon()
        icon1.addPixmap(QtGui.QPixmap("images/ic_new.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.pushButtonNew.setIcon(icon1)
        self.pushButtonNew.setIconSize(QtCore.QSize(32, 32))
        self.pushButtonNew.setObjectName("pushButtonNew")
        self.pushButtonSave = QtWidgets.QPushButton(parent=self.groupBox_3)
        self.pushButtonSave.setGeometry(QtCore.QRect(120, 30, 93, 41))
        self.pushButtonSave.setStyleSheet("background-color: rgb(170, 255, 127);")
        icon2 = QtGui.QIcon()
        icon2.addPixmap(QtGui.QPixmap("images/ic_save.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.pushButtonSave.setIcon(icon2)
        self.pushButtonSave.setIconSize(QtCore.QSize(32, 32))
        self.pushButtonSave.setObjectName("pushButtonSave")
        self.pushButtonDelete = QtWidgets.QPushButton(parent=self.groupBox_3)
        self.pushButtonDelete.setGeometry(QtCore.QRect(232, 30, 111, 41))
        self.pushButtonDelete.setStyleSheet("background-color: rgb(170, 255, 127);")
        icon3 = QtGui.QIcon()
        icon3.addPixmap(QtGui.QPixmap("images/ic_delete.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.pushButtonDelete.setIcon(icon3)
        self.pushButtonDelete.setIconSize(QtCore.QSize(32, 32))
        self.pushButtonDelete.setObjectName("pushButtonDelete")
        self.label_7 = QtWidgets.QLabel(parent=self.centralwidget)
        self.label_7.setGeometry(QtCore.QRect(210, 0, 301, 31))
        palette = QtGui.QPalette()
        brush = QtGui.QBrush(QtGui.QColor(255, 0, 0))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.WindowText, brush)
        brush = QtGui.QBrush(QtGui.QColor(255, 0, 0))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Text, brush)
        brush = QtGui.QBrush(QtGui.QColor(255, 0, 0))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.ButtonText, brush)
        brush = QtGui.QBrush(QtGui.QColor(255, 0, 0))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.WindowText, brush)
        brush = QtGui.QBrush(QtGui.QColor(255, 0, 0))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Text, brush)
        brush = QtGui.QBrush(QtGui.QColor(255, 0, 0))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.ButtonText, brush)
        brush = QtGui.QBrush(QtGui.QColor(255, 0, 0))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.WindowText, brush)
        brush = QtGui.QBrush(QtGui.QColor(255, 0, 0))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Text, brush)
        brush = QtGui.QBrush(QtGui.QColor(255, 0, 0))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.ButtonText, brush)
        self.label_7.setPalette(palette)
        font = QtGui.QFont()
        font.setPointSize(15)
        font.setBold(True)
        font.setWeight(75)
        self.label_7.setFont(font)
        self.label_7.setStyleSheet("color: rgb(255, 0, 0);")
        self.label_7.setObjectName("label_7")
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 650, 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 - QDateTime Demo"))
        self.groupBox.setTitle(_translate("MainWindow", "Product Details:"))
        self.label_3.setText(_translate("MainWindow", "Product ID:"))
        self.label_4.setText(_translate("MainWindow", "Product Name:"))
        self.label_5.setText(_translate("MainWindow", "Unit Price:"))
        self.label_6.setText(_translate("MainWindow", "Time Tracking:"))
        self.dateTimeEditTracking.setDisplayFormat(_translate("MainWindow", "dd/MM/yyyy HH:mm:ss"))
        self.label.setText(_translate("MainWindow", "Select a Catalog:"))
        self.label_2.setText(_translate("MainWindow", "List of Products:"))
        self.groupBox_3.setTitle(_translate("MainWindow", "Action:"))
        self.pushButtonNew.setText(_translate("MainWindow", "New"))
        self.pushButtonSave.setText(_translate("MainWindow", "Save"))
        self.pushButtonDelete.setText(_translate("MainWindow", "Delete"))
        self.label_7.setText(_translate("MainWindow", "Product Management"))

Bước 7: Ta tạo file “MainWindowEx.py” kế thừa từ “MainWindow.py” và tiến hành viết các sự kiện người dùng tương tác:

import datetime
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QListWidgetItem
from Dataset import Dataset
from FileFactory import FileFactory
from MainWindow import Ui_MainWindow
from Product import Product

class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        super().__init__()
        self.fileFactory = FileFactory()
        self.dataset=None
        self.selectedCatalog=None
        self.selectedProduct=None

Trong MainWindowEx ta bổ sung constructor để khởi tạo 4 biến đối tượng:

  • Biến đối tượng fileFactory là biến dùng để gọi đối tượng FileFactory nhằm sử dụng Serialize và Deserialize dữ liệu với định dạng JSonArray.
  • Biến đối tượng dataset để lưu trữ danh sách hướng đối tượng các Category mà người sử dụng tương tác trên giao diện.
  • Biến đối tượng selectedCatalog để lưu trữ Category đang lựa chọn trên QComboBox.
  • Biến đối tượng selectedProduct để lưu trữ Product đang lựa chọn trên QListWidget.

Tiếp theo ta Override hàm setupUi để khởi tạo giao diện cũng như gán các signal và slot cho giao diện tương tác:

def setupUi(self, MainWindow):
    super().setupUi(MainWindow)
    self.MainWindow=MainWindow
    self.dataset = self.fileFactory.readData("database.json", Dataset)
    self.dataset.reModel()
    self.loadCatalogForComboBox()
    self.cboCatalog.activated.connect(self.processSelectedCatalog)
    self.listWidgetProduct.itemSelectionChanged.connect(self.processSelectedProduct)
    self.pushButtonNew.clicked.connect(self.processNew)
    self.pushButtonSave.clicked.connect(self.processSave)
    self.pushButtonDelete.clicked.connect(self.processDelete)

Khi khởi động phần mềm, chương trình sẽ đọc dữ liệu (deserialize) “database.json” thành hướng đối tượng Dataset và biến đối tượng dataset sẽ lưu trữ danh sách dữ liệu (các Category) này.

Hàm reModel() được triệu gọi để mô hình hóa đầy đủ cấu trúc dữ liệu trong Json thành mô hìn hướng đối tượng.

Sau đó hàm loadCatalogForComboBox() sẽ được triệu gọi để hiển thị toàn bộ Catalog trong biến đối tượng dataset lên giao diện QComboBox:

def loadCatalogForComboBox(self):
    for i in range(self.dataset.size()):
        catalog=self.dataset.item(i)
        self.cboCatalog.addItem(str(catalog),catalog)

Chương trình dùng vòng lặp để duyệt qua các Category trong biến đối tượng dataset, sau đó nó sẽ được để hiển thị lên giao diện. 

Sau đó hàm processSelectedCatalog() sẽ xử lý khi người dùng nhấn chọn Category trong QComboBox thì danh sách Product của Category này sẽ hiển thị vào QListWidget:

def processSelectedCatalog(self):
    self.selectedCatalog=self.cboCatalog.currentData(Qt.ItemDataRole.UserRole)
    self.listWidgetProduct.clear()
    for i in range(self.selectedCatalog.size()):
        product=self.selectedCatalog.item(i)
        item=QListWidgetItem()
        item.setData(Qt.ItemDataRole.UserRole, product)
        item.setText(str(product))
        self.listWidgetProduct.addItem(item)

-Hàm processNew dùng để xóa các dữ liệu đang nhập trên giao diện và focus vào ô ID để người dùng nhập dữ liệu mới được nhanh chóng hơn:

def processNew(self):
    self.lineEditProductId.setText("")
    self.lineEditProductName.setText("")
    self.lineEditUnitPrice.setText("")
    self.selectedProduct=None
    self.lineEditProductId.setFocus()

Ta có một vài lưu ý với hàm processNew:

  • biến selectedProduct sẽ được gán None để đánh dấu ta không còn chọn Product nào trong QListWidget, mà đây sẽ là Product mới hoàn toàn
  • gọi hàm setFocus() cho ID để người sử dụng nhập liệu nhanh chóng nhất.

-Hàm processSave dùng để lưu dữ liệu, nếu selectedCatalog mà bằng None thì không xử lý (người dùng cần chọn Category trước). Nếu selectedProduct mà bằng None là lưu mới, ngược lại là lưu cập nhật:

def processSave(self):
    if self.selectedCatalog==None:
        return
    id=int(self.lineEditProductId.text())
    name=self.lineEditProductName.text()
    price=float(self.lineEditUnitPrice.text())
    tracking=self.dateTimeEditTracking.dateTime()
    date_format = '%Y-%m-%d %H:%M:%S'
    trackingFormat=tracking.toPyDateTime().strftime(date_format)

    product=Product(id,name,price,trackingFormat)
    item = QListWidgetItem()
    item.setData(Qt.ItemDataRole.UserRole, product)
    item.setText(str(product))
    if self.selectedProduct==None:
        self.selectedProduct=product
        self.selectedCatalog.add(product)
        self.listWidgetProduct.addItem(item)
    else:
        index=self.selectedCatalog.index(self.selectedProduct)
        self.selectedProduct=product
        self.selectedCatalog.update(index,self.selectedProduct)
        item = self.listWidgetProduct.item(index)
        item.setData(Qt.ItemDataRole.UserRole,self.selectedProduct)
        item.setText(str(self.selectedProduct))
    self.fileFactory.writeData("database.json", self.dataset)

Sau khi thêm một Product mới thì chương trình sẽ cập nhật lại dữ liệu và lưu xuống tập tin database.json

– Hàm processDelete dùng để xóa Product đang chọn ra khỏi QListWidget:

def processDelete(self):
    if self.selectedProduct!=None:
        index = self.selectedCatalog.index(self.selectedProduct)
        self.selectedCatalog.removeByItem(self.selectedProduct)
        self.fileFactory.writeData("database.json", self.dataset)
        self.processSelectedCatalog()
        self.processNew()

-Sau khi xóa thành công thì cũng cập nhật lại danh sách cho biến đối tượng dataset, hiển thị lại dữ liệu trên giao diện, đồng thời cũng lưu lại tập tin database.json.

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

import datetime
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QListWidgetItem
from Dataset import Dataset
from FileFactory import FileFactory
from MainWindow import Ui_MainWindow
from Product import Product

class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        super().__init__()
        self.fileFactory = FileFactory()
        self.dataset=None
        self.selectedCatalog=None
        self.selectedProduct=None
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow
        self.dataset = self.fileFactory.readData("database.json", Dataset)
        self.dataset.reModel()
        self.loadCatalogForComboBox()
        self.cboCatalog.activated.connect(self.processSelectedCatalog)
        self.listWidgetProduct.itemSelectionChanged.connect(self.processSelectedProduct)
        self.pushButtonNew.clicked.connect(self.processNew)
        self.pushButtonSave.clicked.connect(self.processSave)
        self.pushButtonDelete.clicked.connect(self.processDelete)
    def loadCatalogForComboBox(self):
        for i in range(self.dataset.size()):
            catalog=self.dataset.item(i)
            self.cboCatalog.addItem(str(catalog),catalog)
    def processSelectedCatalog(self):
        self.selectedCatalog=self.cboCatalog.currentData(Qt.ItemDataRole.UserRole)
        self.listWidgetProduct.clear()
        for i in range(self.selectedCatalog.size()):
            product=self.selectedCatalog.item(i)
            item=QListWidgetItem()
            item.setData(Qt.ItemDataRole.UserRole, product)
            item.setText(str(product))
            self.listWidgetProduct.addItem(item)

    def processSelectedProduct(self):
        current_row = self.listWidgetProduct.currentRow()
        if current_row < 0:
            return
        item = self.listWidgetProduct.item(current_row)
        self.selectedProduct = item.data(Qt.ItemDataRole.UserRole)
        self.lineEditProductId.setText(str(self.selectedProduct.id))
        self.lineEditProductName.setText(self.selectedProduct.name)
        self.lineEditUnitPrice.setText(str(self.selectedProduct.price))
        date_format = '%Y-%m-%d %H:%M:%S'
        timetracking=datetime.datetime.strptime(self.selectedProduct.timetracking,date_format)
        self.dateTimeEditTracking.setDateTime(timetracking)
    def processNew(self):
        self.lineEditProductId.setText("")
        self.lineEditProductName.setText("")
        self.lineEditUnitPrice.setText("")
        self.selectedProduct=None
        self.lineEditProductId.setFocus()
    def processSave(self):
        if self.selectedCatalog==None:
            return
        id=int(self.lineEditProductId.text())
        name=self.lineEditProductName.text()
        price=float(self.lineEditUnitPrice.text())
        tracking=self.dateTimeEditTracking.dateTime()
        date_format = '%Y-%m-%d %H:%M:%S'
        trackingFormat=tracking.toPyDateTime().strftime(date_format)

        product=Product(id,name,price,trackingFormat)
        item = QListWidgetItem()
        item.setData(Qt.ItemDataRole.UserRole, product)
        item.setText(str(product))
        if self.selectedProduct==None:
            self.selectedProduct=product
            self.selectedCatalog.add(product)
            self.listWidgetProduct.addItem(item)
        else:
            index=self.selectedCatalog.index(self.selectedProduct)
            self.selectedProduct=product
            self.selectedCatalog.update(index,self.selectedProduct)
            item = self.listWidgetProduct.item(index)
            item.setData(Qt.ItemDataRole.UserRole,self.selectedProduct)
            item.setText(str(self.selectedProduct))
        self.fileFactory.writeData("database.json", self.dataset)

    def processDelete(self):
        if self.selectedProduct!=None:
            index = self.selectedCatalog.index(self.selectedProduct)
            self.selectedCatalog.removeByItem(self.selectedProduct)
            self.fileFactory.writeData("database.json", self.dataset)
            self.processSelectedCatalog()
            self.processNew()

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

– Bước 8: Cuối cùng ta tạo lớp “MyApp.py” và viết mã lệnh như dưới đây để khởi chạy chương trình:

from PyQt6.QtWidgets import QApplication, QMainWindow

from MainWindowEx import MainWindowEx

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

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

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

https://www.mediafire.com/file/jtvc75cgfhxtijq/LearnQDateTimeEdit.rar/file

Như vậy tới đây Tui đã trình bày xong lý thuyết cũng như kỹ thuật sử dụng QDateTimeEdit , củng cố bài cũ QComboBox, QListWidget và ứng dụng vào quản lý Product, củng cố lại các kiến thức liên quan tới lập trình hướng đối tượng, cách serialize và deserialize đối tượng ra JSonArray, củng cố lại kiến thức và kỹ thuật liên quan tới hiển thị và tương tác dữ liệu trên giao diện.

Các bạn cố gắng thực hành lại bài này nhiều lần để nắm rõ hơn về lập trình hướng đối tượng, cách tạo các lớp độc lập để tái sử dụng tốt hơn

Bài học sau Tui sẽ trình bày về QTableWidget để tạo ra widget cho người dùng lưu trữ và hiển thị dữ liệu dạng dòng và cột, nó cũng là một trong các Widget quan trọng và phổ biến, được sử dụng thường xuyên trong các phần mềm. Các bạn chú ý theo dõi

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

Bài 20: QTimeEdit – Basic Widgets – PyQt6

Trong bài QDateEdit các bạn đã được học kỹ lưỡng về cách dùng widget để lưu trữ và xử lý dữ liệu Ngày-Tháng-Năm. Bài học này Tui sẽ trình bày về cách sử dụng QTimeEdit để lưu trữ Giờ-Phút.

Trong lưu trữ dữ liệu chúng ta cũng thường có các dữ liệu [ngày tháng năm], [ngày tháng năm giờ phút giây], [giờ phút giây] tùy từng tình huống mà chúng ta sẽ lựa chọn kiểu dữ liệu phù hợp. Ví dụ như lưu năm sinh thì ta dùng Ngày Tháng Năm, lưu thời gian đặt đơn hàng thì ta dùng ngày tháng năm giờ phút giây, lưu trữ thời gian thực hiện thì ta dùng giờ phút giây hoặc kết hợp cả ngày tháng năm (ví dụ như đặt đơn hàng và yêu cầu giao hàng lúc 10h sáng ngày 20/12/2023, thì lúc này ta dùng vừa ngày tháng năm vừa giờ phút giây. hoặc tách ra làm 2 thuộc tính, thuộc tính chỉ chứa ngày tháng năm và thuộc tính chỉ chứa giờ phút giây), Ví dụ dưới đây là phần mềm Tracking Tasks có sử dụng QDateEdit và QTimeEdit, cuối bài học này Tui sẽ trình bày chi tiết cách thiết kế và lập trình phần mềm này:

Trước khi vào coding ta xem một số thuộc tính, phương thức, signal thường dùng của widget này:

Thuộc tính, phương thức, signalÝ nghĩa chức năng
QTimeEdit(self)Constructor để tạo đối tượng QTimeEdit. 
time()Hàm trả về đối tượng QTime. Do đó để chuyển về Python datetime.time ta dùng hàm toPyTime()
minimumTimeThuộc tính thiết lập giờ nhỏ nhất
maximumTimeThuộc tính thiết lập giờ lớn nhất
setDisplayFormat(format)Chuỗi format hiển thị giờ.
Ví dụ:
setDisplayFormat(“HH:mm:ss AP”)
editingFinishedsignal để lắng nghe khi người dùng kết thúc quá trình lựa chọn giờ (nhấn enter)
timeChangedsignal để lắng nghe khi người dùng đang thay đổi lựa chọn giờ

Dưới đây là các bước khai báo và sử dụng QTimeEdit:

Bước 1: Khai báo và khởi tạo đối tượng QTimeEdit

self.time_edit = QTimeEdit(self)

Bước 2: Thiết lập các giá trị cho các thuộc tính

time=QTime(10,15,35)
self.time_edit.setTime(time)
self.time_edit.setDisplayFormat("HH:mm:ss AP")

Mã lệnh trên tạo đối tượng QTime(10,15,35). 10 giờ, 15 phút, 35 giây.

hàm setTime để thiết lập giờ của đối tượng QTimeEdit.

Định dạng “HH:mm:ss AP” để thiết lập hiển thị giờ cho QTimeEdit thông qua hàm setDisplayFormat.

  • H là định dạng 24 giờ, Ta dùng HH để hiển thị giờ bổ sung thêm số 0 đằng trước khi giờ <10. Ví dụ như 8 giờ thì hiển thị 08. Ngoài ra nếu dùng h là định dạng 12 giờ.
  • m là định dạng phút, ta dùng mm để hiển thị phút bổ sung thêm số 0 đằng trước khi phút <10. Ví dụ 9 phút thì hiển thị 09
  • s là định dạng giây, ta dùng ss để hiển thị giây bổ sung thêm số 0 đằng trước khi giây <10. Ví dụ 5 giây thì hiển thị 05
  • AP là đại diện hiển thị AM hay PM

Bước 3: Thiết lập signal cho QTimeEdit khi người sử dụng lựa chọn xong Giờ

self.time_edit.editingFinished.connect(self.update_time)
def update_time(self):
    value = self.time_edit.time()
    print(str(value.toPyTime()))

Hàm time() sẽ trả về dữ liệu có kiểu QTime, nên muốn đưa về Python time thì ta gọi thêm hàm toPyTime()

Tuy nhiên editingFinished signal thì người dùng cần nhấn phím Enter để xác nhận. Do đó để tiện lại ta có thể dùng timeChanged signal, bất cứ khi nào người dùng lựa chọn Giờ trên giao diện thì ta sẽ lấy được dữ liệu này:

self.time_edit.timeChanged.connect(self.change_time)
def change_time(self):
    value = self.time_edit.time()
    print(str(value.toPyTime()))

Dưới đây là Full code minh họa:

import sys

from PyQt6.QtCore import QTime
from PyQt6.QtWidgets import QApplication, QWidget, QTimeEdit, QLabel, QFormLayout


class MainWindow(QWidget):
    def __init__(self):
        super().__init__()

        self.setWindowTitle('PyQt QTimeEdit')
        self.setMinimumWidth(200)

        # create a grid layout
        layout = QFormLayout()
        self.setLayout(layout)

        self.time_edit = QTimeEdit(self)
        time=QTime(10,15,35)
        self.time_edit.setTime(time)
        self.time_edit.setDisplayFormat("HH:mm:ss A")

        self.time_edit.editingFinished.connect(self.update_time)
        self.time_edit.timeChanged.connect(self.change_time)

        self.result_label = QLabel('', self)

        layout.addRow('Time:', self.time_edit)
        layout.addRow(self.result_label)
    def update_time(self):
        value = self.time_edit.time()
        print(str(value.toPyTime()))
        self.result_label.setText(str(value.toPyTime()))
        pass
    def change_time(self):
        value = self.time_edit.time()
        print(str(value.toPyTime()))
        self.result_label.setText(str(value.toPyTime()))
app = QApplication([])
window = MainWindow()
window.show()
app.exec()

Chạy mã lệnh trên ta có kết quả:

Bây giờ Tui sẽ hướng dẫn các bạn cách thức áp dụng QDateEdit và QTimeEdit vào việc phát triển phần mềm Tracking Tasks như dưới đây:

Mô tả các chức năng của phần mềm:

  • Chương trình cung cấp giao diện để nhập các Task, thông tin chi tiết của Task bao gồm: Tiêu đề, nội dung thực hiện, ngày hết hạn, giờ hết hạn, và trạng thái đã hoàn thành hay chưa
  • Khi nhập Task thành công thì các Task sẽ được đưa vào QListWidget đồng thời các Task nào chưa hoàn thành thì có biểu tượng màu đó, task nào đã hoàn thành thì có biểu tượng màu xanh
  • Chương trình sẽ lưu và phục hồi dữ liệu bằng serialize và deserialize JSON ARRAY
  • Chương trình cung cấp chức năng chỉnh sửa Task
  • Chương trình cung cấp nút xóa, sẽ xóa những Task được checked trong QListWidget
  • Chương trình cung cấp chức năng selection trong QListWidget, khi người dùng bấm chuột hoặc di chuyển item trong QListWidget thì chi tiết của Task đang lựa chọn sẽ được hiển thị lại trong các widget bên màn hình bên phải.

Ta tạo một dự án tên “LearnQTimeEdit” trong Pycharm, có cấu trúc tập tin và thư mục như dưới đây:

  • Thư mục “images” lưu trữ các icon của phần mềm như Window Icon, Add Icon, Save Icon, Remove Icon, Icon cho 2 trạng thái Finished và not Finished task
  • Task.py Là file lưu mã lệnh của lớp Task để định nghĩa một task bao gồm: title, content, deadline, deadlinetime, isfinish
  • Tasks.py Là file lưu mã lệnh để xử lý danh sách các Task, chẳng hạn các chức năng: thêm, truy suất, sửa, xóa Task
  • FileFactory.py” là mã lệnh dùng để ghi toàn bộ dữ liệu xuống ổ cứng với định dạng JSonArray, cũng như phục hồi và mô hình hóa lại dữ liệu hướng đối tượng
  • MainWindow.ui” là file thiết kế giao diện bằng Qt Designer
  • MainWindow.py” là file python code được generate tự động từ “MainWindow.ui”. Cách thiết kế và tích hợp công cụ tự động Generate đã được hướng dẫn rất chi tiết ở những bài học đầu tiên, các bạn cần xem lại
  • MainWindowEx.py” là mã lệnh ta bổ sung, kế thừ từ lớp được generate trong MainWindow.py để xử lý các sự kiện người dùng
  • MyApp.py” là mã lệnh để thực thi chương trình
  • database.json” là file JSonArray để lưu trữ các dữ liệu mà người dùng thao tác trên phần mềm.

Bước 1: Viết mã lệnh cho “Task.py”, mã lệnh được thể hiện như dưới đây:

class Task:
    def __init__(self,title,content,deadline,deadlinetime,isfinish):
        self.title=title
        self.content=content
        self.deadline=deadline
        self.deadlinetime=deadlinetime
        self.isfinish=isfinish
        pass
    def __str__(self):
        return self.title

Ta khai báo lớp Task cùng với constructor và các đối số chính xác như trên để việc serialize và deserialize JSon data được chính xác.

Bước 2: Viết mã lệnh cho “Tasks.py” để xử lý danh sách các Task, mã lệnh được thể hiện như dưới đây:

from Task import Task
class Tasks:
    def __init__(self):
        self.lists=[]
    def item(self,index)->Task:
        return self.lists[index]
    def add(self,task):
        self.lists.append(task)
    def addAll(self,tasks):
        for i in range(len(tasks)):
            task=tasks[i]
            self.add(task)
    def index(self,task):
        i=self.lists.index(task)
        return i
    def update(self,index,task)->Task:
        self.lists[index]=task
        return self.lists[index]
    def removeByIndex(self,index)->Task:
        return self.lists.pop(index)
    def removeByItem(self,item):
        self.lists.remove(item)
    def clear(self):
        self.lists.clear()
    def size(self):
        return len(self.lists)

Lớp Tasks ở trên Tui thiết kế các phương thức để ta có thể sử dụng lưu danh sách, cũng như tương tác các danh sách chẳng hạn:

  • Hàm item(self,index) là hàm trả về Task theo index
  • Hàm index(self,task) là hàm trả về vị trí của task trong danh sách
  • Hàm add(self,task) để thêm mới một task vào danh sách
  • Hàm addAll(self,tasks) để thêm mới nhiều task vào danh sách
  • Hàm update(self,index,task) để cập nhật dữ liệu cho Task
  • Hàm removeByIndex(self,index) để xóa 1 task ra khỏi danh sách tại ví trí index
  • Hàm removeByItem(self,item) để xóa 1 task ra khỏi danh sách tại ví trí item
  • Hàm clear(self) xóa toàn bộ task ra khỏi danh sách
  • Hàm size(self) trả về kích thước (số lượng) các phần tử trong danh sách

Bước 3: Viết mã lệnh cho “FileFactory.py“, class này dùng để serialize và deserialize dữ liệu với định dạng JSonArray:

import json
import os

class FileFactory:
    #path: path to serialize array of product
    #arrData: array of Product
    def writeData(self,path,arrData):
        jsonString = json.dumps([item.__dict__ for item in arrData],default=str)
        jsonFile = open(path, "w")
        jsonFile.write(jsonString)
        jsonFile.close()
    #path: path to deserialize array of Product
    #ClassName: Product
    def readData(self,path,ClassName):
        if os.path.isfile(path) == False:
            return []
        file = open(path, "r")
        # Reading from file
        self.arrData = json.loads(file.read(), object_hook=lambda d: ClassName(**d))
        file.close()
        return self.arrData

Lưu ý “FileFactory.py” là có mã lệnh không đổi, sử dụng chung cho mọi dự án muốn lưu dữ liệu dạng JSONARRAY, nó đã được trình bày ở bài học trước.

Bước 4: Dùng Qt Designer để thiết kế giao diện “MainWindow.ui” cho phần mềm.

Các bạn kéo thả các Widget và đặt tên giống như hình minh họa ở trên.

Bước 5: Dùng công cụ để Generate Python code cho “MainWindow.ui” ta có file “MainWindow.py”:

# Form implementation generated from reading ui file 'MainWindow.ui'
#
# Created by: PyQt6 UI code generator 6.5.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(566, 385)
        font = QtGui.QFont()
        font.setPointSize(11)
        MainWindow.setFont(font)
        icon = QtGui.QIcon()
        icon.addPixmap(QtGui.QPixmap("images/ic_logo.jpg"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        MainWindow.setWindowIcon(icon)
        MainWindow.setStyleSheet("")
        self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
        self.centralwidget.setStyleSheet("")
        self.centralwidget.setObjectName("centralwidget")
        self.groupBox = QtWidgets.QGroupBox(parent=self.centralwidget)
        self.groupBox.setGeometry(QtCore.QRect(10, 30, 211, 311))
        self.groupBox.setStyleSheet("background-color: rgb(230, 255, 246);")
        self.groupBox.setObjectName("groupBox")
        self.verticalLayout = QtWidgets.QVBoxLayout(self.groupBox)
        self.verticalLayout.setObjectName("verticalLayout")
        self.listWidgetTask = QtWidgets.QListWidget(parent=self.groupBox)
        self.listWidgetTask.setObjectName("listWidgetTask")
        self.verticalLayout.addWidget(self.listWidgetTask)
        self.groupBox_2 = QtWidgets.QGroupBox(parent=self.centralwidget)
        self.groupBox_2.setGeometry(QtCore.QRect(230, 30, 321, 241))
        self.groupBox_2.setStyleSheet("background-color: rgb(248, 255, 215);")
        self.groupBox_2.setObjectName("groupBox_2")
        self.label_2 = QtWidgets.QLabel(parent=self.groupBox_2)
        self.label_2.setGeometry(QtCore.QRect(10, 30, 31, 16))
        self.label_2.setObjectName("label_2")
        self.label_3 = QtWidgets.QLabel(parent=self.groupBox_2)
        self.label_3.setGeometry(QtCore.QRect(10, 90, 51, 16))
        self.label_3.setObjectName("label_3")
        self.lineEditTitle = QtWidgets.QLineEdit(parent=self.groupBox_2)
        self.lineEditTitle.setGeometry(QtCore.QRect(60, 30, 241, 20))
        self.lineEditTitle.setObjectName("lineEditTitle")
        self.textEditContent = QtWidgets.QTextEdit(parent=self.groupBox_2)
        self.textEditContent.setGeometry(QtCore.QRect(60, 60, 241, 81))
        self.textEditContent.setObjectName("textEditContent")
        self.radFinished = QtWidgets.QRadioButton(parent=self.groupBox_2)
        self.radFinished.setGeometry(QtCore.QRect(60, 210, 83, 18))
        self.radFinished.setObjectName("radFinished")
        self.radNotFinished = QtWidgets.QRadioButton(parent=self.groupBox_2)
        self.radNotFinished.setGeometry(QtCore.QRect(150, 210, 101, 20))
        self.radNotFinished.setObjectName("radNotFinished")
        self.label_4 = QtWidgets.QLabel(parent=self.groupBox_2)
        self.label_4.setGeometry(QtCore.QRect(10, 150, 51, 20))
        self.label_4.setObjectName("label_4")
        self.dateEditDeadline = QtWidgets.QDateEdit(parent=self.groupBox_2)
        self.dateEditDeadline.setGeometry(QtCore.QRect(60, 150, 110, 22))
        self.dateEditDeadline.setCalendarPopup(True)
        self.dateEditDeadline.setDate(QtCore.QDate(2023, 12, 14))
        self.dateEditDeadline.setObjectName("dateEditDeadline")
        self.label_5 = QtWidgets.QLabel(parent=self.groupBox_2)
        self.label_5.setGeometry(QtCore.QRect(10, 180, 41, 16))
        self.label_5.setObjectName("label_5")
        self.timeEditDeadline = QtWidgets.QTimeEdit(parent=self.groupBox_2)
        self.timeEditDeadline.setGeometry(QtCore.QRect(60, 180, 111, 22))
        self.timeEditDeadline.setObjectName("timeEditDeadline")
        self.label = QtWidgets.QLabel(parent=self.centralwidget)
        self.label.setGeometry(QtCore.QRect(30, 0, 491, 31))
        font = QtGui.QFont()
        font.setFamily("Times New Roman")
        font.setPointSize(18)
        font.setBold(False)
        font.setItalic(False)
        font.setWeight(9)
        self.label.setFont(font)
        self.label.setStyleSheet("font: 75 18pt \"Times New Roman\";\n"
"color: rgb(0, 0, 255);")
        self.label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
        self.label.setObjectName("label")
        self.groupBox_3 = QtWidgets.QGroupBox(parent=self.centralwidget)
        self.groupBox_3.setGeometry(QtCore.QRect(230, 280, 321, 61))
        self.groupBox_3.setStyleSheet("background-color: rgb(255, 216, 249);")
        self.groupBox_3.setObjectName("groupBox_3")
        self.pushButtonNew = QtWidgets.QPushButton(parent=self.groupBox_3)
        self.pushButtonNew.setGeometry(QtCore.QRect(20, 20, 71, 31))
        self.pushButtonNew.setStyleSheet("background-color: rgb(255, 255, 127);")
        icon1 = QtGui.QIcon()
        icon1.addPixmap(QtGui.QPixmap("images/ic_add.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.pushButtonNew.setIcon(icon1)
        self.pushButtonNew.setIconSize(QtCore.QSize(24, 24))
        self.pushButtonNew.setObjectName("pushButtonNew")
        self.pushButtonSave = QtWidgets.QPushButton(parent=self.groupBox_3)
        self.pushButtonSave.setGeometry(QtCore.QRect(120, 20, 71, 31))
        self.pushButtonSave.setStyleSheet("background-color: rgb(255, 255, 127);")
        icon2 = QtGui.QIcon()
        icon2.addPixmap(QtGui.QPixmap("images/ic_save.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.pushButtonSave.setIcon(icon2)
        self.pushButtonSave.setIconSize(QtCore.QSize(24, 24))
        self.pushButtonSave.setObjectName("pushButtonSave")
        self.pushButtonRemove = QtWidgets.QPushButton(parent=self.groupBox_3)
        self.pushButtonRemove.setGeometry(QtCore.QRect(220, 20, 81, 31))
        self.pushButtonRemove.setStyleSheet("background-color: rgb(255, 255, 127);")
        icon3 = QtGui.QIcon()
        icon3.addPixmap(QtGui.QPixmap("images/ic_remove.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.pushButtonRemove.setIcon(icon3)
        self.pushButtonRemove.setIconSize(QtCore.QSize(24, 24))
        self.pushButtonRemove.setObjectName("pushButtonRemove")
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 566, 22))
        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", "Tran Duy Thanh - Tracking Tasks"))
        self.groupBox.setTitle(_translate("MainWindow", "List of Tasks:"))
        self.groupBox_2.setTitle(_translate("MainWindow", "Details of Task:"))
        self.label_2.setText(_translate("MainWindow", "Title:"))
        self.label_3.setText(_translate("MainWindow", "Content:"))
        self.radFinished.setText(_translate("MainWindow", "Finished"))
        self.radNotFinished.setText(_translate("MainWindow", "Not Finished"))
        self.label_4.setText(_translate("MainWindow", "Deadline:"))
        self.label_5.setText(_translate("MainWindow", "Time:"))
        self.label.setText(_translate("MainWindow", "Tracking Tasks"))
        self.groupBox_3.setTitle(_translate("MainWindow", "Actions:"))
        self.pushButtonNew.setText(_translate("MainWindow", "New"))
        self.pushButtonSave.setText(_translate("MainWindow", "Save"))
        self.pushButtonRemove.setText(_translate("MainWindow", "Remove"))

Bước 6: Ta tạo file “MainWindowEx.py” kế thừa từ “MainWindow.py” và tiến hành viết các sự kiện người dùng tương tác:

import datetime

from PyQt6.QtCore import Qt
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import QMessageBox, QListWidgetItem

from FileFactory import FileFactory
from MainWindow import Ui_MainWindow
from Task import Task
from Tasks import Tasks

class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        super().__init__()
        self.tasks=Tasks()
        self.fileFactory = FileFactory()
        self.selectedTask=None

Trong MainWindowEx ta bổ sung constructor để khởi tạo 3 biến đối tượng:

  • Biến đối tượng tasks để lưu trữ danh sách hướng đối tượng các Task mà người sử dụng tương tác trên giao diện
  • Biến đối tượng fileFactory là biến dùng để gọi đối tượng FileFactory nhằm sử dụng Serialize và Deserialize dữ liệu với định dạng JSonArray.
  • Biến đối tượng selectedTask để lưu trữ Task đang lựa chọn trên QListWidget (Task hiện hành)

Tiếp theo ta Override hàm setupUi để khởi tạo giao diện cũng như gán các signal và slot cho giao diện tương tác:

def setupUi(self, MainWindow):
    super().setupUi(MainWindow)
    self.MainWindow=MainWindow
    arrData= self.fileFactory.readData("database.json", Task)
    self.tasks.addAll(arrData)
    self.showTasksIntoQListWidget()
    self.pushButtonNew.clicked.connect(self.processNew)
    self.pushButtonSave.clicked.connect(self.processSave)
    self.pushButtonRemove.clicked.connect(self.processRemove)
    self.listWidgetTask.itemSelectionChanged.connect(self.processItemSelection)

Khi khởi động phần mềm, chương trình sẽ đọc dữ liệu (deserialize) “database.json” thành danh sách hướng đối tượng Task và biến đối tượng tasks sẽ lưu trữ danh sách dữ liệu này.

Sau đó hàm showTasksIntoQListWidget() sẽ được triệu gọi để hiển thị toàn bộ Task trong biến đối tượng tasks lên giao diện QListWidget:

def showTasksIntoQListWidget(self):
    self.listWidgetTask.clear()
    for index in range(self.tasks.size()):
        task=self.tasks.item(index)
        item=QListWidgetItem()
        item.setData(Qt.ItemDataRole.UserRole, task)
        item.setText(str(task))
        item.setCheckState(Qt.CheckState.Unchecked)
        if task.isfinish==True:
            item.setIcon(QIcon("images/ic_finished.png"))
        else:
            item.setIcon(QIcon("images/ic_notfinished.png"))
        if isinstance(task.deadline,str):
            task.deadline= datetime.date.fromisoformat(task.deadline)
        if isinstance(task.deadlinetime,str):
            task.deadlinetime=datetime.time.fromisoformat(task.deadlinetime)
        self.listWidgetTask.addItem(item)

Chương trình dùng vòng lặp để duyệt qua các Task trong biến đối tượng tasks, sau đó sẽ tạo các đối tượng QListWidgetItem để hiển thị lên giao diện. Nếu Task nào là FINISHED thì dùng icon finished (có màu xanh), chưa finished thì icon màu đỏ.

Ngoài ra vì dữ liệu ngày tháng khi serialize xuống ổ cứng với định dạng JSon thì nó là chuỗi, nên ta dùng hàm isinstance để kiểm tra xem thuộc tính deadline có phải đang là chuỗi hay không, nếu là chuỗi thì ta ép kiểu qua kiểu Python Date bằng hàm: datetime.date.fromisoformat(task.deadline)

Tương tự như vậy ta cũng viết mã lệnh để kiểm tra thuộc tính deadlinetime, ta chuyển qua Python time bằng hàm: datetime.time.fromisoformat(task.deadlinetime)

-Hàm processNew dùng để xóa các dữ liệu đang nhập trên giao diện và focus vào ô title để người dùng nhập dữ liệu mới được nhanh chóng hơn:

def processNew(self):
    self.lineEditTitle.setText("")
    self.textEditContent.setText("")
    self.dateEditDeadline.setSpecialValueText(None)
    self.radFinished.setAutoExclusive(False)
    self.radNotFinished.setAutoExclusive(False)
    self.radFinished.setChecked(False)
    self.radNotFinished.setChecked(False)
    self.radFinished.setAutoExclusive(True)
    self.radNotFinished.setAutoExclusive(True)
    self.selectedTask=None
    self.lineEditTitle.setFocus()

Ta có một vài lưu ý với hàm processNew:

  • Bởi vì tính chất đặc biệt của QRadioButton nên ta gọi hàm setAutoExclusive(False) trước khi gỡ bỏ các checked của RadioButton, sau đó ta gọi setAutoExclusive(True)
  • Biến đối tượng selectedTask=None để đảm bảo lúc này chưa chọn Task nào trên giao diện, vì lúc này ta đang thêm mới 1 task.

-Hàm processSave dùng để lưu dữ liệu, nếu selectedTask mà khác None lưu cập nhật, còn selectedTask =None thì lưu mới:

def processSave(self):
    title=self.lineEditTitle.text()
    content=self.textEditContent.toPlainText()
    date=self.dateEditDeadline.date().toPyDate()
    time=self.timeEditDeadline.time().toPyTime()
    isFinished=self.radFinished.isChecked()
    task=Task(title,content,date,time,isFinished)
    if self.selectedTask==None:
        self.tasks.add(task)
    else:
        index=self.tasks.index(self.selectedTask)
        self.tasks.update(index,task)
    self.selectedTask = task
    self.showTasksIntoQListWidget()
    self.fileFactory.writeData("database.json",self.tasks.lists)

Sau khi thêm một Task mới thì chương trình sẽ cập nhật danh sách và lưu xuống tập tin database.json

– Hàm processRemove dùng để xóa các QListWidgetItem được checked trên giao diện ra khỏi QListWidget:

def processRemove(self):
    answer = QMessageBox.question(
        self.MainWindow,
        'Confirmation',
        'Do you want to remove checked Items?',
        QMessageBox.StandardButton.Yes |
        QMessageBox.StandardButton.No
    )
    if answer == QMessageBox.StandardButton.No:
        return
    size=self.listWidgetTask.count()
    for index in range(size-1,-1,-1):
        item=self.listWidgetTask.item(index)
        if item.checkState()==Qt.CheckState.Checked:
            self.tasks.removeByIndex(index)
    self.selectedTask = None
    self.showTasksIntoQListWidget()
    self.fileFactory.writeData("database.json", self.tasks.lists)

-Mã lệnh vòng lặp ở trên Tui chạy ngược lại để khi xóa trong danh sách không bị ảnh hưởng thứ tự.

-Sau khi xóa thành công thì cũng cập nhật lại danh sách cho biến đối tượng tasks, hiển thị lại dữ liệu trên giao diện, đồng thời cũng lưu lại tập tin database.json.

Chương trình sẽ hiển thị cửa sổ xác nhận có muốn xóa hay không, nếu đồng ý xóa thì chương trình sẽ xóa, sau đó sẽ lưu (serialize) lại dữ liệu xuống JSonArray cũng như hiển thị lại giao diện. Hình minh họa xóa:

-Cuối cùng là hàm/signal “processItemSelection“, hàm này sẽ lắng nghe người dùng đang chọn QListWidgetItem nào trên giao diện và hiển thị thông tin chi tiết lên các ô dữ liệu. Người dùng có thể dùng phím mũi tên di chuyển hoặc click chuột vào item bất kỳ:

def processItemSelection(self):
    row=self.listWidgetTask.currentRow()
    task=self.tasks.item(row)
    self.lineEditTitle.setText(task.title)
    self.textEditContent.setText(task.content)
    self.dateEditDeadline.setDate(task.deadline)
    self.timeEditDeadline.setTime(task.deadlinetime)
    if task.isfinish:
        self.radFinished.setChecked(True)
        self.radNotFinished.setChecked(False)
    else:
        self.radFinished.setChecked(False)
        self.radNotFinished.setChecked(True)
    self.selectedTask=task

-Tùy từng thông tin của các task đang chọn mà chương trình sẽ hiển thị dữ liệu lên các widget cho phù hợp.

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

import datetime

from PyQt6.QtCore import Qt
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import QMessageBox, QListWidgetItem

from FileFactory import FileFactory
from MainWindow import Ui_MainWindow
from Task import Task
from Tasks import Tasks

class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        super().__init__()
        self.tasks=Tasks()
        self.fileFactory = FileFactory()
        self.selectedTask=None
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow
        arrData= self.fileFactory.readData("database.json", Task)
        self.tasks.addAll(arrData)
        self.showTasksIntoQListWidget()
        self.pushButtonNew.clicked.connect(self.processNew)
        self.pushButtonSave.clicked.connect(self.processSave)
        self.pushButtonRemove.clicked.connect(self.processRemove)
        self.listWidgetTask.itemSelectionChanged.connect(self.processItemSelection)
    def showTasksIntoQListWidget(self):
        self.listWidgetTask.clear()
        for index in range(self.tasks.size()):
            task=self.tasks.item(index)
            item=QListWidgetItem()
            item.setData(Qt.ItemDataRole.UserRole, task)
            item.setText(str(task))
            item.setCheckState(Qt.CheckState.Unchecked)
            if task.isfinish==True:
                item.setIcon(QIcon("images/ic_finished.png"))
            else:
                item.setIcon(QIcon("images/ic_notfinished.png"))
            if isinstance(task.deadline,str):
                task.deadline= datetime.date.fromisoformat(task.deadline)
            if isinstance(task.deadlinetime,str):
                task.deadlinetime=datetime.time.fromisoformat(task.deadlinetime)
            self.listWidgetTask.addItem(item)
    def processNew(self):
        self.lineEditTitle.setText("")
        self.textEditContent.setText("")
        self.dateEditDeadline.setSpecialValueText(None)
        self.radFinished.setAutoExclusive(False)
        self.radNotFinished.setAutoExclusive(False)
        self.radFinished.setChecked(False)
        self.radNotFinished.setChecked(False)
        self.radFinished.setAutoExclusive(True)
        self.radNotFinished.setAutoExclusive(True)
        self.selectedTask=None
        self.lineEditTitle.setFocus()
    def processSave(self):
        title=self.lineEditTitle.text()
        content=self.textEditContent.toPlainText()
        date=self.dateEditDeadline.date().toPyDate()
        time=self.timeEditDeadline.time().toPyTime()
        isFinished=self.radFinished.isChecked()
        task=Task(title,content,date,time,isFinished)
        if self.selectedTask==None:
            self.tasks.add(task)
        else:
            index=self.tasks.index(self.selectedTask)
            self.tasks.update(index,task)
        self.selectedTask = task
        self.showTasksIntoQListWidget()
        self.fileFactory.writeData("database.json",self.tasks.lists)
    def processRemove(self):
        answer = QMessageBox.question(
            self.MainWindow,
            'Confirmation',
            'Do you want to remove checked Items?',
            QMessageBox.StandardButton.Yes |
            QMessageBox.StandardButton.No
        )
        if answer == QMessageBox.StandardButton.No:
            return
        size=self.listWidgetTask.count()
        for index in range(size-1,-1,-1):
            item=self.listWidgetTask.item(index)
            if item.checkState()==Qt.CheckState.Checked:
                self.tasks.removeByIndex(index)
        self.selectedTask = None
        self.showTasksIntoQListWidget()
        self.fileFactory.writeData("database.json", self.tasks.lists)
    def processItemSelection(self):
        row=self.listWidgetTask.currentRow()
        task=self.tasks.item(row)
        self.lineEditTitle.setText(task.title)
        self.textEditContent.setText(task.content)
        self.dateEditDeadline.setDate(task.deadline)
        self.timeEditDeadline.setTime(task.deadlinetime)
        if task.isfinish:
            self.radFinished.setChecked(True)
            self.radNotFinished.setChecked(False)
        else:
            self.radFinished.setChecked(False)
            self.radNotFinished.setChecked(True)
        self.selectedTask=task
    def show(self):
        self.MainWindow.show()

– Bước 7: Cuối cùng ta tạo lớp “MyApp.py” và viết mã lệnh như dưới đây để khởi chạy chương trình:

from PyQt6.QtWidgets import QApplication, QMainWindow

from MainWindowEx import MainWindowEx

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

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

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

https://www.mediafire.com/file/p8f1hbqvr7w7o6s/LearnQTimeEdit.rar/file

Như vậy tới đây Tui đã trình bày xong lý thuyết cũng như kỹ thuật sử dụng QTimeEdit , củng cố bài cũ QDateEdit và ứng dụng vào quản lý Task, củng cố lại các kiến thức liên quan tới lập trình hướng đối tượng, cách serialize và deserialize đối tượng ra JSonArray, củng cố lại kiến thức và kỹ thuật liên quan tới QListWidget để hiển thị và tương tác dữ liệu trên giao diện.

Các bạn cố gắng thực hành lại bài này nhiều lần để nắm rõ hơn về lập trình hướng đối tượng, cách tạo các lớp độc lập để tái sử dụng tốt hơn

Bài học sau Tui sẽ trình bày về QDateTimeEdit để tạo ra widget cho người dùng vừa nhập Ngày tháng năm vừa nhập giờ phú giây, nó cũng là một trong các Widget quan trọng và phổ biến, được sử dụng thường xuyên trong các phần mềm. Các bạn chú ý theo dõi

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