Bài 5: QMainWindow và kỹ thuật kế thừa để tùy chỉnh Signals/Slots trong PyQt6 và Qt Designer

QMainWindow là một lớp cửa sổ cho ta bố trí và thiết kế các control trên giao diện của cửa sổ này để tạo ra các màn hình tương tác đáp ứng yêu cầu của người sử dụng. QMainWindow nó tương tự như Form trong C# Winform, MainWindow trong C# WPF, JFrame trong Java…

Ta có thể thiết kế giao diện bằng Qt Designer (kéo thả – Design time) hoặc bằng coding PyQt6 – Python (Run time). Đồng thời ta cũng có thể viết các kỹ thuật kế thừa từ lớp giao diện được Generate để tách lập hoàn toàn giao diện với xử lý mã lệnh cho các tác vụ trong tương lai, và khi giao diện thay đổi mà Generate lại thì các mã lệnh xử lý mà ta mới bổ sung vẫn không bị ảnh hưởng.

Vì vậy trong bài học này, Tui sẽ hướng dẫn các kiến thức và kỹ thuật liên quan tới QMainWindow như sau:

  1. Cách thiết kế cửa sổ QMainWindow lúc Design time (sử dụng Qt Designer hoặc Qt Creator)
  2. Các thuộc tính và phương thức/slots thường sử dụng trong QMainWindow
  3. Cách tạo lớp kế thừa QMainWindow và cách tùy chỉnh các Signals/Slots

Ta tạo một dự án trong Pycharm đặt tên là “LearnQMainWindow“, Bạn cần thành thạo bài học số 4 trước khi học bài này, cần học tuần tự từ bài đầu tiên cho tới bài cuối cùng. Do đó từ bài sau trở đi Tui sẽ không nhắc lại các bước tạo dự án cũng như mở Qt Designer, đồng thời thao tác tự động Generate Python code cho các Giao diện cũng không được nhắc lại.

Trong dự án “LearnQMainWindow” tạo một cửa sổ QMainWindow bằng công cụ Qt Designer và đặt tên là “MyQMainWindow.ui” (trong màn hình New Form chọn Main Window rồi nhấn nút Create). Màn hình này có giao diện như dưới đây:

Ta thiết kế màn hình giao diện như trên bằng cách kéo thả các control:

Các chức năng bao gồm (chi tiết các loại Control sẽ được học ở những bài sau):

Loại ControlTên ControlDữ liệu/nhãn hiển thịChức năng
QLineEditlineEditNameTran Duy ThanhDùng để nhập dữ liệu, đồng thời sẽ chuyển dữ liệu này qua màn hình khác khi nhấn nút “Send Name“, và sẽ tự động cập nhật lại dữ liệu khi màn hình mới thay đổi dữ liệu.
QPushButtonpushButtonSendNameSend NameNút lệnh này sẽ truyền dữ liệu trong ô lineEditName qua màn hình mới. (sẽ được học ở bài số 6)
QPushButtonpushButtonVisitBlogVisit BlogNút lệnh này sẽ mở blog: https://tranduythanh.com/
QPushButtonpushButtonExitExitNút lệnh này sẽ thoát chương trình, tuy nhiên nó sẽ hiển thị Dialog xác thực có muốn thoát hay không.

Để đặt tên control và các nhãn hiển thị thì ta nhấn chuột vào từng control đó trước, sau đó thiết lập các thông số trong mục Property Editor (xem lại bài số 3 được trình bày rất kỹ).

Với nhóm QPushButton ta chọn objectName để đổi tên control, chọn thuộc tính Text trong QAbstractButton để hiển thị nhãn dữ liệu:

Đối với QLineEdit ta chọn objectName để đổi tên control, chọn thuộc tính Text trong QLineEdit để hiển thị nhãn dữ liệu:

Sau khi thiết kế xong, để kiểm tra giao diện trước khi đi qua lập trình xem có phù hợp hay không thì ta vào menu Form/ chọn Preview… hoặc nhấn tổ hợp phím Ctrl+R.

