Pyside的键盘事件处理
pyside有3种键盘处理方式
全局快捷键的最佳实践
- 定义在menu上, 参见 10-08-pyside菜单
事件处理
- ShortcutOverride(QKeyEvent预事件)发送到焦点widget, 决定是否跳过shortcut, 如果跳过, 走默认流程
- 逐层(App→View→Scene→Item)
- 事件
- 先经filter(eventFilters(),可短路 return True)
- 然后event() 可短路accept()
- 如果不跳过
- shortcut匹配, 阻断后续event处理, 一定短路后续所有处理, 不再生成event
- 否则, 走之前的默认流程
这样就可以解释: TextItem输入焦点时,作为焦点widget,accept ShortcutOverride事件,跳过QShortcutMap匹配,直接消耗键事件为文本输入(Qt默认行为,防shortcut干扰编辑)。
QGraphicsView的特殊性
- QGraphicsView 的 viewport(视口 widget 渲染场景)和 sceneEvent(转发 viewport 事件到 scene/item)是其独特桥接机制,与标准 QWidget 直接事件处理不同:View 捕获事件后路由到 Scene/Item 层,避免直接 widget-item 冲突
- 处理顺序:
- viewport 是一个纯QWidget, 这是预处理, 适合处理短路filter, 不处理坐标(因为这里是设备坐标), 全局快捷键, 拦截空白区点击丢焦点事件.
- QGraphicsView 自身的 event() / eventFilter, 这里处理自身事件, 滚轮, 悬停, f1快捷键等等.
- sceneEvent, 这是后处理, 使用scene逻辑坐标, 因此, 处理轻触在这里.
- 这里处理之后事件进入secen->item派发链
| 机制 | QWidget | QGraphicsView |
|---|---|---|
| 事件最先到达 | 自身 | viewport(QWidget) |
| 坐标系 | 自身像素 | viewport 像素 → sceneEvent 转逻辑坐标 |
| 可重写入口 | event() | sceneEvent() + event() |
| 过滤器最早点 | self | viewport() |
- 空白点击不丢选中 → 过滤器装在 viewport()
- 全局快捷键比编辑框优先 → 在 viewport() 里拦截
ShortcutOverride - 统一缩放/坐标篡改 → 重写 sceneEvent() 后改
event.pos()再super() - 性能统计 → 在 sceneEvent() 里打点,能覆盖所有 Scene→Item 事件
纯键盘
- 重写
keyPressEvent, 简单, 开销小, 猴子补丁, 历史最久, 首选方案
import sys
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QApplication, QWidget
app = QApplication(sys.argv)
w = QWidget()
w.keyPressEvent = lambda e: print('key:', e.key()) if e.key() == Qt.Key_B else None
w.show()
sys.exit(app.exec())
- 只对当前 widget 生效
- 简单直接,但只能改“自己”
- 事件过滤器, 通用钩子, 跨对象, 跨事件类型(键盘+鼠标), 次选方案
import sys
from PySide6.QtCore import Qt, QEvent
from PySide6.QtWidgets import QApplication, QWidget
app = QApplication(sys.argv)
w = QWidget()
def keyFilter(obj, ev):
if ev.type() == QEvent.KeyPress and ev.key() == Qt.Key_A:
print('按了 A')
return True # 吃掉事件
return False
w.installEventFilter(keyFilter) # 纯函数当过滤器
w.show()
sys.exit(app.exec())
- 能拦截任何对象、任何事件
- 不破坏原有事件流(返回 False 就继续传)
- QShortcut, 全局/局部热键,无需焦点, 精确控制组合键, 全局热键方案
```python
import sys
from PySide6.QtGui import QKeySequence
from PySide6.QtWidgets import QApplication, QWidget
app = QApplication(sys.argv)
w = QWidget()
from PySide6.QtWidgets import QShortcut
QShortcut(QKeySequence(“Ctrl+C”), w, activated=lambda: print(“Ctrl+C 触发”))
w.show()
sys.exit(app.exec())
- 只要窗口在,**不管谁有焦点**都能响应
- 只能做“快捷键”,拿不到原始 `QKeyEvent`(不知道组合键细节)
- 就算注册在btn上, 他的触发依旧是全局的, parent只是和他的生命周期有关
```py
btn = QPushButton("Owner", main_window)
sc = QShortcut(QKeySequence("A"), btn) # parent 是按钮
# context 默认 WindowShortcut → 整个窗口有效
sc.activated.connect(lambda: print("快捷键A"))
# 再做一个新的widget, 证明和焦点无关
btn2=QPushButton(" No Owner", main_window)
一句话对比
| 方式 | 作用域 | 能否拿原始事件 | 适合场景 |
| ———————— | ———– | ——————- | ——————— |
| 重写虚函数 keyPressEvent | 单个 widget | ✅ | 快速测试、简单 widget |
| 事件过滤器 | 任意对象 | ✅ | 全局拦截 |
| QShortcut | 整个窗口 | ❌(只有 activated) | 热键、菜单加速键 |
键鼠组合
- 在鼠标事件里当场查键盘修饰符(最常用)
```python
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QApplication, QWidget
app = QApplication([])
w = QWidget()
w.mousePressEvent = lambda e: (
print(‘Ctrl+左键’ if e.modifiers() & Qt.ControlModifier and e.button() == Qt.LeftButton else ‘普通左键’)
)
w.show()
app.exec()
- 任何鼠标事件(`mousePress/Move/Release`)都带 `QMouseEvent.modifiers()`
- 现场判断 Ctrl / Shift / Alt / Meta 即可,**一行代码搞定**
------------------------------------------------
2. 用事件过滤器(跨对象或需要预处理)
```python
from PySide6.QtCore import QEvent, QObject, Qt
class FilterShell(QObject):
def eventFilter(self, obj, ev):
if ev.type() == QEvent.MouseButtonPress and ev.modifiers() & Qt.ControlModifier:
print('过滤器截到 Ctrl+点击')
return False # true = 吃掉
return False
shell = FilterShell()
w.installEventFilter(shell)
- 适合“父窗口统一拦截子控件”
- 同样靠
ev.modifiers(),只是换了个拦截点
tab切换焦点 - 全局废弃此能力
# 方案一: 只要这一条就完全屏蔽了
QShortcut(QKeySequence("Tab"), tab).activated.connect(new_son)
# 方案二:
def tab_eater(obj, ev):
if ev.type() == ev.KeyPress and ev.key() == Qt.Key_Tab:
# 这里调用你的逻辑
new_son()
return True # 吃掉,不再分发
return False
app.installEventFilter(tab_eater)
设置nofocus也挺好
setFocusPolicy(Qt.FocusPolicy.NoFocus)
# 这里唯一的问题是, keypressevent会不响应了. 此时为了键盘响应, 设置为下面这个
view.setFocusPolicy(Qt.FocusPolicy.ClickFocus)
# 设置这个, 就是tab可以跳进来
view.setFocusPolicy(Qt.FocusPolicy.TabFocus)
# 此时, 响应快捷键更好的思路是用shortcut
# item-----
# item 这个的意思是键盘tab可以导航过来, 改为false就是不允许tab搞focus
item.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsFocusable, True)
# 接受鼠标点击编辑,但不会被 Tab 键选中。
item.setTextInteractionFlags(Qt.TextEditorInteraction)
# 焦点传递
window.setFocusProxy(tab)
# 表示将 window(通常是 QWidget 或其子类,如 QMainWindow)的焦点代理设置为 tab(另一个 QWidget 对象)。这意味着当 window 接收到焦点时,焦点会自动传递给 tab。
view.setFocus(Qt.FocusReason.TabFocusReason)
# 表示将焦点设置到 QGraphicsView 上,并指定焦点原因是 Tab 键导航。
总结
-
shortcut是优先级最高的, 只要有他别的都没有了, 他是一定会截断的.
-
过滤器有下沉机制, 会从最外层一路执行到最内层, 除非return true.
-
event有冒泡机制, 除非accept —-这个我没验证, 上面2个都验证过了.