前言

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

原视频传送门如下

官方文档链接:Qt for Python

列表控件(QListWidget)

这里我们下载一个faker库,用于生成人名,模拟列表中的元素

1
pip install faker

在初始化函数中,我们创建faker库的实例

1
self.fake = Faker(locale='zh_CN')

创建列表

首先我们需要从QtWidgets中引入QListWidegt

1
from PySide6.QtWidgets import QListWidget

接下来我们可以创建一个列表控件实例

1
self.listWidget = QListWidget()

增删插改查

添加元素

使用addItem方法可以一次向列表中添加一个元素

1
self.listWidget.addItem(self.fake.name())

addItem方法可以接收字符串参数,也可以接收QListWidgetItem类的实例

之所以要额外使用QListWidgetItem这种数据类型,是因为它定义了一些便捷的属性和方法

1
2
from PySide6.QtWidgets import QListWidgetItem
self.listWidget.addItem(QListWidgetItem(self.fake.name()))

当然,逐个添加有时会比较麻烦,我们可以使用addItems方法一次添加多个元素

不过addItems方法只能传入字符串序列

1
self.listWidget.addItems([QListWidgetItem(self.fake.name()) for _ in range(20)])

我们传入20个人名,效果如下:

可以看到,当列表的元素较多时,右侧会出现滚动条,这也是列表的特性之一

插入元素

插入元素同样有insertIteminsertItems两种方法,分别是一次性插入一个和多个元素

insertItem方法需要传入两个参数,分别是插入位置和插入元素

例如我们在刚刚的列表的第三个位置插入数字3

1
self.listWidget.insertItem(2, '3')

insertItems则是指定插入位置以及字符串序列

1
self.listWidget.insertItems(2, ['3','4','5'])

删除元素

删除列表中的元素需要用到takeItem方法

例如我们想删除列表中的第三个元素,我们只需传入被删元素的序号

1
self.listWidget.takeItem(2)

修改元素

要修改列表中的某个元素,我们需要先获取到该元素

此时需要用到item方法,我们将获取的目标元素赋给一个变量

1
itemGet = self.listWidget.item(2)

当我们向列表中添加或者插入元素时,无论是用的字符串还是QListWidgetItem,每个元素最后都会成为QListWidgetItem数据结构,所以我们的接收变量itemGet也是指向一个QListWidgetItem

我们直接使用该数据结构的方法setText即可修改它的文本内容

1
itemGet.setText('6')

或者我们也可以压缩到一行语句

1
self.listWidget.item(2).setText('6')

查找元素

查找元素涉及到findItem方法

我们需要传入两个参数,匹配的字符串,以及匹配模式

当然,匹配模式需要我们先导入核心库

1
from PySide6.QtCore import Qt

下面是一些常用的匹配模式

匹配模式名 特点
MatchContains 匹配包含目标字符串的元素
MatchEndsWith 匹配以目标字符串结尾的元素
MatchStartsWith 匹配以目标字符串开头的元素
MatchCaseSensitive 匹配时区分大小写
MatchRegularExpression 正则匹配
MatchExactly 完美匹配

该方法会返回一个列表变量,该变量中包括所有符合条件的元素

例如我想输出列表中所有包含字的人名

1
2
3
result = self.listWidget.findItems('张', Qt.MatchFlag.MatchContains)
for item in result:
print(item.text())

常用信号和槽

currentItemChanged

currentItemChanged信号可以侦测到当前选则元素的改变

选则(selected)即列表元素的一种显示状态,鼠标点击的列表元素会被选则,背景会被加深为蓝色

列表会默认选则第一个元素,只是它的背景不会被加深

例如我们可以写一个测试程序:当前选则元素改变时,输出新选则元素的内容

1
2
3
4
def bind(self):
self.listWidget.currentItemChanged.connect(self.currentChanged)
def currentChanged(self):
print(self.listWidget.currentItem().text())

currentItem()方法用于获取当前选则的元素

