0 前言

  最近接了一个小项目,主要任务是完成一个界面的设计,而且是基于Python,我第一反应就是使用大名鼎鼎的Qt来设计。Qt最早是用C语言开发的,但是后来也有了基于Python的第三方包,目前最新版是PyQt6.3,但是这个项目中使用的还是普及度更高的PyQt5。正好我也比较喜欢Python编程,于是边学边做,简单总结一些入门要点,授人与渔。

在此特别感谢Chat-GPT的帮助!真的是编程的利器!

1 PyQt5及其基本模块

  要使用PyQt5,首先需要进行安装:pip install PyQt5,如果想要尝鲜也可以安装PyQt6:pip install PyQt6
  PyQt5主要有三个部分:【摘抄自网上】

  • QtCore: 包含了核心的非GUI的功能。主要和时间、文件与文件夹、各种数据、模型、流、URLs、mime类文件、进程与线程一起使用。
  • QtGui: 包含了窗口系统、事件处理、2D图像、基本绘画、字体和文字类
  • QtWidgets: 包含了一些创建桌面的UI元素和控件

  了解包的结构还是非常有必要的,这样在看别人代码时能够有一个比较清晰的认识,也能快速定位到使用的包所在的位置。一般导入某个具体的类时会使用如下所示的import方式:

from PyQt5.QtWidgets import xxxx,xxxx
from PyQt5.QtGui import xxxxx,xxxx
from PyQt5.QtCore import xxxxxx,xxxxx

  这里不太建议使用from xxx import *这样的导入方式,虽然一时省事了,但是对于后期代码的维护和给别人看都十分不友好,也不利于自己对这个包的理解。

2 开发方式

  如果是用C++开发Qt,那必须得下载一个Qt Creator,来实现代码和界面进行关联,使开发过程更加便捷。但是用PyQt5的Python第三方包来开发,只需要下载一个Qt Designer用来设计UI界面即可。

  不过Qt Designer似乎不能通过下载一个执行程序来进行安装,而是通过下载第三方包的方式来下载。这里一般有两种方式。

  • PyQt5-Tools: 运行上面的提到的安装PyQt5的命令pip install PyQt5会自动安装sip(具体是啥有啥功能不太清楚),但不会自动安装Qt Designer,需要再安装PyQt5-Tools:pip install PyQt5-Tools,然后在Python对应版本的site-packages文件夹下面可以找到designer.exe文件,即Qt Designer。

  • PySide2: 除了安装PyQt5-Tools外,还可以安装PySide2这个包,就自带了Qt Designer这个软件,我采用的就是这种方式。和上面一样,也可以在Python的site-packages文件夹下找到,如下图所示。
    在这里插入图片描述

PySide2和PyQt之间的兼容性据说不错,它们之间的关系可以在网上找到很多比较详细的叙述。

  因此实际开发项目时,是使用Qt Designer来设计UI界面,得到一个.ui的文件,然后利用PyQt5安装时自带的工具pyuic5将.ui文件转换为.py文件:

pyuic5 -o mywindow.py mywindow.ui #先是py文件名,再是ui文件名

当然,如果是使用VS Code写代码,也可以考虑安装一个插件,帮助你执行这行命令。

在这里插入图片描述

在这里插入图片描述

之后再新开一个py文件,进行逻辑编写,这样实现了界面与逻辑分离,代码结构更加清晰。如下图所示是一般项目的结构:
在这里插入图片描述

虽然网上有很多都是上来就写代码运行得到窗口的教程,但是个人建议初学者还是先使用软件设计好UI文件,再转换成代码的方式,这样更加直观,而且调整控件位置也更方便。

  综上所述,PyQt5的开发主要是两个核心:ui界面的设计逻辑代码的编写。前者主要是会玩Qt Designer这个软件即可;后者理清楚代码结构也不是很难。

3 UI界面设计(Qt Designer)

  先来看看ui界面怎么玩:打开Qt Designer,点击新建,会弹出一个窗口:
在这里插入图片描述

这里一般是选择Main Window或者Widget,其中Main Window继承自Widget,添加了一些内容,本质二者差不多。这里选择的是Widget。
  建好文件后,得到一个空白页面,接下来就是往里面拖动控件并设计样式了,如下图所示。
在这里插入图片描述

这部分内容过于琐碎,这里只记录一些要点,后续随缘更新,遇到问题建议点对点搜索。

  • Layout部分的控件主要用于设置布局,即实现当窗口拖动时,控件的大小也会随之改变,而且可以利用布局控件设计各控件所占窗口大小的比例。
  • Containers部分的控件都可以设置布局,而且只有设置布局才能实现控件随窗口大小变化而变化! 这一点需要注意。
  • 设置布局之后想要实现控件居左或居右怎么办呢?那就用弹簧吧(Spacer)

