前言

本系列文章为b站PySide6教程以及官方文档的学习笔记

原视频传送门如下

官方文档链接:Qt for Python

常用控件

下拉框(QComboBox)

下拉框在QtDesigner中的控件名为Combo Box

1
2
from PySide6.QtWidgets import QComboBox
cb = QComboBox()

当然一个下拉框只有框架是无法发挥作用的

我们可以通过.addItems的方法通过列表参数向下拉框中添加元素

1
cb.addItems(['1','2','3','4','5'])

效果如下

当然我们还需要将对下拉框的一些操作作为信号,并为其绑定槽

例如currentIndexChanged可以在下拉框选定项目改变时发出信号

highlighted 会于用户在下拉列表中高亮(但未选择)一个项目时发射信号。

当然这些信号也会有返回值传递给槽

例如我们如果想写一个程序在控制台打印当前选中的项目时,有以下两种方法实现

  1. 使用控件的currentText属性获取当前选中项目

    1
    cb.currentIndexChanged.connect(lambda:print(cb.currentText()))
  2. 利用信号的返回的当前选中项目传递给槽

    1
    2
    3
    cb.currentTextChanged.connect(self.print)
    def print(self,choose):
    print(choose)

复选框(QCheckBox)

1
2
from PySide6.QtWidgets import QCheckBox
cb = QCheckBox("我是复选框")

复选框控件有且只有一种信号stateChanged,用于传递复选框选中状态改变的信号

并且该信号会返回一个int类型的参数,选中时返回2,未选择返回0

当然我们也可以通过该控件的isChecked方法来获取当前状态,选中时返回True,未选中返回False

单选框(QRadioButton)

单选框一般需要搭配Group Box来一起使用

当我们将几个单选框加到同一个group后,同组的单选框每次就只能勾选其中的某一个

1
2
3
4
5
6
7
8
9
10
11
12
self.group1 = QButtonGroup(self)
label1 = QLabel("请选择你的CTF方向")
btn1 = QRadioButton("Web")
btn2 = QRadioButton("Crypto")
btn3 = QRadioButton("Pwn")
btn4 = QRadioButton("Misc")
btn5 = QRadioButton("Reverse")
self.group1.addButton(btn1)
self.group1.addButton(btn2)
self.group1.addButton(btn3)
self.group1.addButton(btn4)
self.group1.addButton(btn5)

如果我们想获得此时被选中的单选框,可以通过QButtonGroup的checkedButton来获得

文本框(QTextEdit和QPlainTextEdit)

当我们想要输出大段的文本信息时,考虑使用文本框

在PySide6中,分为富文本框和纯文本框

富文本框中,我们能输出一些特殊格式,例如Markdown、HTML等等

1
2
3
4
text1 = QTextEdit()
text2 = QTextEdit()
text1.setHtml("<h1>我是一级标题</h1><h2>我是二级标题</h2>")
text2.setMarkdown("### 我是三级标题\n\n**我是加粗的文字**\n\n*我是斜体文字*")

效果如下:

纯文本框虽然只能输出纯文本,但是优化做得更好,在载入大批量文本时能更快加载

搭配文本框的方法我们能做到一些特殊效果

例如我们可以使用appendPlainText实现一个程序运行日志的打印效果

滑块(QSlider)

QSlider 用于提供一个滑动条,允许用户通过拖动滑块在一个范围内选择值。它通常用于获取用户输入的数值,特别是当需要从一个预定义范围中选择时

我们可以设置滑块的一些基本属性,像方向、值范围和步进等等

1
2
3
4
5
6
7
# 设置滑条为水平,这里需要从PyQt6.QtCore中导入Qt
self.slider1 = QSlider(Qt.Orientation.Horizontal)
# 设置刻度的位置
self.slider1.setTickPosition(QSlider.TickPosition.TicksBelow)
# 设置最小值和最大值
self.slider1.setMinimum(50)
self.slider1.setMaximum(150)

通常我们会使用滑块的valueChanged信号,来侦测滑块是否滑动

而滑块的值我们可以通过value()方法获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    self.value = QLabel()
self.bind()

mainlayout = QVBoxLayout()
mainlayout.addWidget(self.value)
mainlayout.addWidget(self.slider1)
self.setLayout(mainlayout)

def bind(self):
self.slider1.valueChanged.connect(self.showSlider)

def showSlider(self):
self.value.setText(str(self.slider1.value()))

效果如下:

布局

布局是UI设计中的重要功能

常见布局控件

