图形视图框架: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 和动画之间的关系。
核心要点:Scene 是舞台,View 是镜头,Item 是演员。动画框架让演员动起来。三者配合,能做出非常复杂的图形交互应用。
我的小技巧:调试时可以在 Scene 上画个网格背景,方便观察图元位置。重写 drawBackground() 即可,别用 QGraphicsScene::setBackgroundBrush,那样会覆盖掉网格。
注意:QGraphicsItem 的坐标是相对于父图元的。如果你把图元 A 设为图元 B 的子对象,那么 A 的 (0,0) 就是 B 的当前位置。嵌套变换时很容易晕,我建议用 mapToScene() 和 mapFromScene() 做转换,别自己算。
好了,图形视图框架的内容就聊到这儿。你回去可以试着写一个简单的画板程序,支持拖拽矩形、圆形,再加个删除键。做完这个,你对 Scene、View、Item 的理解就到位了。