十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Qt QGraphicsView实战:实现流畅的滑动卡片窗体与惯性吸附效果

Qt QGraphicsView实战:实现流畅的滑动卡片窗体与惯性吸附效果 简介基于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 QStackedWidget、QScrollArea 和 QGraphicsView 的取舍方案滑动连续感惯性/吸附实现子控件交互一句话评价QStackedWidget无只有切换动画需要额外叠加贝塞尔插值天然支持只适合简单 fade/slide 换页QScrollArea有连续滚动需要自己维护 ScrollBar 位置吸附逻辑散落在各处支持但状态同步麻烦适合长内容滚动不适合页面卡扣QGraphicsView完全可控直接操作 Item 坐标或 view center吸附就是一次动画需要处理事件转发做滑动效果的上限最高QGraphicsView 在技术上更合适的原因有几个。QGraphicsItem 支持独立命中检测、绘制、父子关系一个页面就是一个 Item更新某一页不需要通知整个界面QGraphicsScene 内部用 BSP 树索引 ItemItem 数量多时也不会让场景遍历变慢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 hrefhttps://download.csdn.net/download/huihui1988/2808505 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p
返回列表