Màn hình Preview… hiển thị ra như dưới đây để ta test giao diện:

Dưới đây là các thuộc tính và phương thức(slots) quan trọng thường sử dụng của QMainWindow:

Thuộc tính/phương thức(Slots)Chức năng
objectNameThiết lập tên của cửa sổ, dùng cho triệu gọi khi lập trình tương tác cửa sổ
windowTitleThiết lập tiêu đề của cửa sổ
windowIconThiết lập Icon cho cửa sổ
fontThiết lập font chữ cho các control trong cửa số, font chữ bao gồm tên font chữ, kích thước font chữ, kiểu font chữ như In đậm, In nghiêng, gạch chân…
cursorThiết lập biểu tượng con trỏ chuột hiển thị khi di chuyển trong cửa sổ
toolTipThiết lập gợi ý chức năng cho cửa sổ khi di chuyển chuột vào
geometryThiết lập vị trí xuất hiện của cửa sổ đồng thời xác định chiều rộng và chiều dài cho cửa sổ
minimumSizeKích thước tối thiểu của cửa sổ
maximumSizeKích thước tối đa của cửa sổ
styleSheetThuộc tính dùng cho thiết lập các định dạng nâng cao của cửa sổ dạng HTML. Ta có thể thêm các tài nguyên như hình ảnh, phối màu, font chữ… cho đối tượng
isVisible()Phương thức kiểm tra xem cửa sổ có đang hiển thị hay không
hide()Phương thức dùng để ẩn cửa sổ
show()Phương thức dùng để hiển thị cửa sổ
close()Phương thức dùng để đóng cửa sổ

QMainWindow còn nhiều các thuộc tính và slots khác nữa, trong quá trình học tập, Tui sẽ trình bày thêm khi gặp. Hoặc trong quá trình thực hiện dự án, gặp trường hợp nào thì bạn search theo trường hợp đó, bạn tìm hiểu thêm ở đây.

Bây giờ ta sẽ lưu “MyQMainWindow.ui” lại lần cuối và tiến hành Generate Python code cho giao diện này trong dự án “LearnQMainWindow”:

File “MyQMainWindow.py” sẽ được tạo ra như dưới đây, có lớp tên là “Ui_MainWindow” được tự động tạo ra bởi công cụ Generate:

# Form implementation generated from reading ui file 'MyQMainWindow.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(565, 245)
        font = QtGui.QFont()
        font.setPointSize(11)
        MainWindow.setFont(font)
        self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.pushButtonSendName = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonSendName.setGeometry(QtCore.QRect(40, 130, 151, 41))
        self.pushButtonSendName.setObjectName("pushButtonSendName")
        self.label = QtWidgets.QLabel(parent=self.centralwidget)
        self.label.setGeometry(QtCore.QRect(20, 10, 151, 41))
        font = QtGui.QFont()
        font.setPointSize(15)
        self.label.setFont(font)
        self.label.setObjectName("label")
        self.lineEditName = QtWidgets.QLineEdit(parent=self.centralwidget)
        self.lineEditName.setGeometry(QtCore.QRect(40, 70, 491, 31))
        self.lineEditName.setObjectName("lineEditName")
        self.pushButtonVisitBlog = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonVisitBlog.setGeometry(QtCore.QRect(210, 130, 151, 41))
        self.pushButtonVisitBlog.setObjectName("pushButtonVisitBlog")
        self.pushButtonExit = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonExit.setGeometry(QtCore.QRect(380, 130, 151, 41))
        self.pushButtonExit.setObjectName("pushButtonExit")
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 565, 31))
        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 - QMainWindow"))
        self.pushButtonSendName.setText(_translate("MainWindow", "Send Name"))
        self.label.setText(_translate("MainWindow", "Your Name:"))
        self.lineEditName.setText(_translate("MainWindow", "Tran Duy Thanh"))
        self.pushButtonVisitBlog.setText(_translate("MainWindow", "Visit Blog"))
        self.pushButtonExit.setText(_translate("MainWindow", "Exit"))

