简介:这是一份基于 Qt 框架的甘特图组件源码包,面向需要在桌面应用中展示项目任务时间线、进度或排程信息的 Qt 开发者,也适合想深入学习 QGraphicsView 自定义绘图与模型-视图交互的进阶用户。完整工程包含 27 个文件,压缩包约 1.69MB,其中 5 个 C++ 源文件与 5 个头文件为核心代码,2 个界面文件与 2 个资源文件用于 UI 构建,另有工程配置、可执行程序及说明文档,目录划分清晰,可直接编译运行或集成到现有项目。已有 860 人浏览学习。组件围绕 QGraphicsItem 条形图绘制展开,从 MVC 数据绑定、时间戳坐标换算,到鼠标拖动与事件响应、自定义绘制函数、布局滚动和性能优化均有涉及;同时包含动画与过渡效果、任务依赖等扩展配置,方便按需调整样式与功能。通过阅读源码和工程结构,既能理解甘特图在 Qt 中的完整落地方式,也能复用该组件快速搭建项目管理界面,或在其基础上进行二次开发。
1. 从“表格画甘特”到“场景画甘特”,一个值得下下来拆一拆的版本
先亮结论:如果你手里的 Qt 甘特图任务量一上两千条就开始卡矩阵,那大概率是走错了路——很多人习惯用 QTableView 加自定义 delegate 去做甘特图,行高、列宽、滚动条都是白捡的,但真正拖起来、缩放起来,这套方案的上限很低。我拿到这个“qt 甘特图另一个版本”源码的时候,第一反应也是一样:不就是用 QGraphicsView 重写么?直到我自己把任务行数加到一万条,左右拖时间轴,丝滑到不像是同一个项目后,才意识到这个版本是在底层坐标体系和渲染缓存上做了正经功夫的。它不是商业级全功能甘特控件,而是一个干净的基础框架,适合已经会用 Qt 但需要在甘特图上做深度定制的团队。如果你正在为选 QTableView 还是自绘而犹豫,或者手头有个任务量不大但交互要求高的项目,这份源码值得下载拆一遍。
2. 版本原理:为什么这一版能把数量级的性能打上去
2.1 两种实现方式的实测差距:delegate 绘画 vs 场景 Item 渲染
很多人第一次做甘特图,最常见的方案是拿 QTableView 外加一个自定义 delegate,在 paint() 里去画任务条和依赖线。这种做法的优点很明显:行高、列宽、选中状态、滚动条都跟表格是天然配套的,代码量不大,十几行 delegate 就能画出一个像样的条。但它的问题也很致命:QTableView 只对你看到的行调用 paint(),这在任务数少时很友好,可一旦滚动频繁或数据更新,整个 visible 区域全部重绘,任何浮点坐标的变换都要在 paint() 里临时算一遍,历史经验是超过两千条任务,拖动时间轴就会明显感到 GPU 使用率上不去,掉帧倒还是小事,工作项多了以后 hover 和 selection 的响应都跟着钝。
而这个“另一个版本”走的是 QGraphicsView + QGraphicsItem 路线。它把每一个任务条设计成一个独立的 item,注意是 item 而不是 delegate。在 QGraphicsScene 里,未遮挡的 item 会走自己的缓存,View 在滚动时只需要告诉场景“我看到的区域偏移了”,场景就能利用 item 自身的坐标和层级做局部重绘,不需要像 QTableView 那样每次重算出整个可视区域。更关键的是,QGraphicsView 的 scale() 天然就是做坐标准换的,放大时不用重新计算每个任务的像素位置,只要改一下视图变换矩阵,任务条的边界和字体同步放大。拿同样一台看起来并不新、跑 Ubuntu 的办公机实测,一万行任务,每个行上三五粒依赖线,水平缩放到 0.5 到 3.0 之间拖动,帧率基本稳定在 50 朝上。这在 QTableView 方案下是做不到的,除非你愿意每次缩放都重新 setColumnWidth 并强制 update,那样滚动条和内容之间会有肉眼可见的错位。
2.2 这个版本的坐标体系与缩放机制:从“时间戳”到“像素”的映射
真正读这份源码,最值得先看的是它的坐标映射。普通 QTableView 版甘特图,左列是任务列表,右列是时间轴,两个区域的滚动是同步的;而这个 QGraphicsView 版本把所有东西都放进一个 scene 里,任务名文本也是 item 的附属 element,时间轴不在“列”里,而是直接画在 item 的 paint() 里。它的核心思想是“以场景时间为基准、像素为表现”:源码里常见这样一个函数
class GanttScene : public QGraphicsScene { public: // 把业务时间映射到场景坐标,scaleFactor 表示每秒对应的像素宽度 qreal timeToX(const QDateTime &time) const { return m_originTime.msecsTo(time) / 1000.0 * m_pixelsPerSecond; } // 把场景坐标反推回业务时间,用于拖动和点击命中 QDateTime xToTime(qreal x) const { qint64 msecs = m_originTime.msecsTo(m_originTime) + qint64(x / m_pixelsPerSecond * 1000.0); return m_originTime.addMSecs(msecs); } void setViewRange(const QDateTime &start, const QDateTime &end); private: QDateTime m_originTime; // 场景 0 对应的业务时间原点 qreal m_pixelsPerSecond = 10.0; // 每秒 10 个像素,缩放时改变该值 };这里 m_originTime 是项目开始的最早时间点,所有任务条的位置都相对于它计算。m_pixelsPerSecond 是关键旋钮,它把时间轴的长度直接变成“一秒等于多少像素”。当我们对视图做 scale() 操作时,其实更推荐直接改 m_pixelsPerSecond 再重新绘制,这样能避免 view 变换矩阵与 item 内部坐标相互叠加产生漂移。缩放实现的常见做法是:
void GanttView::zoomIn() { // 以鼠标当前点作为锚点来变换,避免缩放后视角跑飞 QPointF mouseScenePos = mapToScene(m_mouseLastPos); m_pixelsPerSecond *= 1.2; // 重新设置 sceneRect 和视图缩放 resetTransform(); scale(m_pixelsPerSecond / m_basePixelsPerSecond, m_pixelsPerSecond / m_basePixelsPerSecond); // 调整滚动位置,确保鼠标悬停处的任务条不跳走 QPointF newScenePos = mapToScene(m_mouseLastPos); QPointF d = newScenePos - mouseScenePos; scrollToScenePos(scenePos); }注意这里的 resetTransform()。因为 m_pixelsPerSecond 已经变大了,view 的 scale 只做一次绝对缩放,不要用 scale(1.2, 1.2) 这种反复累乘的方式。累乘的坑在于浮点误差会逐步累积,连续缩放十几次后坐标会明显漂移。而这份源码里把 m_pixelsPerSecond 当作唯一可信源,每次都用基准值重新计算,是它长时间缩放不歪的原因。
2.3 任务条 Item 的绘制边界:不重画才是性能关键
真正实现甘特条的是一个继承自 QGraphicsItem 的小类,每个任务一个实例。源码里值得注意的一个细节是 boundingRect() 返回的矩形比视觉绘制更大,留了一圈抗锯齿边距:
QRectF TaskBarItem::boundingRect() const { // 宽度等于时长映射到场景坐标的像素长度 qreal w = m_scene->timeToX(m_end); qreal h = m_rowHeight; // 给边框和阴影多留 2 像素,避免 setClip 把灰边裁掉 return QRectF(0, 0, w, h).adjusted(-2, -2, 2, 2); } void TaskBarItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { Q_UNUSED(option); Q_UNUSED(widget); // 只有在变化时才重绘,否则直接用缓存的 pixmap if (m_cacheDirty) { m_cache = renderToPixmap(); m_cacheDirty = false; } painter->drawPixmap(0, 0, m_cache); }许多初级 QGraphicsView 代码把绘制逻辑全部写进 paint(),每次鼠标移动、选中变化都会触发全部重绘,这是不必要的。这里用 m_cacheDirty 标记,只有任务条时间范围、颜色、进度发生变化时才重新渲染一次,平时 drawPixmap 只需要贴图。这种做法对任务多、频繁水平拖动的场景,能省掉大量 CPU 开销。整份源码里任务条本身并不重画,依赖线用 QGraphicsPathItem 画在另外一层,不做事的时候也是静止的,这才能保证大数据量下的流畅度。
3. 落地复现:把源码编译成自己项目模块
3.1 用 CMake 组织项目结构,避免 qmake 的路径地狱
这份源码是用 CMake 组织的,这比 qmake 那种“每个子项目都要手写 pri 文件”的结构好维护得多。下载后你会看到 src 目录下有三个核心文件:ganttscene.h、ganttview.h、ganttbaritem.h,外加一个 ext 目录放依赖的日期时间工具类。先不用急着改动,先建一个最小 app 工程把源码引进来,验证编译再谈集成。假设你下载的源码放在 thirdparty/gantt/ 下,那么根 CMakeLists.txt 可以这样写:
cmake_minimum_required(VERSION 3.16) project(GanttDemo VERSION 0.1 LANGUAGES CXX) # 设置 C++17 set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 将 gantt 源码加入构建 add_subdirectory(thirdparty/gantt) # 可执行程序 add_executable(gantt_demo main.cpp MainWindow.cpp ) target_link_libraries(gantt_demo PRIVATE gantt_lib Qt::Widgets ) # 把 gantt 资源目录加进来,提供样式表等资源 include_directories(${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/gantt/src)如果源码包里的 gantt_lib 没有写 CMake,或者你更习惯直接连源文件一起编,那就在根 CMakeLists 里显式加:
add_library(gantt_lib STATIC thirdparty/gantt/ganttscene.cpp thirdparty/gantt/ganttview.cpp thirdparty/gantt/ganttbaritem.cpp thirdparty/gantt/ganttitemmgr.cpp ) target_include_directories(gantt_lib PUBLIC thirdparty/gantt ) target_link_libraries(gantt_lib PUBLIC Qt::Widgets)这里有个坑:源码里的 GanttView 与 QGraphicsView 同名,注意别把 Qt 的类搞混。建议 include 时用大写前缀或者干脆 build 成库,然后使用命名空间。我一般会加:
#include "ganttview.h" using Gantt::View; // 如果源码支持命名空间的话如果没有命名空间也没有关系,那就是一个全局唯一类名 GanttView,直接使用没事。这种命名冲突在集成时最容易翻车,平时没坑,到了链接阶段可能出现“对 Qt 库类的重复定义”,那多半就是你自己没加命名空间。
3.2 在 MainWindow 里初始化视图和数据模型,两天能跑通
假设你已经把 gantt_lib 编过了,现在在 MainWindow 构造函数里拉起来。这份源码需要你给它提供两个东西:一个任务列表数据模型,以及它自己生成内部描述的小结构体。通常它自带的 model 是一个简单的 QAbstractItemModel,但为了快速试跑还是用提供好的默认接口。常见做法是直接从 Json 加载一个任务数组,每个任务字段包括 id、name、start、end、dependencies、progress。然后初始化:
#include "GanttScene.h" #include "GanttView.h" #include "GanttModel.h" MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { // 1. 创建场景并设定原点 GanttScene *scene = new GanttScene(this); scene->setOriginDateTime(QDateTime::currentDateTime().addDays(-7)); // 2. 创建视图,传入场景 m_ganttView = new GanttView(scene, this); setCentralWidget(m_ganttView); // 3. 设置视图的基本视口行为 m_ganttView->setRenderHint(QPainter::Antialiasing); m_ganttView->setViewportUpdateMode(QGraphicsView::BoundingRectViewportUpdate); m_ganttView->setDragMode(QGraphicsView::ScrollHandDrag); m_ganttView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); m_ganttView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // 4. 加载演示数据 GanttModel *model = new GanttModel(loadTasksFromJson(":data/sample.json"), scene); scene->setModel(model); }注意第四步:这个版本里 model 和数据是强绑定关系,调用 setModel 后,scene 会根据模型数据自动生成任务条 item 和时间轴刻度。如果数据没显示出来,先检查 JSON 字段是不是 start/end 两个时间字符串用的 UTC 与本地时间混用了。之前有一次同事直接把 UTC 字符串扔进去,结果任务条全部跑到 1970 年去了,和他确认后才知道是本地时区转换丢了。
逻辑说明:这里 viewportUpdateMode 选 BoundingRectViewportUpdate 很重要。默认的 MinimalViewportUpdate 在拖动大量 item 时重绘区域不精确,会看到整块残影;全区域更新又太费。BoundingRect 模式只更新被改变 item 覆盖的矩形联合区域,兼顾正确性和效率。如果你要追求极致流畅,也可以试试 NoViewportUpdate 配合手动 update,不过新手不建议。
3.3 关键参数:时间刻度的最小单位、行高和滚动速度
跑通以后,你会发现默认参数并不一定适合你的业务。这份源码里几处关键设置位置如下:
第一处是时间刻度粒度。默认是“一列代表一天”,但你要是做研发排期,一列一天显然太粗,需要看小时级。在 GanttScene 里通常有个枚举粒度,从小时、天到月都有。调整方式可以在 scene 初始化时调用:
GanttScene::ScaleMode mode = GanttScene::Hour; scene->setScaleMode(mode); scene->setPixelsPerHour(8); // 每小时 8 像素第二处是行高。GanttBarItem 的 boundingRect 高度默认是行高的 60%,如果你想更紧凑,可以改 model 里 rowHeight。但要注意:如果行高太小,任务条之间的竖排依赖线箭头会重叠,肉眼上很难看。一般行高建议在 24 到 30 像素,条高占到行高的 2/3 会比较舒服。
第三处是滚动速度。QGraphicsView 默认的滚动速度对于宽的甘特图来说太快,一滚就跑到很远。这里有两个选择:一是关闭横向滚动条,用鼠标水平拖拽;二是重写 wheelEvent,让滚轮垂直滚动,Shift+滚轮水平滚动。这个版本里,直接把 DragScroll 和鼠标位置绑定,可以实现以鼠标所在点为锚点缩放,但滚动速度需要用固定像素数控制:
void GanttView::wheelEvent(QWheelEvent *event) { // 如果按 Ctrl 就缩放,否则做像素级滚动 if (event->modifiers() & Qt::ControlModifier) { zoomAt(event->position(), event->angleDelta().y() > 0 ? 1.15 : 0.85); event->accept(); return; } int deg = event->angleDelta().y() / 120; qreal delta = deg > 0 ? -m_scrollStepPx : m_scrollStepPx; // 水平滚动优先级更高 if (event->modifiers() & Qt::ShiftModifier) { horizontalScrollBar()->setValue(horizontalScrollBar()->value() + delta); } else { verticalScrollBar()->setValue(verticalScrollBar()->value() + delta); } }这个 m_scrollStepPx 每次滚动的像素距离,我一般设置 80 到 120,太小了滚半天,太大了一滚就跳出可视区。Ctrl+滚轮缩放也是我刚集成成功后第一个加的自定义交互,算是一个高频习惯用法。
4. 避坑:自绘甘特图的四个翻车点
4.1 任务条时不时变成全白,看不到内容
现象:第一次跑起来一切正常,可是快速横向拖动几次后,部分任务条变成纯白色,文字和进度全部消失。点击一下该条才恢复,再过几秒又白掉。
原因出在 QGraphicsItem 的缓存重绘标记上。源码里为了实现性能优化,使用了 item 级别的缓存数据 m_cache,但更新缓存的信号只连到了任务的 start/end 变化通知。当视图做 scale 时,实际上任务条的外观没有变化,可纹理坐标却需要重新生成;如果缓存数据里包含屏幕坐标相关的渐变,缩放后直接 drawPixmap 就会得到大片白底。
解决方式:在 GanttView::zoomIn/zoomOut 完成后,对 scene 中所有 item 调用 update() 并强制它们的缓存重算。可以这么做:
void GanttView::doZoom() { QList<QGraphicsItem*> items = scene()->items(); for (QGraphicsItem *item : items) { if (TaskBarItem *bar = dynamic_cast<TaskBarItem*>(item)) { bar->setCacheDirty(true); bar->update(); } } }还有一种可能:这个“白”是 boundingRect 比实际上绘制矩形小导致裁掉。如果你的 boundingRect 没留边距,而画笔宽度默认 1 像素,paint 的时候右边缘和下边缘就被裁剪掉,看起来像一条没有边框的白色条。这个时候先放大 boundRect 试一下。
4.2 缩放后鼠标点不对位置,点击和拖动错位
现象:本来应该拖在周四的任务,点击后却选中周三,并且拖到周五会跳回周四。尤其是放大两倍以后,所有点选位置往左上角偏移。这是很典型的视图坐标与场景坐标混用问题。你在重写 mousePressEvent 时,如果直接用 event->pos() 去和任务条比较,整个都是错的。因为 event->pos() 是视图坐标,必须先映射到场景坐标再做命中测试。这一个版本的做法通常是:
void GanttView::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { QPointF scenePt = mapToScene(event->position().toPoint()); QGraphicsItem *item = scene()->itemAt(scenePt, QTransform()); // 把选中的 item 转成 TaskBarItem setSelectedItem(item); } QGraphicsView::mousePressEvent(event); }key 在这里就是 mapToScene。而且要特别注意,如果 scene 有 transform(比如 scene->setSceneRect 后又调用了 scale),itemAt 的第二个参数一定要用那个变换矩阵,否则 item 坐标会错乱。源码里给这个类加了个成员函数setSceneTransform(QTransform),在每次缩放时手动更新,这就不会错位了。用起来以后,凡是我自己新加交互,都一律从 scenePos 下手,不要再触碰 viewPos。
4.3 每次刷新数据都会全屏闪白,像是砸帧
现象:每隔几秒后台拉一次任务状态,调用 scene 刷新后,整个甘特图闪一下白屏,然后才恢复。任务少时还能凑合,任务多了闪得厉害,甚至出现闪烁后工具提示信息丢失。
这个问题本质上是每次数据更新,代码里做了一个暴力操作:清空 scene 再重新创建所有 item。这大概是最偷懒也是最多人写的方案。虽然 QGraphicsScene 清空几百个 item 不慢,但重建 item、再触发 paint、再重排列依赖线,整个往返需要和 GPU 同步,视口刷新就会感觉到瞬时的空白。解决的办法是“更新优先于删除重建”。源码里这一版其实也提供了一种更细的机制:每个 task 的数据保存在 model 里,item 只是 model 的影像。更新时只调用 item->update() 并修改时间坐标,不要 remove 整个 item。你可以在请求数据回来后写:
void GanttModel::updateTasks(const QList<TaskData> &where) { for (const TaskData &task : where) { GanttBarItem *bar = m_taskItemMap.value(task.id); if (bar) { bar->setStartTime(task.start); bar->setEndTime(task.end); bar->setProgress(task.progress); bar->update(); // 只重画自己 } else { // 只有新任务才创建 } } }同时关闭 view 的索引缓存避免 item 越清越多:view()->setItemIndexMethod(QGraphicsScene::NoIndex)。索引对于大量移动 item 时有加速作用,但内部会在清空重填时引起更大开销,数据刷新多的场景这里关掉反而更稳。
4.4 点击任务条没反应,拖拽排序根本排不动
现象:期望鼠标放到任务条上按住拖动,可以调整任务顺序,但是实际按的时候总是只能选中整条,拖不到第二行,有时还会把鼠标移动误判成滚轮拖手。排查了很久发现鼠标事件被父视图的 dragMode 吃掉了一部分。QGraphicsView::ScrollHandDrag 在空白区域是拖拽平移,但一旦鼠标点在 item 上,它仍然会先进入 item 的 mousePressEvent。问题是如果你的 task item 没有实现 flags(),默认 ItemIsSelectable 都够,可拖拽时还要加上 ItemIsMovable,否则事件会被视图拦截去滚动。针对这一条,常规做法是给 TaskBarItem 加上标志:
Qt::ItemFlags TaskBarItem::flags() const { return QGraphicsItem::ItemIsSelectable | QGraphicsItem::ItemIsFocusable | QGraphicsItem::ItemIsMovable; }而且还得把 GanttView 的 dragMode 改成 NoDrag,不然你会发现拖拽 item 的移动方向会被限制。真正做任务条跨行拖拽排序时,需要把这个 item 的 y 坐标在拖动结束后通过 scene 层去吸附到目标行的 y。源码版本里面没有自动吸附,但提供了一个坐标转换接口,在元对象属性里读 rowOf(item),也可以自己写:
void GanttView::dropEvent(QDropEvent *event) { QPointF endPos = mapToScene(event->position().toPoint()); TaskBarItem *bar = qgraphicsitem_cast<TaskBarItem*>(selectedItem); int row = scene()->yToRow(endPos.y()); bar->setRow(row); }这个 yToRow 函数是根据行高做整数除法的。注意 center 点在按下时冻结,否则每次 move 事件都会因为 item 位置变化产生抖动。实际处理中,我在 mouseReleaseEvent 后统一对齐一次,效果好很多。
5. 让它能干活:任务依赖、批量更新与万级数据的平滑体验
5.1 依赖线不随任务条移动自动更新
从这份源码的类结构看,依赖线是由一个独立的 QGraphicsPathItem 子类维护的,它监听任务条的移动信号。但默认实现只在任务条 setStartTime 的时候更新路径端点,如果你的业务里任务条允许在场景里拖动,那么依赖线不会自动跟上,会出现箭头插空。一个常见应对是连接 item 的 positionChanged 信号,或者重写 TaskBarItem::itemChange,在位置变化时更新它对应的依赖线。
如果不想侵入性这么强,也可以统一在 scene 层做一个updateDependencyLines()方法,拖动完成后去刷新全部 DependLine 的路径:
void GanttScene::updateDependencyLine(const TaskBarItem *from, const TaskBarItem *to) { QPointF startPt = QPointF(from->rect().right(), from->rect().center().y()); QPointF endPt = QPointF(to->rect().left(), to->rect().center().y()); // 简单折线,拐角水平切 30 像素 QPainterPath path(startPt); qreal midX = (startPt.x() + endPt.x()) / 2.0; path.lineTo(midX, startPt.y()); path.lineTo(midX, endPt.y()); path.lineTo(endPt); m_linesMap.value(from->taskId())->setPath(path); }注意如果用这种折线逻辑,在 startPt 在 endPt 右边(即依赖关系反了)时,要用 U 型绕线,否则线会直接穿到依赖条下面。源码里的 demo 任务都是正序排的,但真实排期常有下游先开始的情况,这一条非常值得自己加。
5.2 批量新增 500 个任务:分帧加 item,界面不冰住
所有 QGraphicsView 场景的大敌都是创建 item 时一次循环塞进上千个,这会让主线程停下来 layout 和 paint,卡出 2 到 3 秒。即使不卡到完全没响应,滚动时也会有明显的车辙感。常规做法是分帧刷新——利用 QTimer 每次 30 到 60 毫秒往 scene 里加若干个 item。源码包里没有把分帧写进去,但我推荐你按这个思路补:
void GanttScene::appendTasksChunked(const QList<TaskData> &tasks) { m_chunkList = tasks; m_chunkIndex = 0; m_chunkTimer = new QTimer(this); m_chunkTimer->setInterval(30); connect(m_chunkTimer, &QTimer::timeout, this, [this]() { int count = 0; while (m_chunkIndex < m_chunkList.size() && count < 40) { addTask(m_chunkList.at(m_chunkIndex++)); count++; } if (m_chunkIndex >= m_chunkList.size()) { m_chunkTimer->stop(); emit sceneReady(); } }); m_chunkTimer->start(); }每帧只做 40 个,对于常规机械硬盘的老设备,也不会出现明显的白屏。期间用户可以做滚动、点击,虽然还没有那 40 个 item 但不会整体阻塞。分帧时注意不要同时刷新依赖线,依赖线很多的话可以等全部任务生成后再拉一次 updateAllDependencyLines,这样曲线不乱。
5.3 万级任务的末期调优:取消阴影和启用 OpenGL
如果你真的要把这份源码推到一万级以上,还有两个推荐操作:第一个是把 TaskBarItem::paint 里的矩形阴影去掉。默认绘制为了好看给任务条加了 1 至 2 像素的阴影,单条的成本不高,但一万条之后,实测 shadow 的 QPainter 保存恢复状态也会占一点开销,不过量级很小,更关键是影响绘制缓存命中。阴影跟主题背景有关,背景一变阴影又要重新 cache。
第二个操作是把 viewport 切换成 OpenGL。很多老代码里只用 raster 结尾,正常滚动和缩放可以接受,但万级 item 加依赖线的情况下,可以用 QOpenGLWidget 获得更好的绘制吞吐:
auto *gl = new QOpenGLWidget(); view->setViewport(gl); view->setViewportUpdateMode(QGraphicsView::FullViewportUpdate);注意不要漏掉第三行。QGraphicsView 默认在 raster 情况下用部分更新,OpenGL 下如果还沿用部分更新,反而会出现混合不清的问题。开了 OpenGL 后如果发现文字模糊,那可能是字体平滑策略问题,建议把绘制文字的 section 设置分开走纹理,不开 GL 至少对于大多数桌面应用,raster 加 BoundingRectViewportUpdate 已经足够。
5.4 从那以后,我每次集成都需要先跑一个最小 Demo
我自己拿到这份源码后,并没有直接把它塞进主工程,而是先建了一个空 QMainWindow,把 scene、view、model 三件套连起来,用演示数据跑通,确认可以编译、滚动、缩放、拖动,然后才把主工程里的数据模型映射到 model 层上。这样做的好处是:当主工程报错、控件不显示时,你能立刻分清是源码的 bug 还是你数据的问题,而不是两个黑匣子叠一起。从那以后我每次集成画图类控件,都会强制在最小 Demo 里把缩放、拖动、更新三件事先跑通,再谈业务,已经是雷打不动的习惯。希望帮到你。
本文还有配套的精品资源,点击获取