新闻详情

新闻详情

首页 / 资讯中心 / 详情

Qt散点图实现:基于QGraphicsView的高性能交互可视化方案

发布时间:2026/9/15 17:37:46来源:尧图网络
Qt散点图实现:基于QGraphicsView的高性能交互可视化方案
简介这是一份面向Qt开发者的散点图实现源码示例。它聚焦于QGraphicsView图形视图框架适合需要掌握自定义二维数据可视化、或想用C在Qt中绘制动态散点图的中初级开发者。压缩包体积仅6KB共含5个文件其中包含2个cpp源文件、1个头文件以及1个pro工程文件结构完整精简可直接用Qt Creator打开构建。已有231人参与学习。源码虽然小巧却完整呈现了散点图从数据绑定到界面渲染的典型流程通过QGraphicsScene管理散点项、子类化QGraphicsItem并重写paint()绘制点形状同时涵盖鼠标交互、视图缩放与平移、批量渲染性能优化等实操技巧。读者可一边阅读代码一边运行调试既能理解图形视图框架的对象组织方式也能借鉴其中事件处理和更新刷新的写法快速迁移到更复杂的大数据量可视化项目中。1. 为什么大数据量散点图不自己 paintEvent 而要选 QGraphicsView两万个点实时刷新如果还在 QWidget::paintEvent 里逐个 drawPoint一次重绘就能让界面卡掉半秒更别提还要做缩放平移和点选。这套qt散点图实现源码.zip用的思路是把每个数据点封装成 QGraphicsItem交给 QGraphicsScene 管理再由 QGraphicsView 按可视区域裁剪。它不是为了替代 matplotlib 或 QCustomPlot 这类重型绘图库而是解决工控看板、设备监控、科研数据预览这类“数据量几万到十几万还要缩放、平移、点选”的 qt 界面需求源码里的 scatterchart.pro、chartview.cpp、chartview.h、main.cpp 四件套提供了一条可以直接复用的完整链路。适合已经会用基础控件、想摸清 Qt 图形视图框架边界的开发者也能给那些正打算从自绘切到 GraphicsView 的工程做一个最小可运行范本。2. 搭出可复用的 qt 散点图骨架Scene、View、Item 三角分工2.1 工程文件与类的初始分工解压qt散点图实现源码.zip后第一件事不是去看 chartview.cpp而是先看 scatterchart.pro。这个文件决定了整个项目依赖哪些 Qt 模块很多初学把自己的 .cpp 加进工程后报QGraphicsScene not found就是 pro 里少了模块声明。Qt 5.15 下典型的工程配置是QT core gui widgets TARGET scatterchart TEMPLATE app SOURCES \ main.cpp \ chartview.cpp \ scatterchart.cpp HEADERS \ chartview.h \ scatterchart.h这里QT widgets是必须的QGraphicsView、QGraphicsScene、QPainter 都依赖这个模块core和gui在 Qt 5 里虽然默认引入但显式写出来能让工程后续迁到 Qt 6 的 CMakeLists.txt 时少踩一个坑。scatterchart.pro.user是 Qt Creator 生成本地配置里面记录的是编译器路径和构建目录不需要提交进版本库但拿到源码后如果没有这个文件用 Qt Creator 直接打开 .pro 也会自动生成不影响编译。main.cpp 的逻辑非常收敛只做一件事构造 QApplication把 ChartView 实例设为窗口的中央部件。真正画散点的代码全部收在 ChartView 和散点 Item 里。这种组织方式的好处是如果你有现成的监控面板只需把 ChartView 当成普通 widget 塞进 QSplitter 或 QTabWidget耦合度很低。2.2 ChartView 如何组装 Scene 和 ViewChartView 继承 QGraphicsView 而不是直接 new 一个裸视图是为了把初始化参数统一封装在构造函数里。核心代码在 chartview.h 和 chartview.cpp 中// chartview.h #ifndef CHARTVIEW_H #define CHARTVIEW_H #include QGraphicsView class QGraphicsScene; class ChartView : public QGraphicsView { Q_OBJECT public: explicit ChartView(QWidget *parent nullptr); void loadPoints(const QString filePath); protected: void resizeEvent(QResizeEvent *event) override; private: QGraphicsScene *m_scene; void setupScene(); }; #endif // CHARTVIEW_H// chartview.cpp #include chartview.h #include scatterchart.h #include QGraphicsScene #include QResizeEvent ChartView::ChartView(QWidget *parent) : QGraphicsView(parent) , m_scene(nullptr) { setupScene(); // 抗锯齿只开在视图层Item 内部不需要重复设置 setRenderHint(QPainter::Antialiasing); // 左键按住拖动整个视图适合大范围浏览散点分布 setDragMode(QGraphicsView::ScrollHandDrag); // 缩放锚点跟随鼠标滚轮向上时向光标位置放大 setTransformationAnchor(QGraphicsView::AnchorUnderMouse); // 窗口尺寸变化时保持场景中心不跑偏 setResizeAnchor(QGraphicsView::AnchorViewCenter); } void ChartView::setupScene() { m_scene new QGraphicsScene(this); // 先给一个足够大的绘图区域后续加载数据时再按数据范围收紧 m_scene-setSceneRect(-500, -500, 1000, 1000); setScene(m_scene); }参数这里有几个细节setDragMode(ScrollHandDrag)会让左键变成抓手直接把视图拖走这适合“先看全局再放大看细节”的散点场景如果改成RubberBandDrag左键就变成框选拖拽浏览就得换成中键或右键交互习惯完全不同。AnchorUnderMouse是地图类应用最常见的缩放锚点光标指哪放大哪而AnchorViewCenter更适合按钮驱动的缩放比如工具栏里的 / - 键。这套源码里直接用了AnchorUnderMouse说明设计目标就是鼠标滚轮为主的浏览体验。2.3 自定义 QGraphicsItem边界与绘制回调散点图中每一个点都是一个 QGraphicsItem 子类工程里的 scatterchart.cpp/h 承担这个职责。这里最容易写错的是boundingRect()和paint()的配比// scatterchart.h #ifndef SCATTERCHART_H #define SCATTERCHART_H #include QGraphicsItem #include QColor class ScatterItem : public QGraphicsItem { public: ScatterItem(qreal x, qreal y, const QColor color); QRectF boundingRect() const override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; protected: void mousePressEvent(QGraphicsSceneMouseEvent *event) override; void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override; private: QColor m_color; qreal m_x; qreal m_y; qreal m_radius 5.0; }; #endif // SCATTERCHART_H// scatterchart.cpp #include scatterchart.h #include QPainter #include QGraphicsSceneMouseEvent ScatterItem::ScatterItem(qreal x, qreal y, const QColor color) : m_color(color), m_x(x), m_y(y) { setPos(m_x, m_y); setAcceptHoverEvents(true); setCursor(Qt::PointingHandCursor); } QRectF ScatterItem::boundingRect() const { // 半径 5外扩 2 像素作为鼠标热区和抗锯齿余量 return QRectF(-m_radius - 2, -m_radius - 2, (m_radius 2) * 2, (m_radius 2) * 2); } void ScatterItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { Q_UNUSED(option); Q_UNUSED(widget); painter-setPen(Qt::NoPen); painter-setBrush(m_color); painter-drawEllipse(QPointF(0, 0), m_radius, m_radius); }这里的边界外扩值得解释一句如果boundingRect()刚好等于圆的外切正方形很多场景下鼠标点击圆边缘会判定成“未命中”因为抗锯齿绘制的像素略微超出几何边界。留出 2 像素余量后命中检测稳定得多代价可以忽略。paint()里用drawEllipse(QPointF(0,0), radius, radius)而不是drawArc前者在现代 Qt 版本里会直接走 GPU 加速的二次贝塞尔光栅化路径后者是 16 分之一的弧线逼近点数量上去后差别很明显。还要注意不要把场景坐标再叠加一次setPos和绘制坐标否则所有点会整体偏移到两倍坐标的位置。场景、视图、Item 三者的职责可以用一张表概括这也是整个qt散点图实现源码最核心的分工组件负责内容本工程对应文件QGraphicsScene管理 Item 生命周期、空间索引、碰撞检测chartview.cpp 中 m_sceneQGraphicsView视口裁剪、坐标变换、滚轮缩放、拖动事件分发chartview.h / chartview.cppQGraphicsItem单点位置、绘制外观、鼠标事件回调scatterchart.h / scatterchart.cpp3. 数据绑定、命中检测与视图交互让散点图可以真正查数3.1 从文本数据批量生成散点 Itemqt散点图实现源码里的数据加载通常走文件路径这是最通用的方式。常见做法是 QFile 加 QTextStream 逐行读取再按逗号或制表符切列。这里不推荐用 QRegExp 做整文件正则解析因为当第一行是表头时反而要额外处理脏数据逐行判断更直观bool ChartView::loadPoints(const QString filePath) { QFile file(filePath); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return false; QTextStream in(file); QVectorQPointF raw; raw.reserve(10000); // 预分配避免频繁扩容 while (!in.atEnd()) { const QString line in.readLine(); const QStringList cols line.split(QLatin1Char(,), Qt::SkipEmptyParts); if (cols.size() 2) continue; bool okX false; bool okY false; const qreal x cols.at(0).trimmed().toDouble(okX); const qreal y cols.at(1).trimmed().toDouble(okY); if (okX okY) raw.append(QPointF(x, y)); } m_scene-clear(); for (const QPointF p : raw) { ScatterItem *item new ScatterItem(p.x(), p.y(), QColor(31, 119, 180)); m_scene-addItem(item); } return true; }这段逻辑里有两个容易踩的坑。第一个是Qt::SkipEmptyParts它在 Qt 5.14 以后取代了旧的QString::SkipEmptyParts如果你把源码里的代码直接往 Qt 5.9 的老工程里搬编译就会报SkipEmptyParts is not a member反过来也一样。第二个是 CSV 解析和 Item 生成分成两步先收集到raw再统一建 Item这样可以在解析阶段把非法行过滤掉也方便后续做坐标归一化。如果数据量在 5 万行以上raw.reserve预分配能明显减少 QVector 扩容时的内存拷贝。实际项目里数据往往不是干净的 CSV可能有重复点、极值点。极值会导致setSceneRect被拉得非常大散点整体缩成一团。处理方式是先扫一遍数据算 min/max再根据数据范围收缩 sceneRectQRectF bounds m_scene-itemsBoundingRect(); m_scene-setSceneRect(bounds.adjusted(-20, -20, 20, 20));这样左侧留出 20 像素余量刻度标签不会贴边。3.2 鼠标点选与悬停反馈的实现源码中交互事件放在 ScatterItem 内部而不是 ChartView 里这个设计对新手不太直观但实际更合理。如果放在 QGraphicsView 的mousePressEvent里拿到的坐标是视口坐标还得用mapToScene才能换算成场景坐标而 Item 内部回调拿到的已经是本地坐标判断是否点在圆内只做一个距离计算即可void ScatterItem::mousePressEvent(QGraphicsSceneMouseEvent *event) { if (event-button() Qt::LeftButton) { // 先把上一次高亮恢复再高亮当前点 setBrush(QColor(214, 39, 40)); // 红色 update(); // 只重绘当前 item 的边界区域 } QGraphicsItem::mousePressEvent(event); } void ScatterItem::hoverEnterEvent(QGraphicsSceneHoverEvent *event) { Q_UNUSED(event); setToolTip(QStringLiteral((%1, %2)).arg(m_x).arg(m_y)); setBrush(m_color.lighter(120)); update(); }update()在这里很重要。QGraphicsItem 的 update 会通知场景把该 Item 的 boundingRect 加入脏区但不会触发整张场景重绘。很多人在 Item 里改了颜色后忘记调用 update画面上毫无反应这不是 bug是更新机制没走完。鼠标事件处理完后记得要调QGraphicsItem::mousePressEvent(event)否则可能破坏框架内部的事件传递链。如果要做“点击后高亮并维持选中”建议配合setFlag(QGraphicsItem::ItemIsSelectable)然后在 paint 里判断option-state QStyle::State_Selected来决定画刷颜色这样比在事件里手动维护状态更符合 Qt 的原生交互模型。本工程源码里用的是比较朴素的事件内 setBrush适合初学理解事件流。3.3 缩放平移参数与常见的误配ChartView 构造里已经设置了ScrollHandDrag和AnchorUnderMouse这是散点图浏览的默认组合。还有一个容易被忽略的是setRubberBandSelectionMode它控制框选时 Item 的命中规则setDragMode(QGraphicsView::RubberBandDrag); setRubberBandSelectionMode(Qt::ContainsItemShape);Qt::ContainsItemShape表示只有完全被框住的 Item 才被选中Qt::IntersectsItemShape则只要沾边就算选中。做数据筛选时后者更顺手做精确抠点分析时前者更合理。下面是三组常用组合的对比组合用户体感适用场景ScrollHandDragAnchorUnderMouse左键拖拽滚轮朝光标方向放大通用散点浏览RubberBandDragAnchorViewCenter左键框选按视图中心缩放数据筛选、圈选分析NoDragAnchorUnderMouse禁止拖拽仅滚轮缩放展示大屏、只读模式有一个误配特别常见在构造函数里先setSceneRect(0, 0, 800, 600)然后在resizeEvent里又根据窗口大小重新设置 sceneRect结果数据点坐标是正的sceneRect 原点却是负的视图一启动就看不到点。正确做法是保留上面 loadPoints 最后的itemsBoundingRect()收缩方式resizeEvent 里只做视口适配不要反复重置 sceneRect。4. 渲染性能与绘图细节散点从能跑到跑得流畅4.1 别把 ItemHasNoContents 误当成散点加速开关在 Qt 的图形视图框架里提到 qt绘图效率比较时很多资料会提到QGraphicsItem::ItemHasNoContents这个 flag。它的本意是告诉框架该 Item 没有可见内容可以被跳过绘制但散点 Item 恰恰是需要绘制的对象。如果给散点加上这个 flag最直接的结果是界面上的点全部消失而不是跑得更快。这个源码里出现过类似尝试正确理解是它适合那些只做坐标占位、不参与视觉输出的辅助 Item。真正对散点绘制有效的三个手段按收益从高到低排列// 1. 缓存绘制结果缩放平移时不再反复执行 paint() item-setCacheMode(QGraphicsItem::DeviceCoordinateCache); // 2. 关闭 Item 的默认转换更新如果移动不频繁 item-setFlag(QGraphicsItem::ItemSendsGeometryChanges, false); // 3. 背景网格用单一 QGraphicsItemGroup而不是每个刻度一个 ItemDeviceCoordinateCache的代价是缓存位图会占用额外的内存一个 20 MB 的场景可能因此多占几 MB,但换来的滚轮缩放流畅度提升非常明显。注意如果 Item 被设置成ItemIsMovable且频繁拖动缓存反而会因为反复失效而降低性能所以移动型散点和静态展示型散点要用不同的缓存策略。paint 内部的绘图指令也值得抠细节。同样的圆不同写法性能差距成倍// 高效无笔 实心画刷 drawEllipse 中心半径重载 painter-setPen(Qt::NoPen); painter-setBrush(m_color); painter-drawEllipse(QPointF(0, 0), m_radius, m_radius); // 低效带宽度画笔 drawArc 弧线逼近 painter-setPen(QPen(m_color, 2)); painter-drawArc(QRectF(-5, -5, 10, 10), 0, 360 * 16);第二段代码使用了360 * 16定义整圆最终会被拆成几十段贝塞尔曲线点数量到一万以上时 CPU 占用差别肉眼可见。drawEllipse的重载在 Qt 内部走短路径且没有画笔时省掉了描边计算。4.2 大数据量批量添加与进度反馈几千个数据点时直接循环addItem毫无压力数据量到 5 万以上时除了绘制Item 对象的构造、场景空间索引的插入也开始成为瓶颈。除了前面reserve预分配另一个实用技巧是分块让出事件循环const int total raw.size(); for (int i 0; i total; i) { ScatterItem *item new ScatterItem(raw.at(i).x(), raw.at(i).y(), QColor(31, 119, 180)); m_scene-addItem(item); // 每 2048 个点处理一次事件避免界面长时间无响应 if ((i 0x7FF) 0) { QCoreApplication::processEvents( QEventLoop::ExcludeUserInputEvents); } }ExcludeUserInputEvents在这里有讲究加载期间仍处理绘制事件和定时器但不响应鼠标键盘输入防止用户在数据没加载完时拖拽视图导致状态错乱。要更精细的反馈可以配合 QProgressDialog 或状态栏进度条加载到一半还能响应取消这是 qt 开发里处理长任务的常规套路。不同数据量级下的策略可以按下面的表来选数据量策略说明 5k直接 Item 绘制无需额外优化5k ~ 50kDeviceCoordinateCache 关闭 ItemIsMovable缩放平移明显提速 50k抽稀或转为 QImage 整体渲染继续用 Item 会拖垮场景索引超过 5 万点再逐项维护就没有意义了。更极端的设计是把点画进一张 QImage然后只给场景添加一个 Image Item这样缩放时图像的清晰度会受损但交互流畅度能保住适合不需要逐点精确查看的分布概览。4.3 QSS 只影响视图层Item 外观靠 QPen 和 QBrush很多人习惯用 QSS 给整个界面换皮肤但 QGraphicsItem 并不是 QWidgetQt 样式表作用不到 Item 内部的画笔和画刷。这套源码里样式表只用在 QGraphicsView 这一层setStyleSheet(R( QGraphicsView { background-color: #f8f9fa; border: 1px solid #d0d7de; border-radius: 4px; } ));背景色、边框、圆角都作用于 viewport散点的颜色仍由ScatterItem构造函数里传入的 QColor 决定。如果你发现 Item 颜色没有按 QSS 变化这不是源码 bug而是机制本来就如此。更可控的做法是把颜色、线宽等外观参数封装成结构体传给 Item需要换肤时直接重建颜色并调用update()。背景网格则用drawLine画在 scene 的最低层或者干脆在 ChartView 里重写drawBackgroundvoid ChartView::drawBackground(QPainter *painter, const QRectF rect) { painter-setPen(QPen(QColor(230, 230, 230), 1)); // 以 50 像素为步长画网格 for (qreal x rect.left(); x rect.right(); x 50) painter-drawLine(QPointF(x, rect.top()), QPointF(x, rect.bottom())); for (qreal y rect.top(); y rect.bottom(); y 50) painter-drawLine(QPointF(rect.left(), y), QPointF(rect.right(), y)); }drawBackground在每次 View 重绘时自动调用网格会跟随缩放步长自适应而不会像普通 Item 那样被随意拉伸变形。这段代码放在 ChartView 子类里比放在场景中合成更干净。5. 动态追加数据时的局部刷新与散点过渡动画5.1 让新点只刷新局部区域运行时新数据进入时直接QGraphicsScene::addItem会把整个场景的脏区范围扩大数据点密集且刷新频率高时全区域重绘的成本会迅速吃掉性能。更克制的做法是只让新增点对应的矩形失效ScatterItem *item new ScatterItem(x, y, QColor(31, 119, 180)); m_scene-addItem(item); // 只把新 item 的边界区域加入重绘队列 m_scene-invalidate(item-boundingRect());invalidate和update的区别在于前者同时会使该区域的缓存失效后者只是排队重绘配合上一章DeviceCoordinateCache使用时invalidate必须被调用否则缓存里的旧位图会被继续使用新点显示不出来。5.2 用 QVariantAnimation 实现散点平滑移入直接新点出现和老点跳变在监控类界面里会显得很突兀。给 Item 单独挂一个 QVariantAnimation从上一帧坐标插值到当前位置视觉上就有“数据流动”的感觉QVariantAnimation *anim new QVariantAnimation(item); anim-setDuration(300); // 300ms 过渡 anim-setStartValue(item-pos()); anim-setEndValue(QPointF(newX, newY)); QObject::connect(anim, QVariantAnimation::valueChanged, item, [item](const QVariant value) { item-setPos(value.toPointF()); }); anim-start(QAbstractAnimation::DeleteWhenStopped);注意把 QVariantAnimation 的父对象设为 item这样 item 销毁时动画会自动停止不会产生悬空指针。DeleteWhenStopped保证动画结束后内存被回收。每秒刷新几百个点以内的场景用这个方案没问题超过则应该取消动画直接 setPos否则动画对象大量堆积反而拖慢主线程。5.3 离线渲染验证点密度与边界裁剪最后给一个快速验证手段。需要确认散点图在不同缩放级别下有没有被错误裁剪时不要反复用肉眼拖视图直接离屏渲染一张图出来对比QImage img(800, 600, QImage::Format_ARGB32); img.fill(Qt::white); QPainter painter(img); m_scene-render(painter, img.rect(), m_scene-sceneRect()); painter.end(); img.save(scatter_preview.png, PNG);打开生成的预览图重点看两个地方散点圆边缘是否完整以及坐标轴边界是否被视口切断。如果发现最外圈的点只剩一半通常不是drawEllipse的问题而是boundingRect没有留出余量按第 2 章里外扩 2 像素的做法即可修复。另一种常见情况是数据量大时预览图点太密集、分不清真实分布可以在渲染前临时把画笔透明度降到 0.5利用叠色的深浅判断点密度。这类做法很适合作为自动测试的一部分把不同数据集的渲染结果保存下来做像素差异比对。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