Bạn cần nhớ là Giao diện thì ta có thể phải thay đổi liên tục trong quá trình phát triển (hiển nhiên), do đó tránh việc viết mã lệnh trong file tự động Generate này, vì mỗi lần đổi giao diện mà Generate lại thì toàn bộ mã lệnh mà ta bổ sung trước đó sẽ ra đi không kèn không trống.

Vậy làm sao để vẫn thay đổi giao diện thoải mái nhưng các mã lệnh bổ sung vẫn trơ gan cùng tuế nguyệt?

Câu trả lời hoàn hảo đó là “Kế thừa

Trong dự án “LearnQMainWindow”, ta tạo thêm 1 file tên là “MyQMainWindowExt.py” + ta tạo lớp cùng tên và kế thừa từ lớp “Ui_MainWindow” (là lớp mà tự động được generate ra trong file “MyQMainWindow.py”. Mọi mã lệnh bổ sung thêm ta viết trong lớp mới này (tức là từ rày về sau ta không viết thêm bất kỳ một mã lệnh nào trong Ui_MainWindow)

Mã lệnh của MyQMainWindowExt như dưới đây:

Trong lớp “MyQMainWindowExt”, Tui viết nó kế thừa từ Ui_MainWindow (xem dòng số 5)

và lớp này Tui định nghĩa lại hàm setupUi, hàm này đơn giản chỉ là gọi lại hàm setupUi ở lớp cha của nó (lớp Ui_MainWindow) và Tui tạo thêm 1 biến thuộc tính cho lớp này ở dòng số 10, mục đích là tái sử dụng mọi nơi trong lớp này

Tui định nghĩa thêm một hàm processSignalAndSlot(). Hàm này có nhiệm vụ triệu gọi tất cả các Signals và Slots nếu có trong quá trình xử lý tác vụ.

Ví dụ trong trường hợp này Tui chỉ mới làm signals là nhấn vào nút Exit thì thoát chương trình, nhưng Tui có hiệu chỉ là khi nhấn vào nút Exit thì sẽ gọi Slots processExit do Tui tạo mới này (thực ra nó là 1 function thôi, nhưng vì nó được triệu gọi trong Signals nên được gọi là Slots). Function này sẽ sử dụng QMesssageBox ở dòng số 16 để xác thực người sử dụng có chắc chắn muốn thoát phần mềm hay không.

Dưới đây là mã lệnh đầy đủ cho lớp “MyQMainWindowExt“:

from PyQt6.QtWidgets import QMessageBox

from MyQMainWindow import Ui_MainWindow

class MyQMainWindowExt(Ui_MainWindow):
    #override setupUi
    #just define attribute MainWindow for reuse in later
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow
    #define methods for assigning Signals and Slots
    def processSignalAndSlot(self):
        self.pushButtonExit.clicked.connect(self.processExit)
    #define slot exit window
    def processExit(self):
        dlg = QMessageBox(self.MainWindow)
        dlg.setWindowTitle("Exit Confirmation")
        dlg.setText("Are you sure you want to Exit?")
        dlg.setStandardButtons(
            QMessageBox.StandardButton.Yes
            | QMessageBox.StandardButton.No
        )
        dlg.setIcon(QMessageBox.Icon.Question)
        button = dlg.exec()
        # check the user confirmation
        button = QMessageBox.StandardButton(button)
        if button == QMessageBox.StandardButton.Yes:
            self.MainWindow.close()
        else:
            pass#do nothing

Tiếp theo, Chúng ta tạo 1 file “MyApp.py” để triệu gọi “MyQMainWindowExt”:

Dưới đây là coding đầy đủ cho MyApp.py:

from PyQt6.QtWidgets import QApplication, QMainWindow

from MyQMainWindowExt import MyQMainWindowExt
#Create QApplication instance
app=QApplication([])
#Create QMainWindow instance
qMainWindow=QMainWindow()
#Create MyQMainWindowExt instance
myWindow=MyQMainWindowExt()
#call setupUi method for MyQMainWindowExt
myWindow.setupUi(qMainWindow)
#call methods for Signal and slots processing
myWindow.processSignalAndSlot()
#call show method to show Window
qMainWindow.show()
#start Event loop
app.exec()

Chạy “MyApp.py” ta có kết quả:

Bây giờ ta nhấn nút “Exit”, phần mềm sẽ hiển thị QMessageBox để xác thực:

Nếu nhấn Yes, chương trình sẽ gọi slots self.MainWindow.close() để thoát phần mềm. Nếu nhấn No thì không làm gì

Tiếp theo ta bổ sung Slots cho nút lệnh “Visit Blog“, nhấn vào nó sẽ mở blog https://tranduythanh.com/

Trong “MyQMainWindowExt.py” ta bổ sung lệnh cho hàm processSignalAndSlot:

    #define methods for assigning Signals and Slots
    def processSignalAndSlot(self):
        self.pushButtonExit.clicked.connect(self.processExit)
        self.pushButtonVisitBlog.clicked.connect(self.openMyBlog)

Đồng thời viết hàm openMyBlog để làm Slots:

    def openMyBlog(self):
        import webbrowser
        webbrowser.open('https://tranduythanh.com/')

mã lệnh đầy đủ của “MyQMainWindowExt“:

from PyQt6.QtWidgets import QMessageBox

from MyQMainWindow import Ui_MainWindow

class MyQMainWindowExt(Ui_MainWindow):
    #override setupUi
    #just define attribute MainWindow for reuse in later
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow
    #define methods for assigning Signals and Slots
    def processSignalAndSlot(self):
        self.pushButtonExit.clicked.connect(self.processExit)
        self.pushButtonVisitBlog.clicked.connect(self.openMyBlog)
    #define slot exit window
    def processExit(self):
        dlg = QMessageBox(self.MainWindow)
        dlg.setWindowTitle("Exit Confirmation")
        dlg.setText("Are you sure you want to Exit?")
        dlg.setStandardButtons(
            QMessageBox.StandardButton.Yes
            | QMessageBox.StandardButton.No
        )
        dlg.setIcon(QMessageBox.Icon.Question)
        button = dlg.exec()
        # check the user confirmation
        button = QMessageBox.StandardButton(button)
        if button == QMessageBox.StandardButton.Yes:
            self.MainWindow.close()
        else:
            pass#do nothing
    def openMyBlog(self):
        import webbrowser
        webbrowser.open('https://tranduythanh.com/')

Chạy “MyApp.py”. Nhấn nút “Visit Blog” chương trình sẽ mở màn hình Blog trên trình duyệt:

Như vậy tới đây Tui đã hướng dẫn hoàn chỉnh cách thiết kế giao diện QMainWindow bằng hình thức kéo thả trong Qt Designer, các thuộc tính, phương thức và Slots quan trọng thường dùng Tui cũng đã giới thiệu qua. Đặc biệt kỹ thuật tạo lớp kế thừa từ QMainWindow và cách t ùy chỉnh các Signals/slots rất quan trọng và hữu dụng cũng được trình bày kỹ lưỡng. Các bạn cần thực hành nhiều lần bài này để hiểu lý thuyết cũng như cách thức xử lý. Nhất là tại vì sao ta nên tạo lớp kế thừa khi xử lý các tác vụ cho các giao diện.

Bài 6 Tui sẽ tiếp tục làm chức năng còn lại của bài này, đó là Tui sẽ hướng dẫn các bạn 2 kỹ thuật chính:

  • Cách thiết kế cửa sổ QMainWindow lúc runtime (sử dụng coding PyQt6 – Python)
  • Cách truyền Tín hiệu (dữ liệu) qua lại giữa các QMainWindow

Mã lệnh của bài học số 5 này bạn có thể tải ở đây:

https://www.mediafire.com/file/ocou1o8050qz1td/LearnQMainWindow.rar/file

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

One thought on “Bài 5: QMainWindow và kỹ thuật kế thừa để tùy chỉnh Signals/Slots trong PyQt6 và Qt Designer”

Leave a Reply