Bài này Tui sẽ trình bày về ứng dụng kiến trúc Model View trong tương tác và hiển thị cơ sở dữ liệu dạng SQL, cụ thể là SQLite Database, cung cấp tính năng lọc dữ liệu theo dòng, lọc dữ liệu theo cột, cũng như cập nhật dữ liệu ngay trong QTableView khi có sự thay đổi, bạn có thể áp dụng để làm các loại cơ sở dữ liệu khác như PostGreSQL, MySQL…
Khi sử dụng PyQt để kết nối tới các loại cơ sở dữ liệu khác nhau ta cần kiểm tra xem PyQt đã có các loại Drivers nào rồi, nếu chưa có ta cần cài đặt.
Để kiểm tra các loại Drivers này ta dùng lệnh dưới đây:
from PyQt6.QtSql import QSqlDatabase
drivers=QSqlDatabase.drivers()
print(drivers)
Giả sử khi chạy lệnh trên, bạn có mảng các Driver sau trong máy tính:
Máy tính có Driver nào thì ta mới sử dụng được Driver đó. Như vậy tùy vào nhu cầu sử dụng mà ta cần cài Driver phù hợp nếu PyQt chưa support trong máy của bạn.
Để kết nối tới các Remote Server ví dụ như MYSQL, Microsoft SQL Server… thì bạn dùng tập các mã lệnh đưới đây:
db = QSqlDatabase('<driver>')
db.setHostName('<localhost>')
db.setPort('<port number>')
db.setDatabaseName('<databasename>')
db.setUserName('<username>')
db.setPassword('<password>')
db.open()
Trong bài học này chúng ta kết nối với Local server, cụ thể là SQLite Database. Ví dụ ta muốn kết nối tới cơ sở dữ liệu “Chinook_Sqlite.sqlite” đặt trong cùng thư mục dự án, thì chúng ta sẽ sử dụng các lệnh sau:
Sau khi kết nối tới SQLite Database thành công, ta có thể tạo đối tượng QSqlTableModel để truy suất tới bảng dữ liệu trong SQLite Database, ví dụ bảng “Employee“:
Hàm setEditStrategy() nhận vào enum để xác định cách thức cập nhật dữ liệu trên model:
Enum
Ý nghĩa chức năng
QSqlTableModel.EditStrategy.OnFieldChange
Mô hình sẽ tự động cập nhật dữ liệu khi người dùng di chuyển ra khỏi ô nhập liệu
QSqlTableModel.EditStrategy.OnRowChange
Mô hình sẽ tự động cập nhật dữ liệu khi người dùng di chuyển ra khỏi dòng nhập liệu
QSqlTableModel.EditStrategy.OnManualSubmit
Chương trình không tự động cập nhật dữ liệu ngay, mà nó lưu những dòng dữ liệu được chỉnh sửa vào caches. Và khi muốn cập nhật thì ta gọi hàm submitAll() hoặc nếu hủy sự thay đổi thì ta gọi hàm revertAll()
Để hiển thị dữ liệu lên QTableView ta gọi lệnh sau:
self.tableView.setModel(self.model)
Để filter dữ liệu theo các thuộc tính ta viết lệnh:
def processFilterName(self,s):
filter_str = 'LastName LIKE "%{}%" or FirstName LIKE "%{}%"'.format(s,s)
self.model.setFilter(filter_str)
Ví dụ mã lệnh ở trên là filter dữ liệu theo LastName và FirstName. Chẳng hạn như lọc ra các Employee mà FirstName và LastName có chứa từ “an”.
Dưới đây là bảng ý nghĩa cú pháp của Filter:
Cú pháp(Pattern)
Ý nghĩa chức năng
columnName=“{}”
Lọc chính xác dữ liệu mà thuộc tính
columnName LIKE “{}%”
Lọc các dòng dữ liệu mà nó chứa chuỗi so khớp đằng trước nó
columnName LIKE “%{}”
Lọc các dòng dữ liệu mà nó chứa chuỗi so khớp đằng sau nó
columnName LIKE “%{}%”
Lọc ra các dòng dữ liệu mà nó có chứa chuỗi so khớp
Trong mỗi pattern ở trên, {} là chuỗi tìm kiếm mà ta phải viết:
“{}”.format(search_str)
Ngoài ra chúng ta có thể dùng QSqlQuery để lọc dữ liệu theo cột:
Mã lệnh ở trên sẽ giúp truy vấn dữ liệu nhưng chỉ lọc ra các cột EmployeeId, FirstName, LastName
Bây giờ ta đi vào chi tiết từng bước để ứng dụng kiến trúc Model View, sử dụng QSqlTableModel để truy vấn/mapping bảng dữ liệu và hiển thị Lên QTableView.
Bước 1: Tạo dự án “LearnModelViewSQLitePart5” có cấu trúc như dưới đây:
“Chinook_Sqlite.sqlite” là cơ sở dữ liệu SQLite mẫu để giới thiệu ở bài học trước
“MainWindow.ui” là file thiết kế giao diện bằng Qt Designer
“MainWindow.py” là file Generate Python code của MainWindow.ui
“MainWindowEx.py” là mã lệnh kế thừa từ Generate Python code để xử lý sự kiện người dùng, nạp giao diện, gán model
“MyApp.py” là file mã lệnh để thực thi chương trình
Bước 2: Thiết kế giao diện “MainWindow.ui” như hình dưới đây:
Ta kéo thả và đặt tên cho các Widget như trên
Bước 3: Generate Python code cho MainWindow.ui, đặt tê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(463, 327)
self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)
self.verticalLayout.setObjectName("verticalLayout")
self.label = QtWidgets.QLabel(parent=self.centralwidget)
self.label.setObjectName("label")
self.verticalLayout.addWidget(self.label)
self.lineEditFilter = QtWidgets.QLineEdit(parent=self.centralwidget)
self.lineEditFilter.setObjectName("lineEditFilter")
self.verticalLayout.addWidget(self.lineEditFilter)
self.tableView = QtWidgets.QTableView(parent=self.centralwidget)
self.tableView.setObjectName("tableView")
self.verticalLayout.addWidget(self.tableView)
self.pushButtonFilter = QtWidgets.QPushButton(parent=self.centralwidget)
self.pushButtonFilter.setObjectName("pushButtonFilter")
self.verticalLayout.addWidget(self.pushButtonFilter, 0, QtCore.Qt.AlignmentFlag.AlignLeft)
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 463, 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.label.setText(_translate("MainWindow", "Filter name:"))
self.pushButtonFilter.setText(_translate("MainWindow", "Filter Columns"))
Bước 5: Tạo “MainWindowEx.py“, kế thừa từ Generate Python code để nạp giao diện, xử lý nghiệp vụ, gán mô hình:
import os.path
from PyQt6.QtSql import QSqlDatabase, QSqlTableModel, QSqlQuery
from MainWindow import Ui_MainWindow
class MainWindowEx(Ui_MainWindow):
def setupUi(self, MainWindow):
super().setupUi(MainWindow)
self.MainWindow=MainWindow
self.loadDatabase()
self.lineEditFilter.textChanged.connect(self.processFilterName)
self.pushButtonFilter.clicked.connect(self.processFilterColumns)
Ta overrice setupUi() để nạp giao diện, gọi hiển thị dữ liệu cũng như xử lý các signal cho widget.
Tiếp theo ta viết hàm loadDatabase(), hàm này sẽ đọc SQLite Database và truy vấn bảng “Empoyee” rồi hiển thị lên QTableView, sử dụng kiến trúc Model View:
import os.path
from PyQt6.QtSql import QSqlDatabase, QSqlTableModel, QSqlQuery
from MainWindow import Ui_MainWindow
class MainWindowEx(Ui_MainWindow):
def setupUi(self, MainWindow):
super().setupUi(MainWindow)
self.MainWindow=MainWindow
self.loadDatabase()
self.lineEditFilter.textChanged.connect(self.processFilterName)
self.pushButtonFilter.clicked.connect(self.processFilterColumns)
def loadDatabase(self):
baseDir=os.path.dirname(__file__)
databasePath=os.path.join(baseDir,"Chinook_Sqlite.sqlite")
self.db=QSqlDatabase("QSQLITE")
self.db.setDatabaseName(databasePath)
self.db.open()
self.model = QSqlTableModel(db=self.db)
self.model.setTable("Employee")
self.model.select()
self.model.setEditStrategy(QSqlTableModel.EditStrategy.OnFieldChange)
self.tableView.setModel(self.model)
def processFilterName(self,s):
filter_str = 'LastName LIKE "%{}%" or FirstName LIKE "%{}%"'.format(s,s)
self.model.setFilter(filter_str)
def processFilterColumns(self):
query = QSqlQuery("SELECT EmployeeId, FirstName, LastName FROM Employee ", db=self.db)
self.model.setQuery(query)
def show(self):
self.MainWindow.show()
Bước 6: Cuối cùng ta viết mã lệnh “MyApp.py”
from PyQt6.QtWidgets import QApplication, QMainWindow
from MainWindowEx import MainWindowEx
app=QApplication([])
myWindow=MainWindowEx()
myWindow.setupUi(QMainWindow())
myWindow.show()
app.exec()
Chạy MyApp.Py ta có kết quả như mong muốn:
Như vậy là Tui đã hướng dẫn xong cách ứng dụng kiến trúc Model View trong tương tác và hiển thị cơ sở dữ liệu dạng SQL, cụ thể là SQLite Database, cung cấp tính năng lọc dữ liệu theo dòng, lọc dữ liệu theo cột, cũng như cập nhật dữ liệu ngay trong QTableView khi có sự thay đổi, bạn có thể áp dụng để làm các loại cơ sở dữ liệu khác.
Bài học sau Tui hướng dẫn các bạn cách trực quan hóa dữ liệu bằng các Chart, đây là một trong các thư viện phổ biến và quan trọng trong trình bày trực quan dữ liệu để đưa ra các đồ thị, biểu đồ. Các bạn chú ý theo dõi
Bài này Tui nâng cấp bài 28, tiếp tục ứng dụng kiến trúc Model View và bổ sung các chức năng cho QTableView:
Nạp ma trận “data” lên QTableView cùng với các cột được lưu trong mảng “columns“? Đồng thời nếu giá <100 thì tô chữ đỏ, nếu tên rỗng thì tô nền vàng:
Thêm, Sửa, Xóa dữ liệu ngay trên giao diện QTableView thông qua việc cập nhật mã lệnh mới cho TableModel.py.
Xử lý signal item selection change
Cách khai báo và sử dụng Enum trong Python
Xử lý Context Menu cho Widget để Chèn Đầu, Chèn Cuối, Chèn Trước, Chèn Sau dữ liệu vào trong QTableView, cũng như thao tác xóa dữ liệu ra khỏi QTableView.
Dùng partial function để triệu gọi các signals/slots có parameter
Import và Export dữ liệu trong model ra JSON ARRAY
Bài này nhiều chức năng, khá khó và tuy nhiên nó có tính ứng dụng cao trong thực tế. Các bạn có gắng thực hành theo từng bước để sau náy có thể áp dụng nó và các tình huống khác nhau.
Bước 1: Tạo dự án “LearnModelViewPart4” có cấu trúc như hình dưới đây:
Thư mục “images” chứa các hình ảnh và biểu tượng cho các QAction và Window Icon
Mã lệnh “InsertBehavior.py” là enum để định nghĩa các kiểu insert trước, sau, đầu, cuối
Mã lệnh “TableModel.py” là lớp Model View, lớp trung tâm của dự án
“MainWindow.ui” là giao diện thiết kế phần mềm
Mã lệnh “MainWindow.py” là Generate Python code từ giao diện
“MainWindowEx.py” là mã lệnh kế thừa từ lớp Generate Python code để xử lý nạp giao diện cũng như xử lý sự kiện người dùng, gán Model.
“MyApp.py” là mã lệnh thực thi chương trình
Bước 2: Tạo mã lệnh enum “InsertBehavior.py”
from enum import Enum
class InsertBehavior(Enum):
INSERT_FIRST = 0
INSERT_LAST = 1
INSERT_ABOVE = 2
INSERT_BELOW=3
Trong InsertBehavior Tui định nghĩa 4 enum, và nó sẽ được dùng cho xử lý Context Menu trên QTableView.
Context Menu trên QTableView (nhấn chuột phải vào QTableView), chương trình sẽ hiển thị lên Context Menu ở trên. Có chức năng tương ứng với enum:
Insert First (INSERT_FIRST = 0), chương trình sẽ thêm một dòng trống ở đầu QTableView
Insert Last (INSERT_LAST = 1), chương trình sẽ thêm một dòng trống ở cuối QTableView
Insert Above (INSERT_ABOVE = 2), chương trình sẽ thêm một dòng trống ở trước dòng đang lựa chọn
Insert Below (INSERT_BELOW=3), chương trình sẽ thêm một dòng trống ở sau dòng đang lựa chọn
Ngoài ra Tui cũng bổ sung thêm 1 Context Menu khi người dùng nhấn chuột phải vào QTableView nhưng không nhấn vào dòng nào:
Trong trường hợp này thì chỉ có duy nhất một QAction là “Insert New Record” và nó dùng chung enum (INSERT_FIRST = 0)
Bước 3: Viết mã lệnh “TableModel.py“, đây là lớp mã lệnh trung tâm của dự án liên quan tới kiến trúc Model View.
Lớp TableModel kế thừa từ QAbstractTableModel, Tui định nghĩa constructor có 2 đối số nhận vào đó là ma trận dữ liệu (data) và mảng tên cột (columns)
from PyQt6 import QtGui
from PyQt6.QtCore import Qt, QAbstractTableModel, QModelIndex
class TableModel(QAbstractTableModel):
def __init__(self, data,columns):
super().__init__()
self.data = data
self.columns=columns
Tiếp theo ta override hàm data() để hiển thị dữ liệu và định dạng dữ liệu như mong muốn:
def data(self, index, role):
value = self.data[index.row()][index.column()]
if role == Qt.ItemDataRole.DisplayRole:
return value
if role==Qt.ItemDataRole.EditRole:
return value
if role == Qt.ItemDataRole.BackgroundRole:
if index.column() == 1 and value == "":
return QtGui.QColor(Qt.GlobalColor.yellow)
if role == Qt.ItemDataRole.ForegroundRole:
if index.column() == 2 and value!="" and float(value) < 100:
return QtGui.QColor(Qt.GlobalColor.red)
Thông qua hàm data() này, chương trình sẽ tô chữ đỏ với dòng dữ liệu có giá <100, tô nền vàng với dòng dữ liệu có tên rỗng.
Tiếp theo ta cần override hàm rowCount() và columnCount() để Model nhận dạng được số dòng và số cột trong giao diện:
Để tạo tiêu đề cột và tiêu đề dòng, ta cần override phương thức headerData():
def headerData(self, section, orientation, role):
if role == Qt.ItemDataRole.DisplayRole:
if orientation == Qt.Orientation.Horizontal:
return str(self.columns[section])
if orientation==Qt.Orientation.Vertical:
return str(section+1)
Qt.Orientation.Horizontal là enum dùng để định nghĩa tiêu đề cột
Qt.Orientation.Vertical là enum dùng để định nghĩa tiêu đề dòng
Tiếp theo ta override 2 hàm flags() và setData():
def flags(self, index):
if not index.isValid():
return Qt.ItemFlag.ItemIsEnabled
return super().flags(index) | Qt.ItemFlag.ItemIsEditable # add editable flag.
def setData(self, index, value, role):
if role == Qt.ItemDataRole.EditRole:
# Set the value into the frame.
self.data[index.row()][index.column()] = value
return True
return False
Hàm flags() dùng để customize tính năng cho người dùng chỉnh sửa dữ liệu trực tiếp trên từng ô dữ liệu
Hàm setData() dùng để bổ sung cho tính năng flags() đó là khi nhấn vào ô nào thì hiển thị chức năng cập nhật (Ô textbox) đồng thời hiển thị lại dữ liệu hiện hành). Cũng như khi người dùng đổi dữ liệu mới thì dữ liệu này sẽ được cập nhật vào ma trận: self.data[index.row()][index.column()] = value
Tiếp theo ta override hàm insertRows() để hỗ trợ việc chèn mới dòng dữ liệu cho Model:
Mã lệnh bên trong hàm insertRows() chương trình sẽ tạo thêm 1 dòng dữ liệu trên QTableView đồng thời cũng chèn dữ liệu rỗng cho ma trận trong biến data.
Hàm insertRows() sẽ dùng cho các chức năng: Chèn đầu, chèn cuối, chèn trước và chèn sau.
Để thực hiện chức năng xóa dữ liệu ta override hàm removeRows():
Đồng thời ta cũng khai báo các signal để xử lý sự kiện selectionChange và đăng ký các Context Menu trên Cho QTableView (khi nhấn chuột phải vào các ô bên trong QTableView hoặc nhấn chuột phải vào tiêu đề dòng).
Hàm processItemSelection() sẽ xử lý lấy dữ liệu đang chọn tại dòng hiện hành và hiển thị thông tin chi tiết lên QLineEdit:
def processItemSelection(self):
index=self.tableViewProduct.currentIndex()
if index.row()>-1:
item=self.data[index.row()]
self.lineEditProductId.setText(item[0])
self.lineEditProductName.setText(item[1])
self.lineEditUnitPrice.setText(str(item[2]))
Ta viết mã lệnh cho hàm onCustomContextMenuRequested() để khởi tạo QMenu context menu:
Bước 7: Cuối cùng ta tạo “MyApp.py” để thực thi chương trình
from PyQt6.QtWidgets import QApplication, QMainWindow
from MainWindowEx import MainWindowEx
app=QApplication([])
myWindow=MainWindowEx()
myWindow.setupUi(QMainWindow())
myWindow.show()
app.exec()
Chạy chương trình lên ta có kết quả như mong muốn:
Như vậy là tới đây Tui đã hướng dẫn đầy đủ và chi tiết kiến trúc Model View và áp dụng cho QTableView trong quản lý Product. Với đầy đủ tính năng: Xem, thêm, sửa, xóa. Đặc biệt là các chức năng Context Menu để hỗ trợ việc chèn dữ liệu cũng như xóa dữ liệu khởi QTableView, và Menu để Export và Import dữ liệu với định dạng JSon Array.
Ở các bài trước chúng ta đã ứng dụng thành thạo kiến trúc Model View để tương tác dữ liệu Employee trên QListview. Trong bài này Tui tiếp tục hướng dẫn các bạn cách dùng Model View cho QTableView để hiển thị dữ liệu dạng bảng. Bạn có thể nạp Ma trận dữ liệu vào QTableView hoặc nạp Danh sách dữ liệu đối tượng dạng List vào QTableView. Sự khác biệt giữa QListView và QTableView là QListView không cần dùng các cột dữ liệu, còn QTableView cần dùng các cột dữ liệu để chi tiết hóa các thuộc tính của đối tượng.
Trong bài này Tui minh họa ví dụ đơn giản về nạp Ma trận Product lên QTableView dùng kiến trúc Model View. Giả sử ta có Ma trận dữ liệu như dưới đây:
Làm thế nào để nạp ma trận “data” lên QTableView cùng với các cột được lưu trong mảng “columns“? Đồng thời nếu giá <100 thì tô chữ đỏ, nếu tên rỗng thì tô nền vàng:
Nếu hiểu và triển khai được bài này thì bạn có thể dễ dàng nạp ma trận dữ liệu cần phân tích lên giao diện, ví dụ ma trận về lịch sử kinh doanh.
Các bạn tiến hành làm từng bước như Tui hướng dẫn dưới đây.
Bước 1: Tạo dự án “LearnModelViewPart3” trong Pycharm có cấu trúc như dưới đây:
Trung tâm của dự án chính là lớp “TableModel.py“. Lớp này kế thừa từ “QAbstractTableModel” để thực hiện kiến trúc Model View. Tui sẽ trình bày chi tiết ở bước sau
“MainWindow.ui” là giao diện thiết kế của phần mềm bằng Qt Designer
“MainWindow.py” là generate python code của giao diện “MainWindow.ui”
“MainWindowEx.py” là lớp kế thừa từ generate python code “MainWindow.py” nó được dùng để nạp giao diện, xử lý sự kiện người dùng, gán model view mà không bị ảnh hưởng khi ta tiếp tục generate code từ giao diện khi thay đổi.
“MyApp.py” là file mã lệnh thực thi chương trình
Bước 2: Tạo lớp mã lệnh “TableModel.py” như dưới đây:
from PyQt6 import QtGui
from PyQt6.QtCore import Qt, QAbstractTableModel
class TableModel(QAbstractTableModel):
def __init__(self, data,columns):
super().__init__()
self.data = data
self.columns=columns
def data(self, index, role):
value=self.data[index.row()][index.column()]
if role == Qt.ItemDataRole.DisplayRole:
return value
if role==Qt.ItemDataRole.BackgroundRole:
if index.column()==1 and value=="":
return QtGui.QColor(Qt.GlobalColor.yellow)
if role==Qt.ItemDataRole.ForegroundRole:
if index.column() == 2 and value<100:
return QtGui.QColor(Qt.GlobalColor.red)
def rowCount(self, index):
return len(self.data)
def columnCount(self, index):
return len(self.columns)
def headerData(self, section, orientation, role):
if role == Qt.ItemDataRole.DisplayRole:
if orientation == Qt.Orientation.Horizontal:
return str(self.columns[section])
if orientation==Qt.Orientation.Vertical:
return str(section+1)
Tui giải thích chi tiết các hàm của TableModel.py như sau:
Constructor __init__(self, data,columns) nhận vào 2 đối số. Đối số data là ma trận dữ liệu Product đã mô tả ở trên, đối số columns là mảng các cột của Product.
Hàm data(self, index, role) được override để hiển thị dữ liệu cũng như các định dạng dữ liệu (màu nền, màu chữ). Trong hàm này Tui có sử dụng 3 Role:
Qt.ItemDataRole.DisplayRole (dùng để hiển thị dữ liệu thông thường)
Qt.ItemDataRole.BackgroundRole (để hiển thị màu nền, trong trường hợp này là nếu dữ liệu của cột tên Product là rỗng thì ta tô nền vàng)
Qt.ItemDataRole.ForegroundRole (để hiển thị màu chữ, trong trường hợp này là nếu dữ liệu của cột giá Product nhỏ hơn 100 thì tô chữ đỏ)
Hàm rowCount(self, index) được override để trả về số dòng dữ liệu trong model
Hàm columnCount(self, index) được override để trả về số cột trong model
Hàm headerData(self, section, orientation, role) được override để vẽ tiêu đề của Cột nằm đứng và header của dòng nằm ngang.
5 Hàm bắt buộc cần được định nghĩa đối với TableModel ở trên. Tương tự cho các loại dữ liệu khác thì bạn cũng tạo 5 hàm này, và điều chỉnh mã lệnh theo nhu cầu sử dụng khác nhau. Còn những hàm nâng cao khác để hỗ trợ thêm mới, chỉnh sửa, cũng như xóa dữ liệu Tui sẽ trình bày chi tiết ở bài học sau.
Bước 3: Thiết kế giao diện “MainWindow.ui” và đặt tên cho widget như hình dưới đây:
Bài này thì ta chỉ cần kéo 1 QTableView ra mà thôi.
Bước 4: Generate python code “MainWindow.py” cho giao diện “MainWindow.ui”
# Form implementation generated from reading ui file 'MainWindow.ui'
#
# Created by: PyQt6 UI code generator 6.4.2
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt6 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(335, 297)
self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.tableViewProduct = QtWidgets.QTableView(parent=self.centralwidget)
self.tableViewProduct.setGeometry(QtCore.QRect(10, 10, 301, 231))
self.tableViewProduct.setObjectName("tableViewProduct")
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 335, 26))
self.menubar.setObjectName("menubar")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(parent=MainWindow)
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "Trần Duy Thanh - QTableView-ModelView"))
Bước 5: Tạo “MainWindowEx.py” kế thừa từ Generate Python code ở trên để nạp giao diện và gán Model View
Dòng lệnh 18 này dùng để khởi tạo đối tượng tableModel, nó nhận vào Ma trận Product và mảng columns
Dòng lệnh 19:
self.tableViewProduct.setModel(self.model)
Dòng lệnh 19 gán model cho QTableView.
Lúc này các hàm trong TableModel sẽ tự động được thực hiện, rất chuyên nghiệp và ảo ma canada.
Bước 6: Tạo file lệnh “MyApp.py” để thực thi chương trình
from PyQt6.QtWidgets import QApplication, QMainWindow
from MainWindowEx import MainWindowEx
app=QApplication([])
myWindow=MainWindowEx()
myWindow.setupUi(QMainWindow())
myWindow.show()
app.exec()
Chạy MyApp.py ta có kết quả như mong muốn:
Bạn quan sát thấy cột Price sẽ tô màu đỏ những Price nào <100, như vậy giá 50 và 70 tự động được tô màu đỏ. Và cột Name có dữ liệu nào rỗng sẽ tô nền vàng.
Như vậy Tui đã trình bày xong cách ứng dụng kiến trúc Model View vào quản lý và hiển thị dữ liệu dạng Ma trận, đã trình bày kỹ cách tạo TableModel kế thừa từ QAbstractTableModel. Cũng như đã trình bày chi tiết ý nghĩa và cách lập trình các hàm quan trọng trong lớp này để ta có thể dễ dàng tùy chỉnh cách thức mà dữ liệu hiển thị trên giao diện như mong muốn.
Bài học sau Tui sẽ mở rộng bài này bằng cách bổ sung các chức năng: Thêm, Sửa, Xóa dữ liệu ngay trên giao diện QTableView để các bạn củng cố hơn nữa cách ứng dụng kiến trúc Model View cũng như nhìn thấy được lợi ích của mô hình này. Các bạn chú ý theo dõi
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 và 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.
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:
Role
Value
Ý nghĩa chức năng
Qt.ItemDataRole.DisplayRole
0
Dùng để hiển thị dữ liệu, kiểu QString.
Qt.ItemDataRole.DecorationRole
1
Dùng để hiển thị định dạng trang trí, biểu tượng, kiểu QColor, QIcon hoặc QPixmap
Qt.ItemDataRole.EditRole
2
Dùng để hiển thị khi dữ liệu chỉnh sửa, kiểu QString
Qt.ItemDataRole.ToolTipRole
3
Dùng để hiển thị Tooltip, kiểu QString
Qt.ItemDataRole.StatusTipRole
4
Dùng để hiển thị cho staus bar, kiểu QString
Qt.ItemDataRole.WhatsThisRole
5
Dùng để hiển thị “What’s this”, kiểu QString
Qt.ItemDataRole.FontRole
6
Dùng để hiển thị Font chữ
Qt.ItemDataRole.TextAlignmentRole
7
Dùng để căn lề chữ
Qt.ItemDataRole.BackgroundRole
8
Dùng để hiển thị màu nền
Qt.ItemDataRole.ForegroundRole
9
Dùng để hiển thị màu chữ
Qt.ItemDataRole.CheckStateRole
10
Dùng để kiểm tra trạng thái
Qt.ItemDataRole.AccessibleTextRole
11
Dùng để truy suất text
Qt.ItemDataRole.AccessibleDescriptionRole
12
Dùng để truy suất mô tả
Qt.ItemDataRole.SizeHintRole
13
Dùng để thiết lập size hint cho item
Qt.ItemDataRole.InitialSortOrderRole
14
Dùng để khởi tạo sắp xếp
Qt.ItemDataRole.UserRole
15
Dù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
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:
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.
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:
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:
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ế.
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
Trong lập trình xử lý tương tác dữ liệu có rất nhiều kỹ thuật, các bài học trước ta dùng các kỹ thuật thông thường và trực tiếp tương tác các đối tượng trên các Widget. Ngoài ra các kỹ sư đã phát triển kiến trúc liên quan tới Model View Controller, hay thường được gọi tắt là mô hình MVC để việc tương tác giao diện người dùng với dữ liệu cũng như các xử lý nghiệp vụ được dễ dàng hơn, dễ tái sử dụng hơn. PyQt6 cũng tích hợp mô hình MVC này và nó được gọi là Qt’s Model/Views Architecture. Chúng ta cùng điểm qua mô hình này:
Các bạn có thể hiểu nôm na mô hình Model View Controller nó hoạt động như sau:
Model: Nắm giữ cấu trúc dữ liệu trong ứng dụng mà nó đang làm việc. Ví dụ danh sách dữ liệu hướng đối tượng Product, Employee, Customer, Order, Provider… Khi người sử dụng thao tác trên View thì nó sẽ thông qua Controller để truy suất tới các Model tương ứng, các Model này sẽ hiển thị lên View thông qua Controller, có thể hiểu Controller là trung gian. Ví dụ trên Giao diện bạn muốn xem Danh sách PRODUCT, thì nó sẽ truyền tín hiện cho Controller PRODUCT, và controller này sẽ chọn đúng mô hình đối tượng PRODUCT, Model sẽ tạo ra danh sách Product và trả về cho Controller, sau đó Controller đẩy danh sách Product này lên View.
View: Là thành phần để hiển thị giao diện người dùng, nó cũng có nhiệm vụ nhận tương tác của người dùng và gọi Controller tương ứng. Nhiều View khác nhau có thể cùng hiển thị một loại dữ liệu, ví dụ bạn có 1 QTableWidget, 1 QListWidget, 1 QComboBox…các Widgets này có thể cùng hiển thị 1 danh sách model Product. Do đó ở đây bạn thấy tính tái sử dụng model rất cao.
Controller: Là thành phần trung gian, nó sẽ nhận các tương tác người dùng trên View, sau đó truyền đổi các tương tác này thành các lệnh để tương tác với Model để nhận các model tương ứng rồi nó sẽ hiển thị kết quả lên View hoặc tương tác với Model khác.
Với PyQt thì các kỹ sư có sự cải biên một xíu, đặc biệt là sự phân biệt giữa Model và View đôi khi nó khá mờ nhạt, khó nhận ra. Đồng thời View và Controller được hợp nhất lại với nhau để tao ra kiến trúc Model/ViewController hay nói tắt là ModelView. PyQt chấp nhận các sự kiện từ người dùng thông qua hệ đều hành và ủy quyền những sự kiện này cho các widget để xử lý. Cái hay của Model View đó là giảm bớt sự cồng kềnh:
Model nắm giữ dữ liệu hoặc tham chiếu dữ liệu và trả về đối tượng đơn hoặc danh sách đối tượng. Khi model được thanh đổi dữ liệu nó sẽ ngay lập tức cập nhật lên giao diện
View sẽ yêu cầu dữ liệu từ Model và hiển thị lên các Widget tương ứng
Cơ chế Delegate giúp chương trình cập nhật dữ liệu mô hình tương ứng lên View một cách tự động.
Bài minh họa dưới đây, Chúng Ta sẽ làm một dự án hiển thị danh sách Employee đơn giản lên QListView sử dụng kiến trúc Model View của PyQt:
Ở hình trên Tui có minh họa là Chúng ta tạo một mô hình lớp Employee, và dùng kiến trúc Model View để hiển thị danh sách Employee lên QListView.
Ta thực hiện từng bước như sau:
Bước 1: Tạo dự án “LearnModelViewPart1” trong Pycharm có cấu trúc như hình dưới đây:
Employee.py là mô hình lớp hướng đối tượng định nghĩa cấu trúc dữ liệu cho Employee gồm id, name, age
EmployeeModel.py là lớp model view cho Employee, nó kế thừa từ “QAbstractListModel” lớp này sẽ lưu trữ danh sách dữ liệu mô hình Employee và hiển thị lên QListView
MainWindow.ui là màn hình giao diện được thiết kế bằng Qt Designer
MainWindow.py là lớp Generate Python của MainWindow.ui
MainWindowEx.py là lớp mã lệnh xử lý sự kiện người dùng cũng như lắp ráp dữ liệu trong mô hình Model View
MyApp.py là lớp thực thi chương trình
Bước 2: Tạo lớp Employee.py có mã lệnh như dưới đây:
Lớp Employee có constructor nhận các đối số id, name, age và nó là các thuộc tính của Employee.
đồng thời hàm __str__ Tui thiết kế để nó tự động hiển thị lên giao diện như mong muốn, bạn có thể sửa lại.
Bước 3: Tạo lớp mô hình “EmployeeModel.py” và có mã lệnh như dưới đây:
from PyQt6.QtCore import QAbstractListModel, Qt
class EmployeeModel(QAbstractListModel):
def __init__(self,employees=None):
super().__init__()
self.employees=employees
def data(self, index, role):
if role == Qt.ItemDataRole.DisplayRole:
emp = self.employees[index.row()]
return str(emp)
def rowCount(self, index):
return len(self.employees)
Lớp EmployeeModel sẽ kế thừa từ lớp QAbstractListModel, chúng ta cần overridfe 3 hàm (bắt buộc):
hàm __init__ để khởi tạo các biến ban đầu cho model, ở đây Tui thiết kế cho nó nhận vào một danh sách Employee
hàm data có 2 đối số chính, đối số index (Có kiểu QIndex) là vị trí của dòng dữ liệu sẽ hiển thị lên QListView, do đó ra sẽ lấy index.row() để trả về integer để truy suất chính xác vị trí của dữ liệu trong danh sách employees. Ở đây chúng ta sử dụng Enum Qt.ItemDataRole để điều hướng cách thức hiển thị dữ liệu, Tui sẽ nó chi tiết ở bài học tiếp theo, trong bài này Chúng ta quan tâm tới DisplayRole có nghĩa là enum dùng để hiển thị dữ liệu. Hàm này dùng str(emp) để nó tự động gọi hàm __str__ mà ta thiết kế trong lớp Employee.py
hàm rowCount trả về số phần tử trong danh sách, bắt buộc chúng ta phải Override hàm này.
Bước 4: Thiết kế giao diện MainWindow.ui bằng Qt Designer
Ta chỉ cần kéo thả ListView trong nhóm Model-Based vào giao diện là xong.
Bước 5: Generate Python code cho MainWindow.ui
# Form implementation generated from reading ui file 'MainWindow.ui'
#
# Created by: PyQt6 UI code generator 6.5.2
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt6 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(351, 251)
self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.listView = QtWidgets.QListView(parent=self.centralwidget)
self.listView.setGeometry(QtCore.QRect(10, 10, 311, 201))
self.listView.setObjectName("listView")
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 351, 22))
self.menubar.setObjectName("menubar")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(parent=MainWindow)
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "Trần Duy Thanh - Model View"))
Bước 6: Tạo MainWindowEx để xử lý sự kiện, gán model cho widget
from Employee import Employee
from EmployeeModel import EmployeeModel
from MainWindow import Ui_MainWindow
class MainWindowEx(Ui_MainWindow):
def __init__(self):
pass
def setupUi(self, MainWindow):
super().setupUi(MainWindow)
self.MainWindow=MainWindow
employees=[]
employees.append(Employee(1,"John",37))
employees.append(Employee(2, "Peter", 24))
employees.append(Employee(3, "Tom", 25))
self.model=EmployeeModel(employees)
self.listView.setModel(self.model)
def show(self):
self.MainWindow.show()
Ta thấy trong hàm override setupUi, Tui tạo ra 3 đối tượng employee và lưu trữ vào danh sách employees, sau đó nó được đưa vào EmployeeModel(employees)
cuối cùng ta gán model này cho QListVIew bằng hàm: setModel(model)
Bước 7: Viết MyApp.py để thực thi chương trình
from PyQt6.QtWidgets import QApplication, QMainWindow
from MainWindowEx import MainWindowEx
app=QApplication([])
myWindow=MainWindowEx()
myWindow.setupUi(QMainWindow())
myWindow.show()
app.exec()
Thực thi MyApp.py ta có kết quả như mong muốn.
Như vậy là tới đây Tui đã trình bày xong mô hình MVC, Qt Model/ViewController. Các bạn biết được lợi ích của mô hình và đặc biệt triển khai được một ví dụ minh họa cụ thể bằng Model View Architecture để hiển thị danh sách Employee lên QListView.
Sourcode đầy đủ của bài Model View này các bạn tải ở đây:
Bài học sau Tui tiếp tục trình bày chi tiết và chuyên sâu về model trong QListView, cung cấp thêm các chức năng: Thêm, Sửa, Xóa trên model. Các bạn chú ý theo dõi