Qt 图形视图 /图形视图框架坐标系统的设计理念和使用方法

文章目录

概述

The Graphics View Coordinate System 图形视图坐标系统 是Qt图形视图框架的重要组成部分,按照帮助文档中的划分,其与 The Graphics View Architecture 图形视图架构 是在一个层级上的,主要包含图形项坐标系、场景坐标系、视图坐标系、坐标映射等内容。本文将结合帮助文档、Qt编程书籍、Qt示例程序等研习图形视图框架坐标系统的设计理念和使用方法。

补充,20230501,

编写HelpDoc的人,大概率是极为熟悉该模块的开发者,他们编写文档时的立意是全局化的,很有可能在写第一个章节时,潜移默化的渗透少许第二、甚至第三四五章节的知识点,尽管他们可能已经在尽力避免类似事情的发生,但那很难做到。所以有很多帮助内容,初读时很有跳跃感,比较吃力。此时先读个大概,了解全局,再回头细读,可能是一种可行的方法。

转载请标明原文链接,

https://blog.csdn.net/quguanxin/category_12597847.html

Qt 坐标系统

图形视图框架的坐标系统基于Qt大框架的坐标系统。逻辑坐标系的使用,使绘制代码独立于绘画设备的分辨率和坐标系,开发者可以在逻辑坐标系中编写绘制代码,QPainter 会自动将其映射到实际的设备坐标系。

在Qt框架下,QPainter类实际掌控着Qt坐标系统,并且它与 QPaintDevice 和 QPaintEngine 类共同组成了Qt的绘制系统 ,而且Qt绘制系统竟然还有一个名字,Arthur 亚瑟 ,这可能是一个项目代号,具体不得而知。QPainter 用以执行绘制操作,QPaintDevice 是一个二维绘制空间,QPainter就是在该空间进行绘制操作,QPaintEngine为painter在不同设备上的绘制操作提供了interface接口。

QPaintDevice类是所有支持绘制的类的基类,它的绘画能力被 QWidget, QImage, QPixmap, QPicture, and QOpenGLPaintDevice 继承。一个Qt绘制设备的默认坐标系原点在Device左上角,X轴向右增长,Y轴向下增长。在基于像素的设备上,以像素为默认长度单位,在打印设备上,默认单位是一个点(1/72英寸)。QPainter的逻辑坐标到QPaintDevice的物理坐标的映射,由QPainter的变换矩阵、视口和窗口处理。

图形视图坐标系统以Qt坐标系统为基础,不支持y轴向上增长的反向坐标系(inverted Y-axis coordinate system)。在传统的笛卡尔坐标系中,坐标原点 (0,0) 位于左下角,y轴向上增长,也就是说 y 值越大,坐标点在垂直方向上越高。而在Qt中采用Y轴向下增长,其原因可能是,在绘制图像时,通常图像的第一行像素位于最上方,向下递增,这符合存储图像数据的方式,如此更加自然。

图形视图的渲染过程

前边提到过,图形视图使用Qt的坐标系统,上述图片段落大意是将图形视图框架的渲染过程代入了 Qt 绘制系统。Qt图形视图框架下,在渲染时,图形视图的场景坐标对应于QPainter的逻辑坐标,视图坐标等同于设备坐标。有关逻辑坐标和设备坐标之间关系的更多内容,在Qt绘制系统的相关文章中讲述。在 '绘制' 这个层次的概念上,Qt图形视图框架并没有脱离QPainter机制,而是在QPainter的基础上进行了扩展和封装,如,自动处理场景的渲染和重绘。

在图形视图中有三个有效effective的坐标系统:图形项坐标系、场景坐标系和视图坐标系。为了简化实现,Qt 图形视图提供了便利函数,允许您在三个坐标系统之间进行映射。

Item图形项坐标系

