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

Leave a Reply