第 2 章

Qt 基础对象

本章共 10 个小节 · PySide6 Basic Tutorial
本章要点:
  1. 了解 QObject 类。
  2. 事件的处理与过滤。
  3. 信号与槽。
  4. QByteArray 与 QBitArray。
  5. QBuffer。
  6. QSysInfo。
  7. 动态属性。
  8. 生成随机数。
2.1

QObject 类与 Qt 对象模型

QObject 类是所有 Qt 对象的基类。所有从 QObject 派生的对象都可以组成对象树。例如,有 A、B、C、D 4 个对象,B 可以作为 A 的子级,C 和 D 可以作为 B 的子级,于是就形成了如图 2-1 所示的对象树。

QObject 对象之间使用信号(signal)和槽(slot)进行通信。当某对象(发送者)的内部状态发生改变或发生特定事件时,它会发出信号。接收者会马上做出响应。信号与槽之间可以是一对一的关系,也可以是多对一或多对多的关系。即一个信号可以连接多个槽,一个槽可以被多个信号连接。

图 2-1
Qt 对象树
Qt 对象树

QObject 对象也支持事件的处理和筛选。派生类可以重写 event 方法来处理接收到的事件。若返回 True,表示当前对象已处理完毕,并且事件不会传递给基类。一般来说,在 event 方法返回前,应调用基类的 event 方法,把未处理的事件传递给基类去处理。

2.2

建立对象的层级关系

前文已述,Qt 对象可通过建立父子关系来组成对象树。两个对象之间建立父子关系有两种方法。

  1. 调用类构造函数时,将要作为父级的对象传递给 parent 参数。
  2. 调用当前对象的 setParent 方法,使当前对象成为另一个对象的子级。

下面来看一个简单的示例。AObject、BObect、CObject 都从 QObject 类派生。

# 导入 QObject 类 from PySide6.QtCore import QObject # 定义三个类,均派生自 QObject class AObject(QObject): # 构造函数 def __init__(self, parent=None) -> None: # 调用基类的构造函数 super().__init__(parent) class BObject(QObject): # 构造函数 def __init__(self, parent=None) -> None: # 调用基类的构造函数 super().__init__(parent) class CObject(QObject): # 构造函数 def __init__(self, parent=None) -> None: # 调用基类的构造函数 super().__init__(parent)

从 QObject 派生的类,其构造函数应包含一个 parent 参数,用于引用其他对象。parent 参数所引用的对象将作为当前对象的父级。

接下来依次实例化上述三个类,注意 AObject 实例为父级对象,其余两个都是它的子级对象。

# 创建 AObject 实例,它没有父级对象,parent 参数可忽略 obj1 = AObject() # 创建 BObject 实例,它的父级对象是 obj1 obj2 = BObject(obj1) # 创建 CObject 实例,它的父级对象也是 obj1 obj3 = CObject(obj1)

也可以用 setParent 方法来建立层级关系。

obj1 = AObject() obj2 = BObject() obj3 = CObject() obj2.setParent(obj1) obj3.setParent(obj1)

若要获取某对象中所包含的子级对象,可以调用 findChildren 方法。例如,在本示例中,调用 obj1 的 findChildren 方法可以返回 obj2 和 obj3。该方法至少需要一个参数,提供待查找对象的类型。在本示例中,需要同时找出 obj2 和 obj3,因此传递给方法的类型应为 QObject 类,因为 AObject 和 BObject 的共同基类是 QObject。

此时,chdobjs 变量中应该包含 obj2 和 obj3,下面代码将输出它们的类名称(访问 __class__ 成员)。

chdobjs = obj1.findChildren(QObject) for obj in chdobjs: print(obj.__class__)

执行后会输出以下内容:

输出结果
<class '__main__.BObject'>
<class '__main__.CObject'>
2.3

事件与 event 方法

当 Qt 对象接收到某个事件后,会调用 event 方法。派生类可以重写该方法,以实现自定义的事件响应行为。

event 方法包含一个参数,类型为 QEvent。此参数包含与事件有关的信息,其中最为核心的是 type 属性,它返回当前事件的类型(由枚举类型 QEvent.Type 定义)。QEvent 只是基类,程序在实际运行时会根据事件的类型,向 event 方法传递不同的参数值。

例如,若当前发生的事件类型为 Show(当窗口或窗口上的可视化组件呈现之后发生),那传递给 event 方法的参数值就是 QShowEvent 类。再如,当事件类型为 MouseButtonPress 时,表示用户按下了鼠标上的某个键,event 方法的参数将接收到 QMouseEvent 类型的对象。该对象将包含与此鼠标事件相关的附加信息——当前鼠标指针所处的位置坐标、哪个键被按下。

下面示例将重写 event 方法,捕捉键盘按下事件(事件类型为 KeyPress),当用户按下的是方向键(上、下、左、右)时,调用 print 函数向控制台输出文本信息。

定义 MyUI 类,它派生自 QWindow,而 QWindow 派生自 QObject 类。因此,MyUI 类也是 QObject 的子类,可以重写 event 方法。

from PySide6.QtCore import * from PySide6.QtGui import * class MyUI(QWindow): # 构造函数 def __init__(self, parent=None): super().__init__(parent) # 设置窗口标题 self.setTitle("示例程序") # 设置窗口大小 self.resize(280, 245) # 处理事件 def event(self, e: QEvent) -> bool: # 判断是否为键盘按下事件 if e.type() == QEvent.KeyPress: keyevent: QKeyEvent = e if keyevent.key() == Qt.Key_Left: print("你按下了【向左】键") if keyevent.key() == Qt.Key_Right: print("你按下了【向右】键") if keyevent.key() == Qt.Key_Up: print("你按下了【向上】键") if keyevent.key() == Qt.Key_Down: print("你按下了【向下】键") # 已处理过,返回 True return True # 其他事件交由基类去处理 return super().event(e)

在 event 方法中,先判断当前接收到的事件类型是否为 KeyPress。若是则表明参数 e 的类型是 QKeyEvent。为了便于访问,上述代码定义了新的变量 keyevent,并批注其类型为 QKeyEvent,后面的代码将使用此变量。

key 属性表示被按下的键(键码),由枚举类型 Qt.Key 定义。Key_Up 表示向上键,Key_Down 表示向下键。同理,Key_Left、Key_Right 对应的是向左键和向右键。

实例化 MyUI 类,并显示窗口。

if __name__ == "__main__": app = QGuiApplication() obj = MyUI() # 显示窗口 obj.show() # 开始消息循环 app.exec()

带有用户界面的应用程序,需要先创建一个 QGuiApplication 实例,用于处理当前应用程序的消息循环。创建 MyUI 实例后,调用 show 方法即可显示窗口(此方法继承自 QWindow 类)。最后调用 QGuiApplication 实例的 exec 方法,消息循环正式运行。exec 方法调用后会一直处于等待状态,直到应用程序即将退出时才会返回。

待窗口显示后,随意按下键盘上的方向键,控制台窗口会打印以下信息:

输出结果
你按下了【向左】键
你按下了【向右】键
你按下了【向上】键
你按下了【向下】键
你按下了【向右】键
你按下了【向左】键

2.3.1 接受与忽略事件

在重写 event 方法时,如果调用 QEvent 对象的 accept 方法,表示当前 Qt 对象已接受该事件,这会使事件不会传播给父级对象。若调用了 QEvent 对象的 ignore 方法,表示当前 Qt 对象将忽略该事件,事件会传播给父级对象。

注意,此处 accept 和 ignore 方法只是控制事件是否向 Qt 对象树的父级传播,而不是向基类传播。

接下来将完成一个示例,该示例定义两个从 QWidget 派生的类。其中,MyWindow 类表示应用程序窗口,MyElement 表示位于窗口上的可视化组件。在 MyElement 类中重写 event 方法,将类型为 MouseButtonPress 的事件(鼠标按下时发生)标记为“忽略”。MouseButtonPress 事件将向上传播到 MyWindow 对象的 event 方法中。

首先要导入需要的类型。

from PySide6.QtCore import * from PySide6.QtGui import * from PySide6.QtWidgets import QWidget, QApplication, QMessageBox

下面代码实现 MyElement 类,派生自 QWidget 类。

class MyElement(QWidget): # 构造函数 def __init__(self, parent: QWidget = None): super().__init__(parent) # 设置大小 self.resize(180, 70) # 重写 event 方法 def event(self, e: QEvent) -> bool: if e.type() == QEvent.MouseButtonPress: # 忽略此事件 e.ignore() return True return super().event(e)

如果上述代码调用的是 e.accept() 方法,那么 MouseButtonPress 类型的事件就不会传给 MyWindow 对象了。但本示例调用的是 e.ignore() 方法,所以 MyWindow 对象会接收到 MouseButtonPress 类型的事件。

接下来实现 MyWindow 类,它的基类也是 QWidget 类。

class MyWindow(QWidget): # 构造函数 def __init__(self, parent=None) -> None: super().__init__(parent, Qt.Window) # 窗口标题 self.setWindowTitle("示例程序") # 窗口大小 self.resize(260, 200) # 重写 event 方法 def event(self, e: QEvent) -> bool: # 如果是鼠标按下事件 if e.type() == QEvent.MouseButtonPress: # 弹出消息框 QMessageBox.information(self, "提示", "父窗口接收到鼠标事件", QMessageBox.Ok) # 已处理,返回 True return True return super().event(e)

MyWindow 类也重写了 event 方法,如果接收到的事件类型是 MouseButtonPress 就调用 QMessageBox.information 静态方法弹出消息对话框。

最后,实例化 MyWindow 类,并且作为 MyElement 对象的父级对象。调用 show 方法将显示窗口。

