PyQt5事件机制
第一阶段:理解基本事件概念
1.1 什么是事件?
事件是PyQt中处理用户输入和系统通知的机制。与信号槽不同,事件更底层,处理更原始的用户交互。
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QVBoxLayout
from PyQt5.QtCore import Qtclass EventDemo(QWidget):def __init__(self):super().__init__()self.initUI()def initUI(self):layout = QVBoxLayout()self.label = QLabel("事件类型:\n- 鼠标事件\n- 键盘事件\n- 窗口事件\n- 绘制事件")layout.addWidget(self.label)self.setLayout(layout)self.setWindowTitle("事件基础概念")self.resize(300, 200)# 鼠标按下事件def mousePressEvent(self, event):button = "左键" if event.button() == Qt.LeftButton else "右键"self.label.setText(f"鼠标{button}按下在位置: ({event.x()}, {event.y()})")# 键盘按下事件def keyPressEvent(self, event):self.label.setText(f"按下了键: {event.key()} 文本: '{event.text()}'")# 窗口大小改变事件def resizeEvent(self, event):self.label.setText(f"窗口大小改变: {event.size().width()}x{event.size().height()}")if __name__ == '__main__':app = QApplication(sys.argv)window = EventDemo()window.show()sys.exit(app.exec_())
1.2 事件处理的基本模式
所有事件处理函数都有相同的模式:
事件机制确实是PyQt5中比较复杂的部分,但通过这种循序渐进的学习方式,你应该能够很好地理解和掌握。记住关键点:
继续练习,你会在实际项目中越来越熟练!
记住关键点:
通过实践这些概念,你会逐渐掌握事件传播的精髓,编写出更加健壮和灵活的PyQt5应用。
-
函数名以"Event"结尾
-
接收一个事件对象作为参数
-
可以调用父类方法确保默认行为
def eventHandler(self, event):# 1. 自定义处理逻辑self.handleCustomLogic(event)# 2. 可选:调用父类方法保持默认行为super().eventHandler(event)# 3. 可选:接受或忽略事件# event.accept() 或 event.ignore()
第二阶段:常用事件处理函数
2.1 鼠标事件
import sys from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QVBoxLayout from PyQt5.QtCore import Qt, QPoint from PyQt5.QtGui import QPainter, QPenclass MouseEventDemo(QWidget):def __init__(self):super().__init__()self.points = []self.initUI()def initUI(self):self.setWindowTitle("鼠标事件演示")self.resize(400, 300)def mousePressEvent(self, event):pos = event.pos()button = self.get_button_name(event.button())print(f"鼠标{button}按下: ({pos.x()}, {pos.y()})")if event.button() == Qt.LeftButton:self.points.append(pos)self.update() # 触发重绘super().mousePressEvent(event)def mouseReleaseEvent(self, event):pos = event.pos()button = self.get_button_name(event.button())print(f"鼠标{button}释放: ({pos.x()}, {pos.y()})")super().mouseReleaseEvent(event)def mouseMoveEvent(self, event):pos = event.pos()# 检查鼠标按钮状态buttons = []if event.buttons() & Qt.LeftButton:buttons.append("左键")if event.buttons() & Qt.RightButton:buttons.append("右键")if buttons:print(f"鼠标移动(按下{'+'.join(buttons)}): ({pos.x()}, {pos.y()})")super().mouseMoveEvent(event)def mouseDoubleClickEvent(self, event):pos = event.pos()button = self.get_button_name(event.button())print(f"鼠标{button}双击: ({pos.x()}, {pos.y()})")super().mouseDoubleClickEvent(event)def wheelEvent(self, event):delta = event.angleDelta().y()direction = "向上" if delta > 0 else "向下"print(f"鼠标滚轮{direction}滚动: {delta}")super().wheelEvent(event)def get_button_name(self, button):if button == Qt.LeftButton:return "左键"elif button == Qt.RightButton:return "右键"elif button == Qt.MiddleButton:return "中键"else:return "未知键"def paintEvent(self, event):painter = QPainter(self)painter.setRenderHint(QPainter.Antialiasing)# 绘制背景painter.fillRect(self.rect(), Qt.white)# 绘制所有点painter.setPen(QPen(Qt.blue, 3))for point in self.points:painter.drawPoint(point)# 绘制说明文字painter.setPen(QPen(Qt.black, 1))painter.drawText(10, 20, "鼠标事件测试 - 查看控制台输出")if __name__ == '__main__':app = QApplication(sys.argv)window = MouseEventDemo()window.show()sys.exit(app.exec_())
2.2 键盘事件
import sys from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QVBoxLayout, QTextEdit from PyQt5.QtCore import Qtclass KeyboardEventDemo(QWidget):def __init__(self):super().__init__()self.initUI()def initUI(self):self.setWindowTitle("键盘事件演示")self.resize(500, 400)layout = QVBoxLayout()# 说明标签label = QLabel("键盘事件测试:\n""1. 先点击窗口空白处确保获得焦点\n""2. 按各种键查看输出\n""3. 注意功能键、字母键、数字键的区别")layout.addWidget(label)# 文本显示区域self.text_display = QTextEdit()self.text_display.setReadOnly(True)layout.addWidget(self.text_display)self.setLayout(layout)# 关键:设置焦点策略self.setFocusPolicy(Qt.StrongFocus)def keyPressEvent(self, event):key = event.key()text = event.text()modifiers = event.modifiers()# 构建事件信息info = f"按键按下 - 键码: {key}"if text and text.isprintable():info += f", 文本: '{text}'"# 检查修饰键mod_list = []if modifiers & Qt.ShiftModifier:mod_list.append("Shift")if modifiers & Qt.ControlModifier:mod_list.append("Ctrl")if modifiers & Qt.AltModifier:mod_list.append("Alt")if mod_list:info += f", 修饰键: {'+'.join(mod_list)}"# 特殊键处理if key == Qt.Key_Escape:info += " (ESC - 退出程序)"self.close()elif key == Qt.Key_Enter or key == Qt.Key_Return:info += " (回车键)"elif key == Qt.Key_Space:info += " (空格键)"elif key == Qt.Key_Backspace:info += " (退格键)"elif key == Qt.Key_Delete:info += " (删除键)"self.text_display.append(info)# 接受事件,阻止默认行为event.accept()def keyReleaseEvent(self, event):key = event.key()self.text_display.append(f"按键释放 - 键码: {key}")event.accept()def mousePressEvent(self, event):# 点击时获得焦点self.setFocus()self.text_display.append("--- 窗口获得焦点,现在可以测试键盘事件 ---")super().mousePressEvent(event)if __name__ == '__main__':app = QApplication(sys.argv)window = KeyboardEventDemo()window.show()sys.exit(app.exec_())
第三阶段:理解事件传播
3.1 事件传播机制
import sys from PyQt5.QtWidgets import (QApplication, QWidget, QLabel, QVBoxLayout, QHBoxLayout, QPushButton, QLineEdit, QGroupBox) from PyQt5.QtCore import Qtclass EventPropagationDemo(QWidget):def __init__(self):super().__init__()self.initUI()def initUI(self):self.setWindowTitle("事件传播演示")self.resize(500, 400)main_layout = QHBoxLayout()# 左侧:控制面板control_panel = QGroupBox("控制面板")control_layout = QVBoxLayout()self.info_display = QLabel("事件信息将显示在这里")self.info_display.setStyleSheet("background-color: #f0f0f0; padding: 10px;")self.info_display.setMinimumHeight(150)control_layout.addWidget(self.info_display)# 传播模式选择mode_group = QGroupBox("传播模式")mode_layout = QVBoxLayout()self.accept_btn = QPushButton("使用 event.accept()")self.ignore_btn = QPushButton("使用 event.ignore()")self.default_btn = QPushButton("使用默认行为")self.accept_btn.clicked.connect(lambda: self.set_propagation_mode("accept"))self.ignore_btn.clicked.connect(lambda: self.set_propagation_mode("ignore"))self.default_btn.clicked.connect(lambda: self.set_propagation_mode("default"))mode_layout.addWidget(self.accept_btn)mode_layout.addWidget(self.ignore_btn)mode_layout.addWidget(self.default_btn)mode_group.setLayout(mode_layout)control_layout.addWidget(mode_group)control_panel.setLayout(control_layout)main_layout.addWidget(control_panel)# 右侧:测试区域test_panel = QGroupBox("测试区域 (点击这里测试)")test_layout = QVBoxLayout()test_label = QLabel("测试说明:\n""1. 选择传播模式\n""2. 点击此区域测试鼠标事件\n""3. 按键盘键测试键盘事件\n""4. 观察事件传播路径")test_layout.addWidget(test_label)# 嵌套的测试组件self.inner_widget = InnerWidget(self)test_layout.addWidget(self.inner_widget)test_panel.setLayout(test_layout)main_layout.addWidget(test_panel)self.setLayout(main_layout)# 初始模式self.propagation_mode = "default"self.update_info("初始状态: 使用默认行为")def set_propagation_mode(self, mode):self.propagation_mode = modeif mode == "accept":self.update_info("模式: 使用 event.accept() - 事件停止传播")elif mode == "ignore":self.update_info("模式: 使用 event.ignore() - 事件继续传播")else:self.update_info("模式: 使用默认行为 - 调用父类方法")def update_info(self, message):self.info_display.setText(message)def mousePressEvent(self, event):self.update_info("父组件收到鼠标按下事件")super().mousePressEvent(event)def keyPressEvent(self, event):if event.key() == Qt.Key_A:self.update_info("父组件收到A键按下事件")super().keyPressEvent(event)class InnerWidget(QWidget):def __init__(self, parent):super().__init__(parent)self.parent_demo = parentself.setStyleSheet("background-color: #e8f4f8; border: 1px solid #aaa;")self.setMinimumSize(200, 150)# 设置焦点策略self.setFocusPolicy(Qt.StrongFocus)def mousePressEvent(self, event):message = "内层组件收到鼠标按下事件"# 根据选择的模式处理事件if self.parent_demo.propagation_mode == "accept":event.accept()message += " -> 使用 accept()"elif self.parent_demo.propagation_mode == "ignore":event.ignore()message += " -> 使用 ignore()"else:# 默认行为:调用父类方法super().mousePressEvent(event)message += " -> 调用父类方法"self.parent_demo.update_info(message)def keyPressEvent(self, event):if event.key() == Qt.Key_A:message = "内层组件收到A键按下事件"if self.parent_demo.propagation_mode == "accept":event.accept()message += " -> 使用 accept()"elif self.parent_demo.propagation_mode == "ignore":event.ignore()message += " -> 使用 ignore()"else:super().keyPressEvent(event)message += " -> 调用父类方法"self.parent_demo.update_info(message)else:super().keyPressEvent(event)def mousePressEvent(self, event):# 点击时获得焦点self.setFocus()self.parent_demo.update_info("内层组件获得焦点")super().mousePressEvent(event)if __name__ == '__main__':app = QApplication(sys.argv)window = EventPropagationDemo()window.show()sys.exit(app.exec_())
3.2 事件传播机制详解
事件传播机制是PyQt5中非常重要但又相对复杂的概念。理解它对于编写复杂的GUI应用至关重要。让我从基础到高级详细讲解。
3.2.1. 事件传播的基本概念
3.2.1.1 什么是事件传播?
事件传播指的是当一个事件发生时,它会在组件树中按照特定路径传递的过程。PyQt5采用冒泡机制(Bubbling),即事件从子组件向父组件传播。关于此规则如何传播,见示例代码。
子组件 → 父组件 → 祖父组件 → ... → 顶级窗口
3.2.1.2 事件传播的三个关键方法
event.accept() # 接受事件,停止传播 event.ignore() # 忽略事件,继续传播 super().event() # 调用父类的事件处理方法 但是,前二者同后者的区别是: event.accept()/event.ignore() → 控制事件传播 super().mousePressEvent(event) → 控制父类默认行为
3.2.1.3 示例代码
import sys from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout,QPushButton, QLabel, QGroupBox, QTextEdit,QComboBox, QFrame) from PyQt5.QtCore import Qtclass MultiLayerEventDemo(QWidget):def __init__(self):super().__init__()self.initUI()def initUI(self):self.setWindowTitle("多层事件传播控制演示")self.resize(700, 600)main_layout = QHBoxLayout()# 左侧控制面板control_panel = self.create_control_panel()main_layout.addWidget(control_panel)# 右侧演示区域demo_panel = self.create_demo_panel()main_layout.addWidget(demo_panel)self.setLayout(main_layout)# 初始化模式self.modes = {"level3": "default","level2": "default","level1": "default","top": "default"}def create_control_panel(self):panel = QGroupBox("控制面板")layout = QVBoxLayout()# 说明info_label = QLabel("事件传播控制:\n\n""为每一层组件选择事件处理模式:\n""- 接受(accept): 事件停止传播\n""- 忽略(ignore): 事件继续传播\n""- 默认(default): 调用父类方法\n\n""点击右侧测试区域观察事件传播路径。")info_label.setWordWrap(True)layout.addWidget(info_label)# 分隔线line = QFrame()line.setFrameShape(QFrame.HLine)line.setFrameShadow(QFrame.Sunken)layout.addWidget(line)# 第三层控制level3_group = QGroupBox("第三层组件设置")level3_layout = QVBoxLayout()self.level3_combo = QComboBox()self.level3_combo.addItems(["默认模式", "接受模式", "忽略模式"])self.level3_combo.currentIndexChanged.connect(lambda: self.update_mode("level3", self.level3_combo.currentIndex()))level3_layout.addWidget(self.level3_combo)level3_group.setLayout(level3_layout)layout.addWidget(level3_group)# 第二层控制level2_group = QGroupBox("第二层组件设置")level2_layout = QVBoxLayout()self.level2_combo = QComboBox()self.level2_combo.addItems(["默认模式", "接受模式", "忽略模式"])self.level2_combo.currentIndexChanged.connect(lambda: self.update_mode("level2", self.level2_combo.currentIndex()))level2_layout.addWidget(self.level2_combo)level2_group.setLayout(level2_layout)layout.addWidget(level2_group)# 第一层控制level1_group = QGroupBox("第一层组件设置")level1_layout = QVBoxLayout()self.level1_combo = QComboBox()self.level1_combo.addItems(["默认模式", "接受模式", "忽略模式"])self.level1_combo.currentIndexChanged.connect(lambda: self.update_mode("level1", self.level1_combo.currentIndex()))level1_layout.addWidget(self.level1_combo)level1_group.setLayout(level1_layout)layout.addWidget(level1_group)# 顶层控制top_group = QGroupBox("顶层窗口设置")top_layout = QVBoxLayout()self.top_combo = QComboBox()self.top_combo.addItems(["默认模式", "接受模式", "忽略模式"])self.top_combo.currentIndexChanged.connect(lambda: self.update_mode("top", self.top_combo.currentIndex()))top_layout.addWidget(self.top_combo)top_group.setLayout(top_layout)layout.addWidget(top_group)# 操作按钮btn_layout = QHBoxLayout()clear_btn = QPushButton("清除日志")clear_btn.clicked.connect(self.clear_log)btn_layout.addWidget(clear_btn)test_btn = QPushButton("测试键盘事件")test_btn.clicked.connect(self.test_keyboard_event)btn_layout.addWidget(test_btn)layout.addLayout(btn_layout)panel.setLayout(layout)return paneldef create_demo_panel(self):panel = QGroupBox("事件传播演示区域")layout = QVBoxLayout()# 日志显示self.log_display = QTextEdit()self.log_display.setReadOnly(True)layout.addWidget(self.log_display)# 嵌套组件演示区域demo_area = QGroupBox("点击或按键测试区域")demo_layout = QVBoxLayout()self.top_level = TopLevelWidget(self)demo_layout.addWidget(self.top_level)demo_area.setLayout(demo_layout)layout.addWidget(demo_area)panel.setLayout(layout)return paneldef update_mode(self, level, index):modes = ["default", "accept", "ignore"]self.modes[level] = modes[index]self.log(f"设置 {level} 为 {modes[index]} 模式")def log(self, message):self.log_display.append(message)def clear_log(self):self.log_display.clear()self.log("日志已清除")def test_keyboard_event(self):self.log("=== 手动触发键盘事件测试 ===")self.log("请点击演示区域确保获得焦点,然后按任意键")# 设置焦点到第三层组件self.top_level.level1.level2.level3.setFocus()class TopLevelWidget(QGroupBox):def __init__(self, parent):super().__init__("顶层窗口", parent)self.parent_demo = parentself.initUI()def initUI(self):layout = QVBoxLayout()self.level1 = Level1Widget(self)layout.addWidget(self.level1)self.setLayout(layout)def mousePressEvent(self, event):mode = self.parent_demo.modes["top"]self.parent_demo.log("📌 顶层窗口: 鼠标按下事件开始")if mode == "accept":self.parent_demo.log(" 顶层窗口: 调用 event.accept() - 事件停止传播")event.accept()elif mode == "ignore":self.parent_demo.log(" 顶层窗口: 调用 event.ignore() - 事件继续传播")event.ignore()super().mousePressEvent(event)else: # defaultself.parent_demo.log(" 顶层窗口: 调用父类方法 - 事件继续传播")super().mousePressEvent(event)def keyPressEvent(self, event):mode = self.parent_demo.modes["top"]self.parent_demo.log("📌 顶层窗口: 键盘按下事件开始")self.parent_demo.log(f" 按键: {event.key()}, 文本: '{event.text()}'")if mode == "accept":self.parent_demo.log(" 顶层窗口: 调用 event.accept() - 事件停止传播")event.accept()elif mode == "ignore":self.parent_demo.log(" 顶层窗口: 调用 event.ignore() - 事件继续传播")event.ignore()super().keyPressEvent(event)else: # defaultself.parent_demo.log(" 顶层窗口: 调用父类方法 - 事件继续传播")super().keyPressEvent(event)class Level1Widget(QGroupBox):def __init__(self, parent):super().__init__("第一层组件", parent)self.parent_top = parentself.initUI()def initUI(self):layout = QVBoxLayout()self.level2 = Level2Widget(self)layout.addWidget(self.level2)self.setLayout(layout)def mousePressEvent(self, event):mode = self.parent_top.parent_demo.modes["level1"]self.parent_top.parent_demo.log("🔹 第一层组件: 鼠标按下事件开始")if mode == "accept":self.parent_top.parent_demo.log(" 第一层组件: 调用 event.accept() - 事件停止传播")event.accept()elif mode == "ignore":self.parent_top.parent_demo.log(" 第一层组件: 调用 event.ignore() - 事件继续传播")event.ignore()super().mousePressEvent(event)else: # defaultself.parent_top.parent_demo.log(" 第一层组件: 调用父类方法 - 事件继续传播")super().mousePressEvent(event)def keyPressEvent(self, event):mode = self.parent_top.parent_demo.modes["level1"]self.parent_top.parent_demo.log("🔹 第一层组件: 键盘按下事件开始")self.parent_top.parent_demo.log(f" 按键: {event.key()}, 文本: '{event.text()}'")if mode == "accept":self.parent_top.parent_demo.log(" 第一层组件: 调用 event.accept() - 事件停止传播")event.accept()elif mode == "ignore":self.parent_top.parent_demo.log(" 第一层组件: 调用 event.ignore() - 事件继续传播")event.ignore()super().keyPressEvent(event)else: # defaultself.parent_top.parent_demo.log(" 第一层组件: 调用父类方法 - 事件继续传播")super().keyPressEvent(event)class Level2Widget(QGroupBox):def __init__(self, parent):super().__init__("第二层组件", parent)self.parent_level1 = parentself.initUI()def initUI(self):layout = QVBoxLayout()self.level3 = Level3Widget(self)layout.addWidget(self.level3)self.setLayout(layout)def mousePressEvent(self, event):mode = self.parent_level1.parent_top.parent_demo.modes["level2"]self.parent_level1.parent_top.parent_demo.log("🔸 第二层组件: 鼠标按下事件开始")if mode == "accept":self.parent_level1.parent_top.parent_demo.log(" 第二层组件: 调用 event.accept() - 事件停止传播")event.accept()elif mode == "ignore":self.parent_level1.parent_top.parent_demo.log(" 第二层组件: 调用 event.ignore() - 事件继续传播")event.ignore()super().mousePressEvent(event)else: # defaultself.parent_level1.parent_top.parent_demo.log(" 第二层组件: 调用父类方法 - 事件继续传播")super().mousePressEvent(event)def keyPressEvent(self, event):mode = self.parent_level1.parent_top.parent_demo.modes["level2"]self.parent_level1.parent_top.parent_demo.log("🔸 第二层组件: 键盘按下事件开始")self.parent_level1.parent_top.parent_demo.log(f" 按键: {event.key()}, 文本: '{event.text()}'")if mode == "accept":self.parent_level1.parent_top.parent_demo.log(" 第二层组件: 调用 event.accept() - 事件停止传播")event.accept()elif mode == "ignore":self.parent_level1.parent_top.parent_demo.log(" 第二层组件: 调用 event.ignore() - 事件继续传播")event.ignore()super().keyPressEvent(event)else: # defaultself.parent_level1.parent_top.parent_demo.log(" 第二层组件: 调用父类方法 - 事件继续传播")super().keyPressEvent(event)class Level3Widget(QGroupBox):def __init__(self, parent):super().__init__("第三层组件 (点击这里测试)", parent)self.parent_level2 = parentself.setMinimumHeight(150) # 增加高度以便更容易点击self.setFocusPolicy(Qt.StrongFocus)# 关键修复:添加一个可点击的子组件layout = QVBoxLayout()self.click_area = ClickableLabel("点击此区域测试事件传播\n(这是一个可点击的标签)")self.click_area.setStyleSheet("background-color: #e0e0e0; padding: 20px; border: 1px solid #aaa;")self.click_area.setAlignment(Qt.AlignCenter)layout.addWidget(self.click_area)self.setLayout(layout)def mousePressEvent(self, event):# 这个方法现在会被调用,因为我们在GroupBox上点击demo = self.parent_level2.parent_level1.parent_top.parent_demomode = demo.modes["level3"]demo.log("🎯 第三层组件(QGroupBox): 鼠标按下事件开始")demo.log(f" 点击位置: ({event.x()}, {event.y()})")if mode == "accept":demo.log(" 第三层组件: 调用 event.accept() - 事件停止传播")event.accept()elif mode == "ignore":demo.log(" 第三层组件: 调用 event.ignore() - 事件继续传播")event.ignore()super().mousePressEvent(event)else: # defaultdemo.log(" 第三层组件: 调用父类方法 - 事件继续传播")super().mousePressEvent(event)def keyPressEvent(self, event):demo = self.parent_level2.parent_level1.parent_top.parent_demomode = demo.modes["level3"]demo.log("🎯 第三层组件: 键盘按下事件开始")demo.log(f" 按键: {event.key()}, 文本: '{event.text()}'")if mode == "accept":demo.log(" 第三层组件: 调用 event.accept() - 事件停止传播")event.accept()elif mode == "ignore":demo.log(" 第三层组件: 调用 event.ignore() - 事件继续传播")event.ignore()super().keyPressEvent(event)else: # defaultdemo.log(" 第三层组件: 调用父类方法 - 事件继续传播")super().keyPressEvent(event)class ClickableLabel(QLabel):"""可点击的标签,用于确保第三层组件能接收鼠标事件"""def __init__(self, text):super().__init__(text)self.setMouseTracking(True)def mousePressEvent(self, event):# 获取父组件(Level3Widget)parent = self.parent()if parent and hasattr(parent, 'mousePressEvent'):# 手动调用父组件的mousePressEventparent.mousePressEvent(event)else:super().mousePressEvent(event)if __name__ == '__main__':app = QApplication(sys.argv)window = MultiLayerEventDemo()window.show()sys.exit(app.exec_())
3.2.2. 事件传播的完整流程
3.2.2.1 事件传播路径
import sys from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QPushButton, QLabel, QGroupBox) from PyQt5.QtCore import Qtclass EventPropagationDemo(QWidget):def __init__(self):super().__init__()self.initUI()def initUI(self):self.setWindowTitle("事件传播完整流程演示")self.resize(500, 400)layout = QVBoxLayout()# 创建嵌套的组件层次self.level1 = Level1Widget(self)layout.addWidget(self.level1)self.setLayout(layout)def mousePressEvent(self, event):print("顶级窗口: 鼠标按下事件")super().mousePressEvent(event)def keyPressEvent(self, event):print("顶级窗口: 键盘按下事件")super().keyPressEvent(event)class Level1Widget(QGroupBox):def __init__(self, parent):super().__init__("第一层组件", parent)self.parent_demo = parentself.initUI()def initUI(self):layout = QVBoxLayout()self.level2 = Level2Widget(self)layout.addWidget(self.level2)self.setLayout(layout)def mousePressEvent(self, event):print("第一层组件: 鼠标按下事件")super().mousePressEvent(event)def keyPressEvent(self, event):print("第一层组件: 键盘按下事件")super().keyPressEvent(event)class Level2Widget(QGroupBox):def __init__(self, parent):super().__init__("第二层组件", parent)self.parent_level1 = parentself.initUI()def initUI(self):layout = QVBoxLayout()self.level3 = Level3Widget(self)layout.addWidget(self.level3)self.setLayout(layout)def mousePressEvent(self, event):print("第二层组件: 鼠标按下事件")super().mousePressEvent(event)def keyPressEvent(self, event):print("第二层组件: 键盘按下事件")super().keyPressEvent(event)class Level3Widget(QGroupBox):def __init__(self, parent):super().__init__("第三层组件(点击这里测试)", parent)self.parent_level2 = parentself.setMinimumHeight(100)self.setFocusPolicy(Qt.StrongFocus)def mousePressEvent(self, event):print("第三层组件: 鼠标按下事件 - 事件开始")print(f"点击位置: ({event.x()}, {event.y()})")# 默认情况下,事件会继续传播(因为调用了父类方法)super().mousePressEvent(event)print("第三层组件: 调用了父类方法,事件继续传播")def keyPressEvent(self, event):print("第三层组件: 键盘按下事件 - 事件开始")print(f"按键: {event.key()}, 文本: '{event.text()}'")# 默认情况下,事件会继续传播super().keyPressEvent(event)print("第三层组件: 调用了父类方法,事件继续传播")if __name__ == '__main__':app = QApplication(sys.argv)window = EventPropagationDemo()window.show()sys.exit(app.exec_())
运行这个程序,点击最内层的第三层组件,你会看到控制台输出:
第三层组件: 鼠标按下事件 - 事件开始 点击位置: (x, y) 第三层组件: 调用了父类方法,事件继续传播 第二层组件: 鼠标按下事件 第一层组件: 鼠标按下事件 顶级窗口: 鼠标按下事件
3.2.3. accept() 和 ignore() 的详细作用
3.2.3.1 事件接受状态
每个事件都有一个"接受状态":
-
默认状态:新创建的事件处于"未决"状态
-
event.accept():标记事件为"已接受",停止传播
-
event.ignore():标记事件为"已忽略",继续传播
3.2.3.2 实际演示
import sys from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QGroupBox, QTextEdit) from PyQt5.QtCore import Qtclass AcceptIgnoreDemo(QWidget):def __init__(self):super().__init__()self.initUI()def initUI(self):self.setWindowTitle("accept() 和 ignore() 详细演示")self.resize(600, 500)main_layout = QVBoxLayout()# 控制面板control_layout = QHBoxLayout()self.accept_btn = QPushButton("使用 accept()")self.ignore_btn = QPushButton("使用 ignore()")self.default_btn = QPushButton("使用默认行为")self.accept_btn.clicked.connect(lambda: self.set_handling_mode("accept"))self.ignore_btn.clicked.connect(lambda: self.set_handling_mode("ignore"))self.default_btn.clicked.connect(lambda: self.set_handling_mode("default"))control_layout.addWidget(self.accept_btn)control_layout.addWidget(self.ignore_btn)control_layout.addWidget(self.default_btn)self.mode_label = QLabel("当前模式: 默认行为")control_layout.addWidget(self.mode_label)main_layout.addLayout(control_layout)# 日志显示self.log_display = QTextEdit()self.log_display.setReadOnly(True)main_layout.addWidget(self.log_display)# 测试区域test_group = QGroupBox("测试区域 (点击各层组件测试事件传播)")test_layout = QVBoxLayout()self.layer1 = EventLayer("第一层", 1, self)test_layout.addWidget(self.layer1)test_group.setLayout(test_layout)main_layout.addWidget(test_group)self.setLayout(main_layout)self.handling_mode = "default"def set_handling_mode(self, mode):self.handling_mode = modeself.mode_label.setText(f"当前模式: {mode}")self.log_display.append(f"=== 切换到 {mode} 模式 ===")def log(self, message):self.log_display.append(message)def mousePressEvent(self, event):self.log("📌 顶级窗口: 收到鼠标事件 (传播终点)")super().mousePressEvent(event)class EventLayer(QGroupBox):def __init__(self, title, level, parent):super().__init__(title, parent)self.parent_demo = parentself.level = levelself.initUI()def initUI(self):layout = QVBoxLayout()if self.level < 3:next_level = EventLayer(f"第{self.level + 1}层", self.level + 1, self.parent_demo)layout.addWidget(next_level)else:# 最内层添加点击提示label = QLabel("点击这里测试事件传播")label.setStyleSheet("background-color: #e0e0e0; padding: 20px;")layout.addWidget(label)self.setLayout(layout)def mousePressEvent(self, event):message = f"🔹 第{self.level}层: 鼠标按下事件"self.parent_demo.log(message)# 根据选择的模式处理事件if self.parent_demo.handling_mode == "accept":event.accept()self.parent_demo.log(f" 第{self.level}层: 调用 event.accept() - 事件停止传播")return # 注意:调用accept()后通常直接返回,不调用父类方法elif self.parent_demo.handling_mode == "ignore":event.ignore()self.parent_demo.log(f" 第{self.level}层: 调用 event.ignore() - 事件继续传播")# 即使调用ignore(),通常还是会调用父类方法# 默认行为或ignore模式:调用父类方法super().mousePressEvent(event)if self.parent_demo.handling_mode == "default":self.parent_demo.log(f" 第{self.level}层: 调用父类方法 - 事件继续传播")if __name__ == '__main__':app = QApplication(sys.argv)window = AcceptIgnoreDemo()window.show()sys.exit(app.exec_())
3.2.4. 不同类型事件的特殊传播规则
3.2.4.1 键盘事件的传播
键盘事件有其特殊性,主要与焦点相关:
import sys from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QLineEdit, QTextEdit, QLabel) from PyQt5.QtCore import Qtclass KeyboardPropagationDemo(QWidget):def __init__(self):super().__init__()self.initUI()def initUI(self):self.setWindowTitle("键盘事件传播演示")self.resize(500, 400)layout = QVBoxLayout()# 说明label = QLabel("键盘事件传播特点:\n""1. 只有获得焦点的组件才能接收键盘事件\n""2. 如果组件不接受事件,会传播给父组件\n""3. 如果所有组件都不接受,事件会被丢弃")layout.addWidget(label)# 输入框1 - 正常处理self.input1 = QLineEdit()self.input1.setPlaceholderText("输入框1 - 正常处理键盘事件")layout.addWidget(self.input1)# 输入框2 - 拦截特定按键self.input2 = KeyboardInterceptLineEdit()self.input2.setPlaceholderText("输入框2 - 拦截A键和B键")layout.addWidget(self.input2)# 日志self.log_display = QTextEdit()self.log_display.setReadOnly(True)layout.addWidget(self.log_display)self.setLayout(layout)def log(self, message):self.log_display.append(message)def keyPressEvent(self, event):# 只有没有任何子组件处理键盘事件时,才会到达这里if event.key() == Qt.Key_Escape:self.log("窗口: 收到ESC键 - 关闭窗口")self.close()else:self.log(f"窗口: 收到未处理按键 {event.key()} - 事件被丢弃")event.accept()class KeyboardInterceptLineEdit(QLineEdit):def __init__(self):super().__init__()def keyPressEvent(self, event):# 拦截特定按键if event.key() == Qt.Key_A:# 获取父组件来记录日志parent = self.parent()if hasattr(parent, 'log'):parent.log("输入框2: 拦截了A键 - 事件被接受,不显示字符")event.accept() # 接受事件,阻止默认行为returnelif event.key() == Qt.Key_B:parent = self.parent()if hasattr(parent, 'log'):parent.log("输入框2: 拦截了B键 - 但调用父类方法,会显示字符")# 调用父类方法,会显示字符super().keyPressEvent(event)return# 其他按键正常处理super().keyPressEvent(event)if __name__ == '__main__':app = QApplication(sys.argv)window = KeyboardPropagationDemo()window.show()sys.exit(app.exec_())
3.2.4.2 鼠标事件与键盘事件的区别
特性 鼠标事件 键盘事件 目标确定 鼠标位置下的组件 获得焦点的组件 传播路径 从子组件向父组件 从焦点组件向父组件 默认行为 通常调用父类方法 通常调用父类方法 常见用途 点击、拖拽、悬停 文字输入、快捷键 3.2.5. 事件传播的实际应用场景
3.2.5.1 场景1:全局快捷键
import sys from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QTextEdit, QLabel from PyQt5.QtCore import Qtclass GlobalShortcutDemo(QWidget):def __init__(self):super().__init__()self.initUI()def initUI(self):self.setWindowTitle("全局快捷键演示")self.resize(500, 400)layout = QVBoxLayout()label = QLabel("全局快捷键实现:\n""- 在窗口任意位置按Ctrl+S保存\n""- 在窗口任意位置按Ctrl+Q退出\n""- 这些快捷键在任何子组件获得焦点时都有效")layout.addWidget(label)self.text_edit = QTextEdit()self.text_edit.setPlaceholderText("在此输入文本...")layout.addWidget(self.text_edit)self.setLayout(layout)def keyPressEvent(self, event):# 检查Ctrl组合键if event.modifiers() & Qt.ControlModifier:if event.key() == Qt.Key_S:self.save_file()event.accept()returnelif event.key() == Qt.Key_Q:self.close()event.accept()return# 其他按键正常传播super().keyPressEvent(event)def save_file(self):content = self.text_edit.toPlainText()print(f"保存内容: {content}")# 实际应用中这里会有文件保存逻辑if __name__ == '__main__':app = QApplication(sys.argv)window = GlobalShortcutDemo()window.show()sys.exit(app.exec_())
3.2.5.2 场景2:事件拦截与修改
import sys from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QLineEdit, QLabel, QPushButton) from PyQt5.QtCore import Qt, QEventclass EventInterceptDemo(QWidget):def __init__(self):super().__init__()self.initUI()def initUI(self):self.setWindowTitle("事件拦截与修改演示")self.resize(500, 300)layout = QVBoxLayout()label = QLabel("事件拦截功能:\n""- 输入框会拦截并修改输入的数字\n""- 将所有数字乘以2\n""- 演示事件处理函数的强大功能")layout.addWidget(label)self.input_normal = QLineEdit()self.input_normal.setPlaceholderText("普通输入框 - 输入数字不变")layout.addWidget(self.input_normal)self.input_intercept = NumberInterceptLineEdit()self.input_intercept.setPlaceholderText("拦截输入框 - 输入数字会自动×2")layout.addWidget(self.input_intercept)self.result_label = QLabel("结果将显示在这里")layout.addWidget(self.result_label)self.setLayout(layout)class NumberInterceptLineEdit(QLineEdit):def keyPressEvent(self, event):# 只处理数字键if event.text().isdigit():original_number = int(event.text())doubled_number = original_number * 2# 获取父组件来显示结果parent = self.parent()if hasattr(parent, 'result_label'):parent.result_label.setText(f"拦截事件: 数字 {original_number} → {doubled_number}")# 修改事件文本from PyQt5.QtCore import QKeyEventfrom PyQt5.QtGui import QKeyEvent# 创建新的事件,文本是加倍后的数字new_event = QKeyEvent(QEvent.KeyPress,event.key(),event.modifiers(),str(doubled_number))# 传递修改后的事件给父类处理super().keyPressEvent(new_event)# 接受原始事件,阻止默认处理event.accept()else:# 非数字键正常处理super().keyPressEvent(event)if __name__ == '__main__':app = QApplication(sys.argv)window = EventInterceptDemo()window.show()sys.exit(app.exec_())
3.2.6. 事件传播的最佳实践
3.2.6.1 什么时候使用 accept()/ignore()
def eventHandler(self, event):if self.should_handle_event(event):# 处理事件self.handle_event(event)# 如果事件已被完全处理,不需要进一步传播event.accept()return # 注意:调用accept()后通常直接返回else:# 事件未被处理,继续传播event.ignore()super().eventHandler(event) # 通常还是会调用父类方法
3.2.6.2 常见模式总结
场景 推荐做法 说明 完全处理事件 event.accept()
+return
事件被完全处理,不需要传播 部分处理事件 处理逻辑 + super().eventHandler(event)
自定义处理,但保持默认行为 仅监控事件 event.ignore()
+super().eventHandler(event)
只记录事件,不改变默认行为 阻止默认行为 自定义处理 + event.accept()
替换默认行为 3.2.7. 调试事件传播
3.2.7.1 事件传播调试工具
def event(self, event):# 记录所有事件类型event_type = event.type()print(f"事件类型: {event_type}")# 调用默认事件处理return super().event(event)def mousePressEvent(self, event):print(f"鼠标按下: ({event.x()}, {event.y()})")print(f"事件接受状态: {'已接受' if event.isAccepted() else '未接受'}")super().mousePressEvent(event)print(f"处理后状态: {'已接受' if event.isAccepted() else '未接受'}")
总结
事件传播机制是PyQt5中非常强大的功能,理解它可以帮助你:
-
编写更灵活的代码:通过合理使用accept/ignore控制事件流向
-
实现全局功能:在父组件中处理子组件的通用事件
-
调试复杂问题:理解为什么某些事件没有按预期工作
-
创建高级交互:实现事件拦截、修改等高级功能
-
事件从子组件向父组件传播(冒泡)
-
accept()
停止传播,ignore()
继续传播 -
默认情况下,调用父类方法会继续传播
-
键盘事件与焦点相关,鼠标事件与位置相关
3.3 super() 调用父类方法的作用
在 PyQt5 的事件处理机制中,调用
super().eventMethod(event)
起着至关重要的作用。让我通过分析你的代码来详细解释它的作用。基本概念
1. 父类方法的作用
当你在自定义组件中重写事件处理方法时,调用父类方法 (
super().eventMethod(event)
) 的主要作用是: -
执行父类的默认行为
-
保持组件的正常功能
-
确保事件处理的完整性
代码分析
def keyPressEvent(self, event):# 拦截特定按键if event.key() == Qt.Key_A:# 获取父组件来记录日志parent = self.parent()if hasattr(parent, 'log'):parent.log("输入框2: 拦截了A键 - 事件被接受,不显示字符")event.accept() # 接受事件,阻止默认行为# returnelif event.key() == Qt.Key_B:parent = self.parent()if hasattr(parent, 'log'):parent.log("输入框2: 拦截了B键 - 但调用父类方法,会显示字符")# 调用父类方法,会显示字符# super().keyPressEvent(event)# return# 其他按键正常处理# super().keyPressEvent(event)
问题分析
在这个代码中,所有调用
super().keyPressEvent(event)
的语句都被注释掉了,导致: -
A 键处理:调用了
event.accept()
但没有调用父类方法,所以字符 'a' 不会显示 -
B 键处理:没有调用父类方法,所以字符 'b' 也不会显示
-
其他按键:没有调用父类方法,所以任何按键都不会在输入框中显示字符
修复后的代码和详细解释
import sys from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout,QLineEdit, QTextEdit, QLabel) from PyQt5.QtCore import Qtclass KeyboardPropagationDemo(QWidget):def __init__(self):super().__init__()self.initUI()def initUI(self):self.setWindowTitle("键盘事件传播演示")self.resize(500, 400)layout = QVBoxLayout()# 说明label = QLabel("键盘事件传播演示:\n""- 输入框1: 正常处理所有按键\n""- 输入框2: 特殊处理A键和B键\n""- 观察调用父类方法的影响")layout.addWidget(label)# 输入框1 - 正常处理self.input1 = QLineEdit()self.input1.setPlaceholderText("输入框1 - 正常处理所有键盘事件")layout.addWidget(self.input1)# 输入框2 - 拦截特定按键self.input2 = KeyboardInterceptLineEdit()self.input2.setPlaceholderText("输入框2 - 特殊处理A键和B键")layout.addWidget(self.input2)# 日志self.log_display = QTextEdit()self.log_display.setReadOnly(True)layout.addWidget(self.log_display)self.setLayout(layout)def log(self, message):self.log_display.append(message)def keyPressEvent(self, event):# 只有没有任何子组件处理键盘事件时,才会到达这里if event.key() == Qt.Key_Escape:self.log("窗口: 收到ESC键 - 关闭窗口")self.close()else:self.log(f"窗口: 收到未处理按键 {event.key()} - 事件被丢弃")event.accept()class KeyboardInterceptLineEdit(QLineEdit):def __init__(self):super().__init__()def keyPressEvent(self, event):# 拦截特定按键if event.key() == Qt.Key_A:# 获取父组件来记录日志parent = self.parent()if hasattr(parent, 'log'):parent.log("输入框2: 拦截了A键 - 调用 event.accept() 但不调用父类方法")parent.log(" → 结果: 字符 'a' 不会显示在输入框中")event.accept() # 接受事件,阻止默认行为# 注意: 不调用 super().keyPressEvent(event)# 所以 QLineEdit 的默认字符输入行为不会执行returnelif event.key() == Qt.Key_B:parent = self.parent()if hasattr(parent, 'log'):parent.log("输入框2: 拦截了B键 - 调用父类方法但不调用 event.accept()")parent.log(" → 结果: 字符 'b' 会显示在输入框中")# 调用父类方法,会显示字符# QLineEdit 的 keyPressEvent 会处理字符显示super().keyPressEvent(event)# 注意: 不调用 event.accept() 或 event.ignore()# 事件状态由父类方法决定returnelif event.key() == Qt.Key_C:parent = self.parent()if hasattr(parent, 'log'):parent.log("输入框2: 拦截了C键 - 调用 event.ignore() 和父类方法")parent.log(" → 结果: 字符 'c' 会显示,事件继续传播")# 先忽略事件,让事件继续传播event.ignore()# 然后调用父类方法处理字符显示super().keyPressEvent(event)return# 其他按键正常处理 - 调用父类方法parent = self.parent()if hasattr(parent, 'log'):parent.log(f"输入框2: 正常处理按键 {event.key()} - 调用父类方法")super().keyPressEvent(event)if __name__ == '__main__':app = QApplication(sys.argv)window = KeyboardPropagationDemo()window.show()sys.exit(app.exec_())
super() 调用父类方法的具体作用
1. 执行默认行为
对于
QLineEdit
,父类的keyPressEvent
负责: -
显示输入的字符
-
处理光标移动(方向键)
-
处理文本编辑(退格键、删除键)
-
处理选择和复制(Ctrl+A, Ctrl+C 等)
2. 不同场景下的效果
场景1:不调用父类方法 + event.accept()
if event.key() == Qt.Key_A:event.accept() # 接受事件# 不调用 super().keyPressEvent(event)return
效果:按键被完全拦截,字符不会显示,事件不会传播
场景2:调用父类方法 + 不设置事件状态
if event.key() == Qt.Key_B:super().keyPressEvent(event) # 调用父类方法# 不调用 event.accept() 或 event.ignore()return
效果:字符会显示,事件状态由父类方法决定
场景3:调用父类方法 + event.ignore()
if event.key() == Qt.Key_C:event.ignore() # 忽略事件super().keyPressEvent(event) # 调用父类方法return
效果:字符会显示,事件会继续传播给父组件
场景4:正常处理(默认)
super().keyPressEvent(event) # 调用父类方法
深入理解:QLineEdit 的 keyPressEvent
实际上,
QLineEdit.keyPressEvent
的默认实现大致如下:def keyPressEvent(self, event):key = event.key()# 处理字符输入if event.text() and event.text().isprintable():self.insert(event.text()) # 插入字符event.accept()# 处理功能键elif key == Qt.Key_Backspace:self.backspace() # 退格event.accept()elif key == Qt.Key_Delete:self.del_() # 删除event.accept()elif key == Qt.Key_Left:self.cursorBackward(False) # 光标左移event.accept()# ... 其他按键处理else:event.ignore() # 无法处理的按键忽略,继续传播
最佳实践总结
1. 何时调用父类方法
场景 是否调用父类方法 说明 完全自定义处理 不调用 需要完全替换默认行为时 增强默认行为 调用 在默认行为前后添加自定义逻辑 部分拦截 选择性调用 只拦截特定按键,其他保持默认 2. 何时使用 accept()/ignore()
场景 事件状态控制 说明 完全拦截 event.accept()
事件已处理,不需要进一步处理 监控但不干扰 event.ignore()
记录事件但允许正常处理 委托给父类 不设置 让父类方法决定事件状态 3. 推荐模式
def keyPressEvent(self, event):# 模式1: 完全自定义处理if self.should_handle_completely(event):self.custom_handling(event)event.accept() # 明确接受事件return # 不调用父类方法# 模式2: 增强父类处理elif self.should_enhance_parent(event):self.before_parent_handling(event) # 父类前处理super().keyPressEvent(event) # 调用父类方法self.after_parent_handling(event) # 父类后处理# 事件状态由父类方法决定# 模式3: 完全委托给父类else:super().keyPressEvent(event) # 完全由父类处理
结论
在 PyQt5 事件机制中,
super().eventMethod(event)
的作用是: -
保持组件的正常功能:确保默认行为(如字符显示、光标移动)正常工作
-
维护事件处理链:确保父类的清理逻辑和额外处理得到执行
-
提供可扩展性:允许在保持默认行为的同时添加自定义逻辑
关键点:
-
不调用父类方法 → 丢失默认行为
-
调用父类方法 → 保持默认行为
-
event.accept()
/event.ignore()
→ 控制事件传播 -
super().eventMethod(event)
→ 控制默认行为执行在你的原始代码中,注释掉所有
super().keyPressEvent(event)
调用导致输入框失去了显示字符的基本功能,这就是为什么需要调用父类方法的原因。 -
第四阶段:高级事件处理
4.1 事件过滤器
import sys from PyQt5.QtWidgets import (QApplication, QWidget, QLabel, QVBoxLayout, QLineEdit, QTextEdit, QPushButton) from PyQt5.QtCore import Qtclass EventFilterDemo(QWidget):def __init__(self):super().__init__()self.initUI()def initUI(self):self.setWindowTitle("事件过滤器演示")self.resize(500, 400)layout = QVBoxLayout()# 说明标签label = QLabel("事件过滤器测试:\n""- 事件过滤器可以在事件到达目标前拦截处理\n""- 下面输入框安装了事件过滤器,会拦截字母'A'\n""- 按钮安装了事件过滤器,会改变鼠标进入/离开效果")layout.addWidget(label)# 输入框 - 过滤键盘事件self.input1 = QLineEdit()self.input1.setPlaceholderText("输入框1 - 正常输入框")layout.addWidget(self.input1)self.input2 = QLineEdit()self.input2.setPlaceholderText("输入框2 - 安装了事件过滤器,按A键会被拦截")layout.addWidget(self.input2)# 按钮 - 过滤鼠标事件self.button1 = QPushButton("按钮1 - 正常按钮")layout.addWidget(self.button1)self.button2 = QPushButton("按钮2 - 安装了事件过滤器,鼠标悬停有效果")layout.addWidget(self.button2)# 显示区域self.text_display = QTextEdit()self.text_display.setReadOnly(True)layout.addWidget(self.text_display)self.setLayout(layout)# 安装事件过滤器self.input2.installEventFilter(self)self.button2.installEventFilter(self)def eventFilter(self, obj, event):# 处理输入框的键盘事件if obj == self.input2 and event.type() == event.KeyPress:if event.key() == Qt.Key_A:self.text_display.append("事件过滤器: 拦截了输入框2的A键输入")return True # 返回True表示事件已被处理,不再传递# 处理按钮的鼠标事件if obj == self.button2:if event.type() == event.Enter:self.button2.setStyleSheet("background-color: #ffeb3b;")self.text_display.append("事件过滤器: 鼠标进入按钮2")elif event.type() == event.Leave:self.button2.setStyleSheet("")self.text_display.append("事件过滤器: 鼠标离开按钮2")elif event.type() == event.MouseButtonPress:self.text_display.append("事件过滤器: 拦截了按钮2的鼠标点击")return True # 拦截点击事件# 其他事件继续正常处理return super().eventFilter(obj, event)if __name__ == '__main__':app = QApplication(sys.argv)window = EventFilterDemo()window.show()sys.exit(app.exec_())
第五阶段:综合应用
5.1 完整的绘图应用
import sys from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout,QLabel, QPushButton, QComboBox, QColorDialog) from PyQt5.QtCore import Qt, QPoint from PyQt5.QtGui import QPainter, QPen, QColorclass DrawingApp(QWidget):def __init__(self):super().__init__()self.points = []self.current_color = QColor(255, 0, 0) # 红色self.brush_size = 3self.drawing_mode = "自由绘制"self.initUI()def initUI(self):self.setWindowTitle("综合绘图应用 - 事件处理演示")self.resize(600, 500)main_layout = QVBoxLayout()# 控制面板control_layout = QHBoxLayout()# 颜色选择color_btn = QPushButton("选择颜色")color_btn.clicked.connect(self.choose_color)control_layout.addWidget(color_btn)# 画笔大小size_label = QLabel("画笔大小:")control_layout.addWidget(size_label)size_combo = QComboBox()size_combo.addItems(["1", "3", "5", "8", "10"])size_combo.setCurrentText("3")size_combo.currentTextChanged.connect(self.set_brush_size)control_layout.addWidget(size_combo)# 绘制模式mode_label = QLabel("绘制模式:")control_layout.addWidget(mode_label)mode_combo = QComboBox()mode_combo.addItems(["自由绘制", "直线", "矩形", "圆形"])mode_combo.currentTextChanged.connect(self.set_drawing_mode)control_layout.addWidget(mode_combo)# 清除按钮clear_btn = QPushButton("清除画布")clear_btn.clicked.connect(self.clear_canvas)control_layout.addWidget(clear_btn)main_layout.addLayout(control_layout)# 状态显示self.status_label = QLabel("状态: 准备绘制 - 点击画布开始")main_layout.addWidget(self.status_label)# 绘图区域self.drawing_widget = DrawingWidget(self)main_layout.addWidget(self.drawing_widget)self.setLayout(main_layout)# 设置焦点self.drawing_widget.setFocus()def choose_color(self):color = QColorDialog.getColor(self.current_color, self, "选择画笔颜色")if color.isValid():self.current_color = colorself.status_label.setText(f"颜色已更改为: RGB({color.red()}, {color.green()}, {color.blue()})")def set_brush_size(self, size):self.brush_size = int(size)self.status_label.setText(f"画笔大小已更改为: {size}")def set_drawing_mode(self, mode):self.drawing_mode = modeself.status_label.setText(f"绘制模式已更改为: {mode}")def clear_canvas(self):self.drawing_widget.points.clear()self.drawing_widget.update()self.status_label.setText("画布已清除")class DrawingWidget(QWidget):def __init__(self, parent):super().__init__(parent)self.parent_app = parentself.points = []self.current_path = []self.dragging = Falseself.start_point = QPoint()self.setMinimumSize(400, 300)self.setStyleSheet("background-color: white; border: 1px solid #ccc;")# 设置焦点策略self.setFocusPolicy(Qt.StrongFocus)def mousePressEvent(self, event):self.setFocus()if event.button() == Qt.LeftButton:self.dragging = Trueself.start_point = event.pos()if self.parent_app.drawing_mode == "自由绘制":# 自由绘制:开始新的路径self.current_path = [(event.pos(), self.parent_app.current_color, self.parent_app.brush_size)]self.points.append(self.current_path)else:# 其他模式:记录起点self.current_path = []self.parent_app.status_label.setText(f"开始绘制 - 模式: {self.parent_app.drawing_mode}")super().mousePressEvent(event)def mouseMoveEvent(self, event):if self.dragging and event.buttons() & Qt.LeftButton:current_pos = event.pos()if self.parent_app.drawing_mode == "自由绘制":# 自由绘制:添加点到当前路径self.current_path.append((current_pos, self.parent_app.current_color, self.parent_app.brush_size))else:# 其他模式:更新当前路径用于预览self.current_path = [(self.start_point, self.parent_app.current_color, self.parent_app.brush_size),(current_pos, self.parent_app.current_color, self.parent_app.brush_size)]self.update()self.parent_app.status_label.setText(f"绘制中 - 从 ({self.start_point.x()}, {self.start_point.y()}) 到 ({current_pos.x()}, {current_pos.y()})")super().mouseMoveEvent(event)def mouseReleaseEvent(self, event):if event.button() == Qt.LeftButton and self.dragging:self.dragging = Falseend_point = event.pos()if self.parent_app.drawing_mode != "自由绘制" and self.current_path:# 完成形状绘制self.points.append(self.current_path.copy())self.current_path = []self.parent_app.status_label.setText(f"完成绘制 - 从 ({self.start_point.x()}, {self.start_point.y()}) 到 ({end_point.x()}, {end_point.y()})")super().mouseReleaseEvent(event)def keyPressEvent(self, event):if event.key() == Qt.Key_C and event.modifiers() & Qt.ControlModifier:# Ctrl+C 清除画布self.points.clear()self.update()self.parent_app.status_label.setText("画布已清除 (Ctrl+C)")event.accept()elif event.key() == Qt.Key_Z and event.modifiers() & Qt.ControlModifier:# Ctrl+Z 撤销if self.points:self.points.pop()self.update()self.parent_app.status_label.setText("撤销上一步操作 (Ctrl+Z)")event.accept()elif event.key() == Qt.Key_Delete:# Delete 键清除self.points.clear()self.update()self.parent_app.status_label.setText("画布已清除 (Delete)")event.accept()else:super().keyPressEvent(event)def paintEvent(self, event):painter = QPainter(self)painter.setRenderHint(QPainter.Antialiasing)# 绘制所有已完成的路径for path in self.points:if len(path) > 1:# 绘制路径for i in range(1, len(path)):start_point, color, size = path[i-1]end_point, _, _ = path[i]pen = QPen(color, size)painter.setPen(pen)painter.drawLine(start_point, end_point)# 绘制当前正在进行的路径(预览)if self.current_path and len(self.current_path) > 1:start_point, color, size = self.current_path[0]end_point, _, _ = self.current_path[-1]pen = QPen(color, size)if self.parent_app.drawing_mode == "直线":painter.setPen(pen)painter.drawLine(start_point, end_point)elif self.parent_app.drawing_mode == "矩形":painter.setPen(pen)rect = self.get_rect_from_points(start_point, end_point)painter.drawRect(rect)elif self.parent_app.drawing_mode == "圆形":painter.setPen(pen)rect = self.get_rect_from_points(start_point, end_point)painter.drawEllipse(rect)else: # 自由绘制for i in range(1, len(self.current_path)):start_p, color_p, size_p = self.current_path[i-1]end_p, _, _ = self.current_path[i]pen = QPen(color_p, size_p)painter.setPen(pen)painter.drawLine(start_p, end_p)def get_rect_from_points(self, p1, p2):"""从两个点计算矩形"""x = min(p1.x(), p2.x())y = min(p1.y(), p2.y())width = abs(p1.x() - p2.x())height = abs(p1.y() - p2.y())return x, y, width, heightif __name__ == '__main__':app = QApplication(sys.argv)window = DrawingApp()window.show()sys.exit(app.exec_())
学习总结
通过这五个阶段的学习,你应该已经掌握了:
-
基础概念:什么是事件,基本的事件处理模式
-
常用事件:鼠标事件、键盘事件的各种处理方法
-
事件传播:理解事件如何在组件间传播,accept/ignore的作用
-
高级技巧:事件过滤器的使用
-
实践是最好的学习方式:多写代码,多实验
-
理解事件传播:这是事件机制的核心
-
合理使用工具:事件过滤器可以解决很多复杂问题
-
关注用户体验:好的事件处理能大大提升应用体验
-
综合应用:在实际项目中综合运用各种事件处理技术
PyQt5事件机制详解 - DeepSeek