Pyside的选中和焦点
09-26 pyside的焦点
- focus焦点, 哪个item现在吃键盘输入
QGraphicsScene::focusItem()返回它- 最好交给系统自动维护, 系统有(微弱)默认样式
- 一个或没有(nullptr), 可以用来判断当前是否输入状态,
- 系统自动维护他, 比如点击空白, scene就会清掉他.
- selected选中, 哪个 item 高亮
- item.isSelected()
返回true - 同一时间可以 N 个(框选一堆)
- 只能自己维护, 系统没有默认样式
- item.isSelected()
基本概念
- QGraphicsItemGroup切换show/hide时, 不会处理焦点.
- QGraphicsScene 只允许一个焦点项(focusItem() 返回单一项), 焦点项会自动失焦.
各自的生命周期
| 动作 | 是否改 focus | 是否改 selected | 备注 |
|---|---|---|---|
| 鼠标左键点 item | ✅ 变成焦点 | ❌ 不改(除非你代码里 setSelected) |
焦点自动转移 |
| 鼠标中键/右键点 item | ❌ 不改焦点 | ❌ 不改选中 | 完全无影响 |
| 橡皮筋框选 | ❌ 不改焦点 | ✅ 把框内 item 设 selected | 焦点仍留在旧 item |
| 按空格开始编辑 | ✅ 焦点不变 | ✅ 通常把该 item 也设 selected | 自己代码联动 |
| 点空白区(默认) | ✅ Scene 清掉焦点 | ❌ 不改选中 | 出现“无焦点但有选中” |
代码 item->setSelected(true) |
❌ 不改焦点 | ✅ 选中位变 | 需要你自己补 setFocusItem 才联动 |
代码 scene->setFocusItem(item) |
✅ 焦点变 | ❌ 不改选中 | 需要你自己补 setSelected 才联动 |
状态处理
我明白了, 判断编辑状态就在focusitemchange就可以了, 但是, 选中状态其实这么干没用, 因为一定要自己维护, 所以应该分散在键盘鼠标的处理事件中, 先scene.clearSelection() 一键清空所有选中位, 然后再item.setSelected(bool)
scene.clearSelection() + item.setSelected(bool) 这个流程其实不如自己手动维护一个选中的状态item引用方便. 自己维护一个currentitem=xxx
# 编辑状态处理
注册处理事件: scene.focusItemChanged(QGraphicsItem *new, QGraphicsItem *old, Qt::FocusReason)
状态判断依据: scene.focusItem()
# 更简洁的方案, 自己维护一个当前选中项目
current=xxx
# 选中状态处理 传统的不合适的方案
scene.clearSelection() 一键清空所有选中位(不会动焦点)
item.setSelected(bool)
想让对象「可被选」必须加: item.setFlag(QGraphicsItem::ItemIsSelectable, True)
api参考
一、选中(Selection)——“谁被高亮”
- 状态位
item.isSelected() -> boolitem.setSelected(bool)
- Scene 级查询
scene.selectedItems() -> QList<QGraphicsItem*>当前所有被选中的对象scene.selectionArea() -> QPainterPath返回最近一次框选的路径(橡皮筋)scene.clearSelection()一键清空所有选中位(不会动焦点)
- 程序式框选
scene.setSelectionArea(QPainterPath, transform)用任意路径批量选/取消scene.setSelectionArea(path, Qt::ReplaceSelection, ...)可加模式参数
- 必备标志
- 想让对象「可被选」必须加: item.setFlag(QGraphicsItem::ItemIsSelectable, True)
- 信号
scene.selectionChanged()每次选中集合变化时触发(含增/减)
二、焦点(Focus)——“谁吃键盘”
- 状态位
item.hasFocus() -> bool当前是否拥有键盘焦点- 重要
scene.focusItem() -> QGraphicsItem*整个场景里谁在吃键盘(可为空) - 没意义
scene.hasFocus() -> bool场景本身是否拥有输入焦点(View 焦点)
- 设置/拿走
scene.setFocusItem(item, reason)强塞焦点(item 需ItemIsFocusable)scene.setFocus(reason)让场景自己拿焦点(不指定 item)scene.clearFocus()场景放弃焦点(focusItem 变空)
- 必备标志
- 想让对象「可被给焦点」:
item.setFlag(QGraphicsItem::ItemIsFocusable, True)
- 想让对象「可被给焦点」:
- 焦点保护(Qt 5.12+)
scene.setStickyFocus(True)禁止“点空白”自动清焦点(焦点不会掉空)
- 信号
scene.focusItemChanged(QGraphicsItem *new, QGraphicsItem *old, Qt::FocusReason)
每次焦点移动都会发,方便你“视觉选中”跟它同步。
三、最常用“对齐”模板(放在 NavManager 或点击事件里)
def pick_new_current(self, item):
# 1. 焦点搬家
self.scene.setFocusItem(item, Qt.MouseFocusReason)
# 2. 选中搬家(先清旧集合,可只清自己)
for i in self.scene.selectedItems():
i.setSelected(False)
item.setSelected(True)
四、调试小技巧
-
打印焦点链
print("focus:", self.scene.focusItem(), "selected:", self.scene.selectedItems()) -
焦点丢失追踪
连接scene.focusItemChanged信号,在槽里断点或日志,一眼看出谁把焦点抢走了。
20251202更新
- win.show的时候会自动activateWindow, activateWindow会沿着tab链分配焦点, 因此, strongfocus会默认拿到焦点, clickfocus不会.
- 如果要确保某个家伙获得焦点可以,
- 内部showevent解决, 这个是比较软弱的做法, 他可能被外部设置抢走. 是个好做法
- 外部统一控制焦点转移, 强硬且一般情况下是傻逼的做法, 唯一c位会导致越复杂越完蛋
# 比如一个view/win都可以写这个
def showEvent(self, event):
super().showEvent(event)
self.setFocus(Qt.FocusReason.OtherFocusReason)
这样就解决了下面两个问题
- 描述下现在的包装链, 判断下焦点位置: QMainWindow 里面有个QSplitter, QSplitter里面放了一个view(gv), view里面除了绑了一个scene之外, 还放了一个QScrollArea, 这里面放了tab_bar(widget), 这个widget里面有个layout, layout里面放了label, 此时win.show, 那么焦点在哪里? view有焦点吗?
- 链路: win-split-view-scrollarea-widget-label
- qscrollarea, tabbar, 都手动设置了nofocus. label默认是nofocus. split默认是nofocus, view默认是strongfocus
- 没有焦点, 因为链路上都是nofocus, 尤其是尾巴上的都是nofocus.
- 疑问是, 即便如此, view本身不是个widget吗? 他为啥不是默认持有焦点的呢?
- 原因就是前面的两点, show的时候焦点沿tab链传递, view设为了clickfocus因此没有获得焦点. tab链上没有任何widget, 所以, 没有人获得焦点, 此时QApplication.focusWidget()是none
- 描述下view(gv) a里面的textitem获得了焦点, 此时触发的是view的focusInEvent? 如果此时点击了另一个view b的textitem, 那么会触发view a的focusoutEvent?
- 这个focusin是否触发取决于是否是从外部转移进来的焦点.
- 这个focusout一定触发
# 调试验证代码, 某个widget的showevent
def showEvent(self, event):
super().showEvent(event)
return
check_focus(False)
QTimer.singleShot(0, check_focus)
# 3. 窗口显示时设置焦点
if self.splitter.count() == 1:
self.splitter.widget(0).setFocus(Qt.FocusReason.OtherFocusReason)
def check_focus(is异步=True):
focus_widget = QApplication.focusWidget()
print(f"焦点检查 (异步={is异步}):")
if focus_widget:
print(f"焦点在: {focus_widget.objectName()} (类型: {type(focus_widget).__name__})")
else:
print("当前没有焦点") # 如果tab链没有东西, 那么永远是这个