if __name__ == "__main__": app = QApplication() # 实例化 MyWindow 类 win = MyWindow() # 实例化 MyElement 类,它的父级对象是 win elm = MyElement(win) # 调整可视化对象在窗口上的位置 elm.move(36, 30) # 显示窗口 win.show() app.exec()

若应用程序中使用了 QWidget 类或其子类,应用程序应使用 QApplication 类管理消息循环。

应用程序运行后,主窗口如图 2-2 所示。窗口上有颜色的区域就是 MyElement 对象。此时单击该区域,会弹出消息“父窗口接收到鼠标事件”,如图 2-3 所示。被单击的是 MyElement 对象,MyWindow 对象却接收到了鼠标事件,这表明事件已成功向上传播。

图 2-2 / 图 2-3
示例窗口
图 2-2 示例窗口
弹出消息框
图 2-3 弹出消息框

2.3.2 sendEvent 与 postEvent

QCoreApplication 类提供了两个静态方法,可以在代码中手动发送事件。

  1. sendEvent 方法:直接将事件发送给目标对象。事件会立即被处理,并等待事件处理完毕此方法才返回。
  2. postEvent 方法:将事件发送给目标对象后立即返回(不会等待),事件被添加到事件队列中。事件的处理需要排队,可能不会马上处理。

典型的使用场景是模拟键盘按键——每个按键需要发送两个事件,模拟键盘按下时发送 KeyPress 事件,模拟键盘松开(释放)时发送 KeyRelease 事件。

下面示例中,窗口上有 5 个按钮,依次模拟以下按键。

  1. “全选”按钮:模拟快捷键【Ctrl+A】。
  2. “删除”按钮:模拟【Delete】按键。
  3. “退格”按钮:模拟【BackSpace】按键。
  4. “<-”按钮:模拟向左的箭头键。
  5. “->”按钮:模拟向左的箭头键。

应用程序窗口为自定义的 MyWindow 类,派生自 QWidget 类。窗口左半部分为 5 个垂直排列的按钮部件(类型为 QPushButton)。窗口右半部分放置的是多行文本输入部件(类型为 QTextEdit)。5 个按钮被单击后将模拟上述 5 个键盘按键,按键将作用于输入部件。

class MyWindow(QWidget): # 构造函数 def __init__(self): super().__init__(None) # 初始化可视化元素 self.initUI() # 窗口标题 self.setWindowTitle("发送键盘事件") def initUI(self): self.btnSelAll = QPushButton("全选") self.btnDelete = QPushButton("删除") self.btnBackspace = QPushButton("退格") self.btnMoveLeft = QPushButton("<-") self.btnMoveRight = QPushButton("->") self.text = QTextEdit() self.grid = QGridLayout(self) self.vbox = QVBoxLayout() self.vbox.addWidget(self.btnSelAll) self.vbox.addWidget(self.btnDelete) self.vbox.addWidget(self.btnBackspace) self.vbox.addWidget(self.btnMoveLeft) self.vbox.addWidget(self.btnMoveRight) self.grid.addLayout(self.vbox, 0, 0) self.grid.addItem(QSpacerItem(15, 0), 0, 1) self.grid.addWidget(self.text, 0, 2) # 连接信号和槽 self.btnSelAll.clicked.connect(self.onSelAll) self.btnDelete.clicked.connect(self.onDel) self.btnBackspace.clicked.connect(self.onBackSpace) self.btnMoveLeft.clicked.connect(self.onMoveLeft) self.btnMoveRight.clicked.connect(self.onMoveRight)
def onSelAll(self): # Ctrl + A key = Qt.Key_A modf = Qt.ControlModifier # 发送键盘按下事件 keyEvt = QKeyEvent(QKeyEvent.KeyPress, key, modf) QApplication.sendEvent(self.text, keyEvt) # 发送键盘释放事件 keyEvt = QKeyEvent(QKeyEvent.KeyRelease, key, modf) QApplication.sendEvent(self.text, keyEvt) def onDel(self): # Delete key = Qt.Key_Delete QApplication.sendEvent(self.text, QKeyEvent(QKeyEvent.KeyPress, key, Qt.NoModifier)) QApplication.sendEvent(self.text, QKeyEvent(QKeyEvent.KeyRelease, key, Qt.NoModifier)) def onBackSpace(self): # BackSpace key = Qt.Key_Backspace modif = Qt.NoModifier QApplication.sendEvent(self.text, QKeyEvent(QKeyEvent.KeyPress, key, modif)) QApplication.sendEvent(self.text, QKeyEvent(QKeyEvent.KeyRelease, key, modif)) def onMoveLeft(self): # Left key = Qt.Key_Left mod = Qt.NoModifier QApplication.sendEvent(self.text, QKeyEvent(QKeyEvent.KeyPress, key, mod)) QApplication.sendEvent(self.text, QKeyEvent(QKeyEvent.KeyRelease, key, mod)) def onMoveRight(self): # Right key = Qt.Key_Right mod = Qt.NoModifier QApplication.sendEvent(self.text, QKeyEvent(QKeyEvent.KeyPress, key, mod)) QApplication.sendEvent(self.text, QKeyEvent(QKeyEvent.KeyRelease, key, mod))

QPushButton 被单击后会发出 clicked 信号,onSelAll、onDel 等方法用于接收信号并做出响应(槽函数)。一条完整的键盘消息应包括键被按下和释放两个动作,因此在调用 sendEvent 方法进行事件模拟时,应依次发送 KeyPress 和 KeyRelease 两个事件。

运行示例程序后,在输入框中随意输入一些字符,然后可以单击窗口左侧的按钮进行测试,如图 2-4 所示。

图 2-4
模拟键盘按键
模拟键盘按键

2.3.3 自定义事件

QEvent.Type 定义了许多内置的事件类型,如 KeyPress、Resize、MouseButtonPress 等。同时,它也为开发人员保留了自定义事件类型的空间。

事件类型本质上是一个整数值,其中 User 的值为 0x3e8(十进制值 1000),MaxUser 的值为 0xffff(十进制值为 65535)。自定义的事件类型的取值必须在 User 与 MaxUser 之间,不能与内置的事件类型重复。

在确定自定义事件类型的值后,需要调用 QEvent 类的静态方法 registerEventType 进行注册。事件注册后,若要触发事件,就可以调用 sendEvent 或 postEvent 方法将事件发送给目标 Qt 对象。

下面示例注册了两个自定义的事件——MyCustomEvent1 和 MyCustomEvent2。窗口上放置了两个按钮(QPushButton 部件),分别用于触发上述自定义事件。具体实现步骤如下。

(1)定义两个变量,设置自定义事件的值。

MyCustomEvent1 = QEvent.Type(QEvent.Type.User + 1) MyCustomEvent2 = QEvent.Type(QEvent.Type.User + 2)

注意,变量在赋值时要明确使用 QEvent.Type 数据类型。表达式 QEvent.Type.User + 1 和 QEvent.Type.User + 2 返回的值都会被解析为 int,因此要将其转换为 QEvent.Type 类型。

(2)定义 AppWindow 类,派生自 QWidget。在构造函数中调用 initUI 方法,初始化应用窗口的可视化对象。

def __init__(self): super().__init__(None) # 初始化用户界面 self.initUI()

(3)下面是 initUI 方法的实现代码。

def initUI(self): # 布局 layout = QVBoxLayout(self) # 标签部件 self.label = QLabel() # 按钮部件 self.btn1 = QPushButton("点这里,触发自定义事件1") self.btn2 = QPushButton("点这里,触发自定义事件2") # 将以上部件添加到布局中 layout.addWidget(self.label) layout.addWidget(self.btn1) layout.addWidget(self.btn2) # 应用布局 self.setLayout(layout) # 为按钮的 clicked 信号绑定槽函数 self.btn1.clicked.connect(self.onClicked1) self.btn2.clicked.connect(self.onClicked2) # 窗口标题 self.setWindowTitle("示例程序") # 窗口大小 self.resize(200, 160)

QLabel 部件用于显示文本信息,两个 QPushButton 部件被单击后会发出 clicked 信号,并由 onClicked1、onClicked2 接收。

(4)以下为 onClicked1 和 onClicked2 的实现代码。

def onClicked1(self): QApplication.postEvent(self, QEvent(MyCustomEvent1)) def onClicked2(self): QApplication.postEvent(self, QEvent(MyCustomEvent2))

上述代码调用 postEvent 方法来发送自定义事件,事件的接收目标是当前窗口对象(AppWindow 实例)。

(5)重写基类的 event 方法,接收并处理自定义事件。

def event(self, e: QEvent) -> bool: if e.type() == MyCustomEvent1: self.label.setText("自定义事件1已触发") return True if e.type() == MyCustomEvent2: self.label.setText("自定义事件2已触发") return True # 其余事件将交给基类处理 return super().event(e)

如果接收到的事件类型是 MyCustomEvent1,就设置标签的文本为“自定义事件1已触发”;如果接收到的是 MyCustomEvent2,就设置标签文本为“自定义事件2已触发”。事件处理完毕后应返回 True,其他未被处理的事件应交给基类去处理(调用基类的 event 方法)。

(6)在初始化应用程序前,注册自定义事件。

QEvent.registerEventType(MyCustomEvent1) QEvent.registerEventType(MyCustomEvent2)

(7)初始化应用程序,呈现 AppWindow 窗口。

app = QApplication() wind = AppWindow() wind.show() app.exec()

(8)运行示例代码,单击窗口上的按钮可触发自定义事件,如图 2-5 所示。

图 2-5
触发自定义事件
单击按钮触发自定义事件

2.3.4 事件过滤器