diabetes数据集线性回归拆解:单特征斜率与R²深度解读 2026/9/15 19:14:28

diabetes数据集线性回归拆解:单特征斜率与R²深度解读

拿到sklearn里的diabetes数据集,很多人第一件事就是LinearRegression().fit(X, y),看两眼R,然后就没有然后了。波士顿房价数据集被讲烂了之后,diabetes成了另一个"练手标配",但说实话,我见过不少…

阅读更多 →
Kutt 自建短链接服务指南:3 条命令完成部署,附生产配置取舍 2026/9/15 19:14:28

Kutt 自建短链接服务指南:3 条命令完成部署,附生产配置取舍

Kutt 自建短链接服务指南:3 条命令完成部署,附生产配置取舍 【免费下载链接】kutt Free Modern URL Shortener. 项目地址: https://gitcode.com/GitHub_Trending/ku/kutt Kutt 是一个免费、现代的自建短链接服务(URL Shortener&#x…

阅读更多 →
5个坑教你搞定wordpress多博客,备案不迷路选哪家好 2026/9/15 19:14:28

5个坑教你搞定wordpress多博客,备案不迷路选哪家好

5个坑教你搞定wordpress多博客,备案不迷路选哪家好 备案流程一头雾水?别慌。很多做wordpress多博客的朋友,代码写得很溜,一到ICP备案就卡壳,不知道材料怎么交,更不知道服务器选哪家才稳。其实,多博客架构对域名和服务器IP的绑…

阅读更多 →
抖音批量下载完整实战:1个主页链接,快速无水印下载全部作品 2026/9/15 19:14:28

抖音批量下载完整实战:1个主页链接,快速无水印下载全部作品

抖音批量下载完整实战:1个主页链接,快速无水印下载全部作品 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and browse…

阅读更多 →
抖音下载器:支持抖音视频、图集与音乐的开源批量下载工具完整指南 2026/9/15 19:14:28

抖音下载器:支持抖音视频、图集与音乐的开源批量下载工具完整指南

抖音下载器:支持抖音视频、图集与音乐的开源批量下载工具完整指南 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and browser fal…

阅读更多 →
Ventoy多系统启动U盘:从分区布局到UEFI安全启动的全解析 2026/9/15 19:11:28

Ventoy多系统启动U盘:从分区布局到UEFI安全启动的全解析

简介:Ventoy是一款开源的多重U盘启动盘制作工具,主要面向需要频繁安装或维护多系统的技术用户、运维人员与电脑维修从业者。它的核心价值在于免去反复格式化U盘的麻烦,只需将ISO/WIM/IMG等镜像文件直接拷贝进U盘,开机时通过菜单自…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