4 逻辑代码的基本结构

  要想写好逻辑代码,首先要清楚它的基本结构。
  前面提到,除ui界面代码,还需要有一个逻辑代码,而逻辑代码个人感觉使用类的形式来组织更加方便,也更优雅。还记得创建ui时选择的类吗?是Widget还是Main Window,逻辑代码类最好是继承这个这个类,即QWidgetQMainWindow。一般的代码结构如下所示。

# 先导入主要的三个模块和各自内部常用的类
from PyQt5 import QtCore, QtWidgets, QtGui
from PyQt5.QtCore import pyqtSlot
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtGui import QColor
# 再导入设计的ui界面转换成的py文件
from Ui_window import Ui_Form

class mywindow(QWidget): #这里一定要继承QWidget类或者是QMainWindow类
    '''初始化函数
    '''
    def __init__(self) -> None:
        super().__init__() #先初始化父类【必须】
        self.ui = Ui_Form()
        self.ui.setupUi(self) #这个函数本身需要传递一个QWidget类,而该类本身就继承了这个,所以可以直接传入self(这就是继承的好处)

    '''按钮连接的槽函数
    '''
    @pyqtSlot() #装饰器,表示该函数是按名称自动被连接
    def on_pushButton_clicked(self):
        print("Pressed the pushButton")


if __name__ ==  "__main__":
	# 这里的代码逻辑基本相同
    app = QApplication([]) #先建立一个app
    wid = mywindow() #初始化一个对象,调用init函数,已加载设计的ui文件
    wid.show() #显示这个ui
    app.exec_() #执行app(运行界面,响应按钮等操作)

这里有一个需要注意,那就是按钮连接的槽函数,这里没有调用connect函数,而是使用了@pyqtSlot()这个装饰器,但前提就是要求函数命名要按照规则来:on_(控件对象名)_信号名(self,内置参数)。此外,想实现这个功能,还需要打开一个开关,这句代码默认在ui界面文件中就有,如下图所示。

在这里插入图片描述

5 常用控件及其使用方法

这部分内容过于琐碎,后续随缘更新,个人建议拿不准先问问Chat-GPT怎么说😅

5.1 QTableView //2023.4.11

  最近查一个代码的bug,发现界面运行起来之后内存在不断增加,通过逐行注释代码,最终找到问题所在。先看原始代码:

table:QTableView = self.ui.tabWidget1.widget(i).findChild(QTableView) #得到对应的表格
model = QStandardItemModel()
for j in range(len(self.packs[i])): #遍历一个列表
	items = [QStandardItem(str(j)), QStandardItem(self.packs[i][j])]
	model.appendRow(items)
table.setModel(model)

以上代码是在一个类中的成员函数当中,理论上都是局部变量,那么在调用函数完毕后所有的局部变量的内存都需要释放,也就是程序总的内存占用应该是平衡的才对,但实际上就是在不断增长(任务管理器数字缓慢上升)
  通过控制变量,最终发现就是函数appendRow的问题,只要一注释这行,内存立刻稳定了。由于pyqt是C语言写的,这里也不方便溯源,所以没找到问题所在。

  最终还是解决了的,那就是换一个函数,通过在网上找TableView刷新数据的方式,最终使用setItem解决了这个问题。改变后的代码如下:

table:QTableView = self.ui.tabWidget1.widget(i).findChild(QTableView) #得到对应的标签页
model = QStandardItemModel()
for j in range(len(self.packs[i])):
	##这种方式会出现内存泄漏
	# items = [QStandardItem(str(j)), QStandardItem(self.packs[i][j])]
	# model.appendRow(items)
	model.setItem(j,0,QStandardItem(str(j)))
	model.setItem(j,1,QStandardItem(self.packs[i][j]))
table.setModel(model)

5.2 如何设置右上角最小化,最大化和关闭按钮

  默认的widget控件虽然在Qt Designer中不显示最大化按钮,但显示时还是有的(快捷键Ctrl R 预览),但是有时候可能会需要设置这个按钮,比如想固定窗口,不让最大化,就可以将最大化的按钮禁用掉。这里记录使能和禁用按钮的一些代码(因为这部分代码没有智能提示)