Item居于live自己的本地坐标系中,可以理解为Item在自己的本地坐标系中运作。图形项坐标系通常以项的中心点 (0,0) 为中心(注意,有的不是哦,后文有提及),这个点也是所有坐标变换的中心。图形项坐标系下的几何图元(Geometric primitives) 通常被称为,点、Item线、Item矩形。

当你创建一个自定义项时,你只需要关注Item本地坐标系即可,场景和视图会帮你执行所有变换。如在 Diagram Scene Example 示例程序中,创建基本流程图形状的代码,是在Item本地坐标系中进行绘制的,根本不用关心场景和视图坐标系的任何。

cpp 复制代码
DiagramItem::DiagramItem(DiagramType diagramType, QMenu *contextMenu, QGraphicsItem *parent) : QGraphicsPolygonItem(parent) {
    ...
    switch (myDiagramType) {
        ...
        case Conditional:  //流程图形状,判定框
            myPolygon << QPointF(-100, 0) << QPointF(0, 100)
                      << QPointF(100, 0) << QPointF(0, -100)
                      << QPointF(-100, 0);
            break;
        case Step:        //流程图形状,过程框
            myPolygon << QPointF(-100, -100) << QPointF(100, -100)
                      << QPointF(100, 100) << QPointF(-100, 100)
                      << QPointF(-100, -100);
            break;
        ...
    }
    setPolygon(myPolygon);
    ...
}

Qt 图形视图框架在尽力的使用Item本地坐标系坐标交互位置信息,以方便开发者使用。例如,当你 (潜台词,在Item对象中) 接收到鼠标按下或拖拽进入事件时,事件(QGraphicsSceneMouseEvent)对象携带的位置会以Item坐标给出(到图形项对象)。QGraphicsItem::contains() 虚函数用于判断某个点是否在物品内,如果在则返回 true,否则返回 false,该函数的参数为Item坐标系下的坐标。类似地,Item的边界矩形和形状也是以Item坐标表示的。下文以 mousePressEvent 鼠标事件为例,观察该事件在视图、场景和图形项中的运行规律,

cpp 复制代码
//在图形项QGraphicsItem类中
virtual void QGraphicsItem::mousePressEvent(QGraphicsSceneMouseEvent *event);
//在场景QGraphicsScene类中
virtual void QGraphicsScene::mousePressEvent(QGraphicsSceneMouseEvent *event);
//是视图QGraphicsView类中
virtual void QGraphicsView::mousePressEvent(QMouseEvent *event);

如上函数同为mousePressEvent原型,QGraphicsView 使用QWidget传统的QMouseEvent 事件类型,而 QGraphicsItem 和 QGraphicsScene 使用 QGraphicsSceneMouseEvent 事件类型,后者可传递两种位置信息,如下,

相关示例代码,放在了后续 'View视图坐标系' 章节中。通过调试信息可确认,在Item对象中接收到的mouseEvent事件,通过 mouseEvent->pos 函数传递出来的是Item坐标系下的坐标。

父坐标 和 子坐标, (同一个视觉位置点的父坐标和子坐标)

不得不穿插对英语句子的理解,只是个人理解,原谅我英语没学好,

At item's position is the coordinate of the item's center point in its parent's coordinate system。

结合上图中QGraphicsItem的构造函数,对上文的一个合理的释义,可能是,"(this这个) Item 的位置" 是指 (this) Item 的中心点在其父级(父Item) 坐标系中的坐标,有时也简称为父坐标 。从某种意义上来说,若一个Item没有任何父(Item),那么场景将被当做是它的父(Item),顶层Item的位置以场景坐标来表示。整体上,这一小段Doc,目的是传递一个父坐标的概念,即,Item的(pos函数)位置是该Item原点在其父Item坐标系中的坐标。