常见的布局控件有以下几种

  1. QVBoxLayout

    • 垂直布局,小部件会被垂直堆叠。
    • 用法示例:用于将一组控件从上到下垂直排列。
  2. QHBoxLayout

    • 水平布局,小部件会被水平排列。
    • 用法示例:用于将一组控件从左到右水平排列。
  3. QGridLayout

    • 网格布局,小部件按行和列排列,形成一个网格。
    • 用法示例:在需要更精细的控制来放置小部件时使用,如科学应用程序的复杂表单。
  4. QFormLayout

    • 表单布局,专为表单设计,提供了标签和字段的垂直布局。
    • 用法示例:用于快速创建标准表单,如登录界面或设置面板。
  5. QStackedLayout

    • 堆叠布局,小部件堆叠在一起,每次只显示一个。
    • 用法示例:用于创建向导或多步骤界面。

示例

让我们假设一个场景:

我们的应用程序由上半部分的设置区与下半部分的功能区组成

首先我们需要创建布局控件的对象实例

1
2
3
4
5
from PySide6.QtWidgets import QVBoxLayout,QHBoxLayout
...
mainlayout = QVBoxLayout()
settinglayout = QHBoxLayout()
visiblelayout = QHBoxLayout()

我们能往布局控件中放入普通控件,也能在布局控件中嵌套布局控件

在本例中,设置区的水平布局和可视化区的水平布局都被嵌套在最大的垂直布局中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
btn1 = QPushButton("按钮1")
btn2 = QPushButton("按钮2")
lb1 = QLabel("标签1")
lb2 = QLabel("标签2")

mainlayout = QVBoxLayout()
settinglayout = QHBoxLayout()
visiblelayout = QHBoxLayout()

settinglayout.addWidget(btn1)
settinglayout.addWidget(btn2)
visiblelayout.addWidget(lb1)
visiblelayout.addWidget(lb2)
mainlayout.addLayout(settinglayout)
mainlayout.addLayout(visiblelayout)
self.setLayout(mainlayout)

向布局中添加控件使用addWidget,嵌套布局则使用addLayout

效果如下

当然,上述代码可以通过改用QFormLayout来简化

1
2
3
4
formlayout = QFormLayout()
formlayout.addRow(btn1,btn2)
formlayout.addRow(lb1,lb2)
mainlayout.addLayout(formlayout)

在格子布局中,我们可以将整个界面想象成由行列索引的格子组成

向格子布局控件传入的参数有5个

1
addwidget(控件名,行数,列数,长,宽)

下面的代码则是利于格子布局简化的代码

1
2
3
4
5
6
gridlayout = QGridLayout()
gridlayout.addWidget(btn1,0,0)
gridlayout.addWidget(btn2,0,1)
gridlayout.addWidget(lb1,1,0)
gridlayout.addWidget(lb2,1,1)
mainlayout.addLayout(gridlayout)

布局的好处

在 PySide6中使用布局有许多好处,主要包括:

  1. 自动调整大小和位置:布局自动管理窗口组件(widgets)的大小和位置。当用户调整主窗口的大小时,布局确保内部组件适当地调整其大小和位置,使界面保持一致和专业的外观。
  2. 更容易的界面维护:使用布局可以使代码更加清晰和易于维护。你可以更容易地添加、移除或修改组件,而不用担心手动更新每个组件的位置和大小。
  3. 跨平台一致性:布局帮助确保应用程序在不同操作系统和设备上提供一致的用户体验,因为它们会根据不同平台的显示特性(如分辨率和字体大小)自动调整组件。
  4. 适应性和响应性:布局提供了更好的适应性,使得界面能够适应不同的屏幕尺寸和方向,这对于现代多设备应用程序特别重要。

对话框

对话框是一种特殊的窗口,用于短暂的、特定目的的交互,通常在完成其任务后就会被关闭。它们与主应用程序窗口相比较独立,但通常是模态的,即在对话框关闭之前,用户不能与主应用程序的其他部分交互。

消息框(QMessageBox)

QMessageBox是用于显示消息的标准对话框。这些对话框通常用于提供信息、询问问题或显示警告。

  1. 模态性质QMessageBox 是模态对话框,这意味着当它打开时,它会阻止用户与程序的其他部分交互,直到对话框被关闭。
  2. 标准按钮:它提供了一组标准按钮(如 Ok, Cancel, Yes, No 等),可以根据需要选择哪些按钮出现在对话框中。
  3. 图标显示QMessageBox 可以显示不同类型的图标(如信息、警告、错误、问号),这有助于向用户传达对话框的性质或重要性。
  4. 简单的文本和格式化文本:它可以显示简单的文本信息或更复杂的格式化文本,如富文本。
  5. 返回值:用户对 QMessageBox 的响应(如点击哪个按钮)可以通过对话框的返回值来捕获,这对于程序的决策流程非常重要。
