Python 库 PySide/PyQt 基础操作,参考视频

# 环境搭建

# pyside6 本体安装

使用 pip 安装 pyside6 库

pip install pyside6

# Qtdesigner

pyside6 库安装完毕后,进入库目录下测试能否打开 designer.exe 。如果你使用的是虚拟环境,那么安装完成后, pyside6 位于 项目名 \env\Lib\site-packages\PySide6 ,在该目录下能找到 designer.exe

# Zeal

Zeal 是一个文档查阅工具,主要用于代码查阅,在 官网 中选择 Windows 下的 Installer (MSI),下载 64-bit MSI
在编写代码时,如果不知道某个控件的信号有哪些,可以在 Zeal 中输入控件名称,在内容中找到 signals 查看。

# 修改存储路径

安装完毕后,打开 Zeal,点击 Edit>>Prefrences。这里是 Zeal 文档下载后默认保存路径,点击 Browse…, 修改默认路径,避免后面文档无下载权限,最后点击 Apply。

# 下载文档

点击 Tools>>Docsets。此处的 Installed 是已经下载的文档,点击 Available 可以下载文档(需要魔法),点击 Download 即可。

# 常见错误

# Connection timed out

连接超时,建议更换时间点下载文档,或者使用魔法,或者去找文档资源手动导入文档。

# Docset storage is read only

出现显示 Docset 存储是只读的情况,修改默认文档路径即可。

# Content rendering error

内容呈现错误,需要删除一个 js 文件。

(1) 找到你阅读文档所在目录,即设置的存储路径,然后找到 js 文件所在目录。例如 HTML 的文档 js 位于
\ 设置的存储路径 \HTML.docset\Contents\Resources\Documents\developer.mozilla.org\static\build\js

(2) 删除 "react-main.01db16f317c6.js" 文件,主要是删除 react-main…js,这串字符不同环境下可能不一样。

(3) 重启 Zeal 即可阅读。

# VScode 插件

在 VScode 中下载插件 PYQT Integration

# 扩展设置

在插件搜索界面,点击 PYQT Integration 插件右下角的设置 -> 扩展设置

# Qtdesigner

找到 Pyqt-integration › Qtdesigner: Path ,一般位于最后一项。将 Qtdesigner 的路径 复制到此处,以 designer.exe 结尾。

# Pyrcc

找到 Pyqt-integration › Pyrcc: Cmd ,一般位于第五项。将 Pyrcc 的路径复制在此处,如果你是虚拟环境,那么路径一般位于 项目名 \env\Scripts\pyside6-rcc.exe

# Pyuic

找到 Pyqt-integration › Pyuic: Cmd ,一般位于倒数第四项,将 Pyuic 的路径复制在此处,如果你是虚拟环境,那么路径一般位于 项目名 \env\Scripts\pyside6-uic.exe

# 测试

拓展设置完毕后,点击资源管理器,右键资源管理器目录的空白处,此时能在选项卡的最后一项看到 PYQT:New Form , 点击可以打开 Qtdesigner

# 基础 框架 & 控件

# import

在创建一个新的 py 文件后,导入相关库,显示一个简单的 UI。

from PySide6.QtWidgets import QApplication, QMainWindow
class MyWindow (QMainWindow):
    def __init__(self):
        super ().__init__()
        # 可选,设置窗口大小
        self.resize (800, 600)
if __name__ == '__main__':
    app = QApplication ([])
    window = MyWindow ()
    window.show ()
    app.exec ()

# QPushButton 控件

基础的按钮控件,需要导入 QPushButton ,传递两个参数,前面是按钮名字,如果没有任何布局,后面的参数一定要传递 self ,不然不会显示在窗口上。

class MyWindow (QMainWindow):
    def __init__(self):
        super ().__init__()
        btn = QPushButton (' 按钮 ',self)

# 属性:geometry

几何属性 geometry ,决定 button 的位置 (x,y) 以及宽高。格式为 btn.setGeometry (x,y, 宽,高),例如设置一个在 (70,50) 上宽 40 高 30 的按钮

class MyWindow (QMainWindow):
    def __init__(self):
        super ().__init__()
        btn = QPushButton (' 按钮 ',self)
        btn.setGeometry (70,50,40,30)

# 属性:ToolTip

提示属性,将鼠标放到按钮出,会出现一个小的悬浮框。

btn.setToolTip (' 点我 ')

# 属性:Text

文本属性,该属性确定了按钮显示的文本,如果在此前的 QPushButton 中设置过文本,那么该属性会覆盖之前的文本内容。

class MyWindow (QMainWindow):
    def __init__(self):
        super ().__init__()
        btn = QPushButton (' 按钮 ',self)
        btn.setGeometry (70,50,40,30)
        btn.setToolTip (' 悬浮框 ')
        btn.setText (' 确认 ')

# 属性:Checkable

在 Qtdesigner 中,有一个属性名为 Checkable ,属性可以让 button 一直处于选中状态,设置属性的方式为 btn.set + 属性名

class MyWindow (QMainWindow):
    def __init__(self):
        super ().__init__()
        btn = QPushButton (' 按钮 ',self)
        btn.setGeometry (70,50,40,30)
        btn.setCheckable (1)

# 其余属性

在 Qtdesigner 中可以查看到控件的所有属性,如需设置某属性,使用 btn.set + 属性名 即可。

# QLabel 控件

基础的标签控件,需要导入 QLabel ,传递两个参数,前面是标签内容,如果没有任何布局,后面的参数一定要传递 self ,不然不会显示在窗口上。

class MyWindow (QMainWindow):
    def __init__(self):
        super ().__init__()
        lb = QLabel (' 标签 ',self)

# 属性:textFormat

QLabel 标签有许多与 QPushbutton 相同的属性,同时 Qlabel 还有一些特有属性,例如 textFormat ,一般特有属性会在属性菜单最下方。
textFormat 规定了文本格式化的方式,有四个选项

  • 自动: Auto Text
  • 纯文档格式:Plain Text
  • 富文档格式: Rich Text
  • markdown 格式: Markdown Text

# 属性:Alignment

对齐方式,需要导入库 from PySide6.QtCore import Qt ,之后设置对齐方式,需要有布局才能看到效果。

lb.setAlignment (Qt.AlignmentFlag.AlignCenter)

# QLineEdit 控件

基础的输入框控件,需要导入 QLineEdit ,传递两个参数,前面是输入框默认的内容,可以为空,如果没有任何布局,后面的参数一定要传递 self ,不然不会显示在窗口上。
拖动控件实现简单的图形化编辑,使用 ctrl + R 预览

line = QLineEdit ('abc',self)

# 常用信号

QLineEdit 有以下信号

信号描述
cursorPositionChanged (int oldPos, int newPos)当光标位置在编辑过程中改变时触发。这个信号携带两个参数:旧光标位置和新光标位置。
editingFinished ()当编辑操作完成时触发,这通常发生在用户按下回车键或关闭窗口时。这个信号没有参数。
inputRejected ()当用户在编辑过程中取消或拒绝输入时触发。这个信号没有参数。
returnPressed ()当用户在编辑框中按下回车键时触发。这个信号没有参数。
selectionChanged ()当选中的文本内容改变时触发。这个信号没有参数。
textChanged (const QString &text)当文本内容改变时触发。这个信号携带一个参数,即新的文本内容。
textEdited (const QString &text)当文本内容被编辑时触发,这包括剪切、粘贴、删除等操作。这个信号携带一个参数,即新的文本内容。

# 属性:PlaceholderText

该属性会在输入框中没有内容时,显示灰色的文本作为提示内容

line.setPlaceholderText (' 请输入内容 ')

# PyQt

