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.