理解上边这段话,着实花费了我不少时间。如上English,Child是子图形项的意思,parent是 this Item's parant 父项的意思,复数形式的coordinates可能通常代表坐标系、单数coordinate可能通常代表坐标点。与前文那句英文类似,这段话里似乎也藏了许多潜台词,故此晦涩。本段开始就告诉我们,子坐标系是相对于(be relative to)父坐标系的,真不太好理解。接着文中提到,如果子级Item未经过变换,则(潜台词,在窗口中,用户视觉上是同一个点,如下图的红点)子坐标与父级坐标之间的差距,与(the two)Items(前面提到的Child和Parent)在父坐标中的距离(也即父Item和子Item中心点之间的距离)相同。只能结合下文具体的例子继续理解,

如果一个未经过变换的子Item恰好位于其父项的中心点上,那么两个项的坐标系统将是相同的。然而,如上图中,红色矩形代表的子级Item,其在父级Item坐标系下的位置是 (10, 0),而不是 (0,0) 原点。此时,视觉红点,其在子级Item坐标系下的坐标为(0, 10),同时它在父级坐标系下的坐标是(10, 10)。如此,同样一个红点的子坐标(0,10)和父坐标(10,10)之间的差异便是(10,0),这个(10,0)也正好是子项和父项之间的距离(两个矩形中心点的距离),这个距离是10个单位长度。而文中 the difference between a child coordinate and a parent coordinate,并没有特定的指代某点,应该是指子Item上的任意的一个点,即,从视图上看到的任意一个点,其子项坐标与父项坐标的差值是一样的,都是子项和父项原点之间的距离。

由于项位置和项变换是与父项相对的,子项的坐标系并不不会受到父项坐标系变换的影响,尽管父项的变动隐式的转换了子项。还在上面的例子中,即使父项被旋转和缩放,子项坐标系下的(0,10)点依然将对应父项坐标系下的(10,10)点。然而,相对于场景,子项将跟随父项的变换和位置。如果父项被缩放2倍,那么,子项在场景中的坐标将变成(20,0),子项坐标系下的(10,0),在父项坐标系下对应的位置依然是(20,0),在场景坐标系中对应的点是(40,0)。

行文至此,我们再倒回头来聊聊前面没理解的 "子坐标系是相对于父坐标系的",其含义是,无论父级图形项如何缩放、旋转等变动,子项在父项做标系下的位置不变,子项坐标系下的点,其对应的父项坐标系下的坐标也还是原来的值,没有变化,这就是所谓的相对性,即(子项任意可视点的)子项坐标系下的坐标与对应的父项坐标系下的坐标都还是原来的坐标。与上述相对性对立的,是子项相对于场景/绘制设备的绝对位置和变换则受父项变换的影响。可以不太精确的定义两个名词,相对坐标系和绝对坐标系。这种父子层级结构让坐标变换更加清晰可控,子项目可以独立于父项的变换来定义自身坐标,但最终的绝对变换位置仍由父项的属性确定。

除了少数例外( 如QGraphicsItem::pos() )之外,QGraphicsItem的函数都在项坐标系中运行,不受该项或其任何父项目的变换的影响。例如,一个项的边界矩形(即boundingRect)始终给出项坐标系下的坐标。

Scene场景坐标系

场景表示所有Item图形项的基础坐标系统。场景坐标系统描述的是所有顶层Item的位置,也是所有从View视图到Scene场景的 QGraphicsSceneMouseEvent 场景型事件的基石。场景中的每个项除了具有Item本地位置和本地边界矩形之外,还有一个场景位置和场景边界矩形 (QGraphicsItem::scenePos(), QGraphicsItem::sceneBoundingRect())。如下所示,

场景位置描述了项在场景坐标系中的位置,而项的场景边界矩形则构成了QGraphicsScene 确定场景中哪些区域已更改的基础。场景中的更改通过 QGraphicsScene::changed() 信号传播,参数是一个场景矩形列表。通过上图中对 boundingRect() 和 sceneBoundingRect() 的对比,可以直观的看出两者的区别,本地边界矩形被用以QGraphicsView的重绘过程,而场景边界矩形是混合了本地边界矩形和坐标系变换之后的场景坐标系下的矩形,其主要用以探测出项在场景中的位置变动。