1
from PySide6.QtWidgets import QMessageBox

不同的消息框显示效果如下

1
QMessageBox.information(self, "标题", "这是一个提示")
1
QMessageBox.warning(self, "标题", "这是一个警告")
1
QMessageBox.critical(self, "标题", "这是一个错误")
1
QMessageBox.question(self, "标题", "这是一个提问")
1
QMessageBox.about(self, "标题", "这是一个关于")

第四个和第五个参数用于设置可供用户点击的选项以及默认选项

1
QMessageBox.information(self, "标题", "这是一个提示", QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes)

我们可以将消息框赋值给一个变量,这样我们就能从返回值中得知用户的选择

1
2
3
answer = QMessageBox.information(self, "标题", "这是一个提示", QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes)
if answer == QMessageBox.Yes:
print("yes")

输入对话框(QInputDialog)

QInputDialog提供了一个简单方便的方式来获取用户输入的单个值。这些值可以是字符串、数字或列表中的选项。

1
2
3
4
5
6
from PySide6.QtWidgets import QInputDialog

# 获取用户输入的字符串
text, ok = QInputDialog.getText(None, "输入对话框", "请输入文本:")
if ok and text:
print("用户输入:", text)

使用这个对话框可以方便地从用户那里获取输入,而不必创建复杂的表单。

当且仅当用户点击OK时,返回的第二个参数为True

当然,除了让用户输入文本,我们也可以让用户输入数字或列表中的选项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
    btn1 = QPushButton('获取一个整形数字')
btn1.clicked.connect(self.getIntDialog)

btn2 = QPushButton('获取一个浮点数字')
btn2.clicked.connect(self.getFloatDialog)

btn3 = QPushButton('获取一个Item')
btn3.clicked.connect(self.getItemDialog)

mainlayout = QVBoxLayout()
mainlayout.addWidget(btn1)
mainlayout.addWidget(btn2)
mainlayout.addWidget(btn3)
self.setLayout(mainlayout)

def getIntDialog(self):
num,ok = QInputDialog.getInt(self,"获取一个整形数字","请输入一个整数")
if ok:
print(num)
else:
print("用户取消了")

def getFloatDialog(self):
num,ok = QInputDialog.getDouble(self,"获取一个浮点数字","请输入一个浮点数")
if ok:
print(num)
else:
print("用户取消了")

def getItemDialog(self):
items = ["C","C++","Python","Java","Rust"]
item,ok = QInputDialog.getItem(self,"获取一个Item","请选择一个编程语言",items)
if ok:
print(item)
else:
print("用户取消了")

当然设置输入框时,我们也能限制输入范围和设置默认值

1
num,ok = QInputDialog.getInt(self,"获取一个整形数字","请输入一个整数",20,0,100,10)

此时我们设置输入框的选择范围为0-100,步长为10,默认值为20

文件对话框(QFileDialog)

QFileDialog允许用户选择文件或文件夹,这在需要打开或保存文件时非常有用。

文件对话框支持过滤文件类型,使用户能够更容易地找到或保存特定类型的文件。

1
2
3
btn = QPushButton('上传文件')
btn.clicked.connect(lambda:print(QFileDialog.getOpenFileName(self,'标题','.','py文件(*.py)')))

我们为按钮绑定上文件对话框,点击后会弹出如下窗口

由于我们传入的第三个参数为.,文件对话框一开始所在的目录即为当前目录

同时第四个参数括号中的类型限制了我们能选择的文件名和文件类型*.py,这里*为通配符

返回值有两个,分别是选中文件的绝对路径选择的文件过滤规则

我们能设置多种文件过滤规则,只需在中间用;;隔开

例如

1
btn.clicked.connect(lambda:print(QFileDialog.getOpenFileName(self,'标题','.','py文件(*.py);;png文件(*.png);;ui文件(*.ui)')))

当然除了打开单个文件,QFileDialog还支持打开多个文件、打开文件夹等操作

方法 功能 参数 返回值
getOpenFileNames 打开多个文件 self,窗体名称,对话框默认打开目录.文件过滤规则 文件路径(绝对)[列表],选择的过滤规则
getExistingDirectory 打开一个文件夹 self,窗体名称,对话框默认打开目录. 文件夹路径(绝对)
getSaveFileName 保存一个文件 self,窗体名称,对话框默认打开目录.保存文件类型 文件路径(绝对),选择的文件类型

