Bài 20: QTimeEdit – Basic Widgets – PyQt6

Trong bài QDateEdit các bạn đã được học kỹ lưỡng về cách dùng widget để lưu trữ và xử lý dữ liệu Ngày-Tháng-Năm. Bài học này Tui sẽ trình bày về cách sử dụng QTimeEdit để lưu trữ Giờ-Phút.

Trong lưu trữ dữ liệu chúng ta cũng thường có các dữ liệu [ngày tháng năm], [ngày tháng năm giờ phút giây], [giờ phút giây] tùy từng tình huống mà chúng ta sẽ lựa chọn kiểu dữ liệu phù hợp. Ví dụ như lưu năm sinh thì ta dùng Ngày Tháng Năm, lưu thời gian đặt đơn hàng thì ta dùng ngày tháng năm giờ phút giây, lưu trữ thời gian thực hiện thì ta dùng giờ phút giây hoặc kết hợp cả ngày tháng năm (ví dụ như đặt đơn hàng và yêu cầu giao hàng lúc 10h sáng ngày 20/12/2023, thì lúc này ta dùng vừa ngày tháng năm vừa giờ phút giây. hoặc tách ra làm 2 thuộc tính, thuộc tính chỉ chứa ngày tháng năm và thuộc tính chỉ chứa giờ phút giây), Ví dụ dưới đây là phần mềm Tracking Tasks có sử dụng QDateEdit và QTimeEdit, 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
QTimeEdit(self)Constructor để tạo đối tượng QTimeEdit. 
time()Hàm trả về đối tượng QTime. Do đó để chuyển về Python datetime.time ta dùng hàm toPyTime()
minimumTimeThuộc tính thiết lập giờ nhỏ nhất
maximumTimeThuộc tính thiết lập giờ lớn nhất
setDisplayFormat(format)Chuỗi format hiển thị giờ.
Ví dụ:
setDisplayFormat(“HH:mm:ss AP”)
editingFinishedsignal để lắng nghe khi người dùng kết thúc quá trình lựa chọn giờ (nhấn enter)
timeChangedsignal để lắng nghe khi người dùng đang thay đổi lựa chọn giờ

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

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

self.time_edit = QTimeEdit(self)

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

time=QTime(10,15,35)
self.time_edit.setTime(time)
self.time_edit.setDisplayFormat("HH:mm:ss AP")

Mã lệnh trên tạo đối tượng QTime(10,15,35). 10 giờ, 15 phút, 35 giây.

hàm setTime để thiết lập giờ của đối tượng QTimeEdit.

Định dạng “HH:mm:ss AP” để thiết lập hiển thị giờ cho QTimeEdit thông qua hàm setDisplayFormat.

  • H là định dạng 24 giờ, Ta dùng HH để hiển thị giờ bổ sung thêm số 0 đằng trước khi giờ <10. Ví dụ như 8 giờ thì hiển thị 08. Ngoài ra nếu dùng h là định dạng 12 giờ.
  • m là định dạng phút, ta dùng mm để hiển thị phút bổ sung thêm số 0 đằng trước khi phút <10. Ví dụ 9 phút thì hiển thị 09
  • s là định dạng giây, ta dùng ss để hiển thị giây bổ sung thêm số 0 đằng trước khi giây <10. Ví dụ 5 giây thì hiển thị 05
  • AP là đại diện hiển thị AM hay PM

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

self.time_edit.editingFinished.connect(self.update_time)
def update_time(self):
    value = self.time_edit.time()
    print(str(value.toPyTime()))

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

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 timeChanged signal, bất cứ khi nào người dùng lựa chọn Giờ trên giao diện thì ta sẽ lấy được dữ liệu này:

self.time_edit.timeChanged.connect(self.change_time)
def change_time(self):
    value = self.time_edit.time()
    print(str(value.toPyTime()))

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

import sys

