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

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

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

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

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

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

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

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

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

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

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

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

import typing

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


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

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

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

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

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

import json
import os

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

# Form implementation generated from reading ui file 'MainWindow.ui'
#
# Created by: PyQt6 UI code generator 6.4.2
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again.  Do not edit this file unless you know what you are doing.


from PyQt6 import QtCore, QtGui, QtWidgets


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    self.employees.clear()

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

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

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

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

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

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

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

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

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

        self.employees.clear()

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

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

from PyQt6.QtWidgets import QApplication, QMainWindow

from MainWindowEx import MainWindowEx

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

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

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

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

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

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

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

Leave a Reply