虽然派生类可通过重写 event 方法来过滤事件(该方法若返回 False,则事件将不再传递给基类),但如果仅为了过滤事件而创建派生类,结果会产生许多不必要的新类型。因此,使用事件过滤器会更合适。

事件过滤器能够实现一个对象监视另一个对象的事件,事件过滤器(监视者)会通过 eventFilter 方法的返回值来决定是否将事件发送给被监视对象。

假设 A 对象需要监视(过滤)发送给 B 对象的事件,那么,监视者是 A 对象,被监视者是 B 对象。B 对象需要调用 installEventFilter 方法注册事件过滤器。调用时必须提供 A 对象的引用。A 对象需要重写 eventFilter 方法,如果方法返回 True,表示事件不会发送给 B 对象,否则返回 False。要解除对 B 对象的事件监视(过滤),请调用 B 对象的 removeEventFilter 方法。

下面示例将从 QWidget 类派生一个自定义窗口类 MyWindow,并且该窗口类重写了 eventFilter 方法,可作为事件过滤器(监视者角色)使用。窗口上放置一个 QLineEdit 部件(单行文本输入框,被监视者角色)。MyWindow 类将监视 QLineEdit 对象的 KeyPress 和 KeyRelease 事件,如果出现空格或“#”字符,就把事件过滤掉。此方案可实现禁止用户输入特定字符的功能。

class MyWindow(QWidget): def __init__(self): super().__init__(None, Qt.Window) # 窗口标题 self.setWindowTitle("事件过滤") # 窗口大小 self.resize(200, 80) # 初始化 UI 部件 self.txtEdit = QLineEdit(self) self.txtEdit.move(15, 17) # 安装事件过滤器 self.txtEdit.installEventFilter(self) def eventFilter(self, watched: QObject, e: QEvent) -> bool: if watched == self.txtEdit: # 检查是否为键盘事件 if e.type() == QEvent.KeyPress or e.type() == QEvent.KeyRelease: keyev = QKeyEvent(e) # 过滤“空格”键和“#”字符 if keyev.key() == Qt.Key_Space or keyev.key() == Qt.Key_NumberSign: # 直接返回 True,将跳过该事件 return True return super().eventFilter(watched, e)

Qt.Key_Space 常量表示空格,Qt.Key_NumberSign 表示数字符号键,即“#”字符。

运行上述示例代码后,在文本框输入内容时,会发现无法输入“#”字符和空格,如图 2-6 所示。

图 2-6
事件过滤器禁止输入井号和空格
无法输入“#”字符和空格

若被监视对象注册了多个过滤器,过滤器的激活顺序与注册顺序相反,即最后注册的过滤器最先被激活。

请思考以下示例:

class EventFilter1(QObject): # 构造函数 def __init__(self, parent=None): super().__init__(parent) # 重写事件过滤方法 def eventFilter(self, watched: QObject, e: QEvent) -> bool: if e.type() == QEvent.MouseButtonPress: print("事件过滤器 - 1") return super().eventFilter(watched, e) class EventFilter2(QObject): # 构造函数 def __init__(self, parent=None): super().__init__(parent) # 重写事件过滤方法 def eventFilter(self, watched: QObject, e: QEvent) -> bool: if e.type() == QEvent.MouseButtonPress: print("事件过滤器 - 2") return super().eventFilter(watched, e) class EventFilter3(QObject): # 构造函数 def __init__(self, parent=None): super().__init__(parent) # 重写事件过滤方法 def eventFilter(self, watched: QObject, e: QEvent) -> bool: if e.type() == QEvent.MouseButtonPress: print("事件过滤器 - 3") return super().eventFilter(watched, e)

上述代码定义了三个类,均重写了 eventFilter 方法,对鼠标按下事件进行过滤。下面代码将上述三个类对象作为按钮部件的事件过滤器。

class MyWindow(QWidget): def __init__(self) -> None: super().__init__(None, Qt.Window) # 窗口标题 self.setWindowTitle("Demo App") # 窗口大小 self.resize(240, 70) # 窗口位置 self.move(548, 438) # 实例化按钮部件 self.btn = QPushButton("请单击这里", self) # 设置按钮的位置 self.btn.move(20, 20) # 实例化三个事件过滤器 self.evtFilter1 = EventFilter1(self) self.evtFilter2 = EventFilter2(self) self.evtFilter3 = EventFilter3(self) # 注册事件过滤器 self.btn.installEventFilter(self.evtFilter1) self.btn.installEventFilter(self.evtFilter2) self.btn.installEventFilter(self.evtFilter3)

事件过滤器的注册顺序为 EventFilter1→EventFilter2→EventFilter3,但它们被激活的顺序则是 EventFilter3→EventFilter2→EventFilter1。

示例程序运行后,单击窗口上的按钮,控制台将输出以下内容:

输出结果
事件过滤器 - 3
事件过滤器 - 2
事件过滤器 - 1
2.4

信号与槽

信号和槽是 Qt 对象之间的通信机制。信号是发送者,槽是接收者。

信号对象用 Signal 类表示。由于该类的成员中存在 __get__ 方法,使其成为只读的描述器(Descriptor)。当通过类型来访问此描述器时,它返回的类型为 Signal(描述器自身的实例);当通过类型实例来访问此描述器时,它将返回 SignalInstance 类型的对象实例。

综上所述,在类中定义信号时使用的是 Signal 类,而操作信号时(如连接槽函数、发送信号等)所使用的实际类型为 SignalInstance。该类定义了以下方法。

  1. connect:将当前信号与接收信号的槽连接。
  2. disconnect:解除信号与槽的连接。
  3. emit:发出信号。

信号的接收者称为槽,它通常是某个函数或某个类中的方法。信号对象必须先调用 connect 方法与槽对象建立连接,当信号发出后,槽对象就会被调用。

下面示例代码将定义 Demo 类,类中包含名为 testSignal 的信号对象。

class Demo(QObject): def __init__(self, parent = None): super().__init__(parent) # 信号 testSignal = Signal()

定义一个名为 slotFunc 的函数,作为 testSignal 信号的槽。当 testSignal 信号发出后该函数会被调用。

def slotFunc(): print('已收到信号')

下面代码实例化 Demo 类,然后将 testSignal 信号与槽 slotFunc 进行连接。

x = Demo() x.testSignal.connect(slotFunc)

当调用信号对象的 emit 方法后将发出信号,已建立连接的槽对象就会被调用。

x.testSignal.emit()

上述代码执行后,若控制台输出“已收到信号”,说明信号与槽能正常通信。

2.4.1 一个信号连接多个槽

当某个信号连接了多个槽对象后,槽对象被调用的顺序将与建立连接的顺序相同。请看下面示例:

from PySide6.QtWidgets import QWidget, QPushButton, QApplication # 以下为槽函数 def func1(): print("响应函数:1") def func2(): print("响应函数:2") def func3(): print("响应函数:3") if __name__ == '__main__': app = QApplication() # 程序窗口 window = QWidget(parent=None) # 窗口标题 window.setWindowTitle("连接多个槽") # 窗口位置及大小 window.setGeometry(766, 550, 230, 90) # 按钮部件 button = QPushButton("请单击这里", window) # 按钮在窗口中的位置 button.move(25, 20) # clicked 信号连接三个槽对象 button.clicked.connect(func1) button.clicked.connect(func2) button.clicked.connect(func3) # 显示窗口 window.show() # 启动消息循环 app.exec()

QPushButton 类表示常见的按钮部件,当用户单击后会发出 clicked 信号。将按钮的 clicked 信号与自定义函数连接,就可以实现响应按钮操作的逻辑代码。上述代码中,clicked 信号依次连接到三个函数上。只要按钮被单击,就会按照此顺序调用三个函数:func1→func2→func3。因此控制台将输出以下内容:

输出结果
响应函数:1
响应函数:2
响应函数:3

如果将 clicked 信号连接三个函数的顺序修改一下:

button.clicked.connect(func3) button.clicked.connect(func1) button.clicked.connect(func2)

那么,按钮被单击后,控制台将输出以下内容:

输出结果
响应函数:3
响应函数:1
响应函数:2

通过本示例可知,槽被调用的顺序与 connect 方法调用的顺序一致。

2.4.2 带参数的信号

信号对象支持传递参数——作为附加数据提供给与信号连接的槽对象。请看下面例子,Test 类中包含 setName 信号。

class Test(QObject): # 信号 setName = Signal(str)

当信号需要参数时,可以向 Signal 类的构造函数传递参数的类型。上述代码为 setName 信号设置了字符串(str)类型的参数。

下面函数将与 setName 信号建立连接。函数必须包含一个参数。

def onSetName(name): print(f"设置了新名称:{name}")

当 setName 调用 emit 方法发送信号时,需要传递一个字符串类型的参数值。该值将由 onSetName 函数的 name 参数接收。下面代码在发出信号时传递字符串常量“Jack”。

x = Test() # 连接信号 x.setName.connect(onSetName) # 发出信号 x.setName.emit("Jack")

以上示例运行后,控制台将输出以下内容:

输出结果
设置了新名称:Jack

如果信号需要定义多个参数,可以为 Signal 类的构造函数提供各参数的类型,例如:

class Test2(QObject): def __init__(self): super().__init__() # 信号 demoSignal = Signal(int, float)

Test2 类中包含 demoSignal 信号,它定义了两个参数:第一个参数是整数类型(int),第二个参数是浮点数类型(float)。因此,demoSignal 信号连接的槽函数必须能接收两个参数。

def demoSlot(a: int, b: float): print(f'第一个参数:{a:d}') print(f'第二个参数:{b:.3f}')

下面代码将实例化 Test2 类,并发出 demoSignal 信号。

