---
title: "Python Qt"
author: "Perrin Yong"
author_profile: https://www.pystone.net/profile/
published_by: "Perrin Yong"
canonical: https://www.pystone.net/notes/python-qt-overview/
type: note
content_role: unspecified
visibility: public
id_stability: rename-stable
source_path: "10-计算机、信息技术与工程/04-应用开发与云端工程/桌面应用开发/Python Qt.md"
content_hash: 3ba372cd196d5049aa9860a6461d3db2c80d9bd8993a7248f66c955081ed2d3b
knowledge_version: 224c990773de.5fa8af6e39fa
site_commit: 224c990773de166d23a886306577dd90379529ce
notes_commit: 5fa8af6e39fa3891d1b9b4832bfa6c4e0ecaaf0a
---
# Python Qt
QT: https://doc.qt.io/qt-6/portingguide.html
QT: https://wiki.qt.io/Main
https://wiki.qt.io/Qt_for_Python
PySide6: https://doc.qt.io/qtforpython-6/quickstart.html

参考教程：
白月黑羽：https://www.byhy.net/tut/py/gui/qt_01/
基于PySide6的GUI程序开发全流程：https://cloud.tencent.com/developer/article/2334289 (计算器程序示例)

简言之博主的博客：https://jwt1399.top/posts/41724.html#toc-heading-1

---

## 教程或示例资料
Code Editor Example：https://doc.qt.io/qtforpython-6.2/examples/example_widgets__codeeditor.html#:~:text=,codeeditor.py
YoloSide： https://github.com/Jai-wei/YOLOv8-PySide6-GUI
PySide代码式教程：https://github.com/muziing/PySide6-Code-Tutorial
PySide2 入门教程：https://github.com/se7enXF/pyside2
PyQt各种测试和例子：https://github.com/PyQt5/PyQt
PyQt-Fluent-Widgets：https://github.com/zhiyiYo/PyQt-Fluent-Widgets

Top 19 Python desktop-app Projects:https://www.libhunt.com/l/python/topic/desktop-app