from PyQt5.QtCore import Qt
# 使用setWindowFlag函数来设置,后面的参数True表示使能,False表示禁用
self.setWindowFlag(Qt.WindowMinimizeButtonHint, False)#最小化按钮
self.setWindowFlag(Qt.WindowMaximizeButtonHint, False)#最大化按钮
self.setWindowFlag(Qt.WindowCloseButtonHint, False)#关闭按钮
self.setWindowFlag(Qt.WindowMinMaxButtonsHint, False)#最小化和最大化按钮
self.setWindowFlag(Qt.WindowMinMaxButtonsHint | Qt.WindowCloseButtonHint, False)#还可以使用或运算一起设置

注意:这里的self是指继承widget或者mainwindow的类,也同样适用于dialog类


  此外,如果觉得默认的按钮太单调,也可以考虑把按钮隐藏,然后自己画一个并设置信号槽函数,实现最小化,最大化和关闭的功能。

from PyQt5.QtCore import Qt
# 首先将标题栏隐藏
self.setWindowFlags(Qt.FramelessWindowHint)
# 然后自己设置按钮并定义槽函数
# 最小化
self.showMinimized()
# 最大化
self.showMaximized()
# 一般显示
self.showNormal()

当然,如果隐藏了标题栏,那么就还涉及到鼠标拖动窗口移动的问题,因此,还需要写一部分代码来处理鼠标事件,这里建议参考这篇博客

5.3 输入控件设置默认内容

  在使用输入控件时,比如LineEdit,TextEdit时,想到有没有什么办法可以直接设置“默认内容”,这样在使用时就不用输入了,找了一些资料,发现果然还真有:

在这里插入图片描述

在使用时也非常简单:

s = self.ui.line_edit.placeholderText()  #直接返回默认内容
# 如果输入则取输入内容,否则取默认内容:
self.line_edit_s = self.ui.line_edit.placeholderText() if self.ui.line_edit.text() == '' else self.ui.line_edit.text()

还发现输出控件也有这个属性,可以用来设置提示语之类的

5.4 关于setObjectName函数的思考

  最近因为项目再次上马PyQt,遇到一些问题,心血来潮想研究一下ui文件导出的py文件的具体实现,然后就看到一大堆的setObjectName函数,如下图所示:

在这里插入图片描述

然后就感觉有点奇怪,明明代码里面都是使用变量名来访问,为什么还需要设置“名称”呢? 经过查找资料,才发现这个ObjectName要比我想象中复杂得多。

  • ObjectName是Qt中对象的唯一标识符(唯一标识符不是变量名!)用来索引对象,要求保持唯一,同时需要为有效的标识符,即数字,字母,下划线,不能以数字开头——和编程语言的变量函数名要求一样。
  • 在使用findChild()findChildren()函数查找子组件时,依据的索引信息就是ObjectName
  • 在使用样式表时设置的也是索引这个
  • 信号槽关联使用的也是这个。我现在才知道,一开始不信,但是尝试把py文件里面的ObjectName改掉,果然原来的槽函数就不会调用了。但细想其实很合理,因为里面设置可以使用名字来索引的函数:QtCore.QMetaObject.connectSlotsByName()从函数名中ByName就可见一斑。

5.5 layout的代码实现

  之所以推荐使用的开发方式是用Qt Designer设计然后转换成python代码而不是直接手敲代码,除了不够直观、参数设置不方便之外,还有就是对于布局的设置有点麻烦。
  但是很显然,会直接敲代码还是很有必要的,在测试代码时非常有用,因此这里想简单总结一下layout在代码上如何实现。

  在使用时,首先要创建一个layout,常用的有QtWidgets.QHBoxLayout(水平布局),QtWidgets.QVBoxLayout(垂直布局),然后在布局中添加控件或者布局等,流程非常简单。

from PyQt5 import QtCore, QtGui, QtWidgets

self.horizontalLayout = QtWidgets.QHBoxLayout(VedioAudioClient) #如果是某个控件设置布局,就可以加入该参数,也可以不加
self.horizontalLayout.setObjectName("horizontalLayout")
self.label = QtWidgets.QLabel(VedioAudioClient)
self.horizontalLayout.addWidget(self.label)  #增加控件
self.verticalLayout_1= QtWidgets.QVBoxLayout()
self.horizontalLayout.addLayout(self.verticalLayout_1) #增加布局
self.horizontalLayout.addItem(spacerItem) #增加弹簧

此外,还有一些setStretchsetSpacing等一些设置布局参数的函数可以调用。

5.6 Updating…

6 遇到的问题和解决方案

  这里主要记录在使用过程中遇到的值得记录的问题和解决方案,会随着后续使用不断更新。

6.1 RuntimeError: wrapped C/C++ object of type has been deleted

这个是因为弹出窗口加上了一句代码:self.setAttribute(Qt.WA_DeleteOnClose),表示关闭窗口时删除所有子控件,最好把这句给去掉。

参考链接