其实currentItemChanged信号会向槽发送两个参数,分别为当前选则元素及上一个选则的元素

此时我们可以简化槽函数

1
2
def currentChanged(self, item):
print(item.text())

itemChanged

itemChanged信号可以侦测到当前选中元素的状态改变

当某个列表元素被设置为可选时,元素左侧会出现小方框,当鼠标勾选该方框时则为选中(checked)

例如我们给第一个元素先设置为未选中状态

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

同时我们使用itemChanged信号来侦测元素的选中状态,当有元素的选中状态发生变化时,itemChanged信号会发送该元素给槽

1
2
3
self.listWidget.itemChanged.connect(self.itemChanged)
def itemChanged(self, item):
print(item.text(), item.checkState())

checkState()属性会返回元素的选中状态,不过并不是布尔值,而是特定的枚举类型

clear

clear()槽在被调用时会清空列表中的所有元素

列表排序

想要对列表中的元素进行排序时,使用sortItems方法

我们只需向该方法传入排序规则即可,升序即AscendingOrder,降序传入DescendingOrder

1
self.listWidget.sortItems(Qt.SortOrder.DescendingOrder)

下面是数字列表的升序演示

该排序规则使用的是字符串排序比较规则,即从第一个字符开始比较,所以不是单纯的数字从小到大

列表的上下文菜单

给列表控件添加上下文菜单的方法与之前介绍的控件上下文菜单一致

首先给列表控件设置上下文菜单的策略

1
self.listWidget.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)

接着我们向其中添加Action,即操作

事实上,这是一种伪上下文菜单,因为我们无法对列表中的具体元素定制上下文菜单的内容,所有元素使用的上下文菜单都是一样的

图形视图框架

简介

在之前文章的内容中,我们想要显示图片,只能通过QLabel控件

但是由于Qlabel本身并不是为浏览图片设计的,所以对交互等限制很大

同时如果我们想显示多张图片时,使用Qlabel也较为麻烦

pyside6中的图形视图框架可以让我们管理大量的自定义2D图元并与之交互

图形视图框架主要包含三个类:QGraphicsItem图元类、QGraphicsScene场景类和QGraphicsView视图类。

简单概括下三者的关系就是:图元放在场景上,场景内容通过视图来显示。下面我们来一一进行讲解

框架核心

在 PySide6的图形视图框架中,场景(Scene)、视图(View)和图元(Graphics Items)之间的关系构成了框架的核心。

场景是一个抽象的二维空间,用于组织和管理图元,但它本身不负责图元的渲染。

图元是场景中的基本构建块,用于表示所有可视化对象。它们存在于场景中,由场景管理。

视图是场景的可视化表示,是用户与场景和图元交互的界面。一个场景可以被多个视图展示,每个视图可以展示场景的不同部分或以不同方式渲染相同的内容。

QGraphicsView类与QGraphicsScene类配套实现了类似Mode/View的架构,这种设计模式旨在分离图形的管理与图形的呈现。

使用图形视图框架的一般流程包括:

  1. 创建一个 QGraphicsScene 实例来存储和管理图形项。
  2. 创建各种 QGraphicsItem 实例,并将它们添加到场景中。
  3. 创建一个 QGraphicsView 实例,将其设置为显示前面创建的场景。
  4. 使用 QGraphicsView 提供的功能来导航和交互场景。

图元类(QGraphicsItem)

图元在图形视图框架中可以表现为文本、图像、标准的几何形状或者是自定义的图形。已经有一些预定义的图元类型提供给开发者使用,包括:

  • QGraphicsLineItem 用于表示直线。
  • QGraphicsRectItem 用于表示矩形。
  • QGraphicsEllipseItem 用于表示椭圆。
  • QGraphicsPixmapItem 用于展示图片。
  • QGraphicsTextItem 用于展示文本。
  • QGraphicsPathItem 用于绘制复杂的图形路径。

