跳转至

界面设计和布局2

您需要高效学习,找工作? 点击咨询 报名实战班

点击查看学员就业情况

直接写代码 开发界面

前面我们都是在 Qt Designer 上添加的Layout和里面的控件。

这种做法比较吸引初学者,因为看起来比较容易上手。

其实,Qt开发熟练后, 更推荐 直接用代码写界面 ,开发效率更高。

这里介绍一下如何 用代码 直接写 界面布局

水平/垂直 布局

最常用的2种类型的 布局 Layout 是:

QHBoxLayout水平布局

QVBoxLayout垂直布局


它们都是 Qt 的 QtWidgets 模块里面定义的类型,所以可以这样导入使用

from PySide6 import QtWidgets

hl = QtWidgets.QHBoxLayout()
vl = QtWidgets.QVBoxLayout()

也可以这样导入使用

from PySide6.QtWidgets import QHBoxLayout, QVBoxLayout

hl = QHBoxLayout()
vl = QVBoxLayout()


有些控件,通常作为 容器控件 ,内部存放其它的控件,比如: QWidgetQFrame

要指定一个容器控件使用某个layout,可以使用 setLayout 方法,比如

from PySide6.QtWidgets import QApplication, QFrame, QHBoxLayout, QLabel

app = QApplication()

frame = QFrame()
hl = QHBoxLayout()  # 创建水平布局 hl

frame.setLayout(hl) # 设置 hl 为 frame的布局

hl.addWidget(QLabel('控件1')) # hl布局中添加一个控件
hl.addWidget(QLabel('控件2')) # hl布局中添加一个控件

frame.resize(250, 100)
frame.show()
app.exec()

这里的 QFrame 可以想象为一个白板控件,里面可以存放其它控件,

由于指定了它使用 水平布局,所以里面添加的 子控件都是水平放置的。

运行结果如下


也可以 在创建 Layout 时的 初始化参数 中指定它作为哪个控件 的 布局。

比如,上面的代码可以等效的写为这样:

from PySide6.QtWidgets import QApplication, QFrame, QHBoxLayout, QLabel

app = QApplication()

frame = QFrame()

# 创建水平布局 hl,并设置其为 frame的布局
hl = QHBoxLayout(frame)

hl.addWidget(QLabel('控件1'))
hl.addWidget(QLabel('控件2'))

frame.resize(250, 100)
frame.show()
app.exec()


前面的代码已经告诉我们了,要在layout中添加控件,使用其 addWidget 方法。

下面再举一例:

from PySide6 import QtWidgets

# 定义的这个Window类型 是 QFrame 的子类,也就是一个QFrame
class Window(QtWidgets.QFrame):
    def __init__(self):
        super().__init__()

        button1 = QtWidgets.QPushButton('按钮1', self)
        button2 = QtWidgets.QPushButton('按钮2', self)
        button3 = QtWidgets.QPushButton('按钮3', self)

        # 创建一个水平layout ,作为本控件的布局, 
        hl = QtWidgets.QHBoxLayout(self)

        # 添加内部子控件
        hl.addWidget(button1)
        hl.addWidget(button2)
        hl.addWidget(button3)

app = QtWidgets.QApplication()
window = Window()
window.resize(400, 200)
window.show()
app.exec()


要在layout中添加 内部 子layout ,使用其 addLayout 方法,比如

from PySide6 import QtWidgets

class Window(QtWidgets.QFrame):
    def __init__(self):
        super().__init__()

        # 创建一个垂直layout,作为本控件的 Layout,也就是 顶级Layout
        layout = QtWidgets.QVBoxLayout(self)

        # 顶级Layout 添加内部控件,一个多行编辑框
        layout.addWidget(QtWidgets.QPlainTextEdit('内部控件多行编辑框')) 

        # 创建一个水平layout , hl
        hl = QtWidgets.QHBoxLayout()

        # 顶级Layout 添加 hl 为 子layout
        layout.addLayout(hl)

        # 子layout 添加 控件
        hl.addWidget(QtWidgets.QPushButton('按钮1'))
        hl.addWidget(QtWidgets.QPushButton('按钮2'))
        hl.addWidget(QtWidgets.QPushButton('按钮3'))

app = QtWidgets.QApplication()
window = Window()
window.resize(400, 200)
window.show()
app.exec()

运行结果如下


这样,通过 Layout , 子Layout, 子控件, 子控件中的layout ,这些 层层组合,完成各种复杂的界面布局。

