Bài 21: QDateTimeEdit – Basic Widgets – PyQt6

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

        layout = QFormLayout()
        self.setLayout(layout)

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

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

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

        self.result_label = QLabel('', self)

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

        self.show()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

from Product import Product

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

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

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

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

from Category import Category
from Product import Product

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

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

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

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

import json
import os

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

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

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

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

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

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


from PyQt6 import QtCore, QtGui, QtWidgets


class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(650, 437)
        font = QtGui.QFont()
        font.setPointSize(12)
        MainWindow.setFont(font)
        icon = QtGui.QIcon()
        icon.addPixmap(QtGui.QPixmap("images/ic_logo.jpg"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        MainWindow.setWindowIcon(icon)
        self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.groupBox = QtWidgets.QGroupBox(parent=self.centralwidget)
        self.groupBox.setGeometry(QtCore.QRect(270, 30, 351, 271))
        self.groupBox.setStyleSheet("background-color: rgb(255, 206, 241);")
        self.groupBox.setObjectName("groupBox")
        self.label_3 = QtWidgets.QLabel(parent=self.groupBox)
        self.label_3.setGeometry(QtCore.QRect(10, 30, 111, 20))
        self.label_3.setObjectName("label_3")
        self.lineEditProductId = QtWidgets.QLineEdit(parent=self.groupBox)
        self.lineEditProductId.setGeometry(QtCore.QRect(80, 60, 211, 22))
        self.lineEditProductId.setObjectName("lineEditProductId")
        self.label_4 = QtWidgets.QLabel(parent=self.groupBox)
        self.label_4.setGeometry(QtCore.QRect(10, 90, 151, 16))
        self.label_4.setObjectName("label_4")
        self.lineEditProductName = QtWidgets.QLineEdit(parent=self.groupBox)
        self.lineEditProductName.setGeometry(QtCore.QRect(80, 120, 211, 22))
        self.lineEditProductName.setObjectName("lineEditProductName")
        self.lineEditUnitPrice = QtWidgets.QLineEdit(parent=self.groupBox)
        self.lineEditUnitPrice.setGeometry(QtCore.QRect(80, 170, 211, 22))
        self.lineEditUnitPrice.setObjectName("lineEditUnitPrice")
        self.label_5 = QtWidgets.QLabel(parent=self.groupBox)
        self.label_5.setGeometry(QtCore.QRect(10, 150, 111, 16))
        self.label_5.setObjectName("label_5")
        self.label_6 = QtWidgets.QLabel(parent=self.groupBox)
        self.label_6.setGeometry(QtCore.QRect(10, 200, 141, 31))
        self.label_6.setObjectName("label_6")
        self.dateTimeEditTracking = QtWidgets.QDateTimeEdit(parent=self.groupBox)
        self.dateTimeEditTracking.setGeometry(QtCore.QRect(80, 230, 221, 31))
        self.dateTimeEditTracking.setDateTime(QtCore.QDateTime(QtCore.QDate(2023, 12, 29), QtCore.QTime(0, 0, 0)))
        self.dateTimeEditTracking.setCalendarPopup(True)
        self.dateTimeEditTracking.setObjectName("dateTimeEditTracking")
        self.groupBox_2 = QtWidgets.QGroupBox(parent=self.centralwidget)
        self.groupBox_2.setGeometry(QtCore.QRect(9, 39, 251, 341))
        self.groupBox_2.setStyleSheet("background-color: rgb(212, 253, 255);")
        self.groupBox_2.setTitle("")
        self.groupBox_2.setObjectName("groupBox_2")
        self.listWidgetProduct = QtWidgets.QListWidget(parent=self.groupBox_2)
        self.listWidgetProduct.setGeometry(QtCore.QRect(10, 100, 231, 221))
        self.listWidgetProduct.setStyleSheet("background-color: rgb(237, 255, 202);")
        self.listWidgetProduct.setObjectName("listWidgetProduct")
        self.label = QtWidgets.QLabel(parent=self.groupBox_2)
        self.label.setGeometry(QtCore.QRect(10, 10, 161, 21))
        self.label.setObjectName("label")
        self.cboCatalog = QtWidgets.QComboBox(parent=self.groupBox_2)
        self.cboCatalog.setGeometry(QtCore.QRect(10, 40, 221, 22))
        self.cboCatalog.setStyleSheet("background-color: rgb(237, 255, 202);")
        self.cboCatalog.setObjectName("cboCatalog")
        self.label_2 = QtWidgets.QLabel(parent=self.groupBox_2)
        self.label_2.setGeometry(QtCore.QRect(10, 70, 161, 21))
        self.label_2.setObjectName("label_2")
        self.groupBox_3 = QtWidgets.QGroupBox(parent=self.centralwidget)
        self.groupBox_3.setGeometry(QtCore.QRect(270, 300, 351, 81))
        self.groupBox_3.setStyleSheet("background-color: rgb(249, 255, 210);")
        self.groupBox_3.setObjectName("groupBox_3")
        self.pushButtonNew = QtWidgets.QPushButton(parent=self.groupBox_3)
        self.pushButtonNew.setGeometry(QtCore.QRect(10, 30, 93, 41))
        self.pushButtonNew.setStyleSheet("background-color: rgb(170, 255, 127);")
        icon1 = QtGui.QIcon()
        icon1.addPixmap(QtGui.QPixmap("images/ic_new.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.pushButtonNew.setIcon(icon1)
        self.pushButtonNew.setIconSize(QtCore.QSize(32, 32))
        self.pushButtonNew.setObjectName("pushButtonNew")
        self.pushButtonSave = QtWidgets.QPushButton(parent=self.groupBox_3)
        self.pushButtonSave.setGeometry(QtCore.QRect(120, 30, 93, 41))
        self.pushButtonSave.setStyleSheet("background-color: rgb(170, 255, 127);")
        icon2 = QtGui.QIcon()
        icon2.addPixmap(QtGui.QPixmap("images/ic_save.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.pushButtonSave.setIcon(icon2)
        self.pushButtonSave.setIconSize(QtCore.QSize(32, 32))
        self.pushButtonSave.setObjectName("pushButtonSave")
        self.pushButtonDelete = QtWidgets.QPushButton(parent=self.groupBox_3)
        self.pushButtonDelete.setGeometry(QtCore.QRect(232, 30, 111, 41))
        self.pushButtonDelete.setStyleSheet("background-color: rgb(170, 255, 127);")
        icon3 = QtGui.QIcon()
        icon3.addPixmap(QtGui.QPixmap("images/ic_delete.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.pushButtonDelete.setIcon(icon3)
        self.pushButtonDelete.setIconSize(QtCore.QSize(32, 32))
        self.pushButtonDelete.setObjectName("pushButtonDelete")
        self.label_7 = QtWidgets.QLabel(parent=self.centralwidget)
        self.label_7.setGeometry(QtCore.QRect(210, 0, 301, 31))
        palette = QtGui.QPalette()
        brush = QtGui.QBrush(QtGui.QColor(255, 0, 0))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.WindowText, brush)
        brush = QtGui.QBrush(QtGui.QColor(255, 0, 0))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Text, brush)
        brush = QtGui.QBrush(QtGui.QColor(255, 0, 0))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.ButtonText, brush)
        brush = QtGui.QBrush(QtGui.QColor(255, 0, 0))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.WindowText, brush)
        brush = QtGui.QBrush(QtGui.QColor(255, 0, 0))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Text, brush)
        brush = QtGui.QBrush(QtGui.QColor(255, 0, 0))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.ButtonText, brush)
        brush = QtGui.QBrush(QtGui.QColor(255, 0, 0))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.WindowText, brush)
        brush = QtGui.QBrush(QtGui.QColor(255, 0, 0))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Text, brush)
        brush = QtGui.QBrush(QtGui.QColor(255, 0, 0))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.ButtonText, brush)
        self.label_7.setPalette(palette)
        font = QtGui.QFont()
        font.setPointSize(15)
        font.setBold(True)
        font.setWeight(75)
        self.label_7.setFont(font)
        self.label_7.setStyleSheet("color: rgb(255, 0, 0);")
        self.label_7.setObjectName("label_7")
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 650, 26))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(parent=MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

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

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "Trần Duy Thanh - QDateTime Demo"))
        self.groupBox.setTitle(_translate("MainWindow", "Product Details:"))
        self.label_3.setText(_translate("MainWindow", "Product ID:"))
        self.label_4.setText(_translate("MainWindow", "Product Name:"))
        self.label_5.setText(_translate("MainWindow", "Unit Price:"))
        self.label_6.setText(_translate("MainWindow", "Time Tracking:"))
        self.dateTimeEditTracking.setDisplayFormat(_translate("MainWindow", "dd/MM/yyyy HH:mm:ss"))
        self.label.setText(_translate("MainWindow", "Select a Catalog:"))
        self.label_2.setText(_translate("MainWindow", "List of Products:"))
        self.groupBox_3.setTitle(_translate("MainWindow", "Action:"))
        self.pushButtonNew.setText(_translate("MainWindow", "New"))
        self.pushButtonSave.setText(_translate("MainWindow", "Save"))
        self.pushButtonDelete.setText(_translate("MainWindow", "Delete"))
        self.label_7.setText(_translate("MainWindow", "Product Management"))

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

from PyQt6.QtWidgets import QApplication, QMainWindow

from MainWindowEx import MainWindowEx

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

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

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

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

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

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

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

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

Leave a Reply