from PyQt6.QtCore import QTime
from PyQt6.QtWidgets import QApplication, QWidget, QTimeEdit, QLabel, QFormLayout


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

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

        # create a grid layout
        layout = QFormLayout()
        self.setLayout(layout)

        self.time_edit = QTimeEdit(self)
        time=QTime(10,15,35)
        self.time_edit.setTime(time)
        self.time_edit.setDisplayFormat("HH:mm:ss A")

        self.time_edit.editingFinished.connect(self.update_time)
        self.time_edit.timeChanged.connect(self.change_time)

        self.result_label = QLabel('', self)

        layout.addRow('Time:', self.time_edit)
        layout.addRow(self.result_label)
    def update_time(self):
        value = self.time_edit.time()
        print(str(value.toPyTime()))
        self.result_label.setText(str(value.toPyTime()))
        pass
    def change_time(self):
        value = self.time_edit.time()
        print(str(value.toPyTime()))
        self.result_label.setText(str(value.toPyTime()))
app = QApplication([])
window = MainWindow()
window.show()
app.exec()

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

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

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

  • Chương trình cung cấp giao diện để nhập các Task, thông tin chi tiết của Task bao gồm: Tiêu đề, nội dung thực hiện, ngày hết hạn, giờ hết hạn, và trạng thái đã hoàn thành hay chưa
  • Khi nhập Task thành công thì các Task sẽ được đưa vào QListWidget đồng thời các Task nào chưa hoàn thành thì có biểu tượng màu đó, task nào đã hoàn thành thì có biểu tượng màu xanh
  • Chương trình sẽ lưu và phục hồi dữ liệu bằng serialize và deserialize JSON ARRAY
  • Chương trình cung cấp chức năng chỉnh sửa Task
  • Chương trình cung cấp nút xóa, sẽ xóa những Task được checked trong QListWidget
  • Chương trình cung cấp chức năng selection trong QListWidget, khi người dùng bấm chuột hoặc di chuyển item trong QListWidget thì chi tiết của Task đang lựa chọn sẽ được hiển thị lại trong các widget bên màn hình bên phải.

Ta tạo một dự án tên “LearnQTimeEdit” 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, Add Icon, Save Icon, Remove Icon, Icon cho 2 trạng thái Finished và not Finished task
  • Task.py Là file lưu mã lệnh của lớp Task để định nghĩa một task bao gồm: title, content, deadline, deadlinetime, isfinish
  • Tasks.py Là file lưu mã lệnh để xử lý danh sách các Task, chẳng hạn các chức năng: thêm, truy suất, sửa, xóa Task
  • 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ước 1: Viết mã lệnh cho “Task.py”, mã lệnh được thể hiện như dưới đây:

class Task:
    def __init__(self,title,content,deadline,deadlinetime,isfinish):
        self.title=title
        self.content=content
        self.deadline=deadline
        self.deadlinetime=deadlinetime
        self.isfinish=isfinish
        pass
    def __str__(self):
        return self.title

Ta khai báo lớp Task 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.

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

from Task import Task
class Tasks:
    def __init__(self):
        self.lists=[]
    def item(self,index)->Task:
        return self.lists[index]
    def add(self,task):
        self.lists.append(task)
    def addAll(self,tasks):
        for i in range(len(tasks)):
            task=tasks[i]
            self.add(task)
    def index(self,task):
        i=self.lists.index(task)
        return i
    def update(self,index,task)->Task:
        self.lists[index]=task
        return self.lists[index]
    def removeByIndex(self,index)->Task:
        return self.lists.pop(index)
    def removeByItem(self,item):
        self.lists.remove(item)
    def clear(self):
        self.lists.clear()
    def size(self):
        return len(self.lists)