v = Test2() # 建立连接 v.demoSignal.connect(demoSlot) # 发出信号 v.demoSignal.emit(300, 100.0830655)

emit 方法的参数个数是可变的。如上述代码,demoSignal 信号需要两个参数,因此在调用 emit 方法时要提供两个参数。运行后控制台的输出内容如下:

输出结果
第一个参数:300
第二个参数:100.083

2.4.3 使用 C++ 成员方法的签名格式

在调用 connect 方法建立信号与槽的连接时,也可以直接使用字符串形式来指定对象的成员名称。其格式与 C++ 方法的签名相同。例如,滑动条部件(QSlider 类)在用户改变滑块的位置后会发出 valueChanged 信号。该信号包含一个整数类型的参数 value,表示当前滑动条的最新值。如果调用 connect 方法时希望使用字符串形式来指定 valueChanged 信号,则它的格式为:

QSlider::valueChanged(int)

不需要指定参数的名称,指明其类型即可。如果 connect 方法是在 QSlider 实例上调用的,则类型可以省略,即

valueChanged(int)

槽对象的字符串形式与信号对象类型,例如:

onChanged(int)

下面以单行文本输入部件(QLineEdit 类)为例进行演示。

from PySide6.QtWidgets import QApplication, QWidget, QLineEdit, QLabel from PySide6.QtCore import SIGNAL, SLOT class AppWindow(QWidget): def __init__(self): super().__init__() # 设置窗口大小 self.resize(280, 90) # 设置窗口标题 self.setWindowTitle("Demo App") # 初始化单行文本输入部件 self.lineTxt = QLineEdit(self) # 建立信号与槽的连接 self.lineTxt.connect(SIGNAL('cursorPositionChanged(int,int)'), self, SLOT('onCursorPsChanged(int,int)')) # 初始化标签部件 self.lb = QLabel(self) # 设置部件在窗口中的位置和大小 self.lineTxt.setGeometry(20,15, 200, 25) self.lb.setGeometry(20, 45, 160, 35) # 接收 cursorPositionChanged 信号的槽 def onCursorPsChanged(self, oldPos, newPos): msg = f'光标已从{oldPos}移动到{newPos}' self.lb.setText(msg)

QLineEdit 类在输入光标的位置发生改变后会发出 cursorPositionChanged 信号。此信号包含两个 int 类型的参数——第一个参数表示光标的旧位置,第二个参数表示光标的新位置。

由于信号对象的 connect 方法并不是直接处理 str(字符串)类型的参数,因此在指定信号名称时要使用 SIGNAL 函数。同理,在指定槽的名称时要使用 SLOT 函数。

下面代码初始化应用程序并显示 AppWindow 窗口。

if __name__ == '__main__': app = QApplication() window = AppWindow() # 显示窗口 window.show() # 启动消息循环 app.exec()

运行示例程序后,在文本输入框中随机输入一些字符,然后通过单击或按键盘上的左右方向键来移动输入光标。文本框下面的标签上会实时显示光标的位置,如图 2-7 所示。

图 2-7
实时显示输入光标的位置
实时显示输入光标的位置

2.4.4 信号的类型重载

在声明信号对象时,如果传递的类型参数为元组(Tuple)类型,表示该信号允许类型重载——可根据参数类型来选择版本,例如:

x = Signal((int,), (bytes,))

信号 x 具有两个类型重载版本:

若 x 信号的声明如下:

x = Signal((int,int,int,), (str,int,))

那么信号 x 将具有以下重载版本:

接下来请看一个示例。

from PySide6.QtCore import QObject, Signal class Demo(QObject): # 信号 testSignal = Signal((str, str,), (float,)) # 槽 def testSlot(self, arg1, arg2 = None): if isinstance(arg1, str) and isinstance(arg2, str): print(f'字符串参数:{arg1}、{arg2}') if isinstance(arg1, float): print(f'浮点数参数:{arg1}') def __init__(self): super().__init__() # 建立信号和槽之间的连接 self.testSignal[str,str].connect(self.testSlot) self.testSignal[float].connect(self.testSlot) # 发送信号 def sendFloat(self): self.testSignal[float].emit(0.55) def sendStr(self): self.testSignal[str,str].emit("hello", "world")

Demo 类中包含 testSignal 信号,它具有以下类型重载:

testSlot 方法将作为 testSignal 信号连接的槽对象,并且是两个重载版本共用的槽对象。因此 testSlot 方法包含两个参数,其中 arg2 为可选参数。如果使用的信号版本为单个 float 类型参数,那么仅使用 arg1 参数即可,arg2 参数保留默认值 None。如果使用的信号版本是两个 str 类型的参数,那么 arg1、arg2 分别存放两个参数的值。

为了分辨信号的重载版本,testSlot 方法中使用了 isinstance 函数来检查 arg1 和 arg2 的实际类型。若两个参数均为 str 类型,那么连接 testSlot 的信号是 testSignal[str,str];若 arg1 的类型为 float,那么连接的信号就是 testSignal[float]。

示例运行后控制台将输出以下内容:

输出结果
字符串参数:hello、world
浮点数参数:0.55

2.4.5 让信号与槽自动建立连接

QMetaObject 类有一个名为 connectSlotsByName 的静态方法。调用方法时需要提供一个 QObject 类型或其子类的对象。connectSlotsByName 方法会遍历指定对象的子级对象集合,为每个子对象的信号成员查找匹配的槽,然后自动建立连接。

要使用上述功能,必须为对象分配一个名称(调用 setObjectName 方法)。虽然多个对象允许设置相同的名称,但是只有匹配的第一个对象才会自动建立信号与槽的连接。因此,建议不要在同一个对象树层次中分配重复的对象名称。

槽对象必须符合以下命名规则才会被自动连接:

on_<对象名称>_<信号>(<参数列表>)

假设某 Qt 对象分配了名称 txtAge,并希望它的 textUpdated 信号能自动连接槽对象,那么用作槽的成员应当命名为 on_txtAge_textUpdated(…)。

下面示例将在窗口中放置一个按钮部件(QPushButton 类),它的 clicked 信号能够自动连接槽对象。

class AppWindow(widgets.QWidget): def __init__(self): super().__init__() # 窗口标题 self.setWindowTitle('Demo') # 窗口大小 self.resize(220, 65) # 初始化按钮部件 self.btn = widgets.QPushButton("Click Me", self) # 设置按钮的位置 self.btn.move(35,20) # 设置对象的名称 self.btn.setObjectName("myButton") # 信号与槽的自动绑定 core.QMetaObject.connectSlotsByName(self) ......

在初始化 AppWindow 类时,一定要调用 QMetaObject.connectSlotsByName,这样当前对象的所有子对象都能够自动完成信号与槽的连接。本示例中按钮部件被命名为 myButton,所以 AppWindow 类中要用作槽的方法必须命名为 on_myButton_clicked,而且要加上 Slot 装饰器。

@core.Slot() def on_myButton_clicked(self): # 弹出消息框 widgets.QMessageBox.information(self, "提示", "你单击了按钮", widgets.QMessageBox.Ok)

上述代码实现当按钮被单击后弹出一个消息框。运行结果如图 2-8 和图 2-9 所示。

图 2-8 / 图 2-9
单击按钮前和单击按钮后弹出消息框
图 2-8 单击按钮前
图 2-9 单击按钮后弹出消息框

2.4.6 示例:随机变换窗口的背景颜色

本示例将实现单击按钮后随机更换窗口背景颜色的功能。

首先,使用列表类型定义一组 QColor 对象,每次更换窗口背景颜色时,将从此列表中随机取出一个元素来填充窗口。

colorList = [ QColor(15, 165, 80), QColor(200, 0, 12), QColor(5, 100, 60), QColor(13, 13, 80), QColor(20, 189, 255), QColor(10, 10, 0), QColor(120, 50, 95), QColor(58, 0, 0), QColor(100, 105, 16), QColor(0, 0, 200), QColor(199, 20, 150), QColor(40, 50, 88), QColor(118, 10, 0), QColor(55, 160, 28), QColor(0, 240, 17), QColor(91, 145, 28), QColor(33, 36, 125), QColor(88, 135, 201), QColor(232, 18, 52), QColor(42, 218, 50), QColor(89, 10, 180) ]

在实例化 QColor 对象时,可以使用 R、G、B 三个整数值来确定颜色。随机选取颜色可以调用 choice 函数。此函数的功能是从一个非空集合中随机取出一个元素并返回。

接着,从 QWidget 类派生出自定义类型 MyWindow,它表示应用程序窗口。

class MyWindow(QWidget): # 设置颜色的信号 setColor = Signal(QColor) # 构造函数 def __init__(self): super().__init__() # 窗口标题 self.setWindowTitle("随机更换背景色") # 窗口大小 self.resize(300, 200) # 初始化按钮部件 self.btn = QPushButton("换颜色", self) # 设置按钮位置 self.btn.move(15, 15) # 连接 clicked 信号 self.btn.clicked.connect(self.onClicked) # 连接 setColor 信号 self.setColor.connect(self.onColorPicked) # 响应按钮部件的 clicked 信号 def onClicked(self): # 随机选一个颜色 selColor = choice(colorList) # 发送 setColor 信号 self.setColor.emit(selColor) # 响应 setColor 信号 def onColorPicked(self, color): # 修改当前窗口背景色 p = QPalette() p.setColor(QPalette.Window, color) self.setPalette(p)

窗口中包含一个按钮部件(QPushButton 对象),被单击时会发出 clicked 信号,对应的槽是 onClicked 方法。在此方法中调用 choice 函数随机获取一个 QColor 对象,然后发出 setColor 信号。setColor 信号带有一个 QColor 类型的参数,将用于设置窗口背景色。

