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

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

Leave a Reply