第六章 PyQt5 的布局管理
6.1 好软件的三个维度
- 能用
最基本也是最重要的衡量标准
- 易用
主要看软件的布局管理架构是否合理,能否快速找到想要的,交互流程的简易
- 好用
友好、人性化的界面
6.2 PyQt5 中的布局管理
- 两种常用的布局方法
- addLayout() 用于在布局中插入子布局
- addWidget() 用于在布局中插入控件
6.3 PyQt5 的绝对位置布局
import sys from PyQt5.QtWidgets import * from PyQt5.QtCore import * from PyQt5.QtGui import * class absPositionDemo(QWidget): def __init__(self): super().__init__() self.setWindowTitle("6.3 PyQt5的绝对位置布局") self.setGeometry(300, 300, 320, 120) self.label1 = QLabel('欢迎', self) self.label1.move(15, 10) self.label2 = QLabel('学习', self) self.label2.move(35, 40) self.label3 = QLabel('PyQt5', self) self.label3.move(55, 70) if __name__ == '__main__': app = QApplication(sys.argv) win = absPositionDemo() win.show() sys.exit(app.exec_())
解析
- 使用 move 方法来对控件进行定位,第一个控件定位在 x=15,y=10 的位置
self.label1 = QLabel('欢迎', self) self.label1.move(15, 10)
总结
绝对定位的优点
- 可以直接定位每个控件的具体位置
绝对定位的缺点 - 如果改变一个窗口的大小,窗口控件的大小和位置就不会自适应改变
- 所生成的窗口在不同系统看起来可能是不一样的
- 在程序中改变字体时可能会破坏布局
- 如果修改布局,比如增加一个控件时,就必须重新布局
6.4 QBoxLayout 框布局
概念可以在水平和垂直方向排布控件,子类有 QHBoxLayout 和 QVBoxLayout
6.4.1 QHBoxLayout 水平布局
概念按照从左到右的顺序来排布控件
常用的方法
- addLayout(self, QLayout, stretch=0) 在窗口的右边添加布局,stretch(伸缩量默认是 0)
- addWidget(self,QWidget,stretch,Qt.Alignment) 在布局中添加控件
- stretch 伸缩量,只适用于 QBoxLayout,控件和窗口会随着变大
- alignment 指定控件的对齐方式
- setSpacing 设置控件的左右距离,通过该方法可以增大空间
例子 6.4.1 QHBoxLayout 水平布局
import sys from PyQt5.QtWidgets import * from PyQt5.QtCore import * from PyQt5.QtGui import * class hBoxLayoutdemo1(QWidget): def __init__(self): super().__init__() self.setWindowTitle("例子 6.4.1 QHBoxLayout 水平布局") layout = QHBoxLayout() layout.addWidget(QPushButton("1")) layout.addWidget(QPushButton("2")) layout.addWidget(QPushButton("3")) layout.addWidget(QPushButton("4")) layout.addWidget(QPushButton("5")) layout.addWidget(QPushButton("6")) self.setLayout(layout) if __name__ == '__main__': app = QApplication(sys.argv) win = hBoxLayoutdemo1() win.show() sys.exit(app.exec_())
6.4.2 QVBoxLayout 垂直布局
import sys from PyQt5.QtWidgets import * from PyQt5.QtCore import * from PyQt5.QtGui import * class hBoxLayoutdemo1(QWidget): def __init__(self): super().__init__() self.setWindowTitle("例子 6.4.1 QHBoxLayout 水平布局") layout = QVBoxLayout() layout.addWidget(QPushButton("1"), 0, Qt.AlignLeft | Qt.AlignTop) layout.addWidget(QPushButton("2"), 0, Qt.AlignLeft | Qt.AlignTop) layout.addWidget(QPushButton("3")) layout.addWidget(QPushButton("4")) layout.addWidget(QPushButton("5"), 0, Qt.AlignLeft | Qt.AlignBottom) layout.addWidget(QPushButton("6"), 0, Qt.AlignLeft | Qt.AlignBottom) layout.setSpacing(10) self.setLayout(layout) if __name__ == '__main__': app = QApplication(sys.argv) win = hBoxLayoutdemo1() win.show() sys.exit(app.exec_())
6.4.3 addStretch()函数的使用
概念设置 stretch 伸缩量后,按比例分配剩余空间
- QBoxLayout.addStretch(int stretch=0) 在布局管理器中增加一个可伸缩的控件(QSpaceItem)
- stretch 参数 表示均分的比例 默认是 0
import sys from PyQt5.QtWidgets import * from PyQt5.QtCore import * from PyQt5.QtGui import * class addStretchDemo(QWidget): def __init__(self): super().__init__() self.setWindowTitle("6.4.3 addStretch()函数的使用") layout = QHBoxLayout() btn1 = QPushButton('button1', self) btn2 = QPushButton('button2', self) btn3 = QPushButton('button3', self) layout.addWidget(btn1) layout.addWidget(btn2) layout.addWidget(btn3) # 设置伸缩量为1 layout.setStretch(2, 1) self.setLayout(layout) if __name__ == '__main__': app = QApplication(sys.argv) win = addStretchDemo() win.show() sys.exit(app.exec_())
6.5 QGridLayout 栅格布局
概念将窗口分割成行和列的网格进行排布控件
常用的方法
- addWidget() 在窗口中添加控件并且指定行列以及跨度和对齐方式
- setSpacing() 设置窗口中的控件的间距
案例 6.5.1 单一的栅格单元格
import sys from PyQt5.QtWidgets import * from PyQt5.QtCore import * from PyQt5.QtGui import * class gridLayoutSingleDemo(QWidget): def __init__(self): super().__init__() self.setWindowTitle("案例6.5.1 单一的栅格单元格") layout = QGridLayout() # 1 names = ['Cls', 'Back', ' ', 'Close', '7', '8', '9', '/', '4', '5', '6', '*', '0', '1', '2', '+'] # 2 positions = [(i, j) for i in range(5) for j in range(4)] # 3 for position, name in zip(positions, names): if name == ' ': continue button = QPushButton(name) # 由于参数是位置参数 所以使用*来解耦元组数据为一个个的位置参数 layout.addWidget(button, *position) self.move(300, 150) self.setLayout(layout) if __name__ == '__main__': app = QApplication(sys.argv) win = gridLayoutSingleDemo() win.show() sys.exit(app.exec_())
解析
- 第一组代码 创建按钮的标签列表
- 第二组代码 创建一个位置列表元组,使用的生成器表达式
- 第三组代码 创建按钮,并通过 addWidget 方法添加到布局中
- 使用*对位置参数列表进行解耦操作,分配到所需的单个位置参数
6.5.2 跨越行与列的栅格单元格
import sys from PyQt5.QtWidgets import * from PyQt5.QtCore import * from PyQt5.QtGui import * class gridLayoutDemo(QWidget): def __init__(self): super().__init__() self.setWindowTitle("6.5.2 跨越行与列的栅格单元格") self.setGeometry(300, 300, 350, 300) layout = QGridLayout() layout.setSpacing(10) title = QLabel("标题") author = QLabel("提交人") review = QLabel("申告内容") titleEdit = QLineEdit() authorEdit = QLineEdit() reviewEdit = QTextEdit() layout.addWidget(QPushButton("nihao"), 0, 0, 1, 2) layout.addWidget(title, 1, 0) layout.addWidget(titleEdit, 1, 1) layout.addWidget(author, 2, 0) layout.addWidget(authorEdit, 2, 1) layout.addWidget(review, 3, 0) layout.addWidget(reviewEdit, 3, 1, 5, 1) self.setLayout(layout) if __name__ == '__main__': app = QApplication(sys.argv) win = gridLayoutDemo() win.show() sys.exit(app.exec_())
解析
- 跨越了 5 行 1 列
layout.addWidget(reviewEdit, 3, 1, 5, 1)
6.6 QFormLayout 表单布局
概念这种是 label-field 式的表单布局---label 通过 field 的 ID 关联 field
import sys from PyQt5.QtWidgets import * from PyQt5.QtCore import * from PyQt5.QtGui import * class formLayoutDemo(QWidget): def __init__(self): super().__init__() self.setWindowTitle("6.6 QFormLayout 表单布局") self.resize(400, 100) layout = QFormLayout() label1 = QLabel("标签1") lineEdit1 = QLineEdit() label2 = QLabel("标签1") lineEdit2 = QLineEdit() label3 = QLabel("标签1") lineEdit3= QLineEdit() layout.addRow(label1, lineEdit1) layout.addRow(label2, lineEdit2) layout.addRow(label3, lineEdit3) self.setLayout(layout) if __name__ == '__main__': app = QApplication(sys.argv) win = formLayoutDemo() win.show() sys.exit(app.exec_())
6.7 嵌套布局
概念简单的单一布局不难,但是复杂的嵌套布局不简单,一般使用 qtdesigner 进行可视化设计
6.7.1 在布局中添加其他的布局
import sys from PyQt5.QtWidgets import * from PyQt5.QtCore import * from PyQt5.QtGui import * class nestLayoutDemo(QWidget): def __init__(self): super().__init__() self.setWindowTitle("6.7.1 在布局中添加其他的布局") # 全局布局 gHLayout = QHBoxLayout() # 局部布局 lHLayout = QHBoxLayout() lVLayout = QVBoxLayout() lGridLayout = QGridLayout() lFormLayout = QFormLayout() # 为布局添加控件 lHLayout.addWidget(QPushButton("1")) lHLayout.addWidget(QPushButton("2")) lVLayout.addWidget(QPushButton("3")) lVLayout.addWidget(QPushButton("4")) lGridLayout.addWidget(QPushButton("5"), 0, 0) lGridLayout.addWidget(QPushButton("6"), 0, 1) lGridLayout.addWidget(QPushButton("7"), 1, 0) lGridLayout.addWidget(QPushButton("8"), 1, 1) lFormLayout.addWidget(QPushButton("9")) lFormLayout.addWidget(QPushButton("10")) lFormLayout.addWidget(QPushButton("11")) lFormLayout.addWidget(QPushButton("12")) # 准备四个窗口控件 lHWidget = QWidget() lVWidget = QWidget() lGridWidget = QWidget() lFormWidget = QWidget() # 使用四个控件设置局部布局 lHWidget.setLayout(lHLayout) lVWidget.setLayout(lVLayout) lGridWidget.setLayout(lGridLayout) lFormWidget.setLayout(lFormLayout) # 将四个Widget控件添加到全局布局中 gHLayout.addWidget(lHWidget) gHLayout.addWidget(lVWidget) gHLayout.addWidget(lGridWidget) gHLayout.addWidget(lFormWidget) self.setLayout(gHLayout) if __name__ == '__main__': app = QApplication(sys.argv) win = nestLayoutDemo() win.show() sys.exit(app.exec_())
总结
- 这样的嵌套布局有一个缺点,四种局部布局需要四个空白控件,假如有十种局部布局,就需要十个空白控件,这样是很麻烦的
- 解决方法就是下面一节讲到的,只要一个空白控件,在控件上进行多种布局的排布
6.7.2 在控件中添加布局
import sys from PyQt5.QtWidgets import * class nestLayoutDemo(QWidget): def __init__(self): super().__init__() self.setWindowTitle("6.7.2 在控件中添加布局") self.resize(700, 200) # 全局布局的空白窗口 父级窗口就是self gWidget = QWidget(self) # 全局布局 gHLayout = QHBoxLayout() # 局部布局 lHLayout = QHBoxLayout() lVLayout = QVBoxLayout() lGridLayout = QGridLayout() lFormLayout = QFormLayout() # 为布局添加控件 lHLayout.addWidget(QPushButton("1")) lHLayout.addWidget(QPushButton("2")) lVLayout.addWidget(QPushButton("3")) lVLayout.addWidget(QPushButton("4")) lGridLayout.addWidget(QPushButton("5"), 0, 0) lGridLayout.addWidget(QPushButton("6"), 0, 1) lGridLayout.addWidget(QPushButton("7"), 1, 0) lGridLayout.addWidget(QPushButton("8"), 1, 1) lFormLayout.addWidget(QPushButton("9")) lFormLayout.addWidget(QPushButton("10")) lFormLayout.addWidget(QPushButton("11")) lFormLayout.addWidget(QPushButton("12")) # 在全局布局中添加已有的布局 gHLayout.addLayout(lHLayout) gHLayout.addLayout(lVLayout) gHLayout.addLayout(lGridLayout) gHLayout.addLayout(lFormLayout) # 给全局空白的那个窗口控件设置全局布局 gWidget.setLayout(gHLayout) if __name__ == '__main__': app = QApplication(sys.argv) win = nestLayoutDemo() win.show() sys.exit(app.exec_())
解析
- 创建一个全局窗口 Widget,指定父级窗口为 self
# 全局布局的空白窗口 父级窗口就是self gWidget = QWidget(self)
- 创建一个全局布局和四个局部布局,然后通过添加布局的方式完成布局
# 全局布局 gHLayout = QHBoxLayout() # 局部布局 lHLayout = QHBoxLayout() lVLayout = QVBoxLayout() lGridLayout = QGridLayout() lFormLayout = QFormLayout() # 在全局布局中添加已有的布局 gHLayout.addLayout(lHLayout) gHLayout.addLayout(lVLayout) gHLayout.addLayout(lGridLayout) gHLayout.addLayout(lFormLayout)
- 设置全局窗口控件的全局布局
# 给全局空白的那个窗口控件设置全局布局 gWidget.setLayout(gHLayout) # 还可以直接在创建布局时就指定应用到哪个窗口控件上 # 全局布局指定应用到的全局控件 gHLayout = QHBoxLayout(gWidget)
6.8 QSplitter 分离器的使用
概念可以拖动子控件的边界来控制子控件的大小,并提供了一个处理子控件拖拽的控制器
- QSplitter 对象中的各个子控件的默认布局是横向的,可以使用 Qt.Vertical 进行垂直布局
- QSplitter 常用的方法
- addWidget() 将小控件添加到 QSplitter 管理的布局中
- indexOf() 返回小控件在 QSplitter 管理器中的索引
- insertWidget() 在指定的索引处插入小控件
- setOrientataion()设置布局的方向
- Qt.Vertical 垂直方向
- Qt.Horizontal 水平方向
- setSizes() 设置控件的初始化大小
- count() 返回 QSplitter 管理器中的小控件的数量
案例 6.8 QSplitter 分离器的使用
import sys from PyQt5.QtWidgets import * from PyQt5.QtCore import * from PyQt5.QtGui import * class splitterDemo(QWidget): def __init__(self): super().__init__() self.setWindowTitle("案例6.8 QSplitter 分离器的使用") self.setGeometry(300, 300, 300, 200) layout = QHBoxLayout() topLeft = QFrame() topLeft.setFrameShape(QFrame.StyledPanel) bottom = QFrame() bottom.setFrameShape(QFrame.StyledPanel) textEdit = QTextEdit() splitter1 = QSplitter(Qt.Horizontal) splitter1.addWidget(topLeft) splitter1.addWidget(textEdit) splitter1.setSizes([100, 200]) splitter2 = QSplitter(Qt.Vertical) splitter2.addWidget(bottom) splitter2.insertWidget(0, splitter1) layout.addWidget(splitter2) self.setLayout(layout) print(splitter2.count()) if __name__ == '__main__': app = QApplication(sys.argv) win = splitterDemo() win.show() sys.exit(app.exec_())
解析
- 第一个分离器添加了两个控件,水平排布并初始化了 splitter1 的大小
splitter1 = QSplitter(Qt.Horizontal) splitter1.addWidget(topLeft) splitter1.addWidget(textEdit) splitter1.setSizes([100, 200])
- 第二个分离器也添加了两个控件,只不过将第一个 splitter1 作为了添加的对象作为第一个控件
splitter2.addWidget(bottom) splitter2.insertWidget(0, splitter1)
- 初始化 QSplitter 对象的大小,需要注意 setSizes 函数需要传递的是一个 Sizes----宽高一体化也即为列表
第七章 PyQt5 信号与槽
7.1 信号与槽介绍
- 信号(Signal) 和槽(Slot)是 Qt 中的核心机制,也是在 PyQt 中对象之间进行通讯的机制
- 信号与槽的优点
- 一个信号可以连接多个槽函数
- 一个信号可以连接另一个信号
- 信号参数可以是任何 Python 类型
- 一个槽可以监听多个信号
- 信号与槽的连接可以是同步的也可以是异步的
- 信号与槽的连接可能会是跨线程的
- 信号可能会断开
7.1.1 定义信号
- 为 QObject 对象创建信号
- pyqtSignal() 函数只能在 QObject 的子类中定义
------pyqtSignal()函数的两个常用参数- type 信号的数据类型
- name 信号的名称,不写信号的名称就为属性的名称
- 信号必须在类创建时定义,不能在创建后动态添加为类的属性
- 信号可以传递多个参数,参数的数据类型是标准的 Python 数据类型
- pyqtSignal() 函数只能在 QObject 的子类中定义
class Foo(QObject): valueChanged = pyqtSignal([dict], [list])
- 为控件创建信号
import sys from PyQt5.QtWidgets import * from PyQt5.QtCore import * from PyQt5.QtGui import * class splitterDemo(QWidget): btnClickedSignal = pyqtSignal()
7.1.2 操作信号
- connect() 将信号绑定到槽函数上
- disconnect() 解除信号与槽函数的连接
- emit() 发射信号
7.1.3 信号与槽的入门应用
- 信号与槽的三种使用方法
- 内置信号与槽的使用
- 自定义信号与槽的使用
- 装饰器的信号与槽的使用
案例 7.1.3 内置信号与槽的使用
import sys from PyQt5.QtWidgets import * from PyQt5.QtCore import * from PyQt5.QtGui import * class signal2SlotDemo(QWidget): def __init__(self): super().__init__() self.setWindowTitle("案例7.1.3 内置信号与槽的使用") layout = QVBoxLayout() btn = QPushButton("测试点击按钮", self) btn.clicked.connect(self.showMsg) layout.addWidget(btn) self.setLayout(layout) def showMsg(self): QMessageBox.information(self, "信息标题", "OK, 弹出测试信息", QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) if __name__ == '__main__': app = QApplication(sys.argv) win = signal2SlotDemo() win.show() sys.exit(app.exec_())
解析
- 使用了按钮类的内置信号 clicked 连接到自定义的槽函数
btn.clicked.connect(self.showMsg)
案例 7.1.3 自定义信号与槽的使用
使用背景
使用内置信号的缺陷
- 内置信号只包含了常用的
- 内置信号满足的条件是不可变的
- 内置信号的触发传递给槽函数的参数是特定的
import sys from PyQt5.QtWidgets import * from PyQt5.QtCore import * from PyQt5.QtGui import * # 信号与槽 # 信号 class QTypeSignal(QObject): # 定义一个信号 sendMsg = pyqtSignal(object) def __init__(self): super().__init__() def run(self): # 发射信号 self.sendMsg.emit("Hello PyQt5") # 槽 class QTypeSlot(QObject): def __init__(self): super().__init__() # 槽对象中的槽函数 def get(self, msg): print("QSlot get msg =>" + msg) class cSignal2SlotDemo(QWidget): def __init__(self): super().__init__() self.setWindowTitle("案例7.1.3 自定义信号与槽的使用") send = QTypeSignal() slot = QTypeSlot() # 1 print("---把信号绑定到槽函数上---") send.sendMsg.connect(slot.get) send.run() # 2 print("---解除信号与槽的连接---") send.sendMsg.disconnect(slot.get) send.run() if __name__ == '__main__': app = QApplication(sys.argv) win = cSignal2SlotDemo() win.show() sys.exit(app.exec_())
解析
- 生成一个信号
sendMsg = pyqtSignal(object)
- 将信号与槽函数绑定
send.sendMsg.connect(slot.get)
- 槽函数接收到信号触发时传递的参数
def get(self, msg): print("QSlot get msg =>" + msg)
- 发射信号触发槽函数的响应
print("QSlot get msg =>" + msg)
import sys from PyQt5.QtWidgets import * from PyQt5.QtCore import * from PyQt5.QtGui import * # 信号与槽 # 信号 class QTypeSignal(QObject): # 定义一个信号 sendMsg = pyqtSignal(str,str) def __init__(self): super().__init__() def run(self): # 发射信号 self.sendMsg.emit("Hello PyQt5", "wbj") # 槽 class QTypeSlot(QObject): def __init__(self): super().__init__() # 槽对象中的槽函数 def get(self, msg1, msg2): print("QSlot get msg =>" + msg1 + msg2) class cSignal2SlotDemo(QWidget): def __init__(self): super().__init__() self.setWindowTitle("案例7.1.3 自定义信号与槽的使用") send = QTypeSignal() slot = QTypeSlot() # 1 print("---把信号绑定到槽函数上---") send.sendMsg.connect(slot.get) send.run() # 2 print("---解除信号与槽的连接---") send.sendMsg.disconnect(slot.get) send.run() if __name__ == '__main__': app = QApplication(sys.argv) win = cSignal2SlotDemo() win.show() sys.exit(app.exec_())
7.1.4 快速进阶
- PyQt 默认有哪些信号与槽
- 如何使用这些信号与槽
-----在之前的章节 3.4.2 快速进阶 中讲述了相关内容
7.2 信号与槽的再细分
7.2.1 内置信号与槽函数
import sys from PyQt5.QtWidgets import * class btn2CloseDemo(QWidget): def __init__(self): super().__init__() self.setWindowTitle("7.2.1 内置信号与槽函数") self.resize(330, 50) btn = QPushButton('关闭', self) btn.clicked.connect(self.close) if __name__ == '__main__': app = QApplication(sys.argv) win = btn2CloseDemo() win.show() sys.exit(app.exec_())
解析
- 单击按钮触发内置信号 clicked 绑定窗口的内置槽函数 self.close
btn.clicked.connect(self.close)
7.2.2 内置信号与自定义槽函数
import sys from PyQt5.QtWidgets import * class Demo(QWidget): def __init__(self): super().__init__() self.setWindowTitle("7.2.2 内置信号与自定义槽函数") self.resize(330, 50) btn = QPushButton("关闭", self) btn.clicked.connect(self.closeWidget) def closeWidget(self): app.quit() if __name__ == '__main__': app = QApplication(sys.argv) win = Demo() win.show() sys.exit(app.exec_())
7.2.3 自定义信号与内置槽函数
import sys from PyQt5.QtWidgets import * from PyQt5.QtCore import * from PyQt5.QtGui import * class Demo(QWidget): # 自定义信号 不带参数 btn_click_signal = pyqtSignal() def __init__(self): super().__init__() self.setWindowTitle("7.2.3 自定义信号与内置槽函数") self.resize(330, 50) btn = QPushButton("关闭", self) # 连接内置信号与槽---发射自定义信号 btn.clicked.connect(self.btn_clicked) # 连接内置信号与槽 self.btn_click_signal.connect(self.close) def btn_clicked(self): self.btn_click_signal.emit() if __name__ == '__main__': app = QApplication(sys.argv) win = Demo() win.show() sys.exit(app.exec_())
7.2.4 自定义信号与槽函数
import sys from PyQt5.QtWidgets import * from PyQt5.QtCore import * from PyQt5.QtGui import * class Demo(QWidget): # 自定义信号 不带参数 btn_click_signal = pyqtSignal() def __init__(self): super().__init__() self.setWindowTitle("7.2.3 自定义信号与内置槽函数") self.resize(330, 50) btn = QPushButton("关闭", self) # 连接内置信号与槽---发射自定义信号 btn.clicked.connect(self.btn_clicked) # 连接内置信号与槽 self.btn_click_signal.connect(self.closeWidget) def btn_clicked(self): self.btn_click_signal.emit() def closeWidget(self): app.quit() if __name__ == '__main__': app = QApplication(sys.argv) win = Demo() win.show() sys.exit(app.exec_()
7.3 信号与槽的高级玩法
7.3.1 高级自定义的信号与槽
概念我们可以根据自己喜欢的方式定义信号与槽函数,并传递参数
一般流程
- 定义信号
- 定义槽函数
- 连接信号与槽函数
- 发射信号
- 定义信号
class MyWidget(QWidget): # 无参数的信号 Signal_NoParameters = pyqtSignal() # 带一个参数(整数)的信号 Signal_OneParameters = pyqtSignal(int) # 带一个参数(整数或者字符串)的重载版本的信号 Signal_OneParameters_Overload = pyqtSignal([int], [str]) # 带两个参数(整数、字符串)的信号 Signal_TwoParameters = pyqtSignal(int, str) # 带两个参数(整数,整数或者整数,字符串)的重载版本的信号 Signal_TwoParameters_Overload = pyqtSignal([int, int], [int, str])
- 定义槽函数
class MyWidget(QWidget): def setValue_NoParameters(self): '''无参数的槽函数''' pass def setValue_Signal_OneParameters(self, nIndex): '''带一个参数(整数)的信号''' pass def setValue_Signal_OneParameters_String(self, szIndex): '''带一个参数(整数或者字符串)的重载版本的信号''' pass def setValue_Signal_TwoParameters(self, x, y): '''带两个参数(整数、字符串)的信号''' pass def setValue_Signal_TwoParameters_Overload(self, x, szy): '''带两个参数(整数,整数或者整数,字符串)的重载版本的信号''' pass
- 连接信号与槽函数
# 连接无参数的信号 def __init__(self): self.Signal_NoParameters.connect(self.Signal_NoParameters)
- 发射信号(emit)
self.Signal_NoParameters.emit()
- 实例
class CustSignal(QObject): # 声明无参数的信号 signalNoParam = pyqtSignal() # 声明一个int类型参数的信号 signalOneIntParam = pyqtSignal(int) # 声明一个int一个str参数的信号 signalInt2StrParam = pyqtSignal(int, str) # 声明一个dict字典类型参数的信号 signalDictParam = pyqtSignal(dict) # 声明一个list列表类型参数的信号 signalListParam = pyqtSignal(list) # 声明一个可选重载类型的信号 signalMultiOverloadParam = pyqtSignal([int, str], [str]) def __init__(self, parent=None): super(CustSignal, self).__init__(parent) # 将信号连接到指定的槽函数上 self.signalNoParam.connect(self.signalNoParamCall) self.signalOneIntParam.connect(self.signalOneIntParamCall) self.signalDictParam.connect(self.signalDictParamCall) self.signalListParam.connect(self.signalListParamCall) self.signalInt2StrParam.connect(self.signalInt2StrParamCall) self.signalMultiOverloadParam[int,str].connect(self.signalMultiOverloadParam_int2strCall) self.signalMultiOverloadParam[str].connect(self.signalMultiOverloadParam_strCall) self.signalNoParam.emit() self.signalOneIntParam.emit(1) self.signalListParam.emit([1, 2, 3]) self.signalDictParam.emit({'name': 'wbj'}) self.signalInt2StrParam.emit(1, 'wbj') self.signalMultiOverloadParam[int, str].emit(2, 'wbj') self.signalMultiOverloadParam[str].emit('520') # 定义槽函数 def signalNoParamCall(self): print('signalNoParam emit') # 发射信号时会携带一个int类型的实参 def signalOneIntParamCall(self, val): print('signalOneIntParam emit,value:', val) def signalInt2StrParamCall(self, val, text): print('signalInt2StrParam emit, value:', val, text) def signalDictParamCall(self, dictParam): print('signalDictParam emit, value:', dictParam) def signalListParamCall(self, listParam): print('signalListParam emit, value:', listParam) def signalMultiOverloadParam_int2strCall(self, val, text): print('signalMultiOverloadParam_int2str emit, value:', val, text) def signalMultiOverloadParam_strCall(self, text): print('signalMultiOverloadParam_str emit, value:', text) if __name__ == '__main__': custSignal = CustSignal()
7.3.2 使用自定义参数
- 在 PyQt 编程过程中,经常遇到给内置无参数信号连接的槽函数传递自定义参数的情况
- 比如 button1.clicked.connect(show_page)
- clicked 信号本身是没有参数的
- 对于 show_page 却希望传递一个参数
def show_page(self, name): print(name, " 点击啦")
- 信号发射出去携带参数为 0 个,实际槽函数接收参数是 1 个,肯定是要报错的
- 两种解决方法来搞定自定义参数的传递
# 第一种方法 lambda表达式 import sys from PyQt5.QtWidgets import * from PyQt5.QtCore import * from PyQt5.QtGui import * class WinForm(QMainWindow): def __init__(self, parent=None): super(WinForm, self).__init__(parent) button1 = QPushButton('Button 1') button2 = QPushButton('Button 2') button1.clicked.connect(lambda: self.onButtonClick(1)) button2.clicked.connect(lambda: self.onButtonClick(2)) layout = QHBoxLayout() layout.addWidget(button1) layout.addWidget(button2) main_frame = QWidget() main_frame.setLayout(layout) self.setCentralWidget(main_frame) def onButtonClick(self, val): print('Button {} 被按下了'.format(val)) QMessageBox.information(self, '信息提示框', 'Button {} clicked'.format(val)) if __name__ == '__main__': app = QApplication(sys.argv) form = WinForm() form.show() sys.exit(app.exec_()) ### 解析 1. 使用lambda表达式传递按钮数字给槽函数,可以传递其他任何的参数 2. 甚至可以传递控件本身,这样可以对控件进行某些操作(比如禁用) # 第二种方法 使用functools中的partial函数 import sys from PyQt5.QtWidgets import * from PyQt5.QtCore import * from PyQt5.QtGui import * from functools import partial class WinForm(QMainWindow): def __init__(self, parent=None): super(WinForm, self).__init__(parent) button1 = QPushButton('Button 1') button2 = QPushButton('Button 2') # 使用lambda表达式完成自定义参数的传递 # button1.clicked.connect(lambda: self.onButtonClick(1)) # button2.clicked.connect(lambda: self.onButtonClick(2)) # 使用functools中的partial函数 button1.clicked.connect(partial(self.onButtonClick, 1)) button2.clicked.connect(partial(self.onButtonClick, 2)) layout = QHBoxLayout() layout.addWidget(button1) layout.addWidget(button2) main_frame = QWidget() main_frame.setLayout(layout) self.setCentralWidget(main_frame) def onButtonClick(self, val): print('Button {} 被按下了'.format(val)) QMessageBox.information(self, '信息提示框', 'Button {} clicked'.format(val)) if __name__ == '__main__': app = QApplication(sys.argv) form = WinForm() form.show() sys.exit(app.exec_()) ### 解析 1. 需要引入partial from functools import partial
7.3.3 装饰器信号与槽
概念通过装饰器的方法来定义信号和槽函数
import sys from PyQt5.QtWidgets import * from PyQt5.QtCore import * from PyQt5.QtGui import * class CustWidget(QWidget): def __init__(self, parent=None): super(CustWidget, self).__init__(parent) self.okButton = QPushButton('OK', self) # 使用setObjectName设置对象名称 self.okButton.setObjectName('okButton') layout = QHBoxLayout() layout.addWidget(self.okButton) self.setLayout(layout) # 表示self对象中的元数据--比如各个控件对象,是通过对象名字来连接槽函数的 # 表示self中的各个对象组件连接槽函数的方法都是通过对象名称的 QMetaObject.connectSlotsByName(self) @pyqtSlot() def on_okButton_clicked(self): print("单击了ok按钮") if __name__ == '__main__': app = QApplication(sys.argv) win = CustWidget() win.show() sys.exit(app.exec_())
用法模板
@PyQt5.QtCore.pyqtSlot(参数) def on_使用setObjectName设置的名称_信号名称(self, 参数): pass ### 上述用法生效的前提是下面这段代码已经执行 QMetaObject.connectSlotsByName(QObject) ### 上面一段代码是根据信号对象名称自动连接到槽函数的核心代码实现 ### 表示QObject中的子孙对象的某些信号可以按照其objectName连接到相应的槽函数 @PyQt5.QtCore.pyqtSlot(参数) def on_okButton_clicked(self): print('单击了ok按钮') ----->等价于 def __init__(self, parent=None): self.okButton.clicked(self.okButton_clicked) def okButton_click(self): print('单击了OK按钮')
7.3.4 信号与槽的断开和连接
应用场景基于某些原因,想要临时或永久断开某个信号与槽的连接
from PyQt5.QtCore import QObject, pyqtSignal class CustSignal(QObject): # 声明无参数的信号 signalNoParam = pyqtSignal() # 声明一个int类型参数的信号 signalIntParam = pyqtSignal(int) def __init__(self, parent=None): super(CustSignal, self).__init__(parent) # 将信号signalNoParam连接到sin1Call和sin2Call这两个槽函数上 self.signalNoParam.connect(self.sin1Call) self.signalNoParam.connect(self.sin2Call) # 将信号signalIntParam连接到signalNoParam上 self.signalIntParam.connect(self.signalNoParam) # 发射信号 self.signalNoParam.emit() self.signalIntParam.emit(1) # 断开信号与槽函数之间的全部连接 self.signalNoParam.disconnect(self.sin1Call) self.signalNoParam.disconnect(self.sin2Call) self.signalIntParam.disconnect(self.signalNoParam) # 将两个信号都连接到槽函数sin1Call上 self.signalNoParam.connect(self.sin1Call) self.signalIntParam.connect(self.sin1Call) # 再次发射信号 self.signalNoParam.emit() self.signalIntParam.emit(1) def sin1Call(self, val='520'): print('1, value:', val) def sin2Call(self, val='wbj'): print('2, value:', val) if __name__ == '__main__': signal = CustSignal()
7.3.5 Qt Designer 神助攻:界面显示与业务逻辑的分离
实例-仿造打印界面
# -*- coding: utf-8 -*- # Form implementation generated from reading ui file 'MainWinSignalSlot02.ui' # # Created by: PyQt5 UI code generator 5.11.2 # # WARNING! All changes made in this file will be lost! from PyQt5 import QtCore, QtGui, QtWidgets import sys class Ui_Form(object): def setupUi(self, Form): Form.setObjectName("Form") Form.resize(653, 300) self.groupBox = QtWidgets.QGroupBox(Form) self.groupBox.setGeometry(QtCore.QRect(0, 0, 571, 251)) self.groupBox.setObjectName("groupBox") self.groupBox_2 = QtWidgets.QGroupBox(self.groupBox) self.groupBox_2.setGeometry(QtCore.QRect(10, 20, 381, 51)) self.groupBox_2.setTitle("") self.groupBox_2.setObjectName("groupBox_2") self.splitter = QtWidgets.QSplitter(self.groupBox_2) self.splitter.setGeometry(QtCore.QRect(10, 10, 361, 28)) self.splitter.setOrientation(QtCore.Qt.Horizontal) self.splitter.setObjectName("splitter") self.label = QtWidgets.QLabel(self.splitter) self.label.setObjectName("label") self.spinBox = QtWidgets.QSpinBox(self.splitter) self.spinBox.setObjectName("spinBox") self.comboBox = QtWidgets.QComboBox(self.splitter) self.comboBox.setObjectName("comboBox") self.label_2 = QtWidgets.QLabel(self.splitter) self.label_2.setObjectName("label_2") self.pushButton_2 = QtWidgets.QPushButton(self.splitter) self.pushButton_2.setObjectName("pushButton_2") self.groupBox_3 = QtWidgets.QGroupBox(self.groupBox) self.groupBox_3.setGeometry(QtCore.QRect(10, 80, 201, 51)) self.groupBox_3.setTitle("") self.groupBox_3.setObjectName("groupBox_3") self.checkBox = QtWidgets.QCheckBox(self.groupBox_3) self.checkBox.setGeometry(QtCore.QRect(10, 10, 83, 28)) self.checkBox.setObjectName("checkBox") self.pushButton = QtWidgets.QPushButton(self.groupBox_3) self.pushButton.setGeometry(QtCore.QRect(100, 10, 83, 28)) self.pushButton.setObjectName("pushButton") self.groupBox_4 = QtWidgets.QGroupBox(self.groupBox) self.groupBox_4.setGeometry(QtCore.QRect(400, 10, 161, 221)) self.groupBox_4.setObjectName("groupBox_4") self.retranslateUi(Form) QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): _translate = QtCore.QCoreApplication.translate Form.setWindowTitle(_translate("Form", "Form")) self.groupBox.setTitle(_translate("Form", "打印控制")) self.label.setText(_translate("Form", "打印份数:")) self.label_2.setText(_translate("Form", "纸张类型")) self.pushButton_2.setText(_translate("Form", "打印")) self.checkBox.setText(_translate("Form", "全局预览")) self.pushButton.setText(_translate("Form", "预览")) self.groupBox_4.setTitle(_translate("Form", "操作结果")) class MyMainWindow(QtWidgets.QMainWindow, Ui_Form): helpSignal = QtCore.pyqtSignal(str) printSignal = QtCore.pyqtSignal(list) # 声明一个多重载版本的信号,包括一个带int和str类型参数的信号以及带str类型参数的信号 previewSignal = QtCore.pyqtSignal([int, str], [str]) def __init__(self, parent=None): super(MyMainWindow, self).__init__(parent) self.setupUi(self) self.initUI() def initUI(self): self.helpSignal.connect(self.showHelpMessage) self.printSignal.connect(self.printPaper) self.previewSignal[int, str].connect(self.previewPaperWithArgs) self.previewSignal[str].connect(self.previewPaper) self.pushButton_2.clicked.connect(self.emitPrintSignal) self.pushButton.clicked.connect(self.emitPreviewSignal) # 发射预览信号 def emitPreviewSignal(self): if self.checkBox.isChecked() == True: self.previewSignal[int, str].emit(1080, "Full Screen") elif self.checkBox.isChecked() == False: self.previewSignal[str].emit("Preview") # 发射打印信号 def emitPrintSignal(self): pList = [] pList.append(self.spinBox.value()) print(pList) pList.append(self.comboBox.currentText()) print(pList) # pList = ['1', '16开'] self.printSignal.emit(pList) def printPaper(self, pList): pList[1] = pList[1] if pList[1] else '16' print(pList[1]) print("打印信息:份数----{}份+ ,纸张----{}开".format(pList[0], pList[1])) def previewPaper(self, text): print(text) def previewPaperWithArgs(self, style, text): print(style, text) # 重载按钮事件 def keyPressEvent(self, e): if e.key() == QtCore.Qt.Key_F1: self.helpSignal.emit("help message") # 显示帮助信息 def showHelpMessage(self, message): print(message) if __name__ == '__main__': app = QtWidgets.QApplication(sys.argv) win = MyMainWindow() win.show() sys.exit(app.exec_()) ### 解析 1. 多重载版本需要显示指定参数类型之后,再去与槽函数进行绑定 self.previewSignal[str].emit("Preview") 2. 信号发射可以传递python数据类型的参数 3. 重写keyPressEvent()方法,对F1键进行功能的扩展 def keyPressEvent(self, e): if e.key() == QtCore.Qt.Key_F1: self.helpSignal.emit("help message")
注意点
- 自定义信号必须要在__init__函数之前定义
- 自定义信号可以传递 int、str、list、dict、object、float、tuple 等很多类型的数据
- 注意 signal 和 slot 之间的逻辑关系,避免出现死循环,比如在 slot 方法中继续发射该信号
7.3.6 多线程中信号与槽函数的使用
- 最简单的多线程使用方法是利用 QThread 函数
案例-多线程操作信号与槽函数
from PyQt5.QtWidgets import QApplication, QWidget from PyQt5.QtCore import QThread, pyqtSignal import sys class Main(QWidget): def __init__(self, parent=None): super(Main, self).__init__(parent) # 创建一个线程实例并设置名称、变量、信号与槽函数 self.thread = MyThread() self.thread.setIdentity('thread1') self.thread.sinOut.connect(self.outText) self.thread.setVal(6) def outText(self, text): print(text) class MyThread(QThread): sinOut = pyqtSignal(str) def __init__(self, parent=None): super(MyThread, self).__init__(parent) self.identity = None def setIdentity(self, text): self.identity = text def setVal(self, val): self.times = int(val) # 执行线程的run方法 self.start() def run(self): while self.times > 0 and self.identity: # 发射信号 self.sinOut.emit(self.identity + "====>" + str(self.times)) self.times -= 1 if __name__ == '__main__': app = QApplication(sys.argv) main = Main() main.show() sys.exit(app.exec_())
案例-多线程处理界面卡顿(主线程更新界面,子线程实时处理数据)
from PyQt5.QtCore import QThread, pyqtSignal, QDateTime from PyQt5.QtWidgets import QApplication, QDialog, QLineEdit import sys import time class BackendThread(QThread): # 通过类成员对象定义信号 update_data = pyqtSignal(str) # 处理业务逻辑 def run(self): while True: data = QDateTime.currentDateTime() currentTime = data.toString("yyyy-MM-dd hh:mm:ss") self.update_data.emit(str(currentTime)) time.sleep(1) class Window(QDialog): def __init__(self, parent=None): super(Window, self).__init__(parent) self.setWindowTitle('PyQt5界面实时更新时间例子') self.resize(400, 100) self.input = QLineEdit(self) self.input.resize(400, 100) self.initUI() def initUI(self): # 创建线程 self.thread = BackendThread() # 连接信号 self.thread.update_data.connect(self.handleDisplay) # 开始线程 self.thread.start() # 将当前的时间输出到文本框 def handleDisplay(self, timeData): self.input.setText(timeData) if __name__ == '__main__': app = QApplication(sys.argv) win = Window() win.show() sys.exit(app.exec_())
7.4 事件处理机制入门
建议一般在使用高级信号与槽函数处理不了问题时,再去考虑使用低级的事件处理机制
- PyQt 为事件处理提供了两种机制:高级的信号与槽函数、低级的事件处理机制
7.4.1 事件和信号与槽的区别
- 信号与槽可以说是事件处理机制的高级封装
- 比如一个按钮,当我们使用这个按钮时,只关心 clicked 信号,至于这个按钮如何接收并处理鼠标点击事件,然后再怎么去发射这个信号的,我们不关心
- 但是当我们去重载按钮事件时,就需要关心事件处理机制了,这时可以根据具体场景的需要,改变鼠标的一些行为,比如现在是鼠标释放时触发 clicked 信号,我们可以修改其为在鼠标按下时触发 clicked 信号
7.4.2 常见的事件类型
常用的事件
- 键盘事件: 按钮按下和松开
- 鼠标事件: 鼠标指针移动、鼠标按键按下和松开
- 拖放事件: 用鼠标进行拖放
- 滚轮事件: 鼠标滚轮滚动
- 绘屏事件: 重绘屏幕的某些部分
- 定时事件: 定时器到时
- 焦点事件: 键盘焦点移动
- 进入和离开事件: 鼠标指针移入 Widget 内,或者移出
- 移动事件:Widget 的位置的改变
- 大小改变事件:Widget 的大小改变
- 显示和隐藏事件: Widget 显示或者隐藏
- 窗口事件: 窗口是否为当前窗口
- Socket 事件
- 剪切板事件
- 字体改变事件
- 布局改变事件
7.4.3 使用事件处理的方法
PyQt 提供了 5 种事件处理和过滤的方法(由弱到强)
- 重新实现事件函数
比如 mousePressEvent(),keyPressEvent(),paintEvent()----最常用且最常规的事件处理方法 - 重新实现(QObject.event)
一般用在 PyQt 没有提供该事件的处理函数的情况下,即增加新事件时 - 安装事件过滤器
### 1. 如果对QObject调用installEventFilter,那么就相当于是为这个QObject安装一个事件过滤器,对于QObject的全部事件来说,它们都会先传递到事件过滤函数eventFilter中 ### 2. 在这个过滤函数中,我们可以抛弃或者修改某些事件 ### 3. 比如可以对自己感兴趣的事件使用自定义的事件处理机制,对其他事件仍旧使用默认的事件处理机制 ### 4. 由于这种方法会对调用installEventFilter的所有事件进行过滤,因此如果要过滤的事件比较多就会降低程序的性能
- 在 QApplication 中安装事件过滤器
### 1. QApplication的事件过滤器将捕获所有QObject的所有事件,而且第一个获得该事件 ### 2. 也就是说,在将事件发送给其他任何一个事件过滤器之前(也就是第三种方法之前),都会先发送给QApplication的事件过滤器
- 重新实现 QApplication 的 notify()方法
### 1. PyQt使用notify()来分发事件 ### 2. 要想在任何事件处理器之前捕获该事件,唯一的方法就是重新实现QApplication的notify() ### 3. 在实际开发中,这种方法一般仅用在调试程序中
7.4.4 经典案例分析
import sys from PyQt5.QtWidgets import (QApplication, QMenu, QWidget) from PyQt5.QtCore import (QEvent, QTimer, Qt) from PyQt5.QtGui import QPainter class Widget(QWidget): def __init__(self, parent=None): super(Widget, self).__init__(parent) self.justDoubleClicked = False self.key = "" self.text = "" self.message = "" self.resize(400, 300) self.move(100, 100) self.setWindowTitle("Events") # 避免受窗口大小重绘事件的影响,可以将0改为3000(3秒),然后再去运行,就可以明白这行代码的意思了 QTimer().singleShot(3000, self.giveHelp) def giveHelp(self): self.text = "请点击这里触发追踪鼠标的功能" self.update() # 重绘事件,也就是触发paintEvent函数 # 重新实现关闭事件 def closeEvent(self, e): print("Closed") # 重新实现上下文菜单事件(默认是鼠标右键触发的这个响应) def contextMenuEvent(self, event): print("响应菜单事件的触发") menu = QMenu(self) oneAction = menu.addAction("&one") twoAction = menu.addAction("&two") oneAction.triggered.connect(self.One) twoAction.triggered.connect(self.Two) if not self.message: menu.addSeparator() threeAction = menu.addAction("&three") threeAction.triggered.connect(self.Three) # 使得菜单展示出来 menu.exec_(event.globalPos()) # 上下文菜单槽函数 def One(self): self.message = "Menu option One" self.update() def Two(self): self.message = "Menu option Two" self.update() def Three(self): self.message = "Menu option Three" self.update() def paintEvent(self, event): text = self.text i = text.find("\n\n") if i > 0: text = text[0:i] if self.key: # 若按下了键盘按键,则在信息文本中记录这个按键的信息 text += "\n\n你按下了:{}".format(self.key) painter = QPainter(self) # 给予文字提示 painter.setRenderHint(QPainter.TextAntialiasing) # 绘制信息文本到指定的矩形框中 painter.drawText(self.rect(), Qt.AlignCenter, text) if self.message: painter.drawText(self.rect(), Qt.AlignBottom | Qt.AlignHCenter, self.message) # 设置5秒后清空message并重绘界面 QTimer().singleShot(5000, self.clearMessage) QTimer().singleShot(5000, self.update) # 清空信息文本的槽函数 def clearMessage(self): self.message = "" # 重新实现调整窗口大小的事件 def resizeEvent(self, event): self.text = "调整窗口大小为: QSize({}, {})".format(event.size().width(), event.size().height()) self.update() # 重新实现鼠标释放事件 def mouseReleaseEvent(self, event): # 若为双击释放,则不跟鼠标移动 # 若为单击释放,则需要改变跟踪功能的状态,如果开启跟踪功能就跟踪,反之就不跟踪 if self.justDoubleClicked: self.justDoubleClicked = False else: # 这里在单击鼠标时对鼠标跟踪功能的状态设置进行反转 self.setMouseTracking(not self.hasMouseTracking()) if self.hasMouseTracking(): self.text = "开启鼠标跟踪功能.\n" + "请移动一下鼠标!\n" + "单击鼠标可以关闭这个功能" else: self.text = "关闭鼠标跟踪功能.\n" + "单击鼠标可以开启这个功能" self.update() # 重写鼠标移动事件(捕获到当前窗口内的鼠标移动后会自动触发该响应) # 只有开启鼠标跟踪功能后,才能捕捉到鼠标的移动,最后根据移动触发该事件的响应 def mouseMoveEvent(self, event): if not self.justDoubleClicked: # 将窗口坐标转换为屏幕坐标 globalPos = self.mapToGlobal(event.pos()) self.text = """鼠标位置: 窗口坐标:QPoint({},{}) 屏幕坐标:APoint({},{}) """.format(event.pos().x(), event.pos().y(), globalPos.x(),globalPos.y()) self.update() # 重新实现鼠标双击事件 def mouseDoubleClickEvent(self, event): self.justDoubleClicked = True self.text = " 你双击了鼠标" self.update() # 重新实现键盘按下事件 def keyPressEvent(self, event): self.key = "" if event.key() == Qt.Key_Home: self.key = "Home" elif event.key() == Qt.Key_End: self.key = "End" elif event.key() == Qt.Key_PageUp: print(event.modifiers()) print(event.text()) if event.modifiers() & Qt.ControlModifier: self.key = "Ctrl+PageUp" else: self.key = "PageUp" elif event.key() == Qt.Key_PageDown: print(event.modifiers()) print(event.text()) if event.modifiers() & Qt.ControlModifier: self.key = "Ctrl+PageDown" else: self.key = "PageDown" elif Qt.Key_A <= event.key() <= Qt.Key_Z: print(event.modifiers()) print(event.text()) print(event.key()) if event.modifiers() & Qt.ShiftModifier: self.key = "Shift+" self.key += event.text() if self.key: self.key = self.key self.update() else: # 将自己感兴趣的事件交给自定义的事件处理,将其他事件仍然交给默认的事件处理机制去处理,这里继承自QWidget,所以其他不感兴趣的事件就交给默认的QWidget中的keyPressEvent去处理 QWidget.keyPressEvent(self, event) # 重写一个新事件(event),适用于PyQt没有提供该事件的处理函数的情况,Tab键由于涉及焦点的切换,不会传递到keyPressEvent,因此需要重新去顶一个处理Tab按下触发的响应事件 def event(self, event): if (event.type() == QEvent.KeyPress and event.key() == Qt.Key_Tab): self.key = "在event()中捕获Tab键" self.update() return True return QWidget.event(self, event) if __name__ == '__main__': app = QApplication(sys.argv) win = Widget() win.show() sys.exit(app.exec_()) ### 解析 1. 建立text和message两个变量,使用paintEvent函数把它们输出到窗口中去 2. update函数的作用就是触发paintEvent事件的响应 3. 重写窗口关闭事件以及右键呼出菜单的上下文菜单响应事件 4. 上下文菜单事件主要影响的是message变量的结果,paintEvent负责将其输出显示在窗口底部 5. 绘制事件paintEvent的主要作用就是时刻跟踪text与message这两个变量的信息,分别绘制在中央和底部 6. 实现鼠标释放事件,双击释放则不跟宗鼠标的移动,若为单击释放,则需要改变跟踪功能的状态,如果开启跟踪状态就跟踪,反之不跟踪 7. 实现鼠标移动和双击事件,单击鼠标会触发鼠标跟踪功能状态的开关切换,只有在跟踪功能开启状态,窗口才能捕获到鼠标的移动进而触发鼠标移动事件的响应 8.self.mapToGlobal(event.pos())将鼠标相对于self的坐标转换为相对于屏幕的全局坐标 9. 对于第二种事件处理方法----event函数的重载 def event(self, event): if (event.type() == QEvent.KeyPress and event.key() == Qt.Key_Tab): self.key = "在event()中捕获Tab键" self.update() return True return QWidget.event(self, event) ### 解析event函数 + 对于窗口所有的事件都会传递给event函数,event函数会根据事件的类型,把事件分配给不同的响应函数进行处理 + 比如对于绘图事件,event会交给paintEvent函数进行处理 + 比如对于鼠标移动事件,event会交给mouseMoveEvent函数处理 + 对于键盘按下事件中的一个特例Tab键,event对其的处理是把焦点从当前窗口控件的位置切换到Tab键次序中的下一个窗口控件的位置,并返回True + 上述代码就重写了Tab键的响应处理逻辑
QObject 的事件过滤器
import sys from PyQt5.QtGui import * from PyQt5.QtWidgets import * from PyQt5.QtCore import * class EventFilter(QDialog): def __init__(self, parent = None): super(EventFilter, self).__init__(parent) self.setWindowTitle("事件过滤器") self.label1 = QLabel("请点击") self.label2 = QLabel("请点击") self.label3 = QLabel("请点击") self.LabelState = QLabel("test") self.image1 = QImage("images/cartoon1.ico") self.image2 = QImage("images/cartoon2.ico") self.image3 = QImage("images/cartoon3.ico") self.resize(600, 300) self.label1.installEventFilter(self) self.label2.installEventFilter(self) self.label3.installEventFilter(self) mainLayout = QGridLayout(self) mainLayout.addWidget(self.label1, 500, 0) mainLayout.addWidget(self.label2, 500, 1) mainLayout.addWidget(self.label3, 500, 2) mainLayout.addWidget(self.LabelState, 600, 1) self.setLayout(mainLayout) def eventFilter(self, watched, event): if watched == self.label1: # 只对label1的点击事件进行过滤,重写其行为,其他事件会被忽略 if event.type() == QEvent.MouseButtonPress: # 这里重写鼠标按下事件 mouseEvent = QMouseEvent(event) if mouseEvent.buttons() == Qt.LeftButton: self.LabelState.setText("按下鼠标左键") elif mouseEvent.buttons() == Qt.MidButton: self.LabelState.setText("按下鼠标中键") elif mouseEvent.buttons() == Qt.RightButton: self.LabelState.setText("按下鼠标右键") # 转换图片大小 transform = QTransform() transform.scale(0.5, 0.5) tmp = self.image1.transformed(transform) self.label1.setPixmap(QPixmap.fromImage(tmp)) if event.type() == QEvent.MouseButtonRelease: self.LabelState.setText("释放鼠标按键") self.label1.setPixmap(QPixmap.fromImage(self.image1)) # 对于其他事件的触发情况,会返回给系统默认的事件处理方法 return QDialog.eventFilter(self, watched, event) if __name__ == '__main__': app = QApplication(sys.argv) win = EventFilter() win.show() sys.exit(app.exec_()) ### 解析 1. 对要过滤的控件设置installEventFilter,设置好的控件的所有事件都会被eventFilter函数接收并处理 2. 针对指定控件的指定事件过滤时,切记不要忘记将其他事件交还给系统默认的处理机制
App 的事件过滤器
import sys from PyQt5.QtGui import * from PyQt5.QtWidgets import * from PyQt5.QtCore import * class EventFilter(QDialog): def __init__(self, parent = None): super(EventFilter, self).__init__(parent) self.setWindowTitle("事件过滤器") self.label1 = QLabel("请点击") self.label2 = QLabel("请点击") self.label3 = QLabel("请点击") self.LabelState = QLabel("test") self.image1 = QImage("images/cartoon1.ico") self.image2 = QImage("images/cartoon2.ico") self.image3 = QImage("images/cartoon3.ico") self.resize(600, 300) # self.label1.installEventFilter(self) # self.label2.installEventFilter(self) # self.label3.installEventFilter(self) mainLayout = QGridLayout(self) mainLayout.addWidget(self.label1, 500, 0) mainLayout.addWidget(self.label2, 500, 1) mainLayout.addWidget(self.label3, 500, 2) mainLayout.addWidget(self.LabelState, 600, 1) self.setLayout(mainLayout) def eventFilter(self, watched, event): print(type(watched)) if watched == self.label1: # 只对label1的点击事件进行过滤,重写其行为,其他事件会被忽略 if event.type() == QEvent.MouseButtonPress: # 这里重写鼠标按下事件 mouseEvent = QMouseEvent(event) if mouseEvent.buttons() == Qt.LeftButton: self.LabelState.setText("按下鼠标左键") elif mouseEvent.buttons() == Qt.MidButton: self.LabelState.setText("按下鼠标中键") elif mouseEvent.buttons() == Qt.RightButton: self.LabelState.setText("按下鼠标右键") # 转换图片大小 transform = QTransform() transform.scale(0.5, 0.5) tmp = self.image1.transformed(transform) self.label1.setPixmap(QPixmap.fromImage(tmp)) if event.type() == QEvent.MouseButtonRelease: self.LabelState.setText("释放鼠标按键") self.label1.setPixmap(QPixmap.fromImage(self.image1)) # 对于其他事件的触发情况,会返回给系统默认的事件处理方法 return QDialog.eventFilter(self, watched, event) if __name__ == '__main__': app = QApplication(sys.argv) win = EventFilter() # 给App添加上事件过滤器,重写win窗口对象上全部控件的全部事件的过滤函数 app.installEventFilter(win) win.show() sys.exit(app.exec_()) ### 解析 1. 事件先交由事件过滤器进行过滤 2. 过滤完成之后,事件才会被交由事件处理器进行相应的逻辑处理
7.5 窗口数据的传递
- 单窗口---各个控件之间传递数据(信号与槽机制)
- 多窗口---各个窗口之间传递数据
- 主窗口获取子窗口中控件的属性
- 通过高级的信号与槽机制(子窗口通过信号发送数据,主窗口通过槽函数接收数据)
7.5.1 单一窗口下的数据传递
单窗口控件之间的数据传递
import sys from PyQt5.QtGui import * from PyQt5.QtWidgets import * from PyQt5.QtCore import * class WinForm(QWidget): def __init__(self, parent = None): super(WinForm, self).__init__(parent) self.initUI() def initUI(self): # 先创建滑块和LCD转换 lcd = QLCDNumber(self) slider = QSlider(Qt.Horizontal, self) vBox = QVBoxLayout() vBox.addWidget(lcd) vBox.addWidget(slider) self.setLayout(vBox) # valueChanged() 是QSlider的一个信号函数,只要slider的值发生改变,它就会发射一个信号,然后通过connect连接信号的接收控件,也就是lcd slider.valueChanged.connect(lcd.display) self.setGeometry(300, 300, 350, 150) self.setWindowTitle("信号与槽函数:连接滑块LCD") if __name__ == '__main__': app = QApplication(sys.argv) win = WinForm() win.show() sys.exit(app.exec_()) ### 解析 1. 首先是创建滑块和LCD 2. 通过QSlider控件的valueChanged()信号函数连接到LCD面板控件的display槽函数
7.5.2 多窗口数据传递:调用属性
应用场景在主窗口添加一个按钮,点击后调用一个对话框,在对话框中进行参数的选择,关闭对话框后选择的参数随之返回给主窗口
小技巧其实这个场景很符合一种模式:主窗口进行 UI 的更新显示,子窗口进行数据的逻辑处理并返回相应的值给主窗口
import sys from PyQt5.QtGui import * from PyQt5.QtWidgets import * from PyQt5.QtCore import * class DateDialog(QDialog): def __init__(self, parent=None): super(DateDialog, self).__init__(parent) self.setWindowTitle("DateDialog") # 在布局中添加控件 layout = QVBoxLayout(self) self.datetime = QDateTimeEdit(self) self.datetime.setCalendarPopup(True) self.datetime.setDateTime(QDateTime.currentDateTime()) layout.addWidget(self.datetime) # 使用两个按钮分别连接accept和reject槽函数 buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, Qt.Horizontal, self) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) layout.addWidget(buttons) # 从对话框中获取当前的日期时间 def dateTime(self): return self.datetime.dateTime() # 使用静态方法创建对话框并返回(date,time,accepted) @staticmethod def getDateTime(parent=None): dialog = DateDialog(parent) result = dialog.exec_() date = dialog.dateTime() return (date.date(), date.time(), result == QDialog.Accepted) class WinForm(QWidget): def __init__(self, parent=None): super(WinForm, self).__init__(parent) self.resize(400, 90) self.setWindowTitle("对话框关闭时返回值给主窗口例子") self.lineEdit1 = QLineEdit(self) self.button1 = QPushButton("弹出对话框1") self.button2 = QPushButton("弹出对话框2") self.button1.clicked.connect(self.onButton1Click) self.button2.clicked.connect(self.onButton2Click) gridLayout = QGridLayout(self) gridLayout.addWidget(self.lineEdit1) gridLayout.addWidget(self.button1) gridLayout.addWidget(self.button2) self.setLayout(gridLayout) def onButton1Click(self): dialog = DateDialog(self) result = dialog.exec_() date = dialog.dateTime() self.lineEdit1.setText(date.date().toString()) print('\n日期时间对话框的返回值') print('date=%s' % str(date.date())) print('time=%s' % str(date.time())) print('result=%s' % result) dialog.destroy() def onButton2Click(self): date, time, result = DateDialog.getDateTime(self) self.lineEdit1.setText(date.toString()) print('\n日期时间对话框的返回值') print('date=%s' % str(date)) print('time=%s' % str(time)) print('result=%s' % result) if __name__ == '__main__': app = QApplication(sys.argv) form = WinForm() form.show() sys.exit(app.exec_()) ### 解析 1. 使用两个按钮的ok和cancel分别连接accept()和reject()槽函数 2. 要么在主窗口内实例化子窗口对象再去调用其中的方法,要么直接在子窗口中封装一个静态方法,可以返回主窗口想要的数据,这样直接在主窗口中通过子类名调用静态方法
7.5.3 多窗口的数据传递: 信号与槽
- 对于多窗口的数据传递,一般是通过子窗口发射信号,主窗口通过槽函数捕获这个信号,然后获取信号内的数据
- 子窗口发射信号有两种
- 内置的信号(传递的数据类型是特定的)
- 自定义信号(可以传递任意 Python 类型的数据)
import sys from PyQt5.QtGui import * from PyQt5.QtWidgets import * from PyQt5.QtCore import * class DateDialog(QDialog): Signal_OneParameter = pyqtSignal(str) def __init__(self, parent=None): super(DateDialog, self).__init__(parent) self.setWindowTitle("子窗口:用来发射信号") # 在布局中添加控件 layout = QVBoxLayout(self) self.label = QLabel(self) self.label.setText("前者发射内置信号\n后者发射自定义信号") self.datetime_inner = QDateTimeEdit(self) self.datetime_inner.setCalendarPopup(True) self.datetime_inner.setDateTime(QDateTime.currentDateTime()) self.datetime_emit = QDateTimeEdit(self) self.datetime_emit.setCalendarPopup(True) self.datetime_emit.setDateTime(QDateTime.currentDateTime()) layout.addWidget(self.label) layout.addWidget(self.datetime_emit) layout.addWidget(self.datetime_inner) # 使用两个button来分别连接accept和reject函数 buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, Qt.Horizontal, self) # 这里将用户对buttons状态的输入传递给当前的日期时间对话框来处理,以保证数据 buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) layout.addWidget(buttons) self.datetime_emit.dateTimeChanged.connect(self.emit_signal) def emit_signal(self, date): date_str = date.toString() self.Signal_OneParameter.emit(date_str) class WinForm(QWidget): def __init__(self, parent=None): super(WinForm, self).__init__(parent) self.resize(400, 90) self.setWindowTitle("信号与槽函数实现多窗口之间的数据传递") self.open_btn = QPushButton("获取时间") self.lineEdit_inner = QLineEdit(self) self.lineEdit_emit = QLineEdit(self) self.open_btn.clicked.connect(self.openDialog) self.lineEdit_inner.setText("接收子窗口内置信号的时间") self.lineEdit_emit.setText("接收子窗口自定义信号的时间") grid = QGridLayout() grid.addWidget(self.lineEdit_inner) grid.addWidget(self.lineEdit_emit) grid.addWidget(self.open_btn) self.setLayout(grid) def openDialog(self): dialog = DateDialog(self) # 连接子窗口的内置信号与主窗口的槽函数 dialog.datetime_inner.dateTimeChanged.connect(self.deal_inner_slot) # 连接子窗口中自定义信号与主窗口的槽函数 dialog.Signal_OneParameter.connect(self.deal_emit_slot) dialog.show() # 这个是在日期时间对话框中数据变化后就触发绑定的槽函数,然后直接就修改了lineEdit的文本值 def deal_inner_slot(self, date): self.lineEdit_inner.setText(date.toString()) def deal_emit_slot(self, date_str): self.lineEdit_emit.setText(date_str) if __name__ == '__main__': app = QApplication(sys.argv) win = WinForm() win.show() sys.exit(app.exec_())
第八章 PyQt5 图形和特效
8.1 窗口风格
8.1.1 设置窗口风格
- 可以为每一个 widget 都设置风格
setStyle(QStyle style)
- 获取当前平台支持的原有的 QStyle 样式
QStyleFactory.keys()
- 对 QApplication 设置 QStyle 样式
QApplication.setStyle(QStyleFactory.create('Windows10'))
注意如果其他的 Widget 没有设置 QStyle,则默认使用 QApplication 设置的 QStyle
案例 8-1 设置窗口风格
import sys from PyQt5.QtGui import * from PyQt5.QtWidgets import * from PyQt5.QtCore import * class WinForm(QWidget): def __init__(self, parent=None): super(WinForm, self).__init__(parent) horizontalLayout = QHBoxLayout() self.styleLabel = QLabel(" Set Style:") self.styleComboBox = QComboBox() # 从QStyleFactory中增加多个显示样式 self.styleComboBox.addItems(QStyleFactory.keys()) print(self.styleLabel.objectName()) # 选择当前的窗口风格 index = self.styleComboBox.findText(QApplication.style().objectName(), Qt.MatchFixedString) # 设置当前的窗口风格 self.styleComboBox.setCurrentIndex(index) # 通过comboBox来选择窗口风格 self.styleComboBox.activated[str].connect(self.handleStyleChanged) horizontalLayout.addWidget(self.styleLabel) horizontalLayout.addWidget(self.styleComboBox) self.setLayout(horizontalLayout) # 改变窗口风格 def handleStyleChanged(self, style): QApplication.setStyle(style) if __name__ == '__main__': app = QApplication(sys.argv) win = WinForm() win.show() sys.exit(app.exec_())
8.1.2 设置窗口样式
Pyqt 使用 setWindowFlags(Qt.WindowFlags)函数设置窗口的样式
- PyQt 中的基本窗口类型
- Qt.Widget 默认窗口,有最大最小化,关闭按钮
- Qt.Window 普通窗口,有最大最小化,关闭按钮
- Qt.Dialog 对话框窗口,有问号和关闭按钮
- Qt.Popup 弹出窗口,窗口无边框
- Qt.ToolTip 提示窗口,窗口无边框,无任务栏
- Qt.SplashScreen 闪屏,窗口无边框,无任务栏
- Qt.SubWindow 子窗口,窗口无按钮,但是有标题
- 自定义顶层窗口的外观标志
- Qt.MSWindowFixedSizeDialogHint # 窗口无法调整大小
- Qt.FramelessWindowHint # 窗口无边框
- Qt.CustomizeWindowHint # 有边框但无标题和按钮,不能移动和拖动
- Qt.WindowTitleHint # 添加标题栏和一个关闭按钮
- Qt.WindowSystemMenuHint # 添加一个系统目录和关闭按钮
- Qt.WindowMaximizeButtonHint # 激活最大化和关闭按钮,禁止最小化
- Qt.WindowMinimizeButtonHint # 激活最小化和关闭按钮,禁止最大化
- Qt.WindowMinMaxButtonHint # 激活最大最小化和关闭按钮
- Qt.WindowCloseButtonHint # 添加一个关闭按钮
- Qt.WindowContextHelpButtonHint # 添加问号和关闭按钮,像对话框窗口那样
- Qt.WindowStaysOnTopHint # 窗口始终处于顶层位置
- Qt.WindowStaysOnBottomHint # 窗口始终处于底层位置
案例 8.1.2 设置窗口样式
import sys from PyQt5.QtGui import * from PyQt5.QtWidgets import * from PyQt5.QtCore import * class WinForm(QWidget): def __init__(self, parent=None): super(WinForm, self).__init__(parent) self.resize(400, 200) self.setWindowTitle("案例8.1.2 设置窗口样式") # 设置无边框的窗口样式 self.setWindowFlags(Qt.FramelessWindowHint) if __name__ == '__main__': app = QApplication(sys.argv) win = WinForm() win.show() sys.exit(app.exec_())
8.1.3 使用自定义的无边框窗口
- 设置窗口标志,实现无边框
# 设置无边框的窗口样式 self.setWindowFlags(Qt.FramelessWindowHint)
- 覆盖实现最大化函数(重写 showMaximized 函数)
### 思路是:使用QDeskWidget类的availableGeometry()函数 # 得到桌面控件 desktop = QApplication.desktop() # rect = desktop.availableGeometry()
- 设置窗口的显示大小为屏幕的最大显示大小
# 设置窗口尺寸 self.setGeometry(rect) # 显示窗口 self.show()
案例 8.1.3 使用自定义的无边框窗口
import sys from PyQt5.QtGui import * from PyQt5.QtWidgets import * from PyQt5.QtCore import * class WinForm(QMainWindow): '''自定义窗口类''' def __init__(self, parent=None): '''构造函数''' # 调用父类的构造函数 super(WinForm, self).__init__(parent) # 设置窗口标志(无边框) self.setWindowFlags(Qt.FramelessWindowHint) # 为便于显示,设置窗口的背景颜色(QCSS) self.setStyleSheet('''background-color: blue;''') def showMaximized(self): '''最大化窗口''' # 得到桌面控件 desktop = QApplication.desktop() # 得到屏幕最大显示尺寸 rect = desktop.availableGeometry() # 设置rect大小 self.setGeometry(rect) # 显示窗口 self.show() if __name__ == '__main__': app = QApplication(sys.argv) win = WinForm() win.showMaximized() sys.exit(app.exec_())
8.2 绘图
8.2.1 图像类
常用的图像类
- QPixmap
- 专门为绘图设计的,在绘图中需要使用 QPixmap
- QImage
- 提供了一个与硬件无关的图像表示函数,可以用于图片的像素级访问
- QPicture
- 一个绘图设备类,它继承自 QPainter 类,可以使用 QPaintrt 的 begin()函数在 QPicture 上绘图,使用 end()函数结束绘图,使用 QPainter 的 save()函数将 QPainter 所使用过的绘图指令保存到文件中
- QBitmap
- 一个继承自 QPixmap 的简单类,它提供了 1bit 深度的二值图像的类
- QBitmap 提供的单色图像,可以用来制作游标(Cursor)或者笔刷(QBrush)
图像类的继承关系
- (QImage/QPicture/QPixmap/QWidge)->QPaintDevice
- QBitmap->QPixmap
8.2.2 简单绘图
import sys from PyQt5.QtGui import * from PyQt5.QtWidgets import * from PyQt5.QtCore import * class WinForm(QWidget): def __init__(self, parent=None): super(WinForm, self).__init__(parent) self.setWindowTitle("8.2.2 简单绘图") # 1 self.lastPoint = QPoint() self.endPoint = QPoint() self.initUI() def initUI(self): # 设置窗口大小为650*500 self.resize(600, 500) # 设置画布大小为400*400 背景色为白色 self.pix = QPixmap(600, 500) self.pix.fill(Qt.white) # 2 def paintEvent(self, e): pp = QPainter(self.pix) # 根据鼠标指针前后两个位置绘制直线 pp.drawLine(self.lastPoint, self.endPoint) # 让前一个坐标值等于后一个坐标值,就能画出连续的线 self.lastPoint = self.endPoint painter = QPainter(self) painter.drawPixmap(0, 0, self.pix) # 3 def mousePressEvent(self, event): # 按下鼠标左键 if event.button() == Qt.LeftButton: self.lastPoint = event.pos() # 4 def mouseMoveEvent(self, event): # 然后移动鼠标指针 if event.buttons() and Qt.LeftButton: self.endPoint = event.pos() # 重新绘制 触发paintEvent self.update() # 5 def mouseReleaseEvent(self, event): # 释放鼠标左键 if event.button() == Qt.LeftButton: self.endPoint = event.pos() # 进行重新绘制 self.update() if __name__ == '__main__': app = QApplication(sys.argv) win = WinForm() win.show() sys.exit(app.exec_()) ### 解析 1. 初始化代码 2. 重构paintEvent函数 3. 重构mousePressEvent函数,使用两个点来绘制线条,这两个点从鼠标事件中获取 4. 重构mouseMoveEvent函数,当鼠标左键按下时获取开始点,每次绘制之后都让结束点和开始点重合,这样确保画出连续的线 5. 重构mouseReleaseEvent函数,当鼠标移动时获取结束点并更新绘制
8.2.3 双缓冲绘图
案例 8-2 绘制矩形,出现矩形
import sys from PyQt5.QtGui import * from PyQt5.QtWidgets import * from PyQt5.QtCore import * class WinForm(QWidget): def __init__(self, parent=None): super(WinForm, self).__init__(parent) self.setWindowTitle("案例8-2 绘制矩形,出现矩形") self.lastPoint = QPoint() self.endPoint = QPoint() self.initUI() def initUI(self): # 设置窗口大小为600*500 self.resize(600, 500) # 设置画布大小为400*400, 背景色白色 self.pix = QPixmap(400, 400) self.pix.fill(Qt.white) # 1 def paintEvent(self, event): pp = QPainter(self.pix) x = self.lastPoint.x() y = self.lastPoint.y() w = self.endPoint.x() - x h = self.endPoint.y() - y pp.drawRect(x, y, w, h) painter = QPainter(self) painter.drawPixmap(0, 0, self.pix) def mousePressEvent(self, event): # 按下鼠标左键 if event.button() == Qt.LeftButton: self.lastPoint = event.pos() def mouseMoveEvent(self, event): # 然后移动鼠标指针 if event.buttons() and Qt.LeftButton: self.endPoint = event.pos() # 重新绘制 self.update() def mouseReleaseEvent(self, event): # 释放鼠标左键 if event.button() == Qt.LeftButton: self.endPoint = event.pos() # 重新绘制 self.update() if __name__ == '__main__': app = QApplication(sys.argv) win = WinForm() win.show() sys.exit(app.exec_()) ### 解析 1. 会出现重影是因为在鼠标移动事件中会不断根据移动来重绘窗口 2. 使用双缓冲技术来绘制一个正宗的矩形
案例 8-3 使用双缓冲技术绘制矩形,避免出现重影
import sys from PyQt5.QtGui import * from PyQt5.QtWidgets import * from PyQt5.QtCore import * class WinForm(QWidget): def __init__(self, parent=None): super(WinForm, self).__init__(parent) self.setWindowTitle("案例8-3 使用双缓冲技术绘制矩形,避免出现重影") self.lastPoint = QPoint() self.endPoint = QPoint() # 辅助画布 self.tmpPix = QPixmap() # 标志是否正在绘图 self.isDrawing = False self.initUI() def initUI(self): # 设置窗口大小为600*500 self.resize(600, 500) # 设置画布大小为400*400, 背景色白色 self.pix = QPixmap(400, 400) self.pix.fill(Qt.white) # 1 def paintEvent(self, event): painter = QPainter(self) x = self.lastPoint.x() y = self.lastPoint.y() w = self.endPoint.x() - x h = self.endPoint.y() - y if self.isDrawing: # 将以前的pix中的内容赋值到tmpPix中,保证以前的内容不消失 self.tmpPix = self.pix pp = QPainter(self.tmpPix) pp.drawRect(x, y, w, h) painter.drawPixmap(0, 0, self.tmpPix) else: pp = QPainter(self.pix) pp.drawRect(x, y, w, h) painter.drawPixmap(0, 0, self.pix) def mousePressEvent(self, event): # 按下鼠标左键 if event.button() == Qt.LeftButton: self.lastPoint = event.pos() self.isDrawing = True def mouseReleaseEvent(self, event): # 释放鼠标左键 if event.button() == Qt.LeftButton: self.endPoint = event.pos() # 重新绘制 self.update() self.isDrawing = False if __name__ == '__main__': app = QApplication(sys.argv) win = WinForm() win.show() sys.exit(app.exec_()) ### 解析 1. 利用双缓冲过技术解决在界面上绘制矩形带来的重影问题 2. 疑惑?其实上述的案例需要双缓冲两个pixmap对象来操作,直接在鼠标左键按下时捕获开始点,在释放时捕获结束点就可以绘制矩形(期间只刷新过一次界面就是在鼠标左键释放时)
8.3 QSS 的 UI 美化
概念 QSS(Qt Style Sheets)即 Qt 样式表
优缺点
- QSS 大量参考的是 CSS,但是功能比 CSS 要弱很多,体现为选择器少
- 可以使用的 QSS 属性也少,并且不是所有的属性都可以应用在 PyQt 控件上
- QSS 可以使界面美化跟业务逻辑代码层分开,利于维护
8.3.1 QSS 的语法规则
- QSS 语法跟 CSS 相同,样式包括两部分
- 选择器指定哪些控件受到影响
- 声明,指定哪些属性应该被设置
- 声明是很多个"属性:值"对,使用(;)隔开,使用大括号({})将所有的声明放在一起
import sys from PyQt5.QtGui import * from PyQt5.QtWidgets import * from PyQt5.QtCore import * class WinForm(QWidget): def __init__(self, parent=None): super(WinForm, self).__init__(parent) btn1 = QPushButton(self) btn1.setText("按钮1") btn2 = QPushButton(self) btn2.setText("按钮2") vbox = QVBoxLayout() vbox.addWidget(btn1) vbox.addWidget(btn2) self.setLayout(vbox) self.setWindowTitle("QSS样式") if __name__ == '__main__': app = QApplication(sys.argv) win = WinForm() qssStyle = ''' QPushButton { color: red } ''' win.setStyleSheet(qssStyle) win.show() sys.exit(app.exec_()) ### 解析 1. 表示设置QPushButton类及其子类的所有实例的前景色是红色 2. QPushButton及其子类就是选择器 3. {color: red}就是声明的集合
8.3.2 QSS 选择器类型
- QSS 选择器类型如下
- 通配符选择器: * , 匹配全部控件
- 类型选择器: QPushButton,匹配所有 QPushButton 以及其子类的实例
- 属性选择器:QPushButton[name="myBtn"],匹配所有的 name 属性是 myBtn 的 QPushButton 实例(注意:这里的 name 属性不一定是类本身具有的,也可以是自定义的属性)
import sys from PyQt5.QtGui import * from PyQt5.QtWidgets import * from PyQt5.QtCore import * class WinForm(QWidget): def __init__(self, parent=None): super(WinForm, self).__init__(parent) btn1 = QPushButton(self) btn1.setProperty('name', 'myBtn') btn1.setText("按钮1") btn2 = QPushButton(self) btn2.setText("按钮2") vbox = QVBoxLayout() vbox.addWidget(btn1) vbox.addWidget(btn2) self.setLayout(vbox) self.setWindowTitle("QSS样式") if __name__ == '__main__': app = QApplication(sys.argv) win = WinForm() qssStyle = ''' QPushButton[name="myBtn"] { color: red } ''' win.setStyleSheet(qssStyle) win.show() sys.exit(app.exec_())
- 类选择器 .QPushButton 匹配全部的 QPushButton 实例,不包括其子类,注意前面有一个.(点号)
- ID 选择器:#myBtn 匹配全部的 ID 为 myBtn 的控件,这里的 ID 实际上就是 objectName 指定的值(是具有唯一性的)
- 后代选择器: QDialog QPushButton 匹配全部的 QDialog 容器下的 QPushButton 类及其子类的实例(子孙后代,直接间接都会影响)
- 子选择器: QDialog > QPushButton 匹配全部的 QDialog 容器中包含的 QPushButton,这里必须是直接子代才会受到影响
8.3.3 QSS 子控件
- QSS 子控件实际上也是一种选择器,其应用在一些复合控件上,典型的是如 QComboBox,该控件的外观是,有一个矩形的外边框,右边有一个下拉箭头,点击之后会弹出下拉列表
QComboBox::drop-down {image: url(dropdown.png)} ### 解析 1. 表示QComboBox的下拉箭头的图片是自定义的 2. ::drop-down子控件选择器可以与上面提到的选择器混合使用 QComboBox#myComboBox::drop-down {image: url(dropdown.png)} # 表示为指定ID为myComboBox的QComboBox控件的下拉箭头自定义图片
案例-QSS 子控件之 QComboBox 的下拉子控件指定样式
import sys from PyQt5.QtGui import * from PyQt5.QtWidgets import * from PyQt5.QtCore import * class WinForm(QWidget): def __init__(self, parent=None): super(WinForm, self).__init__(parent) self.initUI() def initUI(self): combo = QComboBox(self) combo.setObjectName("myComboBox") combo.addItems(['Window', 'Ubuntu', 'Red Hat']) combo.move(50, 50) self.setGeometry(250, 200, 320, 150) self.setWindowTitle("案例-QSS子控件之QComboBox的下拉子控件指定样式") if __name__ == '__main__': app = QApplication(sys.argv) win = WinForm() qssStyle = ''' QComboBox#myComboBox::drop-down { image : url(images/dropdown.png); } ''' win.setStyleSheet(qssStyle) win.show() sys.exit(app.exec_())
8.3.4 QSS 伪状态
概念 QSS 伪状态选择器是以冒号开头的一个选择表达式,例如:hover,表示当鼠标指针经过时的状态
- 伪状态选择器限制了当控件处于某种状态时才可以使用 QSS 规则
- 伪状态只能描述一个控件或者一个复合控件的子控件的状态,所以它只能放在选择器的最后面
- 可以使用感叹号(!)表示状态,例如:!hover 表示的是当鼠标没有经过时的状态
- 多种伪状态可以同时使用,例如:
QCheckBox:hover:checked {color: red} # 表示的是当鼠标经过QCheckBox时,那些选中的前景色会变成red红色
8.3.5 QDarkStyleSheet
- 除了自己写样式表,还可以去网上荡一些成熟且质量很高的 QSS 样式表,比如 QDarkStyleSheet,它是一种用于 PyQt 应用程序的深黑色样式表
样式表下载地址 - 使用方法,直接下载下来解压后放在项目下,然后手动去引用
- 或者使用 pip 安装到整个环境中去,这样写任何项目时都可以使用了(推荐)
pip install qdarkstyle
案例-8.3.5 QDarkStyleSheet 主题样式表
import sys from PyQt5.QtGui import * from PyQt5.QtWidgets import * from PyQt5.QtCore import * import qdarkstyle class WinForm(QWidget): def __init__(self, parent=None): super(WinForm, self).__init__(parent) self.initUI() def initUI(self): combo = QComboBox(self) combo.setObjectName("myComboBox") combo.addItems(['Window', 'Ubuntu', 'Red Hat']) combo.move(50, 50) self.setGeometry(250, 200, 320, 150) self.setWindowTitle("案例-QSS子控件之QComboBox的下拉子控件指定样式") if __name__ == '__main__': app = QApplication(sys.argv) win = WinForm() qssStyle = ''' QComboBox#myComboBox::drop-down { image : url(images/dropdown.png); } ''' app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5()) win.show() sys.exit(app.exec_()) ### 解析 1. 需要导入qdarkstyle模块 2. 需要使用app.setStyleSheet()加载qdarkstyle的样式表 app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5())
8.4 设置窗口背景
概念背景包括背景色和背景图片
- 设置窗口背景主要有三种方法
- 使用 QSS 设置窗口背景
- 使用 QPalette 设置窗口背景
- 重写 paintEvent,使用 QPainter 绘制背景
8.4.1 使用 QSS 设置窗口背景
- 在 QSS 中,设置背景
- background
- background-color
- 设置窗口背景色之后,子控件默认会继承父窗口的背景色
- 可以使用 setPixmap 来为控件设置背景图片
- 可以使用 setIcon 来为控件设置图标
- 使用 setStyleSheet 设置窗口背景图片以及背景色
import sys from PyQt5.QtGui import * from PyQt5.QtWidgets import * from PyQt5.QtCore import * class WinForm(QMainWindow): def __init__(self, parent=None): super(WinForm, self).__init__(parent) self.setObjectName("MainWindow") ### 设置背景图片 # self.setStyleSheet("#MainWindow{border-image:url(./images/python.jpg)}") ### 设置背景色 self.setStyleSheet("#MainWindow{background-color:yellow}") if __name__ == '__main__': app = QApplication(sys.argv) win = WinForm() win.show() sys.exit(app.exec_())
8.4.2 使用 QPalette 设置窗口背景以及背景色---调色板
- 当使用 palette(调色板)来设置背景图片时,需要考虑背景图片的尺寸(右键图片---> 属性--> 分辨率)
- 当背景图片的宽度和高度大于窗口的宽高时,背景图片将会平铺整个背景
-
小于 ,则会加载多个背景图片
import sys from PyQt5.QtGui import * from PyQt5.QtWidgets import * from PyQt5.QtCore import * class WinForm(QMainWindow): def __init__(self, parent=None): super(WinForm, self).__init__(parent) self.setWindowTitle('8.4.2 使用QPalette设置窗口背景以及背景色---调色板') palette = QPalette() ### 设置背景色 # palette.setColor(QPalette.Background, Qt.red) ### 设置背景图片 478*260 palette.setBrush(QPalette.Background,QBrush(QPixmap('./images/python.jpg'))) self.setPalette(palette) ### 当图片尺寸大于窗口时,直接平铺 # self.resize(460,255) ### 小于时,加载多个图片 self.resize(800, 600) if __name__ == '__main__': app = QApplication(sys.argv) win = WinForm() win.show() sys.exit(app.exec_())
8.4.3 使用 paintEvent 设置窗口背景图片以及背景色
import sys from PyQt5.QtGui import * from PyQt5.QtWidgets import * from PyQt5.QtCore import * class WinForm(QWidget): def __init__(self, parent=None): super(WinForm, self).__init__(parent) self.setWindowTitle('8.4.3 使用paintEvent设置窗口背景图片以及背景色') def paintEvent(self, e): painter = QPainter(self) # ### 设置背景色 # painter.setBrush(Qt.black) # painter.drawRect(self.rect()) ### 设置背景图片 平铺整个窗口,随着窗口的改变而改变 pixmap = QPixmap('./images/screen1.jpg') painter.drawPixmap(self.rect(), pixmap) if __name__ == '__main__': app = QApplication(sys.argv) win = WinForm() win.show() sys.exit(app.exec_())
8.5 不规则窗口的显示 1. QWidget 类中比较重要的绘图函数 + setMask(self, QBitmap/QRegion)
- setMask()的作用是为了调用它的控件增加一个遮罩
- 遮住所选区域以外部分,使之看起来是透明的
- 它的参数可以为 QBitmap 或者 QRegion 对象
- 此处调用 QPixmap 的 mask()函数获得图片自身的遮罩,是一个 QBitmap 对象
- 在实例中使用的是 PNG 图片,它的透明部分实际上就是一个遮罩
- paintEvent(self, QPaintEvnet)
- 通过重载 paintEvent()函数绘制窗口背景
- 实现不规则窗口的最简单方式:
- 图片素材既当遮罩层又当背景图片
- 通过重载 paintEvent()函数来绘制窗口背景
def paintEvent(self, e): painter = QPainter(self) painter.drawPixmap(0,0,280,390, QPixmap('./images/dog.jpg')) painter.drawPixmap(300,0,280,390, QBitmap('./images/dog.jpg'))
- 使用一张遮罩层图片来控制窗口的大小,再利用 paintEvent()函数重绘窗口的背景图
def __init__(self, parent=None): super(WinForm, self).__init__(parent) self.setWindowTitle('8.5 不规则窗口的实现示例') self.pix = QBitmap('./images/mask.png') self.resize(self.pix.size()) self.setMask(self.pix) def paintEvent(self, e): painter = QPainter(self) ### 在指定区域直接绘制窗口背景 painter.drawPixmap(0,0,self.pix.width(), self.pix.height(), QPixmap('./images/screen1.jpg'))
- 实现不规则窗口的可拖动
import sys from PyQt5.QtGui import * from PyQt5.QtWidgets import * from PyQt5.QtCore import * class WinForm(QWidget): def __init__(self, parent=None): super(WinForm, self).__init__(parent) self.setWindowTitle('8.5 不规则窗口的实现示例') self.mypix() def mypix(self): self.mypic = './images/boy.png' self.pix = QPixmap(self.mypic, "0", Qt.AvoidDither | Qt.ThresholdAlphaDither | Qt.ThresholdDither) self.resize(self.pix.size()) self.setMask(self.pix.mask()) self.dragPosition = None ### 重定义鼠标按下响应函数 mousePressEvent(QMouseEvent)和鼠标指针移动响应函数 ### mouseMoveEvent(QMouseEvent), 使得不规则窗口能响应鼠标事件,实现随意拖动窗口 # def mousePressEvent(self, evnet: QMouseEvent): if evnet.button() == Qt.LeftButton: self.m_drag = True self.m_DragPosition = evnet.globalPos() - self.pos() evnet.accept() self.setCursor(QCursor(Qt.OpenHandCursor)) def mouseMoveEvent(self, evnet: QMouseEvent): if Qt.LeftButton and self.m_drag: ### 当使用左键移动窗口时修改偏移值 self.move(evnet.globalPos() - self.m_DragPosition) evnet.accept() def mouseReleaseEvent(self, evnet: QMouseEvent): self.m_drag = False self.setCursor(QCursor(Qt.ArrowCursor)) def mouseDoubleClickEvent(self, event:QMouseEvent): self.mypix() ### 在窗口首次绘制时,会加载paintEvent()函数 def paintEvent(self, e): painter = QPainter(self) painter.drawPixmap(0,0,self.pix.width(), self.pix.height(), self.pix) if __name__ == '__main__': app = QApplication(sys.argv) win = WinForm() win.show() sys.exit(app.exec_())
8.5.1 不规则窗口实现动画效果
- 还可以使用 PyQt 设计不规则窗口的动画效果
- pixmap.setMask()函数的作用是为调用它的控件增加一个遮罩
- 遮住所选区域以外的部分,使控件看起来是透明的
- 参数可以是一个 QBitmap 对象或一个 QRegion 对象
- 本例中调用 QPixmap 实例的 self.pix.mask 来获取图片自身的遮罩(这个遮罩是一个 QBitmap)
self.pix = QPixmap(self.mypic[self.i], "0", Qt.AvoidDither | Qt.ThresholdDither | Qt.ThresholdAlphaDither) self.resize(self.pix.size()) self.setMask(self.pix.mask())
- paintEvent()函数每次初始化窗口时只调用一次
- 所以每加载一次图片就要重新调用一次 paintEvent()函数
- 即在更新窗口时调用这个函数
self.timer = QTimer() self.timer.setInterval(500) # 500毫秒 self.timer.timeout.connect(self.timeChange) self.timer.start()
- 完整实现代码
# -*- coding: utf-8 -*- """ 【简介】 不规则窗体的动画实现 """ import sys from PyQt5.QtWidgets import QApplication ,QWidget from PyQt5.QtGui import QPixmap, QPainter , QCursor from PyQt5.QtCore import Qt, QTimer class ShapeWidget(QWidget): def __init__(self,parent=None): super(ShapeWidget,self).__init__(parent) self.i = 1 self.mypix() self.timer = QTimer() self.timer.setInterval(500) # 500毫秒 self.timer.timeout.connect(self.timeChange) self.timer.start() # 显示不规则 pic def mypix(self): self.update() if self.i == 5: self.i = 1 self.mypic = {1: './images/left.png', 2: "./images/up.png", 3: './images/right.png', 4: './images/down.png'} self.pix = QPixmap(self.mypic[self.i], "0", Qt.AvoidDither | Qt.ThresholdDither | Qt.ThresholdAlphaDither) self.resize(self.pix.size()) self.setMask(self.pix.mask()) self.dragPosition = None def mousePressEvent(self, event): if event.button() == Qt.LeftButton: self.m_drag=True self.m_DragPosition=event.globalPos()-self.pos() event.accept() self.setCursor(QCursor(Qt.OpenHandCursor)) def mouseMoveEvent(self, QMouseEvent): if Qt.LeftButton and self.m_drag: self.move(QMouseEvent.globalPos()- self.m_DragPosition ) QMouseEvent.accept() def mouseReleaseEvent(self, QMouseEvent): self.m_drag=False self.setCursor(QCursor(Qt.ArrowCursor)) def paintEvent(self, event): painter = QPainter(self) painter.drawPixmap(0, 0, self.pix.width(),self.pix.height(),self.pix) # 鼠标双击事件 def mouseDoubleClickEvent(self, event): if event.button() == 1: self.i += 1 self.mypix() # 每500毫秒修改paint def timeChange(self): self.i += 1 self.mypix() if __name__ == '__main__': app = QApplication(sys.argv) form = ShapeWidget() form.show() sys.exit(app.exec_())
8.5.2 加载 GIF 动画效果
import sys from PyQt5.QtWidgets import QApplication ,QWidget,QLabel from PyQt5.QtGui import QMovie from PyQt5.QtCore import Qt, QTimer class LoadingGifWin(QWidget): def __init__(self,parent=None): super(LoadingGifWin,self).__init__(parent) self.label = QLabel("", self) self.setFixedSize(217,217) self.setWindowFlags(Qt.Dialog | Qt.CustomizeWindowHint) self.movie = QMovie("./images/loading.gif") self.label.setMovie(self.movie) self.movie.start() if __name__ == '__main__': app = QApplication(sys.argv) loadingWin = LoadingGifWin() loadingWin.show() sys.exit(app.exec_())
8.6 设置样式
8.6.1 为标签添加背景图片
#1 label1 = QLabel(self) label1.setToolTip('这是一个文本标签') label1.setStyleSheet("QLabel{border-image: url(./images/python.jpg);}") label1.setFixedWidth(476) label1.setFixedHeight(259)
8.6.2 为按钮添加背景图片
#2 btn1 = QPushButton(self ) btn1.setObjectName('btn1') btn1.setMaximumSize(48, 48) btn1.setMinimumSize(48, 48) style = ''' #btn1{ border-radius: 4px; background-image: url('./images/add.png'); } #btn1:Pressed{ background-image: url('./images/addhover.png'); } #btn1:hover{ background-image: url('./images/addPressed.png'); } ''' btn1.setStyleSheet(style)
- style 中使用 QPushButton 则表示对所有的按钮都生效
- 可以为特定 btn 设置不同的 style
- 使用 btn1.setObjectName('btn1')为控制设置名称
- 按钮有三种状态
- 正常
- 鼠标悬停
- 按下按钮
8.6.3 缩放图片
filename = r".\images\Cloudy_72px.png" img = QImage( filename ) label1 = QLabel(self) label1.setFixedWidth(120) label1.setFixedHeight(120) result = img.scaled(label1.width(), label1.height(),Qt.IgnoreAspectRatio, Qt.SmoothTransformation); label1.setPixmap(QPixmap.fromImage(result)) #3 vbox=QVBoxLayout() vbox.addWidget(label1) self.setLayout(vbox) self.setWindowTitle("图片大小缩放例子")
8.6.4 设置窗口透明度
- 设置窗口的透明度
- 透明度范围 0.0(全透明)~1.0,默认是 1.0(不透明)
win = QMainWindow() win.setWindowOpacity(0.5)
8.6.5 加载 QSS
- 在 Qt 中使用 style 样式
- 分离主代码和样式
- 在窗口中加载定义的样式文件(.qss)
- 编写 qss 文件
MainWindow{ border-image: url('./images/python.jpg') }
- 在窗口中加载(这里定义公共类来提供工具方法加载样式文件)
class CommonHelper: def __inin__(self): pass @staticmethod def readQss(style): with open(style, 'r') as f: return f.read()
- 在主函数中加载 Qss 文件
app = QApplication(sys.argv) win = MainWindow() styleFile = './style.qss' style = CommonHelper.readQss(styleFile) win.setStyle(style) win.show() sys.exit(app.exec_())
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于