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

Trong bài số 5, 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.

Trong Bài 6 này Tui sẽ tiếp tục hướng dẫn các chức năng còn lại của bài 5:

  • 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

Dĩ nhiên để cho đơn giản trong quá trình tạo ra các giao diện tương tác thì chúng ta nên dùng luôn Qt Designer hoặc Qt Creator để tạo các file UI sau đó dùng tool để tự động tạo ra các mã lệnh. Tuy nhiên Trong bài này thì Tui chủ ý không dùng công cụ Designer, mà sẽ viết code runtime (các bạn có thể dùng tool), sau đó Tui sẽ hướng dẫn một số kỹ thuật truyền dữ liệu qua lại giữa các cửa sổ giao diện. Việc truyền dữ liệu này vô cùng quan trọng vì thực tế khi triển khai phần mềm ta sẽ có nhiều màn hình giao diện khác nhau và hiển nhiên việc truyền dữ liệu qua lại sẽ sảy ra. Ví dụ như ta có màn hình Đăng nhập, đăng nhập thành công thì mở màn hình Chính lên, vậy làm sao truyền được thông tin User từ màn đăng nhập qua màn hình chính? Hay giả sử ta có màn hình A và màn hình B bất kỳ thì làm sao để A truyền dữ liệu qua cho B và ngược lại?

Cụ thể trong bài này ta sẽ làm tiếp tục làm các chức năng sau (mở lại dự án trong bài số 5):

Khi khởi động phần mềm, ta có màn hình chính như dưới đây:

  • Khi nhấn nút “Send Name“, thì một màn hình QMainWindow mới (Tui đặt tiêu đề là Second Window) sẽ hiển thị như dưới đây, đồng thời dữ liệu trong ô QLineEdit cũng được truyền qua Second Window, và lưu ý là chỉ cho phép mở một màn hình Second Window duy nhất (tức là nếu Second Window đã hiển thị lên rồi thì có nhấn bao nhiêu lần nút “Send Name” thì nó cũng không tạo ra thêm bất kỳ màn hình Second Window nào nữa):

Màn hình Second Window sẽ thực hiện các tác vụ chính sau:

  • Khi người sử dụng nhấn vào nút “Red” của Second Window thì màn hình ban đầu sẽ đổi qua nền đỏ:
  • Khi người sử dụng nhấn vào nút “Yellow” của Second Window thì màn hình ban đầu sẽ đổi qua nền Vàng:
  • Khi người sử dụng nhập dữ liệu trong ô QLineEdit của Second Window thì dữ liệu này sẽ tự động truyền lại màn hình ban đầu và cập nhật realtime:
  • Khi nhấn nút “Close” trong màn hình Second Window thì sẽ đóng cửa sổ này để quay lại cửa sổ ban đầu

Ta bắt đầu nhé, trước tiên tạo thêm một lớp giao diện tên là “SecondWindow.py” trong dự án “LearnQMainWindow” (bạn có thể tạo bằng Qt Designer, còn bài này mục đích của Tui là hướng dẫn các bạn cách tự coding ra giao diện):

Dưới đây là chi tiết mã lệnh của “SecondWindow.py“:

from PyQt6 import QtCore
from PyQt6.QtWidgets import QLabel, QLineEdit, QPushButton

class SecondWindow(object):
    def __init__(self,parent):
        self.parent=parent
    def setupUi(self, MainWindow):
        self.MainWindow=MainWindow
        self.MainWindow.setWindowTitle("Tran Duy Thanh - Second Window")
        self.MainWindow.resize(381, 110)

        self.label = QLabel(parent=MainWindow)
        self.label.setText("Change Name:")
        self.label.setGeometry(QtCore.QRect(20, 10, 91, 16))

        self.lineEditFullName = QLineEdit(parent=MainWindow)
        self.lineEditFullName.setGeometry(QtCore.QRect(20, 40, 351, 22))

        self.pushButtonRed = QPushButton(parent=MainWindow)
        self.pushButtonRed.setText("Red")
        self.pushButtonRed.setGeometry(QtCore.QRect(40, 70, 93, 28))

        self.pushButtonYellow = QPushButton(parent=MainWindow)
        self.pushButtonYellow.setText("Yellow")
        self.pushButtonYellow.setGeometry(QtCore.QRect(150, 70, 93, 28))

        self.pushButtonClose = QPushButton(parent=MainWindow)
        self.pushButtonClose.setGeometry(QtCore.QRect(250, 70, 93, 28))
        self.pushButtonClose.setText("Close")

        self.lineEditFullName.setText(self.parent.lineEditName.text())

        QtCore.QMetaObject.connectSlotsByName(MainWindow)

        self.lineEditFullName.textChanged.connect(self.parent.lineEditName.setText)
        self.pushButtonClose.clicked.connect(self.processClose)
        self.pushButtonRed.clicked.connect(self.parent.changeRedColor)
        self.pushButtonYellow.clicked.connect(self.parent.changeYellowColor)
    def processClose(self):
        self.MainWindow.close()
        self.parent.secondWindow=None

Trong lớp trên, ta thấy:

  • Dòng 5-6 định nghĩa một constructor của SecondWindow có đối số truyền vào là parent, và parent chính lớp “MyQMainWindowExt“:
def __init__(self,parent):
    self.parent=parent
  • Dòng 35 là Signal truyền thông báo dữ liệu trong ô nhập liệu của Second Window bị thay đổi qua cho màn hình Parent ( “MyQMainWindowExt“), lúc này ô QLineEdit của màn hình Parent sẽ tự động cập nhật theo Second Window.
self.lineEditFullName.textChanged.connect(self.parent.lineEditName.setText)
  • Dòng 36 là Signal để đóng cửa sổ hiện tại:
self.pushButtonClose.clicked.connect(self.processClose)

Signal này dùng hàm processClose để làm Slot, slot này sẽ đóng cửa sổ hiện tại, đồng thời đánh dấu nó = None trong màn hình parent (“MyQMainWindowExt“), nó được khai báo trong dòng 39 tới 41:

 def processClose(self):
     self.MainWindow.close()
     self.parent.secondWindow=None
  • Dòng 37 và 38: Màn hình Second Window sẽ truyền 2 Signals qua màn hình parent (“MyQMainWindowExt“) là changeRedColor (yêu cầu parent tô nền đỏ), và changeYellowColor (yêu cầu parent tô nền vàng):
self.pushButtonRed.clicked.connect(self.parent.changeRedColor)
self.pushButtonYellow.clicked.connect(self.parent.changeYellowColor)

Bước tiếp theo ta tiến hành chỉnh sửa mã lệnh trong lớp parent (“MyQMainWindowExt“), đây là mã lệnh đầy đủ:

from PyQt6 import QtWidgets
from PyQt6.QtWidgets import QMessageBox, QMainWindow

from MyQMainWindow import Ui_MainWindow
from SecondWindow import SecondWindow


class MyQMainWindowExt(Ui_MainWindow):
    #override setupUi
    #just define attribute MainWindow for reuse in later
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow
        self.secondWindow=None
    #define methods for assigning Signals and Slots
    def processSignalAndSlot(self):
        self.pushButtonExit.clicked.connect(self.processExit)
        self.pushButtonVisitBlog.clicked.connect(self.openMyBlog)
        self.pushButtonSendName.clicked.connect(self.openSecondWindow)
    #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/')
    def openSecondWindow(self):
        if self.secondWindow==None or self.qmainWindow.isVisible()==False:
            self.qmainWindow = QMainWindow()
            self.secondWindow=SecondWindow(self)
            self.secondWindow.setupUi(self.qmainWindow)
            self.qmainWindow.show()
    def changeRedColor(self):
        self.MainWindow.setStyleSheet("background-color: red;")
    def changeYellowColor(self):
        self.MainWindow.setStyleSheet("background-color: yellow;")
  • Dòng 14: Ta bổ sung một biến secondWindow để lưu lại bộ nhớ tham chiếu nhằm xử lý tác vụ cửa sổ này đã mở hay chưa, nếu mở rồi thì không cho mở nữa:
self.secondWindow=None
  • Dòng 19 ta truyền Signal cho nút “Send Name”, khi nhấn vào nút này thì nó sẽ mở màn hình Second Window:
self.pushButtonSendName.clicked.connect(self.openSecondWindow)

Ta định nghĩa hàm “openSecondWindow” để làm Slot cho Signal này, chi tiết được viết trong dòng 40 tới dòng 45:

    def openSecondWindow(self):
        if self.secondWindow==None or self.qmainWindow.isVisible()==False:
            self.qmainWindow = QMainWindow()
            self.secondWindow=SecondWindow(self)
            self.secondWindow.setupUi(self.qmainWindow)
            self.qmainWindow.show()

Hàm trên sẽ kiểm tra xem secondWindow đã được mở hay chưa, nếu chưa thì sẽ mở của sổ này lên. Lưu ý dòng 43:

self.secondWindow=SecondWindow(self)

Dòng 43 tạo một đối tượng SecondWindow với đối số truyền vào là self (self chính là đối tượng hiện tại, cửa sổ hiện tại “MyQMainWindowExt”. Nó được lưu vào biến parent trong Second Window.

  • Cuối cùng là dòng 46 tới dòng 49 là các phương thức được định nghĩa để làm slot đổi màu nền (từ SecondWindow truyền tín hiệu qua cho Parent):
    def changeRedColor(self):
        self.MainWindow.setStyleSheet("background-color: red;")
    def changeYellowColor(self):
        self.MainWindow.setStyleSheet("background-color: yellow;")

Các hàm đổi màu nền này ta dùng setStyleSheet như cú pháp ở trên.

Chạy “MyApp.py” ta có kết quả như mô tả.

Như vậy tới đây Tui đã hướng dẫn đầy đủ và chi tiết xong:

  • 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

Đây là kỹ thuật rất quan trọng và thường xuyên sử dụng trong phần mềm, các bạn chú ý thực hiện lại và cố gắng đọc hiểu cũng như tự tay lập trình được các bài tương tự.

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

https://www.mediafire.com/file/3hjwq8xk0gofe7x/LearnQMainWindow_p2.rar/file

Bài học sau Tui sẽ trình bày về các loại Layout trong bố cục giao diện. Muốn làm giao diện theo ý mình thì bắt buộc phải nắm chắc cách thức bố cục thông qua các đối tượng Layout:

  • QHBoxLayout
  • QVBoxLayout
  • QGridLayout
  • QStackedLayout

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

Leave a Reply