字体对话框(QFontDialog)

1
2
3
4
5
6
7
8
9
from PySide6.QtWidgets import QFontDialog
self.edit = QTextEdit()
btn = QPushButton('点我选择字体')
btn.clicked.connect(self.changeFont)

def changeFont(self):
ok,font = QFontDialog.getFont()
if not ok: return
self.edit.setCurrentFont(font)

该对话框可供用户选择字体

窗口效果如下

返回值中的第二个参数font可以直接用于设置字体

颜色对话框(QColorDialog)

相比于字体对话框,颜色对话框只会有一个返回值,即color

1
color = QColorDialog.getColor()

小项目:图像处理

我们来做一个基于刚学的对话框、下拉框、滑块等控件的图像处理小程序

业务逻辑如下

  1. 上传图片:用户可以通过点击“上传图片”按钮来选择并上传一张图片。

    这里需要用到QFileDialog,并设置文件过滤规则为图片格式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    self.uploadButton = QPushButton("上传图片")
    self.uploadButton.clicked.connect(self.uploadImage)
    layout.addWidget(self.uploadButton)
    ...
    def uploadImage(self):
    filePath, _ = QFileDialog.getOpenFileName(self, "选择图片", "", "Image files (*.jpg *.png)")
    if filePath:
    self.currentImage = Image.open(filePath)
    self.applyEffect()
  2. 选择处理效果:用户可以从下拉框中选择想要应用的图像处理效果:去噪、模糊或锐化。

    当有需要用户做出选择的场景时,下拉框是不错的选择

    1
    2
    3
    self.effectComboBox = QComboBox()
    self.effectComboBox.addItems(["去噪", "模糊", "锐化"])
    layout.addWidget(self.effectComboBox)
  3. 调整效果强度:通过滑动条,用户可以调整所选图像处理效果的强度。

    在实际应用场景时,滑动条的上下范围往往受到实际功能的限制

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    # 滑动条
    self.slider = QSlider(Qt.Horizontal)
    self.slider.setMinimum(1)
    self.slider.setMaximum(10)
    self.slider.setTickInterval(1)
    self.slider.setTickPosition(QSlider.TicksBelow)
    self.slider.valueChanged.connect(self.applyEffect)
    layout.addWidget(self.slider)
    ...
    def applyEffect(self):
    if self.currentImage:
    effect = self.effectComboBox.currentText()
    value = self.slider.value()

    # 对于去噪和锐化,确保滤波器大小是奇数且大于等于3
    filter_size = max(3, 2 * value + 1) if effect != "模糊" else value

    if effect == "去噪":
    self.processedImage = self.currentImage.filter(ImageFilter.MedianFilter(filter_size))
    elif effect == "模糊":
    self.processedImage = self.currentImage.filter(ImageFilter.GaussianBlur(filter_size))
    elif effect == "锐化":
    self.processedImage = self.currentImage.filter(ImageFilter.UnsharpMask(radius=filter_size, percent=150))
    self.displayImage()

    在本例中,滑条的值传给滤波器,在传入之前也需要做一些处理

  4. 实时预览:应用所选效果后,处理后的图像将实时显示在界面上。

    我们直接将处理后的图片设置到label上即可

    1
    2
    3
    4
    5
    self.imageLabel = QLabel("这里显示处理后的图像")
    layout.addWidget(self.imageLabel)
    ...
    def displayImage(self):
    self.imageLabel.setPixmap(self.processedImage.toqpixmap())

最终应用程序的效果如下

子窗口/多窗口

子窗口(Child Windows)

子窗口是指附属于主窗口的窗口。它们通常用于显示应用程序的次要功能或额外信息。子窗口的一些特点包括:

  1. 依赖性:子窗口依赖于父窗口。当父窗口被关闭时,子窗口也会随之关闭。
  2. 空间范围:子窗口通常在父窗口的内部空间中显示,但也可以浮动或被拖出父窗口。
  3. 功能性:子窗口通常用于与主窗口内容相关的特定功能,如设置面板、帮助文档、附加工具等。
  4. 生命周期管理:子窗口的生命周期通常由父窗口管理。

多窗口(Multiple Windows)