setColor 信号对应的槽是 onColorPicked 方法。在该方法中通过修改调色板对象(QPalette 类)的参数来设置窗口背景颜色。这里有两种修改调色板的方法。

(1)声明一个变量并获取当前窗口的调色板对象,修改后调用 setPalette 方法为窗口重新设置调色板。

p = self.palette() p.setColor(QPalette.Window, color) self.setPalette(p)

(2)创建新的调色板实例,修改参数后,调用 setPalette 方法设置新的调色板。应用程序会自动合并新旧调色板的参数。

p = QPalette() p.setColor(QPalette.Window, color) self.setPalette(p)

最后,实例化 MyWindow 窗口,显示窗口并启动主消息循环。

if __name__ == "__main__": app = QApplication() window = MyWindow() # 实例化窗口 window.show() # 显示窗口 app.exec() # 开始消息循环

运行本示例,呈现窗口的初始颜色,如图 2-10 所示。

单击“换颜色”按钮后,窗口会随机切换背景色,如图 2-11 所示。

图 2-10 / 图 2-11
窗口初始颜色与随机更换背景色
图 2-10 窗口的初始颜色
图 2-11 随机更换背景色

2.4.7 信号阻绝器

Signal Blocker(本书将其翻译为“信号阻绝器”)是一个用于暂时阻止对象发出信号的组件,对应类型是 QSignalBlocker 类。

QSignalBlocker 在初始化后会自动阻止目标对象的信号。当 QSignalBlocker 对象发生析构时,会自动恢复目标对象的信号状态。QSignalBlocker 类定义了 __enter__ 和 __exit__ 方法,因此在实例化时可以放到 with 代码块中。

下面示例通过定时器组件(QTimer 类)让标签部件(QLabel)每隔 1 秒显示/隐藏一次——实现标签文字闪烁的效果。当单击“暂停 10 秒”按钮后,标签文字会停止闪烁 10 秒。其原理是使用 QSignalBlocker 类阻止 QTimer 对象的 timeout 信号,等待 10 秒后恢复。

import PySide6.QtCore as core import PySide6.QtWidgets as widgets import PySide6.QtGui as gui class AppWindow(widgets.QWidget): def __init__(self): super().__init__() # 设置窗口标题 self.setWindowTitle('My App') # 设置窗口大小 self.resize(265, 98) # 初始化按钮部件 self.btn = widgets.QPushButton('暂停 10 秒', self) # 设置按钮在窗口中的位置 self.btn.move(20,12) # 建立 clicked 信号与槽的连接 self.btn.clicked.connect(self.onBtnClicked) # 初始化标签部件 self.lb = widgets.QLabel(self) # 设置标签文本 self.lb.setText('示例文本') # 设置字体 font = gui.QFont('仿宋', 15, gui.QFont.Bold) self.lb.setFont(font) # 设置标签在窗口上的位置 self.lb.move(20,50) # 初始化定时器 self.timer = core.QTimer(self) # 建立 timeout 信号与槽的连接 self.timer.timeout.connect(self.onTimeout) # 启动定时器,每隔 1 秒触发一次 self.timer.start(1000) # 槽:接收 timeout 信号 def onTimeout(self): # 显示或隐藏标签部件 if self.lb.isVisible(): self.lb.setVisible(False) else: self.lb.setVisible(True) # 槽:接收 clicked 信号 def onBtnClicked(self): # 阻止定时器上的信号,10 秒后恢复 with core.QSignalBlocker(self.timer): # 构建新的事件循环 evloop = core.QEventLoop(self) # 启动一次性定时器 # 10 秒后执行 quit 方法,退出事件循环 core.QTimer.singleShot(10*1000, evloop.quit) # 事件循环开始,exec 方法一直处于等待状态 # 直到 quit 方法被调用才会返回 # 作用是实现延时 10 秒 evloop.exec() #---------------------------------------------------- if __name__ == '__main__': app = widgets.QApplication() # 实例化窗口 window = AppWindow() # 显示窗口 window.show() # 开始消息循环 app.exec()

QTimer 对象在调用 start 方法后开始循环计时,间隔为 1000 毫秒,即 1 秒。每次计时周期结束时会发出一个 timeout 信号。实例化 QSignalBlocker 对象时通过构造函数传递 QTimer 对象,timeout 信号会被阻止。

在 with 语句内,先创建一个新的消息循环(QEventLoop),接着调用 QTimer 类的静态方法 singleShot 产生单次定时器,延时 10000 毫秒(10 秒)后触发。触发时调用 quit 方法退出事件循环。随后退出 with 语句,QSignalBlocker 对象恢复 timeout 信号。

运行效果如图 2-12 所示。

图 2-12
让文本暂停闪烁
让文本暂停闪烁
2.5

字节序列——QByteArray

QbyteArray 类既可用于处理字节序列,也可以对字符串做简单处理。

QByteArray 类的构造函数有多个重载,最简单的方法是调用无参数的构造函数,创建一个空的字节序列。请看下面例子:

from PySide6.QtCore import QByteArray x = QByteArray() # 此时序列中没有数据,大小为0 print(f'初始大小:{x.size()}') # 追加3字节数据 x.append(b'\x25\x6c\x82') # 再追加3字节 x.append(b'\x45\xa6\xd7') # 再次打印字节序列的大小 print(f'追加数据后的大小:{x.size()}') # 打印序列中所有字节的十六进制表示方式 print(f"数据内容:{x.toHex()}")

append 方法可以向序列追加数据。由于此方法调用后会返回 QByteArray 实例自身,因此可以“链条式”调用 append 方法,例如:

x.append(b'\x20\x5f').append(b'\x60').append(b'\xb9\x42')

x 变量初始化完成后,由于里面没有数据,所以 size 方法返回 0。当调用 append 方法追加 6 字节后,size 方法将返回 6。最后在打印数据内容时,调用了 toHex 方法,将 x 中的所有数据转换为十六进制表示的字符串(1 字节由两个字符组成),即“256c8245a6d7”。

也可以调用以下构造函数,在初始化 QByteArray 对象时设置默认大小,以及填充字节。

def __init__(self, size: int, c: int)

size 参数表示新创建的 QByteArray 对象的默认大小,c 参数表示用于填充默认分配空间的字节。下面是一个例子:

k = QByteArray(3, 16) # 此时初始大小为3 print(f'初始大小:{k.size()}') # 追加5字节 k.append(b'\x12\x30\x3a\x67\xe4') # 原有的3字节加上新的字节,此时大小为8 print(f'追加数据后的大小:{k.size()}')

创建的新序列大小为 3,并用 16 来填充默认空间,即前三个元素均为 16。随后,调用 append 方法追加 5 字节。序列的最终大小为 8。

在 C++ 语言中,QByteArray 类可以更方便地操作字符串(char 数组,char[] 或 char*)。对于 Python 语言,可以选择使用 Python 内置的如 str、bytes 等类型,而不是 QByteArray 类。

2.5.1 替换字符串

要在 QByteArray 中进行字符串替换,需要调用 replace 方法,比较常用的有以下重载:

replace(before: Union[QByteArray, bytes], after: Union[QByteArray, bytes]) -> QByteArray

before 参数指定要被替换的字符串,after 参数指定新的字符串。参数既可以使用 QByteArray 对象,也可以使用 Python 内置的 bytes 类型。方法调用后会返回当前 QByteArray 实例。

下面示例将通过 replace 方法将单词 fine 变成 fish。

from PySide6.QtCore import QByteArray # 创建 QByteArray 对象 x = QByteArray('fine') print(f'原单词:{x.toStdString()}') # 被替换的内容 a = QByteArray('ne') # 新内容 b = QByteArray('sh') # 执行替换 x.replace(a, b) print(f'替换后:{x.toStdString()}')

对于汉字字符串,上述方法也适用(默认使用 UTF-8 编码)。下面示例将“此心安处是天堂”改为“此心安处是吾乡”。

# 创建新的 QByteArray 实例 ostr = QByteArray('此心安处是天堂') print(f"替换前:{ostr.toStdString()}") a = QByteArray('天堂') b = QByteArray('吾乡') # 替换 ostr.replace(a, b) print(f"替换后:{ostr.toStdString()}")

结果如下:

输出结果
替换前:此心安处是天堂
替换后:此心安处是吾乡

如果使用的字符编码不是默认的 UTF-8,则需要保证编码/解码时使用一致的编码格式。例如,下面示例将“一路向南”改为“一路向北”(使用 GB2312 编码)。

# 原字符串,要手动将字符串编码改为 GB2312 d = QByteArray('一路向南'.encode('gb2312')) print(f'替换前:{d.data().decode("gb2312")}') # 编码格式要一致 a = QByteArray('南'.encode('gb2312')) b = QByteArray('北'.encode('gb2312')) # 替换 d.replace(a, b) print(f'替换后:{d.data().decode("gb2312")}')

2.5.2 数值到字符串的转换

调用 QByteArray 类的 number 静态方法,可以将整数或浮点数值转换为字符串。对于整数值,可以通过 base 参数来设定进制(默认为 10,即十进制)。

下面示例将整数值 28 分别转换为二进制、八进制、十进制、十六进制的字符串。

# 整数值 num = 28 # 二进制 x1 = QByteArray.number(num, base=2) print(f'二进制:{x1.toStdString()}') # 八进制 x2 = QByteArray.number(num, base=8) print(f'八进制: {x2.toStdString()}') # 十进制 x3 = QByteArray.number(num, base=10) print(f'十进制: {x3.toStdString()}') # 十六进制 x4 = QByteArray.number(num, base=16) print(f'十六进制:{x4.toStdString()}')

