Bài 9: QGridLayout-Layout Management

Như vậy các bạn đã biết cách bố cục giao diện bằng QVBoxLayout theo phương đứng và QHBoxLayout theo phương ngang, tuy nhiên có nhiều giao diện chúng ta phải bố cục theo dạng dòng và cột (Grid). PyQt6 cung cấp GridLayout để chúng ta thực hiện điều này.

GridLayout chia giao diện thành các dòng và cột, dòng chạy từ trên xuống dưới theo index bắt đầu từ 0, cột chạy từ trái qua phải theo index bắt đầu từ 0.

Giao giữa Dòng và Cột được gọi là Ô. Mỗi Ô như vậy ta có thể để 1 Control/widget vào bên trong, do đó muốn có nhiều control/widget trong một Ô thì ta có thể khai báo thêm 1 Layout, layout này chứa nhiều control/widget.

Trong Qt Designer, ta kéo GridLayout ra giao diện, sau đó kéo các Widget vào GridLayout. Mỗi lần kéo Widget vào layout nó sẽ xuất hiện các đường kẻ màu xanh để cho chúng ta lựa chọn nơi đặt:

Ngoài ra ta có thể trộn nhiều cột, trộn nhiều dòng lại với nhau. Chúng ta chỉ cần kéo Widget chiếm qua các ô khác thì sẽ được trộn dòng hoặc cột tùy hướng chúng ta kéo (ta cần làm các ô mà ta muốn trộn ở trạng thái rỗng không có chứa bất kỳ controls/widgets nào). Ví dụ:

Bây giờ ta trộn 3 cột cho Button có nhãn (0,1), lúc này ta chỉ cần chỉnh size của Button này bằng cách kéo nó qua luôn 3 ô liên tục:

Hay trộn Button (3,1) với 2 cột và 3 dòng:

  • Kéo size Button qua trái chiếm 2 cột:
  • Kéo size Button xuống dưới chiếm 3 dòng:

Tương tự, Nếu muốn trộn 3 dòng cho Button có nhãn (3,4) thì ta kéo size của button này chiếm 3 dòng:

Nếu Widget trong mỗi Ô chiếm không gian chưa hết Ô đó thì ta có thể căn lên các Widget này theo nhiều cách:

(gif: nguồn pythontutorial.net)

Dưới đây là một số thuộc tính liên quan tới căn lề mà chúng ta có thể lập trình:

Alignment FlagÝ nghĩa
AlignAbsoluteNếu hướng từ trái sang phải thì AlignLeft là căn chỉnh với cạnh phải. Tuy nhiên, nếu muốn AlignLeft luôn được căn chỉnh với cạnh trái, thì kết hợp AlignLeft với tùy chọn AlignAbsolute .
AlignBaselineCăn chỉnh widget với đường cơ sở.
AlignBottomCăn chỉnh widget ở bên dưới
AlignCenter
căn chỉnh widget ở giữa
AlignHCenterCăn wiget nằm giữa theo chiều ngang
AlignHorizontal_MaskCăn widget nằm ngang bằng cách kết hợp nhiều loại: AlignLeft | AlignRight | AlignHCenter | AlignJustify | AlignAbsolute
AlignJustifyCăn điều text
AlignLeftCăn widget theo lề trái
AlignRightCăn widget theo lề phải
AlignTopCăn widget theo hướng bên trên
AlignVCenterCăn wiget nằm giữa theo chiều đứng
AlignVertical_MaskCăn widget nằm đứng bằng cách kết hợp nhiều loại: AlignTop | AlignBottom | AlignVCenter | AlignBaseline

Ví dụ 1:

Ứng dụng GridLayout thiết kế màn hình đăng nhập bằng Qt Designer:

Dưới đây là cấu trúc tập tin trong dự án:

“MainWindow.ui” là file giao diện được thiết kế bởi Qt Designer, và “MainWindow.py” là file mã lệnh tạo giao diện bằng cách Generate code Python:

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


from PyQt6 import QtCore, QtGui, QtWidgets


