Bài 4: Xử lý Signals và Slots trong PyQt6 và Qt Designer

Hầu hết mọi giao diện đều cung cấp các chức năng cho người sử dụng tương tác trên giao diện. Các ngôn ngữ như C#, Java chúng ta thường nghe tới thuật ngữ “Event”. Tuy nhiên đối với Python, cụ thể là PyQt6 & Qt Designer thì chúng ta sẽ nghe thêm thuật ngữ Signals Slots.

Signals hiểu nôm na là các hành động được phát đi (truyền đi) bởi các đối tượng trên giao diện (gọi chung là các Widget) khi có cái gì đó xảy ra. Ví dụ như dữ liệu thay đổi trong Input Box, hay thao tác nhấn trên Button… Các Signals này thường được phát sinh bởi các hành động của người sử dụng. Các Signals này khi phát đi nó có thể đính kèm cả dữ liệu mà nó muốn phát. 1 Sender (đối tượng) sẽ có nhiều Signals.

Slots hiểu nôm na là các đối tượng nhận Signals (trong Python thì Hàm/Function cũng có thể được dùng làm Slots, ví dụ như khi nhấn vào Nút Thoát thì ta truyền Signals tới hàm XuLyThoat (hàm này gọi là Slots) thì phần mềm sẽ thoát.). 1 Receiver(đối tượng) sẽ có nhiều Slot.

Các Signals Slots ta có thể cấu hình ngay trên Qt Designer hoặc ta tự lập trình trong code Python.

Các đối tượng trong PyQt6 được xây dựng nhiều Slots có sẵn (gọi là built-in Slots), do đó ta có thể sử dụng trực tiếp các Slot này.

Phần mềm dưới đây Tui sẽ minh họa một số ví dụ của Signals và Slots:

  • Nhập dữ liệu trong QLineEdit thì phát tín hiệu (signals) tới QLabel là QLineEdit đang thay đổi dữ liệu đồng thời truyền dữ liệu từ QLineEdit qua cho QLabel (Như vậy bên QLabel gọi là Slots).
  • Tạo Signals khi nhấn vào 1 QPushButton thì màu nền của MainWindow đổi qua màu đỏ
  • Tạo Signals khi nhấn vào 1 QPushButton thì thoát phần mềm.

Các Signals và Slots ở trên có thể dụng built-in hoặc ta tự viết thêm các hàm để làm Slots.

Tạo một dự án tên “LearnSignalsAndSlots” trong Pycharm (cách tạo dự án và giao diện Qt Designer xem lại bài 2bài 3).

Chọn và Thiết kế giao diện MainWindow như hình dưới đây (lưu với tên file “MyMainWindow.ui” vào dự án “LearnSignalsAndSlots” trong Pycharm). Lưu ý màu nền, màu chữ ta tìm thuộc tính styleSheet để cấu hình:

Trong bài này, các Bạn cứ kéo thả control ra và đặt tên như trong Object Inspector. Chi tiết các controls này sẽ được hướng dẫn ở các bài học tiếp theo. Trong bài này chúng ta chỉ focus vào Signals Slots.

Bước 1: Tạo Signal cho QLineEdit (lineEditName) và Slot cho QLabel (labelName):

  • Nhấn biểu tượng dấu + màu xanh trong mục Signal/Slot Editor:

Lúc này một dòng <sender> <signal> <Receiver> <slot> xuất hiện. Ta chọn như sau:

  • Mục <sender>: Chọn lineEditName
  • Mục <Signal> là gán tín hiêu nào sẽ được truyền đi đối với lineEditName. Ta chọn textChanged(QString):
  • Mục <Receiver> là chọn control labelName nào để nhận Signal.
  • Mục <slot> chọn setText(QString)

Bước 2: Tạo Signal cho pushButtonExit, Slot cho MainWindow. Cách nào tương tự như cho QLineEdit và QLabel nên trong bước này chỉ hiển thị kết quả cuối cùng, còn thao tác các bạn lặp lại như hướng dẫn ở trên.

  • Mục <Sender>: Chọn pushButtonExit
  • Mục <Signal>: Chọn clicked()
  • Mục <Receiver>: Chọn MainWindow
  • Mục <Slot>: Chọn close()

Bước 3: Tạo Signal cho pushButtonChangeColor, Slot cho MainWindow (vì ta muốn khi nhấn vào nút Change Color thì đổi màu nền của MainWindow)

Lưu ý rằng, đối với pushButtonChangeColor thì mục <slot> ta chọn đại một slot nào đó (ví dụ chọn repaint()) vì MainWindow không có sẵn slot đổi màu nền (không có built-in slot đổi màu nền). Vì thế ta sẽ lập trình thêm slot bằng cách viết hàm đổi màu nền cho MainWindow, tức là trong trường hợp này Slot của MainWindow là một hàm mới do ta định nghĩa. Nó sẽ được viết sau khi chúng ta Generate Python code cho giao diện này (sau khi viết xong ta thay thế cho hàm repaint()). Tại sao ta lại lấy đại 1 hàm nào đó làm Slot? bởi vì nếu không lấy hàm nào đó ra làm Slot thì lúc generate code nó không tạo ra lệnh Signal clicked() cho pushButtonChangeColor, mất công ta phải viết bổ sung, do đó đây chỉ là Tips để ta lập trình tạo Slot mới nhanh chóng.

Chúng ta tiến hành lưu giao diện “MyMainWindow.ui” lại để Generate code Python nó ra giao diện cuối cùng.

Sau khi Generate Python Code with PyUIC, ta có mã lệnh “MyMainWindow.py“:

Chi tiết mã lệnh của “MyMainWindow.py“:

# Form implementation generated from reading ui file 'MyMainWindow.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(487, 245)
        self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.lineEditName = QtWidgets.QLineEdit(parent=self.centralwidget)
        self.lineEditName.setGeometry(QtCore.QRect(50, 60, 411, 31))
        font = QtGui.QFont()
        font.setPointSize(15)
        self.lineEditName.setFont(font)
        self.lineEditName.setObjectName("lineEditName")
        self.labelName = QtWidgets.QLabel(parent=self.centralwidget)
        self.labelName.setGeometry(QtCore.QRect(50, 110, 411, 31))
        palette = QtGui.QPalette()
        brush = QtGui.QBrush(QtGui.QColor(170, 0, 255))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.WindowText, brush)
        brush = QtGui.QBrush(QtGui.QColor(255, 255, 0))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Button, brush)
        brush = QtGui.QBrush(QtGui.QColor(170, 0, 255))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Text, brush)
        brush = QtGui.QBrush(QtGui.QColor(170, 0, 255))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.ButtonText, brush)
        brush = QtGui.QBrush(QtGui.QColor(255, 255, 0))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Base, brush)
        brush = QtGui.QBrush(QtGui.QColor(255, 255, 0))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Window, brush)
        brush = QtGui.QBrush(QtGui.QColor(170, 0, 255))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.WindowText, brush)
        brush = QtGui.QBrush(QtGui.QColor(255, 255, 0))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Button, brush)
        brush = QtGui.QBrush(QtGui.QColor(170, 0, 255))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Text, brush)
        brush = QtGui.QBrush(QtGui.QColor(170, 0, 255))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.ButtonText, brush)
        brush = QtGui.QBrush(QtGui.QColor(255, 255, 0))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Base, brush)
        brush = QtGui.QBrush(QtGui.QColor(255, 255, 0))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Window, brush)
        brush = QtGui.QBrush(QtGui.QColor(170, 0, 255))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.WindowText, brush)
        brush = QtGui.QBrush(QtGui.QColor(255, 255, 0))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Button, brush)
        brush = QtGui.QBrush(QtGui.QColor(170, 0, 255))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Text, brush)
        brush = QtGui.QBrush(QtGui.QColor(170, 0, 255))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.ButtonText, brush)
        brush = QtGui.QBrush(QtGui.QColor(255, 255, 0))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Base, brush)
        brush = QtGui.QBrush(QtGui.QColor(255, 255, 0))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Window, brush)
        self.labelName.setPalette(palette)
        font = QtGui.QFont()
        font.setPointSize(15)
        self.labelName.setFont(font)
        self.labelName.setAutoFillBackground(False)
        self.labelName.setStyleSheet("background-color: rgb(255, 255, 0);\n"
"color: rgb(170, 0, 255);")
        self.labelName.setText("")
        self.labelName.setObjectName("labelName")
        self.pushButtonChangeColor = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonChangeColor.setGeometry(QtCore.QRect(50, 150, 181, 41))
        font = QtGui.QFont()
        font.setPointSize(15)
        self.pushButtonChangeColor.setFont(font)
        self.pushButtonChangeColor.setObjectName("pushButtonChangeColor")
        self.pushButtonExit = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonExit.setGeometry(QtCore.QRect(350, 150, 111, 41))
        font = QtGui.QFont()
        font.setPointSize(15)
        self.pushButtonExit.setFont(font)
        self.pushButtonExit.setObjectName("pushButtonExit")
        self.label_2 = QtWidgets.QLabel(parent=self.centralwidget)
        self.label_2.setGeometry(QtCore.QRect(120, 10, 241, 31))
        font = QtGui.QFont()
        font.setPointSize(15)
        self.label_2.setFont(font)
        self.label_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
        self.label_2.setObjectName("label_2")
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 487, 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)
        self.lineEditName.textChanged['QString'].connect(self.labelName.setText) # type: ignore
        self.pushButtonExit.clicked.connect(MainWindow.close) # type: ignore
        self.pushButtonChangeColor.clicked.connect(MainWindow.repaint) # type: ignore
        QtCore.QMetaObject.connectSlotsByName(MainWindow)
        MainWindow.setTabOrder(self.lineEditName, self.pushButtonChangeColor)
        MainWindow.setTabOrder(self.pushButtonChangeColor, self.pushButtonExit)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "Trần Duy Thanh - Signals & Slots"))
        self.pushButtonChangeColor.setText(_translate("MainWindow", "Change Color"))
        self.pushButtonExit.setText(_translate("MainWindow", "Exit"))
        self.label_2.setText(_translate("MainWindow", "Signals & Slots"))

Rõ ràng ta thấy dòng 121 ở trên có:

self.pushButtonChangeColor.clicked.connect(MainWindow.repaint) # type: ignore

Ta sửa thành:

self.pushButtonChangeColor.clicked.connect(self.changeBackground) # type: ignore

Bổ sung dòng lệnh dưới cùng của hàm setupUi:

self.MainWindow=MainWindow

Sau đó Ta viết thêm một hàm để làm Slot, Slot này sẽ đổi màu nền của MainWindow qua màu đỏ:

def changeBackground(self):
self.MainWindow.setStyleSheet("background-color: red;")

Chi tiết mã lệnh cuối cùng của “MyMainWindow.py“:

