基础
  1. 儿子的坐标是用父亲坐标作为原点
  2. 我.mapTo他(某个坐标) —— 把‘我’的坐标系转成 他的坐标系
  3. 我.mapFrom他(某个坐标) —— 把 他 的坐标转成‘我’的坐标

细节

调用者 函数 语义 输入坐标系 输出坐标系
QGraphicsView mapToScene(QPoint) 视口→场景 viewport scene
QGraphicsView mapFromScene(QPoint) 场景→视口 scene viewport
QGraphicsItem mapToScene(QPoint) item→scene item-local scene
QGraphicsItem mapFromScene(QPoint) 场景→item scene item-local
QGraphicsItem mapToParent(QPoint) item→parent item-local parent
QGraphicsItem mapFromParent(QPoint) parent→item parent item-local
QGraphicsItem mapToItem(other, QPt) item→兄弟 item-local other-item-local
QGraphicsItem mapFromItem(other, QPt) 兄弟→item other-item-local item-local
源坐标系 目标坐标系 调用者 关键函数(重载有 QPoint/QRect/QPolygon/QPainterPath)
View 视口 Scene 场景 QGraphicsView mapToScene() / mapFromScene()
Scene 场景 Item 项 QGraphicsItem mapFromScene() / mapToScene()
Item 项 Item 项 QGraphicsItem mapFromItem(item) / mapToItem(item)
Item 项 Parent 父 QGraphicsItem mapFromParent() / mapToParent()
View 视口 Item 项 组合 view.mapToScene()item.mapFromScene()
全局屏幕 Widget QWidget mapFromGlobal() / mapToGlobal()(辅助)
浮动
def keep_top_left():
    top_left_scene = view.mapToScene(0, 0)      # 核心代码: 视口左上角对应的 scene 坐标
    float_rect.setPos(top_left_scene)           

view.horizontalScrollBar().valueChanged.connect(keep_top_left)
view.verticalScrollBar().valueChanged.connect(keep_top_left)
# 窗口大小改变时也补一次
view.resizeEvent = lambda e: (keep_top_left(), QGraphicsView.resizeEvent(view, e))
  1. paint 时瞬移
    paint() 里先 setPos(view.mapToScene(0,0)) 再画;最简单,但每帧都跑,性能一般。

  2. scroll 信号里瞬移(上面代码)
    监听 scrollBar.valueChanged / resizeEvent,滚动或resize时把 item 钉回去;逻辑挪到场景外,paint 不动。

  3. 无视变换
    给 item 加标志item.setFlag(QGraphicsItem.ItemIgnoresTransformations, True)

  4. 真正的“窗口层”——把 item 画到 viewport 上
    重载 QGraphicsView.drawForeground(QPainter, QRectF)

    Python

    复制

    def drawForeground(self, painter, rect):
        # 1. 存住 view 当前的那套变换(滚动、缩放、旋转、抗锯齿……)
        painter.save()
       
        # 2. 把 painter 还原成“裸”坐标系:原点就是 viewport 左上角,1 像素=1 单位
        painter.resetTransform()
       
        # 3. 此时 painter 完全“无视” scene 的滚动/缩放,
        #    下面画的东西永远钉在窗口像素坐标系里
        painter.fillRect(10, 10, 80, 40, Qt.red)
       
        # 4. 恢复 step 1 存的那套变换,免得后面 Qt 自己画网格/选择框时坐标系被搞乱
        painter.restore()
    

    它天生就“钉”在视口,场景怎么滚都不影响;缺点是不能用 QGraphicsItem 那一套选择、碰撞、信号,需要自己接管交互。

  • 要参与 scene 的选中/碰撞吗?
    • 要就选 1/2/3(最简单是 3),
    • 不要就选 4,性能最好。
  • 结论: 方案3简洁方便, 开发效率最高, 执行效率也很好, 完美方案, 但是, 还是要钉在位置上, 这个要点技巧, 很容易漏掉某些事件, 因此过滤器方案比较稳.
  # 3. 钉住函数(这一行是关键)
  def reanchor():
      floater.setPos(view.mapToScene(0, 0))

  # 4. 事件过滤器:任何视口变动都重新钉
  class Hack(QObject):
      def eventFilter(self, obj, ev):
          # Paint/Resize/Wheel/Scroll 都会进来
          if ev.type() in (QEvent.Paint, QEvent.Resize, QEvent.Wheel):
              reanchor()
          return False

  view.viewport().installEventFilter(Hack(view))