Lớp Tasks ở trên Tui thiết kế các phương thức để ta có thể sử dụng lưu danh sách, 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ề Task theo index
  • Hàm index(self,task) là hàm trả về vị trí của task trong danh sách
  • Hàm add(self,task) để thêm mới một task vào danh sách
  • Hàm addAll(self,tasks) để thêm mới nhiều task vào danh sách
  • Hàm update(self,index,task) để cập nhật dữ liệu cho Task
  • Hàm removeByIndex(self,index) để xóa 1 task ra khỏi danh sách tại ví trí index
  • Hàm removeByItem(self,item) để xóa 1 task ra khỏi danh sách tại ví trí item
  • Hàm clear(self) xóa toàn bộ task ra khỏi danh sách
  • Hàm size(self) trả về kích thước (số lượng) các phần tử trong danh sách

Bước 3: 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,arrData):
        jsonString = json.dumps([item.__dict__ for item in arrData],default=str)
        jsonFile = open(path, "w")
        jsonFile.write(jsonString)
        jsonFile.close()
    #path: path to deserialize array of Product
    #ClassName: Product
    def readData(self,path,ClassName):
        if os.path.isfile(path) == False:
            return []
        file = open(path, "r")
        # Reading from file
        self.arrData = json.loads(file.read(), object_hook=lambda d: ClassName(**d))
        file.close()
        return self.arrData

Lưu ý “FileFactory.py” là có mã lệnh không đổi, sử dụng chung cho mọi dự án muốn lưu dữ liệu dạng JSONARRAY, nó đã được trình bày ở bài học trước.