例如下面的代码创建了一个矩形框图元,左上角坐标为同时给它设置可拖拽和可选择的属性

坐标为(120,30),宽50,高30

1
2
3
self.rect = QGraphicsRectItem()
self.rect.setRect(120, 30, 50, 30)
self.rect.setFlags(QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsSelectable)

场景类(QGraphicsScene)

场景是所有图元的容器,提供了一个二维空间,在这个空间中可以添加、移动和管理图元。可以将场景想象成一个无限大的画布,而你可以在任意位置放置图形对象(如矩形、椭圆、文本等)。

场景本身是独立于任何视图的,这意味着你可以有多个视图展示同一个场景的不同部分或以不同的方式(如不同的缩放级别)

我们可以将场景类比于模型,同一个模型可以有不同的展示方式,同时多个场景也可以通过切换展示在同一个视图中。

QGraphicsScene类拥有非常多的管理图元的方法

首先我们通过下面这行代码创建一个场景

1
self.scene = QGraphicsScene()

当然,场景作为一个画布,我们需要为其设置大小

而且,图元在场景中的位置是通过坐标来确定的,所以我们还需要设置坐标原点

下面这行代码通过setSceneRect方法,设置场景的大小为300X300,坐标原点为(0,0),即左上角

1
self.scene.setSceneRect(0, 0, 300, 300)

接下来,通过将addItem方法将之前创建的矩形图元添加到场景中

1
self.scene.addItem(self.rect)

当然,除了直接添加图元实例,QGraphicsScene类还提供了一些方法,用于向场景中快速添加不同种类的图元(免去创建图元这一步骤),这些方法会返回创建的图元的指针,我们可以用变量接收

视图类(QGraphicsView)

图元和场景部分均属于模型,而视图则是实际显示出来的窗口控件(QWidget),与控件一样,它最终会被添加到布局并展示在窗口中

和场景一样,视图也是基于笛卡尔坐标系,左上角为原点,向右为x正轴,向下为y正轴。

如果视图的尺寸小于场景的尺寸,视图会变成一个可滚动的区域,允许用户通过滚动条查看整个场景的内容。即使视图与场景的大小相同,滚动条也会出现(需要手动隐藏)。

下面的代码创建了一个300X300大小的视图控件

1
2
self.view = QGraphicsView()
self.view.resize(300, 300)

接下来我们需要设置视图展示的场景,并将视图添加到布局中

1
2
self.view.setScene(self.scene)
self.mainLayout.addWidget(self.view)

效果如下

交互机制

QGraphicsItem 并没有继承 QObject 类,因此它自身不支持信号与槽的机制,也不能直接应用动画效果。

虽然不能使用信号与槽,图形视图框架也有一些用于和用户交互的机制

  1. 事件处理:

    QGraphicsItem 提供了一系列的事件处理函数,可以被重写来响应不同的事件,例如鼠标点击(mousePressEvent)、鼠标移动(mouseMoveEvent)等。通过重写这些事件处理函数,可以在事件发生时执行特定的逻辑,从而模拟信号和槽的行为。

  2. 场景事件:

    QGraphicsScene 也提供了事件处理机制,比如 itemClickeditemHovered 等事件。通过在场景层面处理这些事件,可以实现对场景中图元事件的响应。由于 QGraphicsScene 继承自 QObject,它能够使用信号和槽机制,从而允许场景与其他 QObject 对象或图元之间的通信。

  3. 定时器:

    对于需要定时更新或检查状态的图元,可以使用 QTimerQTimer 是基于 QObject 的,因此支持信号和槽。通过在图元中使用定时器,可以定时触发特定的行为,而无需直接在图元中实现信号和槽的机制。

  4. 绘制更新

    图元类中提供了paint方法,当图元、场景或者视图发送变化时,Qt 会自动调用paint方法,重新绘制视图

    我们可以通过重写的 paint 方法,在其中添加绘制行为,并设置绘制条件

实例

