图形视图框架:QGraphicsScene、QGraphicsView、QGraphicsItem、图元交互、动画框架

好,咱们今天聊点真正能打的——图形视图框架。说实话,Qt 里让我觉得最爽的模块之一就是它。你想想看,如果你要在界面上画一堆可拖拽、可缩放、还能动起来的图形,用传统 QWidget 硬画?那简直是噩梦。但有了 QGraphicsScene 和 QGraphicsView,这事儿就变得特别优雅。

我个人习惯把图形视图框架比作「舞台 + 摄像机 + 演员」的关系。Scene 是舞台,Item 是演员,View 就是摄像机镜头。你通过 View 去看 Scene 上的 Item,还能拉近拉远、平移视角。这个比喻我用了好多年,带新人时屡试不爽。

核心三件套:Scene、View、Item

先说说这三个家伙各自干什么活。

组件 职责 我踩过的坑
QGraphicsScene 管理图元、事件分发、碰撞检测 别把 Scene 当容器乱塞,它有自己的坐标系统
QGraphicsView 渲染视图、滚动缩放、坐标转换 View 的变换会影响 Item 的鼠标坐标,记得 mapToScene
QGraphicsItem 具体图元、绘制、交互、碰撞形状 重写 boundingRect 一定要准确,否则选中区域会飘

代码写起来其实很简单。我一般这样搭架子:

// 创建场景
QGraphicsScene *scene = new QGraphicsScene(this);
scene->setSceneRect(-200, -200, 400, 400);

// 创建视图
QGraphicsView *view = new QGraphicsView(scene, this);
view->setRenderHint(QPainter::Antialiasing);
view->setDragMode(QGraphicsView::ScrollHandDrag);

// 添加一个矩形图元
QGraphicsRectItem *rect = scene->addRect(-50, -50, 100, 100);
rect->setFlag(QGraphicsItem::ItemIsMovable);
rect->setFlag(QGraphicsItem::ItemIsSelectable);

嗯,这里要注意:setSceneRect 如果不设,Scene 会动态扩展。我曾经有个项目没设这个,结果图元跑出视野后,View 的滚动条乱跳,排查了半天。

自定义图元:别偷懒,重写 boundingRect

内置的矩形、椭圆、文本够用吗?说实话,做正经项目肯定不够。你得自己写图元类。继承 QGraphicsItem,然后重写三个关键函数:

  • boundingRect() —— 返回图元占用的矩形区域,用于重绘和碰撞检测
  • paint() —— 绘制图元内容
  • shape() —— 返回精确的碰撞形状,默认是 boundingRect 的矩形

我给大家看一个我项目里用过的自定义图元——带圆角的可拖拽标签:

class TagItem : public QGraphicsItem {
public:
    TagItem(const QString &text, QGraphicsItem *parent = nullptr)
        : QGraphicsItem(parent), m_text(text) {
        setFlag(ItemIsMovable);
        setFlag(ItemIsSelectable);
        setAcceptHoverEvents(true);
    }

    QRectF boundingRect() const override {
        QFontMetrics fm(QFont("Microsoft YaHei", 12));
        return QRectF(0, 0, fm.horizontalAdvance(m_text) + 20, 30);
    }

    void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override {
        painter->setRenderHint(QPainter::Antialiasing);
        painter->setBrush(isSelected() ? QColor("#FFD700") : QColor("#E0F7FA"));
        painter->setPen(QPen(QColor("#00796B"), 2));
        painter->drawRoundedRect(boundingRect().adjusted(1, 1, -1, -1), 8, 8);
        painter->drawText(boundingRect(), Qt::AlignCenter, m_text);
    }

private:
    QString m_text;
};

这里有个小技巧:setAcceptHoverEvents(true) 可以让图元响应鼠标悬停。我经常用它来做高亮效果,用户体验会好很多。

图元交互:鼠标、键盘、碰撞检测