Bước 4: 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 5: 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.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(566, 385)
        font = QtGui.QFont()
        font.setPointSize(11)
        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)
        MainWindow.setStyleSheet("")
        self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
        self.centralwidget.setStyleSheet("")
        self.centralwidget.setObjectName("centralwidget")
        self.groupBox = QtWidgets.QGroupBox(parent=self.centralwidget)
        self.groupBox.setGeometry(QtCore.QRect(10, 30, 211, 311))
        self.groupBox.setStyleSheet("background-color: rgb(230, 255, 246);")
        self.groupBox.setObjectName("groupBox")
        self.verticalLayout = QtWidgets.QVBoxLayout(self.groupBox)
        self.verticalLayout.setObjectName("verticalLayout")
        self.listWidgetTask = QtWidgets.QListWidget(parent=self.groupBox)
        self.listWidgetTask.setObjectName("listWidgetTask")
        self.verticalLayout.addWidget(self.listWidgetTask)
        self.groupBox_2 = QtWidgets.QGroupBox(parent=self.centralwidget)
        self.groupBox_2.setGeometry(QtCore.QRect(230, 30, 321, 241))
        self.groupBox_2.setStyleSheet("background-color: rgb(248, 255, 215);")
        self.groupBox_2.setObjectName("groupBox_2")
        self.label_2 = QtWidgets.QLabel(parent=self.groupBox_2)
        self.label_2.setGeometry(QtCore.QRect(10, 30, 31, 16))
        self.label_2.setObjectName("label_2")
        self.label_3 = QtWidgets.QLabel(parent=self.groupBox_2)
        self.label_3.setGeometry(QtCore.QRect(10, 90, 51, 16))
        self.label_3.setObjectName("label_3")
        self.lineEditTitle = QtWidgets.QLineEdit(parent=self.groupBox_2)
        self.lineEditTitle.setGeometry(QtCore.QRect(60, 30, 241, 20))
        self.lineEditTitle.setObjectName("lineEditTitle")
        self.textEditContent = QtWidgets.QTextEdit(parent=self.groupBox_2)
        self.textEditContent.setGeometry(QtCore.QRect(60, 60, 241, 81))
        self.textEditContent.setObjectName("textEditContent")
        self.radFinished = QtWidgets.QRadioButton(parent=self.groupBox_2)
        self.radFinished.setGeometry(QtCore.QRect(60, 210, 83, 18))
        self.radFinished.setObjectName("radFinished")
        self.radNotFinished = QtWidgets.QRadioButton(parent=self.groupBox_2)
        self.radNotFinished.setGeometry(QtCore.QRect(150, 210, 101, 20))
        self.radNotFinished.setObjectName("radNotFinished")
        self.label_4 = QtWidgets.QLabel(parent=self.groupBox_2)
        self.label_4.setGeometry(QtCore.QRect(10, 150, 51, 20))
        self.label_4.setObjectName("label_4")
        self.dateEditDeadline = QtWidgets.QDateEdit(parent=self.groupBox_2)
        self.dateEditDeadline.setGeometry(QtCore.QRect(60, 150, 110, 22))
        self.dateEditDeadline.setCalendarPopup(True)
        self.dateEditDeadline.setDate(QtCore.QDate(2023, 12, 14))
        self.dateEditDeadline.setObjectName("dateEditDeadline")
        self.label_5 = QtWidgets.QLabel(parent=self.groupBox_2)
        self.label_5.setGeometry(QtCore.QRect(10, 180, 41, 16))
        self.label_5.setObjectName("label_5")
        self.timeEditDeadline = QtWidgets.QTimeEdit(parent=self.groupBox_2)
        self.timeEditDeadline.setGeometry(QtCore.QRect(60, 180, 111, 22))
        self.timeEditDeadline.setObjectName("timeEditDeadline")
        self.label = QtWidgets.QLabel(parent=self.centralwidget)
        self.label.setGeometry(QtCore.QRect(30, 0, 491, 31))
        font = QtGui.QFont()
        font.setFamily("Times New Roman")
        font.setPointSize(18)
        font.setBold(False)
        font.setItalic(False)
        font.setWeight(9)
        self.label.setFont(font)
        self.label.setStyleSheet("font: 75 18pt \"Times New Roman\";\n"
"color: rgb(0, 0, 255);")
        self.label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
        self.label.setObjectName("label")
        self.groupBox_3 = QtWidgets.QGroupBox(parent=self.centralwidget)
        self.groupBox_3.setGeometry(QtCore.QRect(230, 280, 321, 61))
        self.groupBox_3.setStyleSheet("background-color: rgb(255, 216, 249);")
        self.groupBox_3.setObjectName("groupBox_3")
        self.pushButtonNew = QtWidgets.QPushButton(parent=self.groupBox_3)
        self.pushButtonNew.setGeometry(QtCore.QRect(20, 20, 71, 31))
        self.pushButtonNew.setStyleSheet("background-color: rgb(255, 255, 127);")
        icon1 = QtGui.QIcon()
        icon1.addPixmap(QtGui.QPixmap("images/ic_add.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.groupBox_3)
        self.pushButtonSave.setGeometry(QtCore.QRect(120, 20, 71, 31))
        self.pushButtonSave.setStyleSheet("background-color: rgb(255, 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(24, 24))
        self.pushButtonSave.setObjectName("pushButtonSave")
        self.pushButtonRemove = QtWidgets.QPushButton(parent=self.groupBox_3)
        self.pushButtonRemove.setGeometry(QtCore.QRect(220, 20, 81, 31))
        self.pushButtonRemove.setStyleSheet("background-color: rgb(255, 255, 127);")
        icon3 = QtGui.QIcon()
        icon3.addPixmap(QtGui.QPixmap("images/ic_remove.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.pushButtonRemove.setIcon(icon3)
        self.pushButtonRemove.setIconSize(QtCore.QSize(24, 24))
        self.pushButtonRemove.setObjectName("pushButtonRemove")
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 566, 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", "Tran Duy Thanh - Tracking Tasks"))
        self.groupBox.setTitle(_translate("MainWindow", "List of Tasks:"))
        self.groupBox_2.setTitle(_translate("MainWindow", "Details of Task:"))
        self.label_2.setText(_translate("MainWindow", "Title:"))
        self.label_3.setText(_translate("MainWindow", "Content:"))
        self.radFinished.setText(_translate("MainWindow", "Finished"))
        self.radNotFinished.setText(_translate("MainWindow", "Not Finished"))
        self.label_4.setText(_translate("MainWindow", "Deadline:"))
        self.label_5.setText(_translate("MainWindow", "Time:"))
        self.label.setText(_translate("MainWindow", "Tracking Tasks"))
        self.groupBox_3.setTitle(_translate("MainWindow", "Actions:"))
        self.pushButtonNew.setText(_translate("MainWindow", "New"))
        self.pushButtonSave.setText(_translate("MainWindow", "Save"))
        self.pushButtonRemove.setText(_translate("MainWindow", "Remove"))

Bước 6: 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.QtGui import QIcon
from PyQt6.QtWidgets import QMessageBox, QListWidgetItem

from FileFactory import FileFactory
from MainWindow import Ui_MainWindow
from Task import Task
from Tasks import Tasks

class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        super().__init__()
        self.tasks=Tasks()
        self.fileFactory = FileFactory()
        self.selectedTask=None

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

  • Biến đối tượng tasks để lưu trữ danh sách hướng đối tượng các Task mà người sử dụng tương tác trên giao diện
  • 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 selectedTask để lưu trữ Task đang lựa chọn trên QListWidget (Task hiện hành)

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
    arrData= self.fileFactory.readData("database.json", Task)
    self.tasks.addAll(arrData)
    self.showTasksIntoQListWidget()
    self.pushButtonNew.clicked.connect(self.processNew)
    self.pushButtonSave.clicked.connect(self.processSave)
    self.pushButtonRemove.clicked.connect(self.processRemove)
    self.listWidgetTask.itemSelectionChanged.connect(self.processItemSelection)

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

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

def showTasksIntoQListWidget(self):
    self.listWidgetTask.clear()
    for index in range(self.tasks.size()):
        task=self.tasks.item(index)
        item=QListWidgetItem()
        item.setData(Qt.ItemDataRole.UserRole, task)
        item.setText(str(task))
        item.setCheckState(Qt.CheckState.Unchecked)
        if task.isfinish==True:
            item.setIcon(QIcon("images/ic_finished.png"))
        else:
            item.setIcon(QIcon("images/ic_notfinished.png"))
        if isinstance(task.deadline,str):
            task.deadline= datetime.date.fromisoformat(task.deadline)
        if isinstance(task.deadlinetime,str):
            task.deadlinetime=datetime.time.fromisoformat(task.deadlinetime)
        self.listWidgetTask.addItem(item)

Chương trình dùng vòng lặp để duyệt qua các Task trong biến đối tượng tasks, sau đó sẽ tạo các đối tượng QListWidgetItem để hiển thị lên giao diện. Nếu Task nào là FINISHED thì dùng icon finished (có màu xanh), chưa finished thì icon màu đỏ.

Ngoài ra vì dữ liệu ngày tháng khi serialize xuống ổ cứng với định dạng JSon thì nó là chuỗi, nên ta dùng hàm isinstance để kiểm tra xem thuộc tính deadline có phải đang là chuỗi hay không, nếu là chuỗi thì ta ép kiểu qua kiểu Python Date bằng hàm: datetime.date.fromisoformat(task.deadline)

Tương tự như vậy ta cũng viết mã lệnh để kiểm tra thuộc tính deadlinetime, ta chuyển qua Python time bằng hàm: datetime.time.fromisoformat(task.deadlinetime)

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

def processNew(self):
    self.lineEditTitle.setText("")
    self.textEditContent.setText("")
    self.dateEditDeadline.setSpecialValueText(None)
    self.radFinished.setAutoExclusive(False)
    self.radNotFinished.setAutoExclusive(False)
    self.radFinished.setChecked(False)
    self.radNotFinished.setChecked(False)
    self.radFinished.setAutoExclusive(True)
    self.radNotFinished.setAutoExclusive(True)
    self.selectedTask=None
    self.lineEditTitle.setFocus()

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

  • Bởi vì tính chất đặc biệt của QRadioButton nên ta gọi hàm setAutoExclusive(False) trước khi gỡ bỏ các checked của RadioButton, sau đó ta gọi setAutoExclusive(True)
  • Biến đối tượng selectedTask=None để đảm bảo lúc này chưa chọn Task nào trên giao diện, vì lúc này ta đang thêm mới 1 task.

-Hàm processSave dùng để lưu dữ liệu, nếu selectedTask mà khác None lưu cập nhật, còn selectedTask =None thì lưu mới:

def processSave(self):
    title=self.lineEditTitle.text()
    content=self.textEditContent.toPlainText()
    date=self.dateEditDeadline.date().toPyDate()
    time=self.timeEditDeadline.time().toPyTime()
    isFinished=self.radFinished.isChecked()
    task=Task(title,content,date,time,isFinished)
    if self.selectedTask==None:
        self.tasks.add(task)
    else:
        index=self.tasks.index(self.selectedTask)
        self.tasks.update(index,task)
    self.selectedTask = task
    self.showTasksIntoQListWidget()
    self.fileFactory.writeData("database.json",self.tasks.lists)

Sau khi thêm một Task mới thì chương trình sẽ cập nhật danh sách và lưu xuống tập tin database.json

– Hàm processRemove dùng để xóa các QListWidgetItem được checked trên giao diện ra khỏi QListWidget:

def processRemove(self):
    answer = QMessageBox.question(
        self.MainWindow,
        'Confirmation',
        'Do you want to remove checked Items?',
        QMessageBox.StandardButton.Yes |
        QMessageBox.StandardButton.No
    )
    if answer == QMessageBox.StandardButton.No:
        return
    size=self.listWidgetTask.count()
    for index in range(size-1,-1,-1):
        item=self.listWidgetTask.item(index)
        if item.checkState()==Qt.CheckState.Checked:
            self.tasks.removeByIndex(index)
    self.selectedTask = None
    self.showTasksIntoQListWidget()
    self.fileFactory.writeData("database.json", self.tasks.lists)

-Mã lệnh vòng lặp ở trên Tui chạy ngược lại để khi xóa trong danh sách không bị ảnh hưởng thứ tự.

-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 tasks, 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.

Chương trình sẽ hiển thị cửa sổ xác nhận có muốn xóa hay không, nếu đồng ý xóa thì chương trình sẽ xóa, sau đó sẽ lưu (serialize) lại dữ liệu xuống JSonArray cũng như hiển thị lại giao diện. Hình minh họa xóa:

-Cuối cùng là hàm/signal “processItemSelection“, hàm này sẽ lắng nghe người dùng đang chọn QListWidgetItem nào trên giao diện và hiển thị thông tin chi tiết lên các ô dữ liệu. Người dùng có thể dùng phím mũi tên di chuyển hoặc click chuột vào item bất kỳ:

def processItemSelection(self):
    row=self.listWidgetTask.currentRow()
    task=self.tasks.item(row)
    self.lineEditTitle.setText(task.title)
    self.textEditContent.setText(task.content)
    self.dateEditDeadline.setDate(task.deadline)
    self.timeEditDeadline.setTime(task.deadlinetime)
    if task.isfinish:
        self.radFinished.setChecked(True)
        self.radNotFinished.setChecked(False)
    else:
        self.radFinished.setChecked(False)
        self.radNotFinished.setChecked(True)
    self.selectedTask=task

-Tùy từng thông tin của các task đang chọn mà chương trình sẽ hiển thị dữ liệu lên các widget cho phù hợp.

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

import datetime

from PyQt6.QtCore import Qt
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import QMessageBox, QListWidgetItem

from FileFactory import FileFactory
from MainWindow import Ui_MainWindow
from Task import Task
from Tasks import Tasks

class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        super().__init__()
        self.tasks=Tasks()
        self.fileFactory = FileFactory()
        self.selectedTask=None
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow
        arrData= self.fileFactory.readData("database.json", Task)
        self.tasks.addAll(arrData)
        self.showTasksIntoQListWidget()
        self.pushButtonNew.clicked.connect(self.processNew)
        self.pushButtonSave.clicked.connect(self.processSave)
        self.pushButtonRemove.clicked.connect(self.processRemove)
        self.listWidgetTask.itemSelectionChanged.connect(self.processItemSelection)
    def showTasksIntoQListWidget(self):
        self.listWidgetTask.clear()
        for index in range(self.tasks.size()):
            task=self.tasks.item(index)
            item=QListWidgetItem()
            item.setData(Qt.ItemDataRole.UserRole, task)
            item.setText(str(task))
            item.setCheckState(Qt.CheckState.Unchecked)
            if task.isfinish==True:
                item.setIcon(QIcon("images/ic_finished.png"))
            else:
                item.setIcon(QIcon("images/ic_notfinished.png"))
            if isinstance(task.deadline,str):
                task.deadline= datetime.date.fromisoformat(task.deadline)
            if isinstance(task.deadlinetime,str):
                task.deadlinetime=datetime.time.fromisoformat(task.deadlinetime)
            self.listWidgetTask.addItem(item)
    def processNew(self):
        self.lineEditTitle.setText("")
        self.textEditContent.setText("")
        self.dateEditDeadline.setSpecialValueText(None)
        self.radFinished.setAutoExclusive(False)
        self.radNotFinished.setAutoExclusive(False)
        self.radFinished.setChecked(False)
        self.radNotFinished.setChecked(False)
        self.radFinished.setAutoExclusive(True)
        self.radNotFinished.setAutoExclusive(True)
        self.selectedTask=None
        self.lineEditTitle.setFocus()
    def processSave(self):
        title=self.lineEditTitle.text()
        content=self.textEditContent.toPlainText()
        date=self.dateEditDeadline.date().toPyDate()
        time=self.timeEditDeadline.time().toPyTime()
        isFinished=self.radFinished.isChecked()
        task=Task(title,content,date,time,isFinished)
        if self.selectedTask==None:
            self.tasks.add(task)
        else:
            index=self.tasks.index(self.selectedTask)
            self.tasks.update(index,task)
        self.selectedTask = task
        self.showTasksIntoQListWidget()
        self.fileFactory.writeData("database.json",self.tasks.lists)
    def processRemove(self):
        answer = QMessageBox.question(
            self.MainWindow,
            'Confirmation',
            'Do you want to remove checked Items?',
            QMessageBox.StandardButton.Yes |
            QMessageBox.StandardButton.No
        )
        if answer == QMessageBox.StandardButton.No:
            return
        size=self.listWidgetTask.count()
        for index in range(size-1,-1,-1):
            item=self.listWidgetTask.item(index)
            if item.checkState()==Qt.CheckState.Checked:
                self.tasks.removeByIndex(index)
        self.selectedTask = None
        self.showTasksIntoQListWidget()
        self.fileFactory.writeData("database.json", self.tasks.lists)
    def processItemSelection(self):
        row=self.listWidgetTask.currentRow()
        task=self.tasks.item(row)
        self.lineEditTitle.setText(task.title)
        self.textEditContent.setText(task.content)
        self.dateEditDeadline.setDate(task.deadline)
        self.timeEditDeadline.setTime(task.deadlinetime)
        if task.isfinish:
            self.radFinished.setChecked(True)
            self.radNotFinished.setChecked(False)
        else:
            self.radFinished.setChecked(False)
            self.radNotFinished.setChecked(True)
        self.selectedTask=task
    def show(self):
        self.MainWindow.show()

– Bước 7: 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([])
window=MainWindowEx()
window.setupUi(QMainWindow())
window.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/p8f1hbqvr7w7o6s/LearnQTimeEdit.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 QTimeEdit , củng cố bài cũ QDateEdit và ứng dụng vào quản lý Task, 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 QListWidget để 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ề QDateTimeEdit để tạo ra widget cho người dùng vừa nhập Ngày tháng năm vừa nhập giờ phú giây, 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.

One thought on “Bài 20: QTimeEdit – Basic Widgets – PyQt6”

Leave a Reply