下面我们通过一个例子来体现前面提到的四种交互机制

首先我们创建一个新的图元类NewRectItem,继承QGraphicsRectItem

其中我们重写鼠标响应以及悬停的事件处理函数,加上我们想要的反馈:鼠标点击时图元变色、悬停时边框变红,用于测试事件处理机制

重写paint方法来添加自定义的绘图条件及行为:当矩形图元在鼠标下方时会在中间画一个圆,用于测试绘制更新机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class NewRectItem(QGraphicsRectItem):
def __init__(self, x, y, width, height):
super(NewRectItem, self).__init__(x, y, width, height)
self.setAcceptHoverEvents(True) # 启用悬停事件
self.setFlags(QGraphicsItem.ItemIsMovable )

# 1. 事件处理:
# 重写鼠标点击事件来改变框内颜色
def mousePressEvent(self, event):
self.setBrush(QColor(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)))

# 重写悬停事件来改变边框颜色
def hoverEnterEvent(self, event):
self.setPen(QPen(Qt.red, 3))

def hoverLeaveEvent(self, event):
self.setPen(QPen(Qt.black, 1))

# 2. 绘制更新:
# 重写paint来添加自定义的绘图条件及行为
def paint(self, painter, option, widget):

super().paint(painter, option, widget)
if self.isUnderMouse():
# 如果被选中,在中间绘制一个圆
centerX = self.boundingRect().x()+self.boundingRect().width()/2
centerY = self.boundingRect().y()+self.boundingRect().height()/2
radius = min(self.boundingRect().width(),self.boundingRect().height())/4 # 圆的半径为矩形最短边的四分之一

# 设置画笔和画刷来绘制圆
painter.setPen(QPen(QColor(0, 0, 0, 127), 2)) # 设置圆的边框颜色和宽度
painter.setBrush(QColor(0, 0, 0, 127)) # 设置圆的填充颜色和透明度

# 绘制圆
painter.drawEllipse(QPointF(centerX, centerY), radius, radius)

接下来我们也自定义一个新场景MyScene

通过重写场景事件mouseDoubleClickEvent来达到交互效果:双击鼠标时,在场景添加一个矩形图元

同时设置一个每秒触发一次的计时器,用于测试计时器机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MyScene(QGraphicsScene):
def __init__(self, parent=None):
super(MyScene, self).__init__(parent)
self.timer = QTimer()
self.timer.timeout.connect(self.onTimeout)
self.timer.start(1000)

# 3. 场景事件: 重写 mouseDoubleClickEvent 来响应鼠标双击
def mouseDoubleClickEvent(self, event):
rect = NewRectItem(event.scenePos().x() - 25, event.scenePos().y() - 25, 50, 50)
self.addItem(rect)

# 4. 定时器: 将timeout信号与自定义的槽函数onTimeout连接
def onTimeout(self):
# 定时器触发的行为:旋转所有图元
for item in self.items():

centerX = item.boundingRect().x()+item.boundingRect().width()/2
centerY = item.boundingRect().y()+item.boundingRect().height()/2

item.setTransformOriginPoint(centerX, centerY)
item.setRotation(item.rotation() + 10)

需要注意的是,由于图元是沿着变换原点旋转,所以每次触发旋转事件时需要使用setTransformOriginPoint方法更新变换原点,将其矫正到矩形中央的坐标

最后我们在场景中添加几个矩形图元

1
2
3
4
# 初始化一些图元
for _ in range(5):
rect = NewRectItem(random.randint(0, 150), random.randint(0, 150), 50, 50)
self.scene.addItem(rect)

交互效果如下:

实践:OCR可视化

这两天调研了一下WPS的图片转文字功能后,突然发现Pyside6的图形视图框架及列表控件非常适合用来实现OCR可视化

右侧文字部分在被鼠标覆盖时,背景颜色会加深,这与列表控件的特性很相似

而左侧的检测框则可以看成是一个个图元,因为它也具有能交互的特性