class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(524, 261)
        self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)
        self.verticalLayout.setObjectName("verticalLayout")
        self.gridLayout = QtWidgets.QGridLayout()
        self.gridLayout.setObjectName("gridLayout")
        self.horizontalLayout = QtWidgets.QHBoxLayout()
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.pushButtonLogin = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonLogin.setObjectName("pushButtonLogin")
        self.horizontalLayout.addWidget(self.pushButtonLogin)
        self.pushButtonExit = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonExit.setObjectName("pushButtonExit")
        self.horizontalLayout.addWidget(self.pushButtonExit)
        self.gridLayout.addLayout(self.horizontalLayout, 4, 1, 1, 1)
        self.label = QtWidgets.QLabel(parent=self.centralwidget)
        self.label.setMinimumSize(QtCore.QSize(0, 48))
        self.label.setMaximumSize(QtCore.QSize(16777215, 50))
        self.label.setStyleSheet("color: rgb(255, 0, 0);\n"
"font: 75 14pt \"MS Shell Dlg 2\";")
        self.label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
        self.label.setObjectName("label")
        self.gridLayout.addWidget(self.label, 0, 0, 1, 2)
        self.lineEditPassword = QtWidgets.QLineEdit(parent=self.centralwidget)
        self.lineEditPassword.setEchoMode(QtWidgets.QLineEdit.EchoMode.Password)
        self.lineEditPassword.setObjectName("lineEditPassword")
        self.gridLayout.addWidget(self.lineEditPassword, 2, 1, 1, 1)
        self.chkSaveInformation = QtWidgets.QCheckBox(parent=self.centralwidget)
        self.chkSaveInformation.setObjectName("chkSaveInformation")
        self.gridLayout.addWidget(self.chkSaveInformation, 3, 1, 1, 1)
        self.lineEditUsername = QtWidgets.QLineEdit(parent=self.centralwidget)
        self.lineEditUsername.setObjectName("lineEditUsername")
        self.gridLayout.addWidget(self.lineEditUsername, 1, 1, 1, 1)
        self.label_3 = QtWidgets.QLabel(parent=self.centralwidget)
        self.label_3.setObjectName("label_3")
        self.gridLayout.addWidget(self.label_3, 2, 0, 1, 1)
        self.label_2 = QtWidgets.QLabel(parent=self.centralwidget)
        self.label_2.setObjectName("label_2")
        self.gridLayout.addWidget(self.label_2, 1, 0, 1, 1)
        self.verticalLayout.addLayout(self.gridLayout)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 524, 26))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(parent=MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

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

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "Tran Duy Thanh"))
        self.pushButtonLogin.setText(_translate("MainWindow", "Login"))
        self.pushButtonExit.setText(_translate("MainWindow", "Exit"))
        self.label.setText(_translate("MainWindow", "Login Screen"))
        self.chkSaveInformation.setText(_translate("MainWindow", "Save login information"))
        self.label_3.setText(_translate("MainWindow", "Password:"))
        self.label_2.setText(_translate("MainWindow", "User Name:"))

“MainWindowEx.py” là file mã lệnh được viết kế thừa nhằm xử lý các sự kiện liên quan mà không ảnh hưởng tới giao diện khi có thay đổi trong tương lai:

from MainWindow import Ui_MainWindow


class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        pass
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow
    def show(self):
        self.MainWindow.show()

Cuối cùng như thường lệ được trình bày trong các bài học trước, ta tạp file “MyApp.py” để thực thi chương trình:

from PyQt6.QtWidgets import QApplication, QMainWindow

from MainWindowEx import MainWindowEx

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

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

Như vậy ta thấy sau khi tạo GridLayout, việc thêm widget vào layout sẽ làm theo các bước:

Bước 1:

Tạo đối tượng QGridLayout:

layout = QGridLayout()

Bước 2:

Thiết lập layout cho parent:

parent.setLayout(layout)

Bước 3:

Gán các Widget/Control vào trong layout:

layout.addWidget(widget, row, column, rowSpan, columnSpan, alignment)
  • widget là các control/widget mà ta muốn sắp xếp trên layout QGridLayout
  • row là vị trí dòng mà ta muốn gán wiget/control trong QGridLayout, tính theo index từ 0
  • column là vị trí cột mà ta muốn gán wiget/control trong QGridLayout, tính theo index từ 0.
  • rowSpan là số dòng mà ta muốn widget sẽ chiếm trên giao diện.
  • columnSpan là số cột mà ta muốn widget sẽ chiếm trên giao diện.
  • alignment là cách căn lề widget trong ô trên QGridLayout

Code của ví dụ 1 các bạn có thể tải ở đây:

https://www.mediafire.com/file/8ky1vqmb9vrgf8w/LearnQGridLayout.rar/file

Ví dụ 2: Caro game

Tiếp theo Tui sẽ minh họa cách dùng QGridLayout để vẽ giao diện màn hình chơi Caro lúc runtime, đồng thời hướng dẫn cách xử lý gán sự kiện cho các Button bằng signal lúc runtime để biết được Button nào đang thực hiện:

Tạo một dự án tên “Carogame” có cấu trúc như dưới đây:

“MainWindow.ui” được thiết kế bởi Qt Designer có giao diện như sau:

Giao diện trên ta tiết lập QVBoxLayout cho màn hình chính, các ô nhập liệu và button “Draw Caro” nằm trong QHBoxLayout. Cuối cùng ở bên dưới ta có 1 ScrollArea, ScrollArea này chứa QGridLayout để vẽ bàn cờ Caro.

“MainWindow.py” khi generate python code của giao diện:

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


from PyQt6 import QtCore, QtGui, QtWidgets