- [Qt-Material](https://qt-material.readthedocs.io/en/latest/index.html#qt-material)：PySide6、PySide2和PyQt5的 Material Design 风格样式表
- [QDarkStyleSheet](https://github.com/ColinDuquesnoy/QDarkStyleSheet) ： 一个黑暗风格的样式表
- [PyQt5_stylesheets](https://github.com/RedFalsh/PyQt5_stylesheets) ：PyQt5 风格样式表
- [CustomWidgets](https://github.com/PyQt5/CustomWidgets)：PyQt Custom Widgets - PyQt 自定义控件
- [PyQt Examples](https://github.com/PyQt5/PyQt)：PyQt各种测试和例子
- [3rd-Apps](https://github.com/PyQt5/3rd-Apps)：Collecting 3rd App

来源: 简言之
文章作者: 简简
文章链接: [https://jwt1399.top/posts/41724.html#toc-heading-171](https://jwt1399.top/posts/41724.html#toc-heading-171)
本文章著作权归作者所有，任何形式的转载都请注明出处。
## 工具
QtAwesome https://github.com/spyder-ide/qtawesome
**[PySide6-Code-Tutorial](https://github.com/muziing/PySide6-Code-Tutorial)**

https://github.com/5yutan5/PyQtDarkTheme
https://github.com/KhamisiKibet/QT-PyQt-PySide-Custom-Widgets
## 参考项目
https://github.com/pyQode/pyQode
https://github.com/TenderOwl/Frog/tree/master
https://github.com/reflex-dev/reflex/tree/main

SpyderIDE(专为科学开发和数据分析设计的强大 Python IDE): https://github.com/spyder-ide/spyder
OpenShot(多平台的视频编辑软件): https://github.com/OpenShot/openshot-qt
Orange(基于组件的数据可视化和分析工具): https://github.com/biolab/orange3

https://github.com/CadQuery/CQ-editor
https://github.com/gmarull/qtmodern
https://github.com/Javacr/PyQt5-YOLOv5
https://github.com/Jai-wei/YOLOv8-PySide6-GUI
https://github.com/Wanderson-Magalhaes/Modern_GUI_PyDracula_PySide6_or_PyQt6
## 开发环境与流程


* 配置虚拟环境
* 安装PySide6 (6.3.2)
* 配置 open qt designer菜单项
![assets/image-20231110180950213.png](/media/bbc81808eab946ef289f.png)

* 配置rcc与uic菜单项

```cpp
Program -->​​D:\...\venv\Scripts\pyside6-uic.exe​​
Arguments --> $FileName$ -o $FileNameWithoutAllExtensions$.py
```

```cpp
Program -->​​ ​​D:\...\venv\Scripts\pyside6-rcc.exe​​
Arguments --> $FileName$ -o $FileNameWithoutAllExtensions$_rc.py
```

* 打包exe
```python
pip install pyinstaller
pip install auto-py-to-exe
```

auto-py-to-exe实际上是对pyinstaller的封装.
选择入口文件即可.
pyinstall教程:https://www.byhy.net/tut/py/etc/toexe/

## 编程知识

## 框架特征
在 Qt 系统中，控件（widget）是 `层层嵌套` 的，除了最顶层的控件，其他的控件都有父控件。

## 编程规范

代码模块化: 通常应该把 一个窗口和其包含的控件，对应的代码 全部封装到类中.
## 接口用法
### 事件循环
事件循环 - 使应用程序能够响应用户的交互和系统事件，如鼠标点击、键盘输入、窗口关闭等。

`QApplication`的成员函数exec  用于启动 PyQt 应用程序的事件循环,并等待直到应用退出。

当界面上一个控件被操作时，就会发出 `信号` ，英文叫 `signal`，表明一个事件（比如被点击、被输入文本）发生了。

处理 signal 的函数 叫做 `slot`

```py
//把 button 被 点击（clicked） 的信号（signal）， 连接（connect）到了 handleCalc 这样的一个 slot上
button.clicked.connect(handleCalc)
```

### 绘制相关
move决定这个控件相对于父窗口的左上角的位置


### 常用控件

菜单栏:
```cpp
菜单栏（QMenuBar） ->  菜单（QMenu）->  子菜单（QMenu） -> QAction
```


#### MDI 多个子窗口

#### 树控件

## 界面布局

布局经验（via. 白月黑羽）
- 先不使用任何Layout，把所有控件 按位置 摆放在界面上
- 然后先从 最内层开始 进行控件的 Layout 设定
- 逐步拓展到外层 进行控件的 Layout设定
- 最后调整 layout中控件的大小比例， 优先使用 Layout的 layoutStrentch 属性来控制


## 多线程问题处理
Qt建议：
* 只在主线程中操作界面。在子线程操作界面可能会有意外问题，如输出显示不全，甚至程序崩溃。
* 使用信号处理子线程的事件。

信号在多线程问题中的使用：
* 自定义一个Qt 的 QObject类，里面封装一些自定义的 Signal信号。
	一种信号定义为 该类的 一个 静态属性，值为Signal 实例对象即可。
	Signal实例对象的初始化参数指定的类型，就是 发出信号对象时，传递的参数数据类型。
	可以定义 `多个` Signal静态属性，对应这种类型的对象可以发出的 `多种` 信号。

* 定义主线程执行的函数处理Signal信号（通过connect方法）
* 在新线程需要操作界面的时候，就通过自定义对象 发出 信号
* 主线程信号处理函数，被触发执行，获取Signal里面的参数，执行必要的更新界面操作。


```python
from PySide6.QtWidgets import QApplication, QTextBrowser
from PySide6.QtUiTools import QUiLoader
from threading import Thread
import time
import sys

from PySide6.QtCore import Signal, QObject

class MySignals(QObject):
    text_print = Signal(str, str)
    update_table = Signal(str)

## 实例化
global_ms = MySignals()

class Stats:
    def __init__(self):
        self.ui = QUiLoader().load('ui/multi_thread.ui')
        self.ui.infoBox1.setText("初始文本\n")
        self.ui.infoBox2.setText("初始文本\n")
        global_ms.text_print.connect(self.printToGui)

    def printToGui(self, infoBoxName, text):
        infoBox = getattr(self.ui, infoBoxName)
        print("printToGui called: " + str(text))
        infoBox.append(str(text))
        infoBox.append(' --infobox-append--')
        infoBox.ensureCursorVisible()

    def task1(self):
        def threadFunc():
            time.sleep(2)
            # 通过Signal的emit触发执行主线程里面的处理函数
            print("线程1触发输出")
            global_ms.text_print.emit("infoBox1", '输出内容1')

        thread = Thread(target = threadFunc)
        thread.start()

    def task2(self):
        def threadFunc():
            time.sleep(4)
            print("线程2触发输出")
            global_ms.text_print.emit("infoBox2", '输出内容2')

        thread = Thread(target = threadFunc)
        thread.start()

app = QApplication()
sts = Stats()
sts.ui.show()
sts.task1()
sts.task2()
app.exec_()
```

## QSS样式

## QSS样式基本语法
文档：https://doc.qt.io/qt-5/stylesheet-syntax.html

更详细的笔记见：
[Python Qt QSS样式用法](obsidian://open?vault=ObsidianVault&file=4%20%E4%BF%A1%E6%81%AF%E6%8A%80%E6%9C%AF%E9%A2%86%E5%9F%9F%2F%E6%A1%8C%E9%9D%A2%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91%2FPython%20Qt%20QSS%E6%A0%B7%E5%BC%8F%E7%94%A8%E6%B3%95)

**选择器**用于选择一个或多个需要应用样式的 Qt 部件

![assets/image-20231115162013069.png](/media/5c9541f30ac6617a4b13.png)


![assets/image-20231115162029989.png](/media/493f8f88d2181f2e182e.png)

```css

QTextEdit { background-color: yellow }
QTextEdit { background-color: #e7d8d8 }
QTextEdit { background-image: url(gg03.png); }
QTextEdit { margin:10px 11px 12px 13px }

/*
solid 实线 dashed 虚线 dotted 点
*/
*[myclass=bar2btn]:hover{ border:1px solid #1d649c; }

/*字体、大小、颜色*/
*{ font-family:微软雅黑; font-size:15px; color: #1d649c; }

QPushButton { width:50px; height:20px; }

```

![assets/image-20231115162640822.png](/media/adebad97c506f6c3b92a.png)
图片来源：https://www.byhy.net/tut/py/gui/qt_09/


## QSS样式在工程中的用法与开发流程
## 组合方式
### QApplication 引用.qss 文件

QSS选择器
```python
app = QApplication(sys.argv)
form = QSSSelector()
## 选择器
#指定按钮
qssStyle = '''
	QPushButton[name="btn2"] {
		background-color:red;
		color:yellow;
		height:120;
		font-size:60px;
	}
	QPushButton[name="btn3"] {
		background-color:blue;
		color:yellow;
		height:60;
		font-size:30px;
	}
'''
form.setStyleSheet(qssStyle)
form.show()
sys.exit(app.exec_())
```

QSS子控件选择器
```python
app = QApplication(sys.argv)
form = QSSSubControl()
'''
通过名字来引用,#myComboBox相当于web里通过id来引用
drop-down是下拉子控件
'''
qssStyle = '''
   QComboBox#myComboBox::drop-down {
	   image:url(../picture/icon/first.png)
   }
'''
form.setStyleSheet(qssStyle)
form.show()
sys.exit(app.exec_())
```


## 结合Python数据可视化

## Matplotlib

```python
import sys
from PySide6.QtWidgets import QApplication, QVBoxLayout, QMainWindow, QWidget
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
import numpy as np

class MplWidget(QWidget):
    def __init__(self, parent=None):
        super(MplWidget, self).__init__(parent)

        # 创建一个新的图标Figure
        # 创建一个Matplotlib图标的画布FigureCanvas。  FigureCanvas是将这个图表转换为可以在PySide6中显示的小部件
        self.canvas = FigureCanvas(Figure())

        vertical_layout = QVBoxLayout()
        vertical_layout.addWidget(self.canvas)

        # 在画布上创建一个1x1网格的第1个子图
        self.canvas.axes = self.canvas.figure.add_subplot(111)
        self.setLayout(vertical_layout)

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.central_widget = QWidget()
        self.setCentralWidget(self.central_widget)
        layout = QVBoxLayout(self.central_widget)

        self.mpl_widget = MplWidget(self.central_widget)
        layout.addWidget(self.mpl_widget)

        self.plot()

    def plot(self):
        mu, sigma = 100, 15
        print( str(np.random.randn(10000)))
        x = mu + sigma * np.random.randn(10000)
        self.mpl_widget.canvas.axes.clear()

        # x: 这是直方图将要展示的数据集。在这个例子中，x应该是一个包含数据点的数组或列表。
        # 50: 表示直方图的条形（bins）的数量。在这个例子中，数据将被分成50个条形来展示。
        # density=1: 当设置为1时，表示直方图的纵轴将显示密度而不是计数。这意味着直方图下的总面积（或总高度）等于1。
        self.mpl_widget.canvas.axes.hist(x, 50, density=1, facecolor='g', alpha=0.75)
        self.mpl_widget.canvas.axes.set_xlabel('Smarts') # X轴标签
        self.mpl_widget.canvas.axes.set_ylabel('Probability')
        self.mpl_widget.canvas.axes.set_title('Histogram of IQ') # 标题
        self.mpl_widget.canvas.axes.text(60, .025, r'$\mu=100,\ \sigma=15$') # 在指定位置添加文本
        self.mpl_widget.canvas.axes.axis([40, 160, 0, 0.03]) # 设置坐标轴范围
        self.mpl_widget.canvas.axes.grid(True) # 显示网格
        self.mpl_widget.canvas.draw() # 更新画布上的绘图，这样更改才会显示在GUI上。

app = QApplication(sys.argv)
main = MainWindow()
main.show()
sys.exit(app.exec_())
```

## PyQtGraph
优点： 大数据量的作图性能高于 Matplotlib， 动态更新图的性能也比Matplotlib高。并且和Qt图形界面框架完美融合。
缺点： 作图功能没有Matplotlib多，开发社区没有Matplotlib大。

## 工程架构
需要将代码进行模块化，使其易于维护和扩展，有两种方案：

（1）将所有UI绘制在一个`main_window.ui`文件中（将不同部分分发给不同的类）
**优点**：UI设计集中，易于统一管理和调整。
**缺点**：代码中分发逻辑可能变得复杂。

1. **定义接口或回调**：在主窗口类中定义接口或回调函数，以便不同的部分可以与主窗口通信。
2. **创建子组件类**：为UI中的每个独立部分创建单独的Python类。
3. **在主窗口类中实例化**：在加载了`main_window.ui`之后，根据UI中的不同部分实例化相应的子组件类，并将相关UI元素传递给它们。
```python
class PartA:
    def __init__(self, ui_component):
        self.ui_component = ui_component
        # 初始化PartA

class MainWindow:
    def __init__(self):
        self.ui = load_ui('main_window.ui')
        self.part_a = PartA(self.ui.someComponent)

```

（2）**多个UI文件（每个部分一个UI文件）**：
- **优点**：模块化程度高，易于维护和扩展。
- **缺点**：需要更多的文件组织和管理。

**对于较大、复杂的项目，第一种方案可能更优**；而对于小型或中等规模的项目，第二种方案可能更为合适。

## 模块拆解合并工作流

在一个UI文件当中绘制。
![assets/image-20231128151300120.png](/media/b3edbcb094f82965fa0e.png)

绘制好后，可将各个部分拆到不同的UI文件当中。
![assets/image-20231128151446641.png](/media/8406557cc64fc1fc1103.png)

每个UI文件编写对应的类，在运行时对UI进行初始化。
![assets/image-20231128150928996.png](/media/ef3568f7f5e22e773a14.png)

类中的代码如下：
```python
from PySide6.QtUiTools import QUiLoader

class ControlArea():
    def __init__(self):
        super(ControlArea, self).__init__()
        loader = QUiLoader()
        self.ui = loader.load("ui/frm_control_area.ui")
```

用一个.ui文件和.py文件作为主窗口的布局和总体的管理：
![assets/image-20231128151640508.png](/media/fcc02ad533d8bafbc724.png)
在MainWindow类中逐个对子模块进行实例化和绑定。
```python
class MainWindow(QObject):
    def __init__(self):
        QObject.__init__(self)
        print("init main window")
        self.ui = QUiLoader().load('ui/main_window_framework.ui')
        print("load main window ui done")
        ## PRINT ==> SYSTEM
        print('System: ' + platform.system())
        print('Version: ' +platform.release())

        # 实例化子部分
        self.actionMenuArea = ActionMenuArea()
        self.controlArea = ControlArea()
        self.dataArea = DataArea()

        # 将子部分添加到主窗口
        self.ui.frmLeft.layout().addWidget(self.controlArea.ui)
        self.ui.frmLeft.layout().addWidget(self.actionMenuArea.ui)
        self.ui.frmMiddle.layout().addWidget(self.dataArea.ui)
```

## 单例模式用法


```python
from functools import wraps

def Singleton(orig_cls):
    orig_new = orig_cls.__new__
    instance = None

    @wraps(orig_cls.__new__)
    def __new__(cls, *args, **kwargs):
        nonlocal instance
        if instance is None:
            instance = orig_new(cls, *args, **kwargs)
        return instance
    orig_cls.__new__ = __new__
    return orig_cls

```

```python
## import common utils
from utils.common_utils import *

@Singleton
class Recorder:
    def __init__(self):
        self.isRecording = False

    def Start(self):
        pass

    def Stop(self):
        pass


## 使用
obj1 = Recorder()
obj2 = Recorder()
## obj1 和 obj2 是相同的实例
print(obj1 is obj2) # 输出 True
```

注意事项
- 确保单例的创建和使用不会导致循环依赖。
- 在单例类中，避免在`__init__`方法中编写复杂的初始化逻辑，因为它只会在第一次实例化时执行。
- 考虑线程安全问题，尤其是在多线程环境中使用单例。


## 循环引用问题处理
循环引用：两个或多个模块相互导入对方，导致Python无法正确初始化这些模块。

解决方案：
1. 重新组织代码结构：调整代码结构，避免模块间的直接循环依赖。
* 将相关的函数或类重构到一个新的模块中
* 创建一个新的模块作为共同依赖。
* 通过使用观察者模式、信号槽机制或回调函数来减少模块间的直接依赖。

2. 使用`import`而不是`from ... import ...`。

2. 延迟导入：在函数或方法内部进行导入，而不是在模块顶部。这种“延迟导入”可以避免初始化时的循环依赖问题。


```python
def my_function():
    from data.action_data_manager import ActionDataManager
    # 使用 ActionDataManager

```

### from import和import
`from ... import ...`
1. **导入特定对象**：使用`from module import name`语句可以直接从模块中导入一个或多个特定的对象（如函数、类或变量）。
2. **名称空间**：导入的对象直接进入当前名称空间，无需使用模块名称作为前缀。
3. **循环引用问题**：如果两个模块互相`from ... import ...`对方，Python在导入过程中需要立即解析这些对象。如果被导入的对象尚未定义（因为相互依赖尚未解决），就会导致错误。

`import ...`
1. **导入整个模块**：使用`import module`语句会导入整个模块。
2. **名称空间**：必须通过模块名称来访问模块内的对象，例如`module.name`。
3. **循环引用问题**：`import`在处理循环引用时通常更为宽容。这是因为`import`语句导入模块本身，而不是其中的特定对象。当Python执行`import`语句时，它只是加载模块并使其可用，而不需要立即解析模块内的所有内容。因此，即使存在循环引用，只要避免在模块级别执行依赖于彼此的代码，通常不会出现问题。

### UI和Data模块互相引用的解决方案

1. 使用事件或信号槽机制（QT框架原生做法）
这是避免循环依赖的一种优雅方式。

```python
class ActionDataManager:
    data_changed = Signal()  # 使用适当的信号库

    def addActionItem(self, action):
        self.actions.append(action)
        self.data_changed.emit()  # 触发信号

```

```python
class DataArea(QWidget):
    def __init__(self, data_manager):
        super(DataArea, self).__init__()
        data_manager.data_changed.connect(self.updateUI)

    def updateUI(self):
        actions = ActionDataManager().getActions()
        # 更新UI逻辑
```

2. 使用回调函数
```python
class ActionDataManager:
    def __init__(self):
        self.on_data_changed = None

    def addActionItem(self, action):
        self.actions.append(action)
        if self.on_data_changed:
            self.on_data_changed()

```

```python
class DataArea(QWidget):
    def __init__(self, data_manager):
        super(DataArea, self).__init__()
        data_manager.on_data_changed = self.updateUI

```

## 踩坑集

Q：QUiLoader.load ui文件时，报错栈溢出：
Process finished with exit code -1073740791 (0xC0000409)
A: 需要先实例化QApplication([])，再加载UI文件。


Q: QUiLoader().load的方式加载UI，使用Pycharm编写代码的时候，会出现编辑器报Cannot find reference或者Unresolved attribute reference的情况，如何解决?


## 感谢
ref: https://www.byhy.net/tut/py/gui/qt_02/
ref: https://blog.51cto.com/u_12072082/5569244
ref: https://blog.csdn.net/weixin_44593822/article/details/113834208
https://qtdebug.com/qtbook-qss-selector/
https://jwt1399.top/posts/41724.html#toc-heading-171