表格布局 (Grid Layout)

QGridLayout表格布局, 添加控件时,需要指定行号/列号,

行号/列号从 0 开始,比如 要实现如下界面

对应代码如下

from PySide6 import QtWidgets

class Window(QtWidgets.QFrame):
    def __init__(self):
        super().__init__()

        button1 = QtWidgets.QPushButton('按钮1', self)
        button2 = QtWidgets.QPushButton('按钮2', self)
        button3 = QtWidgets.QPushButton('按钮3', self)

        # 创建一个水平layout作为内部layout
        gl = QtWidgets.QGridLayout()
        gl.addWidget(button1, 0 , 0) # 添加到第1行,第1列
        gl.addWidget(button2, 0 , 1) # 添加到第1行,第2列       
        gl.addWidget(button3, 1 , 1) # 添加到第2行,第2列 

        # 指定自身使用的layout
        self.setLayout(gl)

app = QtWidgets.QApplication()
window = Window()
window.resize(400, 200)
window.show()
app.exec()


表格布局可以

通过 setColumnStretch 指定每个 表格列 的宽度比例

通过 setColumnMinimumWidth 指定每个 表格列 的最小宽度

层叠布局 (Stacked Layout)

QStackedLayout 是 一种层叠布局, 可以通过代码指定要显示其中的哪一层

比如这样的界面

对应代码如下

from PySide6 import QtWidgets

class Window(QtWidgets.QFrame):
    def __init__(self):
        super().__init__()

        # 主Layout
        mainLayout = QtWidgets.QVBoxLayout(self)

        # 界面上方是一排按钮
        button1 = QtWidgets.QPushButton('按钮1', self)
        button2 = QtWidgets.QPushButton('按钮2', self)
        button3 = QtWidgets.QPushButton('按钮3', self)
        btnLayout = QtWidgets.QHBoxLayout()
        btnLayout.addWidget(button1)
        btnLayout.addWidget(button2)
        btnLayout.addWidget(button3)

        # 按钮添加到主界面
        mainLayout.addLayout(btnLayout)

        # 3个页面的容器控件
        page1 = QtWidgets.QWidget()
        page2 = QtWidgets.QWidget()
        page3 = QtWidgets.QWidget()

        # 3个页面各自的内容
        label1 = QtWidgets.QLabel('第1页',page1)
        label2 = QtWidgets.QLabel('第2页',page2)
        label3 = QtWidgets.QLabel('第3页',page3)

        # 3个页面放到 QStackedLayout 里面
        stackedLayout = QtWidgets.QStackedLayout()
        stackedLayout.addWidget(page1)
        stackedLayout.addWidget(page2)
        stackedLayout.addWidget(page3)

        # QStackedLayout 放入主界面下方
        mainLayout.addLayout(stackedLayout)

        # 3个按钮点击行为
        button1.clicked.connect(
            lambda:stackedLayout.setCurrentWidget(page1))
        button2.clicked.connect(
            lambda:stackedLayout.setCurrentWidget(page2))
        button3.clicked.connect(
            lambda:stackedLayout.setCurrentWidget(page3))

app = QtWidgets.QApplication()
window = Window()
window.show()
app.exec()


上例中,通过3个按钮触发显示不同的界面, 当然也可以通过任何其它方式触发, 比如 主窗口的菜单选择触发。

显示stack layout 里面哪个界面,可以上例中使用 setCurrentWidget 方法,参数是 界面对应的 Widget 对象。也可以使用 setCurrentIndex 方法, 通过索引指定显示,比如

# 展示第1页
stackedLayout.setCurrentIndex(0)

# 展示第2页
stackedLayout.setCurrentIndex(1)

# 展示第3页
stackedLayout.setCurrentIndex(2)

QMainWindow 里面的 Layout

上面的代码示例中, 使用QFrame作为 程序界面的 顶级控件类型,

实际应用中,我们开发的程序界面, 顶级控件类型 大都是 QMainWindow 主窗口类

因为它是 可以 具有 菜单栏工具栏状态栏中央控件(Central Widget) 的 特殊 Widget, 正好适合作为 一个 主窗口。

image


主窗口的主体内容在 中央控件(Central Widget) 中。

通常,我们需要对 Central Widget 进行一些布局设置。

这些当然可以在 Qt Designer 里面设置。

