09-26 pyside的焦点

  1. focus焦点, 哪个item现在吃键盘输入
    1. QGraphicsScene::focusItem() 返回它
    2. 最好交给系统自动维护, 系统有(微弱)默认样式
    3. 一个或没有(nullptr), 可以用来判断当前是否输入状态,
      1. 系统自动维护他, 比如点击空白, scene就会清掉他.
  2. selected选中, 哪个 item 高亮
    1. item.isSelected() 返回 true
    2. 同一时间可以 N 个(框选一堆)
    3. 只能自己维护, 系统没有默认样式
基本概念
  1. QGraphicsItemGroup切换show/hide时, 不会处理焦点.
  2. 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)——“谁被高亮”

  1. 状态位
    • item.isSelected() -> bool
    • item.setSelected(bool)
  2. Scene 级查询
    • scene.selectedItems() -> QList<QGraphicsItem*> 当前所有被选中的对象
    • scene.selectionArea() -> QPainterPath 返回最近一次框选的路径(橡皮筋)
    • scene.clearSelection() 一键清空所有选中位(不会动焦点)
  3. 程序式框选
    • scene.setSelectionArea(QPainterPath, transform) 用任意路径批量选/取消
    • scene.setSelectionArea(path, Qt::ReplaceSelection, ...) 可加模式参数
  4. 必备标志
    • 想让对象「可被选」必须加: item.setFlag(QGraphicsItem::ItemIsSelectable, True)
  5. 信号
    • scene.selectionChanged() 每次选中集合变化时触发(含增/减)

二、焦点(Focus)——“谁吃键盘”

  1. 状态位
    • item.hasFocus() -> bool 当前是否拥有键盘焦点
    • 重要scene.focusItem() -> QGraphicsItem* 整个场景里谁在吃键盘(可为空)
    • 没意义scene.hasFocus() -> bool 场景本身是否拥有输入焦点(View 焦点)
  2. 设置/拿走
    • scene.setFocusItem(item, reason) 强塞焦点(item 需 ItemIsFocusable
    • scene.setFocus(reason) 让场景自己拿焦点(不指定 item)
    • scene.clearFocus() 场景放弃焦点(focusItem 变空)
  3. 必备标志
    • 想让对象「可被给焦点」: item.setFlag(QGraphicsItem::ItemIsFocusable, True)
  4. 焦点保护(Qt 5.12+)
    • scene.setStickyFocus(True) 禁止“点空白”自动清焦点(焦点不会掉空)
  5. 信号
    • 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)

四、调试小技巧

  1. 打印焦点链

    print("focus:", self.scene.focusItem(), "selected:", self.scene.selectedItems())
    
  2. 焦点丢失追踪
    连接 scene.focusItemChanged 信号,在槽里断点或日志,一眼看出谁把焦点抢走了。

20251202更新
  1. win.show的时候会自动activateWindow, activateWindow会沿着tab链分配焦点, 因此, strongfocus会默认拿到焦点, clickfocus不会.
  2. 如果要确保某个家伙获得焦点可以,
    1. 内部showevent解决, 这个是比较软弱的做法, 他可能被外部设置抢走. 是个好做法
    2. 外部统一控制焦点转移, 强硬且一般情况下是傻逼的做法, 唯一c位会导致越复杂越完蛋
  # 比如一个view/win都可以写这个
  def showEvent(self, event):
    super().showEvent(event)
    self.setFocus(Qt.FocusReason.OtherFocusReason)

这样就解决了下面两个问题

  1. 描述下现在的包装链, 判断下焦点位置: 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
  2. 描述下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链没有东西, 那么永远是这个