上述代码的执行结果如下:

输出结果
二进制:11100
八进制: 34
十进制: 28
十六进制:1c

对于浮点数值,number 方法可通过 format 参数设置浮点数的格式控制符,默认为 'g'。precision 参数表示小数位精度。方法签名如下:

number(arg__1, format='g', precision=6)

下面例子将保留三位小数。

number = 439.682145 b = QByteArray.number(number, 'f', 3)

'f' 表示浮点数,转换为字符串的结果为“439.682”。将格式控制符设置为 'E',可以显示为科学记数法。

number = 439.682145 b = QByteArray.number(number, 'E', 3) # 结果:4.397E+02

2.5.3 字符串到数值的转换

QByteArray 类提供了一组方法,可以将表示数值的字符串转换为对应的数值。

  1. toInt:将当前字符串转换为对应的有符号整数值。
  2. toUInt:将当前 QByteArray 中的字符串转换为对应的无符号整数值。
  3. toFloat:转换为浮点数值。
  4. toDouble:转换为双精度数值。

这些方法都有一个 base 参数,指的是原字符串所表示数值的进制,2 表示二进制,8 表示八进制等。方法调用后会返回一个包含两个元素的元组对象。其中,第一个元素表示转换结果(数值),第二个元素是布尔值(bool),若为 True 表示转换成功,否则表示失败。

下面是两个示例。

# 二进制 b = QByteArray('1011') val, res = b.toInt(2) if res: print(val) # 输出:11 # 十二进制 b = QByteArray('433') val, res = b.toInt(12) if res: print(val) # 输出:615

第一个例子中,调用 toInt 方法时指明“1011”为二进制数值,转换后的整数值为 11(print 函数默认打印的是十进制)。第二个例子中,“433”为十二进制数值,转换后的整数值为 615,计算过程为:

计算过程
615=4×12²+3×12¹+3×12⁰

下面的两个例子将转换并输出浮点数值和双精度数值。

# 浮点数值 b = QByteArray('123.456') val, res = b.toFloat() if res: print(val) # 输出:123.45600128173828 # 双精度数值 b = QByteArray('92998.520453115') val, res = b.toDouble() if res: print(val) # 输出:92998.520453115

在调用 toFloat 方法后得到的浮点数由于二进制的存储误差,123.456 后面的小数位并没有全部置 0。

2.5.4 重复字符串

repeated 方法可以复制原 QByteArray 对象中的内容,并重复指定的次数。请看下面的例子:

x = QByteArray('一二三') x = x.repeated(3) # 输出:一二三一二三一二三 print(x.toStdString())

上述代码中,原字符为“一二三”,重复 3 次后变为“一二三一二三一二三”。此处要注意,repeated 方法不直接修改原有的内容,而是复制原内容后再做重复处理。新的字符串内容将从 repeated 方法返回。因此,x 变量要用 repeated 方法返回的 QByteArray 实例再次赋值。

若调用 repeated 方法时向参数传递了 0,即重复 0 次,那么方法将返回包含空字符串的 QByteArray 对象。

2.5.5 数据截取

要从 QByteArray 对象所包含的数据中提取部分字节序列,可以使用以下几种方法。

(1)chop:直接从原数据中移除字节。移除的量将从数据的末尾开始计算。例如下面例子,原数据中包含 7 字节,chop(4) 将移除最后 4 字节。

x = QByteArray(b'\x12\x5e\x40\x65\x18\xa1\x7c') # 移除后面4字节 x.chop(4) # 打印结果 msg = ', '.join(f'0x{i:x}' for i in x.data()) # 输出:0x12, 0x5e, 0x40 print(msg)

(2)chopped:此方法不会直接处理原数据,而是复制原数据并从末尾移除指定数量的字节。下面的例子先创建包含 8 字节的 QByteArray 实例,然后调用 chopped 方法移除最后 3 字节并且返回新的 QByteArray 实例。

# 8字节 arr = QByteArray(b'\xd2\x19\xc3\x55\x28\xe4\x75\x3f') # 移除后3字节,并返回新的 QByteArray 实例 arrc = arr.chopped(3) # 输出:0xd2, 0x19, 0xc3, 0x55, 0x28 print(', '.join(f'0x{i:x}' for i in arrc.data()))

(3)first:从原数据的开始处提取指定数量的字节,并返回新的 QByteArray 实例。下面的代码将提取数据的前 3 字节。

d = QByteArray(b'\x84\xed\xa6\x9f\x24\x73\xb6') # 提取前3字节 d2 = d.first(3) # 输出:0x84, 0xed, 0xa6 print(', '.join(f'0x{n:x}' for n in d2.data()))

(4)last:从数据的末尾提取指定的字节数,并返回新的 QByteArray 实例。下面代码演示了从原数据的末尾提取 2 字节。

# 6字节 x = QByteArray(b'\x66\x32\x8b\x96\x51\x13') # 提取最后2字节 y = x.last(2) # 输出:0x51, 0x13 print(', '.join(f'0x{n:x}' for n in y.data()))

2.5.6 切片

切片也是截取数据的另一种方法,sliced 方法可以从 QByteArray 对象原有数据的任意位置,提取出任意长度的字节,组成新的 QByteArray 对象返回。

sliced 方法有两个重载。

  1. sliced(pos):从 pos 参数所指定的位置开始提取,直到数据末尾。pos 将从 0 开始计算。例如,若要从第一字节开始提取,pos 参数为 0;若要从第三字节处开始提取,pos 参数的值为 2。
  2. sliced(pos,n):pos 参数指定要提取数据的起点位置,基于 0 计算。即第二字节为 1,第五字节为 4,等等。n 参数指定要提取的字节数。

下面示例将从原数据中提取出两个切片。

da = QByteArray('workstation') # 切片 1:从第三字节起,连续提取4字节 sl1 = da.sliced(2, 4) # 输出:rkst print(sl1.toStdString()) # 切片 2:从第八字节起提取,直到末尾 sl2 = da.sliced(7) # 输出:tion print(sl2.toStdString())

第一个切片从第三字节起(“r”),提取 4 字节,得到“rkst”;第二个切片则从第八字节处开始,直到数据的末尾,即从第二个“t”开始到最后一个字符“n”,得到“tion”。

2.5.7 Base64 字符串

QByteArray 类提供了 toBase64 实例方法,可将当前字符串进行 Base64 编码。编码后返回新的 QByteArray 实例。解码时可以调用 fromBase64 静态方法。

下面的示例是典型用法。

# 原字符串 arr = QByteArray('watermelon') # 获得 Base64 字符串 bs = arr.toBase64() # 输出:d2F0ZXJtZWxvbg== print(bs.toStdString()) # 从 Base64 字符串中还原 arr2 = QByteArray.fromBase64(bs) # 输出:watermelon print(arr2.toStdString())

字符串“watermelon”编码后得到 Base64 字符串“d2F0ZXJtZWxvbg==”,末尾的两个“=”起填充作用。如果希望忽略末尾的填充符号,可以在调用 toBase64 方法时指定 OmitTrailingEquals 选项。

bs = arr.toBase64(QByteArray.OmitTrailingEquals)

这样便可获得不带“=”的 Base64 字符串。

编码结果
d2F0ZXJtZWxvbg

如果编码后的 Base64 字符串用于 URL,需要对一些字符做特殊处理。例如,将字符串“鸿雁不堪愁里听,云山况是客中过”进行常规的 Base64 编码,得到以下结果:

常规 Base64 编码
6bi/6ZuB5LiN5aCg5oSB6YeM5ZCs77yM5LqR5bGx5Ya15piv5a6i5Lit6L+H

其中的“/”“+”字符若用在 URL 上会被编码为“%2F”“%2B”。为了避免错误(“%”在 SQL 语句中表示通配符,不能直接存入数据库),在调用 toBase64 方法时可以指定 Base64UrlEncoding 选项。解码时同理。

srcData = QByteArray('鸿雁不堪愁里听,云山况是客中过') # 编码 enc = srcData.toBase64(QByteArray.Base64UrlEncoding) # 输出:6bi_6ZuB5LiN5aCq5oSB6YeM5ZCs77yM5LqR5bGx5Ya15piv5a6i5Lit6L-H print(enc.toStdString()) # 解码 decData = QByteArray.fromBase64(enc, QByteArray.Base64UrlEncoding) # 输出:鸿雁不堪愁里听,云山况是客中过 print(decData.toStdString())

这时候,“/”字符被替换为“_”,“+”被替换为“-”。编码后的 Base64 字符可直接用在 URL 上,例如:

URL 示例
https://somesite.com/query?key=
6bi_6ZuB5LiN5aCq5oSB6YeM5ZCs77yM5LqR5bGx5Ya15piv5a6i5Lit6L-H

2.5.8 拆分字符串

使用 split 方法将字符串拆分成若干子串,在调用方法时需要指定一个分隔符。拆分后,分隔符被移除,并返回一个 QByteArray 对象列表。

下面的例子将字符串“D301%542168%K7”以“%”为分隔符进行拆分。

v = QByteArray("D301%542168%K7") # 以"%"为分隔符,拆分字符串 nList = v.split('%') # 打印拆分后各部分的内容 print(*[val.toStdString() for val in nList], sep=', ')

拆分后会得到以下三个子字符串:

拆分结果
D301, 542168, K7

2.5.9 频数统计

count 方法用于统计指定的内容在当前 QByteArray 对象中出现的次数(频数)。请看下面的例子,将统计“apple”在原字符串中出现的次数。

arr = QByteArray('apple and pineapple') # 要统计的子串 key = QByteArray('apple') # 统计子串出现的次数 n = arr.count(key) # 输出: apple 出现了 2 次 print(f'apple 出现了{n}次')
2.6