如果不使用 Qt Designer, 而是直接使用代码开发界面,如何设置 Central Widget 和 里面的布局呢?

示例代码如下:

from PySide6 import QtWidgets

class MWindow(QtWidgets.QMainWindow):

    def __init__(self):

        super().__init__()
        self.resize(400, 200)

        # 创建一个 中央控件 Central Widget
        centralWidget = QtWidgets.QWidget(self)
        self.setCentralWidget(centralWidget)

        # 创建 垂直layout,作为 Central Widget 的布局,称之为 主layout
        mainLayout = QtWidgets.QVBoxLayout(centralWidget)

        # 创建 主layout 的 一个 子layout
        topLayout = QtWidgets.QHBoxLayout()    
        mainLayout.addLayout(topLayout)  # 添加 子 Layout   

        # 子layout 添加内部控件
        topLayout.addWidget(QtWidgets.QLabel('姓名'))     
        topLayout.addWidget(QtWidgets.QLineEdit(self))    

        # 主layout 里面 再添加 一个编辑框
        mainLayout.addWidget(QtWidgets.QPlainTextEdit(self))     

app = QtWidgets.QApplication()
window = MWindow()
window.show()
app.exec()

运行结果如下:

layout内 控件的 摆放位置

水平垂直的 layout, 放入控件 通常都是 平均分散在 layout 里面的

from PySide6.QtWidgets import QApplication, QFrame, QHBoxLayout, QLabel

app = QApplication()

frame = QFrame()
frame.setFixedSize(400, 200)
frame.setStyleSheet('''
QFrame { border: 1px solid green; background: white;}
QLabel {background: SkyBlue; }
''')

hl = QHBoxLayout(frame)

label1 = QLabel('1')
label1.setFixedSize(30,30)
hl.addWidget(label1)

label2 = QLabel('2')
label2.setFixedSize(30,30)
hl.addWidget(label2)

frame.show()
app.exec()

运行效果如图

addStretch 左右靠边,居中

如果,我们想让元素尽量都挤在左边, 右边尽量空白, 可以通过layout布局的 addStretch 方法

from PySide6.QtWidgets import QApplication, QFrame, QHBoxLayout, QLabel

app = QApplication()

frame = QFrame()
frame.setFixedSize(400, 200)
frame.setStyleSheet('''
QFrame { border: 1px solid green; background: white;}
QLabel {background: SkyBlue; }
''')

hl = QHBoxLayout(frame)

label1 = QLabel('1')
label1.setFixedSize(30,30)
hl.addWidget(label1)

label2 = QLabel('2')
label2.setFixedSize(30,30)
hl.addWidget(label2)

hl.addStretch() # layout 结尾 `addStretch`

frame.show()
app.exec()

addStretch 其实就是添加了一个 QSpacerItem (也就是Qt设计师界面上的Spacer)

layout 结尾 addStretch , 可以让该layout后面尽量空白占据,实现 内部控件 靠左

运行效果如图


同理, layout 开头 addStretch , 可以让该layout前面尽量空白占据,实现 内部控件 靠右


如果 开头结尾 都 addStretch, 前后都尽量空白占据,就实现了 居中 的效果

from PySide6.QtWidgets import QApplication, QFrame, QHBoxLayout, QLabel

app = QApplication()

frame = QFrame()
frame.setFixedSize(400, 200)
frame.setStyleSheet('''
QFrame { border: 1px solid green; background: white;}
QLabel {background: SkyBlue; }
''')

hl = QHBoxLayout(frame)

hl.addStretch() # layout 开头 `addStretch`

label1 = QLabel('1')
label1.setFixedSize(30,30)
hl.addWidget(label1)

label2 = QLabel('2')
label2.setFixedSize(30,30)
hl.addWidget(label2)

hl.addStretch() # layout 结尾 `addStretch`

frame.show()
app.exec()

alignment 参数 指定位置

可以在调用 Layout 的 addWidget 方法时,通过 alignment 参数,指定控件在layout内部的位置,如下

from PySide6.QtWidgets import QApplication, QFrame, QHBoxLayout, QLabel
from PySide6 import QtGui

app = QApplication()

frame = QFrame()
frame.setFixedSize(400, 200)
frame.setStyleSheet('''
QFrame { border: 1px solid green; background: white;}
QLabel {background: SkyBlue; }
''')

hl = QHBoxLayout(frame)

