Pyside的事件
pyside的事件机制
我在macos15.7中使用pyside6编写app, 在gv(view/scene)架构中要处理快捷键. 我的edititem在输入状态时, 貌似不响应shortcut和action, 是这样的吗? 我有2组快捷键, 分别处理两个情况:
- 处理所有的edititem都不在编辑状态的快捷输入, 比如
- 移动光标(item的选中状态),
- 移动/交换某个item
- 某个textitem在编辑状态, 输入时, 响应快捷键, 比如
- 结束编辑,
- 移动光标.
那么我要怎么分布这些快捷键? 我本来考虑都用无父的action来处理, 但是这样似乎对于tab, enter,这种单独的按键不太行, 对于编辑状态的textitem也貌似有点问题. 是我哪里搞错了, 还是什么原因?
不要再给代码了, 咱们说原理.
- 编辑状态的事件必须在QGraphicsTextItem内部处理, 其中子类化重载keyPressEvent是比较简洁低耦合的处理方式, 包括编辑态时回车和tab的响应. 那么如何判断自身处在编辑态呢? hasfocus只是获得焦点, 不代表自身在编辑态吧?
- 非编辑态用action处理, 此时能处理enter/tab吗?
我感觉对于pyside6的事件处理还是比较懵的, 你能给我新建一个测试文件, 让我看明白, widget 和 graphicitem的所有事件处理机制吗? 尤其是action/shortcut, 我期望能干净的处理全局快捷键和编辑状态的快捷键. 现在看来. 用action+item自己的presskey是不是最干净简洁低耦合的方案? 最清爽不容易冲突的方案? 用个例子说明下.
我看了我原始的代码, 我原本在graphicview的keypressevent里面处理了编辑态的item的快捷键. 为啥他能处理?
另外, 我感觉对于pyside6的事件处理还是比较懵的, 尤其是action/shortcut, 我期望能干净的处理全局快捷键和编辑状态的快捷键. 现在看来. 用action+item自己的presskey是不是最干净简洁低耦合的方案? 最清爽不容易冲突的方案?
编辑态和非编辑态的事件传播是怎样的? action/shortcut/filter的机制是怎样发挥作用的? 帮忙讲讲原理, 让我建立基本的mindmap
我看你的描述, 先说了shortcut是最后的处理机会, 优先是EventFilter > keyPressEvent > Shortcut, 但是流程图的步骤中又是:
- shortcut处理
- eventfilter处理, 此时可以拦截.
- 目标的责任链(keypressevent), 此时也可以拦截.
那么他们的顺序究竟是怎样的? 这里貌似矛盾了, 讲清楚他们的顺序, 以及每一个手段的拦截关系. 是必然被拦截, 还是可以拦截, 还是不可以拦截.
你说的好乱啊, 我问的是graphicview里面的graphictextitem, 你说了一堆的widget. 然后, 下面这些说法对吗?
QAction / QShortcut 机制, 实际是在父widget上安装filter
你讲的是widget, 我问的是graphicitem, 这个一样吗? 内部编辑器是一个QWidget,有自己的焦点和事件处理!也就是说graphicitem中有个widget?
另外, 方向键(上下左右), 回车键, tab键, 在shortcut/action中处理貌似是可以的. 但是, 如果在keypressevent中就不一定了. 为什么? shortcut/action/filter的优先级很高吗? 这些事件响应的手段之间的阻挡关系式怎样的? 另外, 我的app里面是多个window, 然后里面分栏, 每个栏里面有widget作为tabbar, 然后每个栏都是个graphicview里面还有能进入编辑状态的textitem. 所以我这个架构本身就有点复杂, 所以这是我需要一个牢靠的事件架构的原因.
建议
graph TD
subgraph "Window级事件域"
W1[MainWindow] --> WAction[Window全局Action]
W1 --> WFilter[Window事件过滤器]
end
subgraph "Pane级事件域(分栏)"
P1[PaneWidget] --> PAction[Pane局部Action]
P1 --> PFilter[Pane事件过滤器]
end
subgraph "View级事件域"
V1[GraphicsView] --> VAction[View专用Action]
V1 --> VFilter[View事件过滤器]
end
subgraph "Item级事件域"
I1[TextItem] --> IKeyPress[Item.keyPressEvent]
I1 --> IInternal[Item内部状态]
end
W1 --> P1
P1 --> V1
V1 --> I1
style WAction fill:#f9f
style PAction fill:#ff9
style VAction fill:#9ff
style IKeyPress fill:#9f9
| 层级 | 职责 | 手段 | 快捷键示例 |
|---|---|---|---|
| Window | 应用级命令(保存、退出) | QAction + ApplicationShortcut 上下文 |
Ctrl+S, Ctrl+Q |
| Pane | 面板级命令(切换标签、分割) | QAction + WindowShortcut 上下文 |
Ctrl+Tab, Ctrl+T |
| View | 视图级命令(缩放、平移) | QAction + WidgetShortcut 上下文 |
+/-, Space |
| Item | 编辑态命令(结束编辑、缩进) | 重载 keyPressEvent |
Enter, Tab, Esc |
原理澄清
[非编辑态]
用户按键 → Shortcut系统 → QAction → Command对象 → Scene/View
↑
纯数据,无状态
[编辑态]
用户按键 → Item.keyPressEvent → Item内部状态管理 → 编辑命令
↑
自包含,不依赖外部
事件图
OS 键盘事件
↓
QGraphicsView(第一层:可选预处理)
↓
QGraphicsScene(第二层:可选预处理)
↓
QGraphicsItem(第三层:焦点项)
↓
【分岔】
├─ 有 TextEditorInteraction 标志?
│ └─ YES → QTextControl(内部编辑器,消耗事件)
│ └─ 事件结束
│ └─ NO → item.keyPressEvent() → item.ignore()
│ ↓
│ 事件继续冒泡
│ ↓
└─ NO焦点项 → Shortcut系统 → QAction → 全局命令
| 机制 | 触发时机 | 作用域 | 能否拦截 Editor |
|---|---|---|---|
| eventFilter | 事件到达对象前 | 任意对象 | ✅ 能拦截 |
| keyPressEvent | 事件到达对象时 | 当前对象 | ❌ 不能拦截 Editor |
| QAction/Shortcut | 事件未被 accept 时 | 全局/Widget | ❌ 不能拦截 Editor |
┌─────────────────────────────────────────────────┐
│ Qt 事件处理三大层次 │
├─────────────────────────────────────────────────┤
│ Level 1: 事件过滤器 (Event Filters) │
│ ↑↓ 最先/最后拦截 │
├─────────────────────────────────────────────────┤
│ Level 2: 事件处理函数 (Event Handlers) │
│ ↑↓ 对象自身处理 │
├─────────────────────────────────────────────────┤
│ Level 3: 动作系统 (Actions/Shortcuts) │
│ ↑↓ 全局快捷键/菜单 │
└─────────────────────────────────────────────────┘
非编辑态, 此时焦点在view上
关键点:非编辑态时,Action/Shortcut在View处理之前就有机会匹配!
键盘按下 → 操作系统 → QApplication
↓
QApplication.event()
↓
[Level 1] QApplication的事件过滤器
↓
[Level 3] QShortcut/QAction 全局匹配 ←─┐
↓ │
QGraphicsView.keyPressEvent() │
↓ │
[Level 1] View的事件过滤器 │
↓ │
super().keyPressEvent() → 传递给Scene → 传递给Item
│ │
└────→ 如果View不处理 ────────────────┘
编辑态
编辑态时,焦点在Item的”内部编辑器”,不是Item本身
内部编辑器是一个独立的QWidget,有自己的事件循环
Action/Shortcut默认无法”穿透”到这个内部编辑器
键盘按下 → 操作系统 → QApplication
↓
QApplication.event()
↓
[Level 1] App过滤器
↓
[Level 3] QShortcut 尝试匹配 ❌ ←─ 通常失败!
↓
QGraphicsView.keyPressEvent() ←─ 你可能在这里拦截了!
↓
super().keyPressEvent() → QGraphicsScene
↓
QGraphicsScene.keyPressEvent()
↓
QGraphicsTextItem.keyPressEvent() ←─ 编辑态在这里
│ 内部有QTextDocument
↓
[文本编辑器内部处理] ←─ 事件被”吞噬”在这里
- QAction / QShortcut 机制, 实际是在父widget上安装filter,
- Action匹配时机:在QApplication.event()分发时检查,早于具体Widget的keyPressEvent!
- filter机制, 安装顺序:后安装的先执行(栈式), 返回值:
- True:事件被过滤,不再传递
- False:继续传递给下一个过滤器或对象本身
从早到晚的拦截机会:
- QApplication的事件过滤器 (最早)
- QShortcut/QAction 全局匹配
- 目标Widget的事件过滤器
- 目标Widget.keyPressEvent() ← 你通常在这里处理
- 父Widget的事件传播
重要:一旦事件被accept(),传播链就终止!
一、事件流的本质:不是传播,是”责任链”
Qt 的事件处理基于 “处理即终止” 原则,不是自动传播,而是先到先得,处理即消化。
graph TD
OS[OS键盘事件] --> QtEventLoop[Qt事件循环]
QtEventLoop -->|未标记| ShortcutSystem[Shortcut系统]
QtEventLoop -->|已标记目标| TargetChain[目标对象链]
TargetChain --> View[QGraphicsView]
View --> Scene[QGraphicsScene]
Scene --> FocusItem[当前焦点Item]
FocusItem -->|编辑态| QTextControl[QTextControl内部编辑器]
FocusItem -->|非编辑态| ItemKeyPress["keyPressEvent()"]
QTextControl -->|"accept()"| EventConsumed[事件被消耗, 传播终止]
ItemKeyPress -->|"ignore()"| BubbleUp[事件冒泡回Scene/View]
ShortcutSystem -->|匹配成功| ShortcutAction[QAction触发]
ShortcutSystem -->|无匹配| EventDiscarded[事件丢弃]
非编辑态
graph TD
A[用户按键] --> B{QGraphicsView是否有焦点?}
B -->|是| C{View是否有匹配的QAction?}
C -->|是| D[触发QAction]
C -->|否| E{Scene是否有事件过滤器?}
E -->|是| F[过滤器处理]
E -->|否| G{Scene是否有focusItem?}
G -->|是| H["item.keyPressEvent()"]
H -->|"item.ignore()"| I[事件返回View]
I --> J{View是否有Shortcut?}
J -->|是| K[QShortcut触发]
J -->|否| L[事件丢弃]
B -->|否| M[事件不进入Scene]
编辑态
graph TD
A[用户按键] --> B[事件直达QTextControl]
B --> C{QTextControl是否处理此键?}
C -->|"是(字母/数字/Enter/Tab)"| D["QTextControl.accept()"]
D --> E[事件立即终止, 不向上传播]
C -->|"否(Esc等特殊键)"| F{QTextControl是否允许冒泡?}
F -->|是| G["调用item.keyPressEvent()"]
F -->|否| H[事件丢弃]
G --> I{"item是否调用super()?"}
I -->|是| J[事件传给父类QGraphicsTextItem]
J --> K[事件可能被父类处理或ignore]
eventfilter
战场:事件到达目标前,最高优先级
权限:能拦截编辑态事件,但必须主动安装在 QTextControl 上(难度大,不推荐)
graph TD
A[事件生成] --> B{是否被监控?}
B -->|是| C["EventFilter::eventFilter()"]
C --> D{返回True?}
D -->|True| E[事件被拦截, 传播终止]
D -->|False| F[事件继续传递给目标对象]
B -->|否| G[直达目标对象]
subgraph "监控范围"
H[可监控任何QObject]
I[包括QGraphicsView/Scene/Item]
J["包括QTextControl(需手动安装)"]
end
keypressevent 自治机制
graph TD
A[事件到达对象] --> B{对象是否重载keyPressEvent?}
B -->|是| C[执行用户代码]
C --> D{"是否调用event.accept()?"}
D -->|"是(默认)"| E[事件被标记为已处理, 传播终止]
D -->|"否(event.ignore())"| F[事件标记为未处理, 向上传播]
B -->|否| G[调用父类默认实现]
G --> H{"父类是否accept()?"}
H -->|是| I[事件终止]
H -->|否| J[继续冒泡]
shortcut 全局路由机制
战场:事件循环中,独立于对象传递链,最低优先级(实际上是最后机会)
权限:无法看到编辑态事件,因为已被 QTextControl.accept()
graph TD
A[事件在事件循环中] --> B{"事件是否被accept()?"}
B -->|是| C[Shortcut系统忽略此事件]
B -->|否| D{事件是否匹配快捷键?}
D -->|是| E[触发QAction/QShortcut]
D -->|否| F[事件继续传递]
subgraph "匹配范围"
G[仅在当前Widget及其父链]
H[需要Widget有焦点或Shortcut上下文允许]
I[Edit态事件已被accept, 不可见]
end
┌─────────────────────────────────────────────────────────────┐
│ Qt键盘事件处理总览 │
│ “事件不是传播,是责任链处理” │
└─────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────┴──────────────────┐
│ 事件生成(OS → Qt事件循环) │
└──────────────────┬──────────────────┘
│
┌──────────────────┴──────────────────┐
│ 第一步:Shortcut系统预检 │
│ (事件是否已accept?否 → 匹配快捷键)│
└──────────────────┬──────────────────┘
│
┌──────────────────┴──────────────────┐
│ 第二步:EventFilter链(若有) │
│ (返回True → 事件终止) │
└──────────────────┬──────────────────┘
│
┌──────────────────┴──────────────────┐
│ 第三步:目标对象责任链 │
└──────────────────┬──────────────────┘
│
┌───────────────────┼───────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 非编辑态 │ │ 编辑态 │ │ 无焦点项 │
├──────────────┤ ├──────────────┤ ├──────────────┤
│View→Scene→Item│ │直达QTextControl│ │回到Shortcut │
│可ignore()冒泡 │ │强制accept() │ │系统 │
│Shortcut可见 │ │Shortcut不可见│ │ │
└──────────────┘ └──────────────┘ └──────────────┘
编辑态 = 事件黑洞:一旦开启 TextEditorInteraction,事件进入独立宇宙,Shortcut 系统失明
非编辑态 = 事件民主:事件自由流动,Shortcut 系统可见,可冒泡
优先级: EventFilter > keyPressEvent > Shortcut
graph TD
A[OS生成QKeyEvent] --> B[Qt事件队列]
B --> C{事件投递到目标Widget前}
C --> D1[阶段1: ShortcutOverride事件]
D1 --> D2{QShortcut是否匹配?}
D2 -->|是| D3[发送ShortcutOverride事件]
D3 --> D4{目标是否accept override?}
D4 -->|accept| D5[事件被软化,允许Shortcut激活]
D4 -->|ignore| D6[Shortcut被阻止]
D2 -->|否| D7[跳过ShortcutOverride]
C --> E1[阶段2: EventFilter执行]
E1 --> E2{filter返回True?}
E2 -->|True| E3[事件被拦截,生命周期结束]
E2 -->|False| E4[事件继续传递给目标]
E4 --> F1[阶段3: 目标keyPressEvent]
F1 --> F2{"是否调用event.accept()?"}
F2 -->|"accept (默认)"| F3[事件被消费,停止冒泡]
F2 -->|"explicit ignore()"| F4[事件标记为未处理,向上冒泡]
F4 --> G1{父Widget响应}
G1 -->|有keyPressEvent| G2[父类处理]
G1 -->|无| G3{Shortcut系统最终检查}
G3 -->|事件未被accept且匹配| G4[触发QAction/QShortcut]
G3 -->|事件已被accept| G5[Shortcut被屏蔽]
graph LR
A[事件生成] --> B(EventFilter)
B -->|返回True| C[事件死亡]
B -->|返回False| D(keyPressEvent)
D -->|"accept()"| E[事件死亡]
D -->|"ignore()"| F{Shortcut系统}
F -->|匹配且未被阻挡| G[触发Shortcut]
F -->|事件已被accept| H[Shortcut被阻挡]
| 按键 | 在keyPressEvent中 | 在Shortcut中 | 原因 |
|---|---|---|---|
| 方向键 | 默认accept(),不冒泡 | 可捕获 | QWidget默认处理焦点移动,主动accept() |
| Enter | 默认accept(),不冒泡 | 可捕获 | 默认激活按钮/默认操作,主动accept() |
| Tab | 默认accept(),不冒泡 | 可捕获 | 焦点切换键,被事件系统特殊处理 |
| 字母/数字 | 默认accept(),输入字符 | 可捕获 | 输入事件,被当前焦点Widget accept() |
为什么 Shortcut 能”反杀”默认行为?
核心机制:ShortcutOverride 事件
Shortcut 系统先发制人,在事件投递前询问目标是否允许
如果目标不反对(ignore() the override),Shortcut 就劫持事件
默认情况下,Widget 对 ShortcutOverride 是 accept() 的(允许 Shortcut)
但 QTextControl 对 所有 override 都是 ignore() 的(垄断事件)
# Qt内部简化逻辑
def deliverKeyEvent(event):
# 阶段1:ShortcutOverride预检
if matchesShortcut(event): # 事件是否匹配注册快捷键?
override_event = QShortcutOverrideEvent(event)
sendEvent(focus_widget, override_event)
if override_event.isAccepted(): # 目标Widget说"我允许Shortcut"
# 阻止原本的keyPressEvent投递
triggerShortcutAction() # 触发QAction
return # 事件处理结束
# 阶段2:正常keyPressEvent投递
sendKeyPressEvent(event) # 这会触发EventFilter→keyPressEvent链
非编辑态
键盘事件 → QGraphicsView → QGraphicsScene → QGraphicsItem
↑
QAction有机会匹配(因为焦点在View)
编辑态
键盘事件 → QGraphicsView → QGraphicsScene
↑ ↓
QAction QGraphicsProxyWidget(内部编辑器)
↓
QLineEdit/QTextEdit ← 焦点在这里!
↓
事件被处理,不传播
最终问题:
所以编辑态下, graphicedititem是没有机会处理keypressevent的. 此时只用用eventfilter.
或者用graphicview的keypressevent. 那么, 这两个方案, 哪个更合理些呢?
在这个问题上, deepseek, kimi都开始胡说八道了. 我要用代码验证下.
- 无父action/shortcut.
- 全局filter
- graphicview:
- keypressevent
- eventfilter
- grahpicedititem的
- keypressevent
- eventfilter
graph TD
A[键盘事件到达View] --> B{当前焦点是否在TextItem?}
B -->|是| C{Item是否开启TextEditorInteraction?}
C -->|"是 (编辑态)"| D["Item.keyPressEvent()执行"]
D --> E{是否特殊快捷键?}
E -->|"是 (Tab/Enter/Esc)"| F[Item处理并accept]
E -->|否| G["调用super()传给QTextControl"]
C -->|"否 (非编辑态)"| H["Item.keyPressEvent()执行"]
H --> I{"Item是否accept()?"}
I -->|否| J[事件冒泡到View]
B -->|否| K["View.keyPressEvent()处理"]
K --> L{是否View级快捷键?}
L -->|是| M[View处理并accept]
L -->|否| N{是否匹配QAction?}
N -->|是| O[触发Action]
N -->|否| P[事件丢弃]
graph TD
A[按键发生] --> B{Qt检查是否有匹配的QAction/Shortcut}
B -->|有匹配| C[生成QShortcutOverride事件]
C --> D{"ShortcutOverride是否被accept()?"}
D -->|"是 (阻止Shortcut)"| E1[继续传递为QKeyEvent]
E1 --> F["keyPressEvent被执行"]
F --> G{"keyPressEvent中是否accept()?"}
G -->|是| H[事件终止]
G -->|否| I["event.ignore() → 事件冒泡给父Widget"]
D -->|"否 (允许Shortcut)"| E2[触发QAction/Shortcut]
E2 --> J["QAction::triggered()信号发射"]
J --> K[事件被消费,keyPressEvent不会执行]
B -->|无匹配| L[直接传递为QKeyEvent]
L --> F
graph TD
A[按键发生] --> B[ShortcutOverride决策点]
B -->|路径A: accept Override| C[keyPressEvent执行]
C --> D{"keyPressEvent中accept()?"}
D -->|是| E[事件终止]
D -->|否| F[事件冒泡给父Widget]
B -->|路径B: ignore Override| G[QAction触发]
G --> H[事件终止]
B -->|路径C: 无匹配Shortcut| C
style B fill:#f9f,stroke:#333,stroke-width:4px
style G fill:#f9f,stroke:#333,stroke-width:4px
graph TD
A[OS键盘事件] --> B["QApplication::notify()"]
B --> C{事件类型检查}
C -->|QEvent::KeyPress| D[检查是否有匹配的QAction/QShortcut]
D -->|找到匹配| E[生成QEvent::ShortcutOverride]
D -->|无匹配| F[直接投递QKeyEvent]
E --> G{目标Widget是否处理Override?}
G -->|"接受(accpet)"| H[阻止QAction触发]
G -->|"忽略(ignore)"| I[触发QAction,阻止QKeyEvent]
graph TD
A[按键事件到达] --> B[QApplication检查是否有注册Action/Shortcut]
B -->|有| C[生成ShortcutOverride事件]
B -->|无| D[直接生成KeyPress事件]
C --> E{"目标Widget的event()处理Override?"}
E -->|"Widget.accept()"| F1[keyPressEvent将执行]
E -->|"Widget.ignore()"| G1[QAction触发,keyPressEvent跳过]
E -->|"Widget未处理(默认)"| H1{父类默认行为?}
H1 -->|"文本输入Widget"| I1["accept() - 阻止Action"]
H1 -->|"普通Widget"| J1["ignore() - 允许Action"]
D --> K1[keyPressEvent执行]
style C fill:#f9f,stroke:#333,stroke-width:4px
style E fill:#f9f,stroke:#333,stroke-width:4px
所以最终结论:
- 用无父action/shortcut处理全局快捷键.
- 用graphictextitem的keypressevent处理编辑态的快捷键.
- 不论是方向键, tab, enter, esc, 删除键等等按键都可以这么处理:
- 在非编辑态, 无父action处理这些快捷键(他们分别有针对item的各种整体性操作)
- 在编辑态, 用graphictextitem的keypressevent处理这些快捷键, 这些操作只要针对正在编辑的文本内容.
可能你还是没理解, 咱们就搞一个enter键来说明.
- 如果此时全局都没有item在编辑状态, 那么:
无父action会找到当前焦点的view(他是个分栏)的当前选中状态的item, 然后给他新建一个弟弟item放在旁边. - 如果此时有一个item在编辑状态, 那么:
这个item退出编辑状态, 但是保持选中状态.
这其中focus状态由qt系统维护, 选中状态由我手动维护.
这个方案是不是一个最合理的方案?
还有更终极的疑问
new_act = QAction(txt, _global_menu)
new_act.setShortcut(QKeySequence(keys))
这里的keys是不是不支持单字符? 有文档吗? 我去看看, 和文档没关系, 支持单字符, 我之前就是成功了tab.
下面这两种写法是不同的吗? 太神奇了. 一个有响应, 一个没有
new_act = QAction(txt, _global_menu)
new_act.setShortcut(QKeySequence(“Tab”))
QShortcut(QKeySequence(“Tab”), tab).activated.connect(new_son)
是的就是不一样的. 并且shortcut要注册在widget上面.
搏斗了一天, 应该用shortcut搞单字符快捷键. 并且注册在view上面, 不对window也行, QTabWidget也行, 所有的widget都行吧
目前我的app里面是多个window, 然后里面分栏, 每个栏里面有widget作为tabbar, 然后每个栏都是个graphicview里面还有能进入编辑状态的textitem.
目前我的做法是:
- 直接在viewclass里面写那些单按键的shortcut,
- 原本带修饰键的快捷键还留在无父action里面.
- 编辑态的快捷键在textitem的子类里面写keypressevent
但是看样例代码, 应该不需要一定把shortcut放到view上面, 放到window上也行. 应该是只要声明到父widget上都可以的. 你觉得放到哪个层级更合理?
其实看代码行数最合理, window做的事情本来就比view少, 所以放到win上挺合适.