[安装插件](#vscode - 插件) 后,右键 VScode 资源管理器空白处,在弹出的选项卡最下方可以看到 PYQT:New Form , 打开后在新建菜单栏选择 Widget,即可新建一个 Form。

# 基础控件

# Layouts

布局控件,用于规定其余控件的布局,框选需要布局的控件后,可选择水平 / 垂直等方式,设置 Layouts 后,控件会随着界面的放大缩小一起变化。

# Buttons

按钮控件栏,包含一般的常用按钮:

  • Push Button 基础按钮
  • Tool Button 工具按钮
  • Radio Button 独选按钮
  • Check Button 勾选按钮
  • Command Button 跳转按钮

# Input Widgets

输入框控件,包含多种输入框或选择框:

  • Combo Box 下拉组合框
  • Line Edit 输入框
  • Text Edit 富文本框
  • Plain Text Edit 纯文本框
  • Spin Box 旋转框

# Display Widgets

展示框控件,包含多种输出类型控件:

  • Label 标签
  • Text Browser 文本浏览器

# 界面和代码转换

在 PyQt 中设计好界面后,将文件保存为.ui 文件,接下来需要将.ui 文件转为 python 可用的.py 文件并且调用,提供两种转换方式。

# 命令行转换

使用 pyside6-uic 进行转换,假设虚拟环境位于 项目名 env\Scripts 下,在该目录下打开 cmd,将 项目名 \QT\ 目录下的 counter.ui 文件转换为 Py 文件并放到 项目名 \ 目录下。

D:\ 项目名 \env\Scripts>pyside6-uic ../../QT/counter.ui -o ../../counter.py

# VScode 插件转换

在 VS code 资源管理器中,右键点击需要转换的 ui 文件,选择最后一项 PYTQ:Compile Form 即可将 ui 转换为 py 文件。

# 调用 ui

使用静态编译文件,提供两种方法,一种是单个类继承,另一种是多类一起继承。

# 单继承

在将 ui 文件转换为 py 代码后,存放在项目跟目录下,只需调用 py 文件的 Ui_Form 类即可,同时使用一个参数将 Ui_Form 接收。

注意:此处需要 class 参数和 ui 创建时选择的窗体类型一致,例如创建 ui 时选择 Widget,此时需要写 class MyWindow (QWidget) 而不是 class MyWindow (QMainWindow)

from PySide6.QtWidgets import QApplication, QMainWindow, QWidget
from counter import Ui_Form
class MyWindow (QWidget):
    def __init__(self):
        super ().__init__()
        self.ui = Ui_Form ()
        self.ui.setupUi (self)
if __name__ == '__main__':
    app = QApplication ([])
    window = MyWindow ()
    window.show ()
    app.exec ()

# 多继承

在 python 中,可继承多个类,于使将代码中 Class 同时继承窗口类型和自定义的 Ui_Form 类,简化代码。

from PySide6.QtWidgets import QApplication, QMainWindow, QWidget
from counter import Ui_Form
class MyWindow (QWidget, Ui_Form):
    def __init__(self):
        super ().__init__()
        self.setupUi (self)
if __name__ == '__main__':
    app = QApplication ([])
    window = MyWindow ()
    window.show ()
    app.exec ()

# 三种主要窗体

在 PyQt 中有三种窗体类型,QMainWindow,QWidget,QDialog。

# QWidget

QWidget 类是所有用户界面对象的基类。
窗口部件是用户界面的一个原子:它从窗口系统接收鼠标、键盘和其它事件,并且将自己的表现形式绘制在屏幕上。每一个窗口部件都是矩形,并且它们按 Z 轴顺序排列。一个窗口部件可以被它的副窗口部件或者它前面的窗口部件盖住一部分。
QWidget 有很多成员函数,但是它们中的一些有少量的直接功能:例如,QWidget 有字体属性,但是自己从来不用。为很多继承它的子类提供了实际的功能,比如 QLabel、QPushButton、QCheckBox 等等。
没有父窗体的小部件始终是一个独立的窗口(顶级窗口部件)。非窗口的小部件为子部件,它们在父窗口中显示。Qt 中大多数部件主要被用作子部件。例如:可以显示一个按钮作为顶层窗口,但大多数人更喜欢将按钮内置于其它部件,如 QDialog。

# QMainWindow

QMainWindow 类提供一个有菜单条、工具栏、状态条的主应用程序窗口(例如:开发 Qt 常用的 IDE-Visual Studio、Qt Creator 等)。
一个主窗口提供了构建应用程序的用户界面框架。Qt 拥有 QMainWindow 及其相关类来管理主窗口。
QMainWindow 拥有自己的布局,我们可以使用 QMenuBar(菜单栏)、QToolBar(工具栏)、QStatusBar(状态栏)以及 QDockWidget(悬浮窗体),布局有一个可由任何种类小窗口所占据的中心区域。

# QDialog

QDialog 类是对话框窗口的基类。
对话框窗口是一个顶级窗体,主要用于短期任务以及和用户进行简要通讯。QDialog 可以是模式的也可以是非模式的。QDialog 支持扩展性并且可以提供返回值。它们可以有默认按钮。QDialog 也可以有一个 QSizeGrip 在它的右下角,使用 setSizeGripEnabled ()。
注意:QDialog(以及其它使用 Qt::Dialog 类型的 widget)使用父窗口部件的方法和 Qt 中其它类稍微不同。对话框总是顶级窗口部件,但是如果它有一个父对象,它的默认位置就是父对象的中间。它也将和父对象共享工具条条目。

# 使用原则

* 如果需要嵌入到其他窗体中,则基于 QWidget 创建。
* 如果是主窗体,则基于 QMainWindow 创建。
* 如果是顶级对话框,则基于 QDialog 创建。

# 信号与槽

信号(signal)与槽(slot)是 Qt 的核心机制,PyQt 中每一个 QObject 对象(包括各种窗口和控件)都支持信号与槽机制,通过信号与槽之间的关联以实现对象之间的通信,当信号发射时,连接的槽函数就自动执行,信号与槽通过对象的 signal.connect () 连接的。
PyQt5 使用信号与槽的主要特点:
* 一个信号可以使用多个槽
* 一个槽可以监听多个信号
* 信号与信号之间可以互联
* 信号与槽之间的连接可以跨线程
* 信号与槽的连接方式既可以是同步也可以是异步
* 信号的参数可以是任何 Python 类型

# 基础使用方法

示例:在 Qt Designer 中设计一个简易的登录框,静态编译后在 python 中导入。
当出现点击事件时,获取用户名和密码的信号,调用自定义的函数判断账号密码是否正确,使用弹窗回显信息。
此示例中,函数使用 self. 属性名.text () 方式获取信号,此处的属性名在 Qt Designer 中点击对应的控件后在右侧属性栏可以看到。

from PySide6.QtWidgets import QApplication, QWidget,QMessageBox
from QT.Ui_login import Ui_Form
class MyWindow (QWidget,Ui_Form):
    def __init__(self):
        super ().__init__()
        # 绑定
        self.setupUi (self)
        # 获取按钮 Clicker 事件后执行 loginFuc 函数
        self.pushButton.clicked.connect (self.loginFuc)
    
    def loginFuc (self):
        # 获取账号和密码的信号
        account = self.lineEdit.text ()
        passwd = self.lineEdit_2.text ()
        if account == 'root' and passwd == '123456':
            self.showMsg (' 密码正确 ')
        else:
            self.showMsg (' 密码错误 ')
    # 弹窗
    def showMsg (self,msg):
        QMessageBox.information (self,' 提示 ',msg)
if __name__ == '__main__':
    app = QApplication ([])
    window = MyWindow ()
    window.show ()
    app.exec ()

# 向槽函数中传递多个参数

在 PyQt 中,使用 connect () 方法连接一个信号到一个槽函数时,只能传递一个函数或者一个可调用的对象作为参数。然而,我们经常会遇到需要多参数传递的情况。

# lambda

例如设置按钮 1 绑定槽函数 on_button,假设定义 on_button 接收两个参数,则可以使用 lambda 方式传参,使用 lambda 可以传递任何其他东西 --- 甚至是按钮组件本身(假如,槽打算把传递信号的按钮修改为不可用)

button1.clicked.connect (lambda: self.on_button (name,type))
def on_button (self, name, type):
    pass

# partial

使用 functools 里的 partial 函数

button1.clicked.connect (partial (self.on_button, 1, 2))

# 在 Qt designer 中绑定信号与槽

除了使用代码绑定信号与槽,Qt designer 中也能绑定信号与槽,不过功能较少,一般都需要通过代码绑定。
在 Qt designer 中拖入两个控件,例如拖入一个滑块 Horizontal Slider 和一个标签 Lael,之后点击左上角功能区的绑定信号与槽(一般是 “设置” 右下方的图标),选中滑条并拖动到 Label 上完成绑定,此时在 配置连接 界面中,在配置界面选中 显示从 QWidget 继承的信号与槽,选中滑条的信号 sliderMoved,再选中 Label 的槽 setNum 即可完成绑定。此时按下 ctrl + r,拖动滑条即可看到效果。
除了拖动两个控件绑定,还可以只选中一个控件并且拖动到窗口空白区,此时与窗口绑定,例如事件:点击按钮后关闭窗口。

# 常用控件

# QCombo Box

QCombo Box 是一个下拉选择框,使用该控件规定用户只能选择下拉菜单中的某一个选项。
使用示例如下,示例中使用 QVBoxLayout 做布局,在用户切换选择时,会弹出用户切换后的选项。

from PySide6.QtWidgets import QApplication, QWidget,QComboBox,QVBoxLayout,QMessageBox
# 登录框
class MainWindow (QWidget):
    def __init__(self):
        super ().__init__()
        
        combo = QComboBox ()
        combo.addItems (['a','b','c'])
        combo.currentTextChanged.connect (lambda: self.showMsg (combo.currentText ()))
        mainlayout = QVBoxLayout ()
        mainlayout.addWidget (combo)
        self.setLayout (mainlayout)
    def showMsg (self,msg):
        QMessageBox.information (self,' 提示 ',msg)

# QCheck Box

QCheck Box 是一个选择框,该控件只有一个信号 (signal),即在用户改变其状态 (选中,取消选中) 时。
stateChanged 会自带传递一个 int 参数,当选择框由未勾选变为勾选时传递参数 2,反之传递 0。
使用 isChecked (),可以获取到选中框的状态,返回 bool 类型的值。

class MainWindow (QWidget):
    def __init__(self):
        super ().__init__()
        
        check = QCheckBox (' 选中 ')
        check.stateChanged.connect (self.showMsg)
        # isChecked () 方法获取选中状态
        btn = QPushButton (' 获取状态 ')
        btn.clicked.connect (lambda:print (check.isChecked ()))
        mainlayout = QVBoxLayout ()
        mainlayout.addWidget (check)
        mainlayout.addWidget (btn)
        self.setLayout (mainlayout)
    # 获取选中框在改变时的返回值,值为 0 或 2
    def showMsg (self,msg):
        print (msg)

# QRadioBox

# QGroupBox

Radio button 是一个选中按钮,当存在多个选择按钮时,只能选择一个,但是有时,我们需要选中多个按钮,例如我们需要选择性别,然后选择国家。此时就需要用到 Group Box,位于同一个 Group Box 的多个按钮,每次只能选中一个。

# QButtonGroup

QGroupBox 可以在 Qt designer 中直接设计,但是 QButtonGroup 则只能能用代码的形式进行构建。QButtonGroup 与 QGroupBox 实现的功能相同,创建一个组,将按钮放置在组中。
这里给出一个简单的代码示例,UI 界面有三行,第一行有三个按钮绑定在 QButtonGroup1 中,第二行的两个按钮绑定在 QButtonGroup2 中,第三行的 Label 接收到按钮变化时实时更新。

from PySide6.QtWidgets import QApplication, QWidget,QButtonGroup,QLabel,QRadioButton,QHBoxLayout,QVBoxLayout
# 登录框
class MyWindow (QWidget):
    def __init__(self):
        super ().__init__()
        # 设置按钮
        self.group1 = QButtonGroup (self)
        label1 = QLabel (' 选中你的语言 ')
        btn1 = QRadioButton ('python')
        btn2 = QRadioButton ('java')
        btn3 = QRadioButton ('c++')
        # 将按钮加入 QButtonGroup
        self.group1.addButton (btn1)
        self.group1.addButton (btn2)
        self.group1.addButton (btn3)
        # 绑定信号
        btn1.toggled.connect (self.change_text)
        btn2.toggled.connect (self.change_text)
        btn3.toggled.connect (self.change_text)
        self.group2 = QButtonGroup (self)
        label2 = QLabel (' 请选择你平均写代码时间 ')
        btn4 = QRadioButton ('1 hour')
        btn5 = QRadioButton ('2 hour')
        self.group2.addButton (btn4)
        self.group2.addButton (btn5)
        btn4.toggled.connect (self.change_text)
        btn5.toggled.connect (self.change_text)
        self.label_show = QLabel (' 请选择编程语言和时间 ')
        # 设置水平布局
        h1 = QHBoxLayout ()
        h1.addWidget (label1)
        h1.addWidget (btn1)
        h1.addWidget (btn2)
        h1.addWidget (btn3)
        # 设置第二行水平布局
        h2 = QHBoxLayout ()
        h2.addWidget (label2)
        h2.addWidget (btn4)
        h2.addWidget (btn5)
        # 设置垂直布局
        mainLayout = QVBoxLayout ()
        mainLayout.addLayout (h1)
        mainLayout.addLayout (h2)
        mainLayout.addWidget (self.label_show)
        self.setLayout (mainLayout)
    def change_text (self):
        language = self.group1.checkedButton () # 返回按钮组 1 中被选中的按钮
        time = self.group2.checkedButton () # 返回按钮组 2 中被选中的按钮
        if language is not None and time is not None:
            self.label_show.setText (
                f' 你的编程语言是:{language.text ()} \t 你平均敲代码时间是:{time.text ()}'
            )
if __name__ == '__main__':
    app = QApplication ([])
    window = MyWindow ()
    window.show ()
    app.exec ()

# QTextEdit & QPlanTextEdit

QTextEdit 为富文本框,QPlanTextEdit 为纯文本输入框。
基础属性:setText,setMarkdown,setHtml
基本信号:textchange

textEdit = QPlainTextEdit ()
textEdit.setPlainText ('title')
textEdit.appendPlainText ('content')
btn = QPushButton ('add')
btn.clicked.connect (lambda: textEdit.appendPlainText (' 追加 '))
self.mainLayout = QVBoxLayout ()
self.mainLayout.addWidget (textEdit)
self.mainLayout.addWidget (btn)
self.setLayout (self.mainLayout)

QTextEdit 设置 markdown ,需要注意缩进格式,确保 markdown 能正常显示

class MyWindow (QWidget):
    def __init__(self):
        super ().__init__()
        self.resize (300, 200)
        self.gridlayout = QGridLayout ()
        self.richtext = QTextEdit ()
        self.richtext.setPlaceholderText (' 请输入内容 ')
        MarkdownStr = """
# title1
`支持代码格式`
# title2
- 1
- 2
- 3
<table>
  <tr>
    <th > 方法 </th>
    <th > 说明 </th>
  </tr>
  <tr>
    <td rowspan="5">addWidget (Widget,row,col,alignment)</td>
    <td>
    给网格布局添加部件,设置指定的行和列,起始位置的默认值为(0,0)
    </td>
  </tr>
  <tr>
    <td>widget:所添加的控件 </td>
  </tr>
  <tr>
    <td>row:控件的行数,默认从 0 开始 </td>
  </tr>
  <tr>
    <td>column:控件的列数,默认从 0 开始 </td>
  </tr>
  <tr>
    <td>alignment:对齐方式。</td>
  </tr>
</table>
"""
        self.richtext.setMarkdown (MarkdownStr)
        self.gridlayout.addWidget (self.richtext,0,0)
        self.setLayout (self.gridlayout)

# QSlider

QSlider 是一个常用的滑条控件,常用的信号为:valueChanged,即滑条被改变时,同时滑条具有一些特殊的属性,例如刻度尺,在创建滑条时,需要设置滑条是水平还是垂直,这需要从 Pyside6.QtCore 中导入 Qt 库。
以下提供一个滑条的基础代码,在代码中有一个 self.sender (),该函数主要获取被改变的控件,例如在存在相同多个控件时,使用该函数获取到被用户改变的控件。同时代码中有两种获取滑条当前值的方式,使用 self.slider.value (), 或者 slef.sender ().value ()

from PySide6.QtWidgets import QApplication, QWidget, QSlider, QVBoxLayout, QLabel
from PySide6.QtCore import Qt
class MyWindow (QWidget):
    def __init__(self):
        super ().__init__()
        self.resize (800, 600)
        
        mainlayout = QVBoxLayout ()
        # 设置滑条为水平和垂直
        self.slider1 = QSlider (Qt.Orientation.Horizontal)
        self.slider2 = QSlider (Qt.Orientation.Vertical)
        # 设置刻度位置
        self.slider1.setTickPosition (QSlider.TickPosition.TicksBelow)
        self.slider2.setTickPosition (QSlider.TickPosition.TicksAbove)
        # 设置刻度间隔 (值越大刻度越少)
        self.slider1.setTickInterval (5)
        self.slider2.setTickInterval (10)
        # 设置滑条最小值和最大值
        self.slider1.setMinimum (50)
        self.slider1.setMaximum (200)
        self.label = QLabel (' 请拖动滑条 ')
        self.label2 = QLabel ()
        self.slider1.valueChanged.connect (self.showSlider)
        self.slider2.valueChanged.connect (self.showSlider)
        mainlayout.addWidget (self.label)
        mainlayout.addWidget (self.label2)
        mainlayout.addWidget (self.slider2)
        mainlayout.addWidget (self.slider1)
        
        self.setLayout (mainlayout)
    def showSlider (self):
        X_axis_value = self.slider1.value ()
        Y_axis_value = self.slider2.value ()
        current_Widget = self.sender ()
        if current_Widget == self.slider1:
            slider = 'Slider 1'
        else:
            slider = 'Sliser 2'
        self.label2.setText (f' 你改变了: {slider} 改变后值为: {current_Widget.value ()}, 当前参数为 ({X_axis_value}, {Y_axis_value})')
    
if __name__ == '__main__':
    app = QApplication ([])
    windows = MyWindow ()
    windows.show ()
    app.exec ()

# 高级控件

# QListWidget

# 常用方法

QListWidget 是一个用于显示列表项的组件,每个列表项通常由一个图标和一个文本组成。用户可以通过单击或选择列表项来与其进行交互。

from PySide6.QtWidgets import QListWidget
self.listWidget = QListWidget ()

# 增加元素

QListWidget 可以使用 addItem 或 addItems 来实现假如一个或多个元素,addItem 添加元素可传入 str 或者使用 QListWidgetItem(需要导入), 使用 addItems 添加多个元素时,只能添加 str 的 Sequence(类比列表)
使用 item (索引) 的方式访问元素。
以下为一个创建和访问示例,这里使用了 Faker 库生成随机人名并添加到 QListWidget

from PySide6.QtWidgets import QListWidget, QListWidgetItem
from faker import Faker
self.fake = Faker (locale='zh_CN')
self.listWidget = QListWidget ()
# 添加单个元素
self.listWidget.addItem (self.fake.name ())
self.listWidget.addItem (QListWidgetItem (self.fake.name ()))
# 添加多个元素
self.listWidget.addItems ([self.fake.name () for _ in range (10)])
# 访问索引为 1 的元素
print (self.listWidget.item (1))

相较于直接添加 str, 使用 QListWidgetItem 添加元素能够设置图标等属性,具有较高的灵活性,一般推荐使用 QListWidgetItem 操作元素

# 插入元素

同样的,插入元素也可以插入单个或多个元素,使用 insertItem 插入单个元素时也支持使用 str 或者使用 QListWidgetItem,格式为 insertItem (row, 元素) 这里的 row 代表插入元素的行索引,使用 addItems 添加多个元素时,只能添加 str 的 Sequence(类比列表),格式为 insertItems (row, Sequence [str]) 这里的 Sequence 表示一个列表。

from PySide6.QtWidgets import QListWidget, QListWidgetItem,
# 插入单个元素
self.listWidget.insertItem (0,QListWidgetItem ('100'))
# 插入多个元素
self.listWidget.insertItems (0,[self.fake.ssn () for _ in range (10)])

# 删除元素

如果需要删除某个元素,首先确认需要删除元素的索引,使用 takeItem (索引) 删除对应元素

# 删除索引为 1 的元素
self.listWidget.takeItem (1)

# 修改元素

在修改元素前,我们需要获取到元素,再使用 setText () 对元素进行修改

# 将索引为 1 的元素改为 'new_str'
self.listWidget.item (1).setText ('new_str')

# 查找元素

查找元素使用 findItems (str, flag) 接收两个参数,返回满足搜索条件的 QListWidgetItem 列表,str 即为要查找的字符串,flag 为需要查找的模式,需要导入 Qt 库,下面介绍常用的 flage

名称功能
Qt.MatchFlag.MatchContains是否包含搜索词
Qt.MatchFlag.MatchStartsWith是否以 str 开头
Qt.MatchFlag.MatchEndsWith是否以 str 结尾
Qt.MatchFlag.MatchCaseSensitive区分大小写
Qt.MatchFlag.MatchRegularExpression正则匹配

例如需要在 listWidget 中查找元素是否包含字符 ' 王'

from PySide6.QtCore import Qt
# 搜索 listWidget 是否存在 'A', 返回存放满足搜索条件的对象列表
result = self.listWidget.findItems ('A', Qt.MatchFlag.MatchContains)
# 输出满足搜索条件的元素内容
print ([item.text () for item in result])

# 获取当前选中元素

self.listWidget.currentItem () 获取到当前选中的元素,返回选中的 QListWidgetItem 如果没有任何选中则返回 None

# 输出列表长度

count () 方法用于输出列表的最大索引,以下示例遍历并输出 listWidget 中的所有元素

print ([self.listWidget.item (i).text () for i in range (self.listWidget.count ())])

# 删除所有元素

clear () 是一个自带的槽函数,可删除 listWidget 所有元素

self.listWidget.clear ()

# 列表排序

QListWidget 内置了一个槽函数 sortItems 实现排序功能,该方法接收一个参数来判断是升序还是降序,使用方法如下:

# 升序
self.listWidget.sortItems (Qt.SortOrder.AscendingOrder)
# 降序
self.listWidget.sortItems (Qt.SortOrder.DescendingOrder)

由于 QListWidget 的元素为字符串,进行排序时会先比对第一个字符并排序,之后再第一个字符相同的情况下往后比对。例如升序排列:11, 20, 3, 9

# 滚动到指定项

scrollToItem 用于滚动 QListWidget 以确保指定的项(QListWidgetItem)在可视区域内。
方法签名: scrollToItem (self, QListWidgetItem, hint=QListWidget.ScrollHint.PositionAtTop)
传递参数:

  • item: QListWidgetItem,这是你想要滚动到的项。
  • hint: QListWidget.ScrollHint,这是一个可选参数,用于指定滚动到项的位置。它可以是以下值之一:
    • QListWidget.ScrollHint.PositionAtTop: 滚动项到视图的顶部。
    • QListWidget.ScrollHint.PositionAtBottom: 滚动项到视图的底部。
    • QListWidget.ScrollHint.PositionAtCenter: 滚动项到视图的中心。

以下代码使用 scrollToItem 方法滚动到第 80 个项,并使其位于视图顶部

list_scrool_to = self.listWidget.item (80)
self.listWidget.scrollToItem (list_scrool_to, QListWidget.ScrollHint.PositionAtTop)

# 常用信号

QListWidget 的一些常用信号

信号说明
currentItemChanged (QListWidgetItem *current, QListWidgetItem *previous)当 QListWidget 中当前选中的项发生变化时触发。current 是新的选中项,previous 是之前的选中项。
currentRowChanged (int currentRow)当 QListWidget 中当前选中的行的索引发生变化时触发。currentRow 是新的选中行的索引。
currentTextChanged (const QString &currentText)当 QListWidget 中当前选中项的文本发生变化时触发。currentText 是新的选中项的文本。
itemActivated (QListWidgetItem *item)当用户通过键盘或鼠标激活一个项(通常是按下回车键或双击)时触发。item 是被激活的项。
itemChanged (QListWidgetItem *item)当 QListWidget 中的一个项的内容发生变化时触发。item 是内容发生变化的项。
itemClicked (QListWidgetItem *item)当用户点击一个项时触发。item 是被点击的项。
itemDoubleClicked (QListWidgetItem *item)当用户双击一个项时触发。item 是被双击的项。
itemEntered (QListWidgetItem *item)当鼠标光标进入一个项时触发。item 是光标进入的项。
itemPressed (QListWidgetItem *item)当用户按下鼠标按钮在一个项上时触发。item 是被按下的项。
itemSelectionChanged ()当 QListWidget 中的项选择发生变化时触发,无论是因为用户的选择还是程序代码的改变。这个信号不提供关于哪个项被选中或取消选中的信息。

# currentIndexChanged

currentIndexChanged (QListWidgetItem *Current QListWidgetItem *previous) 在改变选中的元素时触发,信号会传递三个两个 QListWidgetItem 类型参数,Current 为当前选中的元素,previous 为之前选中的元素,当定义的槽函数只接收一个参数时,会接收到 Current。
以下代码在用户改变选中后输出改变后的元素内容

# 不使用信号传递的参数
self.listWidget.currentItemChanged.connect (lambda:print (self.listWidget.currentItem ().text ()))
# 使用信号自动传递的参数
self.listWidget.currentItemChanged.connect (self.listChange)
def listChange (self, current, previous):
    print (f"current:{current.text ()}\nprevioues:{previous.text ()}")

# 上下文菜单

在 QListWidget 中设置上下文菜单,实现鼠标右键点击后的自定义菜单,由于鼠标右键会选择元素,所以执行删除元素时只需获取到当前选择的元素即可。

from PySide6.QtGui import QAction
# 删除元素
self.delateCurrentItem = QAction (' 删除元素 ')
self.delateCurrentItem.triggered.connect (lambda:self.listWidget.takeItem (self.listWidget.currentRow ()))
# 降序排列
self.descendingCurrentItem = QAction (' 降序排列 ')
self.descendingCurrentItem.triggered.connect (lambda:self.listWidget.sortItems (Qt.SortOrder.DescendingOrder))
# 将上下文菜单加入 listWidget
self.listWidget.setContextMenuPolicy (Qt.ContextMenuPolicy.ActionsContextMenu)
self.listWidget.addActions ([self.delateCurrentItem, self.descendingCurrentItem])

# 选中和选择

在 QListWidget 中, 选择 selected 表示鼠标指定了当前元素,而选中 (check) 是一个 CheckBox
被选中,使用 setCheckState (Qt.CheckState ()) 为元素创建一个 checkBox

self.listWidget.item (0).setCheckState (Qt.CheckState ())

选中和选中的信号也不同,具体使用方法如下

# 选择
self.listWidget.currentItemChanged.connect (self.getChanged)
# 选中
self.listWidget.itemChanged.connect (self.onItemChanged)
def getChanged (self, current, previous):
    print ("当前项已更改:", current.text () if current else "None")
def onItemChanged (self, item):
    print ("项内容已更改:", item.text ())

# QTableWidget

QTableWidget 是表格控件,表格元素由行 (row) 和列 (column) 确定构成,每个单元格中的数据类型都为 QTableWidgetItem

# 创建表格

创建表格之前需要先设置表格行和列,设置的方法为 setRowCountsetColumnCount

self.tabel = QTableWidget ()
self.tabel.setRowCount (50)
self.tabel.setColumnCount (3)

# 设置表头和伸缩样式

设置表头的方法为 setHorizontalHeaderLabels (Sequence [str]) 该参数接收一个 str 列表作为表头。
如果希望表格能够跟随页面一起放大缩小,我们可以设置表格的伸缩样式,对于水平上的伸缩样式,可以使用 horizontalHeader ().setSectionResizeMode (QHeaderView.ResizeMode.Stretch) 该方法需要导入 `QHeaderView 库

from PySide6.QtWidgets import QHeaderView
# 设置表头
self.tabel.setHorizontalHeaderLabels ([' 姓名 ',' 地址 ',' 邮箱 '])
# 设置水平伸缩样式
self.tabel.horizontalHeader ().setSectionResizeMode (QHeaderView.ResizeMode.Stretch)

# 添加元素

在表格中添加元素的方法为 setItem (row:int, column:int, item:QTableWidgetItem) 需要传入行,列和表格元素,元素的数据类型为 QTableWidgetItem。
以下代码演示,将二维列表里的数据添加到表格中

from PySide6.QtWidgets import QApplication, QWidget,QTableWidget, QTableWidgetItem,QVBoxLayout
from faker import Faker
class ChoseTpyeWindow (QWidget):
    def __init__(self, *args, **kwargs):
        super ().__init__(*args, **kwargs)
        self.resize (680,420)
        self.fake = Faker (locale='zh_CN')
        # 使用 fake 创建数据
        self.data = [[self.fake.name (),self.fake.address (),self.fake.ascii_free_email ()] for _ in range (50)]
        self.tabel = QTableWidget ()
        # 设置表格行数为 data 长度,列数为 data 中最大行的长度
        self.tabel.setRowCount (len (self.data))
        self.tabel.setColumnCount (max ([len (row) for row in self.data]))
        # 将二维列表数据加入 tabel
        self.tabel.setItem (0, 0, QTableWidgetItem (' 你好 '))
        for rowIndex, row in enumerate (self.data):
            for columnIndex, item in enumerate (row):
                self.tabel.setItem (rowIndex,columnIndex,QTableWidgetItem (item))
        self.mainLayout = QVBoxLayout ()
        self.mainLayout.addWidget (self.tabel)
        self.setLayout (self.mainLayout)

在这里我们用到了函数 enumerate 该函数接收了一个列表,会返回两个值,列表的索引和该索引的元素,即相较于传统的 for row i self.data, 使用 enumerate 能够获取 row 和对应的索引 rowIndex

# 删除元素

QTableWidget 使用 takeItem (row: int,column: int) -> QTableWidgetItem 来删除一个元素,该参数接收 int 类型的行和列,返回被删除元素 QTableWidgetItem

print (self.tabel.takeItem (0,0).text ())

该方法会删除 (0,0) 单元格内的元素,单元格仍然会存在

# 修改元素

QTableWidget 也是通过 item (row:int, column:int) 来获取 row 行 column 列对应的值,返回属性为 QTableWidgetItem , 我们可以通过 setText (text:str) 修改属性。
相较于 setItem () 方法,该方法获取到了 QTableWidgetItem,除了修改元素,还能修改其他属性,例如字体,背景等等

self.tabel.item (0, 0).setText ('newItem')

设置前景色 (字体色) 和背景色示例:

from PySide6.QtCore import Qt
# 前景色
self.tabel.item (0,0).setForeground (Qt.GlobalColor.red)
# 背景色
self.tabel.item (0,1).setBackground (Qt.GlobalColor.blue)

# 行 & 列 基础操作

下面简单介 QTableWidget
中对行和列的一些基础操作

方法描述
setRowCount (rows: int)添加行,例如在底部新增一行 setRowCount (table.rowCount () + 1)
insertRow (row: int)插入行,输入需要插入的索引
removeRow (rows: int)删除行,row 是需要删除的行索引
setRowCount (rows: int)设置行数
rowCount ()获取行数
setColumnCount (columns: int)添加列,例如在右侧新增一列 table.setColumnCount (table.columnCount () + 1)
insertColumn (columns: int)插入列
removeColumn (columns: int)删除列
setColumnCount (columns: int)设置列数
columnCount ()获取列数

# 表格排序

QTableWidget 自带排序的方法 setSortingEnabled (True) ,开启之后,当我们点击表头时,表格会以被点击的表头列的数据做排序。

self.tabel.setSortingEnabled (True)

# 获取多个选中元素

在 QTabelWidget 中,鼠标左键拖动可以同时选中多个元素或按住 CTRL 也能多选,使用内置的 selectedItems () 可获取这些选中元素,该方法返回一个由 QTabelWidgetItem 组成的列表
假设我们创建了一个按钮并绑定到槽函数,我们希望点击按钮时能够输出所有的被选内容。

def getitems (self):
    print ([item.text () for item in self.tabel.selectedItems ()])

# 常用信号

QTableWidget 的信号一般分为两类,一种是以 cell 开头,这类信号会携带元素的行和列,一种是以 current 开头,这一类信号携带的是 QTableWidgetItem。

信号描述
cellActivated (int row, int column)当单元格被激活(通常是通过键盘导航)时触发。激活意味着单元格获得了焦点。
cellChanged (int row, int column)当单元格的内容被编辑并更改后触发。这通常发生在用户编辑单元格并按下回车键或离开单元格后。
cellClicked (int row, int column)当单元格被鼠标点击时触发。
cellDoubleClicked (int row, int column)当单元格被鼠标双击时触发。
cellEntered (int row, int column)当鼠标进入单元格时触发。
cellPressed (int row, int column)当单元格被鼠标按下时触发。
currentCellChanged (int currentRow, int currentColumn, int previousRow, int previousColumn)在当前单元格(即拥有焦点的单元格)改变时触发。提供了新单元格和旧单元格的行和列索引。
currentItemChanged (QTableWidgetItem *current, QTableWidgetItem *previous)在当前项(即拥有焦点的项)改变时触发。提供了新项和旧项的指针。
itemActivated (QTableWidgetItem *item)当项被激活时触发。激活通常意味着项获得了焦点。
itemChanged (QTableWidgetItem *item)当项的内容或状态被更改时触发。这适用于项的数据(如文本)或状态(如复选框的勾选状态)。
itemClicked (QTableWidgetItem *item)当项被鼠标点击时触发。
itemDoubleClicked (QTableWidgetItem *item)当项被鼠标双击时触发。
itemEntered (QTableWidgetItem *item)当鼠标进入项时触发。
itemPressed (QTableWidgetItem *item)当项被鼠标按下时触发。
itemSelectionChanged ()当表格中的选择改变时触发。不提供具体的选择信息,只是通知选择已经改变。

# 单元格被点击

同样的,单元格被点击有两个信号 cellClicked 会返回 int 类型的 row 和 column,itemClicked 则会直接返回元素的 QTableWidgetItem

self.tabel.cellClicked.connect (lambda row, column: print (f"cellClicked: row-{row} columt-{column}"))
self.tabel.itemClicked.connect (lambda item: print (f"itemClicked: row-{item.row ()} columt-{item.column ()} item-{item.text ()}"))

# 搜索和跳转

在 QTableWidget 中内置有搜索方法 findItems (text: str, flags: MatchFlag) -> List [QTableWidgetItem] 该方法接收一个需要搜索的 str 和搜索的方法 flage,返回一个由满足搜索条件的 QTableWidgetItem 组成的 List。

from PySide6.QtCore import Qt
result = self.tabel.findItems (' 搜索 ',Qt.MatchFlag.MatchContains)
for item in result:
    print (f"item: {item.text ()}")

如果需要跳转到搜索内容,可以使用 scrollToItem (item: QTableWidgetItem,hint: ScrollHint = ...) 该方法接收一个需要填转的 QTableWidgetItem ,hint 用来确定需要让元素位于中间、顶部还是底部。

self.tabel.scrollToItem (result [0],QTableWidget.ScrollHint.PositionAtTop)

# 合并单元格

合并单元格使用的方法是 setSpan (row: int, column: int, rowSpan: int, columnSpan: int) 该方法需要传递四个参数:起始单元格的行和列,需要占据的行数和需要占据的列数,需要占据的行最少填 1,此时不合并行,例如我们希望 (0,0) 单元格占两行三列:

self.tabel.setSpan (0, 0, 2, 3)

使用该方法后,在表格上,划定的区域将被合并,但如果我们使用 item 方法访问区域内的单元格值,它仍然是合并之前的数值

# 上下文菜单

QTableWidget 的上下文菜单与之前设置上下文菜单的方式基本相同,示例使用右键菜单输出选中的元素:

self.getSelected = QAction (' 输出选择元素 ')
self.getSelected.triggered.connect (self.showIteminfo)
self.tabel.setContextMenuPolicy (Qt.ContextMenuPolicy.ActionsContextMenu)
self.tabel.addAction (self.getSelected)
        
def showIteminfo (self):
    print ([item.text () for item in self.tabel.selectedItems ()])

# 布局控件

Pyside 中,最常用的布局有四种:

  • 水平布局 QHBoxLayout
  • 垂直布局 QVBoxLayout
  • 网格布局 QGirdLayout
  • 表单布局 QformLayout
    以下代码中的窗口均为 QWidget,主窗口 QMainWindow 会有自带的布局。

# 基础布局

导入布局类并实例化,将控件添加到布局中,然后设置布局。以下代码实现有一个简易的登录框界面布局。

# 垂直布局
self.mainlayout = QVBoxLayout ()
# 添加控件
self.mainlayout.addWidget (QLabel (' 用户名 '))
self.mainlayout.addWidget (QLineEdit ())
self.mainlayout.addWidget (QLabel (' 密码 '))
self.mainlayout.addWidget (QLineEdit ())
self.mainlayout.addWidget (QPushButton (' 登录 '))
# 设置布局
self.setLayout (self.mainlayout)

# 嵌套布局

运行 基础布局 的代码后,可以看到所有控件都在垂直排列,此时如果希望 Label 用户名和输入框 Edit 位于同一行,就需要使用嵌套布局,新增一个水平布局 userLayout,将用户名 Lael 和输入框 Edit 加入水平布局,再将 userlayout 添加到垂直布局 mainlayout 中。

  • 添加控件:self.mainlayout.addWidget ()
  • 添加布局:self.mainlayout.addLayout ()
# 垂直控件
self.mainlayout = QVBoxLayout ()
self.userlayout = QHBoxLayout ()
self.passwdlayout = QHBoxLayout ()
# 添加控件
self.userlayout.addWidget (QLabel (' 用户名 '))
self.userlayout.addWidget (QLineEdit ())
self.passwdlayout.addWidget (QLabel (' 密码 '))
self.passwdlayout.addWidget (QLineEdit ())
# 添加布局(嵌套布局)
self.mainlayout.addLayout (self.userlayout)
self.mainlayout.addLayout (self.passwdlayout)
self.mainlayout.addWidget (QPushButton (' 登录 '))
# 设置布局
self.setLayout (self.mainlayout)

# 表单布局

QFormLayout 按照类似 HTML 表单的方式将窗口分割成行和列,每个表单元素都放置在一个单独的行中。每一行通常包含一个标签(用于描述表单元素的用途)和一个表单控件(如文本框、下拉框等)。

以上登录框界面用表单布局可以简化为:

# 垂直控件
self.mainlayout = QVBoxLayout ()
# 表单布局   
self.formlayout = QFormLayout ()
self.formlayout.addRow (' 用户名 ', QLineEdit ())
self.formlayout.addRow (' 密码 ', QLineEdit ())
self.mainlayout.addLayout (self.formlayout)
self.mainlayout.addWidget (QPushButton (' 登录 '))
self.setLayout (self.mainlayout)

# 常用方法

方法说明
addRow (label, field)在表单布局中添加一行。label 是一个描述表单元素用途的字符串或 QWidget,field 是要添加的表单控件
setAlignment (label, alignment)设置标签的对齐方式。label 可以是字符串或 QWidget,alignment 可以是 Qt 中的对齐方式(如 Qt.AlignLeft、Qt.AlignRight 等)
setSpacing (spacing)设置表单元素之间的间距,spacing 是一个整数,表示像素值
setFieldGrowthPolicy (policy)设置表单元素的伸展策略。policy 可以是 QFormLayout.FieldsStayAtSizeHint、QFormLayout.ExpandingFieldsGrow、QFormLayout.AllNonFixedFieldsGrow
setFormAlignment (alignment)设置表单布局的对齐方式,alignment 可以是 Qt 中的对齐方式
setLabelAlignment (alignment)设置标签的对齐方式,alignment 可以是 Qt 中的对齐方式。
removeRow (row)移除指定位置的表单行
rowWrapPolicy ()返回表单布局的换行策略
rowCount ()返回表单布局中的行数
itemAt (index)返回指定索引位置的表单项

# 网格布局

QGridLayout 将窗口或小部件划分为一个规则的网格,并将小部件放置在网格的不同位置上。QGridLayout 可以自动调整小部件的大小和位置,以适应窗口的大小调整。
使用 QGridLayout 布局时,可以通过指定行和列的索引来将小部件放置在网格的特定位置。可以使用 addWidget () 函数将小部件添加到网格布局中。还可以使用 addLayout () 函数将另一个布局添加到网格布局中,并将其放置在特定的行和列中。

以上登录框界面用网格布局可以简化为:

# 网格控件
self.girdlayout = QGridLayout ()
self.girdlayout.addWidget (QLabel (' 用户名 '),0,0)
self.girdlayout.addWidget (QLineEdit (),0,1)
self.girdlayout.addWidget (QLabel (' 密码 '),1,0)
self.girdlayout.addWidget (QLineEdit (),1,1)
# 设置 button,独占两列
self.girdlayout.addWidget (QPushButton (' 登录 '),2,0,1,2)
self.setLayout (self.girdlayout)

# 常用方法

方法说明
addWidget (Widget,row,col,alignment)给网格布局添加部件,设置指定的行和列,起始位置的默认值为(0,0)
widget:所添加的控件
row:控件的行数,默认从 0 开始
column:控件的列数,默认从 0 开始
alignment:对齐方式。
addWidget (widget,fromRow,fromColulmn,rowSpan,columnSpan,alignment)所添加的的控件跨越很多行或者列的时候,使用这个函数
widget:所添加的控件
fromRow:控件的起始行数
fronColumn:控件的起始列数
rowSpan:控件跨越的行数
column:控件跨越的列数
alignment:对齐方式
setSpacing (int spacing)设置软件在水平和垂直方向的间隔

对齐方式 alignment 为一个可选项,默认是中对其,使用时需要导入库 from PySide6.QtCore import Qt 之后可以设置左对齐 alignment=Qt.AlignLeft 或者是右对齐 alignment=Qt.AlignRight

# 方便控件

方便控件是在开发过程中,为了减轻代码重复性较高造成的开发难度,Qt 内置了一些已经封装好的控件以供直接调用,这种控件就是内置方便控件。

# QMessageBox

QMessageBox 是一个对话框控件,对话框有一个很重要的属性:模态(Modal)。可以理解永远出现在最顶层一个的弹窗,不进行选择就无法操作其他窗口。
在 Qt Designer 中,创建一个窗口 Widget 并选中,在右侧的 Form:QWidget 的最后一项可以看到模态设置 windowModality,这里有三种模态
NonModal 无模态,WindowModal:窗口模态,出现在此程序的最顶层,如果不关掉则无法操作其他窗口,ApplicationModal:系统模态,悬浮在整个系统之上,如果不关掉无法操作电脑上的其他程序。
QMessageBox 需要传递四个参数:窗体,标题,内容,界面按钮,默认按钮
以下为一个示例,表示在自身窗口(self)上,创建一个信息框,默认有三个按钮,默认按钮为 Ok。

def btnClicked (self):
        replay = QMessageBox.information (self,' 标题 ',' 内容 ',QMessageBox.StandardButton.Ok |
                                         QMessageBox.StandardButton.No | QMessageBox.StandardButton.Discard ,
                                         QMessageBox.StandardButton.Ok)
        
        if replay == QMessageBox.StandardButton.Ok:
            info = ' 你点击了 OK'
        elif replay == QMessageBox.StandardButton.No:
            info = ' 你点击了 NO'
        elif replay == QMessageBox.StandardButton.Discard:
            info = ' 你点击了 Discard'

# QInputDialog

QInputDialog 主要用来创建一个子窗口,可以接收用户的输入,有多种静态方法:

  • getDouble: 获取浮点数
  • getInt: 获取整数
  • getItem: 获取指定列表的某一项(类似下拉框)
  • getMultiLineText: 获取多行文字(类似文本框)
  • getText: 获取单行文字(类似输入框)

QInputDialog 的返回值为 (数字,状态),状态是一个 bool 类型变量,表示用户点击了 ok 还是 Cancel,此时用两个参数接收,当用户点击 ok 时,输出用户选择的数。
getInt 的参数:窗口 (slef),内容,默认数字,最小数,最大数,步长

self.btn.clicked.connect (self.getint)
def getint (self):
    replay = QInputDialog.getInt (self,' 标题 ',' 内容 ',1,0,100,1)
    return replay

QInputDialog 的参数包括:窗口 (self),内容,供选择的列表,默认的选项索引,是否能被编辑 (默认为 True)。
这里使用 btn 绑定传入匿名函数的方法与 getInt 示例中绑定函数的实现效果一致。

self.btn2.clicked.connect (lambda:print (QInputDialog.getItem (self,' 标题 ',' 内容 ',['a','b','c'],0,False)))

以下是获取单行文字和获取多行文字的示例,单行文字需要导入 QLineEdit

# 单行文本
self.btn3.clicked.connect (lambda:print (QInputDialog.getText (self,' 窗口 ',' 内容 ',QLineEdit.EchoMode.Normal),' 默认值 '))
# 多行文本
self.btn4.clicked.connect (lambda:print (QInputDialog.getMultiLineText (self,' 窗口 ',' 内容 ',' 默认 ')))

# QFileDialog

QFileDialog 是一个文件对话框,一些常用的静态方法:

  • getOpenFileName 打开一个文件
  • getOpenFileNames 打开多个文件
  • getExistingDirectory 打开一个文件夹
  • getSaveFileName 保存一个文件.

getOpenFileName 打开一个文件浏览器并选择打开一个文件,传递参数:窗体类型 (self),对话框标题,打开文件路径 (从哪里打开),过滤文件。
过滤文件类型,在括号中使用通配符 * 表示。

"所有文件 (*.)"
# 如果需要提供多个格式选项,用两个分号隔开 
"所有文件 (*.);;py 文件 (*.py)"
# 如果同一个选择中包含多个格式,用一个空格隔开
"py 文件 (*.py *.pyd)"

getOpenFileName 返回一个元组,元组中有两个参数,第一个参数为选择的文件绝对路径,第二个参数为过滤文件类型,例如 ('D:\Python\test.py', ' 所有文件 (*.py)'),如果是 getOpenFileNames ,则输出参数的第一个为元素一个列表,列表的每一个元素都是一个文件绝对路径。

# 打开文件
self.btn.clicked.connect (lambda:print (QFileDialog.getOpenFileName (self,' 选择文件这是标题 ','.','All Files (*);;py 文件 (*.py *.pyd)')))
# 打开文件夹
self.btn2.clicked.connect (lambda:print (QFileDialog.getExistingDirectory (self,' 选择文件这是标题 ','.')))
# 保存文件
self.btn3.clicked.connect (lambda:print (QFileDialog.getSaveFileName (self,' 选择文件这是标题 ','.','All Files (*);;py 文件 (*.py *.pyd)')))

# QFontDialog

QFontDialog 是一个样式选择控件,常用于改变文本样式。常用的方法是 getFont (),会返回一个元组,第一个元素表示当前窗口是否被正常选择,第二个元素表示获取到的 font, 将获取到的 font 设置到文本样式,使用 self.TextEdit.setFont (font)

ok, font  = QFontDialog.getFont ()
if ok:
    self.edit.setFont (font)

# QColorDialog

QFontDialog 是一个颜色选择控件,常用于改变文本颜色。常用的方法是 getColor (), 通常用 color 返回值,然后使用 setTextColor (color) 设置选择的颜色。

color = QColorDialog.getColor ()
self.edit.setTextColor (color)

# 子窗口 & 多窗口

例如打开一个游戏主窗口,此时如果需要查看装备,则会进到另一个窗口,那么查看装备的窗口就是一个子窗口。而多窗口则表示能够同时出现多个窗口,例如在打开资料页面的同时打开编辑页面进行文本编写。

# 开关子窗口

子窗口的三种常用方法

  • show () 打开窗口
  • close () 关闭窗口
  • hide () 隐藏窗口

关于子窗口的三种常用方法示例代码如下

class MyWindow (QWidget):
    def __init__(self):
        super ().__init__()
        self.resize (500, 400)
        self.subwindow = SubWindow ()
        self.lb = QLabel (' 主窗口 ')
        self.btn = QPushButton (' 打开子窗口 ')
        self.btn.clicked.connect (self.openSubWindow)
        self.btnclose = QPushButton (' 关闭子窗口 ')
        self.btnclose.clicked.connect (self.closeSubWindow)
        self.btnHide = QPushButton (' 隐藏子窗口 ')
        self.btnHide.clicked.connect (self.hideSunWindow)
        self.mainlayout = QVBoxLayout ()
        self.mainlayout.addWidget (self.lb)
        self.mainlayout.addWidget (self.btn)
        self.mainlayout.addWidget (self.btnclose)
        self.mainlayout.addWidget (self.btnHide)
        self.setLayout (self.mainlayout)
    def openSubWindow (self):
        self.subwindow.show ()
    
    def closeSubWindow (self):
        self.subwindow.close ()
    
    def hideSunWindow (self):
        self.subwindow.hide ()
# 子窗口
class SubWindow (QWidget):
    def __init__(self):
        super ().__init__()
        self.resize (500, 400)
        self.lb = QLabel (' 这是子窗口 ')
        self.lineEdit = QLineEdit ()
        self.lineEdit.setText (' 这是子窗口文本框 ')
        self.mainlayout = QVBoxLayout ()
        self.mainlayout.addWidget (self.lb)
        self.mainlayout.addWidget (self.lineEdit)
        self.setLayout (self.mainlayout)
if __name__ == '__main__':
    app = QApplication ()
    Window = MyWindow ()
    Window.show ()
    app.exec ()

# 窗口信号传递

# 定义信号

一个控件本身存在多个信号,例如 button 的 clicked 信号,checkbox 的 isckeck 等,但是这些信号并不能满足需求,有时我们需要自己定义信号并触发槽,这就涉及到自定义信号。
在定义信号之前,首先需要导入库,之后将定义内容写在函数体外,即 __init__ 的上方。

from PySide6.QtCore import Signal

定义信号的形式为 信号名 = Signal (数据类型) ,需要传入类型,可以是整数、字符串、列表、字典等多种类型,或者在不清楚类型时可以写 object

# 主窗口向子窗口传递信号

要想将主窗口中的某信号传递到子窗口,一般需要三个步骤:

    1. 导入库并定义信号
    1. 对信号进行绑定,信号名称.connect (需要激活的函数)
    1. 使用 信号名.emit (发送值) 发送信号

在以下示例中,首先定义了一个信号 textchanged = Signal (str) ,之后使用 self.textchanged.connect (self.setEdit) 将信号绑定到槽函数 setEdit,而 setEdit 将会更改子窗口的 Label 内容,设置 btn 被点击时发送信号到子窗口。

class MyWindow (QWidget):
    # 定义信号
    textchanged = Signal (str)
    def __init__(self):
        super ().__init__()
        
        self.lineEdit = QLineEdit ()
        self.btn = QPushButton (' 发送数据到子窗口 ')
        self.mainlayout = QVBoxLayout ()
        self.mainlayout.addWidget (self.lineEdit)
        self.mainlayout.addWidget (self.btn)
        self.setLayout (self.mainlayout)
        self.bind ()
    def bind (self):
        self.subwinow = SubWindow ()
        self.subwinow.show ()
        # 绑定信号,在信号触发时改变子窗口的 Edit
        self.textchanged.connect (self.setEdit)
        # 发送信号,在 btn 被点击时使用 sendValue 函数发送信号
        self.btn.clicked.connect (self.sendValue)
    
    def sendValue (self):
        text  = self.lineEdit.text ()
        # 将获取到的 lineEdit 内容发送
        self.textchanged.emit (text)
    
    def setEdit (self,text):
        self.subwinow.label.setText (text)
        
# 子窗口
class SubWindow (QWidget):
    def __init__(self):
        super ().__init__()
        self.label = QLabel ()
        self.mainlayout = QVBoxLayout ()
        self.mainlayout.addWidget (self.label)
        self.setLayout (self.mainlayout)

上述代码将子窗口改变 Label 的槽函数写在了主窗口类下,下面展示将槽函数写道子窗口类的代码:

class MyWindow (QWidget):
    # 定义信号
    textchanged = Signal (str)
    def __init__(self):
        super ().__init__()
        
        self.lineEdit = QLineEdit ()
        self.btn = QPushButton (' 发送数据到子窗口 ')
        self.mainlayout = QVBoxLayout ()
        self.mainlayout.addWidget (self.lineEdit)
        self.mainlayout.addWidget (self.btn)
        self.setLayout (self.mainlayout)
        self.bind ()
    def bind (self):
        self.subwinow = SubWindow ()
        self.subwinow.show ()
        # 连接主窗口的信号到子窗口的槽函数
        self.textchanged.connect (self.subwinow.setEdit)
        # 发送信号,在 btn 被点击时使用 sendValue 函数发送信号
        self.btn.clicked.connect (self.sendValue)
    
    def sendValue (self):
        text  = self.lineEdit.text ()
        # 将获取到的 lineEdit 内容发送
        self.textchanged.emit (text)
            
# 子窗口
class SubWindow (QWidget):
    def __init__(self):
        super ().__init__()
        self.label = QLabel ()
        self.mainlayout = QVBoxLayout ()
        self.mainlayout.addWidget (self.label)
        self.setLayout (self.mainlayout)
    
    def setEdit (self,text):
        self.label.setText (text)

# 子窗口向主窗口传递参数

从子窗口向主窗口传递参数时需要把信号定义到子窗口,但是子窗口本身并不知道主窗口的存在,需要在子窗口的 def __init__(self,parent) 中设置一个 parent 参数,同时在主窗口的 self.subwinow = SubWindow (self) 中设置 SubWindow (self) 将主窗口函数传递过来,现在即可使用 parent 去调用主窗口中的函数。

class MyWindow (QWidget):
    def __init__(self):
        super ().__init__()
        
        self.lineEdit = QLineEdit ()
        self.mainlayout = QVBoxLayout ()
        self.mainlayout.addWidget (self.lineEdit)
        self.setLayout (self.mainlayout)
        self.bind ()
    def bind (self):
        self.subwinow = SubWindow (self)
        self.subwinow.show ()
    
    def setEdit (self,text):
        self.lineEdit.setText (text)
            
# 子窗口
class SubWindow (QWidget):
    # 定义信号
    sendValueToMain = Signal (str)
    # 传入父类
    def __init__(self,parent):
        super ().__init__()
        self.parent = parent
        self.lineEdit = QLineEdit ()
        self.btn = QPushButton (' 发送信号到父窗口 ')
        self.mainlayout = QVBoxLayout ()
        self.mainlayout.addWidget (self.lineEdit)
        self.mainlayout.addWidget (self.btn)
        self.setLayout (self.mainlayout)
        # 将自定义信号与父类的槽绑定
        self.sendValueToMain.connect (self.parent.setEdit)
        # 设置触发并发送信号
        self.btn.clicked.connect (self.sendValue)
    
    def sendValue (self):
        text = self.lineEdit.text ()
        self.sendValueToMain.emit (text)

如果不希望使用这种添加参数 parent 的方法,也可以将信号的触发写在主函数中

class MyWindow (QWidget):
    def __init__(self):
        super ().__init__()
        
        self.lineEdit = QLineEdit ()
        self.mainlayout = QVBoxLayout ()
        self.mainlayout.addWidget (self.lineEdit)
        self.setLayout (self.mainlayout)
        self.bind ()
    def bind (self):
        self.subwinow = SubWindow ()
        # 自定义信号与槽绑定
        self.subwinow.sendValueToMain.connect (self.setEdit)
        self.subwinow.show ()
    
    def setEdit (self,text):
        self.lineEdit.setText (text)
            
# 子窗口
class SubWindow (QWidget):
    # 定义信号
    sendValueToMain = Signal (str)
    def __init__(self):
        super ().__init__()
        self.lineEdit = QLineEdit ()
        self.btn = QPushButton (' 发送信号到父窗口 ')
        self.mainlayout = QVBoxLayout ()
        self.mainlayout.addWidget (self.lineEdit)
        self.mainlayout.addWidget (self.btn)
        self.setLayout (self.mainlayout)
        # 设置触发并发送信号
        self.btn.clicked.connect (self.sendValue)
    
    def sendValue (self):
        text = self.lineEdit.text ()
        self.sendValueToMain.emit (text)

# 窗口之间参数传递

如果两个窗口并没有继承关系,但是需要进行参数传递,依旧可以使用上述参数传递方式。
例如,需要在 class A 中的 btn 被触发时,传递 list 到 class B,操作方式为:

  1. 导入 from PySide6.QtCore import Signal, Slot
  2. 在 class A 中定义信号 Signal 和需要传递的参数类型
  3. 在 class A 中创建一个槽函数用于发送信号
  4. 将 class A 的按钮点击事件绑定到槽函数
  5. 在 class B 中创建槽函数,用于接收信号传递的参数
  6. 在 class A 中将信号绑定到槽函数

A.py

from PySide6.QtWidgets import QApplication, QPushButton, QDialog
from PySide6.QtCore import Signal # 1. 导入库
import sys
# 导入 B 类
from B import B
class A (QDialog):
    # 2. 定义一个信号,接受一个 list 作为参数
    list_signal = Signal (list)
    def __init__(self):
        super ().__init__()
        self.some_list = [1, 2, 3, 4]  # 需要传递的列表
        self.btn = QPushButton ('Send List', self)
        self.btn.clicked.connect (self.on_btn_clicked) # 4. 绑定 btn 事件到发送信号的槽函数
        b_instance = B ()  # 创建 B 类的实例
        self.list_signal.connect (b_instance.handle_list_received) # 6. 将信号连接到 class B 的槽
    # 3. 创建槽函数发送信号
    def on_btn_clicked (self):
        # 当按钮被点击时,触发信号并传递列表
        self.list_signal.emit (self.some_list)
if __name__ == "__main__":
    app = QApplication (sys.argv)
    a_dialog = A ()
    a_dialog.show ()
    sys.exit (app.exec ())
from PySide6.QtCore import Signal, Slot
class B:
    def __init__(self):
        super ().__init__()
        # 这里的内容将会在信号发送后,槽函数执行前执行
    # 5. 定义接收 class A 的槽函数
    @Slot (list)
    def handle_list_received (self, lst):
        # 处理接收到的 list
        print ("Received list:", lst)

# 菜单栏

# 设计菜单栏

如果要设置菜单,需要在 Qt designer 中选择 Main Window,此时在窗口的左上角能够看到 "在这里输入" 的字样,这就是菜单栏。在 Qt 中, 菜单栏 (QMenuBar) 下还包括有 菜单 (QMenu) ,每一个菜单中包含若干个 选项 (QAction)
例如在菜单栏中创建 "文件","编辑","选择" 等菜单,"文件" 菜单下又有 "新建文件","打开文件","打开文件夹" 等选项,每个选项下还可以添加子选项。

在 Qt designer 中,选项无法直接设置中文,需要设置英文名后在属性区的 text 中设置中文。

设置好菜单栏后,在右下角能看到动作编辑器,在此双击一个 QAction 即可编辑快捷键,图标等等。
鼠标右键窗口空白处,可以选择添加工具栏,此时能在窗口上新增一个工具栏,可以将工具栏拖动至窗口左侧,同时可以将 动作编辑器 中的 QAction 拖动至工具栏中。
QAction 有一个信号 triggered,当被点击时触发。

# 触发信号
self.action1.triggered.connect (lambda:print ("触发"))

# 创建菜单栏

要设计一个完整的菜单,首先确定 QAction ,之后确定 QAction 所在的菜单 QMenu ,最后将菜单加入菜单栏 QMenuBar 。由于 QMainWindow 自带布局,可以直接调用 QMenuBar,QMenu 和 QAction 则需要导入库并创建。

from PySide6.QtWidgets import QMenu, QMenuBar
from PySide6.QtGui import QAction
# 创建 QAction
self.action 名称 = QAction ('ui 显示内容 ',self)
# 创建 Menu
self.menu 名称 = QMenu ('ui 显示内容 ',self)
# 创建 MenuBar
self.menubar 名称 = QMenuBar (self)
# 将 Action 添加到 Menu
self.menu 名称.addAction (self.action 名称)
# 将 Menu 添加到 MenuBar
self.menubar 名称.addMenu (self.menu 名称)
# 如果非 MainWindow,需要设置布局
self.mainlayout = QVBoxLayout ()
self.mainlayout.addWidget (self.menuBar)
self.setLayout (self.mainlayout)

如果是在 MainWindow 中,则可以无需再创建布局,且可以直接调用自带的 menuBar,简化创建方法

# 创建 Action
self.openfile = QAction (' 打开文件 ',self)
# 创建 Menu
self.fileMenu = QMenu (' 文件 ',self)
# 获取 MainWindow 自带布局
self.menu = self.menuBar ()
# 将 Action 添加到 Menu
self.fileMenu.addAction (self.openfile)
# 将 Menu 添加到 MenuBar
self.menu.addMenu (self.fileMenu)

# 嵌套菜单

如果希望在 Menu 下,选择一个 Action ,此时出现二级目录供选择,那么此时这个 Action 也是一个 QMenu,假设名为 QMenu1,则此时是将 QMenu1 嵌套在了 QMenu 下,而 QMenu1 下在加入 QAction ,这就是嵌套菜单,代码实现如下:

# Action 内容
self.pythonAction = QAction ('python 文件 ',self)
self.cppAction = QAction ('c 文件 ',self)
# 二级目录
self.moreMenu = QMenu (' 文件类型 ')
# 将 Action 添加到二级目录
self.moreMenu.addActions ([self.pythonAction,self.cppAction])
# 将二级目录添加到一级目录
self.fileMenu.addMenu (self.moreMenu)

# 设置图标

QStyle.StandardPixmap 有一些自带的图标,使用时需要首先导入 QStyle 库:

from PySide6.QtWidgets import QStyle
self.pythonAction = QAction (self.style ().standardIcon (QStyle.StandardPixmap.SP_DirOpenIcon),' 打开文件 ',self)

# 右键菜单

右键菜单 也叫 上下文菜单,在一个窗口中,存在控件和窗体,两个部件都分别有上下文菜单,即鼠标右键窗体和控件是不同的两个事件。

# 窗体右键菜单

在窗体空白处鼠标右键时弹出的内容,即弹出的 QAction ,首选需要导入 Qt 库,之后定义上下文菜单策略,设置 QAction 并添加到策略中

from PySide6.QtCore import Qt
# 设置上下文菜单策略
self.setContextMenuPolicy (Qt.ContextMenuPolicy.ActionsContextMenu)
# 创建并添加 Action
self.copy  = QAction (' 复制 ')
self.pase =  QAction (' 粘贴 ')
self.addActions ([self.copy,self.pase])
# 绑定逻辑
self.copy.triggered.connect (lambda:print ('copy'))
self.pase.triggered.connect (lambda:print ('pase'))

# 控件右键菜单

这里以 LineEdit 举例,该控件本身自带右键菜单,使用与窗体相同的修改方式进行修改,在以下代码中,LineEdit 的上下文菜单有 "发送" 和 "显示" ,当选择发送时,调用槽函数 SendValueToEdit2 将内容发送的 Edit2,选择显示时则打印内容。

self.LineEdit = QLineEdit ()
self.LineEdit2 = QLineEdit ()
# 单个控件修改
self.LineEdit.setContextMenuPolicy (Qt.ContextMenuPolicy.ActionsContextMenu)
self.sendValue = QAction (' 发送 ')
self.showValue = QAction (' 显示 ')
self.LineEdit.addActions ([self.sendValue,self.showValue])
self.sendValue.triggered.connect (self.SendValueToEdit2)
self.showValue.triggered.connect (lambda:print (1))
def SendValueToEdit2 (self):
    value = self.LineEdit.text ()
    self.LineEdit2.setText (value)

# 折叠菜单

ToolBox 是一个容器控件,可用于制作容器控件,设计一个 QWidget 用于存放选项卡,然后将所有的选项卡存放在 ToolBx0 中,代码示例如下:

from PySide6.QtWidgets import QToolBox
class MainWindow (QWidget):
    def __init__(self):
        super ().__init__()
        # 创建容器
        self.toolBox = QToolBox ()
        # 创建 INFO 折叠选项卡内容
        self.InfoWidget = QWidget ()
        self.Label = QLabel (' 输入信息 ')
        self.LineEdit  = QLineEdit ()
        self.InfoWidgetLayout = QVBoxLayout ()
        self.InfoWidgetLayout.addWidget (self.Label)
        self.InfoWidgetLayout.addWidget (self.LineEdit)
        self.InfoWidget.setLayout (self.InfoWidgetLayout)
        # 创建 Type 折叠选项卡内容
        self.TypeWidget = QWidget ()
        self.Label = QLabel (' 选择类型 ')
        self.ComboBox = QComboBox ()
        self.ComboBox.addItems (['a','b','c'])
        self.TypeLayout = QVBoxLayout ()
        self.TypeLayout.addWidget (self.Label)
        self.TypeLayout.addWidget (self.ComboBox)
        self.TypeWidget.setLayout (self.TypeLayout)
        # 将选项卡加入 ToolBox
        self.toolBox.addItem (self.InfoWidget, ' 选择信息 ')
        self.toolBox.addItem (self.TypeWidget, ' 选择类型 ')
        self.mainlayout = QVBoxLayout ()
        self.mainlayout.addWidget (self.toolBox)
        self.setLayout (self.mainlayout)

# 资源加载

# 内置图标

要想使用 QStyle 的内置图标,首选需要导入 QStyle,之后通过 setPixmap 设置图像的方式将图标设置为 Style 下的内置图标,内置图标均以 SP_ 开头

self.lb = QLabel ()
self.lb.setPixmap (self.style ().standardPixmap (QStyle.StandardPixmap.SP_DialogSaveButton))

# 自定义资源文件

在资源打包时,有时我们希望能够将我们自定义的资源文件也打包,例如需要读取的 excel 等,假如我们需要打包的数据非常多,这会造成打包困难,pyQt 提供了 Rcc(内嵌资源文件)来解决这种问题。

# qrc

在 Qt 中,我们自定义的资源文件,如图片,execel 等将会被转换成二进制的形式存放在 py 文件中,这种存储方式就是通过 qrc 实现。
qrc 机制是一种资源管理系统,它允许你将应用程序所需的静态资源,如图像、样式表、字体和音频文件,嵌入到可执行文件中而不是作为外部文件存在。这样做的好处是资源管理更加安全,因为它们不会丢失或被意外修改,同时也简化了应用程序的分发。
qrc 文件是一个 XML 格式的文件,通常命名为 resources.qrc 或者任何其他自定义名称。这个文件定义了资源的逻辑分组以及它们在磁盘上的实际位置。例如:

<RCC>
    <qresource prefix="/images">
        <file>icons/app_icon.png</file>
        <file>icons/close.png</file>
    </qresource>
    <qresource prefix="/styles">
        <file>main.css</file>
    </qresource>
</RCC>

这里的 <qresource> 标签定义了一个资源集合,prefix 属性指定了资源的前缀,而 <file> 标签则列出了具体文件的相对路径。

# Rcc

打开 Qt designer 在右下角的资源浏览器,在此我们能看到 resource rool 即资源的根节点,点击编辑按钮,新建资源文件,此时弹出文件浏览器用于选择新建资源的存放位置,之后在右侧创建路径,我们可以将新建资源文件理解为创建了一个数据库,而创建路径则是创建一个表,之后我们就可以点击 ' 新建文件 ' 按钮在这个 ' 表 ' 中放入文件。
创建完后,我们可以在 vs code 中看到一个.qrc 后缀的文件,鼠标右键后选择 PYQT:compile resource 即可编译成一个 py 文件,假如我们创建了 image.qrc, 那么编译后即为 image_rc.py

如果需要使用资源文件,在导入资源文件后,以 :/ 前缀名 / 文件名 访问,例如:

import image_rc
self.lb = QLabel ()
self.lb.setPixmap (QPixmap (':/image/1.png'))

# 实践

# 登陆界面和计算器

# 编译和导入

在 Qt Designer 中设计一个简单的计算器和登陆界面,分别保存为 项目名 \QT\ 目录下的 counter.ui 和 login.ui,使用 vscode 插件,右键 ui 文件选择 PYQT: Compile Form 静态编译两个文件。

编译完成后,两个文件名为 Ui_counter.py 和 Ui_login.PY,需要将两个文件中的 class Ui_Form (object) 的类名更换,此处更替为 class Ui_Counter (object)class Ui_Login (object) ,现在可将文件导入

from PySide6.QtWidgets import QApplication, QWidget,QMessageBox
# 导入 ui 编译后的文件
from QT.Ui_counter import Ui_Counter
from QT.Ui_login import Ui_Login

# 创建窗口

实现两个界面之间的跳转功能,初始显示登录界面,用户登录成功后显示计算器界面,首先写两个界面的基础类,然后实例化两个窗口。

# 登录界面
class LoginWindow (QWidget,Ui_Login):
    def __init__(self):
        super ().__init__()
        self.setupUi (self)
# 主界面
class CounterWindow (QWidget,Ui_Counter):
    def __init__(self):
        super ().__init__()
        self.setupUi (self)
if __name__ == '__main__':
    app = QApplication ([])
    # 实例化两个窗口
    LWindow = LoginWindow ()
    CWindow = CounterWindow ()
    # 先显示登录窗口
    LWindow.show ()
    app.exec ()

# 用户登录

用户输入账号密码,如果正确,则跳转到计算器界面,如果错误,则弹窗提示。

# 登录框
class LoginWindow (QWidget,Ui_Login):
    def __init__(self):
        super ().__init__()
        self.setupUi (self)
        # 获取按钮 Clicker 事件后执行 loginFuc 函数
        self.pushButton.clicked.connect (self.loginFuc)
    
    def loginFuc (self):
        # 获取账号和密码信号
        account = self.lineEdit.text ()
        passwd = self.lineEdit_2.text ()
        if account == 'root' and passwd == '123456':
            self.close ()
            CWindow.show ()
        else:
            # 密码错误时弹窗提示
            QMessageBox.information (self,' 提示 ',' 密码错误 ')

# 计算功能

实现计算器的主要功能,主要是对信号与槽知识点部分的简单运用。
此处实现计算器功能,将基础运算按钮内容存储在一个字符串中,使用 eval 函数执行字符串的内容,在定义函数去实现清除,回退等功能。
由于需要绑定的按钮较多,创建一个函数专用于绑定,保持 init 代码段的简易性,该代码段最好不要超过 15 行。

在 pyside6 框架中,clicked.connect () 方法用于将一个信号(如按钮点击)连接到一个槽函数(即一个处理信号的函数)。如果需要传递参数到槽函数,不能直接使用 connect 方法,因为 connect 期望接收一个不带参数的函数。
此处在按钮点击事件传递了 lambda,lambda 表达式创建了一个匿名函数,这个匿名函数没有参数,但它会调用 self.count ('1')。当按钮被点击时,这个 lambda 函数被调用,进而调用 count 方法并传递了字符串 '1' 作为参数。
此处使用 lambda 是为了创建一个可以立即执行的函数,但这个函数的内容(即 self.count ('1'))会在按钮点击事件发生时才执行。这样,就可以将带有参数的函数作为槽函数使用。

# 完整代码

以下是该实践项目的完整代码,这段代码实现了用户登录界面和计算器界面,当用户输入账号密码并成功登录后,可以使用计算器的功能。
若要运行此代码,需要在 Qt Designer 中设计两个 ui 界面并且编译,在本代码的目录下创建一个子目录 QT 用于存储编译后的两个 py 文件,确保静态编译文件名和文件中的 class 名与代码对应,且每个控件的属性名与代码中相同。

from PySide6.QtWidgets import QApplication, QWidget,QMessageBox
from QT.Ui_counter import Ui_Counter
from QT.Ui_login import Ui_Login
# 登录框
class LoginWindow (QWidget,Ui_Login):
    def __init__(self):
        super ().__init__()
        self.setupUi (self)
        # 获取按钮 Clicker 事件后执行 loginFuc 函数
        self.pushButton.clicked.connect (self.loginFuc)
    
    def loginFuc (self):
        # 获取账号和密码信号
        account = self.lineEdit.text ()
        passwd = self.lineEdit_2.text ()
        if account == 'root' and passwd == '123456':
            self.close ()
            CWindow.show ()
        else:
            # 密码错误时弹窗提示
            QMessageBox.information (self,' 提示 ',' 密码错误 ')
# 主界面
class CounterWindow (QWidget,Ui_Counter):
    def __init__(self):
        super ().__init__()
        self.setupUi (self)
        # 执行绑定函数
        self.bind ()
        # 存储计算内容
        self.result = ''
    # 绑定函数
    def bind (self):
        self.pushButton.clicked.connect (lambda: self.count ('1'))
        self.pushButton_2.clicked.connect (lambda: self.count ('2'))
        self.pushButton_3.clicked.connect (lambda: self.count ('3'))
        self.pushButton_4.clicked.connect (lambda: self.count ('4'))
        self.pushButton_5.clicked.connect (lambda: self.count ('5'))
        self.pushButton_6.clicked.connect (lambda: self.count ('6'))
        self.pushButton_7.clicked.connect (lambda: self.count ('7'))
        self.pushButton_8.clicked.connect (lambda: self.count ('8'))
        self.pushButton_9.clicked.connect (lambda: self.count ('9'))
        self.pushButton_10.clicked.connect (lambda: self.count ('0'))
        self.pushButton_11.clicked.connect (self.back) # 回退按钮
        self.pushButton_12.clicked.connect (lambda: self.count ('*'))
        self.pushButton_13.clicked.connect (lambda: self.count ('/'))
        self.pushButton_14.clicked.connect (lambda: self.count ('+'))
        self.pushButton_15.clicked.connect (lambda: self.count ('-'))
        self.pushButton_16.clicked.connect (self.equal) # 计算按钮
        self.pushButton_17.clicked.connect (self.clear) # 清除按钮
    
    def count (self,number):
        self.lineEdit.clear ()
        self.result += number
        self.lineEdit.setText (self.result)
    
    def equal (self):
        try:
            self.numberResual = eval (self.result)
            self.lineEdit.setText (str (self.numberResual))
        except:
            QMessageBox.information (self,' 提示 ',' 请输入正确的计算公式 ')
        
    def back (self):
        self.result = self.result [:-1]
        self.lineEdit.setText (self.result)
    def clear (self):
        self.result = ''
        self.lineEdit.clear ()
if __name__ == '__main__':
    app = QApplication ([])
    # 实例化两个窗口
    LWindow = LoginWindow ()
    CWindow = CounterWindow ()
    # 先显示登录窗口
    LWindow.show ()
    app.exec ()

# 单位换算器

实现一个单位换算器,该换算器由三个 ComboBox,两个 LineEdit 和两个 QLabel 构成,要求用户可以选择需要换算的类型:长度或者质量。用户可以在上下两个输入框输入数字,并在 ComboBox 中选则单位,按下回车或更换单位后,另一个框的数据会变化,完成单位换算,同时 QLabel 显示换算内容。

# 编译和导入

在 Qt Designer 中设计一个简单的进制转换界面,保存在 项目名 \QT\ 目录下的 converter.ui,使用 vscode 插件,右键 ui 文件选择 PYQT: Compile Form 静态编译文件。

编译完成后的文件名为 Ui_converter.py,将文件中的 class Ui_Form (object) 的类名更换为 class Ui_converter (object) ,import 导入。

from PySide6.QtWidgets import QApplication,QWidget,QMessageBox
from QT.Ui_converter import Ui_converter

# 设置下拉框内容

使用一个字典存储数据类型,为了计算简单,需要标注化存储数据,同时由于是字典存储,在 addItems 时需要使用字典的 keys () 获取键数据。设置一个绑定函数绑定信号与槽。

class MyWindow (QWidget,Ui_converter):
    def __init__(self):
        super ().__init__()
        self.setupUi (self)
        # 字典存储数据类型,标准化处理
        self.lengthVar = {' 厘米 ':1,' 分米 ':10,' 米 ':100,' 千米 ':100000}
        self.weightVar = {' 克 ':1,' 斤 ':500,' 千克 ':1000}
        self.TypeDict = {' 长度 ':self.lengthVar,' 质量 ':self.weightVar}
        self.oneInputComboBox.addItems (self.lengthVar.keys ())
        self.twoInputComboBox.addItems (self.lengthVar.keys ())
        self.dataTypeComboBox.addItems (self.TypeDict.keys ())
        self.bind ()

# 绑定与取消

在本次绑定函数中,EditLine 使用的是 editingFinished 方法,这个方法会在 EditLine 失去焦点(例如回车键,鼠标点击别处)时发送信号,如果使用 textChanged, 用户在持续输出的状态下输入框会被更新,导致用户无法输入三位及以上的数字。

def bind (self):
        self.oneInputEditLine.editingFinished.connect (self.NumberChanged)
        self.twoInputEditLine.editingFinished.connect (self.NumberChanged)
        self.dataTypeComboBox.currentTextChanged.connect (self.dataTypeChanged)
        self.oneInputComboBox.currentIndexChanged.connect (self.NumberChanged)
        self.twoInputComboBox.currentIndexChanged.connect (self.NumberChanged)

同时需要定义一个 unbind 函数用于取消绑定,在用户切换换算类型,如从长度切换到质量时,需要先取消所有绑定,删除内容,然后再绑定。unbind 只需将 bind 中的 connect 改为 disconnect 即可。

self.oneInputEditLine.editingFinished.disconnect (self.NumberChanged)

# 计算与更新

在 bind 函数中,除了切换类型的 dataTypeComboBox,其余的控件发射信号对应的槽均为 NumberChanged,这个函数负责换算单位后更新 Edit 和 Label,且保证用户输入的 Edit 不更新。
为了不更新用户的输入框,该函数需要判断是谁发来的信号,函数定义了三个变量 currentOctal,anotherOctal 和 anotherSender 方便更新 Edit,currentOctal 即为用户所输入行的 ComboBox,anotherOctal 即为另一个 ComboBox,anotherSender 为另一行的 Edit,两个 ComboBox 用于在字典中确定键从而计算,由于用户输入的 Edit 不用更新,所以只设置另一个需要更新的 Edit。

currentSender = self.sender ()
        if currentSender in (self.oneInputEditLine, self.oneInputComboBox):
            value = self.oneInputEditLine.text () or 0
            currentOctal = self.oneInputComboBox
            anotherOctal = self.twoInputComboBox
            anotherSender = self.twoInputEditLine

使用 try,except 对用户输入进行检查,如输入不正确则弹窗提示并且 Return 终止后续程序,检查无误后就可以开始进行单位换算了。
在计算时,首先将用户的输入转化为基本单位,由于基本单位是 1,所以做乘法,之后由基本单位转到目标单位。
如果用户输入 1 分米,self.oneNumber 将 1 分米转为 100 厘米,self.twoNumber 再将 100 厘米转换为 1 米。

self.oneNumber = self.oneNumber * self.TypeDict [currentType][currentOctal.currentText ()]
        self.twoNumber = self.oneNumber/self.TypeDict [currentType][anotherOctal.currentText ()]
        self.originDataLabel.setText (f'{value}{currentOctal.currentText ()} =')
        self.transDataLabel.setText (f'{self.twoNumber}{anotherOctal.currentText ()}')
        anotherSender.setText (str (self.twoNumber))

# 切换计算类型

使用 dataTypeComboBox 可以切换计算类型,此时调用 dataTypeChanged 函数。在切换时,需要先断开所有的信号与槽的绑定,清空 Edit,之后更换两行 combox 的内容并重新绑定。

# 完整代码

以下是该实践项目的完整代码,这段代码实现了一个基础的单位换算功能,用户可自主输入数字和切换运算单位实现单位换算。
若要运行此代码,需要在 Qt Designer 中设计 ui 界面并且编译,在本代码的目录下创建一个子目录 QT 用于存储编译后的 py 文件,确保静态编译文件名和文件中的 class 名与代码对应,且每个控件的属性名与代码中相同。

from PySide6.QtWidgets import QApplication,QWidget,QMessageBox
from QT.Ui_converter import Ui_converter
class MyWindow (QWidget,Ui_converter):
    def __init__(self):
        super ().__init__()
        self.setupUi (self)
        # 字典存储数据类型,标准化处理
        self.lengthVar = {' 厘米 ':1,' 分米 ':10,' 米 ':100,' 千米 ':100000}
        self.weightVar = {' 克 ':1,' 斤 ':500,' 千克 ':1000}
        self.TypeDict = {' 长度 ':self.lengthVar,' 质量 ':self.weightVar}
        self.oneInputComboBox.addItems (self.lengthVar.keys ())
        self.twoInputComboBox.addItems (self.lengthVar.keys ())
        self.dataTypeComboBox.addItems (self.TypeDict.keys ())
        self.bind ()
    # 绑定函数
    def bind (self):
        self.oneInputEditLine.editingFinished.connect (self.NumberChanged)
        self.twoInputEditLine.editingFinished.connect (self.NumberChanged)
        self.dataTypeComboBox.currentTextChanged.connect (self.dataTypeChanged)
        self.oneInputComboBox.currentIndexChanged.connect (self.NumberChanged)
        self.twoInputComboBox.currentIndexChanged.connect (self.NumberChanged)
    # 取消绑定
    def unbind (self):
        self.oneInputEditLine.editingFinished.disconnect (self.NumberChanged)
        self.twoInputEditLine.editingFinished.disconnect (self.NumberChanged)
        self.dataTypeComboBox.currentTextChanged.disconnect (self.dataTypeChanged)
        self.oneInputComboBox.currentIndexChanged.disconnect (self.NumberChanged)
        self.twoInputComboBox.currentIndexChanged.disconnect (self.NumberChanged)
    # 计算更新 Edit 的内容
    def NumberChanged (self):
        # 获取当前计算类型,长度还是质量
        currentType = self.dataTypeComboBox.currentText ()
        # 获取当前是谁发来的信号
        currentSender = self.sender ()
        if currentSender in (self.oneInputEditLine, self.oneInputComboBox):
            value = self.oneInputEditLine.text () or 0
            currentOctal = self.oneInputComboBox
            anotherOctal = self.twoInputComboBox
            anotherSender = self.twoInputEditLine
        else: 
            value = self.twoInputEditLine.text () or 0
            currentOctal = self.twoInputComboBox
            anotherOctal = self.oneInputComboBox
            anotherSender = self.oneInputEditLine
        # 计算数值然后放入每一个输入框
        try:
            self.oneNumber = float (value)
        except:
            QMessageBox.information (self,' 提示 ',' 请输入正确的数字 ')
            return
        # one num = 50 * 10  two 500 / 1
        self.oneNumber = self.oneNumber * self.TypeDict [currentType][currentOctal.currentText ()]
        self.twoNumber = self.oneNumber/self.TypeDict [currentType][anotherOctal.currentText ()]
        self.originDataLabel.setText (f'{value}{currentOctal.currentText ()} =')
        self.transDataLabel.setText (f'{self.twoNumber}{anotherOctal.currentText ()}')
        anotherSender.setText (str (self.twoNumber))
    # 切换计算类型
    def dataTypeChanged (self,Type):
        self.unbind ()
        self.oneInputComboBox.clear ()
        self.twoInputComboBox.clear ()
        self.oneInputEditLine.clear ()
        self.twoInputEditLine.clear ()
        if Type == ' 质量 ':
            self.oneInputComboBox.addItems (self.weightVar)
            self.twoInputComboBox.addItems (self.weightVar)
        else:
            self.oneInputComboBox.addItems (self.lengthVar)
            self.twoInputComboBox.addItems (self.lengthVar)
        self.bind ()
if __name__ == '__main__':
    app = QApplication ([])
    windows = MyWindow ()
    windows.show ()
    app.exec ()

# 翻译器

实现一个翻译器,用户在左侧输入框输入内容,点击翻译按钮后会将内容翻译成目标语言输出在右侧,下方还会输出语句示例。翻译所用的 API 可以对接有道 API,但在本次项目中,我使用本地部署 ollama 的 gemma:2b 语言模型提供翻译。

# 编译和导入

在 Qt Designer 中设计一个简单的进制转换界面,保存在 项目名 \QT\ 目录下的 translate.ui,使用 vscode 插件,右键 ui 文件选择 PYQT: Compile Form 静态编译文件。

编译完成后的文件名为 Ui_converter.py,将文件中的 class Ui_Form (object) 的类名更换为 class Ui_translate (object) ,import 导入。

from PySide6.QtWidgets import QApplication, QWidget
from QT.Ui_translate import Ui_translate
from ai import aitrain

# 翻译模块

翻译模块写在 ai.py 的 aitrain 类中,通过调用 ollama 并且使用 prompt 让 gemma:2b 对用户输入的语言进行翻译,该类接收三个参数 forLanguage:输入语言类型,toLanguage:输出语言类型,text:输入语言内容。以下代码实现了一个非常简单的翻译效果。

import ollama
class aitrain:
    def translateWord (self, forLanguage, toLanguage, text ):
        forLag, toLag, content = forLanguage, toLanguage, text 
        if forLag == ' 自动 ':
            forLag = ''
        
        prompt = f'''
        请将以下句子中的 {forLag} 内容翻译成 {toLag}, 请注意,你只能输出需要翻译的句子的翻译后内容,不能输出其他内容。需要翻译的句子:{content}
        '''
        try:
            response = ollama.chat (model='gemma:2b', messages=[
            {
                'role': 'user',
                'content': prompt,
            },
        ])
            result = response ['message']['content']
        except Exception as e:
        # 如果发生错误,记录错误信息
            result = f"Error: {e}"
        return result

# 完整代码

主界面的实现较为简单,实现基础的绑定后,调用 ai.py 里的翻译类进行翻译,返回内容直接填入两个 Edit 中,下面给出完整代码。
此处在文本框中添加了占位符 self.plainTextEdit.setPlaceholderText () ,在文本框中无内容时,会出现灰色的提示内容。

from PySide6.QtWidgets import QApplication, QWidget
from QT.Ui_translate import Ui_translate
from ai import aitrain
class MyWindow (QWidget, Ui_translate):
    def __init__(self):
        super ().__init__()
        self.setupUi (self)
        self.radioButton.setChecked (True)
        # 0:自动,1:汉语,2:英语,3:日语
        self.fromLanguage = ' 自动 '
        # 设置占位符
        self.plainTextEdit.setPlaceholderText (' 请输入需要翻译的内容 ')
        self.bind ()
    def bind (self):
        self.pushButton.clicked.connect (self.translate)
        self.radioButton.clicked.connect (lambda:self.setFromlanguage (' 自动 '))
        self.radioButton_2.clicked.connect (lambda:self.setFromlanguage (' 汉语 '))
        self.radioButton_3.clicked.connect (lambda:self.setFromlanguage (' 英语 '))
        self.comboBox.setCurrentText (' 英语 ')
        
        
    def translate (self):
        # 获取输入文本
        inputText = self.plainTextEdit.toPlainText ()
        at = aitrain ()
        result = at.translateWord (self.fromLanguage, self.comboBox.currentText (), inputText)
        # 将结果放入两个文本框
        self.plainTextEdit_2.setPlainText (result)
        self.plainTextEdit_3.setPlainText (result)
    def setFromlanguage (self,language):
        self.fromLanguage = language
    
    def setToLanguage (self,language):
        self.translate (language)
        print (language)
if __name__ == '__main__':
    app = QApplication ()
    Window = MyWindow ()
    Window.show ()
    app.exec ()

# 启动界面和图像模糊

本次制作一个启动界面和一个图像模糊的软件,软件可以导入图片,对图片进行高斯模糊然后保存,本次不涉及 Qt Designer 的界面制作,所有界面均直接由代码完成。

# 导入

本次导入了 PIL 库用做图像处理,Image 用于打开图像,ImageFilter 用于图片模糊处理,ImageQt 用于将图片转换为可识别的 Qt 格式

from PySide6.QtWidgets import QApplication, QLabel, QWidget, QSlider, QVBoxLayout, QPushButton, QFileDialog, QGridLayout
from PySide6.QtCore import Qt, QTimer
from PySide6.QtGui import QFont, QPixmap
from PIL import ImageQt,ImageFilter, Image

# 启动界面

本次使用图片加文字的方式作为启动界面,运行程序时,会先打开启动界面,延时两秒然后进入主界面。设置无边框窗口并放入图片,同时加入了鼠标的拖动事件。

class LoadingWindow (QWidget):
    def __init__(self):
        super ().__init__()
        # 设置窗口标志为无边框窗口
        self.setWindowFlag (Qt.WindowType.FramelessWindowHint)
        # 设置窗口熟悉为半透明
        self.setAttribute (Qt.WidgetAttribute.WA_TranslucentBackground)
        self.mousePressed = False # 鼠标是否被按下
        self.offsetX = 0 # 鼠标相对于窗口的 X 轴偏移量
        self.offsetY = 0 # 鼠标相对于窗口的 Y 轴偏移量
        # 创建 QPixmap 对象并加载指定路径图片
        self.pixmap = QPixmap ("./log.png")
        # 获取 QPixmap 对象的尺寸
        self.size = self.pixmap.size ()
        self.pic = QLabel (self)
        self.pic.setFixedSize (400,300)
        # 将加载的图片设置到 QLabel 控件中
        self.pic.setPixmap (self.pixmap)
        # 设置图片居中对齐
        self.pic.setAlignment (Qt.AlignmentFlag.AlignCenter)
        # 启用 QLabel 的缩放功能,使图片缩放适应 QLabel
        self.pic.setScaledContents (True)
        self.label = QLabel (' 加载中...')
        self.label.setFont (QFont ("微软雅黑",20))
        
        self.mainlayout = QGridLayout ()
        self.mainlayout.addWidget (self.pic,0,0,3,3)
        self.mainlayout.addWidget (self.label,2,2)
        self.setLayout (self.mainlayout)
        # 定义单次计时器,延迟 2000 毫秒
        QTimer.singleShot (2000,self.openmainwindow)

# 主界面

主界面会提供了一个 btn 用于打开指定路径下的图片,同时有一个滑条来控制图片的模糊程度,另一个 btn 则可以将处理后的图进行保存。

# 完整代码

from PySide6.QtWidgets import QApplication, QLabel, QWidget, QSlider, QVBoxLayout, QPushButton, QFileDialog, QGridLayout
from PySide6.QtCore import Qt, QTimer
from PySide6.QtGui import QFont, QPixmap
from PIL import ImageQt,ImageFilter, Image
class LoadingWindow (QWidget):
    def __init__(self):
        super ().__init__()
        self.setWindowFlag (Qt.WindowType.FramelessWindowHint)
        self.setAttribute (Qt.WidgetAttribute.WA_TranslucentBackground)
        self.mousePressed = False # 鼠标是否被按下
        self.offsetX = 0 # 鼠标相对于窗口的 X 轴偏移量
        self.offsetY = 0 # 鼠标相对于窗口的 Y 轴偏移量
        self.pixmap = QPixmap (".\log.png")
        self.size = self.pixmap.size ()
        self.pic = QLabel (self)
        self.pic.setFixedSize (400,300)
        self.pic.setPixmap (self.pixmap)
        self.pic.setAlignment (Qt.AlignmentFlag.AlignCenter)
        self.pic.setScaledContents (True)
        self.label = QLabel (' 加载中...')
        self.label.setFont (QFont ("微软雅黑",20))
        
        self.mainlayout = QGridLayout ()
        self.mainlayout.addWidget (self.pic,0,0,3,3)
        self.mainlayout.addWidget (self.label,2,2)
        self.setLayout (self.mainlayout)
        QTimer.singleShot (2000,self.openmainwindow)
    
    def openmainwindow (self):
        self.close ()
        self.mainlwindow = MyWindow ()
        self.mainlwindow.show ()
    
    # 鼠标事件,用户点击鼠标左键自动调用
    def mousePressEvent (self, event):
        if event.button () == Qt.LeftButton:
            self.mousePressed = True
            self.offsetX = event.globalPosition ().x () - self.pos ().x ()
            self.offsetY = event.globalPosition ().y () - self.pos ().y ()
    
    # 鼠标事件,用户移动鼠标自动调用
    def mouseMoveEvent (self, event):
        if self.mousePressed:
            x = event.globalPosition ().x () - self.offsetX
            y = event.globalPosition ().y () - self.offsetY
            self.move (x, y)
    # 鼠标事件,用户释放鼠标左键自动调用    
    def mouseReleaseEvent (self, event):
        if event.button () == Qt.LeftButton:
            self.mousePressed = False
class MyWindow (QWidget):
    def __init__(self):
        super ().__init__()
        self.resize (500, 400)
        self.imgshow = QLabel ()
        # 设置图片显示区域大小 (此设置不会缩放图片)
        self.imgshow.setFixedSize (480,380)
        
        self.btn = QPushButton (' 导入图片 ')
        self.btn.clicked.connect (self.getImg)
        self.btn2 = QPushButton (' 保存图片 ')
        self.btn2.clicked.connect (self.saveImg)
        self.slider = QSlider (Qt.Orientation.Horizontal)
        self.slider.setRange (0,100)
        self.slider.setTickPosition (QSlider.TickPosition.TicksBelow)
        # 设置刻度间隔
        self.slider.setTickInterval (10)
        self.slider.valueChanged.connect (self.resizeImage)
        self.mainlayout = QVBoxLayout ()
        self.mainlayout.addWidget (self.btn)
        self.mainlayout.addWidget (self.btn2)
        self.mainlayout.addWidget (self.slider)
        self.mainlayout.addWidget (self.imgshow)
        self.setLayout (self.mainlayout)
        self.img = None
        self.pic = None
    def getImg (self):
        # 获取图像路径
        Image_Pach,_ = QFileDialog.getOpenFileName (self,' 选择图像 ','',' 图像文件 (*.png *.jpg *.imge)')
        if Image_Pach:
            self.img = Image.open (Image_Pach)
            # 调整图片大小
            self.resizeImage (self.slider.value ())
    
    def saveImg (self):
        if self.pic:
            save_path, _ = QFileDialog.getSaveFileName (self,' 保存图片 ','',' 图像文件 (*.png *.jpg *.jpeg)')
            if save_path:
                self.pic.save (save_path)
    def resizeImage (self,value):
        if self.img:
            # 计算缩放比例
            w_ratio = self.imgshow.width () /self.img.width
            h_ratio = self.imgshow.height () /self.img.height
            scale_ratio = min (w_ratio, h_ratio)
            # 缩放图片,模糊图片
            new_size = (int (self.img.width * scale_ratio), int (self.img.height * scale_ratio))
            self.pic = self.img.filter (ImageFilter.GaussianBlur (value)).resize (new_size, Image.ANTIALIAS)
            # 显示图片
            self.imgshow.setPixmap (ImageQt.toqpixmap (self.pic))
if __name__ == '__main__':
    app = QApplication ()
    Window = LoadingWindow ()
    Window.show ()
    app.exec ()

# 动态控件

本次以一个实际的例子来讲解动态创建和操作控件:在用户购买商品时,创建一个产品选型的界面供用户选择合适的产品,假定用户需要选择的内容有:产品,配件,授权,服务。产品默认显示 comboBox, 配件,授权,服务如有需要,需要点击 Btn 来创建一个 comboBox 供用户选择,在 py designer 中,设计界面如下:

本次使用了 PySide6-Fluent-Widgets 组件库,该组件库提供免费版本,但是无法在 designer 中直接显示组件效果。

在本次设计中,展示的设计界面只是为了方便看到整体布局,实际上还需要更改部分 ui 类的内容,包括给按钮添加图标,删除选择按钮上的行等,同时建议手写 ui 代码便于理解,编写代码时注意对相同类型的控件命名最好有规范性,这涉及到后面对动态控件的处理。

# UI 创建

ui 文件将会实现整体 ui 界面,以下给出 ui 文件的代码以及给出运行效果:

在图 ui 中,你会发现 ' 添加 XX' 的按钮上方的行被删除,该行将在用户点击按钮时候才会显示,同样的,相关代码会被注释掉,以下为选型窗口 ui ProductSelectionUI

from PySide6 import QtCore, QtGui, QtWidgets
import qfluentwidgets
from qfluentwidgets import CaptionLabel, HorizontalSeparator, PrimaryToolButton, PushButton, ScrollArea, SpinBox, FluentIcon
class Ui_ProductSelectionUI (object):
    def setupUi (self, Form):
        Form.setObjectName ("Form")
        Form.resize (540, 739)
        self.verticalLayout_2 = QtWidgets.QVBoxLayout (Form)
        self.verticalLayout_2.setObjectName ("verticalLayout_2")
        self.VerticalLayout = QtWidgets.QVBoxLayout ()
        self.VerticalLayout.setObjectName ("VerticalLayout")
        # 滚动窗口
        self.ScrollArea = ScrollArea (Form)
        self.ScrollArea.setWidgetResizable (True)
        self.ScrollArea.setObjectName ("ScrollArea")
        # 滚动窗口的 QWidget
        self.ScrollAreaWidgetContents = QtWidgets.QWidget ()
        self.ScrollAreaWidgetContents.setGeometry (QtCore.QRect (0, 0, 518, 629))
        self.ScrollAreaWidgetContents.setObjectName ("ScrollAreaWidgetContents")
        self.verticalLayout_4 = QtWidgets.QVBoxLayout (self.ScrollAreaWidgetContents)
        self.verticalLayout_4.setContentsMargins (0, 0, 0, 0)
        self.verticalLayout_4.setObjectName ("verticalLayout_4")
        # 滚动窗口的整体垂直布局控件,滚动窗口的每一行都添加到此控件
        self.ScrollVerticalLayout = QtWidgets.QVBoxLayout ()
        self.ScrollVerticalLayout.setObjectName ("ScrollVerticalLayout")
        # 产品分割线的行布局容器
        self.ProductDividerHorizontalLayout = QtWidgets.QHBoxLayout ()
        self.ProductDividerHorizontalLayout.setSizeConstraint (QtWidgets.QLayout.SetDefaultConstraint)
        self.ProductDividerHorizontalLayout.setObjectName ("ProductDividerHorizontalLayout")
        # 产品分割线行的左分割线
        self.ProductLeftHorizontalSeparator = HorizontalSeparator (self.ScrollAreaWidgetContents)
        self.ProductLeftHorizontalSeparator.setMaximumSize (QtCore.QSize (5, 3))
        self.ProductLeftHorizontalSeparator.setObjectName ("ProductLeftHorizontalSeparator")
        self.ProductDividerHorizontalLayout.addWidget (self.ProductLeftHorizontalSeparator)
        # 产品信息
        self.ProductCaptionLabel = CaptionLabel (self.ScrollAreaWidgetContents)
        # 控制 QWidget 大小策略的类
        sizePolicy = QtWidgets.QSizePolicy (QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
        sizePolicy.setHorizontalStretch (0)
        sizePolicy.setVerticalStretch (0)
        sizePolicy.setHeightForWidth (self.ProductCaptionLabel.sizePolicy ().hasHeightForWidth ())
        self.ProductCaptionLabel.setSizePolicy (sizePolicy)
        self.ProductCaptionLabel.setMinimumSize (QtCore.QSize (50, 20))
        self.ProductCaptionLabel.setMaximumSize (QtCore.QSize (200, 20))
        self.ProductCaptionLabel.setObjectName ("ProductCaptionLabel")
        self.ProductDividerHorizontalLayout.addWidget (self.ProductCaptionLabel)
        # 产品分割线行的右分割线
        self.ProductRightHorizontalSeparator = HorizontalSeparator (self.ScrollAreaWidgetContents)
        self.ProductRightHorizontalSeparator.setMinimumSize (QtCore.QSize (5, 3))
        self.ProductRightHorizontalSeparator.setMaximumSize (QtCore.QSize (300, 3))
        self.ProductRightHorizontalSeparator.setObjectName ("ProductRightHorizontalSeparator")
        self.ProductDividerHorizontalLayout.addWidget (self.ProductRightHorizontalSeparator)
        self.ScrollVerticalLayout.addLayout (self.ProductDividerHorizontalLayout)
        # 产品选择行的布局容器
        self.ProductSelectHorizontalLayout = QtWidgets.QHBoxLayout ()
        self.ProductSelectHorizontalLayout.setObjectName ("ProductSelectHorizontalLayout")
        spacerItem = QtWidgets.QSpacerItem (5, 20, QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Minimum)
        self.ProductSelectHorizontalLayout.addItem (spacerItem)
        # 产品选择行的 ComboBox
        self.ProductComboBox = qfluentwidgets.ComboBox (self.ScrollAreaWidgetContents)
        self.ProductComboBox.setObjectName ("ProductComboBox")
        self.ProductComboBox.addItem ("A 产品")
        self.ProductComboBox.addItem ("B 产品")
        self.ProductComboBox.addItem ("C 产品")
        self.ProductComboBox.addItem ("D 产品")
        self.ProductComboBox.addItem ("E 产品")
        self.ProductSelectHorizontalLayout.addWidget (self.ProductComboBox)
        # 产品选择行的 SpinBox
        self.ProductSpinBox = SpinBox (self.ScrollAreaWidgetContents)
        self.ProductSpinBox.setMaximumSize (QtCore.QSize (150, 33))
        self.ProductSpinBox.setMinimum (1)
        self.ProductSpinBox.setProperty ("transparent", True)
        self.ProductSpinBox.setObjectName ("ProductSpinBox")
        self.ProductSelectHorizontalLayout.addWidget (self.ProductSpinBox)
        # 产品选择行的确认按钮
        self.ProductConfirmPrimaryToolButton = PrimaryToolButton (self.ScrollAreaWidgetContents)
        self.ProductConfirmPrimaryToolButton.setIcon (FluentIcon.ACCEPT_MEDIUM)
        self.ProductConfirmPrimaryToolButton.setMaximumSize (QtCore.QSize (32, 30))
        self.ProductConfirmPrimaryToolButton.setObjectName ("ProductConfirmPrimaryToolButton")
        self.ProductSelectHorizontalLayout.addWidget (self.ProductConfirmPrimaryToolButton)
        # 产品选择行的取消按钮
        self.ProductCancellationPrimaryToolButton = PrimaryToolButton (self.ScrollAreaWidgetContents)
        self.ProductCancellationPrimaryToolButton.setIcon (FluentIcon.CANCEL_MEDIUM)
        self.ProductCancellationPrimaryToolButton.setMaximumSize (QtCore.QSize (32, 30))
        self.ProductCancellationPrimaryToolButton.setObjectName ("ProductCancellationPrimaryToolButton")
        self.ProductSelectHorizontalLayout.addWidget (self.ProductCancellationPrimaryToolButton)
        # 创建弹簧
        spacerItem1 = QtWidgets.QSpacerItem (5, 20, QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Minimum)
        self.ProductSelectHorizontalLayout.addItem (spacerItem1)
        self.ScrollVerticalLayout.addLayout (self.ProductSelectHorizontalLayout)
        # 授权分割线行的布局容器
        self.AuthorisationDividerHorizontalLayout = QtWidgets.QHBoxLayout ()
        self.AuthorisationDividerHorizontalLayout.setObjectName ("AuthorisationDividerHorizontalLayout")
        # 授权分割线的左侧分割线
        self.AuthorisationLeftHorizontalSeparator = HorizontalSeparator (self.ScrollAreaWidgetContents)
        self.AuthorisationLeftHorizontalSeparator.setMaximumSize (QtCore.QSize (5, 3))
        self.AuthorisationLeftHorizontalSeparator.setObjectName ("AuthorisationLeftHorizontalSeparator")
        self.AuthorisationDividerHorizontalLayout.addWidget (self.AuthorisationLeftHorizontalSeparator)
        # 授权分割线行的 Label
        self.AuthorisationCaptionLabel = CaptionLabel (self.ScrollAreaWidgetContents)
        self.AuthorisationCaptionLabel.setMinimumSize (QtCore.QSize (50, 20))
        self.AuthorisationCaptionLabel.setMaximumSize (QtCore.QSize (200, 20))
        self.AuthorisationCaptionLabel.setObjectName ("AuthorisationCaptionLabel")
        self.AuthorisationDividerHorizontalLayout.addWidget (self.AuthorisationCaptionLabel)
        # 授权分割线的右分割线
        self.AuthorisationRightHorizontalSeparator = HorizontalSeparator (self.ScrollAreaWidgetContents)
        self.AuthorisationRightHorizontalSeparator.setMinimumSize (QtCore.QSize (5, 3))
        self.AuthorisationRightHorizontalSeparator.setMaximumSize (QtCore.QSize (300, 3))
        self.AuthorisationRightHorizontalSeparator.setObjectName ("AuthorisationRightHorizontalSeparator")
        self.AuthorisationDividerHorizontalLayout.addWidget (self.AuthorisationRightHorizontalSeparator)
        self.ScrollVerticalLayout.addLayout (self.AuthorisationDividerHorizontalLayout)
        # # 授权选择行的布局容器
        # self.AuthorisationSelectHorizontalLayout1 = QtWidgets.QHBoxLayout ()
        # self.AuthorisationSelectHorizontalLayout1.setObjectName ("AuthorisationSelectHorizontalLayout1")
        # spacerItem2 = QtWidgets.QSpacerItem (5, 20, QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Minimum)
        # self.AuthorisationSelectHorizontalLayout1.addItem (spacerItem2)
        # # 授权选择行的 ComboBox
        # self.AuthorisationComboBox1 = qfluentwidgets.ComboBox (self.ScrollAreaWidgetContents)
        # self.AuthorisationComboBox1.setObjectName ("AuthorisationComboBox1")
        # self.AuthorisationComboBox1.addItem ("模块 A 授权")
        # self.AuthorisationComboBox1.addItem ("模块 B 授权")
        # self.AuthorisationComboBox1.addItem ("模块 C 授权")
        # self.AuthorisationSelectHorizontalLayout1.addWidget (self.AuthorisationComboBox1)
        # # 授权选择行的 SpinBox
        # self.AuthorisationSpinBox1 = SpinBox (self.ScrollAreaWidgetContents)
        # self.AuthorisationSpinBox1.setMaximumSize (QtCore.QSize (150, 33))
        # self.AuthorisationSpinBox1.setMinimum (1)
        # self.AuthorisationSpinBox1.setObjectName ("AuthorisationSpinBox1")
        # self.AuthorisationSelectHorizontalLayout1.addWidget (self.AuthorisationSpinBox1)
        # # 授权选择行的确认按钮
        # self.AuthorisationConfirmPrimaryToolButton1 = PrimaryToolButton (self.ScrollAreaWidgetContents)
        # self.AuthorisationConfirmPrimaryToolButton1.setIcon (FluentIcon.ACCEPT_MEDIUM)
        # self.AuthorisationConfirmPrimaryToolButton1.setMaximumSize (QtCore.QSize (32, 30))
        # self.AuthorisationConfirmPrimaryToolButton1.setObjectName ("AuthorisationConfirmPrimaryToolButton1")
        # self.AuthorisationSelectHorizontalLayout1.addWidget (self.AuthorisationConfirmPrimaryToolButton1)
        # # 授权选择行的取消按钮
        # self.AuthorisationCancellationPrimaryToolButton1 = PrimaryToolButton (self.ScrollAreaWidgetContents)
        # self.AuthorisationCancellationPrimaryToolButton1.setIcon (FluentIcon.CANCEL_MEDIUM)
        # self.AuthorisationCancellationPrimaryToolButton1.setMaximumSize (QtCore.QSize (32, 30))
        # self.AuthorisationCancellationPrimaryToolButton1.setObjectName ("AuthorisationCancellationPrimaryToolButton1")
        # self.AuthorisationSelectHorizontalLayout1.addWidget (self.AuthorisationCancellationPrimaryToolButton1)
        # spacerItem3 = QtWidgets.QSpacerItem (5, 20, QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Minimum)
        # self.AuthorisationSelectHorizontalLayout1.addItem (spacerItem3)
        # self.ScrollVerticalLayout.addLayout (self.AuthorisationSelectHorizontalLayout1)
        
        # 增加授权选择行 的按钮布局容器
        self.AuthorisationAddHorizontalLayout = QtWidgets.QHBoxLayout ()
        self.AuthorisationAddHorizontalLayout.setObjectName ("AuthorisationAddHorizontalLayout")
        spacerItem4 = QtWidgets.QSpacerItem (5, 20, QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Minimum)
        self.AuthorisationAddHorizontalLayout.addItem (spacerItem4)
        # 增加授权选择行的按钮
        self.AuthorisationPushButton = PushButton (self.ScrollAreaWidgetContents)
        self.AuthorisationPushButton.setObjectName ("AuthorisationPushButton")
        self.AuthorisationAddHorizontalLayout.addWidget (self.AuthorisationPushButton)
        spacerItem5 = QtWidgets.QSpacerItem (5, 20, QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Minimum)
        self.AuthorisationAddHorizontalLayout.addItem (spacerItem5)
        self.ScrollVerticalLayout.addLayout (self.AuthorisationAddHorizontalLayout)
        # 配件分割线行的布局容器
        self.ModuleDividerHorizontalLayout = QtWidgets.QHBoxLayout ()
        self.ModuleDividerHorizontalLayout.setObjectName ("ModuleDividerHorizontalLayout")
        self.ModuleLeftHorizontalSeparator = HorizontalSeparator (self.ScrollAreaWidgetContents)
        self.ModuleLeftHorizontalSeparator.setMaximumSize (QtCore.QSize (5, 3))
        self.ModuleLeftHorizontalSeparator.setObjectName ("ModuleLeftHorizontalSeparator")
        self.ModuleDividerHorizontalLayout.addWidget (self.ModuleLeftHorizontalSeparator)
        self.ModuleCaptionLabel = CaptionLabel (self.ScrollAreaWidgetContents)
        self.ModuleCaptionLabel.setMinimumSize (QtCore.QSize (50, 20))
        self.ModuleCaptionLabel.setMaximumSize (QtCore.QSize (200, 20))
        self.ModuleCaptionLabel.setObjectName ("ModuleCaptionLabel")
        self.ModuleDividerHorizontalLayout.addWidget (self.ModuleCaptionLabel)
        self.ModuleRightHorizontalSeparator = HorizontalSeparator (self.ScrollAreaWidgetContents)
        sizePolicy = QtWidgets.QSizePolicy (QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Maximum)
        sizePolicy.setHorizontalStretch (0)
        sizePolicy.setVerticalStretch (0)
        sizePolicy.setHeightForWidth (self.ModuleRightHorizontalSeparator.sizePolicy ().hasHeightForWidth ())
        self.ModuleRightHorizontalSeparator.setSizePolicy (sizePolicy)
        self.ModuleRightHorizontalSeparator.setMinimumSize (QtCore.QSize (5, 3))
        self.ModuleRightHorizontalSeparator.setMaximumSize (QtCore.QSize (300, 3))
        self.ModuleRightHorizontalSeparator.setObjectName ("ModuleRightHorizontalSeparator")
        self.ModuleDividerHorizontalLayout.addWidget (self.ModuleRightHorizontalSeparator)
        self.ScrollVerticalLayout.addLayout (self.ModuleDividerHorizontalLayout)
        # # 配件选择行布局
        # self.ModuleSelectHorizontalLayout1 = QtWidgets.QHBoxLayout ()
        # self.ModuleSelectHorizontalLayout1.setObjectName ("ModuleSelectHorizontalLayout1")
        # spacerItem6 = QtWidgets.QSpacerItem (5, 20, QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Minimum)
        # self.ModuleSelectHorizontalLayout1.addItem (spacerItem6)
        # self.ModuleComboBox1 = qfluentwidgets.ComboBox (self.ScrollAreaWidgetContents)
        # self.ModuleComboBox1.setObjectName ("ModuleComboBox1")
        # self.ModuleComboBox1.addItem ("A 配件")
        # self.ModuleComboBox1.addItem ("B 配件")
        # self.ModuleComboBox1.addItem ("C 配件")
        # self.ModuleSelectHorizontalLayout1.addWidget (self.ModuleComboBox1)
        # self.ModuleSpinBox1 = SpinBox (self.ScrollAreaWidgetContents)
        # self.ModuleSpinBox1.setMaximumSize (QtCore.QSize (150, 33))
        # self.ModuleSpinBox1.setMinimum (1)
        # self.ModuleSpinBox1.setObjectName ("ModuleSpinBox1")
        # self.ModuleSelectHorizontalLayout1.addWidget (self.ModuleSpinBox1)
        # # 配件选择行的确认按钮
        # self.ModuleConfirmPrimaryToolButton1 = PrimaryToolButton (self.ScrollAreaWidgetContents)
        # self.ModuleConfirmPrimaryToolButton1.setIcon (FluentIcon.ACCEPT_MEDIUM)
        # self.ModuleConfirmPrimaryToolButton1.setMaximumSize (QtCore.QSize (32, 30))
        # self.ModuleConfirmPrimaryToolButton1.setObjectName ("ModuleConfirmPrimaryToolButton1")
        # self.ModuleSelectHorizontalLayout1.addWidget (self.ModuleConfirmPrimaryToolButton1)
        # # 配件选择行的取消按钮
        # self.ModuleCancellationPrimaryToolButton1 = PrimaryToolButton (self.ScrollAreaWidgetContents)
        # self.ModuleCancellationPrimaryToolButton1.setIcon (FluentIcon.CANCEL_MEDIUM)
        # self.ModuleCancellationPrimaryToolButton1.setMaximumSize (QtCore.QSize (32, 30))
        # self.ModuleCancellationPrimaryToolButton1.setObjectName ("ModuleCancellationPrimaryToolButton1")
        # self.ModuleSelectHorizontalLayout1.addWidget (self.ModuleCancellationPrimaryToolButton1)
        # spacerItem7 = QtWidgets.QSpacerItem (5, 20, QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Minimum)
        # self.ModuleSelectHorizontalLayout1.addItem (spacerItem7)
        # self.ScrollVerticalLayout.addLayout (self.ModuleSelectHorizontalLayout1)
        # 服务分割行
        self.ModuleAddHorizontalLayout = QtWidgets.QHBoxLayout ()
        self.ModuleAddHorizontalLayout.setObjectName ("ModuleAddHorizontalLayout")
        spacerItem8 = QtWidgets.QSpacerItem (5, 20, QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Minimum)
        self.ModuleAddHorizontalLayout.addItem (spacerItem8)
        self.ModulePushButton = PushButton (self.ScrollAreaWidgetContents)
        self.ModulePushButton.setObjectName ("ModulePushButton")
        self.ModuleAddHorizontalLayout.addWidget (self.ModulePushButton)
        spacerItem9 = QtWidgets.QSpacerItem (5, 20, QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Minimum)
        self.ModuleAddHorizontalLayout.addItem (spacerItem9)
        self.ScrollVerticalLayout.addLayout (self.ModuleAddHorizontalLayout)
        self.ServiceDividerHorizontalLayout = QtWidgets.QHBoxLayout ()
        self.ServiceDividerHorizontalLayout.setObjectName ("ServiceDividerHorizontalLayout")
        self.ServiceLeftHorizontalSeparator = HorizontalSeparator (self.ScrollAreaWidgetContents)
        self.ServiceLeftHorizontalSeparator.setMaximumSize (QtCore.QSize (5, 3))
        self.ServiceLeftHorizontalSeparator.setObjectName ("ServiceLeftHorizontalSeparator")
        self.ServiceDividerHorizontalLayout.addWidget (self.ServiceLeftHorizontalSeparator)
        self.ServiceCaptionLabel = CaptionLabel (self.ScrollAreaWidgetContents)
        sizePolicy = QtWidgets.QSizePolicy (QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
        sizePolicy.setHorizontalStretch (0)
        sizePolicy.setVerticalStretch (0)
        sizePolicy.setHeightForWidth (self.ServiceCaptionLabel.sizePolicy ().hasHeightForWidth ())
        self.ServiceCaptionLabel.setSizePolicy (sizePolicy)
        self.ServiceCaptionLabel.setMinimumSize (QtCore.QSize (50, 20))
        self.ServiceCaptionLabel.setMaximumSize (QtCore.QSize (200, 20))
        self.ServiceCaptionLabel.setObjectName ("ServiceCaptionLabel")
        self.ServiceDividerHorizontalLayout.addWidget (self.ServiceCaptionLabel)
        self.ServiceRightHorizontalSeparator = HorizontalSeparator (self.ScrollAreaWidgetContents)
        sizePolicy = QtWidgets.QSizePolicy (QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
        sizePolicy.setHorizontalStretch (0)
        sizePolicy.setVerticalStretch (0)
        sizePolicy.setHeightForWidth (self.ServiceRightHorizontalSeparator.sizePolicy ().hasHeightForWidth ())
        self.ServiceRightHorizontalSeparator.setSizePolicy (sizePolicy)
        self.ServiceRightHorizontalSeparator.setMinimumSize (QtCore.QSize (5, 3))
        self.ServiceRightHorizontalSeparator.setMaximumSize (QtCore.QSize (300, 3))
        self.ServiceRightHorizontalSeparator.setObjectName ("ServiceRightHorizontalSeparator")
        self.ServiceDividerHorizontalLayout.addWidget (self.ServiceRightHorizontalSeparator)
        self.ScrollVerticalLayout.addLayout (self.ServiceDividerHorizontalLayout)
        # # 服务选择行布局
        # self.ServiceSelectHorizontalLayout1 = QtWidgets.QHBoxLayout ()
        # self.ServiceSelectHorizontalLayout1.setObjectName ("ServiceSelectHorizontalLayout1")
        # spacerItem10 = QtWidgets.QSpacerItem (5, 20, QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Minimum)
        # self.ServiceSelectHorizontalLayout1.addItem (spacerItem10)
        # self.ServiceComboBox1 = qfluentwidgets.ComboBox (self.ScrollAreaWidgetContents)
        # self.ServiceComboBox1.setObjectName ("ServiceComboBox1")
        # self.ServiceComboBox1.addItem ("A 服务")
        # self.ServiceComboBox1.addItem ("B 服务")
        # self.ServiceSelectHorizontalLayout1.addWidget (self.ServiceComboBox1)
        # self.ServiceSpinBox1 = SpinBox (self.ScrollAreaWidgetContents)
        # self.ServiceSpinBox1.setMaximumSize (QtCore.QSize (150, 33))
        # self.ServiceSpinBox1.setMinimum (1)
        # self.ServiceSpinBox1.setObjectName ("ServiceSpinBox1")
        # self.ServiceSelectHorizontalLayout1.addWidget (self.ServiceSpinBox1)
        # # 服务选择行的确认按钮
        # self.ServiceConfirmPrimaryToolButton1 = PrimaryToolButton (self.ScrollAreaWidgetContents)
        # self.ServiceConfirmPrimaryToolButton1.setIcon (FluentIcon.ACCEPT_MEDIUM)
        # self.ServiceConfirmPrimaryToolButton1.setMaximumSize (QtCore.QSize (32, 30))
        # self.ServiceConfirmPrimaryToolButton1.setObjectName ("ServiceConfirmPrimaryToolButton1")
        # self.ServiceSelectHorizontalLayout1.addWidget (self.ServiceConfirmPrimaryToolButton1)
        # # 服务选择行的取消按钮
        # self.ServiceCancellationPrimaryToolButton1 = PrimaryToolButton (self.ScrollAreaWidgetContents)
        # self.ServiceCancellationPrimaryToolButton1.setIcon (FluentIcon.CANCEL_MEDIUM)
        # self.ServiceCancellationPrimaryToolButton1.setMaximumSize (QtCore.QSize (32, 30))
        # self.ServiceCancellationPrimaryToolButton1.setObjectName ("ServiceCancellationPrimaryToolButton1")
        # self.ServiceSelectHorizontalLayout1.addWidget (self.ServiceCancellationPrimaryToolButton1)
        # spacerItem11 = QtWidgets.QSpacerItem (5, 20, QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Minimum)
        # self.ServiceSelectHorizontalLayout1.addItem (spacerItem11)
        # self.ScrollVerticalLayout.addLayout (self.ServiceSelectHorizontalLayout1)
        self.ServiceAddHorizontalLayout = QtWidgets.QHBoxLayout ()
        self.ServiceAddHorizontalLayout.setObjectName ("ServiceAddHorizontalLayout")
        spacerItem12 = QtWidgets.QSpacerItem (5, 20, QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Minimum)
        self.ServiceAddHorizontalLayout.addItem (spacerItem12)
        self.ServicePushButton = PushButton (self.ScrollAreaWidgetContents)
        self.ServicePushButton.setObjectName ("ServicePushButton")
        self.ServiceAddHorizontalLayout.addWidget (self.ServicePushButton)
        spacerItem13 = QtWidgets.QSpacerItem (5, 20, QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Minimum)
        self.ServiceAddHorizontalLayout.addItem (spacerItem13)
        self.ScrollVerticalLayout.addLayout (self.ServiceAddHorizontalLayout)
        # 滚动框最下方的弹簧
        spacerItem14 = QtWidgets.QSpacerItem (20, 500, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Maximum)
        self.ScrollVerticalLayout.addItem (spacerItem14)
        self.verticalLayout_4.addLayout (self.ScrollVerticalLayout)
        self.ScrollArea.setWidget (self.ScrollAreaWidgetContents)
        self.VerticalLayout.addWidget (self.ScrollArea)
        spacerItem15 = QtWidgets.QSpacerItem (20, 20, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
        self.VerticalLayout.addItem (spacerItem15)
        self.LectotypeHorizontalLayout = QtWidgets.QHBoxLayout ()
        self.LectotypeHorizontalLayout.setContentsMargins (-1, 0, -1, -1)
        self.LectotypeHorizontalLayout.setObjectName ("LectotypeHorizontalLayout")
        spacerItem16 = QtWidgets.QSpacerItem (40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
        self.LectotypeHorizontalLayout.addItem (spacerItem16)
        self.LectotypeConfirmPushButton = PushButton (Form)
        sizePolicy = QtWidgets.QSizePolicy (QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
        sizePolicy.setHorizontalStretch (0)
        sizePolicy.setVerticalStretch (0)
        sizePolicy.setHeightForWidth (self.LectotypeConfirmPushButton.sizePolicy ().hasHeightForWidth ())
        self.LectotypeConfirmPushButton.setSizePolicy (sizePolicy)
        self.LectotypeConfirmPushButton.setMinimumSize (QtCore.QSize (80, 0))
        self.LectotypeConfirmPushButton.setObjectName ("LectotypeConfirmPushButton")
        self.LectotypeHorizontalLayout.addWidget (self.LectotypeConfirmPushButton)
        spacerItem17 = QtWidgets.QSpacerItem (40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
        self.LectotypeHorizontalLayout.addItem (spacerItem17)
        self.LectotypeCancellationPushButton = PushButton (Form)
        self.LectotypeCancellationPushButton.setMinimumSize (QtCore.QSize (80, 0))
        self.LectotypeCancellationPushButton.setObjectName ("LectotypeCancellationPushButton")
        self.LectotypeHorizontalLayout.addWidget (self.LectotypeCancellationPushButton)
        spacerItem18 = QtWidgets.QSpacerItem (40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
        self.LectotypeHorizontalLayout.addItem (spacerItem18)
        self.VerticalLayout.addLayout (self.LectotypeHorizontalLayout)
        spacerItem19 = QtWidgets.QSpacerItem (20, 15, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
        self.VerticalLayout.addItem (spacerItem19)
        self.verticalLayout_2.addLayout (self.VerticalLayout)
        self.retranslateUi (Form)
        QtCore.QMetaObject.connectSlotsByName (Form)
    def retranslateUi (self, Form):
        _translate = QtCore.QCoreApplication.translate
        Form.setWindowTitle (_translate ("Form", "Form"))
        self.ProductCaptionLabel.setText (_translate ("Form", "产品"))
        self.ProductComboBox.setText (_translate ("Form", "A 产品"))
        self.ProductComboBox.setProperty ("items_", _translate ("Form", "A 产品 \n"
"B 产品 \n"
"C 产品 \n"
"D 产品 \n"
"E 产品"))
        self.AuthorisationCaptionLabel.setText (_translate ("Form", "授权"))
#         self.AuthorisationComboBox1.setText (_translate ("Form", "模块 A 授权"))
#         self.AuthorisationComboBox1.setProperty ("items_", _translate ("Form", "模块 A 授权 \n"
# "模块 B 授权 \n"
# "模块 C 授权"))
        self.AuthorisationPushButton.setText (_translate ("Form", "添加授权"))
        self.ModuleCaptionLabel.setText (_translate ("Form", "配件"))
#         self.ModuleComboBox1.setText (_translate ("Form", "A 配件"))
#         self.ModuleComboBox1.setProperty ("items_", _translate ("Form", "A 配件 \n"
# "B 配件 \n"
# "C 配件"))
        self.ModulePushButton.setText (_translate ("Form", "添加配件"))
        self.ServiceCaptionLabel.setText (_translate ("Form", "服务"))
#         self.ServiceComboBox1.setText (_translate ("Form", "A 服务"))
#         self.ServiceComboBox1.setProperty ("items_", _translate ("Form", "A 服务 \n"
# "B 服务"))
        self.ServicePushButton.setText (_translate ("Form", "添加服务"))
        self.LectotypeConfirmPushButton.setText (_translate ("Form", "确认"))
        self.LectotypeCancellationPushButton.setText (_translate ("Form", "取消"))

# 创建动态控件

我们期望的效果是当用户点击 ' 添加 XX' 的按钮时,能够在按钮上方添加一行,新增的行包含 comboBox, spinBox 以及两个按钮,我们定义了一个函数 AddSelectHorizontalLayout 作为 ' 添加 XX' 按钮的槽函数,另外我们需要定义几个全局变量:

  • self.AuthorisationType 列表,Authorisation 的 comboBox 下拉框每一项的内容
  • self.AuthorisationNum = 4 按钮 ' 添加授权 ' 的布局所在行
  • self.ModuleType 列表,Module 的 comboBox 下拉框每一项的内容
  • self.ModuleNum = 6 按钮 ' 添加配件 ' 的布局所在行
  • self.ServiceType 列表,Service 的 comboBox 下拉框每一项的内容
  • self.ServiceNum = 8 按钮 ' 添加服务 ' 的布局所在行
  • self.ButtonCounter int, 用来记录创建了多少行,这个数字将会用于命名创建的控件
  • self.DynamicControlList list,存储创建的控件

现在,让我们实现动态创建功能:

def AddSelectHorizontalLayout (self, Control):
        # 定义控件类型与对应的数据列表
        ControlMap = {
            'Authorisation': (self.AuthorisationType, self.AuthorisationNum),
            'Module': (self.ModuleType, self.ModuleNum),
            'Service': (self.ServiceType, self.ServiceNum),
        }
        
        # 获取控件类型对应的数据列表和行索引
        DataList, InsertIndex = ControlMap [Control]
        # 调整为实际插入位置
        InsertIndex -= 1  
        # 更新索引
        if Control == 'Authorisation':
            # 更新 Add 按钮的全局变量
            self.AuthorisationNum += 1
            self.ModuleNum += 1
            self.ServiceNum += 1
        elif Control == 'Module':
            self.ModuleNum += 1
            self.ServiceNum += 1
        elif Control == 'Service':
            self.ServiceNum += 1
        # 创建控件,为控件设置唯一的 objectName
        comboBox = ComboBox (self.ScrollAreaWidgetContents)
        comboBox.setObjectName (f"ComboBox {self.ButtonCounter}")
        # 将 list 写入 ComboBox
        for type in DataList:
            comboBox.addItem (type)
        # 授权选择行的 SpinBox
        spinBox = SpinBox (self.ScrollAreaWidgetContents)
        spinBox.setMaximumSize (QtCore.QSize (150, 33))
        spinBox.setMinimum (1)
        spinBox.setObjectName (f"AuthorisationSpinBox {self.ButtonCounter}")
        ButtonLeft = PrimaryToolButton (self.ScrollAreaWidgetContents)
        ButtonLeft.setIcon (FluentIcon.ACCEPT_MEDIUM)
        ButtonLeft.setMaximumSize (QtCore.QSize (32, 30))
        ButtonLeft.setObjectName (f"ConfirmButton {self.ButtonCounter}")
        ButtonRight = PrimaryToolButton (self.ScrollAreaWidgetContents)
        ButtonRight.setIcon (FluentIcon.CANCEL_MEDIUM)
        ButtonRight.setMaximumSize (QtCore.QSize (32, 30))
        ButtonRight.setObjectName (f"CancellationButton {self.ButtonCounter}")
        # 创建两个弹簧
        SpacerItemLeft = QtWidgets.QSpacerItem (5, 20, QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Minimum)
        SpacerItemRight = QtWidgets.QSpacerItem (5, 20, QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Minimum)
        # 绑定按钮事件信号和槽
        ButtonLeft.clicked.connect (self.ClickPrimaryToolButton)
        ButtonRight.clicked.connect (self.ClickPrimaryToolButton)
        # 创建布局并添加到滚动框
        self.HorizontalLayout = QHBoxLayout ()
        self.HorizontalLayout.setObjectName (f"AddHorizontalLayout {self.ButtonCounter}")
        self.HorizontalLayout.addItem (SpacerItemLeft)
        self.HorizontalLayout.addWidget (comboBox)
        self.HorizontalLayout.addWidget (spinBox)
        self.HorizontalLayout.addWidget (ButtonLeft)
        self.HorizontalLayout.addWidget (ButtonRight)
        self.HorizontalLayout.addItem (SpacerItemRight)
        self.ScrollVerticalLayout.insertLayout (InsertIndex,self.HorizontalLayout)
        # 将新的控件添加到列表中,以便之后访问
        typeList = [self.ButtonCounter, comboBox, spinBox, ButtonLeft, ButtonRight]
        self.DynamicControlList.append (typeList)
        # 更新按钮的序号
        self.ButtonCounter += 1

我们接收一个参数 Control 这是一个字符串类型参数,我们在发送信号是传入,用来确认是哪一个按钮发送的信号,进而确定需要创建的控件索引 InsertIndex 同时我们使用列表存放了所有动态控件,用于对动态创建控件的访问。

# 访问动态控件

接下来,我们需要实现:当用户点击确认按钮时,锁定 comboBox 和 spinBox,在创建控件时,我们已经绑定了点击按钮触发的槽函数 ClickPrimaryToolButton 这个函数将接收信号,首先判断信号是谁发出的,然后从存放了所有动态控件的二维列表 self.DynamicControlList 中获取到 send () 信号所在的列表,并操作列表的其余控件:

def ClickPrimaryToolButton (self):
        sender = self.sender ()
        SenderObjectName = sender.objectName ()
        # 根据 sender 确认设置二维列表的第二个索引
        OneDimensionalIndex = 0
        if 'ComboBox' in SenderObjectName:
            OneDimensionalIndex = 1
        elif 'SpinBox' in SenderObjectName:
            OneDimensionalIndex = 2
        elif 'Confirm' in SenderObjectName:
            OneDimensionalIndex = 3
        elif 'Cancellation' in SenderObjectName:
            OneDimensionalIndex = 4
        # 获取 sender 所在行的全部控件,即获取二维列表第一索引 index
        for index in range (len (self.DynamicControlList)):
            if sender == self.DynamicControlList [index][OneDimensionalIndex]:
                if OneDimensionalIndex == 3:
                    self.DynamicControlList [index][1].setEnabled (False)
                    self.DynamicControlList [index][2].setEnabled (False)
                    self.DynamicControlList [index][3].hide ()
                elif OneDimensionalIndex == 4:
                    # 只在确认按钮被隐藏时执行
                    if self.DynamicControlList [index][3].isHidden ():
                        self.DynamicControlList [index][1].setEnabled (True)
                        self.DynamicControlList [index][2].setEnabled (True)
                        self.DynamicControlList [index][3].show ()

# 输出信息

在用户选择完毕之后,我们希望用户点击 ' 确认 ' 按钮时,我们能够获取到用户所选择的所有信息,我们将遍历 self.DynamicControlList 将每一行被锁定的内容输出,当然在正常形况下既视不锁定的行也应该输出,在代码段直接去掉判断语句即可:

def ConfirmAllControl (self):
        # 创建一个字典来存储 ComboBox 的当前文本和 SpinBox 的值
        ConfirmControlMap = {}
        for AllControlList in self.DynamicControlList:
            # 只输出被锁定的行
            if not AllControlList [3].isVisible ():
                # 将当前 Data 作为键,SpinBox 的值作为值存储在字典中
                ConfirmControlMap [AllControlList [1].currentText ()] = AllControlList [2].value ()
        # 打印字典内容
        print ("===ConfirmControlMap:===")
        for data, value in ConfirmControlMap.items ():
            print (f"{data}: {value}")

# 删除动态控件

如果需要删除创建的动态控件,除了需要删除控件所在的布局,还需要删除控件列表中的元素,并更新三个 ADDButton 的索引,因此,我们需要在 self.DynamicControlList 中多存入两个数据:动态控件的布局,以及动态控件的所属,布局将会用于删除操作,而所属方便确定布局具体由哪个 ADDBtn 创建,从而便于更新 ADDBtn 的索引。
因此我们需要更改 DynamicControlList 的添加代码,同时我们也需要继续写按钮点击事件的处理函数,在点击取消的时候,判断该行其余控件是否被锁定,是则解锁,否则直接删除该行,更新索引,删除控件列表,更多改动请直接参考完成代码。

# 创建布局并添加到滚动框
HorizontalLayout = QHBoxLayout ()
HorizontalLayout.setObjectName (f"AddHorizontalLayout {self.ButtonCounter}")
HorizontalLayout.addItem (SpacerItemLeft)
HorizontalLayout.addWidget (comboBox)
HorizontalLayout.addWidget (spinBox)
HorizontalLayout.addWidget (ButtonLeft)
HorizontalLayout.addWidget (ButtonRight)
HorizontalLayout.addItem (SpacerItemRight)
self.ScrollVerticalLayout.insertLayout (InsertIndex,HorizontalLayout)
# 将新的控件添加到列表中,以便之后访问
typeList = [self.ButtonCounter, comboBox, spinBox, ButtonLeft, ButtonRight, HorizontalLayout, Control]
self.DynamicControlList.append (typeList)

# 完整代码

from PySide6.QtWidgets import QApplication, QWidget,QHBoxLayout
from PySide6 import QtCore,QtWidgets
from ProductSelectionUI import Ui_ProductSelectionUI
from qfluentwidgets import ComboBox, PrimaryToolButton, SpinBox, FluentIcon
class ChoseTpyeWindow (QWidget, Ui_ProductSelectionUI):
    def __init__(self, *args, **kwargs):
        super ().__init__(*args, **kwargs)
        self.setupUi (self)
        # 变量设置
        # 设置 ComboBox 内容列表
        self.AuthorisationType = [' 模块 A 授权 ', ' 模块 B 授权 ', ' 模块 C 授权 ']
        self.ModuleType = ['A 配件 ', 'B 配件 ', 'C 配件 ']
        self.ServiceType = ['A 服务 ', 'B 服务 ']
        # 按钮的 objectName 计数器
        self.ButtonCounter = 0
        # 存放所有的授权类动态控件,列表格式:[[ButtonCounter, comboBox, spinBox, ButtonLeft ,ButtonRight, 布局,所属类],[...]]
        self.DynamicControlList = []
        # 将第一行的产品控件添加进去
        ProductControlList = []
        ProductControlList.append ('Product')
        ProductControlList.append (self.ProductComboBox)
        ProductControlList.append (self.ProductSpinBox)
        ProductControlList.append (self.ProductConfirmPrimaryToolButton)
        ProductControlList.append (self.ProductCancellationPrimaryToolButton)
        ProductControlList.append (self.ProductSelectHorizontalLayout)
        ProductControlList.append ('Product')
        self.DynamicControlList.append (ProductControlList)
        # 按钮的行索引,' 添加授权 ':AuthorisationNum, ' 添加配件 ':ModuleNum, ' 添加服务 ':ServiceNum 
        self.AuthorisationNum = 4
        self.ModuleNum = 6
        self.ServiceNum = 8
        self.bind ()
    def bind (self):
        # Add 按钮的绑定事件
        self.AuthorisationPushButton.clicked.connect (lambda:self.AddSelectHorizontalLayout (Control='Authorisation'))
        self.ModulePushButton.clicked.connect (lambda:self.AddSelectHorizontalLayout (Control='Module'))
        self.ServicePushButton.clicked.connect (lambda:self.AddSelectHorizontalLayout (Control='Service'))
        # 产品行的两个按钮
        self.ProductConfirmPrimaryToolButton.clicked.connect (self.ClickPrimaryToolButton)
        self.ProductCancellationPrimaryToolButton.clicked.connect (self.ClickPrimaryToolButton)
        # 最后的确认和取消按钮
        self.LectotypeConfirmPushButton.clicked.connect (self.ConfirmAllControl)
        self.LectotypeCancellationPushButton.clicked.connect (self.close)
    
    # 增加产品选择行的槽函数,{Control: 指定控件,Type: 指定 ComboBox 的 list}
    def AddSelectHorizontalLayout (self, Control):
        # 定义控件类型与对应的数据列表
        ControlMap = {
            'Authorisation': (self.AuthorisationType, self.AuthorisationNum),
            'Module': (self.ModuleType, self.ModuleNum),
            'Service': (self.ServiceType, self.ServiceNum),
        }
        
        # 获取控件类型对应的数据列表和行索引
        DataList, InsertIndex = ControlMap [Control]
        # 调整为实际插入位置
        InsertIndex -= 1  
        # 更新索引
        if Control == 'Authorisation':
            # 更新 Add 按钮的全局变量
            self.AuthorisationNum += 1
            self.ModuleNum += 1
            self.ServiceNum += 1
        elif Control == 'Module':
            self.ModuleNum += 1
            self.ServiceNum += 1
        elif Control == 'Service':
            self.ServiceNum += 1
        # 创建控件,为控件设置唯一的 objectName
        comboBox = ComboBox (self.ScrollAreaWidgetContents)
        comboBox.setObjectName (f"ComboBox {self.ButtonCounter}")
        # 将 list 写入 ComboBox
        for type in DataList:
            comboBox.addItem (type)
        # 授权选择行的 SpinBox
        spinBox = SpinBox (self.ScrollAreaWidgetContents)
        spinBox.setMaximumSize (QtCore.QSize (150, 33))
        spinBox.setMinimum (1)
        spinBox.setObjectName (f"AuthorisationSpinBox {self.ButtonCounter}")
        ButtonLeft = PrimaryToolButton (self.ScrollAreaWidgetContents)
        ButtonLeft.setIcon (FluentIcon.ACCEPT_MEDIUM)
        ButtonLeft.setMaximumSize (QtCore.QSize (32, 30))
        ButtonLeft.setObjectName (f"ConfirmButton {self.ButtonCounter}")
        ButtonRight = PrimaryToolButton (self.ScrollAreaWidgetContents)
        ButtonRight.setIcon (FluentIcon.CANCEL_MEDIUM)
        ButtonRight.setMaximumSize (QtCore.QSize (32, 30))
        ButtonRight.setObjectName (f"CancellationButton {self.ButtonCounter}")
        # 创建两个弹簧
        SpacerItemLeft = QtWidgets.QSpacerItem (5, 20, QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Minimum)
        SpacerItemRight = QtWidgets.QSpacerItem (5, 20, QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Minimum)
        # 绑定按钮事件信号和槽
        ButtonLeft.clicked.connect (self.ClickPrimaryToolButton)
        ButtonRight.clicked.connect (self.ClickPrimaryToolButton)
        # 创建布局并添加到滚动框
        HorizontalLayout = QHBoxLayout ()
        HorizontalLayout.setObjectName (f"AddHorizontalLayout {self.ButtonCounter}")
        HorizontalLayout.addItem (SpacerItemLeft)
        HorizontalLayout.addWidget (comboBox)
        HorizontalLayout.addWidget (spinBox)
        HorizontalLayout.addWidget (ButtonLeft)
        HorizontalLayout.addWidget (ButtonRight)
        HorizontalLayout.addItem (SpacerItemRight)
        self.ScrollVerticalLayout.insertLayout (InsertIndex,HorizontalLayout)
        # 将新的控件添加到列表中,以便之后访问
        typeList = [self.ButtonCounter, comboBox, spinBox, ButtonLeft, ButtonRight, HorizontalLayout, Control]
        self.DynamicControlList.append (typeList)
        # 更新按钮的序号
        self.ButtonCounter += 1
    # 按钮点击事件处理函数
    def ClickPrimaryToolButton (self):
        sender = self.sender ()
        SenderObjectName = sender.objectName ()
        # 根据 sender 确认设置二维列表的第二个索引
        OneDimensionalIndex = 0
        if 'ComboBox' in SenderObjectName:
            OneDimensionalIndex = 1
        elif 'SpinBox' in SenderObjectName:
            OneDimensionalIndex = 2
        elif 'Confirm' in SenderObjectName:
            OneDimensionalIndex = 3
        elif 'Cancellation' in SenderObjectName:
            OneDimensionalIndex = 4
        # 获取 sender 所在行的全部控件,即获取二维列表第一索引 index
        for index in range (len (self.DynamicControlList)):
            if sender == self.DynamicControlList [index][OneDimensionalIndex]:
                if OneDimensionalIndex == 3:
                    self.DynamicControlList [index][1].setEnabled (False)
                    self.DynamicControlList [index][2].setEnabled (False)
                    self.DynamicControlList [index][3].hide ()
                elif OneDimensionalIndex == 4:
                    # 只在确认按钮被隐藏时执行
                    if self.DynamicControlList [index][3].isHidden ():
                        self.DynamicControlList [index][1].setEnabled (True)
                        self.DynamicControlList [index][2].setEnabled (True)
                        self.DynamicControlList [index][3].show ()
                    else:
                        Layout = self.DynamicControlList [index][5]
                        parentLayout = self.ScrollVerticalLayout
                        Layout_Control = self.DynamicControlList [index][6]
                    # 更新索引
                    if Layout_Control == 'Authorisation':
                        # 更新 Add 按钮的全局变量
                        self.AuthorisationNum -= 1
                        self.ModuleNum -= 1
                        self.ServiceNum -= 1
                    elif Layout_Control == 'Module':
                        self.ModuleNum -= 1
                        self.ServiceNum -= 1
                    elif Layout_Control == 'Service':
                        self.ServiceNum -= 1
                    # 删除布局
                    self.delete_layout (parentLayout, Layout)
                    # 删除列表中的对应元素
                    del self.DynamicControlList [index]
                    break  # 删除元素后跳出循环
    # 按钮删除事件,传递父控件和需移除控件
    def delete_layout (self,parentLayout,Layout):
        # 从父布局中移除
        parentLayout.removeItem (Layout)
        # 删除布局中的每个控件
        for i in reversed (range (Layout.count ())): 
            widget = Layout.itemAt (i).widget ()
            if widget is not None:
                # 如果 widget 存在,从水平布局中移除并删除它
                Layout.removeWidget (widget)
                widget.deleteLater ()  # 使用 deleteLater 来安全地删除控件
        # 删除水平布局本身,立即释放资源
        Layout.deleteLater ()
    
    # 用于打印所有 ComboBox 的当前文本
    def ConfirmAllControl (self):
        # 创建一个字典来存储 ComboBox 的当前文本和 SpinBox 的值
        ConfirmControlMap = {}
        for AllControlList in self.DynamicControlList:
            # 将当前 Data 作为键,SpinBox 的值作为值存储在字典中
            ConfirmControlMap [AllControlList [1].currentText ()] = AllControlList [2].value ()
        # 打印字典内容
        print ("===ConfirmControlMap:===")
        for data, value in ConfirmControlMap.items ():
            print (f"{data}: {value}")
if __name__ == '__main__':
    app = QApplication ([])
    Window = ChoseTpyeWindow ()
    Window.show ()
    app.exec ()

# 学生管理系统

本次项目将设计一个学生管理系统,该系统由学生管理、班级管理、老师管理三个模块组成,使用 qfluentwidgets 组件,代码将从 0 开始逐步设计。

# SQLite

数据库使用 SQLite,使用 cmd 创建数据库 student_system

sqlite3 student_system.db
PRAGMA encoding = 'UTF-8';

在创建过程中如果出现失误,想要删除表格数据

# 删除表格所有数据
DELETE FROM DBname;
# 重置自增主键计数器:
DELETE FROM sqlite_sequence WHERE name = 'DBname';

# classes 表

classes 表用于存储班级的基本信息,字段以及属性如下:

字段名数据类型约束条件描述
class_idINTPRIMARY KEY AUTOINCREMENT班级 ID、主键、自增
class_nameVARCHAR (255)NOT NULL班级名称

创建表 classes 的 SQLite 语句

CREATE TABLE IF NOT EXISTS classes (
    class_id INTEGER PRIMARY KEY AUTOINCREMENT,
    class_name VARCHAR (255) NOT NULL
);

# student 表

student 表用于存储学生的基本信息以及成绩,字段以及属性如下

字段名数据类型约束条件描述
student_idINTPRIMARY KEY, AUTOINCREMENT学生 ID、主键、自增
student_nameVARCHAR (255)NOT NULL学生姓名
student_numberVARCHAR (255)NOT NULL UNIQUE学号、唯一约束
genderINTNOT NULL性别,1 表示男,2 表示女
class_idINTNOT NULL, FOREIGN KEY (class_id) REFERENCES class (class_id)班级 ID,外键,关联 classes 表的 class_id
chinese_scoreFLOAT语文分数
math_scoreFLOAT数学分数
english_scoreFLOAT英语分数

创建 student 表的 SQLite 语句

CREATE TABLE IF NOT EXISTS students (
    student_id INTEGER PRIMARY KEY AUTOINCREMENT,
    student_name VARCHAR (255) NOT NULL,
    student_number VARCHAR (255) NOT NULL UNIQUE,
    gender INT NOT NULL,
    class_id INT NOT NULL,
    chinese_score FLOAT,
    math_score FLOAT,
    english_score FLOAT,
    FOREIGN KEY (class_id) REFERENCES classes (class_id)
);

# user 表

user 表用于存储系统用户的基本信息及角色信息,字段以及属性如下

字段名数据类型约束条件描述
user_idINTPRIMARY KEY AUTOINCREMENT用户 ID、主键、自增
usernameVARCHAR (255)NOT NULL UNIQUE用户名,唯一约束
passwordVARCHAR (255)NOT NULL密码
nicknameVARCHAR (255)昵称
user_roleINT用户角色,1 表示管理员,2 表示老师
class_idVARCHAR (255)管理班级,存储为 '1, 2, 3' 形式的字符串,管理 classes 表的 class_id 字符串
CREATE TABLE IF NOT EXISTS users (
    user_id INTEGER PRIMARY KEY AUTOINCREMENT,
    username VARCHAR (255) NOT NULL UNIQUE,
    password VARCHAR (255) NOT NULL,
    nickname VARCHAR (255),
    user_role INT,
    class_id VARCHAR (255)
);

# 主窗口和基本布局

实现学生管理模块的基本布局,主窗口包含新增、删除、导入、导出、统计等按钮,搜索框、下拉选择框和一个展示表格,基础的创建方法再次便不在赘述,在此只说明设计中需要注意的点:
按钮、下拉框等控件和表格之间应有分隔,我们将水平布局的父 Widget 设置为 qfluentwidgets 中的 CardWidget ,再将这些按钮等控件添加到水平布局里,实现卡片包裹住水平布局的效果。

from qfluentwidgets import CardWidget
card_widget = CardWidget (self)
buttons_layout = QHBoxLayout (card_widget)

如果我们像设置按钮颜色,就要使用的 qss 样式,qfluentwidgets 的 qss 样式设置为 setCustomStyleSheet (widget: QWidget, lightQss: str, darkQss: str) ,即传入需要设置的 Widget 和亮色、暗色下的 qss 样式,使用示例如下

from qfluentwidgets import setCustomStyleSheet
qss = 'PushButton {border-radius: 10px}'
setCustomStyleSheet (button, qss, qss)

而在本项目中,我们新建了 custom_style.py 专门用来存放 qss 样式

BUTTON_STYLE = """
QPushButton {
    border: none;
    padding: 5px 10px;
    font-family: 'Seqoe UI', 'Microsoft YaHei';
    font-size: 14px;
    color: white;
    border-radius: 5px;
}
QPushButton:hover {
    background-color: rgba (255, 255, 255, 0.1);
}
QPushButton:pressed {
    background-color: rgba (255, 255, 255, 0.2);
}
"""
ADD_BUTTON_STYLE = BUTTON_STYLE + """
QPushButton {
    background-color: #0d6efd;
}
QPushButton:hover {
    background-color: #0b5ed7;
}
QPushButton:pressed {
    background-color: #0a58ca;
}
"""
DELETE_BUTTON_STYLE = BUTTON_STYLE + """
QPushButton {
    background-color: #dc3545;
}
QPushButton:hover {
    background-color: #0bb2d3b;
}
QPushButton:pressed {
    background-color: #b02a37;
}
"""
BATCH_DELETE_BUTTON_STYLE = BUTTON_STYLE + """
QPushButton {
    background-color: #fd7e14;
}
QPushButton:hover {
    background-color: #e96b10;
}
QPushButton:pressed {
    background-color: #dc680f;
}
"""
UPDATE_BUTTON_STYLE = BUTTON_STYLE + """
QPushButton {
    background-color: #198754;
}
QPushButton:hover {
    background-color: #157347;
}
QPushButton:pressed {
    background-color: #146c43;
}
"""
UPDATE_BUTTON_STYLE = BUTTON_STYLE + """
QPushButton {
    background-color: #6f42c1;
}
QPushButton:hover {
    background-color: #5936a2;
}
QPushButton:pressed {
    background-color: #4a2d8e;
}
"""
EXPORT_BUTTON_STYLE = BUTTON_STYLE + """
QPushButton {
    background-color: #20c997;
}
QPushButton:hover {
    background-color: #1aa179;
}
QPushButton:pressed {
    background-color: #198b6d;
}
"""

学生管理页面的布局如下

from PySide6.QtWidgets import (QApplication,QWidget,QVBoxLayout,
                               QHBoxLayout,QPushButton)
from qfluentwidgets import CardWidget,PushButton,SearchLineEdit,TableWidget,setCustomStyleSheet
from custom_style import ADD_BUTTON_STYLE, BATCH_DELETE_BUTTON_STYLE
import sys
class StudentInterface (QWidget):
    def __init__(self):
        super ().__init__()
        self.setObjectName ('StudentInterface')
        self.setup_ui ()
    
    def setup_ui (self):
        layout = QVBoxLayout (self)
        # 顶部按钮
        card_widget = CardWidget (self)
        buttons_layout = QHBoxLayout (card_widget)
        self.addButton = PushButton (' 新增 ',self)
        setCustomStyleSheet (self.addButton, ADD_BUTTON_STYLE,ADD_BUTTON_STYLE)
        self.searchInput = SearchLineEdit (self)
        self.searchInput.setPlaceholderText (' 搜索学生姓名或学号...')
        self.searchInput.setFixedWidth (500)
        self.batchDeleteButtom = PushButton (' 批量删除 ',self)
        setCustomStyleSheet (self.batchDeleteButtom, BATCH_DELETE_BUTTON_STYLE,BATCH_DELETE_BUTTON_STYLE)
        buttons_layout.addWidget (self.addButton)
        buttons_layout.addWidget (self.searchInput)
        buttons_layout.addStretch (1) # 填充
        buttons_layout.addWidget (self.batchDeleteButtom)
        layout.addWidget (card_widget)
        # 添加 table
        self.table_widget = TableWidget (self)
        self.table_widget.setBorderRadius (8) # 设置圆角
        self.table_widget.setBorderVisible (True) # 设置表格边框可见
        layout.addWidget (self.table_widget)
        self.setLayout (layout)
        self.setStyleSheet ("StudentInterface {background: white}")
        self.resize (1280,760)
if __name__ == '__main__':
    app = QApplication (sys.argv)
    window = StudentInterface ()
    window.show ()
    sys.exit (app.exec ())

# 添加表格数据

现在我们需要往表格中写入一些数据,首先在 setup_ui 中,我们需要进行一些基础设置

# 添加 table
self.table_widget = TableWidget (self)
self.table_widget.setBorderRadius (8) # 设置圆角
self.table_widget.setBorderVisible (True) # 设置表格边框可见
# 设置表头
self.table_widget.setColumnCount (11)
self.table_widget.setHorizontalHeaderLabels ([""," 学生 ID"," 姓名 "," 学号 "," 性别 "," 班级 "," 语文 "," 数学 "," 英语 "," 总分 "," 操作 "])
# 填充以确保布满 widget
self.table_widget.horizontalHeader ().setSectionResizeMode (QHeaderView.ResizeMode.Stretch)

现在我们需要定义 load_data 用于存放表格数据,在导入数据库之前,先用一个列表存放数据。
在此我们设置用 self.students 存放数据,建议在 __init__ 中先设置 self.students = []

def load_data (self):
        self.students = [
            {"student_id": 1, "student_name": "张三", "student_number":"20240928", "gender":1, "class_id":1, "chinese_score":12, "matn_score":12, "english_score":12},
            {"student_id": 2, "student_name": "李四", "student_number":"20240928", "gender":1, "class_id":1, "chinese_score":12, "matn_score":12, "english_score":12},
            {"student_id": 3, "student_name": "王五", "student_number":"20240928", "gender":1, "class_id":1, "chinese_score":12, "matn_score":12, "english_score":12},
            {"student_id": 4, "student_name": "赵六", "student_number":"20240928", "gender":1, "class_id":1, "chinese_score":12, "matn_score":12, "english_score":12}
        ]

现在我们需要导入数据,我们设置了使用 populate_table 去遍历数据,将 rowstudent_info 传递给 setup_table_row , 该函数将完成对数据的处理和添加工作。
另外,不要忘记在 __init__ 中调用 self.populate_table ()

def populate_table (self):
    self.table_widget.setRowCount (len (self.students))
    for row, student_info in enumerate (self.students):
        self.setup_table_row (row, student_info)
def setup_table_row (self, row, student_info):
    checkBox = QCheckBox ()
    self.table_widget.setCellWidget (row, 0, checkBox)
    # 赋值数列
    for col, key in enumerate (["student_id", "student_name", "student_number", "gender", "class_id", "chinese_score", "matn_score", "english_score"]):
        value = student_info.get (key, '')
        # 设置性别
        if key == 'gender':
            value = ' 男 ' if value == 1 else ' 女 ' if value == 2 else value == ' 未知 '
        # 单元格赋值
        item = QTableWidgetItem (str (value))
        self.table_widget.setItem (row, col+1, item)
        # 设置总分
        if key == "english_score":
            count_score = student_info.get ("chinese_score", '') + student_info.get ("matn_score", 0.0) + student_info.get ("english_score", 0.0)
            self.table_widget.setItem (row, col+2, QTableWidgetItem (str (count_score)))

# 创建数据库基类

在本次项目中,我们将频繁访问数据库,为了方便,我们需要创建一个数据库的基类,在使用数据库时,只需继承基类即可。
为了方便操作,我们使用了上下文管理器,当执行流进入 with 语句块时,对象会调用它的 __enter__ 方法,而在离开 with 语句块时,无论是因为正常完成还是因为异常,都会调用 __exit__ 方法。以此实现自动的连接和关闭数据库。

cursor.execute (query, params) 方法用于执行 SQL 语句,query 是一个字符串,代表要执行的 SQL 语句。params 是一个参数序列或映射,用于传递给 SQL 语句中的占位符。

import sqlite3
class DatabaseManage ():
    def __init__(self, dbfile):
        self.dbfile = dbfile
        self.connection = None
    
    def __enter__(self):
        self.create_connection ()
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.close_connection ()
    def create_connection (self):
        if self.connection is None:
            try:
                self.connection = sqlite3.connect (self.dbfile)
                self.connection.row_factory = sqlite3.Row
                return self.connection
            except Exception as e:
                print (f"无法连接到数据库 {e}")
        return self.connection
    def fetch_query (self, query, params=None, single=False):
        result = None
        if self.connection:
            try:
                cursor =  self.connection.cursor ()
                cursor.execute (query, params) if params is not None else cursor.execute (query)
                if single:
                    result = cursor.fetchone () # 查询一条记录
                else:
                    result = cursor.fetchall () # 查询多条
            except Exception as e:
                print (f' 错误 {e}')
        return result
    def execute_query (self, query, params=None):
        if self.connection:
            try:
                cursor =  self.connection.cursor ()
                cursor.execute (query, params) if params is not None else cursor.execute (query)
                self.connection.commit ()
                return True
            except Exception as e:
                print (f' 异常 {e}')
                self.connection.rollback ()
                return None
        else:
            print (f"没有建立连接")
        return None
    def close_connection (self):
        if self.connection:
            self.connection.close ()

# 写入学生数据

在数据库基类创建完成后,我们先用 python 写入一些数据到 students 表,由于 students 表中存在 classes 表的外键,首先需要写入 classes 表,该表为班级信息,这里直接使用 Navicat 写入六条数据。
现在可以使用 faker 库和 random 库向 students 表随机写入一些数据,在数据库基类中:

from faker import Faker
import random
if __name__ == '__main__':
    dbfile = 'SQLite/student_system.db'
    with DatabaseManage (dbfile) as db:
        # 执行查询
        query = "SELECT * FROM students WHERE (gender = ? AND class_id = ?)"
        params = (1,2)
        students = db.fetch_query (query, params)
        for student in students:
            print (dict (student))
        # 插入学生数据
        query = "INSERT INTO students (student_name, student_number, gender, class_id, chinese_score, math_score, english_score)  VALUES (' 李四 ', '20240930', '2', '2', '90.0', '85.0', '78.0');"
        db.execute_query (query)
        # 批量插入数据
        fake = Faker ('zh_CN')
        for i in range (180):
            query = f"INSERT INTO students (student_name, student_number, gender, class_id, chinese_score, math_score, english_score)  VALUES ('{fake.name ()}', '{1000+i}', '{random.choice ([1, 2])}', '{random.randint (1, 6)}', '{random.randint (50,99)}', '{random.randint (45, 99)}', '{random.randint (30, 99)}');"
            print (query)
            db.execute_query (query)

# 创建学生模型

在创建完基类数据后,我们需要创建学生模型,构造查询所有学生信息的方法,该方法继承 DatabaseManage,将输出的每一列表元素由 sqlite3.Row 转换成字典:

from .base_db import DatabaseManage
class StudentDB (DatabaseManage):
    # 获取所有学生信息
    def fetch_students (self):
        query = """
        SELECT s.*, c.class_name
        FROM students s
        JOIN classes c ON s.class_id = c.class_id ;
        """
        # self.fetch_query 输出的列表元素为 sqlite3.Row 类型,需要转为 dict
        return [dict (student) for student in self.fetch_query (query)]

现在,我们可以将 def load_data (self) 中的虚拟数据删去,导入学生模型类,从数据库获取数据。

from .database.student_db import StudentDB
def load_data (self):
    # 从数据库中获取数据
    dbfile = 'SQLite/student_system.db'
    with StudentDB (dbfile) as db:
        # self.students 为一个列表,列表中的每个元素均为字典
        self.students = db.fetch_students ()

# 杂项

# PySide6-Fluent-Widgets

PySide6-Fluent-Widgets 是一个用于美化的组件库,下载免费版本:

# lite version
pip install PySide6-Fluent-Widgets -i https://pypi.org/simple/
# full version (supports acrylic components)
pip install "PySide6-Fluent-Widgets [full]" -i https://pypi.org/simple/

使用方法:
修改 envname\Lib\site-packages\qfluentwidgets\common\config.py
ctrl 多选 QT designer 里的同类部件,右键,提升为
基类名称不变
提升的类名,基类去掉 Q
头文件:qfluentwidgets
点击提升

# styleSheet

记录几个简单的 stylesheet

# 悬停背景色

.QComboBox:hover {
    background-color: rgb (170, 255, 255);
}
更新于 阅读次数