# Form implementation generated from reading ui file 'MyMainWindow.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(487, 245)
        self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.lineEditName = QtWidgets.QLineEdit(parent=self.centralwidget)
        self.lineEditName.setGeometry(QtCore.QRect(50, 60, 411, 31))
        font = QtGui.QFont()
        font.setPointSize(15)
        self.lineEditName.setFont(font)
        self.lineEditName.setObjectName("lineEditName")
        self.labelName = QtWidgets.QLabel(parent=self.centralwidget)
        self.labelName.setGeometry(QtCore.QRect(50, 110, 411, 31))
        palette = QtGui.QPalette()
        brush = QtGui.QBrush(QtGui.QColor(170, 0, 255))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.WindowText, brush)
        brush = QtGui.QBrush(QtGui.QColor(255, 255, 0))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Button, brush)
        brush = QtGui.QBrush(QtGui.QColor(170, 0, 255))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Text, brush)
        brush = QtGui.QBrush(QtGui.QColor(170, 0, 255))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.ButtonText, brush)
        brush = QtGui.QBrush(QtGui.QColor(255, 255, 0))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Base, brush)
        brush = QtGui.QBrush(QtGui.QColor(255, 255, 0))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Window, brush)
        brush = QtGui.QBrush(QtGui.QColor(170, 0, 255))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.WindowText, brush)
        brush = QtGui.QBrush(QtGui.QColor(255, 255, 0))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Button, brush)
        brush = QtGui.QBrush(QtGui.QColor(170, 0, 255))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Text, brush)
        brush = QtGui.QBrush(QtGui.QColor(170, 0, 255))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.ButtonText, brush)
        brush = QtGui.QBrush(QtGui.QColor(255, 255, 0))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Base, brush)
        brush = QtGui.QBrush(QtGui.QColor(255, 255, 0))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Window, brush)
        brush = QtGui.QBrush(QtGui.QColor(170, 0, 255))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.WindowText, brush)
        brush = QtGui.QBrush(QtGui.QColor(255, 255, 0))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Button, brush)
        brush = QtGui.QBrush(QtGui.QColor(170, 0, 255))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Text, brush)
        brush = QtGui.QBrush(QtGui.QColor(170, 0, 255))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.ButtonText, brush)
        brush = QtGui.QBrush(QtGui.QColor(255, 255, 0))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Base, brush)
        brush = QtGui.QBrush(QtGui.QColor(255, 255, 0))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Window, brush)
        self.labelName.setPalette(palette)
        font = QtGui.QFont()
        font.setPointSize(15)
        self.labelName.setFont(font)
        self.labelName.setAutoFillBackground(False)
        self.labelName.setStyleSheet("background-color: rgb(255, 255, 0);\n"
"color: rgb(170, 0, 255);")
        self.labelName.setText("")
        self.labelName.setObjectName("labelName")
        self.pushButtonChangeColor = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonChangeColor.setGeometry(QtCore.QRect(50, 150, 181, 41))
        font = QtGui.QFont()
        font.setPointSize(15)
        self.pushButtonChangeColor.setFont(font)
        self.pushButtonChangeColor.setObjectName("pushButtonChangeColor")
        self.pushButtonExit = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonExit.setGeometry(QtCore.QRect(350, 150, 111, 41))
        font = QtGui.QFont()
        font.setPointSize(15)
        self.pushButtonExit.setFont(font)
        self.pushButtonExit.setObjectName("pushButtonExit")
        self.label_2 = QtWidgets.QLabel(parent=self.centralwidget)
        self.label_2.setGeometry(QtCore.QRect(120, 10, 241, 31))
        font = QtGui.QFont()
        font.setPointSize(15)
        self.label_2.setFont(font)
        self.label_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
        self.label_2.setObjectName("label_2")
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 487, 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)
        self.lineEditName.textChanged['QString'].connect(self.labelName.setText) # type: ignore
        self.pushButtonExit.clicked.connect(MainWindow.close) # type: ignore
        self.pushButtonChangeColor.clicked.connect(self.changeBackground) # type: ignore
        QtCore.QMetaObject.connectSlotsByName(MainWindow)
        MainWindow.setTabOrder(self.lineEditName, self.pushButtonChangeColor)
        MainWindow.setTabOrder(self.pushButtonChangeColor, self.pushButtonExit)
        self.MainWindow=MainWindow
    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "Trần Duy Thanh - Signals & Slots"))
        self.pushButtonChangeColor.setText(_translate("MainWindow", "Change Color"))
        self.pushButtonExit.setText(_translate("MainWindow", "Exit"))
        self.label_2.setText(_translate("MainWindow", "Signals & Slots"))
    def changeBackground(self):
        self.MainWindow.setStyleSheet("background-color: red;")

Cuối cùng ta tạo một file Python “MyApp.py” trong dự án Pycharm để gọi các lệnh chạy giao diện phần mềm:

Mã lệnh của “MyApp.py” tương tự như các bài học trước, ta có chi tiết:

from PyQt6.QtWidgets import QApplication, QMainWindow

from MyMainWindow import Ui_MainWindow

app=QApplication([])

qMainWindow=QMainWindow()

myWindow=Ui_MainWindow()

myWindow.setupUi(qMainWindow)

qMainWindow.show()

app.exec()

Chạy phần mềm lên ta có kết quả như sau:

Ta gõ dữ liệu trong ô QLineEdit tới đâu thì QLabel sẽ tự động cập nhật dữ liệu tới đó. Như vậy QLineEdit (Sender) đã truyền Signal “textChanged” cho QLabel (Receiver), và QLabel dùng Slot setText để nhận dữ liệu.

Tương tự như vậy ta thử nghiệm nhất vào nút QPushButton “Change Color”, lúc này màu nền của MainWindow sẽ chuyển qua màu đỏ:

Cuối cùng là khi người sử dụng nhấn vào nút QPushButton (Sender) “Exit” thì nó sẽ truyền Signal “clicked” tới MainWindow (Receiver) và MainWindow dùng Slot close để nhận tín hiệu, tức là nó thoát phần mềm.

Như vậy Tui đã hướng dẫn xong phần Signals và Slot rất chi tiết, các bạn cố gắng đọc thật kỹ lý thuyết, cũng như cơ chế hoạt động của chúng. Phân biệt đâu là Sender, đâu là Receiver, đâu là Signals, đâu là Slot. Đồng thời phải biết cách dùng Qt Designer để cấu hình các Signals và Slot, ngoài ra phải biết cách định nghĩa hàm trong Python để làm Slot.

Đây là coding đầy đủ của bài này:

https://www.mediafire.com/file/w2xr6oaxkoqud6o/LearnSignalsAndSlots.rar/file

Các bài học sau Tui sẽ trình bày chi tiết về MainWindow cũng như các thuộc tính, Signal, Slot và event của nó. Sau khi nắm chắc MainWindow thì chúng ta sẽ học về Layout để biết cách bố cục giao diện, biết được bố cục giao diện thì ta mới đi chi tiết vào từng Widgets cụ thể được.

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

Bài 3: Ý nghĩa các thành phần trong dự án PyQt6-Qt Designer

Trong bài 1 bài 2 các bạn đã biết được cách tạo dự án với giao diện PyQt6 và công cụ thiết kế giao diện Qt Designer, cũng như biết cách tích hợp chúng vào Pycharm để hỗ trợ việc lập trình xử lý giao diện được nhanh chóng và dễ dàng.

Trong bài này, Tui sẽ giải thích chi tiết từng mã lệnh thành phần của một dự án PyQt6 – Qt Designer cũng như cơ chế hoạt động của event loop.

Bước 1: Tạo một dự án tên “MyFirstApplication” trong Pycharm

Bước 2: Mở phần mềm Qt Designer bằng cách bấm chuột phải vào dự án MyFirstApplication/Chọn “Create new Qt Designer“. Lưu ý bạn phải học qua bài số 2 thì mới thấy các công cụ trong External Tools.

Sau khi chọn Create new Qt Designer, phần mềm này sẽ được kích hoạt, chúng ta chọn MainWindow như hình dưới đây:

Nhấn nút “Create” để tạo màn hình cửa sổ giao diện

Mục 1: Mục này được gọi là Widget Box, nó chứa hầu hết các layout, controls của giao diện, được chia thành 8 nhóm với hơn 50 layout và controls thường sử dụng được cung cấp. Lập trình viên có thể kéo thả các control này vào phần thiết kế giao diện ở mục số 2.

  • Nhóm Layouts: Nhóm này cung cấp cách thức bố trí cấu trúc giao diện, như vậy trước khi thiết kế một giao diện thì chúng ta cần quan sát xem cấu trúc tổng quan của giao diện là gì để có thể lựa chọn và phối hợp các layout này lại với nhau nhằm tạo ra một giao diện như mong muốn. Các Layout được cung cấp gồm “Vertical Layout”, “Horizontal Layout”, “Grid Layout” và “Form Layout”.
  • Nhóm Spacers: Nhóm này dùng để tạo và định nghĩa các vùng trống của các Control, có 2 loại Spacer được cung cấp đó là “Horizontal Spacer” và “Verticla Spacer”
  • Nhóm Buttons: Là nhóm các control liên quan tới các nút lệnh, chẳng hạn như “Push Button”, “Radio Button”, “Checkbox”, “Command Link Button”, “Dialog Button Box” và “Tool Button”
  • Nhóm Item Views (Model-Based): Nhóm này chứa các control hiển thị dữ liệu dạng bảng, dạng cây, các control bao gồm “List View”, “Tree View”, “Table View” và “Column View”
  • Nhóm Item Widget (Item-Based): Nhóm chứa các Widget dạng danh sách, cây, bảng, các widget bao gồm “List Widget”, “Tree Widget”, và “Table Widget”
  • Nhóm Container: Nhóm này được xem là các đối tượng khung chứa để chứa các controls, phân nhóm các control trên giao diện tương tác để cho nó rõ ràng và dễ sử dụng hơn. Các Containers bao gồm “Group Box”
  • Nhóm Input Widgets: Đây là các nhóm Controls mà người sử dụng thường dùng để nhập và hiển dữ liệu, các controls bao gồm “Combo Box”, “Font Combo Box”, “Line Edit”, “Text Edit”, “Plain Text Edit”, “Spin Box”, “Double Spin Box”, Các control liên quan tới ngày tháng năm như “Time Edit”, “Date Edit”, “Date/Time Edit”, và các control khác như Dial, Key Sequence Edit, cùng với các slider và Scroll Bar như “Vertical Slider”, “Horizontal Slider”, “Vertical Scroll Bar” và “Horizontal Scroll Bar”
  • Nhóm Display Widgets: Nhóm này là các controls thường được dùng để hiển thị mà không cho nhập liệu, chẳng hạn như “Label”, “Text Browser”, “Graphics View”, “Calendar Widget”, “LCD Number”, “Progress Bar”, “Horizontal Line”, “Vertical Line” và “Open GL Widget”