label1 = QLabel('1')
label1.setFixedSize(30,30)
hl.addWidget(
    label1,
    alignment=QtGui.Qt.AlignLeft|QtGui.Qt.AlignTop) # 左上

label2 = QLabel('2')
label2.setFixedSize(30,30)
hl.addWidget(
    label2,
    alignment=QtGui.Qt.AlignRight|QtGui.Qt.AlignBottom) # 右下

frame.show()
app.exec()

Layout 内边距/间隔

一个Layout缺省是有几个像素的边距的,所以内部的内容不是紧贴着layout边界的。

可以用 Layout 的 setContentsMargins 方法 代码设置 layout对应Widget 的 内部内容的 左/上/右/下 外部边距 ,如下

from PySide6.QtWidgets import QApplication, QFrame, QHBoxLayout, QLabel

app = QApplication()

frame = QFrame()
frame.setFixedSize(400, 200)
frame.setStyleSheet('''
QFrame { border: 1px solid green; background: white;}
QLabel {background: SkyBlue; }
''')

hl = QHBoxLayout(frame)
hl.setContentsMargins(0, 0, 0, 0) # 边距设置为0 

label1 = QLabel('1')
label1.setFixedSize(30,30)
hl.addWidget(label1)

label2 = QLabel('2')
label2.setFixedSize(30,30)
hl.addWidget(label2)

hl.addStretch() # layout 结尾 `addStretch`

frame.show()
app.exec()

由于边距都被设置为0,所以靠左的控件紧贴layout边界了。


但是可以发现元素之间还是有间隔。这个间隔的大小可以通过 layout的 setSpacing 方法设置

from PySide6.QtWidgets import QApplication, QFrame, QHBoxLayout, QLabel

app = QApplication()

frame = QFrame()
frame.setFixedSize(400, 200)
frame.setStyleSheet('''
QFrame { border: 1px solid green; background: white;}
QLabel {background: SkyBlue; }
''')

hl = QHBoxLayout(frame)
hl.setContentsMargins(0, 0, 0, 0) 
hl.setSpacing(0) # 间隔设置为0

label1 = QLabel('1')
label1.setFixedSize(30,30)
hl.addWidget(label1)

label2 = QLabel('2')
label2.setFixedSize(30,30)
hl.addWidget(label2)

hl.addStretch()

frame.show()
app.exec()


子控件如果比较多,可以使用 setSpacing 方法整体设置统一的间隔。

如果其中某个控件后面间隔比较独特,可以使用 addSpacing 方法额外设置间隔,比如

from PySide6.QtWidgets import QApplication, QFrame, QHBoxLayout, QLabel

app = QApplication()

frame = QFrame()
frame.setFixedSize(400, 200)
frame.setStyleSheet('''
QFrame { border: 1px solid green; background: white;}
QLabel {background: SkyBlue; }
''')

hl = QHBoxLayout(frame)
hl.setContentsMargins(0, 0, 0, 0) 
hl.setSpacing(20) # 间隔设置为20

label1 = QLabel('1')
label1.setFixedSize(30,30)
hl.addWidget(label1)

label2 = QLabel('2')
label2.setFixedSize(30,30)
hl.addWidget(label2)

hl.addSpacing(20) # 在原来的间隔基础上再增加20,就是40
label3 = QLabel('3')
label3.setFixedSize(30,30)
hl.addWidget(label3)

hl.addStretch()

frame.show()
app.exec()

Layout 内部元素长度比例

可以 :

  • 通过 layout 的 setStretch 方法设置其 内部控件/内部布局 的长度比例。

  • 通过 layout 的 setMinimumWidth / setMaximumWidth / setMinimumHeight / setMaximumHeight 方法设置其 内部控件/内部布局 的 最小/最大 宽度/高度。

大家可以运行如下代码示例,调整主窗口的宽度,来验证左右两边控件的宽度比例的变化。

from PySide6.QtWidgets import QApplication, QMainWindow,\
    QLabel,QWidget,QHBoxLayout


class Left(QWidget):
    def __init__(self,parent):
        super().__init__(parent)

        # 左边控件设置 最小宽度 和 最大宽度
        self.setMinimumWidth(200) 
        self.setMaximumWidth(300) 

        self.setStyleSheet('''*{
border-right: 1px solid green; /* 绿色边界 */
background-color: #f9fafc;          
}''')

        layout = QHBoxLayout(self)
        layout.setContentsMargins(0,0,0,0)
        layout.addWidget(QLabel('左边'))