cpp 复制代码
QRectF Arrow::boundingRect() const {
    qreal extra = (pen().width() + 20) / 2.0;

    return QRectF(line().p1(), QSizeF(line().p2().x() - line().p1().x(), line().p2().y() - line().p1().y()))
        .normalized()
        .adjusted(-extra, -extra, extra, extra);
}

View视图坐标系

视图坐标系是窗口部件的坐标系。视图坐标系下的每个单元都对应一个像素。这个坐标系统的特殊之处在于,它是相对于 widget部件 或 viewport视口的,不受被观察场景的影响。QGraphicsView 的viewport 视口的左上角坐标永远是(0,0),右下角坐标永远是(视口宽度,视口高度)。

所有鼠标事件和拖放事件最初都是以视图坐标接收的,你需要将这些坐标映射到场景中,以便与Item项交互。也可以这么理解,用户的键鼠事件等最早(originally)是由视图对象捕获的,而场景对象可能是作为事件过滤器被安装到关联的视图对象上的,为此我们进行了如下验证。我们扩展 Diagram Scene Example 示例程序,添加从QGraphicsView派生的GraphicsView类,并分别在DiagramItem、DiagramScene、DiagramView 中新实现或修改 mousePressEvent 虚函数,

cpp 复制代码
//派生图形项中对鼠标按压事件的处理
void DiagramItem::mousePressEvent(QGraphicsSceneMouseEvent *event) {
    qDebug() << "\r\n--#-- DiagramItem::mousePressEvent --#--";
    qDebug() << "DiagramItem::pos:" << event->pos();
    qDebug() << "DiagramItem::scenePos:" << event->scenePos();
    //must
    return QGraphicsPolygonItem::mousePressEvent(event);
}
//派生场景中对鼠标按压事件的处理
void DiagramScene::mousePressEvent(QGraphicsSceneMouseEvent *mouseEvent) {
    qDebug() << "\r\n--#-- DiagramScene::mousePressEvent --#--";
    qDebug() << "DiagramScene::pos:" << mouseEvent->pos();
    qDebug() << "DiagramScene::scenePos:" << mouseEvent->scenePos();
    ...
}
//派生视图中对鼠标按压事件的处理
void DiagramView::mousePressEvent(QMouseEvent *event) {
    qDebug() << " \r\n--#-- DiagramView::mousePressEvent --#--";
    qDebug() << "DiagramView::localPos:" << event->localPos();
    qDebug() << "DiagramView::windowPos:" << event->windowPos();
    qDebug() << "DiagramView::screenPos:" << event->screenPos();
    qDebug() << "DiagramView::pos:" << event->pos();             //==localPos
    qDebug() << "DiagramView::globalPos:" << event->globalPos(); //==screenPos
    //must
    return QGraphicsView::mousePressEvent(event);
}

/* //创建一个ShpeItem点击其靠近右下角的某个位置
--#-- DiagramView::mousePressEvent --#--
DiagramView::localPos: QPointF(355,265)
DiagramView::windowPos: QPointF(564,332)
DiagramView::screenPos: QPointF(664,432)
DiagramView::pos: QPoint(355,265)       //==localPos   //以视图左上角为00
DiagramView::globalPos: QPoint(664,432) //==screenPos

--#-- DiagramScene::mousePressEvent --#--
DiagramScene::pos: QPointF(0,0)             //只要点击位置在Item内,则始终为00
DiagramScene::scenePos: QPointF(2573,2562)

--#-- DiagramItem::mousePressEvent --#--
DiagramItem::pos: QPointF(75,78)           //以Item中心位置为00原点
DiagramItem::scenePos: QPointF(2573,2562)  //与在场景对象中的捕获坐标值是一致的
*/