class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(758, 722)
        self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.centralwidget)
        self.verticalLayout_2.setObjectName("verticalLayout_2")
        self.verticalLayout = QtWidgets.QVBoxLayout()
        self.verticalLayout.setObjectName("verticalLayout")
        self.horizontalLayout = QtWidgets.QHBoxLayout()
        self.horizontalLayout.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetMinimumSize)
        self.horizontalLayout.setContentsMargins(-1, -1, -1, 0)
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.label = QtWidgets.QLabel(parent=self.centralwidget)
        self.label.setObjectName("label")
        self.horizontalLayout.addWidget(self.label)
        self.lineEditRows = QtWidgets.QLineEdit(parent=self.centralwidget)
        self.lineEditRows.setObjectName("lineEditRows")
        self.horizontalLayout.addWidget(self.lineEditRows)
        self.label_2 = QtWidgets.QLabel(parent=self.centralwidget)
        self.label_2.setObjectName("label_2")
        self.horizontalLayout.addWidget(self.label_2)
        self.lineEditColumn = QtWidgets.QLineEdit(parent=self.centralwidget)
        self.lineEditColumn.setObjectName("lineEditColumn")
        self.horizontalLayout.addWidget(self.lineEditColumn)
        self.pushButtonDrawCaro = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonDrawCaro.setObjectName("pushButtonDrawCaro")
        self.horizontalLayout.addWidget(self.pushButtonDrawCaro)
        self.verticalLayout.addLayout(self.horizontalLayout)
        self.verticalLayout_2.addLayout(self.verticalLayout)
        self.scrollArea = QtWidgets.QScrollArea(parent=self.centralwidget)
        self.scrollArea.setWidgetResizable(True)
        self.scrollArea.setObjectName("scrollArea")
        self.scrollAreaWidgetContents_2 = QtWidgets.QWidget()
        self.scrollAreaWidgetContents_2.setGeometry(QtCore.QRect(0, 0, 734, 608))
        self.scrollAreaWidgetContents_2.setObjectName("scrollAreaWidgetContents_2")
        self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents_2)
        self.verticalLayout_3.setObjectName("verticalLayout_3")
        self.gridLayoutCaro = QtWidgets.QGridLayout()
        self.gridLayoutCaro.setSpacing(0)
        self.gridLayoutCaro.setObjectName("gridLayoutCaro")
        self.verticalLayout_3.addLayout(self.gridLayoutCaro)
        self.scrollArea.setWidget(self.scrollAreaWidgetContents_2)
        self.verticalLayout_2.addWidget(self.scrollArea)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 758, 26))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(parent=MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

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

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
        self.label.setText(_translate("MainWindow", "Number Of Rows:"))
        self.lineEditRows.setText(_translate("MainWindow", "50"))
        self.label_2.setText(_translate("MainWindow", "Number Of Columns:"))
        self.lineEditColumn.setText(_translate("MainWindow", "50"))
        self.pushButtonDrawCaro.setText(_translate("MainWindow", "Draw Caro"))

Tương tự như các bài trước, ta tạo “MainWindowEx.py” kế thừa từ lớp generate giao diện để xử lý các sự kiện cũng như không bị ảnh hưởng mã lệnh khi tương lai Giao diện có sự thay đổi:

from functools import partial

from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QPushButton

from MainWindow import Ui_MainWindow


class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        self.previous=''
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow
        self.pushButtonDrawCaro.clicked.connect(self.processDrawCaro)
    def processDrawCaro(self):
        row=int(self.lineEditRows.text())
        column=int(self.lineEditColumn.text())
        for i in  range(row):
            self.gridLayoutCaro.setRowStretch(i,1)
            for j in range(column):
                self.gridLayoutCaro.setColumnStretch(j,1)
                btn= QPushButton()
                btn.setFixedWidth(50)
                btn.setFixedHeight(50)
                btn.sizePolicy().setHorizontalStretch(1)
                btn.sizePolicy().setVerticalStretch(1)
                self.gridLayoutCaro.addWidget(btn, i, j,
                         alignment=Qt.AlignmentFlag.AlignVertical_Mask|Qt.AlignmentFlag.AlignHorizontal_Mask)
                btn.clicked.connect(partial(self.processClicked, btn))
    def processClicked(self,btn):
        if len(btn.text()) > 0:
            return
        if self.previous=="X":
            self.previous="O"
        else:
            self.previous = "X"
        if len(btn.text())==0:
            btn.setText(self.previous)
        else:
            btn.setText(self.previous)
    def show(self):
        self.MainWindow.show()

Ta quan sát slot “processDrawCaro” để vẽ các Button lúc runtime.

trong dòng 30 ta học thêm kỹ thuật mới đó là “partial” có 2 đối số: đối số thứ nhất là slot xử lý sự kiện khi Button được nhấn, đối số thứ 2 ta truyền đối tượng button vào để quá trình xử lý ta biết được chính xác Button nào được nhấn.

Slot “processClicked” sẽ có 2 đối số, đối số thứ 2 là biến btn , biến này chính là PushButton được nhấn, dựa vào Button này ta sẽ xử lý chính xác Button nào đang được nhấn.

Ngoài ra coding còn bổ sung biến previous, biến này đánh dấu là trước đó ‘X’ hay ‘O’ đã được chọn.

Cuối cùng ta tạo lớp “MyApp.py” để thực thi chương trình:

from PyQt6.QtWidgets import QApplication, QMainWindow

from MainWindowEx import MainWindowEx

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

Chạy chương trình lên ta có giao diện:

Ta nhấn nút “Draw Caro”, chương trình sẽ vẽ:

Coding đầy đủ của bài “Caro Game” này các bạn tải ở đây:

https://www.mediafire.com/file/ilcxryavbdk6teq/Carogame.rar/file

Như vậy Tui đã trình bày xong cách sử dụng QGridLayout, với 2 ví dụ quan trọng: thiết kế giao diện lúc Design Time và thiết kế giao diện lúc Runtime thông qua bài vẽ bàn cơ caro. Khi thiết kế màn hình với dạng dòng và cột thì các bạn có thể liên tưởng tới cách sử dụng QGridLayout.

Ngoài ra trong các ví dụ ta cũng thấy được sự kết hợp của nhiều loại layout khác nhau, cũng như cách dùng ScrollArea để thiết kế các màn hình mà có nội dung vượt quá khả năng lưu trữ của màn hình.

Bài học sau Tui sẽ hướng dẫn QFormlayout để sử dụng trong việc thiết kế giao diện có dạng nhập liệu (data-entry) form.

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

Bài 8: QHBoxLayout-Layout Management

Trong bài 7 Chúng ta đã biết cách dùng QVBoxLayout để sắp xếp các controls/widgets theo phương đứng. Trong bài này Tui sẽ trình bày về QHBoxLayout để bố cục các controls/widgets theo phương ngang. Cũng như cách thức phối hợp QVBoxLayout và QHBoxLayout để thiết kế màn hình giải phương trình bậc 1.

Tạo một dự án tên là “LearnQHBoxLayout” trong Pycharm, sau đó dùng Qt Designer để thiết kế màn hình theo các bước như dưới đây:

Bấm chuột phải vào dự án ==> chọn External Tools==> Chọn Create new Qt Designer.

Ta kéo thả “Horizontal Layout” vào giao diện của MainWindow

Tiếp tục kéo thả thêm 3 Button như hình dưới đây:

Quan sát thấy 3 Button sẽ được sắp xếp theo phương nằm ngang

Tương tự như QVBoxLayout, ta có thể căn Trái, Căn Phải, Căn Giữa các Controls/Widgets trong layout bằng cách dùng Spacer, cụ thể là dùng Horizontal Spacer.

Để căn trái ta kéo Horizontal Spacer vào layout như hình dưới đây:

Nếu kéo Horizontal Spacer như trên thì các Controls/Widgets sẽ được căn trái trong Layout, tương tự như chức năng Dock Left.

Để căn phải ta kéo Horizontal Spacer vào layout như hình dưới đây:

Ta lưu ý khi nhấn Spacer hay bất kỳ 1 Widget nào vào layout thì sau khi kéo Widget vào ta khoan hãy nhả chuột ra, mà cần rê rê chuột tới vị trí ta muốn thả, khi nào có đường kẻ màu xanh xuất hiện thì ta nhả chuột ra thì nó sẽ được bố cục chính xác nơi mà ta mong muốn:

Để căn giữa layout ta kéo 2 Spacer nằm ở 2 bên của Layout thì lúc này các Widgets sự được căn giữa layout.

Để thăm các Spacer giữa các Widgets trong một layout ta chỉ việc kéo Spacer đó chèn vào giữa các Widgets mà ta muốn bố cục. Ví dụ:

Lưu ý là muốn Spacer nằm ở đâu thì lúc kéo vào giao diện ta rê rê chuột, khi nào thanh màu xanh xuất hiện thì ta nhả chuột ra:

Tương tự như QVBoxLayout, thì layout này cũng hỗ trợ các Margin để ta định vị thêm các khoảng cách tương đối của các Widgets nằm bên trong Layout so với nó. Khoảng cách này ta gọi là Margin:

Ví dụ như sau khi chỉnh sửa Margin ta có:

Ở giao diện trên ta chỉnh:

  • layoutLeftMargin =85
  • layoutTopMargin=92
  • layoutRightMargin=100
  • layoutBottomMargin=90

Bây giờ ta thử kết hợp QVBoxLayout và QHBoxLayout để thiết kế giao diện màn hình giải phương trình bậc 1:

Bước 1:

Tiếp tục bấm chuột phải vào dự án “LearnQHBoxLayout” ==> Chọn External Tools==>Chọn Create new Qt Designer.

Chọn QMainWindow để thiết kế giao diện, Lưu giao diện lại với tên “MainWindowFirstDegreeEquation.ui

Bước 2:

Kéo Vertical Layout vào giao diện

Tiếp tục hiệu chỉnh layout của MainWindow qua Lay out Vertically bằng cách bấm chuột phải vào vùng trống bất kỳ của MainWindow==> Chọn “Lay out” ==> Chọn “Lay Out Vertically“:

Lúc này giao diện sẽ tự động bố cục như dưới đây:

Bước 3:

Kéo thêm một Label vào giao QVBoxLayout và đặt nhãn là “First Degree Equation“, chỉnh Font Size=15 và Bold=True trong cửa sổ Property:

Bước 4:

Kéo thêm một QHBoxLayout vào bên trong QVBoxLayout và cho nó ở bên dưới “First Degree Equation”, layout này để hiển thị nhãn Hệ số A và ô nhập liệu Hệ số A:

Kéo Label và LineEdit vào QHBoxLayout ở trên để làm ô nhập liệu cho hệ số a:

Bước 5:

Tương tự như vậy, ta lặp lại bước 4 để làm cho hệ số b.

Kéo thêm một QHBoxLayout vào bên trong QVBoxLayout và cho nó ở bên dưới QHBoxLayout hệ số a:

Kéo Label và LineEdit vào QHBoxLayout ở trên để làm ô nhập liệu cho hệ số b:

Bước 6:

Tương tự như vậy, ta lặp lại bước 5 để thêm QHBoxLayout kết quả:

Kéo thêm một QHBoxLayout vào bên trong QVBoxLayout và cho nó ở bên dưới QHBoxLayout Hệ số b:

Kéo Label và LineEdit vào QHBoxLayout ở trên để làm ô hiển thị kết quả giải phương trình:

Bước 7:

Tương tự như vậy, ta lặp lại bước 6 để thêm 3 Button “Solution” (giải phương trình), “Clear” (xóa dữ liệu trên giao diện để nhập phương trình mới), “Exit” (Thoát phần mềm).

Kéo thêm một QHBoxLayout vào bên trong QVBoxLayout và cho nó ở bên dưới QHBoxLayout Kết quả:

Tiếp tục lần lượt kéo 3 Button vào QHBoxLayout ở trên:

Trong bài này chúng ta chưa tiến hành giải phương trình, mà chúng ta chỉ dừng lại ở việc thiết kế giao diện thông qua sự kết hợp giữa QHBoxLayout và QVBoxLayout. Các bạn có thể bổ sung thêm các Spacer, hay hiệu chỉnh lại font chữ của các Widget cho phù hợp theo ý mình.

Các bạn tiến hành đặt tên cho các control/widgets tương ứng với các nhãn (Bấm chuột phải vào Widget/chọn change Object Name.., Hoặc trong Property sửa object Name). Chi tiết các control này sẽ được học trong phần Basic Widgets.

Sau khi cập nhập giao diện thì nhấn nút lưu để đảm bảo giao diện này được cập nhật mới nhất vì nó đồng bộ qua dự án bên Pycharm.

Bước 8:

Generate code giao diện “MainWindowFirstDegreeEquation.ui” bằng cách bấm chuột phải vào nó ==> Chọn “External Tools” ==> Chọn “Generate Python Code with PyUIC”:

Lúc này file mã nguồn của giao diện sẽ tự động được tạo ra “MainWindowFirstDegreeEquation.py”:

# Form implementation generated from reading ui file 'MainWindowFirstDegreeEquation.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(500, 235)
        self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.centralwidget)
        self.verticalLayout_2.setObjectName("verticalLayout_2")
        self.verticalLayout = QtWidgets.QVBoxLayout()
        self.verticalLayout.setObjectName("verticalLayout")
        self.label = QtWidgets.QLabel(parent=self.centralwidget)
        font = QtGui.QFont()
        font.setPointSize(15)
        font.setBold(True)
        font.setWeight(75)
        self.label.setFont(font)
        self.label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
        self.label.setObjectName("label")
        self.verticalLayout.addWidget(self.label)
        self.horizontalLayout = QtWidgets.QHBoxLayout()
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.label_2 = QtWidgets.QLabel(parent=self.centralwidget)
        self.label_2.setObjectName("label_2")
        self.horizontalLayout.addWidget(self.label_2)
        self.lineEditCoefficientA = QtWidgets.QLineEdit(parent=self.centralwidget)
        self.lineEditCoefficientA.setObjectName("lineEditCoefficientA")
        self.horizontalLayout.addWidget(self.lineEditCoefficientA)
        self.verticalLayout.addLayout(self.horizontalLayout)
        self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
        self.horizontalLayout_3.setObjectName("horizontalLayout_3")
        self.label_3 = QtWidgets.QLabel(parent=self.centralwidget)
        self.label_3.setObjectName("label_3")
        self.horizontalLayout_3.addWidget(self.label_3)
        self.lineEditCoefficientB = QtWidgets.QLineEdit(parent=self.centralwidget)
        self.lineEditCoefficientB.setObjectName("lineEditCoefficientB")
        self.horizontalLayout_3.addWidget(self.lineEditCoefficientB)
        self.verticalLayout.addLayout(self.horizontalLayout_3)
        self.horizontalLayout_4 = QtWidgets.QHBoxLayout()
        self.horizontalLayout_4.setObjectName("horizontalLayout_4")
        self.label_4 = QtWidgets.QLabel(parent=self.centralwidget)
        self.label_4.setMinimumSize(QtCore.QSize(0, 0))
        self.label_4.setObjectName("label_4")
        self.horizontalLayout_4.addWidget(self.label_4)
        self.lineEditsolution = QtWidgets.QLineEdit(parent=self.centralwidget)
        self.lineEditsolution.setObjectName("lineEditsolution")
        self.horizontalLayout_4.addWidget(self.lineEditsolution)
        self.verticalLayout.addLayout(self.horizontalLayout_4)
        self.horizontalLayout_6 = QtWidgets.QHBoxLayout()
        self.horizontalLayout_6.setObjectName("horizontalLayout_6")
        self.pushButtonSolution = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonSolution.setObjectName("pushButtonSolution")
        self.horizontalLayout_6.addWidget(self.pushButtonSolution)
        self.pushButtonClear = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonClear.setObjectName("pushButtonClear")
        self.horizontalLayout_6.addWidget(self.pushButtonClear)
        self.pushButtonExit = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonExit.setObjectName("pushButtonExit")
        self.horizontalLayout_6.addWidget(self.pushButtonExit)
        self.verticalLayout.addLayout(self.horizontalLayout_6)
        self.verticalLayout_2.addLayout(self.verticalLayout)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 500, 26))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(parent=MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

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

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
        self.label.setText(_translate("MainWindow", "First Degree Equation"))
        self.label_2.setText(_translate("MainWindow", "Coefficient a:"))
        self.label_3.setText(_translate("MainWindow", "Coefficient b:"))
        self.label_4.setText(_translate("MainWindow", "The Solution:"))
        self.pushButtonSolution.setText(_translate("MainWindow", "Solution"))
        self.pushButtonClear.setText(_translate("MainWindow", "Clear"))
        self.pushButtonExit.setText(_translate("MainWindow", "Exit"))

Tạo một lớp “MainWindowFirstDegreeEquationEx.py” kế thừa từ “MainWindowFirstDegreeEquation.py”

from MainWindowFirstDegreeEquation import Ui_MainWindow


class MainWindowFirstDegreeEquationEX(Ui_MainWindow):
    def __init__(self):
        pass
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow
        self.MainWindow.setWindowTitle("Tran Duy Thanh - Demo Layout Management")
    def show(self):
        self.MainWindow.show()

Tiếp tục tạo một lớp “MyApp.py”

from PyQt6.QtWidgets import QApplication, QMainWindow

from MainWindowFirstDegreeEquatioEx import MainWindowFirstDegreeEquationEX

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

Cấu trúc dự án đầy đủ:

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

Như vậy giao diện đã chạy đúng như mong muốn thiết kế của chúng ta

Bước 9:

Bây giờ chúng ta tiếp tục bổ sung mã lệnh cho “MainWindowFirstDegreeEquatioEx.py” để xử lý các chức năng cho các Button “Solution”, “Clear” và “Exit”:

from MainWindowFirstDegreeEquation import Ui_MainWindow


class MainWindowFirstDegreeEquationEX(Ui_MainWindow):
    def __init__(self):
        pass
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow
        self.MainWindow.setWindowTitle("Tran Duy Thanh - Demo Layout Management")
        self.pushButtonExit.clicked.connect(self.process_exit)
        self.pushButtonClear.clicked.connect(self.process_clear)
        self.pushButtonSolution.clicked.connect(self.process_solution)
    def process_solution(self):
        a = float(self.lineEditCoefficientA.text())
        b = float(self.lineEditCoefficientB.text())
        if a==0 and b ==0:
            self.lineEditsolution.setText("Infinities solutions")
        elif a==0 and b!=0:
            self.lineEditsolution.setText("No solution")
        else:
            self.lineEditsolution.setText("X="+str(-b/a))
    def process_clear(self):
        self.lineEditCoefficientA.setText("")
        self.lineEditCoefficientB.setText("")
        self.lineEditsolution.setText("")
        self.lineEditCoefficientA.setFocus()
    def process_exit(self):
        self.MainWindow.close()
    def show(self):
        self.MainWindow.show()

Chạy “MyApp.py” ta thử nghiệm một số phương trình và có các kết quả như hình dưới đây:

  • Thử nghiệm với phương trình vô số nghiệm
  • Thử nghiệm với phương trình vô nghiệm
  • Thử nghiệm với phương trình có nghiệm

Như vậy tới đây Tui đã hướng dẫn đầy đủ và chi tiết cách thức sử dụng QHBoxLayout cũng như cách phối hợp với QVBoxLayout để thiết kế giao diện, cùng với việc sử dụng một số Widget cơ bản như QLineEdit, QPushButton, QLabel để thiết kế giao diện giải phương trình bậc 1. Đồng thời cũng ôn tập lại cách thức khai báo và sử dụng Signal với Slot.

Dựa vào ví dụ ở trên để các bạn có thể thiết kế các màn hình giao diện tương tự, ví dụ như màn hình Giải phương trình bậc 2, hay các màn hình tính toán số học, máy tính bỏ túi…

Giao diện và mã lệnh của dự án này các bạn tải ở đây:

https://www.mediafire.com/file/n68gedyqrejy8hr/LearnQHBoxLayout.rar/file

Bài học sau Tui sẽ hướng dẫn chi tiết về cách sử dụng QGridLayout

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

Bài 7: QVBoxLayout-Layout Management

PyQt cũng tương tự như các nền tảng khác, nó cung cấp một số layout management để giúp ta có thể thêm nhiều control/Widget và bố cục giao diện một cách dễ dàng hơn. Trong các bài học này Tui sẽ trình bày về:

  • QVBoxLayout (sắp xếp các control/Widget theo chiều đứng)
  • QHBoxLayout(sắp xếp các control/Widget theo chiều ngang)
  • QGridLayout (sắp xếp các control/Widget theo dòng và cột)
  • QFormLayout (sắp xếp các control/Widget theo dạng Data-Entry)

Trước tiên ta làm quen với QVBoxLayout . Tạo một dự án tên “LearnQVBoxLayout” trong Pycharm, sau đó dùng QT Designer để thiết kế giao diện đặt tên file là MainWindow.ui , sau đó sử dụng QVBoxLayout:

Cách tạo dự án và cách sử dụng QT Designer đã được hướng dẫn chi tiết ở các bài trước, nên bài này Tui không nói lại:

Trong mục Layouts, ta kéo “Vertical Layout” vào màn hình thiết kế.

Ngoài ra, với QMainWindow, nếu bạn muốn thiết lập layout cho nó thì nhấn chuột phải vào vị trí trống bất kỳ trên giao diện -> sau đó chọn “Lay out”-> rồi chọn các loại Layout mà mình muốn bố cục, ví dụ này Tui chọn “Layout vertically”

Lúc này QVBoxLayout mà bạn kéo vào nó có hình dạng như dưới đây:

Và nếu bạn muốn bỏ thiết lập layout thì ta Bấm chuột phải vào Layout đó rồi chọn “Layout out” –> chọn “Break Layout“:

Bây giờ trong QVBoxLayout mà bạn kéo thả ở trên, ta kéo thêm 4 Buttons vào bên trong:

Ta quan sát thấy, các Control khi kéo vào nó sẽ sắp xếp theo phương đứng như trên.

Chúng ta có thể căn lề các Controls/Widgets bằng cách dùng Spacer, ví dụ muốn căn lề Bottom (muốn các Controls/widgets chỉ nằm ở phía bên dưới của Layout) ta kéo Vertical Spacer như sau:

Bạn có thể Generate Python code (MainWindow.py) để xem cách thức PyQt6 tạo giao diện:

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


from PyQt6 import QtCore, QtGui, QtWidgets


class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(397, 284)
        self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.centralwidget)
        self.verticalLayout_2.setObjectName("verticalLayout_2")
        self.verticalLayout = QtWidgets.QVBoxLayout()
        self.verticalLayout.setObjectName("verticalLayout")
        spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding)
        self.verticalLayout.addItem(spacerItem)
        self.pushButton_2 = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButton_2.setObjectName("pushButton_2")
        self.verticalLayout.addWidget(self.pushButton_2)
        self.pushButton_3 = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButton_3.setObjectName("pushButton_3")
        self.verticalLayout.addWidget(self.pushButton_3)
        self.pushButton_4 = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButton_4.setObjectName("pushButton_4")
        self.verticalLayout.addWidget(self.pushButton_4)
        self.pushButton_5 = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButton_5.setObjectName("pushButton_5")
        self.verticalLayout.addWidget(self.pushButton_5)
        self.verticalLayout_2.addLayout(self.verticalLayout)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 397, 22))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(parent=MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

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

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
        self.pushButton_2.setText(_translate("MainWindow", "PushButton 1"))
        self.pushButton_3.setText(_translate("MainWindow", "PushButton 2"))
        self.pushButton_4.setText(_translate("MainWindow", "PushButton 3"))
        self.pushButton_5.setText(_translate("MainWindow", "PushButton 4"))

Ta thấy khi Generate code, dòng 22, 23 là các mã lệnh để thêm Spacer vào Layout.

Muốn căn lề Top (Muốn các Controls/Widgets nằm ở phía bên trên layout), ta kéo (di chuyển) Vertical Spacer như sau:

Bạn có thể hiểu nó giống như chức năng DOCK, gắn cố định các control ở một góc nào đó trên giao diện.

Bạn có thể Generate Python code (MainWindow.py) để xem cách thức PyQt6 tạo giao diện có liên quan tới căn lề Top ở trên:

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


from PyQt6 import QtCore, QtGui, QtWidgets


class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(397, 284)
        self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.centralwidget)
        self.verticalLayout_2.setObjectName("verticalLayout_2")
        self.verticalLayout = QtWidgets.QVBoxLayout()
        self.verticalLayout.setObjectName("verticalLayout")
        self.pushButton_2 = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButton_2.setObjectName("pushButton_2")
        self.verticalLayout.addWidget(self.pushButton_2)
        self.pushButton_3 = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButton_3.setObjectName("pushButton_3")
        self.verticalLayout.addWidget(self.pushButton_3)
        self.pushButton_4 = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButton_4.setObjectName("pushButton_4")
        self.verticalLayout.addWidget(self.pushButton_4)
        self.pushButton_5 = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButton_5.setObjectName("pushButton_5")
        self.verticalLayout.addWidget(self.pushButton_5)
        spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding)
        self.verticalLayout.addItem(spacerItem)
        self.verticalLayout_2.addLayout(self.verticalLayout)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 397, 22))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(parent=MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

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

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
        self.pushButton_2.setText(_translate("MainWindow", "PushButton 1"))
        self.pushButton_3.setText(_translate("MainWindow", "PushButton 2"))
        self.pushButton_4.setText(_translate("MainWindow", "PushButton 3"))
        self.pushButton_5.setText(_translate("MainWindow", "PushButton 4"))

Ta thấy khi Generate code, dòng 34, 35 là các mã lệnh để thêm Spacer vào Layout.

Muốn Căn lề giữa, ta kéo 2 Verticle Spacer:

Bạn có thể Generate Python code (MainWindow.py) để xem cách thức PyQt6 tạo giao diện có liên quan tới căn lề Giữa ở trên:

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


from PyQt6 import QtCore, QtGui, QtWidgets


class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(397, 284)
        self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.centralwidget)
        self.verticalLayout_2.setObjectName("verticalLayout_2")
        self.verticalLayout = QtWidgets.QVBoxLayout()
        self.verticalLayout.setObjectName("verticalLayout")
        spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding)
        self.verticalLayout.addItem(spacerItem)
        self.pushButton_2 = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButton_2.setObjectName("pushButton_2")
        self.verticalLayout.addWidget(self.pushButton_2)
        self.pushButton_3 = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButton_3.setObjectName("pushButton_3")
        self.verticalLayout.addWidget(self.pushButton_3)
        self.pushButton_4 = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButton_4.setObjectName("pushButton_4")
        self.verticalLayout.addWidget(self.pushButton_4)
        self.pushButton_5 = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButton_5.setObjectName("pushButton_5")
        self.verticalLayout.addWidget(self.pushButton_5)
        spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding)
        self.verticalLayout.addItem(spacerItem1)
        self.verticalLayout_2.addLayout(self.verticalLayout)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 397, 22))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(parent=MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

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

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
        self.pushButton_2.setText(_translate("MainWindow", "PushButton 1"))
        self.pushButton_3.setText(_translate("MainWindow", "PushButton 2"))
        self.pushButton_4.setText(_translate("MainWindow", "PushButton 3"))
        self.pushButton_5.setText(_translate("MainWindow", "PushButton 4"))

Ta thấy khi Generate code, dòng 22,23 và 36, 37 là các mã lệnh để thêm Spacer vào Layout để căn lề giữa.

Ngoài ra ta có thể thêm các Spacer giữa các Control/Widgets với nhau, ví dụ:

Bạn có thể Generate Python code (MainWindow.py) để xem cách thức PyQt6 tạo giao diện có liên quan tới căn lề Giữa ở trên:

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


from PyQt6 import QtCore, QtGui, QtWidgets


class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(397, 284)
        self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.centralwidget)
        self.verticalLayout_2.setObjectName("verticalLayout_2")
        self.verticalLayout = QtWidgets.QVBoxLayout()
        self.verticalLayout.setObjectName("verticalLayout")
        spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding)
        self.verticalLayout.addItem(spacerItem)
        self.pushButton_2 = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButton_2.setObjectName("pushButton_2")
        self.verticalLayout.addWidget(self.pushButton_2)
        self.pushButton_3 = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButton_3.setObjectName("pushButton_3")
        self.verticalLayout.addWidget(self.pushButton_3)
        self.pushButton_4 = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButton_4.setObjectName("pushButton_4")
        self.verticalLayout.addWidget(self.pushButton_4)
        spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding)
        self.verticalLayout.addItem(spacerItem1)
        self.pushButton_5 = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButton_5.setObjectName("pushButton_5")
        self.verticalLayout.addWidget(self.pushButton_5)
        spacerItem2 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding)
        self.verticalLayout.addItem(spacerItem2)
        self.verticalLayout_2.addLayout(self.verticalLayout)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 397, 22))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(parent=MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

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

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
        self.pushButton_2.setText(_translate("MainWindow", "PushButton 1"))
        self.pushButton_3.setText(_translate("MainWindow", "PushButton 2"))
        self.pushButton_4.setText(_translate("MainWindow", "PushButton 3"))
        self.pushButton_5.setText(_translate("MainWindow", "PushButton 4"))

Ta thấy khi Generate code, dòng 22,23 và 33, 34 và 38,39 là các mã lệnh để thêm Spacer vào Layout để căn lề cho các Controls/Widgets.

Để thử nghiệm giao diện thì ta vào menu Form/ Chọn Preview:

Qt Designer sẽ hiển thị giao diện cho ta thử nghiệm, nó sẽ chạy đúng như vậy khi ta nạp vào mã lệnh Python trong Pycharm hoặc các tool desktop application khác.

Dưới đây là mã lệnh Python:

  • MainWindow.ui –> là file giao diện được tạo ra từ Qt Designer
  • MainWindow.py –> là file Python được Generate từ PyUIC (đã cung cấp ở bên trên)
  • MainWindowEx.py–> là file Python mà ta sẽ viết 1 lớp kế thừa từ MainWindow.py (UI_MainWindow), mục đích tách ra lớp kế thừa để bất cứ khi nào ta đổi giao diện và generate lại python thì không bị ảnh hưởng tới các mã lệnh mà ta đã viết trước đó.
  • MyApp.py –> file python khai báo để chạy dự án này

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

from MainWindow import Ui_MainWindow

class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        pass
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow
    def show(self):
        self.MainWindow.show()

Như vậy, khi ta muốn bổ sung thêm bất kỳ mã lệnh nào để xử lý cho giao diện này thì chỉ nên viết trong “MainWindowEx.py“. Không nên viết trong file “MainWindow.py”, vì nếu viết trong “MainWindow.py” thì khi có nhu cầu thay đổi giao diện và generate lại code python sẽ làm mất các mã lệnh mới của mình trước đó.

Còn đây là mã lệnh của MyApp.py:

from PyQt6.QtWidgets import QApplication, QMainWindow

from MainWindowEx import MainWindowEx

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

Chạy chương trình “MyApp.py” ta có kết quả như mong muốn.

QVBoxLayout còn nhiều thuộc tính và các tính năng khác, tuy nhiên chúng ta chỉ cần nắm tới đây là đủ để bố trí giao diện nằm đứng theo ý mình rồi.

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

https://www.mediafire.com/file/ma2kylupmdh175c/LearnQVBoxLayout.rar/file

Bài sau Tui sẽ trình bày về cách sử dụng QHBoxLayout để bố cục các control/widgets theo phương nằm ngang.

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

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

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

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 "$FileDir$\$FileName$" -o "$FileDir$\$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

Nếu máy MAC:

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

Hoặc:

https://tranduythanh.com/software/QtDesigner.dmg

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.