class Right(QWidget):
    def __init__(self,parent):
        super().__init__(parent)

        layout = QHBoxLayout(self)
        layout.setContentsMargins(0,0,0,0)
        layout.addWidget(QLabel('右边'))


class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()
        self.resize(500,300)     
        topWidget = QWidget(self)
        self.setCentralWidget(topWidget)

        main_layout  = QHBoxLayout(topWidget)
        main_layout.setSpacing(0)

        # 左边 
        left = Left(topWidget)
        main_layout.addWidget(left)

        # 右边 
        right = Right(topWidget)
        main_layout.addWidget(right)

        # 设置 QHBoxLayout 内部 左右2边 元素宽度比例
        main_layout.setStretch(0,1) # 第1个元素占比 1/4
        main_layout.setStretch(1,3) # 第2个元素占比 3/4


app = QApplication()
mw = MainWindow()
mw.show()
app.exec()


除了上面使用 setStretch 定义layout中控件长度比例,

layout 也可以在添加子控件时,直接指定长度比例,如下

class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()
        self.resize(500,300)     
        topWidget = QWidget(self)
        self.setCentralWidget(topWidget)

        main_layout  = QHBoxLayout(topWidget)
        main_layout.setSpacing(0)

        # 左边 
        left = Left(topWidget)
        main_layout.addWidget(left, 1) # 第1个元素占比 1/4

        # 右边 
        right = Right(topWidget)
        main_layout.addWidget(right,3) # 第2个元素占比 3/4

其它技巧

从一个窗口跳转到另外一个窗口

经常有朋友问我,程序开始的时候显示一个窗口(比如登录窗口),操作后进入到另外一个窗口,怎么做。

方法很简单,主要就是 实例化另外一个窗口,显示新窗口,关闭老窗口。

如下代码所示

from PySide6 import QtWidgets
import sys

class Window2(QtWidgets.QMainWindow):

    def __init__(self):
        super().__init__()
        self.setWindowTitle('窗口2')

        centralWidget = QtWidgets.QWidget()
        self.setCentralWidget(centralWidget)

        button = QtWidgets.QPushButton('按钮2')

        grid = QtWidgets.QGridLayout(centralWidget)
        grid.addWidget(button)


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle('窗口1')

        centralWidget = QtWidgets.QWidget()
        self.setCentralWidget(centralWidget)

        button = QtWidgets.QPushButton('打开新窗口')
        button.clicked.connect(self.open_new_window)

        grid = QtWidgets.QGridLayout(centralWidget)
        grid.addWidget(button)

    def open_new_window(self):
        # 实例化另外一个窗口
        self.window2 = Window2()
        # 显示新窗口
        self.window2.show()
        # 关闭自己
        self.close()


app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()


点击这里下载 一个登录切换到主窗口 的示例代码包


如果经常要在两个窗口来回跳转,可以使用 hide() 方法 隐藏窗口, 而不是 close() 方法关闭窗口。 这样还有一个好处:被隐藏的窗口再次显示时,原来的操作内容还保存着,不会消失。

弹出模式对话框

有的时候,我们需要弹出一个模式对话框输入一些数据,然后回到 原窗口。

所谓模式对话框,就是弹出此对话框后, 原窗口就处于不可操作的状态,只有当模式对话框关闭才能继续。

参考如下代码

from PySide6 import QtWidgets
import sys

class MyDialog(QtWidgets.QDialog):
    def __init__(self):
        super().__init__()
        self.setWindowTitle('模式对话框')

        self.resize(500, 400)
        self.textEdit = QtWidgets.QPlainTextEdit(self)
        self.textEdit.setPlaceholderText("请输入薪资表")
        self.textEdit.move(10, 25)
        self.textEdit.resize(300, 350)

        self.button = QtWidgets.QPushButton('统计', self)
        self.button.move(380, 80)

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle('主窗口')

        centralWidget = QtWidgets.QWidget()
        self.setCentralWidget(centralWidget)

        button = QtWidgets.QPushButton('打开模式对话框')
        button.clicked.connect(self.open_new_window)

        grid = QtWidgets.QGridLayout(centralWidget)
        grid.addWidget(button)

    def open_new_window(self):
        # 实例化一个对话框类
        self.dlg = MyDialog()        
        # 显示对话框,代码阻塞在这里,
        # 等待对话框关闭后,才能继续往后执行
        self.dlg.exec()

app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()