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 事件

纯键盘


  1. 重写 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 生效
  • 简单直接,但只能改“自己”

  1. 事件过滤器, 通用钩子, 跨对象, 跨事件类型(键盘+鼠标), 次选方案
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 就继续传)

  1. 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) | 热键、菜单加速键 |

键鼠组合


  1. 在鼠标事件里当场查键盘修饰符(最常用)
    ```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个都验证过了.