6.2 pyqt中的多线程问题

  在qt代码中,如果在非GUI线程刷新控件样式或者更新控件相关的程序,那么可能会导致控件样式显示失败或者数据更新失败,严重者还有可能直接程序崩溃。
  因此比较合理的做法是使用pyqy中的pyqtSignal类,通过自定义信号的发射与响应函数,来实现与界面的“沟通”,从而刷新界面上的数据和样式。
  关于pyqtSignal的使用,为了使得代码结构更加紧凑,这里是将它定义在需要更新的界面类中,然后把信号相关的响应函数也写在类中,而信号的激发可以在非GUI线程外部。

# 先导入主要的三个模块和各自内部常用的类
from PyQt5 import QtCore, QtWidgets, QtGui
from PyQt5.QtCore import pyqtSlot,pyqtSignal
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtGui import QColor
# 再导入设计的ui界面转换成的py文件
from Ui_window import Ui_Form

class mywindow(QWidget): #这里一定要继承QWidget类或者是QMainWindow类
    '''初始化函数
    '''
    sig = pyqtSignal(list) #要定义为类属性,参数传发射数据的类型
    def __init__(self) -> None:
        super().__init__() #先初始化父类【必须】
        self.ui = Ui_Form()
        self.ui.setupUi(self) #这个函数本身需要传递一个QWidget类,而该类本身就继承了这个,所以可以直接传入self(这就是继承的好处)
        self.sig.connect(self.func)

    '''按钮连接的槽函数
    '''
    @pyqtSlot() #装饰器,表示该函数是按名称自动被连接
    def on_pushButton_clicked(self):
        print("Pressed the pushButton")

	def func(self, l:list) #这个函数的参数要和信号声明时的参数保持一致
		print("发送的数据为: ", l)

if __name__ ==  "__main__":
	# 这里的代码逻辑基本相同
    app = QApplication([]) #先建立一个app
    wid = mywindow() #初始化一个对象,调用init函数,已加载设计的ui文件
    wid.show() #显示这个ui
    app.exec_() #执行app(运行界面,响应按钮等操作)



# 其他线程中的函数,可以不在同一文件
win = mywindow() #窗口变量

# 一大堆显示函数

# 线程刷新数据
def thread1():
	s = [1,2,3,4,5]
	win.sig.emit(s) #参数还是要和声明信号时保持一致

总结来说,大概有这么几点:①信号声明最好是放在窗口类中,更加紧凑(有说这个信号只能在类中使用,不能直接定义一个全局变量);②声明时的参数,响应函数的参数,激发时传递的数据类型全部保持一致,这个很重要;③如果不同线程都会刷新界面,建议分开信号传递,不要都用一个信号,怕出问题。

关于在PyQt中使用多线程,可以看看这篇文章,写得很详细

6.3 退出界面时退出系统

如果是使用sys.exit(app.exec_()),当关闭界面时,只会关闭当前的GUI线程,如果有其他线程在运行是不会关闭的,显然不符合实际情况,表现为控制台指令执行不会结束。所以可以改为os._exit(app.exec_()), 实现关闭界面即退出系统。

6.4 Updating…

7 参考链接

  • Qt Widgets C++ Classes
    这是C++语言中的一个类列表,但同样适用于Python编程,可以查看类中有哪些函数

  • Qt GUI设计
    知乎大佬的总结,模块比较齐全

8 扩展

8.1 Qt中使用数据库

  值得一提的是,如果想在Qt中使用数据库,可以考虑使用其自带的库来实现,这里放一个使用SQLite的例程,可以作为参考。

from PyQt5.QtSql import QSqlDatabase, QSqlQuery

def createDB():
    # 创建一个通用数据库对象,参数"QSQLITE"代表通用数据库为SQLite数据库类型
    db = QSqlDatabase.addDatabase("QSQLITE")
    # 指定SQLite数据库的文件名
    db.setDatabaseName("./db/database.db")
    if not db.open():
        print("无法建立与数据库的连接")
        return False
    # 创建查询功能
    query = QSqlQuery()
    # 执行创建表格的指令
    query.exec('create table people(id int primary key,name varchar(10),address varchar(50))')
    # 执行往表格中插入数据的指令
    query.exec('insert into people values(1, "Kirigaya", "GitHub")')
    db.close()
    return True

if __name__ == '__main__':
    createDB()

如果想使用其他数据库,比如MySQL,需要将addDatabase函数的参数改为QMYSQL

参考链接

8.2 python自带GUI库Tkinter

  作为python自带的GUI库,虽然控件更少,但使用也更简单,比较适合界面不是很复杂的应用(貌似没有图形界面操作,只能用代码编写)

教程链接

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