☰
Qt QGraphicsView实战:实现流畅的滑动卡片窗体与惯性吸附效果
2026/10/2 19:37:52 网站建设 项目流程

简介:基于Qt的图形视图框架,这份压缩包提供了一个可运行的滑动窗体示例工程,面向入门级Qt开发者,帮助理解场景、视图与图元三者协作构建可视化界面的核心机制。示例将工具栏与图片均作为场景中的图元,直观演示了如何在场景中摆放元素并通过视图滚动呈现,可用于学习自定义图元、场景管理和视图交互等基础技能。压缩包共25个文件,以jpg、svg、png等图片资源为主,同时包含qrc资源索引、cpp与pro源码工程以及txt说明文档,整体约664KB,轻量且结构完整,便于直接编译运行并修改扩展。目前已有3306人学习或下载,适合想要通过一个小型案例快速上手QGraphicsView滑动窗体效果的开发者。通过该工程可掌握从场景搭建、图元添加到视图显示的基本流程,并借助附带的多类图片资源进行替换练习,为后续开发更复杂的交互界面打下坚实基础。 做上位机界面的时候,经常遇到一个需求:用 Qt 的 QGraphicsView 把一个参数卡片面板做成可以左右滑动的窗体效果,类似手机端那种手指一划就换页,松手自动吸附到最近的卡片。第一次做这个功能,我第一反应是用 QStackedWidget 配 QPropertyAnimation,结果只有翻页动画,滑动过程中间那一段根本看不着,跟手感和惯性完全无从谈起;后来换成 QScrollArea,是能滚了,但吸附到某一页、边缘回弹、循环滑动这些逻辑写起来非常别扭,性能也一般。最后把方案定在 QGraphicsView 上。这篇把完整思路、代码和踩坑记录都分享出来,适合正在做 Qt 界面开发、上位机、组态软件、地图类应用,或者只是想把界面做得更细腻的读者参考。

1. 为什么选 QGraphicsView 来做滑动窗体

1.1 滑动窗体到底在解决什么问题

需求本质上就是:一组等宽页面按顺序排列,视口一次只显示一页,用户可以用鼠标、触摸或滚轮拖动,让页面跟随手移动;松手后要么停在原页,要么吸附到相邻页。还可以延伸出惯性:手甩得越快,飞得越远。

从实现角度拆解,它由四件事组成:

  1. 多个页面需要独立摆放、独立绘制;
  2. 页面位置要能连续修改,中间过程可见;
  3. 拖拽结束后要有平滑动画补位;
  4. 滑动过程要有能撑得起的渲染性能。

这样拆开之后,选型就清楚了。绝大多数“滑动窗体”实现得别扭,不是不会写动画,而是容器选错了。

1.2 QStackedWidget、QScrollArea 和 QGraphicsView 的取舍

方案滑动连续感惯性/吸附实现子控件交互一句话评价
QStackedWidget无,只有切换动画需要额外叠加贝塞尔插值天然支持只适合简单 fade/slide 换页
QScrollArea有连续滚动需要自己维护 ScrollBar 位置,吸附逻辑散落在各处支持,但状态同步麻烦适合长内容滚动,不适合页面卡扣
QGraphicsView完全可控直接操作 Item 坐标或 view center,吸附就是一次动画需要处理事件转发做滑动效果的上限最高

QGraphicsView 在技术上更合适的原因有几个。QGraphicsItem 支持独立命中检测、绘制、父子关系,一个页面就是一个 Item,更新某一页不需要通知整个界面;QGraphicsScene 内部用 BSP 树索引 Item,Item 数量多时也不会让场景遍历变慢;QGraphicsView 的 viewport 变换(scale/rotate)建立在一套统一坐标系上,后续想放大、裁剪都顺手;QPropertyAnimation 可以直接对坐标属性做插值,配合各种缓动曲线,惯性、弹簧都只是换个曲线的事。

1.3 我最终选型的理由

选 QGraphicsView 不只是因为动画,更重要的是“后续扩展”。比如想把滑动卡片做成带磁吸效果,或者在卡片后面加一层背景装饰,或者把整个视口缩小成缩略图导航,在 Scene/View 体系下都是加 Item、改映射的事,代码不需要推翻重来。如果一开始图省事用 QScrollArea,这些需求基本都要重写。我见过不少项目从 QScrollArea 迁移到 QGraphicsView,不是因为 QScrollArea 写不出来,而是因为每加一个交互需求,代码复杂度就失控一次。

2. 坐标系统:滑动效果背后的数学基础

2.1 三套坐标,别搞混

QGraphicsView 里有三个坐标系:场景坐标(Scene)、视图坐标(View)、视口坐标(Viewport)。场景坐标是整个虚拟世界的坐标,Item 的位置用 setPos 设置时用的就是它;视图坐标是 View 部件内部的坐标;视口坐标是实际绘制窗口的坐标,通常和视图坐标重合,但在有边框、滚动条或 transform 时会存在偏移。