Mục 2: Mục 2 là mục thiết kế giao diện chính của phần mềm, các control, layout trong mục 1 sẽ được kéo thả và mục này, các bố cục của giao diện sẽ được hiển thị chi tiết trong mục số 3 Object Inspector. Để thay đổi (định dạng) các control nào thì ta nhấn chuột và control đó rồi chỉnh trong mục 4 Property Editor, để thêm các Signal và Slot thì chỉnh trong mục 5.

Như vậy có thể nói, mục 2 là mục trung tâm của thiết kế giao diện tương tác người dùng. Trong mục 2 cũng cung cấp Context Menu (nhấn chuột phải) để sử dụng thêm các cấu hình khác:

Mục 3: Mục 3 là mục Object Inspector. Mục này hiển thị chi tiết các bố cục của giao diện, các control được hiển thị trong table với 2 cột, cột Object là cột thể hiện các tên biến control, cột Class là thể hiện Object này được tạo ra từ class nào. Dựa vào mô tả sơ lược này mà ta có thể dễ dàng biết được các control thuộc lớp nào để lập trình tương tác cho đúng:

Ví dụ hình ở trên ta thấy Object có tên là “myTitle” thuộc class QLabel.

Và nhìn vào hình trên ta cũng biết được cấu trúc (bố cục) tổng quan của giao diện.

Mục 4: Đây là mục Property Editor, là mục dùng để thiết lập các thuộc tính cho Control trên giao diện, rất quan trọng.

Tùy vào từng đối tượng chúng ta chọn trên giao diện mà trong mục Property Editor này sẽ hiển thị các thông số khác nhau, sau đó ta có thể tùy chỉnh giá trị của các thuộc tính trong này một cách dễ dàng.

Ví dụ minh họa các thuộc tính của màn hình MainWindow:

Ví dụ màn hình các thuộc tính của QLabel:

Mục 5: Là mục gán các Signal/Slot Editor (event) cho các control trên giao diện

Chẳng hạn như muốn gán Signal nhấn nút lệnh cho control thì chúng ta khai báo trong này (kéo thả bổ sung thêm 1 Push Button vào giao diện như dưới đây):

minh họa ở trên: Khi nhấn vào nút “Click Me” thì dữ liệu trên QLabel title sẽ bị xóa trắng.

Mục 6: Mục này là các Menu và tool bars, các lệnh thường dùng sẽ được hiển thị ở đây để lập trình viên lựa chọn một cách nhanh chóng.

Như vậy là các thành phần trong giao diện Qt Designer đã được trình bày ở trên, chi tiết các thành phần này sẽ được giải thích cặn kẽ về lý thuyết cũng như cách thức lập trình ở các bài học tương ứng.

Bây giờ chúng ta lưu giao diện này vào dự án “MyFirstApplication” trong Pycharm để chúng ta tiếp tục giải thích ý nghĩa của các kỹ thuật lập trình đối với giao diện MainWindow này. Đặt tên giao diện là “MyFirstApplication.ui

Ta tiến hành dùng External tool để tạo mã lệnh Python cho giao diện:

Sau khi bấm “Generate Python Code with PyUIC” thì mã lệnh “MyFirstApplication.py” được tạo ra như dưới đây:

Tạm thời trong bài này chúng ta chưa tiến hành hiệu chỉnh “MyFirstApplication.py”, các bài sau chúng ta sẽ tiến hành hiệu chỉnh bằng cách kế thừa từ lớp này để vẫn đảm bảo được cấu trúc tự động Generate Coding và vẫn giữ được các mã lệnh mới bổ sung trong các lớp kế thừa (tránh chỉnh sửa trực tiếp trong lớp Generate này vì nó sẽ bị mất nếu ta Generate lưu đè lên code cũ).

# Form implementation generated from reading ui file 'MyFirstApplication.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(958, 796)
        self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.myTitle = QtWidgets.QLabel(parent=self.centralwidget)
        self.myTitle.setGeometry(QtCore.QRect(70, 10, 421, 81))
        font = QtGui.QFont()
        font.setPointSize(20)
        self.myTitle.setFont(font)
        self.myTitle.setObjectName("myTitle")
        self.clickMe = QtWidgets.QPushButton(parent=self.centralwidget)
        self.clickMe.setGeometry(QtCore.QRect(110, 150, 171, 41))
        self.clickMe.setObjectName("clickMe")
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 958, 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)
        self.clickMe.clicked.connect(self.myTitle.clear) # type: ignore
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
        self.myTitle.setText(_translate("MainWindow", "https://tranduythanh.com/"))
        self.clickMe.setText(_translate("MainWindow", "Click Me To Clear Title"))

Tiếp theo ta tạo thêm một file Python với tên “MyApp.py“:

Bổ sung mã lệnh cho “MyApp.py“:

from PyQt6.QtWidgets import QApplication, QMainWindow
# import sys for Accessing to command line arguments
import sys

from MyFirstApplication import Ui_MainWindow

# You need one (and only one) QApplication instance per application.
# Pass in sys.argv to allow command line arguments for your app.

app=QApplication(sys.argv)
# If you know you won't use command line arguments QApplication([]) works too
#app=QApplication([])

#Create QMainWindow
qMainWindow=QMainWindow()
#Call Ui_MainWindow in the MyFirstApplication.py, of course we can change the name
myWindow=Ui_MainWindow()
#Call setupUi method
myWindow.setupUi(qMainWindow)
#call show method
qMainWindow.show()
#Start the Event loop
app.exec()

Chạy “MyApp.py” ta có giao diện như mong muốn:

Nếu như chúng ta nhấn nút lệnh “Click Me To Clear Title” thì title sẽ biến mất.

Bây giờ chúng ta khám phá các mã lệnh trong file “MyApp.py“:

from PyQt6.QtWidgets import QApplication, QMainWindow

Trong mã lệnh của chúng ta có dùng các lớp QApplication và QMainWindow nên chúng ta import nó, các lớp này nằm trong gói PyQt6.QtWidgets

Mỗi một phần mềm chúng ta khai báo 1 và chỉ 1 đối tượng QApplication. Đối tượng này có thể không nhận bất kỳ đối số nào hoặc nhận đối số từ command line thông quy sys.argv:

app=QApplication(sys.argv)

Nếu không muốn nhận thông số nào cả (parameter):

app=QApplication([])

Đối tượng này sẽ nắm giữ toàn bộ Event Loops (có thể hiểu là nắm giữ toàn bộ các sự kiện phát sinh xảy ra trên màn hình cửa sổ)

Tiếp theo ta cần khai báo 1 đối tượng QMainWindow()

#Create QMainWindow
qMainWindow=QMainWindow()

Đối tượng qMainWindow sẽ được truyền vào lớp Ui_MainWindow thông qua hàm setupUi để khởi tạo giao diện:

#Call Ui_MainWindow in the MyFirstApplication.py, of course we can change the name
myWindow=Ui_MainWindow()
#Call setupUi method
myWindow.setupUi(qMainWindow)

Lưu ý rằng lớp Ui_MainWindow là do chương trình tự tạo ra và tự đặt tên khi tạo mã lệnh từ giao diện, ta có thể đổi tên tùy ý.

hàm setupUi(qMainWindow) sẽ tiến hành nạp lại giao diện.

sau đó ta gọi hàm show() để hiển thị giao diện:

#call show method
qMainWindow.show()

Nhưng nhớ rằng, nó sẽ tự động tắt màn hình giao diện ngay lập tức thì lúc này ta chưa kích hoạt Event loop. Do đó ta bắt buộc phải gọi lệnh sau:

#Start the Event loop
app.exec()

Vậy Event Loop là gì? nó đóng vai trò gì trong chương trình?

Lõi của Qt Application là lớp QApplication , mỗi một ứng dụng cần một và chỉ một đối tượng QApplication. Đối tượng này nắm giữ và điều khiển các Even loop của ứng dụng (các thao tác người dùng). Mọi thao tác người dùng trên giao diện sẽ được đẩy vào hàng đợi gọi là Event Queue, sau đó các Event này sẽ được xử lý luôn hoặc chuyển tiếp tới các tác vụ khác. Ví dụ như bạn Click click click bạn nhấn nhấn nhấn ….. trên các control hay trên Window thì các sự kiện này sẽ được đưa vào hàng đợi và nó sẽ được xử lý tuần tự, hết event này sẽ tới event khác trong queue. Và tại một thời điểm thì chỉ có một event loop được chạy cho mỗi ứng dụng.

Như vậy tới đây Tui đã trình bày xong Ý nghĩa các thành phần trong dự án PyQt6-Qt Designer. Các bạn đã biết cách tạo giao diện, hiểu được cấu trúc thành phần trên giao diện cũng như ý nghĩa của chúng. Đặc biệt hiểu cách thức gọi mã lệnh để tương tác người dùng, hiểu được tại sao phải dùng các dòng lệnh đó cũng như nắm rõ quy trình hoạt động của Event loop.

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

https://www.mediafire.com/file/52m26det0ho3zh7/MyFirstApplication.rar/file

Bài sau Tui sẽ trình bày chi tiết về MainWindow cũng như nói rõ về cơ chế hoạt động của SignalsSlots trên ứng dụng.

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

Bài 2: Tích hợp Qt Designer và PyUIC vào Pycharm

Trong Bài 1 các bạn đã biết cách cài đặt và sử dụng Qt Designer. Tuy nhiên việc mở phần mềm Qt Designer để thiết kế, sau đó chuyển qua Pycharm cũng làm mất khá nhiều thời gian. Và đặc biệt ta cũng cần biết được các tên biến control được khai báo và bố trí như thế nào trong giao diện “.ui”. Vì vậy bài này Tui sẽ hướng dẫn các bạn cách thức tích hợp Qt Designer vào Pycharm để chỉ cần một cú click chuột là có thể mở giao diện lên để cập nhật thiết kế cũng như tạo mới, đồng thời cũng hướng dẫn cách tích hợp PyUIC để tự động tạo mã nguồn Python từ các giao diện “.ui” bằng một cú click chuột.

Các bạn xem hình dưới đây:

Làm thế nào để ta tích hợp được 3 menu chức năng vào External Tools:

  • menu “Create new Qt Designer“, dùng để mở phần mềm Qt Designer và thiết kế một giao diện mới
  • menu “Open Selected Qt Designer“, dùng để mở file giao diện thiết kế đang chọn để cập nhật thêm giao diện cho nó
  • menu “Generate Python Code with PyUIC“, chức năng này dùng để tự động tạo mã lệnh Python cho giao diện đang chọn.

Để làm được điều trên, Chúng ta tiến hành các bước như sau:

Bước 1: Vào menu File/ chọn Settings

Bước 2: Tìm tới mục “External Tools” như hình dưới đây:

Trong màn hình External Tools, bạn quan sát thấy ở bên trên có danh sách các nút lệnh “+”, “-“, …. dùng để thêm menu, xóa menu, chỉnh sửa menu, di chuyển vị trí xuất hiện:

Bước 3: Tạo menu “Create new Qt Designer

Menu này có chức năng là nhấn vào thì mở phần mềm Qt Designer lên để tạo màn hình giao diện thiết kế mới.

Nhấn vào biểu tượng dấu “+” để tạo mới một menu:

Sau đó tiến hành định nghĩa cho các fields như sau:

  • Name: Nhập vào tên của menu, ví dụ “Create new Qt Designer”
  • Description: Nhập vào mô tả cho menu, ví dụ “This function used to Create new Qt Designer”
  • Program: Trỏ tới chính xác file designer.exe mà ta cài Qt Designer, trong trường hợp này thì nó là đường dẫn “C:\Program Files (x86)\Qt Designer\designer.exe“, dĩ nhiên tùy vào lúc bạn cài đặt, phải trỏ cho đúng
  • Working directory: Mục này phải cho phần mềm biết đường dẫn hiện tại của dự án là gì. Ta dùng chính xác mã lệnh: $ProjectFileDir$

Sau đó nhấn nút “OK” để tạo menu “Create new Qt Designer”, xem kết quả:

Bước 4: Tạo menu “Open Selected Qt Designer

Menu này có chức năng là khi lập trình viên bấm chuột phải file giao diện “.ui” thì giao diện này sẽ được mở lên trong Qt Designer để ta tiếp tục thiết kế. Mọi sự thay đổi lúc thiết kế lúc ta lưu trữ nó sẽ tự động cập nhật trong dự án.

Trong màn hình External Tools ta tiếp tục lặp lại thao tác nhấn vào nút “+” để mở màn hình thêm menu mới:

  • Name: Nhập vào tên của menu, ví dụ “Open Selected Qt Designer
  • Description: Nhập vào mô tả cho menu, ví dụ “This tool used to open Open Selected Qt Designer”
  • Program: Trỏ tới chính xác file designer.exe mà ta cài Qt Designer, trong trường hợp này thì nó là đường dẫn “C:\Program Files (x86)\Qt Designer\designer.exe“, dĩ nhiên tùy vào lúc bạn cài đặt, phải trỏ cho đúng
  • Arguments: Cần cho phần mềm biết ta đang bấm chuột phải vào file giao diện “.ui” nào. Ta dùng chính xác mã lệnh: “$FileDir$\$FileName$”. Lưu ý là có nháy kép bao bọc mã lệnh để trong trường hợp bạn lưu source code trong thư mục có khoảng trắng thì nó vẫn hiểu. Đối số này sẽ được truyền cho phần mềm Qt Designer, nên khi mở phần mềm lên nó sẽ tự động mở màn hình giao diện theo đúng đường dẫn này.
  • Working directory: Mục này phải cho phần mềm biết đường dẫn hiện tại của dự án là gì. Ta dùng chính xác mã lệnh: $ProjectFileDir$

Nhấn nút “OK” để tạo menu “Open Selected Qt Designer“:

Bước 5: Tạo menu “Generate Python Code with PyUIC

Menu này có chức năng là tự động tạo mã lệnh Python từ giao diện “.ui”, nó rất hữu ích để cho lập trình viên biết được các biến control nào được bố trí trong giao diện để lập trình tương tác sau này.

Trong màn hình External Tools ta tiếp tục lặp lại thao tác nhấn vào nút “+” để mở màn hình thêm menu mới:

  • Name: Nhập vào tên của menu, ví dụ “Generate Python Code with PyUIC
  • Description: Nhập vào mô tả cho menu, ví dụ “This function used to generate Python Code with PyUIC”
  • Program: Trỏ tới chính xác file python.exe mà ta cài Python, trong trường hợp này thì nó là đường dẫn “C:\Python311\python.exe“, dĩ nhiên tùy vào lúc bạn cài đặt, phải trỏ cho đúng
  • Arguments: Cần cho phần mềm biết ta đang bấm chuột phải vào file giao diện “.ui” nào và file mã lệnh Python nào nên được tạo ra cho giao diện này. Ta dùng chính xác mã lệnh: -m PyQt6.uic.pyuic “$FileName$” -o “$FileNameWithoutExtension$.py”. Lưu ý là có nháy kép bao bọc mã lệnh để trong trường hợp bạn lưu source code trong thư mục có khoảng trắng thì nó vẫn hiểu. Đối số này sẽ được truyền cho phần mềm Qt Designer, nên khi mở phần mềm lên nó sẽ tự động mở màn hình giao diện theo đúng đường dẫn này.
  • Working directory: Mục này phải cho phần mềm biết đường dẫn hiện tại của dự án là gì. Ta dùng chính xác mã lệnh: $ProjectFileDir$

Nhấn nút “OK” để tạo menu “Generate Python Code with PyUIC“:

Bước 6: Thử nghiệm các menu vừa tạo

Ta có 2 nơi để sử dụng các menu này trong Pycharm:

  • Vào menu Tools/ chọn External Tools/ chọn các menu
  • hoặc bấm chuột phải vào dự án/file chọn External Tools/ Chọn các menu

Hoặc:

  • Ví dụ bây giờ ta chọn chức năng “Create new Qt Designer“, phần mềm Qt Designer sẽ hiển thị như dưới đây với màn hình chọn New Form:
  • Nếu ta bấm chuột phải vào file “HelloWorldDialog.ui” rồi chọn “Open Selected Qt Designer“, chương trình Qt Designer sẽ được mở lên cùng với màn hình giao diện đang chọn này:
  • Nếu ta bấm chuột phải vào file “HelloWorldDialog.ui” rồi chọn “Generate Python Code with PyUIC“, chương trình sẽ tự động tạo mã lệnh “HelloWorldDialog.py” cho giao diện này:

Chọn Generate Python Code with PyUIC, ta có kết quả:

Bước 7: Lập trình để hiển thị cửa sổ giao diện bằng lớp Generate Python

Khi “HelloWorldDialog.py” được tạo ra thì ta có thêm một phương án lựa chọn cho việc hiển thị cửa sổ giao diện (tức là phương án tải file .ui vẫn dùng bình thường). Cách số 2 này ta làm như sau:

Tạo một lớp Python, ví dụ đặt tên “TestHelloWorldDialogUI.py”, sau đó ta viết mã lệnh như sau:

from PyQt6.QtWidgets import QApplication, QDialog
from HelloWorldDialog import Ui_Dialog

app=QApplication([])
dialog=QDialog()
window = Ui_Dialog()
window.setupUi(dialog)
dialog.show()
app.exec()

Chạy mã lệnh “TestHelloWorldDialogUI.py” lên ta có kết quả:

Như vậy rõ ràng ta vẫn có được giao diện như mong muốn, nhưng phương án Generate Python code này có gì hay hơn phương án load file UI như đã nói trong bài số 1? Phương án này hay hơn rất nhiều đó là ta có thể dễ dàng truy suất tới các biến control trên giao diện thông các các lớp, có thể dễ dàng mở rộng mã lớp, tạo thêm các lớp kế thừa để bổ sung thêm các chức năng khác một cách tối ưu nhất có thể.

Source code bài này tải ở đây:

https://www.mediafire.com/file/lpehrxzl7clssbj/HelloWorld.rar/file

Các bài học sau Tui sẽ trình bày chi tiết từng layout và control cụ thể để các bạn có thể tự tay xây dựng được các giao diện theo ý muốn của mình.

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

Bài 1: Cài đặt và sử dụng Python – Qt Designer

Python có nhiều nền tảng và thư viện để thiết kế giao diện, chẳng hạn như khi thiết kế giao diện Web thì có thể dùng Django, Flask Microservice, thiế kế giao diện desktop thì dùng tkinter hoặc các thư viện khác.

Để giúp Sinh viên và lập trình viên dễ dàng thiết kế giao diện dạng trực quan dạng kéo thả, cũng như triển khai các phần mềm tương tác người dùng bằng Python được dễ dàng hơn, chuỗi bài học này Tui sẽ hướng dẫn các kỹ thuật liên quan tới Python – Qt Designer để thiết kế giao diện, cùng với việc sử dụng thành thạo gói thư viện PyQt6 sẽ giúp cho người học dễ dàng thiết kế và xử lý giao diện người dùng, chạy các mô hình máy học trên giao diện.

Để học tốt các chuỗi bài học này, hiển nhiên các bạn phải có kiến thức chút ít về Python, cũng như một số công cụ cần được cài đặt sẵn trong máy như:

Sau đó ta tiến hành cài đặt và sử dụng Python – Qt Designer theo 5 bước như dưới đây:

Bước 1: Cài đặt PyQt6

python -m pip install PyQt6

Nếu cài đặt thành công bạn sẽ thấy các thông báo kết quả như trên.

Bước 2: Tải và cài đặt Qt Designer

Link tải ở đây:

https://build-system.fman.io/static/public/files/Qt%20Designer%20Setup.exe

Hoặc ở đây:

https://tranduythanh.com/software/QtDesignerSetup.rar

Sau khi tải về, các bạn sẽ thấy tập tin “Qt Designer Setup.exe” như dưới đây:

Ta tiến hành double click vào file trên để cài đặt:

Bấm Next để tiếp tục

Mặc định Destination Folder sẽ nằm trong ổ C như hình, ta tiến hành bấm nút “Install” để bắt đầu cài đặt, và chờ chương trình cài đặt:

Sau khi cài đặt hoàn tất thì bạn thấy giao diện hiển thị ra như dưới đây:

Nhấn “Next” để ra màn hình Comleting the Qt Designer Setup Wizard:

Màn hình trên có nút checked “Run Qt Designer”, ta nhấn nút Finish để vừa kết thúc và mở phần mềm này lên:

Ngoài ra ta có thể tìm thấy phần mềm trong thanh công cụ tìm kiếm của hệ điều hành:

Bước 3: Tạo dự án trong Pycharm

Khởi động phần mềm Pycharm Community:

Nhấn vào nút “New Project” để tạo dự án

Đặt dự án tên “HelloWorld”. Hiển nhiên mới học một cái gì đó thì ta cứ nên tạo “HelloWorld” cho nó thân thiện và bảo vệ môi trường. Các thông số khác cấu hình như trên. Sau đó Nhấn nút “Create” để tạo dự án, màn hình dự án mới sẽ được hiển thị như dưới đây:

Sau đó ta tạo một file python tên là “main.py” bằng cách nhấn chuột phải vào dự án “HelloWorld” rồi chọn “New” sau đó chọn “Python File”:

Một màn hình mới sẽ xuất hiện như dưới đây:

Ta nhập tên là “main”, phía bên dưới chọn “Python file” rồi nhấn phím Enter:

Như vậy file main.py được tạo ra như trên, ta để đó và quay lại phần mềm Qt Designer để thiết kế giao diện:

Bước 4: Thiết kế giao diện với Qt Designer

  • Đầu tiên ta chọn biểu tượng 1 để hiển thị cửa sổ “New Form – Qt Designer
  • Sau đó ở mục 2 ta chọn “Dialog with Button Bottom”, dĩ nhiên đây là ví dụ cho Hello World, còn tùy thuộc vào nhu cầu thiết kế giao diện mà bạn tùy chọn các loại khác nhau
  • Cuối cùng ta nhấn nút Create ở mục số 3 để tạo Form

Giao diện thiết kế Form:

Chi tiết các thành phần trong giao diện Qt-Designer sẽ được trình bày kỹ lưỡng ở các bài học tiếp theo. Trong bài này ta chỉ cần làm các công việc sau:

  • Tạo tiêu đề mới cho cửa sổ Dialog
  • Tạo 1 Label

Để tạo tiêu đề mới cho cửa sổ ta nhấn vào tiêu đề của Dialog và sau đó quan sát mục “Property Editor”, tìm tới thuộc tính windowTitle:

Mặc định ta thấy tiêu đề đang để “Dialog”, ta nhấn vào nó và chỉnh qua tiêu đề mới như hình dưới đây:

Ví dụ đổi tiêu đề qua “Tran Duy Thanh – Hello World”

Tiếp theo ở mục Widget Box, ta vào nhóm “Display Widgets“, nhấn và kéo control “Label” vào màn hình Form:

Sau đó sửa text hiển thị thành “Hello World”

Để sửa text hiển thị, ta nhấn vào Label đó trước, sau đó nhìn vào mục Property Editor. Tìm tới thuộc tính text và chỉnh qua “Hello World”

Ta cũng có thể hiệu chỉnh chữ lớn lên bằng cách tìm tới thuộc tính Font của control label này:

Theo hình trên thì ta chỉnh Point Size là 20, chữ in đậm (Bold)

Ngoài ra, Qt Designer cũng hỗ trợ chúng ta thiết kế Label có chuỗi dạng HTML:

Như ta thấy, ở màn hình trên, trong thuộc tính text của Label, ta nhấn vào nút có dấu “…”, chương trình sẽ hiển thị Edit Text của Label lên, trong màn hình này ta thoái mái định dạng cách thức hiển thị dữ liệu.

Sau khi tiết kế xong, ta cần lưu màn hình này vào dự án HelloWorld trong Pycharm.

Ta nhấn Ctrl +S hoặc nhấn vào biểu tựu đĩa mềm để lưu.

chú ý ta cần lưu vào đúng nơi dự án HelloWorld, đặt tên là “HelloWorldDialog.ui

Màn hình trên cho ta thấy kết quả sau khi lưu file giao diện ui thành công.

Bước 5: Lập trình để hiển thị giao diện người dùng.

Trong file “main.py” ta viết mã lệnh như sau:

from PyQt6 import uic
from PyQt6.QtWidgets import QApplication

Form, Window = uic.loadUiType("HelloWorldDialog.ui")

app = QApplication([])
window = Window()
form = Form()
form.setupUi(window)
window.show()
app.exec()

Sau đó ta tiến hành chạy file main.py :

Bấm chuột phải vào ‘main.py’ rồi chọn Run ‘main’, ta có kết quả giao diện:

Như vậy tới đây các bạn đã thành công trong việc cài đặt và sử dụng Qt Designer, biết cách dùng coding Python để nạp giao diện để chạy chương trình.

Ngoài bản Qt Designer phiên bản nhỏ gọn ở trên, các bạn có thể tải Qt Creator với nhiều tính năng phong phú tuy nhiên khá nặng: https://www.qt.io/offline-installers

Các bài sau, Tui sẽ hướng dẫn cách tích hợp Qt Designer vào ngay bên trong công cụ Pycharm cũng như cách tự động generate code Python từ file giao diện để giúp lập trình thiết kế giao diện một cách nhanh chóng và tiện lợi hơn.

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

Giới thiệu Deeplearning4j

Hiện nay có nhiều nền tảng hỗ trợ triển khai các mô hình máy học, và chúng được viết bằng nhiều ngôn ngữ lập trình khác nhau. Chẳng hạn như Python cung cấp nhiều thư viện như Keras, Gluon; C#/F# là ngôn ngữ được viết trong nền tảng máy học ML .NET; Java/Kotlin, Scala, Clojure là các ngôn ngữ được viết trong nền tảng máy học Deeplearning4j.

Thậm chí chúng ta có thể kết hợp nhiều ngôn ngữ lập trình khác nhau để xây dựng ra các mô hình máy học linh động, đáp ứng hầu hết các nhu cầu của người dùng.

Tương tự như các nền tảng máy học được viết bằng Python, hay các nền tảng máy học được viết bằng C#/F# thì Deeplearning4j cũng là một thư viện mã nguồn mở, nó hỗ trợ hầu hết các hướng tiếp cận của các mô hình máy học, chẳng hạn như:

DL4J(Deeplearning4j) cũng hỗ trợ nhiều loại giải thuật khác nhau trong Machine Learning, bao gồm (Tui chưa liệt kê hết):

  • Support Vector Machines
  • Linear regression
  • Logistic regression
  • Naïve Bayes
  • Linear discriminant analysis
  • Decision trees
  • Neural Networks (Multilayer perceptron)

Chi tiết về DL4J người đọc có thể xem thêm tại link https://deeplearning4j.konduit.ai/

Source code đầy đủ ứng dụng của DL4J: https://github.com/deeplearning4j

Và các bài của Enrique Llerena Dominguez https://ienjoysoftware.dev/

Về cơ bản, ứng dụng máy học thường có 3 bước làm việc chính, đó là bước PREPARING (data and platform), TRAINING và bước PREDICTION. Và nó là giống nhau không lệ thuộc vào chúng ta chọn nền tảng hay ngôn ngữ nào, Tui cũng đã viết trong bài này các bạn có thể đọc lại.

Tui minh họa lại các bước sử dụng DeepLearning4J cho 3 bước xử lý với một chương trình máy học như dưới đây:

Bước Preparing gồm 3 bước:

  • (1) Problem description: Mô tả vấn đề mà ta muốn xử lý bằng các mô hình máy học. Tùy vào vấn đề mà ta muốn giải quyết để lựa chọn các dữ liệu phù hợp
  • (2) Dataset description: Mô tả về dữ liệu để giải quyết vấn đề mà ta muốn. Bước Problem description và Dataset description có thể hoán đổi cho nhau. Đôi khi ta có dữ liệu trước rồi mới chợt nghĩ ra các vấn đề liên quan tới dữ liệu. Hoặc ta có vấn đề trước rồi mới nghĩ cách tìm ra các dữ liệu phù hợp cho vấn đề.
  • (3) Sau đó ta tạo dự án và tích hợp DeepLearning4j vào, chi tiết Integrated DeepLearning4j sẽ được trình bày ở bài sau.

Bước Training gồm có 7 bước:

  • (1) Load Dataset: Nạp dữ liệu để chuẩn bị cho train mô hình
  • (2) Data Normalization: cần phải được chuẩn hóa thì mô hình mới hiểu, tùy vào từng mô hình máy học mà kỹ thuật chuẩn hóa sẽ khác nhau, nên khi nào vào bài toán cụ thể thì ta sẽ lựa chọn kỹ thuật chuẩn hóa phù hợp.
  • (3) Train and test selection: Dữ liệu cần được tách thành các phần train và set với các tỉ lệ phù hợp, việc lựa chọn tỉ lệ cũng đóng vai trò quan trong cho chất lượng của mô hình
  • (4) Model configuration: Mỗi mô hình trước khi train cần được cài đặt các thông số, việc cài đặt này quyết định tới chất lượng mô hình, do đó cần có kinh nghiệm
  • (5) Train model: bắt đầu train mô hình
  • (6) Evaluate model: mô hình train xong cần phải được đánh giá chất lượng xem có tốt hay không, có nhiều thang đo khác nhau để đo lường chất lượng của mô hình. Tùy từng bài toán mà ta lựa chọn các thang đo phù hợp. Nếu mô hình đạt chất lượng không tốt thì có thể quay lại bước Preparing để làm lại dữ liệu, hay quay lại bước train and test selection để lựa chọn lại tỉ lệ cũng như cấu hình lại các thông số để train mô hình. Lưu ý trong máy học không có mô hình đúng hay mô hình sai, mà chỉ có mô hình phù hợp hay không.
  • (7) Export model: Sau khi mô hình được đánh giá chất lượng thì nó cần được lưu xuống ổ cứng để phục vụ cho việc tái sử dụng và các công đoạn prediction. Lưu ý cần lưu luôn cả chuẩn hóa của mô hình để phục vụ cho quá trình chuẩn hóa dữ liệu đầu vào phục vụ prediction

Bước Prediction gồm có 5 bước:

  • (1) Load model: Dự án sẽ tải mô hình được lưu ở bước Training, bao gồm tải luôn chuẩn hóa
  • (2) Format Input Data: Dữ liệu muốn được prediction thì cần được format cho đúng đầu vào, vì mỗi loại thư viện có các lớp đặc tả khác nhau, nó cần được đưa về đúng format. DL4J cũng vậy, nó dùng INDArray làm dữ liệu đầu vào, thì rõ ràng các dữ liệu thô của chúng ta muốn prediction phải đưa về INDArray
  • (3) Input Data Normalization: Dữ liệu phải được chuẩn hóa để phù hợp khi đưa vào mô hình, nó phái giống với chuẩn hóa của mô hình lúc train.
  • (4) Feed Input Data: Bắt đầu đưa dữ liệu được chuẩn vào vào mô hình để prediction
  • (5) Prediction Results: Là kết quả prediction sau khi chạy mô hình với kết quả đầu vào ta đã chuẩn hóa.

Như vậy ở trên Tui đã vẽ lại mô hình tổng quan, cũng như liệt kê sơ lược các bước chính trong quá trình xử lý mô hình máy học.