多窗口指的是独立的、可以同时运行的多个窗口。在多窗口应用中,每个窗口都可以独立执行操作,而不一定要依赖于主窗口。多窗口的特点包括:

  1. 独立性:每个窗口都是独立的实例,可以单独打开、关闭,而不影响其他窗口。
  2. 功能分离:每个窗口可以承担不同的任务,功能之间的独立性较强。
  3. 用户交互:用户可以同时与多个窗口交互,窗口之间可能有或没有数据和事件的交互。
  4. 生命周期管理:每个窗口可能有自己的生命周期管理,关闭一个窗口不会影响其他窗口。

创建子窗口

与主窗口一样,子窗口需要我们再继承一个窗口的类

而在父窗口的部分实例化子窗口的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MyWindow(QWidget):
def __init__(self):
super().__init__()

self.lb = QLabel('这个是主窗口')

self.subWindow = SubWindow()
self.subWindow.show()

self.mainlayout = QVBoxLayout()
self.mainlayout.addWidget(self.lb)
self.setLayout(self.mainlayout)

class SubWindow(QWidget):
def __init__(self):
super().__init__()

self.lb = QLabel('这个是子窗口')

self.mainlayout = QVBoxLayout()
self.mainlayout.addWidget(self.lb)
self.setLayout(self.mainlayout)

实例化子窗口后,我们通过show()方法来展示

当在主窗口中定义子窗口的实例时,一定要将其定义为self的变量,否则它将会在展示的一瞬间被垃圾回收机制给回收

窗口的开闭

在 PySide6 中,show(), close(), 和 hide() 是窗口对象常用的几个方法,用于控制窗口的可见性和生命周期。

  1. show()

    • 用法show() 方法用于显示窗口。如果一个窗口是新创建的或者之前被隐藏了,使用 show() 可以使其变为可见状态。
    • 场景:这个方法通常在窗口首次初始化之后调用,或者在窗口被隐藏之后需要再次显示时调用。
  2. close()

    • 用法close() 方法用于关闭窗口。这不仅会隐藏窗口,而且会触发窗口的关闭事件(closeEvent),可以在其中执行一些清理操作。
    • 场景:当你想要结束窗口的生命周期时,例如用户完成了操作,或者不再需要显示该窗口时,可以调用 close()。对于子窗口,close() 会使其从父窗口的子窗口列表中移除。
    • 注意:关闭主窗口通常会导致整个应用程序退出。
  3. hide()

    • 用法hide() 方法用于临时隐藏窗口,而不是关闭。窗口仍然存在于内存中,但用户界面上不可见。
    • 场景:当你想临时从视图中移除窗口,但可能稍后需要重新显示它时,使用 hide() 是合适的。这对于不想终止窗口实例,而只是暂时不显示它的情况很有用。

示例

我们将关闭、显示、隐藏子窗口的按钮集成到主窗口中,方便进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class MyWindow(QWidget):
def __init__(self):
super().__init__()

self.lb = QLabel('这个是主窗口')

self.subWindow = SubWindow()
self.subWindow.show()

self.btnClose = QPushButton('关闭子窗口')
self.btnClose.clicked.connect(lambda: self.subWindow.close())

self.btnShow = QPushButton('显示子窗口')
self.btnShow.clicked.connect(lambda: self.subWindow.show())

self.btnHide = QPushButton('隐藏子窗口')
self.btnHide.clicked.connect(lambda: self.subWindow.hide())

self.mainlayout = QVBoxLayout()
self.mainlayout.addWidget(self.lb)
self.mainlayout.addWidget(self.btnClose)
self.mainlayout.addWidget(self.btnShow)
self.mainlayout.addWidget(self.btnHide)
self.setLayout(self.mainlayout)
class SubWindow(QWidget):
def __init__(self):
super().__init__()

self.lb = QLabel('这个是子窗口')
self.lineEdit = QTextEdit()
self.lineEdit.setText('这是子窗口的文本框')

self.mainlayout = QVBoxLayout()
self.mainlayout.addWidget(self.lb)
self.mainlayout.addWidget(self.lineEdit)
self.setLayout(self.mainlayout)

下面是测试效果

可以发现,当我们关闭子窗口并重新展示后,子窗口会回到一个固定的地方,这说明原来的子窗口被彻底清除,重新创建了一个子窗口;而当我们将子窗口隐藏后重新展示时,子窗口仍然在隐藏前的地方。

自定义信号

通常自定义一个信号时共有以下三个步骤

  1. 定义信号名以及传参类型

    1
    2
    From Pyside6.QtCore import Signal
    signalname = Signal(object)

    这里的参数类型除了object对象,还可以是str、int等等