QBuffer

QBuffer 类封装了一组 API,以类似于文件的方式访问 QByteArray 对象。这使得 QByteArray 的读写操作变得更简单。

在初始化 QBuffer 实例时,其内部会自动创建 QByteArray 实例,用于存储字节序列。

mybuff = QBuffer()

由于 QBuffer 也是 QObject 的子类,因此在调用其构造函数时可以传递父对象的引用。

rootObj = ...... mybuff = QBuffer(parent=rootObj)

若希望使用现有的 QByteArray 对象,也可以在调用 QBuffer 构造函数时传递现有 QByteArray 对象。

arr = QByteArray('abc') buffer = QBuffer(arr)

也可以在实例化 QBuffer 对象后,调用 setBuffer 方法设置关联的 QByteArray 对象。

arr = QByteArray('abc') buffer = QBuffer() buffer.setBuffer(arr)

2.6.1 基本的读写操作

QBuffer 对象可以调用 write 方法或 putChar 方法写入数据。write 方法可以一次性写入多字节,而 putChar 只写入 1 字节(或 ASCII 字符)。

下面的例子先实例化一个 QBuffer 对象,然后调用 write 方法写入一组字节序列(由字符串“abcd”组成),接着写入单个字符“M”。

from PySide6.QtCore import QBuffer buff = QBuffer() # 打开 buffer buff.open(QBuffer.WriteOnly) # 第一次写入 buff.write(b'abcd') # 第二次写入 buff.putChar('M') # 输出:b'abcdM' print(buff.buffer()) # 使用后可以关闭 buffer buff.close()

QBuffer 对象在读写之前需要调用 open 方法将其打开。调用时需要指定一个打开模式——在 QIODeviceBase 类中定义的 OpenMode 枚举。由于 QBuffer 类间接派生自 QIODeviceBase 类,因此可以通过 QBuffer 类来访问 OpenMode 类型的值。上述代码中使用了 WriteOnly,表示本次仅对 QBuffer 对象进行写入操作。OpenMode 枚举常用的值如下。

  1. ReadOnly:只读模式。
  2. WriteOnly:写入模式。注意此模式会清除原有的数据。
  3. Append:追加模式。从原有数据的末尾开始写入,不会清除旧的数据。
  4. Truncate:此模式会清除原有的数据。
  5. Text:文本模式。在读取数据时,会自动将文本行末尾的换行符统一转换为“\n”;在写入数据时,会将换行符转换为平台所需要的字符,如在 Windows 下会转换为“\r\n”。
  6. ExistingOnly:此模式主要用于文件操作,要求目标文件必须存在,否则会出错。
  7. NewOnly:此模式也是用于文件操作。创建新的文件,若文件已存在,就会出错。

要从 QBuffer 对象中读出已写入的数据,可以使用 read、readLine、readAll 等方法。readLine 方法每次调用时都读取一行文本;而 readAll 方法是一次性读取所有数据;read 方法只读取其中一部分(或全部)数据。read 方法可以通过 maxlen 参数指定允许读取的最大字节数,调用后返回实际读取的字节数。

下面的例子将从 QBuffer 对象中读出前 5 字节。

buffer = QBuffer() if buffer.open(QBuffer.WriteOnly): # 写入10字节 buffer.write(b'ktx5dW3Pyb') # 关闭 buffer buffer.close() # 以只读方式重新打开 buffer if buffer.open(QBuffer.ReadOnly): # 读出5字节 data = buffer.read(5) # 输出:b'ktx5d' print(data) # 关闭 buffer buffer.close()

2.6.2 使用已有的 QByteArray 对象

在调用 QBuffer 类的构造函数时,可以通过参数传递来使用现有的 QByteArray 对象。在 QBuffer 对象中进行写入操作后,被关联的 QByteArray 对象中的数据也会同时改变。QBuffer 对象仅引用 QByteArray 对象,并不控制其生命周期。因此,为了保证读写操作能顺利进行,在 QBuffer 对象关闭(或调用 setBuffer 切换到其他 QByteArray 对象)前,代码调用者必须确保关联的 QByteArray 对象是可用的。

下面的例子先创建一个 QByteArray 实例,并将数据初始化为 "abc"。随后以此 QByteArray 对象为基础创建 QBuffer 实例,再通过 QBuffer 对象在 "abc" 的后面写入 "xyz"。

from PySide6.QtCore import QByteArray, QBuffer # 初始数据为"abc" arr = QByteArray('abc') # 创建 QBuffer 实例 buff = QBuffer(arr) # 打开 QBuffer 对象 buff.open(QBuffer.Append) # 写入"xyz" buff.write(b'xyz') # 关闭 QBuffer 对象 # 检查一下 QByteArray 对象是否已改变 # 输出:b'abcxyz' print(arr)

QByteArray 对象已存在字符串 abc,通过 QBuffer 对象写入 xyz 后,最终的数据变为 abcxyz。

2.6.3 设置读写位置

使用 seek 方法可以设定当前的读(或写)位置(偏移量)。偏移量的计算以 0 为基础,即 seek(0) 表示 QBuffer 对象将从数据的开头读取(或写入)内容。若要从第三字节处写入数据,可以调用 seek(2)。

下面的示例先创建 QByteArray 实例,初始数据为字符串 abcdeopq。随后创建 QBuffer 实例,从第六字节处(偏移量为 5)写入 4 字节,最终字符串变为 abcdefghi。读出数据时,将读取的偏移量设置为 3(第四字节),连续读入 3 字节,得到字符串 def。

from PySide6.QtCore import QByteArray, QBuffer arr = QByteArray('abcdeopq') # 输出:abcdeopq print(f'原字符串:{arr.toStdString()}') # 创建 QBuffer 实例 buffer = QBuffer(arr) # 打开 QBuffer 实例 buffer.open(QBuffer.WriteOnly) # 定位读写位置 buffer.seek(5) # 写入4字节 buffer.write(b'fghi') # 关闭 QBuffer 对象 buffer.close() # 输出:abcdefghi print(f'写入后:{arr.toStdString()}') # 读取数据 bufRead = QBuffer(arr) bufRead.open(QBuffer.ReadOnly) # 设置读写位置 bufRead.seek(3) # 读入3字节 arr2 = bufRead.read(3) # 输出:def print(f'读到的部分内容:{arr2.toStdString()}')
2.7

位序列——QBitArray

QbitArray 类与 QByteArray 相似,但 QBitArray 是面向二进制位的。也就是说,QBitArray 对象中的元素只有两个值:True(1)或者 False(0)。在初始化 QBitArray 实例时,可以通过构造函数指定数组的大小,例如:

bits = QBitArray(3)

上述代码将创建新的 QBitArray 实例,并指定其中包含 3 个元素,所有元素默认为 False。若希望初始化时将所有元素填充为 True,可以通过构造函数的第二个参数来设置。

bits = QBitArray(4, True)

上述代码初始化 QBitArray 实例,设置数组大小为 4,所有元素的默认值皆为 True。

要打印 QBitArray 中各元素的值,可以使用以下代码:

print(*[bits.at(i) for i in range(0, bits.size())], sep=', ')

at 方法可以获取指定索引处元素的值。索引是基于 0 的,即第一个元素的索引为 0,最后一个元素的索引为 n-1。上面代码执行后会输出以下信息:

输出结果
True, True, True, True

2.7.1 设置和清除二进制位

设置指定元素的值应调用 setBit 方法,它有两个重载。

  1. setBit(i):将索引为 i 的元素的值设置为 True。
  2. setBit(i, val):将索引为 i 的元素的值设置为 val。val 参数为布尔类型。

下面代码先创建一个包含 4 个元素的 QBitArray 实例,所有元素均初始化为 False。然后调用 setBit 方法将第三个元素的值设置为 True。

bt = QBitArray(4, False) # 设置第三个元素的值为 True bt.setBit(2) # 输出: False, False, True, False print(*[bt.at(i) for i in range(0, bt.size())], sep=', ')

下面的例子初始化包含 8 个元素的 QBitArray 对象,所有元素的默认值都设置为 True,然后分别设置第 2、5、6 个元素的值为 False。

barr = QBitArray(8, True) # 将第 2、5、6 个元素的值设置为 False barr.setBit(1, False) barr.setBit(4, False) barr.setBit(5, False) # 输出: True, False, True, True, False, False, True, True print(*[barr.at(x) for x in range(0, barr.size())], sep=',')

若要清除某个元素的值(设置为 False),除了可以使用 setBit(x, False),也可以通过 clearBit 方法来实现,例如:

bitarr = QBitArray(10, True) # 清除第 1、3、5、7、9 个元素的值 bitarr.clearBit(0) bitarr.clearBit(2) bitarr.clearBit(4) bitarr.clearBit(6) bitarr.clearBit(8) # 输出: False, True, False, True, False, True, False, True, False, True elements = [bitarr.at(n) for n in range(0, bitarr.size())] print(*elements, sep=', ')

调用 clearBit 方法只要指定元素的索引即可,QBitArray 对象会自动将元素的值设置为 False。

需要注意的是,如果调用的是 clear 方法,它会清除 QBitArray 对象中的所有元素,而不是修改元素的值。调用 clear 方法后,QBitArray 对象的 size 方法将返回 0,isEmpty 方法将返回 True。

2.7.2 频数统计

此功能与 QByteArray 类的 count 方法相同,用于统计某个布尔值在 QBitArray 对象中出现的次数,例如:

bits = QBitArray(10) # 设置第一、四、五、九个元素的值为 True bits.setBit(0, True) bits.setBit(3, True) bits.setBit(4, True) bits.setBit(8, True) # 统计 False 出现的次数 n = bits.count(False) # 输出: False 出现了 6 次 print(f"{False}出现了{n}次")

count 方法还有一个无参数的重载,其功能与 size 方法相同,用于获取当前 QBitArray 实例中的元素个数。

2.8

QSysInfo

QSysInfo 类用于获取系统的基础信息,如 CPU 架构、操作系统内核、系统版本等。下面代码将在控制台打印系统和 CPU 的基本信息。

from PySide6.QtCore import QSysInfo # 系统名称 sysname = QSysInfo.productType() # 系统版本 sysver = QSysInfo.productVersion() # 内核名称 kername = QSysInfo.kernelType() # 内核版本 kerver = QSysInfo.kernelVersion() # CPU 架构 cpuarch = QSysInfo.currentCpuArchitecture() # 打印系统信息 print(f'系统:{sysname} {sysver}') print(f'内核:{kername}({kerver})') print(f'CPU架构:{cpuarch}')

其运行结果如下:

输出结果
系统:windows 11
内核:winnt(10.0.22621)
CPU架构:x86_64
2.9

Qt 的动态属性

此处所说的动态属性并非指 Python 自身的属性系统,而是 Qt 的属性系统。凡是从 QObject 类派生的类型,都可以设置动态属性。动态属性不需要在类定义时声明,可以在代码中通过 setProperty 方法直接设置。属性名称为字符串类型,而属性值既可以是内置类型的值,也可以是自定义类型的实例。

获取属性值可以使用 property 方法,调用时需要传递属性名称。调用 dynamicPropertyNames 方法可以获得当前对象中所有动态属性的名称列表。属性名称用 QByteAray 类表示。

请看一个示例。

obj = QObject() # 设置动态属性 obj.setProperty("prop1", 500) obj.setProperty("prop2", 'abc') # 获取动态属性的名称列表 props = obj.dynamicPropertyNames() for x in props: # 打印属性名称与对应的值 propName = x.toStdString() print(f'{propName} = {obj.property(propName)}')

上述代码先在 obj 对象上设置两个动态属性,属性名分别为 prop1、prop2,然后调用 dynamicPropertyNames 方法获得动态属性的名称列表,最后打印动态属性的名称和对应的值。

示例运行后将输出以下结果:

输出结果
prop1 = 500
prop2 = abc

property 和 setProperty 方法也可用来读写 Qt 对象的非动态属性。下面以 QLabel 类为例,通过 setProperty 方法设置它的 windowTitle、size 和 text 属性。

from PySide6.QtWidgets import QApplication, QLabel from PySide6.QtCore import QSize if __name__ == '__main__': app = QApplication() lb = QLabel() # 设置属性 # windowTitle: 窗口标题 # size: 窗口大小 # text: 标签上呈现的文本 lb.setProperty('windowTitle', 'Demo App') lb.setProperty('size', QSize(250, 130)) lb.setProperty('text', '示例文本') lb.show() app.exec()

windowTitle 属性对应 windowTitle 和 setWindowTitle 方法,size 属性对应 size 和 resize 方法,text 属性对应 text 和 setText 方法。

运行效果如图 2-13 所示。

图 2-13
用 setProperty 方法设置 QLabel 对象的属性
用 setProperty 方法设置 QLabel 对象的属性
2.10

生成随机数

生成随机数值,可以使用 Python 内置的随机数生成函数(例如 random 模块下的 randint、random 等函数),但对于 Qt 项目,建议使用 QtCore 模块中的 QRandomGenerator 类。

下面是 QRandomGenerator 类的最简单用法——实例化后直接调用 generate 方法。

from PySide6.QtCore import QRandomGenerator # 实例化 rand = QRandomGenerator() n = 0 while n < 7: # 生成一个随机数 r = rand.generate() print(r, end=' ') n += 1

generate 方法每次调用都会返回一个随机的整数值。上述代码通过 while 循环生成 7 个随机整数,并打印到控制台。结果如下:

输出结果
853323747 2396352728 3025954838 2985633182 2815751046 340588426 3587208406

也可以调用 generateDouble 方法生成浮点数。

n = 0 while n < 7: # 生成一个随机数 r = rand.generateDouble() print(r, end=' ') n += 1

生成的浮点数值在 0~1 范围内,例如:

输出结果
0.557944348128205 0.6951468956434482 0.07929942259928047 0.06940391353220021
0.8479977992079486 0.1860916562717726 0.14888814171680542

2.10.1 设置随机数的种子

直接实例化 QRandomGenerator 类时,默认设置的种子是 1,因此每次运行应用程序后都会生成相同的随机数。可以用下面的代码来验证。

from PySide6.QtCore import QRandomGenerator def genRandInt(): rand = QRandomGenerator() # 生成5个随机数 for i in range(0, 5): print(rand.generate(), end=' ') print() # 换行 if __name__ == "__main__": # 执行三次 for x in range(0, 3): genRandInt()

genRandInt 是自定义函数,用于生成 5 个随机整数,在运行阶段连续三次调用该函数。从控制台的输出可以看到,三次实例化 QRandomGenerator 对象后所产生的随机数都是一样的。

输出结果
853323747 2396352728 3025954838 2985633182 2815751046
853323747 2396352728 3025954838 2985633182 2815751046
853323747 2396352728 3025954838 2985633182 2815751046

这些随机数是通过算法生成的(也称伪随机数),并非真正意义上的随机,因此具有规律性。如果生成的随机数是用于安全方面的,如加密、短信验证码等,攻击者可以根据规律预知下一个随机数值,从而进行恶意提交数据或信息轰炸等。

为了增加推测随机数的难度,可以考虑在实例化 QRandomGenerator 对象时指定一个种子(每次实例化时所指定的种子均不同)。常用的方法是使用时间值,如当前时间的总秒数、总毫秒数,或当前日期的总小时数等。

下面的示例将使用当前时间与 00:00:00 的时间差(总秒数)来设置随机数的种子。

from PySide6.QtCore import QRandomGenerator, QTime # 当前时间 currTime = QTime.currentTime() # 0 时 zero = QTime(0, 0, 0) # 得到时差的总秒数 totalSecs = zero.secsTo(currTime) # 实例化 QRandomGenerator rand = QRandomGenerator(totalSecs) # 生成 5 个随机数 for i in range(0, 5): n = rand.generate() print(n, end=' ')

可以尝试多次运行应用程序,此时发现每次生成的随机数并不相同,规律性不明显,安全性有所提高。

四次运行结果
# 第一次运行:
1230926884 3838824595 3655569805 4008070190 770077286
# 第二次运行:
1634385072 325323857 460065842 2846913223 1289398778
# 第三次运行:
3630061071 1570905298 29361579 188463289 985322278
# 第四次运行:
3934119818 2439380856 2303324033 1630700456 3603977815

2.10.2 使用内置的 QRandomGenerator 实例

尽管使用时间差设置随机数种子具备一定程度的安全性,但攻击者只要算出时间差也是可以预测下一个随机数的。因此,如果代码对安全要求比较高,那就得通过系统层面所提供的功能来生成随机数(包括通过硬件来生成随机数,如环境电压、鼠标/键盘的操作,或与计算机连接的信号噪声等)。这些基于系统层面的随机数算法比较综合且复杂,很难找到规律,可预测难度大。

QRandomGenerator 类公开以下三个静态成员,可以获取特定的实例。

  1. global 方法:返回一个共享的 QRandomGenerator 实例,该实例由基于系统层面的、具有一定安全性的种子创建,并且实例是共享的,访问时性能优越,占用资源少。大多数情况下推荐使用此实例来生成离散型随机数。
  2. securelySeeded 方法:使用系统层面的算法生成随机数,且具有可靠的安全性。此方法与 global 方法相似,但此方法返回的不是共享的对象实例,每次调用都会创建新的 QRandomGenerator 实例。因此,securelySeeded 方法比较消耗资源,只在需要生成大量随机数时使用。一般情况下使用 global 方法更佳。
  3. system 方法:此方法返回的也是共享的 QRandomGenerator 实例。此方法将使用操作系统层面的算法(包括硬件生成)来生成随机数。此方法通常用于设置种子,再以此种子为基础使用伪随机数算法生成随机数。

下面的示例将使用全局共享的 QRandomGenerator 对象来生成 9 个随机数。

# 获取全局共享的实例 rand = QRandomGenerator.global_() # 生成9个随机数 for i in range(1, 10): num = rand.generate() print(num, end='\n' if i % 3 == 0 else ' ')

将上述代码运行三次后,得到以下结果:

三次运行结果
# 第一次:
2254275905 2000608760 2058096574
998133270 1454645182 895832046
2932870971 1599245497 1648096071
# 第二次:
4156177851 2575768562 343431902
1631767061 2481187429 1193912935
4089031774 1959297329 919304449
# 第三次:
4289980107 1241748556 4152402115
301445846 3235752732 1789050098
201142820 1187791119 1525500384

根据上述结果可以看出,每次执行代码,所生成的整数是不重复的。

2.10.3 指定随机数的范围

bounded 方法可以指定生成随机数的“边界”——限定最小值(包含)和最大值(不包含)。

下面的示例将生成 8 个 0~99 的随机整数。

rand = QRandomGenerator.global_() # 生成 0~99 的随机整数 nums = [] for c in range(0, 7): n = rand.bounded(0, 100) nums.append(n) # 打印生成的随机数 # 输出:37, 50, 78, 47, 21, 98, 91 print(*nums, sep=', ')

上述代码中,由于限定随机数的最小值为 0,可以省略该值,仅指定最大值(不包含)即可。

n = rand.bounded(100)