通过上运行结果,我们验证了Qt鼠标事件在Qt图形视图框架下的传递顺序,基本可推定,视图对象会在最初捕获用户事件,然后可能的转换过程方案有:转为视图对象直接将 QMouseEvent 转为 QGraphicsSceneMouseEvent 事件;场景对象是视图对象的事件过滤器,图形项对象是场景的事件过滤器,由场景和图形项对象各自分别转换,但由于图形项没有从QObject继承,即无法安装过滤器,这种方案的可能性不大。针对上述问题,后续将研究相关源码,并在其他文章中继续讨论。

当使用未经过任何变换(如缩放、旋转等)的视图observing观察场景时,场景上的unit单位长度对应屏幕上的一个像素。也即在没有应用任何变换的情况下,场景坐标与视图坐标是相互对应的。

例如,如果在场景坐标系中有两个点A(10,10) 和 B(20,10),那么它们在未经变换的视图中的像素距离就是10个像素。这种对应的关系使得最开始在场景上布局和定位图形项时很方便,因为你可以直接以像素为单位来思考。若应用了任何变换,比如缩放或旋转,场景坐标与视图坐标之间就不再是简单的1-1对应关系了,这时就需要使用Qt提供的坐标映射函数来在不同坐标系之间转换。

map坐标映射

通常,在处理场景中的图形项时,需要在场景、图形项、视图之间进行必要的坐标或任意矩形的映射。

例如,当你在 QGraphicsView 的视口中单击鼠标时,可以通过调用 QGraphicsView::mapToScene() 将坐标映射到场景,然后调用QGraphicsScene::itemAt() 来查询光标下的图形项对象。如果你想知道一个图形项在视图视口中的位置,可以在图形项对象上调用QGraphicsItem::mapToScene(),然后在视图上调用QGraphicsView::mapFromScene()。如果你想找出视口中椭圆内的所有图形项,可以将椭圆对象的 QPainterPath 传递给 mapToScene() 映射到场景,然后将映射后的路径传递给QGraphicsScene::items()。

在图形项和场景之间,通过调用QGraphicsItem::mapToScene() and QGraphicsItem::mapFromScene() 函数来相互映射坐标和形状。在图形项与其父项之间通过调用QGraphicsItem::mapToParent() and QGraphicsItem::mapFromParent()来进行映射,或者通过调用 QGraphicsItem::mapToItem() 和 QGraphicsItem::mapFromItem() 在(同一个场景下)不同图形项之间进行映射。所有的映射函数都支持点、矩形、多边形和路径,以 mapToScene 为例,在QGraphicsItem类(图左) 和 QGraphicsView类(图右),有以下6种重载,

下文将对部分场景下的坐标转换过程,通过示例程序进行说明。

场景坐标转项坐标

在整个Qt图形视图坐标系统中,场景坐标处于至关重要的承上启下的位置。且通过上文可知,场景类并没有直接提供坐标映射的接口,这些映射接口仅存在于QGraphicsItem类 和 QGraphicsView类中。那么在场景对象的处理函数中,如何进行坐标的相关运算呢?

除了上述 itemAt 函数之外,QGraphicsScene 类还提供了 6 个items(...) 的重载函数、selectedItems() 、views() 等函数,获取目标图形项实例对象。一旦通过场景坐标获取到了图形项实例和视图实例,便可以使用QGraphicsItem类 和 QGraphicsView类中的坐标映射接口。

在 Diagram Scene Example 示例程序,场景类的实现中,就使用了items函数的一个版本,

