返回「计算机、信息技术与工程」

Python Qt

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

更多
Markdown 结构化数据
本文目录 31 个章节

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

来源: 简言之 文章作者: 简简 文章链接: https://jwt1399.top/posts/41724.html#toc-heading-171 本文章著作权归作者所有,任何形式的转载都请注明出处。

工具

QtAwesome https://github.com/spyder-ide/qtawesome 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

  • 配置rcc与uic菜单项

Program -->​​D:\...\venv\Scripts\pyside6-uic.exe​​
Arguments --> $FileName$ -o $FileNameWithoutAllExtensions$.py
Program -->​​ ​​D:\...\venv\Scripts\pyside6-rcc.exe​​
Arguments --> $FileName$ -o $FileNameWithoutAllExtensions$_rc.py
  • 打包exe
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

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

绘制相关

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

常用控件

菜单栏:

菜单栏(QMenuBar) ->  菜单(QMenu)->  子菜单(QMenu) -> QAction

MDI 多个子窗口

树控件

界面布局

布局经验(via. 白月黑羽)

  • 先不使用任何Layout,把所有控件 按位置 摆放在界面上
  • 然后先从 最内层开始 进行控件的 Layout 设定
  • 逐步拓展到外层 进行控件的 Layout设定
  • 最后调整 layout中控件的大小比例, 优先使用 Layout的 layoutStrentch 属性来控制

多线程问题处理

Qt建议:

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

信号在多线程问题中的使用:

  • 自定义一个Qt 的 QObject类,里面封装一些自定义的 Signal信号。 一种信号定义为 该类的 一个 静态属性,值为Signal 实例对象即可。 Signal实例对象的初始化参数指定的类型,就是 发出信号对象时,传递的参数数据类型。 可以定义 多个 Signal静态属性,对应这种类型的对象可以发出的 多种 信号。

  • 定义主线程执行的函数处理Signal信号(通过connect方法)

  • 在新线程需要操作界面的时候,就通过自定义对象 发出 信号

  • 主线程信号处理函数,被触发执行,获取Signal里面的参数,执行必要的更新界面操作。

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样式用法

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

assets/image-20231115162013069.png

assets/image-20231115162029989.png


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 图片来源:https://www.byhy.net/tut/py/gui/qt_09/

QSS样式在工程中的用法与开发流程

组合方式

QApplication 引用.qss 文件

QSS选择器

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子控件选择器

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

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元素传递给它们。
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

绘制好后,可将各个部分拆到不同的UI文件当中。 assets/image-20231128151446641.png

每个UI文件编写对应的类,在运行时对UI进行初始化。 assets/image-20231128150928996.png

类中的代码如下:

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 在MainWindow类中逐个对子模块进行实例化和绑定。

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)

单例模式用法

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
## 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. 重新组织代码结构:调整代码结构,避免模块间的直接循环依赖。
  • 将相关的函数或类重构到一个新的模块中
  • 创建一个新的模块作为共同依赖。
  • 通过使用观察者模式、信号槽机制或回调函数来减少模块间的直接依赖。
  1. 使用import而不是from ... import ...

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

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框架原生做法) 这是避免循环依赖的一种优雅方式。
class ActionDataManager:
    data_changed = Signal()  # 使用适当的信号库

    def addActionItem(self, action):
        self.actions.append(action)
        self.data_changed.emit()  # 触发信号
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逻辑
  1. 使用回调函数
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()
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