图元交互说白了就是三件事:鼠标拖拽、键盘控制、碰撞检测。

鼠标交互我一般重写这几个虚函数:

  • mousePressEvent —— 记录点击位置
  • mouseMoveEvent —— 更新图元位置
  • mouseReleaseEvent —— 释放后的处理
  • keyPressEvent —— 键盘操作(记得 setFocus())

举个例子,让图元只能水平拖动:

void MyItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) {
    QPointF delta = event->pos() - event->lastPos();
    moveBy(delta.x(), 0);  // 只允许水平移动
}

碰撞检测呢?Scene 提供了 items()collidingItems()。我做过一个连线工具,每次拖拽图元时检测是否与其他图元重叠,如果重叠就自动吸附。代码大概这样:

QList<QGraphicsItem *> colliding = collidingItems();
for (QGraphicsItem *item : colliding) {
    if (item->type() == MyItem::Type) {
        // 执行吸附逻辑
        MyItem *other = qgraphicsitem_cast<MyItem*>(item);
        if (other) alignTo(other);
    }
}

我曾经犯过一个错:在碰撞检测里直接修改图元位置,结果触发了递归调用,程序直接崩了。后来加了个 m_isAligning 标志位才解决。

动画框架:让图元动起来

Qt 的动画框架和图形视图框架配合得天衣无缝。核心是 QPropertyAnimation,它可以对图元的任意属性做插值动画。

最常用的几个属性:

  • pos —— 位置移动
  • rotation —— 旋转
  • scale —— 缩放
  • opacity —— 透明度

看个例子,让图元从 A 点平滑移动到 B 点:

QPropertyAnimation *anim = new QPropertyAnimation(myItem, "pos");
anim->setDuration(1000);
anim->setStartValue(QPointF(0, 0));
anim->setEndValue(QPointF(200, 150));
anim->setEasingCurve(QEasingCurve::OutBounce);
anim->start();

这里 OutBounce 曲线会让图元像皮球一样弹跳几下才停住。我特别喜欢这个效果,做演示时很抓眼球。

如果你想让多个图元同时动,可以用 QParallelAnimationGroup:

QParallelAnimationGroup *group = new QParallelAnimationGroup;
group->addAnimation(anim1);
group->addAnimation(anim2);
group->start();

或者按顺序动,用 QSequentialAnimationGroup。我做过一个流程图编辑器,节点展开时就是按顺序动画,一个一个弹出来,效果很丝滑。

知识体系结构图

下面这张图是我自己梳理的,把本章的核心逻辑串起来了。你看一眼就能明白 Scene、View、Item 和动画之间的关系。

图形视图框架核心结构 QGraphicsScene 管理图元 · 事件分发 QGraphicsView 渲染视图 · 坐标转换 QGraphicsItem 绘制 · 交互 · 碰撞 图元交互机制 鼠标拖拽 键盘控制 碰撞检测 事件过滤 动画框架 QPropertyAnimation 位置动画 旋转动画 缩放动画 透明度动画

核心要点:Scene 是舞台,View 是镜头,Item 是演员。动画框架让演员动起来。三者配合,能做出非常复杂的图形交互应用。

我的小技巧:调试时可以在 Scene 上画个网格背景,方便观察图元位置。重写 drawBackground() 即可,别用 QGraphicsScene::setBackgroundBrush,那样会覆盖掉网格。

注意:QGraphicsItem 的坐标是相对于父图元的。如果你把图元 A 设为图元 B 的子对象,那么 A 的 (0,0) 就是 B 的当前位置。嵌套变换时很容易晕,我建议用 mapToScene()mapFromScene() 做转换,别自己算。

好了,图形视图框架的内容就聊到这儿。你回去可以试着写一个简单的画板程序,支持拖拽矩形、圆形,再加个删除键。做完这个,你对 Scene、View、Item 的理解就到位了。

公众号:蓝海资料掘金营,微信deep3321