cpp 复制代码
void DiagramScene::mouseReleaseEvent(QGraphicsSceneMouseEvent *mouseEvent) {
    if (line != 0 && myMode == InsertLine) {
        QList<QGraphicsItem *> startItems = items(line->line().p1());
        if (startItems.count() && startItems.first() == line)
            startItems.removeFirst();
        QList<QGraphicsItem *> endItems = items(line->line().p2());
        if (endItems.count() && endItems.first() == line)
            endItems.removeFirst();
        ...
}

在Diagram Scene Example 示例程序,场景类的实现中,添加如下itemAt函数的使用测试,

cpp 复制代码
void DiagramScene::mousePressEvent(QGraphicsSceneMouseEvent *mouseEvent) {
...
    #if 1
    QTransform deviceTransform; //in param
    QGraphicsItem *tempitem = this->itemAt(mouseEvent->scenePos(), deviceTransform);
    QPointF ItemLocalPt = tempitem->mapFromScene(mouseEvent->scenePos());
    qDebug() << "DiagramScene::ItemLocalPt:" << ItemLocalPt;
    #endif
...
}

如上,通过调用场景类的 itemAt 函数,得到 QGraphicsItem 实例对象指针后,便可使用 QGraphicsItem 的映射函数啦。

视图坐标转图形项坐标

如上,视图中提供(be available)了与图形项类相同的映射函数,用于在视图和场景之间进行映射。要从视图映射到图形项,你首先需要将视图坐标映射到场景坐标系,然后再从场景映射到图形项坐标系。我们扩展 Diagram Scene Example 示例程序,

cpp 复制代码
//从QGraphicsView派生DiagramView,替代原view实例 /实现如下非功能的测试函数
void DiagramView::mousePressEvent(QMouseEvent *event) {
  //QPoint#pos()==QPointF#localPos()
  qDebug() << "DiagramView# localPos:" << event->pos() << "windowPos:" << event->windowPos() << "screenPos:" << event->screenPos();
#if 1
    //在视图中得到鼠标位置的Item本地坐标
    QPointF itemLocalPos;
    //视图坐标转场景坐标 //event->pos()==event->localPos()
    QPointF scenePtFromView = mapToScene(event->pos());
    //获取鼠标事件发生的位置对应的QGraphicsItem实例
    QGraphicsItem *item = this->scene()->itemAt(scenePtFromView, QTransform());
    if (item)
        itemLocalPos = item->mapFromScene(scenePtFromView);
    qDebug() << "DiagramView# localpos:" << itemLocalPos << "scenePos:" << scenePtFromView;
#endif
    //must
    return QGraphicsView::mousePressEvent(event);
}

//在DiagramItem追加如下测试函数
void DiagramItem::mousePressEvent(QGraphicsSceneMouseEvent *event) {
    qDebug() << "DiagramItem# localpos:" << event->pos() << "scenePos:" << event->scenePos();
    //must
    return QGraphicsPolygonItem::mousePressEvent(event);
}

/* 运行修改后的示例程序,添加一个ShpeItem,点击其中的位置,运行结果如,
DiagramView# localPos: QPoint(187,171) windowPos: QPointF(396,238) screenPos: QPointF(496,338)
DiagramView# localpos: QPointF(5,47) scenePos: QPointF(2405,2468)
DiagramItem# localpos: QPointF(5,47) scenePos: QPointF(2405,2468)
*/

如上代码,将View视图实例QMouseEvent事件中的本地坐标(以视图Widget左上角为原点),也即视图坐标系下的坐标,通过 QGraphicsView::mapToScene 函数转换为场景坐标系下的坐标,进而找到该场景坐标下的Item实例,之后通过调用Item的mapFromScene函数,将场景坐标映射到Item本地坐标系下。通过对比DiagramView对象和DiagramItem对象内的打印信息,可验证转换效果。

在 Drill Down Example 这个示例程序中,

cpp 复制代码
void View::mouseReleaseEvent(QMouseEvent *event) {
    if (QGraphicsItem *item = itemAt(event->pos())) {
        if (ImageItem *image = qgraphicsitem_cast<ImageItem *>(item))
            showInformation(image);
    }
    QGraphicsView::mouseReleaseEvent(event);
}

如上,我们细看 QGraphicsView 类,可知,QGraphicsView 自身与 QGraphicsScene 类一样,同样提供了 itemAt(...) 和 items(...) 函数。但是本质上还是要将视图坐标映射到场景坐标系,然后再从场景映射到图形项坐标系。透过以下 QGraphicsView::items 函数的源码,可以证明这一点,

cpp 复制代码
//
QGraphicsItem *QGraphicsView::itemAt(const QPoint &pos) const {
    Q_D(const QGraphicsView);
    if (!d->scene)  return 0;
    const QList<QGraphicsItem *> itemsAtPos = items(pos);
    return itemsAtPos.isEmpty() ? 0 : itemsAtPos.first();
}
//
QList<QGraphicsItem *> QGraphicsView::items(const QPoint &pos) const {
    Q_D(const QGraphicsView);
    if (!d->scene)
        return QList<QGraphicsItem *>();
    // ### Unify these two, and use the items(QPointF) version in
    // QGraphicsScene instead. The scene items function could use the viewport
    // transform to map the point to a rect/polygon.
    if ((d->identityMatrix || d->matrix.type() <= QTransform::TxScale)) {
        // Use the rect version
        QTransform xinv = viewportTransform().inverted();
        return d->scene->items(xinv.mapRect(QRectF(pos.x(), pos.y(), 1, 1)), Qt::IntersectsItemShape, Qt::DescendingOrder, viewportTransform());
    }
    // Use the polygon version
    return d->scene->items(mapToScene(pos.x(), pos.y(), 1, 1), Qt::IntersectsItemShape, Qt::DescendingOrder, viewportTransform());
}

图形项之间的坐标转换

以 Diagram Scene Example Arrow 类的 updatePosition 函数为例,说明Item图形项之间的坐标映射过程。

cpp 复制代码
void Arrow::updatePosition() {
    //将图形项myStartItem/myEndItem的原点转换为另一个Arrow图形项的坐标系下
    QLineF line(mapFromItem(myStartItem, 0, 0), mapFromItem(myEndItem, 0, 0));
    //绘制myStartItem/myEndItem中心点之间的线段
    setLine(line);
}

如上,为了绘制两个ShapeItem之间的连接线,将myStartItem项的中心原点映射为线段的起点,将myEndItem项的中心原点映射为线段的终点,构建 QLineF 对象并更新 QGraphicsLineItem 类内部维护的线段成员变量。特别需要注意的是,图形项QGraphicsLineItem的本地坐标系和场景坐标系是重合的,因此,this->mapFromItem(myStartItem, 0, 0) 与 myStartItem->mapToScene(QPointF(0,0)) 是相等的,而 this->mapToScene(QPointF(0,0)) 和 this->scenePos() 等返回的都是(0, 0)场景坐标原点。

其他

前面讲到 itemAt 或 items 函数时,它们都有一个QTransform 对象作为输入参数,在 QGraphicsItem 的 mapFromScene、mapToScene等函数实现、QGraphicsItemPrivate的诸多实现中都用到了 QTransform 成员变量或函数参数。该类在 Qt 大框架的模块划分上属于 Qt GUI 模块,隶属 Qt 绘制系统。

要更彻底掌握 Qt 绘制系统坐标系统 和 Qt 图形视图框架坐标系统的设计思路和使用方法,或者仅是想稍加深入的阅读上述框架的源代码,QTransform 是绕不开的知识点。它用于指定二维坐标系统变换,提供了平移(translate)、缩放(scale)、剪切(shear)、旋转(rotate) 和投影(project) 等基本变换操作,常被用于渲染图形时对坐标系统进行变换。QTransform是一个真正的3x3矩阵,允许进行透视投影变换,与QMatrix相比,它功能更加强大,在Qt中QTransform被推荐用作变换类。限于文章篇幅,此文不再对此展开。