滑动窗体的本质是改变视口在场景中的位置。改法有两种:一是修改每个 Item 的 pos,让所有页面一起动;二是保持 Item 不动,让 View 的“镜头”移动,也就是调用 centerOn()。我更推荐第二种。理由是页面之间有关联结构时,只移动一个镜头比移动所有 Item 更省事,而且以后加缩放、旋转,中心点不会漂移。

2.2 QGraphicsItem 的绘制契约

每个页面是一个 QGraphicsItem 子类,至少要重写两个函数:

class SlidePageItem : public QGraphicsItem { public: SlidePageItem(qreal w, qreal h) : m_w(w), m_h(h) {} QRectF boundingRect() const override { return QRectF(0, 0, m_w, m_h); } void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override { // 这里画卡片背景、边框、内容等等 } private: qreal m_w = 0.0; qreal m_h = 0.0; };

boundingRect 必须返回 Item 在局部坐标系里的绘制范围,这决定了 View 何时认为它需要重绘,也影响了点击命中测试。paint 里所有绘制都基于局部坐标,不需要关心它在场景里的位置,这让 Item 内部的代码非常干净。页面的位置完全由 setPos 控制,比如第 i 页就摆到(i * pageWidth, 0)。

2.3 一个简单的场景摆放

auto *scene = new QGraphicsScene(this); const int pageCount = 6; const qreal pageW = 900.0; const qreal pageH = 600.0; for (int i = 0; i < pageCount; ++i) { auto *page = new SlidePageItem(pageW, pageH); page->setPos(i * pageW, 0); scene->addItem(page); } auto *view = new QGraphicsView(scene); view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);

这里有个很多教程不会讲的细节:sceneRect 最好前后各留一页宽的空白。默认 sceneRect 如果刚好等于内容大小,centerOn 一到边界就顶死了,视觉上非常生硬;留白后可以做出“拖出边界再弹回”的手感。留白的量可以这样设置:

scene->setSceneRect(-pageW, -pageH, (pageCount + 2) * pageW, pageH * 3);

Item 的绘制不会受空白影响,因为它只画在局部坐标的 boundingRect 范围内,空白区域只是场景的可视范围。

3. 核心实现:从搭建场景到惯性滚动

3.1 事件处理:用“镜头移动”模拟手指拖拽

我建议在 QGraphicsView 的子类里重写鼠标事件。核心思路:按下时把鼠标的 view 坐标通过 mapToScene 转成场景坐标,记录为上一次场景坐标;移动时用 mapToScene 计算当前场景坐标和上一次场景坐标的差,这个差值就是手指相对场景“推”了多少;然后利用 centerOn() 改变视口中心。

void SlideView::mousePressEvent(QMouseEvent *event) { m_lastScenePos = mapToScene(event->pos()); m_dragging = true; m_frames.clear(); stopAnimation(); QGraphicsView::mousePressEvent(event); } void SlideView::mouseMoveEvent(QMouseEvent *event) { if (!m_dragging) { QGraphicsView::mouseMoveEvent(event); return; } const QPointF scenePos = mapToScene(event->pos()); const qreal dx = scenePos.x() - m_lastScenePos.x(); // 手指向右拖,说明想看左边内容,镜头中心要左移 m_centerX -= dx; centerOn(m_centerX, 0); m_lastScenePos = scenePos; m_frames.append(qMakePair(scenePos.x(), QDateTime::currentMSecsSinceEpoch())); if (m_frames.size() > 5) m_frames.removeFirst(); QGraphicsView::mouseMoveEvent(event); }

注意最后调用基类,是为了让默认行为继续处理其它必要逻辑。但如果页面内部还有可点击控件,这个调用时机和事件传递需要额外处理,后面坑章节会展开。

3.2 惯性:让滑动有“甩出去”的感觉

惯性不能靠硬编码的固定动画,需要先估算松手瞬间速度。我用最近几帧的位移和时间差来算:

qreal SlideView::estimateVelocity() const { if (m_frames.size() < 2) return 0; const auto &last = m_frames.last(); const auto &first = m_frames.first(); qint64 dtMs = last.second - first.second; if (dtMs <= 0) return 0; return (last.first - first.first) * 1000.0 / dtMs; }

返回值单位是“场景坐标每秒”。在 mouseReleaseEvent 中,用速度预测落点,再启动一个 QPropertyAnimation 飞过去:

void SlideView::mouseReleaseEvent(QMouseEvent *event) { m_dragging = false; const qreal v = estimateVelocity(); const qreal predicted = m_centerX - v * 0.2; const int target = clampedIndex(predicted); animateToIndex(target); QGraphicsView::mouseReleaseEvent(event); } void SlideView::animateToIndex(int index) { stopAnimation(); const qreal targetX = index * m_pageWidth; auto *anim = new QPropertyAnimation(this, "centerX"); anim->setStartValue(m_centerX); anim->setEndValue(targetX); anim->setDuration(350); anim->setEasingCur <p> <a href="https://download.csdn.net/download/huihui1988/2808505" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询