Đối với Deeplearning4j để sử dụng được ta cần chuẩn bị:

  • Bước 1: Cài đặt JDK 19
  • Bước 2: cài công cụ lập trình: IntelliJ IDEA Community Edition (hoặc Eclipse), Tui đề nghị dùng IntelliJ IDEA Community Edition vì nó tối ưu hơn. Và các bài viết tiếp theo Tui cũng dùng IntelliJ IDEA Community Edition
  • Bước 3: Trong IDEA Community tạo dự án dạng Maven, và cấu hình file pom.xml như dưới đây:
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"
                                               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                                               xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.deeplearning4j</groupId>
    <artifactId>dl4j-examples</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <name>Introduction to DL4J</name>
    <description>A set of examples introducing the DL4J framework</description>

    <properties>
        <dl4j-master.version>1.0.0-M2.1</dl4j-master.version>
        <!-- Change the nd4j.backend property to nd4j-cuda-X-platform to use CUDA GPUs -->
        <!-- <nd4j.backend>nd4j-cuda-10.2-platform</nd4j.backend> -->
        <ND4J-Version>1.0.0-M1.1</ND4J-Version>
        <nd4j.backend>nd4j-native-platform</nd4j.backend>
        <java.version>19</java.version>
        <maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
        <maven.minimum.version>3.3.1</maven.minimum.version>
        <exec-maven-plugin.version>1.4.0</exec-maven-plugin.version>
        <maven-shade-plugin.version>2.4.3</maven-shade-plugin.version>
        <jcommon.version>1.0.23</jcommon.version>
        <jfreechart.version>1.0.13</jfreechart.version>
        <logback.version>1.1.7</logback.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <junit.version>5.8.0-M1</junit.version>
        <javacv.version>1.5.5</javacv.version>
        <spark.version>2.4.8</spark.version>
    </properties>


    <repositories>
        <repository>
            <id>sonatype-nexus-snapshots</id>
            <name>Sonatype Nexus Snapshots</name>
            <url>https://oss.sonatype.org/content/repositories/snapshots</url>
            <releases>
                <enabled>false</enabled>
            </releases>
            <snapshots>
                <enabled>true</enabled>
                <updatePolicy>daily</updatePolicy>  <!-- Optional, update daily -->
            </snapshots>
        </repository>



    </repositories>

    <dependencies>
        <dependency>
            <groupId>org.nd4j</groupId>
            <artifactId>${nd4j.backend}</artifactId>
            <version>${dl4j-master.version}</version>
        </dependency>


        <dependency>
            <groupId>org.datavec</groupId>
            <artifactId>datavec-api</artifactId>
            <version>${dl4j-master.version}</version>
        </dependency>
        <dependency>
            <groupId>org.datavec</groupId>
            <artifactId>datavec-data-image</artifactId>
            <version>${dl4j-master.version}</version>
        </dependency>
        <dependency>
            <groupId>org.datavec</groupId>
            <artifactId>datavec-local</artifactId>
            <version>${dl4j-master.version}</version>
        </dependency>
        <dependency>
            <groupId>org.deeplearning4j</groupId>
            <artifactId>deeplearning4j-datasets</artifactId>
            <version>${dl4j-master.version}</version>
        </dependency>
        <dependency>
            <groupId>org.deeplearning4j</groupId>
            <artifactId>deeplearning4j-core</artifactId>
            <version>${dl4j-master.version}</version>
        </dependency>

        <dependency>
            <groupId>org.deeplearning4j</groupId>
            <artifactId>resources</artifactId>
            <version>${dl4j-master.version}</version>
        </dependency>

        <dependency>
            <groupId>org.deeplearning4j</groupId>
            <artifactId>deeplearning4j-ui</artifactId>
            <version>${dl4j-master.version}</version>
        </dependency>
        <dependency>
            <groupId>org.deeplearning4j</groupId>
            <artifactId>deeplearning4j-zoo</artifactId>
            <version>${dl4j-master.version}</version>
        </dependency>
        <!-- ParallelWrapper & ParallelInference live here -->
        <dependency>
            <groupId>org.deeplearning4j</groupId>
            <artifactId>deeplearning4j-parallel-wrapper</artifactId>
            <version>${dl4j-master.version}</version>
        </dependency>
        <!-- Used in the feedforward/classification/MLP* and feedforward/regression/RegressionMathFunctions example -->
        <dependency>
            <groupId>jfree</groupId>
            <artifactId>jfreechart</artifactId>
            <version>${jfreechart.version}</version>
        </dependency>
        <dependency>
            <groupId>org.jfree</groupId>
            <artifactId>jcommon</artifactId>
            <version>${jcommon.version}</version>
        </dependency>
        <!-- Used for downloading data in some of the examples -->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.3.5</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>${logback.version}</version>
        </dependency>

        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>javacv-platform</artifactId>
            <version>${javacv.version}</version>
        </dependency>

        <!-- Test dependency. Ignore for your own application. -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>



    </dependencies>
    <!-- Maven Enforcer: Ensures user has an up to date version of Maven before building -->
    <build>
        <plugins>

            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.0.0-M5</version>
                <inherited>true</inherited>
                <dependencies>
                    <dependency>
                        <groupId>org.apache.maven.surefire</groupId>
                        <artifactId>surefire-junit-platform</artifactId>
                        <version>3.0.0-M5</version>
                    </dependency>
                </dependencies>
            </plugin>

            <plugin>
                <artifactId>maven-enforcer-plugin</artifactId>
                <version>1.0.1</version>
                <executions>
                    <execution>
                        <id>enforce-default</id>
                        <goals>
                            <goal>enforce</goal>
                        </goals>
                        <configuration>
                            <rules>
                                <requireMavenVersion>
                                    <version>[${maven.minimum.version},)</version>
                                    <message>********** Minimum Maven Version is ${maven.minimum.version}. Please upgrade Maven before continuing (run "mvn --version" to check). **********</message>
                                </requireMavenVersion>
                            </rules>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>${maven-compiler-plugin.version}</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>com.lewisd</groupId>
                <artifactId>lint-maven-plugin</artifactId>
                <version>0.0.11</version>
                <configuration>
                    <failOnViolation>true</failOnViolation>
                    <onlyRunRules>
                        <rule>DuplicateDep</rule>
                        <rule>RedundantPluginVersion</rule>
                        <!-- Rules incompatible with Java 9
                        <rule>VersionProp</rule>
                        <rule>DotVersionProperty</rule> -->
                    </onlyRunRules>
                </configuration>
                <executions>
                    <execution>
                        <id>pom-lint</id>
                        <phase>validate</phase>
                        <goals>
                            <goal>check</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>${exec-maven-plugin.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>exec</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <executable>java</executable>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>${maven-shade-plugin.version}</version>
                <configuration>
                    <shadedArtifactAttached>true</shadedArtifactAttached>
                    <shadedClassifierName>${shadedClassifier}</shadedClassifierName>
                    <createDependencyReducedPom>true</createDependencyReducedPom>
                    <filters>
                        <filter>
                            <artifact>*:*</artifact>
                            <excludes>
                                <exclude>org/datanucleus/**</exclude>
                                <exclude>META-INF/*.SF</exclude>
                                <exclude>META-INF/*.DSA</exclude>
                                <exclude>META-INF/*.RSA</exclude>
                            </excludes>
                        </filter>
                    </filters>
                </configuration>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <transformers>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                                    <resource>reference.conf</resource>
                                </transformer>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.eclipse.m2e</groupId>
                    <artifactId>lifecycle-mapping</artifactId>
                    <version>1.0.0</version>
                    <configuration>
                        <lifecycleMappingMetadata>
                            <pluginExecutions>
                                <pluginExecution>
                                    <pluginExecutionFilter>
                                        <groupId>com.lewisd</groupId>
                                        <artifactId>lint-maven-plugin</artifactId>
                                        <versionRange>[0.0.11,)</versionRange>
                                        <goals>
                                            <goal>check</goal>
                                        </goals>
                                    </pluginExecutionFilter>
                                    <action>
                                        <ignore/>
                                    </action>
                                </pluginExecution>
                            </pluginExecutions>
                        </lifecycleMappingMetadata>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>

Như vậy Tui đã giới thiệu sơ lược về DeepLearning4j, các bước xử lý cho bài toán liên quan tới máy học. Cũng như các công cụ phần mềm cần phải cài đặt trước, các bạn nhớ chuẩn bị đủ.

Các bài học sau Tui sẽ minh họa chi tiết cách dùng DeepLearning4j vào bài toán phân loại, sử dụng đúng quy trình ở trên mà Tui đã thiết kế

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

Đọc và Suy Ngẫm

Những gì mình không làm được không có nghĩa là người khác không làm được, những gì mình làm được thì người khác vẫn có thể làm tốt hơn. Hãy luôn khiêm tốn, luôn học hỏi, luôn không ngừng rèn luyện kiến thức và đạo đức để bản thân ngày càng hoàn thiện hơn, sống có ích hơn.

TDT

Những tia hi vọng bé nhỏ của một ai đó có thể bị cuốn trôi nếu chúng ta không biết lắng nghe và thấu cảm.

TDT


Xét cho cùng, ở đời ai cũng khổ. Người khổ cách này, người cách khác. Bí quyết là biết tìm cái vui trong cái khổ. Vì chỉ sống thôi cũng đã quý lắm rồi. Người ta không bao giờ nên phí phạm cái sống, coi thường sự sống.

Nhà văn Thạch Lam

Hãy đi đến tận cùng của tuyệt vọng, để thấy tuyệt vọng cũng đẹp như một bông hoa.

Nhạc sĩ Trịnh Công Sơn

Làm và đọc nhiều thứ làm cho tán lực. Mắt nhìn và tai nghe, mỗi thứ có diệu dụng. Khổng Giáo chữa bệnh ngoài da. Phật giáo chữa bệnh trong cốt tủy. Chữ hiếu trong Đạo Phật khác Khổng. Tu cũng là hình thức báo hiếu cha mẹ, cả cha mẹ các đời trước.

Họa sĩ Nguyễn Gia Trí

Kẻ nào không quan tâm tới người khác, chẳng những sẽ gặp nhiều sự khó khăn nhất trong đời, mà còn là người có hại nhất cho xã hội. Hết thảy những kẻ thất bại đều thuộc hạng người đó.

Alfred Adler


Bố linh thiêng lắm, nhà sập mà không bị cháy khi đang nấu bánh chưng

🤣”Bố linh thiêng lắm, nhà sập mà không bị cháy khi đang nấu bánh chưng”
👉Tui ghét từ “linh thiêng” này vô cùng.

😌Mỗi lần về thăm Mẹ, Mẹ Tui luôn review lại các tình tiết mang tính bước ngoặc trong bộ phim cuộc đời khốn khổ của mình, lại nhắc tới câu chuyện linh thiêng này. Tui hay hỏi cắc cớ lại “linh thiêng thì nhà đã không sập, linh thiêng thì không để nhà mình khổ thế này”. Mẹ tui đuối lý không nói gì thêm, chỉ nhìn Tui với đôi mắt trìu mến hình viên đạn và không quên kèm theo lời khen truyền thống “vớ cái thằng ngục tội”. Tui thích từ “ngục tội” này lắm, nếu ai đó đã từng được Tui gọi là “ngục tội” thì thực sự Tui rất quý mến họ.
😌Vì Tui là Con trai trưởng trong nhà, nên hầu như mọi thủ tục về cúng bái thì Ông Nhung thầy cúng và người lớn đều đè 1 thằng 7 tuổi ra giống như một vật tế thần. Nên Tui ngán tới tận cổ, giờ 40 tuổi rồi Tui vẫn còn sợ, và Tui luôn muốn tối giản mọi thứ tới mức Con Dâu của Mẹ Tui cũng không hài lòng với Tui.
😌Tuần 49 ngày của Bố Tui, Mẹ Tui đã ghánh 1 quang ghánh ra Lộc An bằng cách đi bộ, cả đi cả về là trên dưới 20 KM. Giữa trời nắng chang chang như thiêu đốt cả đôi dép Lào thần thánh để chỉ có 1 số đồ cúng mà nếu thiếu thì sợ anh em bên chồng, hàng xóm họ chê trách, họ nói ra nói vào, tới giờ cũng vậy thôi. Kiểu đời nó vậy, toàn sống sợ hãi dưới cái nhìn phiến diện của người khác, chưa bao giờ là sống cho mình. Tự nhiên cả cuộc đời tại sao phải cố gắng sống để làm hài lòng theo người khác mà không phải sống để mình hài lòng trong khuôn khổ? hoàn cảnh mình có sao thì mình dùng vậy là được rồi. Lúc Bố còn sống, Tui cũng còn nhớ câu nói của Bố “Xanh xanh chín chín, chín bỏ làm mười”.
😌Bất chợt giữa cái nắng gay gắt đó, trời đổ cơn cuồng phong, rồi mưa bão tới càn quét thổi bay 13 căn nhà khu tuyệt tình cốc trong đó có nhà Tui.
😌Lúc nhà bắt đầu sập, trong nhà có Tui, Anh Tùng Ngọng, Anh Củ Phày và thằng Ngọc Ngõ với nồi bánh chưng đang nấu để cúng. Thằng Ngõ nó mới 3 tuổi, nó nhìn lên nóc nhà nó chỉ chỉ và nói “nhà sập nhà sập…” thế là mấy Anh Em chúng Tôi chạy vù ra ngoài lúc đó trời mưa to sấm sét, nhưng Tui vẫn chưa cảm nhận được “mà.y sắp chế.t rồi con ạ”, Tui vẫn chạy xuống vườn lượm những quả Bơ rụng, nhà Tui hồi đó có 2 cây bơ cổ thụ, 1 quả nặng cũng trên dưới 1 ký, bơ ăn rất ngon. Bơ là thức ăn mà mỗi mùa nó chín thì 100% nó là thức ăn cứu đói của nhà Tui, dằm bơ với nước mắm và ăn cơm, không cần cá thịt gì cả. Lâu lâu Tui vẫn ăn lại dưới Sài Gòn để hồi tưởng lại cảm giác của quá khứ.
😌Hôm đó Tui và mọi người chờ mãi mà không thấy Mẹ đi chợ về, mãi tới gần chiều mới biết Mẹ cũng gặp mưa to và bị đứt quang ghánh, cùng với kiệt sức nên nghỉ ở nhà bà Kế, mãi sau hồi phục mới bưng đồ về được. Về chứng kiến căn nhà bị sập, Mẹ Tui đã khóc ngất, rồi gào thét các câu thảm thiết quen thuộc “Vớ con ơi, vớ trời cao đất dày ơi ….”.. nhưng không ai trả lời Mẹ Tui cả cho dù chỉ 1 tiếng “Ơi, gọi ta có gì đó”…và cứ thế Mẹ Tui khóc mãi như chưa từng được khóc, khóc như lúc đòi nhảy xuống huyệt mộ của Bố vậy may mà lúc đó có Thím Định giữ lại được. Hàng xóm và anh em dòng họ nhìn Mẹ Tui và 4 đứa con của Mẹ Tui, họ tội nghiệp quá họ đã tự làm 1 gian nhà tạm đủ để mấy Mẹ con vào ngủ.
😌Lúc nhà sập thì phải cúng ngoài trời, Tui là con Trưởng nên phải “Chống Gậy” đứng gục bên Bàn Thờ giữa cái nắng gay gắt. Tới gần tối Tui chịu không nổi, Tui quăng luôn cái Gậy đó và nói “không chống nữa”, rồi ngồi bệp xuống sân 2 chân đạp đạp tùm lum thể hiện sự phản động với phong tục. Tui vẫn còn nhớ như in cảnh đó, nếu là họa sĩ Tui có thể vẽ lại được khung cảnh bàn thờ ngoài trời bên cạnh căn nhà sập.
Cuối cùng thì tuần 49 ngày cũng “xong”!
Sau này Mẹ Tui và mọi người ngồi kể lại đều nói “Bố linh thiêng lắm, nhà sập mà không bị cháy khi đang nấu bánh chưng”.
Tui chưa bao giờ tin vào sự linh thiêng này cả.

to be continued


🤬Bất cứ kẻ nào làm đấng sinh thành buồn, đã có gia đình mà còn không lo được cho đấng sinh thành còn quay qua cào cấu, làm cho họ không có được 1 ngày vui vẻ thì đều là kẻ bất hiếu táng tận lương tâm. Bất hiếu sẽ bị qủa báo trừng trị thích đáng.

Thơ – Hãy bỏ ngoài tai Mẹ nhé

Tất cả vì sự nghiệp Giáo Dục!
Data Science Lab building! Room 407, KMOU


Lại 1 ngày 2h sáng nữa trôi qua!
Bước chân kẻ độc hành như tê dại
Liêu xiêu về phòng dưới cơn cuồng phong của biển cả
Ồ kìa sao ta nghe tiếng trống bụng đói cồn cào
Họ hàng ơi, hãy hỏi thăm ta thay vì trách móc
Ta đau lòng khi nghe tiếng Mẹ khóc buồn phiền vì người thân
Cuộc đời Mẹ đã khổ biết bao
Con vẫn nhớ hàng đêm Mẹ khóc bên những nong tằm
Bán mặt cho đất bán lưng cho trời
Mẹ còn nhớ không đôi bàn tay ấy?
Đã vuốt 2 má con đến máu chảy
Bởi sự cùng cực của đời
Nay gần đất xa trời
Cớ sao còn nghe những lời vô cảm
Lòng mẹ đau, nước mắt Mẹ rơi
Trái tim con đớn đau chừng nào
Hãy bỏ ngoài tai Mẹ nhé!
Những kẻ hay trách móc
Là những kẻ đầy nhóc lỗi lầm
Nhưng họ không nhận ra
Bởi sự về hùa, nhỏ mọn, hẹp hòi, ích kỷ, nông cạn thâm căn cố đế


2h sáng ngày 13.04.2022

10 năm chờ đợi có lâu không?

🥰10 năm chờ đợi có lâu không?🥰

🤣Mỗi lần nhắc tới câu chuyện “chờ 10 năm” Tui lại uất ức và ứa nước mắt, bất cứ khi nào Tui cũng có thể nhớ tới thảm cảnh “chờ 10 năm”, nó trở thành truyền thuyết mà Tui luôn nhắc lại với Mẹ cũng như kể cho Vợ nghe. Nó giống như một câu truyện trào phúng mang dáng dấp dòng văn hiện thực phản ánh cuộc sống nghèo khổ mà nhà văn Nam Cao đã viết như “Một bữa no”, “Đời thừa” hay tác phẩm “Chí Phèo”. Tui thích đọc các tác phẩm của nhà văn Nam Cao lắm, vì nó rất đồng cảm với cuộc đời nghèo khó mà Tui và gia đình đang trải qua thời bấy giờ, Các ý tứ của nhà Văn Nam cao để trong các tác phẩm nó làm lay động Tui nhiều lắm, Tui đọc và khóc nhiều vì nó quá đúng với hoàn cảnh của mình.

😏Trong cái nắng chang chang của một buổi trưa hè cách đây hơn 30 năm, mọi người nhí nhố đứng ở đường “Be”, là đường dành cho xe “Be” đi, Tui cũng không biết có đúng không hoặc là Tui phát âm sai, xe be hay là xe ben nữa. Từ tuyệt tình cốc nhà Tui leo bộ lên đó cũng khoảng trên dưới 1km để đón xe đi chợ ở Lộc An (8km), mọi người ngày xưa cũng thường đi bộ và ghánh hàng ra Lộc An. Hồi đó “Ông Lộc Khàn” với giọng nói đặc khẹt có chiếc xe được các dây thừng bó lại như bó 1 con heo vậy. Chiếc xe ấy thật là vĩ đại, nó chở mấy chục người ngồi trong, rồi ngồi trên mui, chắc mọi người sẽ không tưởng tượng được. Con đường đau khổ, mưa ngập, tạo thành các rãnh voi xe quật qua quật lại nếu ai đó có Bầu chắc đẻ tại chỗ, thật vi diệu cho con đường ấy, giờ báo đài đăng nhiều cảnh này lắm, nhưng Tui chưa thấy nó xi nhê gì với ngày trước.

😏Tui và Mẹ cũng ra được tới Lộc An, lần đầu tiên Tui thấy một “con quái vật” nó to đùng đang nằm giữa đường ở ngay ngã 3 Lộc An, Tui nhảy cỡng lên như mọi ngày thấy máy bay bay qua nhà ở trên trời “Mẹ ơi, tàu kìa, tàu kìa…sao Tàu nó to vậy hả Mẹ?”, Mẹ tui cười khanh khách “Vớ cái thằng ngục tội, đó là xe đò”. Tui chả quan tâm, cứ gọi nó là Tàu, vì nó bự, mãi sau này Tui mới biết nó là chiếc xe 45 chỗ. Nói sai cũng đâu có sao, như từ “Lẩu” mãi tới học xong cao đẳng, đại học Tui mới biết từ “Lẩu”, hay là từ “siêu thị”. Chúng rất mới mẻ với 1 thằng người rừng như Tui, tuy nhiên không biết không sao cả, nó không ảnh hưởng gì tới hòa bình thế giới.

😏Mẹ và Tui rất thèm ăn sầu riêng, nhưng hồi đó sầu riêng nó mắc vô cùng, 1 quả sầu riêng có thể đổi ra ăn cơm tới cả tuần lận. Mà Mẹ Tui thì đâu có nhiều tiền. Hồi đó Mẹ Tui cất tiền kỹ lắm, Tui còn nhớ như in, có vài đồng à, mà Mẹ Tui bỏ vào 1 cái bị quấn lại mấy lớp rồi nhét nó vào bên trong quần đang mặc rồi cuộn lại 1 lần nữa. Tui tưởng tượng nếu có đứa nào cướp giật thì chắc nó chỉ rinh Mẹ Tui đi để nuôi tốn cơm thôi chứ không cỡ nào mà lấy được cái bịch tiền thần thánh đó.

🤩Tới quầy Sầu Riêng, thơm ngon lắm (nhiều người không thích mùi của nó), Tui và Mẹ thèm mà không dám mua ăn. Hai Mẹ con nhà Tui ngồi xổm đó 2 tay chống cằm nhìn người ta ăn (trên Tui gọi là nhìn mồm) một cách thèm thuồng, Tui thì năn nỉ mãi “Mẹ ơi, Con thèm quá Mẹ mua cho Con ăn đi”, Tui cứ lải nhải mãi Mẹ Tui cũng bực mình. Rồi bất ngờ Mẹ Tui nảy ra “một sáng kiến vĩ đại” mà tới bây giờ Tui vẫn còn uất ức. Đó là “Đợi họ ăn xong mình xin hột đem về trồng, đợi nó có trái rồi ăn, nhanh có trái lắm Con ạ”. Thật tuyệt vời, 1 ý kiến tuyệt vời đã lừa được một thằng ngáo rừng như Tui thời đó.

😱Các bạn biết không, ngày xưa Sầu Riêng trồng bằng hột, 10 năm mới có trái lận. Nhưng Tui không có biết, tui chỉ biết về cứ cách 20 mét lại trồng một hạt. Tui đợi mãi, đợi mãi, đợi mãi, đợi mãi, đợi tới lúc lớp 10 Tui phải đi ở trọ ở Bảo Lâm nhưng nó cũng không có trái. Mãi tới lớp 12 gì đó nó có trái nhưng người ăn đầu tiên không phải là Tui, mà nó là thằng Ngọc Ngõ em Tui. Vậy đấy, còn tức tới bây giờ nè. tức tràn bảng họng. Cuộc chờ đợi 10 năm thật là ngắn ngủi với 1 cuộc đời nhưng nó lại dài đăng đẵng với một mong ước nhỏ nhoi “được ăn sầu riêng”.

😱Cuộc sống khốn khó tới mức đôi khi Con người lại “ruồng bỏ” chính bản thân mình, không dám chiều chuộng bản thân mình 1 điều gì cả mặc dù mình khao khát được nó, mặc dù nó chỉ là 1 quả sầu riêng, nó chỉ là 1 bữa ăn, chỉ là 1 lần ăn cho no, 1 bữa ăn cho đã mồm, ăn cho thỏa thích dù chỉ 1 lần…. Nhưng cái khốn khó dường như nó còn kìm hãm cả sự phát triển trí tuệ của con người, nó bít hầu như các đường đi của con người.

👩‍🏫Bố Mẹ là xuất phát điểm của Con cái, nếu sinh ra trong một gia đình giàu có, Bố Mẹ có tầm nhìn, có hiểu biết thì hiển nhiên Con cái sẽ được hưởng lợi và phát triển tốt hơn. Với sự hiểu biết và khả năng tài chính của Bố Mẹ thì họ sẽ vạch ra các con đường đúng đắn ngay từ đầu, giúp cho đứa trẻ có thể dễ dàng thành công hơn.

😭Còn sinh ra trong một gia đình nghèo khó, hiển nhiên chúng ta phải nỗ lực hết sức, hầu như không có ai được định hướng cả, ta đi, ta đi, ta đi, đi mãi trên con đường đau khổ của cuộc đời đã đem lại, hoàn toàn không có mục tiêu gì cả, ai may mắn thì sẽ thành công. Chắc Tui cũng là 1 trong số may mắn đó, dĩ nhiên sự may mắn chỉ trở thành hiện thực đối với những ai có nghị lực, có lòng quyết tâm, kiên trì, bền gan vượt qua mọi trở ngại. Tất cả mọi người chúng ta đều được các Đấng Sinh Thành cho ta 1 tài sản lớn, đó chính là tấm thân bằng xương bằng thịt này, nên cho dù bạn nghèo tới mấy, bạn không có gì cả nhưng bạn không bao giờ được nói “Bố Mẹ Tui không cho Tui cái gì cả”. Không, ít nhất là họ cho bạn 1 Sự Sống trên cõi đời này, bạn không chọn được cách bạn sinh ra, nhưng bạn chọn được cách bạn sống như thế nào.

🥰Xukhôm linxki đã từng nói “Con người sinh ra không phải để tan biến đi như một hạt cát vô danh. Họ sinh ra để lưu lại dấu ấn trên mặt đất, trong trái tim người khác”.

Chúc các bạn cuối tuần vui vẻ, luôn lạc quan yêu đời, luôn để lại dấu ấn tốt trong lòng mọi người!

————————————————————–

Nếu bạn không đủ sức mạnh hãy Cứ chờ đợi, cứ chờ đợi khi đủ mạnh

Hoang Đảo Ký Sự 26.03.2022

Mẹ ơi đừng để Chị Em Con thất học

😭Một đêm giông bão hồi còn học tiểu học, dưới ánh đèn le lói từ bàn thờ của Bố chiếu xuống, Tui bất chợt thấy nước mắt Mẹ lăn dài trên 2 gò má nám đen vì sự đau khổ của cuộc đời đem lại. Rồi Mẹ tui nói trong đứt quãng “Mẹ hết tiền rồi…, không còn đủ cho tụi con ăn học nữa…”. Những tiếng nấc của Mẹ sau đó cứ không dừng.

🥳Bất giác như có một luồng ánh sáng đẩy thẳng vào não bộ và Tui đã thốt lên “Mẹ ơi đừng để Chị Em Con thất học”, câu nói của 1 đứa trẻ chưa học xong tiểu học.

🥰Khi nghe câu nói này, Mẹ tui đã ôm chầm lấy Tui mà khóc rất lớn, Tui cũng khóc theo, tính Tui mít ướt lắm mặc dù ở ngoài luôn xàm xí như mọi người từng thấy, Mẹ Tui nói sẽ ráng vay mượn và làm nhiều hơn để lo cho tụi Tui. Điều này là quá sức chịu đựng với một người Phụ Nữ, 1 tay nuôi 4 đứa con thơ mồ côi Bố khi còn rất nhỏ.

😁Tui cũng không hiểu được là tại sao Tui có thể nói ra được câu nói đó khi còn học sinh tiểu học. Và Mẹ tui đã khóc hết nước mắt, ra sức cầy cuốc, chạy vạy, vay mượn nhiều nơi chỉ để lo cho chúng tôi ăn học. Tới khi ra trường rồi nhà Tui vẫn còn nợ.

😎Hồi tiểu học Tui học rất dốt, tới lớp 3 Tui cũng chưa ghép được chữ để đọc. Tui vẫn còn nhớ như in cái từ “Xanh” mà Tui cứ đọc là “xờ a nhờ … anh xờ anh Nhan”, cứ thế từ “Xanh” thì đọc thành từ “Nhan”. Mẹ Tui tức quá chỉ nói “Cái đồ Ngu ăn C..” kèm theo 1 cú Trọt (cốc, ký) vào đầu, nước mắt và nước mũi Tui lại chảy rưng rưng. Thà rằng đọc “Nờ a Na … Mãng Cầu” thì vẫn còn chấp nhận được.

🤨Bắt đầu Tui thấy hoang mang, sao lại lỡ mồm nói câu “Mẹ ơi đừng để Chị Em Con thất học”. Tui lo lắng tột độ. Tui biết mình trí não kém cỏi, chỉ thiên về EQ chứ kém IQ. Biết thân biết phận nên Tui đã tăng thời lượng học bài hơn. Ngoài giờ lên trường rồi đi làm với Mẹ ra, tới tối là Tui vùi đầu học tới rạng sáng, có hôm 3h sáng Mẹ tui dậy thấy Tui còn ngồi bên đèn dầu học, lại nói “vớ con ơi đi ngủ đi con”, Tui trả lời “vưng”. Sau đó 4h sáng Mẹ Tui dậy lại vẫn thấy Tui học. Đây chính là cách học Lấy Cần Cù Bù Thông Minh của Tui. Sau 1 thời gian, Tự nhiên đầu óc Tui sáng hẳn ra, chỉ cần đọc qua bài trong sách là có thể hiểu được nguyên lý, cuộc đua xếp thứ hạng TOP 1 trong lớp, trong trường bắt đầu từ đây với Tui. Dĩ nhiên tới bây giờ Tui vẫn tự hào về thành tích học tập của mình, các bạn cùng trang lứa chắc cũng không thể nào quên.

🥳Tui cũng đã tạo được thói quen 2h sáng mới đi ngủ (rất cực đoan, các bạn đừng làm theo). Hồi học ở Sài Gòn, có đợt ngồi nhiều quá Tui bị liệt chân tưởng thành tàn phế do học quá sức cùng với phương thức ngồi sai bét, may mà cứu chữa kịp thời. Giờ thì khỏe như trâu. Cho tới bây giờ 40 tuổi rồi Tui vẫn còn thói quen đó, vừa học, nghiên cứu, làm dự án tới sáng. Vợ Tui cũng trách móc suốt “bỏ vợ bỏ con, sao không ở 1 mình đi, lấy vợ làm gì…. ” … đại loại các câu trách móc xàm xí kiểu vậy (Mỗi lần nghe câu trách móc này là Tui nộ khí xung thiên, muốn phi tới bịt mồm bả bằng 1 nụ hôn nồng cháy😘, nhưng mà Bug nhiều quá cần phải fix trước).

😏Mãi sau này Tui mới hiểu vì sao từ 1 người ngu dốt có thể trở nên thông minh, thực ra nó chính là nguyên lý triết học “Khi đủ về lượng thì chất sẽ thay đổi”. Nếu mình kém cỏi về IQ thì mình cứ cố gắng học đi học lại, học tới học lui, người ta học 1 lần hiểu thì mình học 10 lần, 100 lần có sao đâu. Cứ kiên trì bền bỉ là được.

🤩Việc học một kiến thức mới nó giống như 1 Cánh ruộng khô cằn mà ta bắt đầu đổ nước vào, ta có cố đổ bao nhiêu đi nữa thì nó cũng ngay lập tức hết nước liền, nhưng nếu ta kiên trì bền bỉ đổ nước hàng ngày thì tới một lúc nào đó đồng ruộng khô cằn ấy nó sẽ ẩm ướt, và nó bắt đầu giữ nước cho bạn, lúc đó bạn đổ nước vào thì nó không rút ngay mà nó vẫn còn giữ cho bạn. Nhưng nếu bạn dừng đổ nước thì nước sẽ hết. Đó là lý do vì sao chúng ta phải học liên tục, phải trau dồi kiến thức hàng ngày là vậy. Cho dù ta có giỏi tới mấy ở thời điểm T1 nếu như ỷ y tự cao tự đại không chịu học hành thêm thì tới thời điểm T2 ta sẽ lạc hậu ngay.

🥰p/s: Hình ảnh minh họa là của 1 Em bé Irac mồ côi Bố Mẹ, Em ấy khao khát có được tình thương yêu của Bố Mẹ, nên Em ấy đã vẽ người Mẹ và chui vào trong bụng của Mẹ nằm khoanh tròn lại. Ai còn Bố Mẹ xin đừng làm đấng sinh thành buồn, ngoài kia nhiều cuộc đời còn thiếu thốn, khát khao sự yêu thương của Bố Mẹ lắm.

Chúc các bạn Sinh Viên có thêm động lực học!

————————————————-

Hoang đảo ký sự 24.03.2022