
简介本资源是一份基于Qt框架实现甘特图的完整C源码工程面向具备基础Qt开发经验的中初级桌面应用开发者解决项目管理类软件中任务时间轴可视化与交互控制的核心需求。压缩包共12个文件5个头文件.h用于定义数据结构与对话框接口4个.cpp实现核心逻辑如任务增删、时间计算与图形渲染另含1个Qt工程配置.pro、1个说明性txt及1个.user用户配置总大小仅14KB轻量易集成。已有3109人学习下载反映出其在教学演示与小型项目快速落地中的实用价值。读者可直接编译运行掌握QGraphicsView场景绘图、QDateTime时间建模、信号槽驱动视图更新、拖放式任务编辑等关键技术点并复用模块化设计思路——如ActionStruct.h与Solving.h分别封装行为逻辑与调度算法为后续扩展资源依赖、关键路径分析等功能提供清晰架构基础。1. Qt 实现甘特图源码不是控件堆砌而是时间轴任务槽拖拽反馈的三重协同系统你试过用 Qt 写甘特图吗不是调个 QTableWidget 加几条线就叫甘特图——真正落地的工业级排程界面得让产线调度员一眼看清「A工单在C设备上从第3天8:00干到第5天14:30」还得支持鼠标拖动调整工期、按 Shift 多选后批量平移、右键弹出「插入空闲段」「拆分任务」「关联依赖箭头」等上下文操作。这份开源的 Qt 甘特图源码正是基于 QGraphicsView 构建的轻量级可嵌入组件不依赖第三方商业库如 Qt Charts 或第三方甘特控件纯 C/Qt5.15 实现含完整时间刻度缩放、任务条渲染、依赖关系连线、滚动同步、导出 PNG/PDF 等能力。它适合嵌入 MES 调度模块、实验室资源预约系统、项目管理 Demo 演示或作为 Qt 自定义视图开发的教学范本。如果你正卡在「QGraphicsItem 怎么响应拖拽又不卡顿」「如何让时间轴缩放时文字不糊」「任务条被遮挡后右键菜单定位偏移」这类具体问题上这份源码就是你该拆的第一份真实工程。2. 从零理解甘特图核心结构为什么必须用 QGraphicsView 而非 QWidget 堆叠甘特图本质是二维时空映射横轴为连续时间秒/分钟/小时/天纵轴为离散资源设备、人员、工位。当任务数超 50 条、时间跨度达 30 天、需支持 0.5x~4x 缩放时传统 QWidget QPainter 绘制会迅速陷入性能黑洞——每次 resize 或 scroll 都触发全量重绘CPU 占用飙升拖拽延迟明显。而 QGraphicsView 的场景-视图分离架构天然适配此类需求任务条、时间轴、网格线、依赖箭头全部作为独立 QGraphicsItem 存于 QGraphicsScene 中视图仅负责裁剪与变换GPU 加速合成滚动和缩放几乎无感。更重要的是它提供精确的坐标映射scenePos ↔ viewPos、事件拦截链mousePressEvent → mouseMoveEvent → mouseReleaseEvent 可逐层控制、Z 值层级管理确保时间轴在底层、任务条居中、菜单浮顶层这是 QWidget 手动 manage 子控件永远无法优雅解决的。2.1 时间轴模块动态刻度生成与像素对齐策略时间轴TimeAxis是整个甘特图的基准尺。源码中TimeAxisItem : public QGraphicsWidget封装了刻度逻辑关键不在“画多少条线”而在“何时重算刻度密度”。它不预设固定间隔如每小时一条而是根据当前视图宽度、最小像素间隔阈值默认 60px、时间跨度自动选择最优粒度// timeaxisitem.cpp 关键片段 void TimeAxisItem::updateScale() { const qreal viewWidth scene()-views().first()-viewport()-width(); const qint64 totalMs mEndTimeMS - mStartTimeMS; const qreal msPerPixel static_castqreal(totalMs) / viewWidth; // 根据像素密度选择刻度单位毫秒→秒→分钟→小时→天 if (msPerPixel 1000) { mTickUnit TickUnit::Millisecond; mTickStep 500; // 每500ms一条 } else if (msPerPixel 60000) { mTickUnit TickUnit::Second; mTickStep 30; // 每30秒一条 } else if (msPerPixel 3600000) { mTickUnit TickUnit::Minute; mTickStep 15; // 每15分钟一条 } else if (msPerPixel 86400000) { mTickUnit TickUnit::Hour; mTickStep 2; // 每2小时一条 } else { mTickUnit TickUnit::Day; mTickStep 1; // 每天一条 } update(); // 触发重绘 }提示mTickStep并非固定步长而是“显示密度控制阀”。例如当msPerPixel 50000约每像素50秒若强行每分钟画一条线会导致刻度文字严重重叠而设为每15分钟一条则文字间距 ≈ 750px清晰可读。此逻辑避免了“缩放后刻度糊成一片”的经典翻车现场。2.2 任务条渲染QGraphicsRectItem 的复用与状态管理每个任务对应一个GanttTaskItem : public QGraphicsRectItem。但源码并未为每个任务 new 一个 item——它采用对象池Object Pool模式缓存已创建的 task itemsetTaskData()方法复用实例而非重建极大减少内存分配。更关键的是任务条状态正常/选中/拖拽中/禁用通过setPen()和setBrush()动态切换且严格遵循 Qt 渲染规则// gantttaskitem.cpp void GanttTaskItem::updateAppearance() { switch (mState) { case Normal: setPen(QPen(Qt::darkGray, 1)); setBrush(QColor(173, 216, 230, 200)); // 浅蓝半透 break; case Selected: setPen(QPen(Qt::blue, 2, Qt::DashLine)); setBrush(QColor(135, 206, 250, 220)); // 亮蓝 break; case Dragging: setPen(QPen(Qt::red, 2, Qt::SolidLine)); setBrush(QColor(255, 105, 180, 180)); // 粉红高亮 break; case Disabled: setPen(QPen(Qt::gray, 1, Qt::DotLine)); setBrush(QColor(211, 211, 211, 150)); // 灰白 break; } update(); // 仅重绘自身区域非全屏 }注意update()调用范围精准到 item 边界矩形配合QGraphicsView::setViewportUpdateMode(QGraphicsView::BoundingRectViewportUpdate)确保拖拽时只有任务条区域刷新背景网格与时间轴完全不动帧率稳定在 60fps 以上。2.3 依赖关系连线贝塞尔曲线与锚点绑定机制甘特图中「任务B必须在任务A完成后启动」需可视化为带箭头的连线。源码用QGraphicsPathItem绘制三次贝塞尔曲线起点锚定在任务A右边界中点终点锚定在任务B左边界中点并随任务位置变化实时更新// dependencylineitem.cpp void DependencyLineItem::updatePath() { if (!mFromTask || !mToTask) return; // 获取任务条在 scene 坐标系中的位置 QRectF fromRect mFromTask-sceneBoundingRect(); QRectF toRect mToTask-sceneBoundingRect(); QPointF startPoint fromRect.topRight(); // A任务右上角 QPointF endPoint toRect.topLeft(); // B任务左上角 // 构造贝塞尔控制点水平偏移 垂直缓冲 QPointF ctrl1 startPoint QPointF(100, 0); QPointF ctrl2 endPoint QPointF(-100, 0); QPainterPath path; path.moveTo(startPoint); path.cubicTo(ctrl1, ctrl2, endPoint); setPath(path); // 箭头头用 addArrowHead() 方法绘制小三角形 }关键设计DependencyLineItem通过QGraphicsItem::ItemSendsGeometryChanges标志监听父项任务条位置变更itemChange(GraphicsItemChange change, const QVariant value)中捕获ItemPositionHasChanged事件立即调用updatePath()。这比轮询或信号槽更高效且避免了因任务快速拖拽导致连线瞬时错位的“黑匣子”现象。3. 快速集成到你的 Qt 项目三步完成嵌入与数据绑定这份源码以模块化方式组织无需修改主程序架构即可接入。核心是GanttView继承自QGraphicsView与GanttScene继承自QGraphicsScene两个类其余为辅助项时间轴、任务条、连线等。集成过程不涉及 moc 编译异常、UI 文件冲突或信号重复连接等常见坑。3.1 步骤一添加源文件并配置 CMakeLists.txt将下载包中src/gantt/目录下所有.h和.cpp文件复制到你的项目目录如myapp/src/gantt/然后在CMakeLists.txt中追加# myapp/CMakeLists.txt set(GANTT_SOURCES src/gantt/ganttview.cpp src/gantt/ganttscene.cpp src/gantt/timeaxisitem.cpp src/gantt/gantttaskitem.cpp src/gantt/dependencylineitem.cpp src/gantt/ganttexporter.cpp ) set(GANTT_HEADERS src/gantt/ganttview.h src/gantt/ganttscene.h src/gantt/timeaxisitem.h src/gantt/gantttaskitem.h src/gantt/dependencylineitem.h src/gantt/ganttexporter.h ) add_library(gantt STATIC ${GANTT_SOURCES} ${GANTT_HEADERS}) target_include_directories(gantt PUBLIC src/gantt) target_link_libraries(myapp PRIVATE gantt)参数说明gantt定义为静态库避免符号冲突PUBLIC src/gantt确保你的业务代码能#include ganttview.htarget_link_libraries将其链接进主应用。若用 qmake对应添加HEADERS $$PWD/src/gantt/*.h和SOURCES $$PWD/src/gantt/*.cpp。3.2 步骤二在 UI 中声明 GanttView 并初始化场景假设你在MainWindow中嵌入甘特图.h文件中声明成员// mainwindow.h #include gantt/ganttview.h class MainWindow : public QMainWindow { Q_OBJECT private: GanttView *m_ganttView; GanttScene *m_ganttScene; };.cpp中初始化推荐在setupUi()后// mainwindow.cpp void MainWindow::initGantt() { m_ganttScene new GanttScene(this); m_ganttView new GanttView(m_ganttScene, this); // 设置时间范围2024-01-01 00:00:00 到 2024-01-31 23:59:59 QDateTime start QDateTime::fromString(2024-01-01 00:00:00, yyyy-MM-dd hh:mm:ss); QDateTime end QDateTime::fromString(2024-01-31 23:59:59, yyyy-MM-dd hh:mm:ss); m_ganttScene-setTimeRange(start, end); // 添加3个资源设备 m_ganttScene-addResource(设备A); m_ganttScene-addResource(设备B); m_ganttScene-addResource(设备C); // 将视图放入 centralWidget 的布局假设已有 QVBoxLayout *mainLayout mainLayout-addWidget(m_ganttView); }逻辑说明GanttScene是数据容器GanttView是渲染窗口。setTimeRange()定义横轴总跨度addResource()注册纵轴资源返回资源索引int后续添加任务时需指定该索引。3.3 步骤三加载任务数据并触发渲染任务数据以结构体形式传入GanttTaskData包含必要字段struct GanttTaskData { int resourceId; // 对应 addResource() 返回的索引 QDateTime startTime; // 任务开始时间 QDateTime endTime; // 任务结束时间 QString title; // 任务标题显示在条上 QString description; // 悬停提示文本 bool isCritical; // 是否关键路径影响连线颜色 };批量添加示例// mainwindow.cpp void MainWindow::loadSampleTasks() { QListGanttTaskData tasks; tasks GanttTaskData{0, QDateTime::fromString(2024-01-05 08:00:00, yyyy-MM-dd hh:mm:ss), QDateTime::fromString(2024-01-07 16:00:00, yyyy-MM-dd hh:mm:ss), 工单#1001, A设备加工, false}; tasks GanttTaskData{1, QDateTime::fromString(2024-01-06 09:00:00, yyyy-MM-dd hh:mm:ss), QDateTime::fromString(2024-01-08 12:00:00, yyyy-MM-dd hh:mm:ss), 工单#1002, B设备检测, true}; // 关键路径 tasks GanttTaskData{2, QDateTime::fromString(2024-01-09 10:00:00, yyyy-MM-dd hh:mm:ss), QDateTime::fromString(2024-01-10 18:00:00, yyyy-MM-dd hh:mm:ss), 工单#1003, C设备装配, false}; m_ganttScene-addTasks(tasks); // 添加依赖工单#1002 必须在 #1001 之后开始 m_ganttScene-addDependency(0, 1); // 从索引0的任务指向索引1的任务 }参数说明addTasks()内部自动为每个任务创建GanttTaskItem并加入 sceneaddDependency(int fromIndex, int toIndex)根据任务在 scene 内部列表中的顺序索引建立连线。注意索引按addTasks()调用顺序递增非 resourceId4. 避坑指南五个血泪经验换来的高频报错与修复方案甘特图集成中最容易栽跟头的地方往往藏在 Qt 坐标系转换、事件拦截优先级、内存生命周期这些“看不见的角落”。以下是实测中反复出现的 5 类问题按「现象 → 原因 → 解决」给出可直接抄的方案。4.1 现象拖拽任务条时鼠标指针脱离任务条边缘松手后任务跳回原位原因GanttTaskItem::mouseMoveEvent()中未正确调用QGraphicsItem::mouseMoveEvent(event)导致QGraphicsView的默认拖拽逻辑被阻断QGraphicsItemGroup或QGraphicsScene无法感知位置变更。解决在自定义mouseMoveEvent结尾强制调用父类方法确保 Qt 底层坐标更新链完整void GanttTaskItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { // ... 自己的拖拽计算逻辑 if (m_isDragging) { // 更新任务数据中的 startTime/endTime m_taskData.startTime scenePosToDateTime(event-scenePos().x(), true); m_taskData.endTime scenePosToDateTime(event-scenePos().x() width(), false); } QGraphicsRectItem::mouseMoveEvent(event); // ← 关键必须调用 }4.2 现象缩放时间轴后刻度文字模糊、锯齿严重尤其在 1.5x 或 2.25x 缩放时原因QPainter默认使用Qt::SmoothTransformation但对小字号文本如 8pt 时间标签会过度插值导致发虚且未启用QPainter::Antialiasing对文字路径进行抗锯齿。解决在TimeAxisItem::paint()开头显式设置渲染提示void TimeAxisItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) { painter-setRenderHint(QPainter::Antialiasing, true); // 文字抗锯齿 painter-setRenderHint(QPainter::TextAntialiasing, true); // 文本专用抗锯齿 painter-setRenderHint(QPainter::SmoothPixmapTransform, false); // 关闭图片插值刻度线是矢量 // ... 后续绘制逻辑 }4.3 现象右键点击任务条弹出菜单但菜单位置总偏移 20px且随滚动条位置变化原因QGraphicsView::mapToGlobal()返回的是 viewport 坐标而QMenu::exec()需要屏幕绝对坐标当视图存在水平/垂直滚动时mapToGlobal()未补偿horizontalOffset()和verticalOffset()。解决手动计算滚动偏移并校正void GanttView::contextMenuEvent(QContextMenuEvent *event) { QPoint globalPos mapToGlobal(event-pos()); // 补偿滚动偏移 globalPos.rx() horizontalScrollBar()-value(); globalPos.ry() verticalScrollBar()-value(); QMenu menu; menu.addAction(编辑任务); menu.addAction(删除任务); menu.exec(globalPos); // ← 此时位置精准 }4.4 现象添加大量任务200 条后首次渲染极慢5sCPU 占用 100%原因GanttScene::addTasks()默认逐个addItem()每次 addItem 触发 scene 重排与视图更新且GanttTaskItem构造时默认调用update()造成 N 次无效重绘。解决批量添加前禁用 scene 更新添加后再统一刷新void GanttScene::addTasks(const QListGanttTaskData tasks) { setItemIndexMethod(QGraphicsScene::NoIndex); // 关闭索引加速查找 for (const auto task : tasks) { GanttTaskItem *item new GanttTaskItem(task); item-setFlag(QGraphicsItem::ItemIsMovable, true); item-setFlag(QGraphicsItem::ItemIsSelectable, true); addItem(item); // 此时不触发重绘 } setItemIndexMethod(QGraphicsScene::BspTreeIndex); // 恢复索引 views().first()-viewport()-update(); // 强制一次全量更新 }4.5 现象导出 PDF 时任务条颜色变浅、文字缩放失真且依赖连线消失原因QPainter导出 PDF 使用QPaintEngine::Pdf其对QLinearGradient笔刷支持不完整且QGraphicsView::render()默认使用QPainter::DrawChildren模式可能忽略QGraphicsPathItem依赖连线的绘制。解决导出时切换为QPainter::DrawEverything模式并用纯色笔刷替代渐变bool GanttExporter::exportToPdf(const QString filePath, const QRectF exportRect) { QPdfWriter writer(filePath); writer.setPageSize(QPagedPaintDevice::A4); QPainter painter(writer); // 关键启用 DrawEverything确保所有 item包括 path被绘制 m_ganttView-render(painter, QRectF(), exportRect, Qt::IgnoreAspectRatio); // 若仍有问题临时替换任务条笔刷为纯色 foreach (auto item, m_ganttScene-items()) { if (GanttTaskItem *task dynamic_castGanttTaskItem*(item)) { task-setBrush(QColor(173, 216, 230)); // 去掉 alpha 通道 } } return true; }5. 进阶技巧实现「跨天任务自动拆分」与「依赖冲突实时预警」甘特图真正的价值不在静态展示而在动态约束检查与智能辅助。源码预留了扩展钩子以下两个技巧可直接提升你的系统专业度任务自动拆分应对日历工作制依赖冲突预警避免计划硬伤。5.1 技巧一按工作日历自动拆分跨天任务支持节假日屏蔽实际排程中「任务耗时72小时」不等于「从周一8:00连续干到周三8:00」需跳过周末与法定假日。源码GanttScene提供splitTaskByCalendar()接口传入QCalendar实例与工作时段规则// 定义工作日历周一至周五 8:00-12:00, 13:00-17:00屏蔽 2024 国庆假期 QCalendar calendar; calendar.setWorkDays(Qt::Monday | Qt::Tuesday | Qt::Wednesday | Qt::Thursday | Qt::Friday); calendar.addWorkHours(QTime(8, 0), QTime(12, 0)); calendar.addWorkHours(QTime(13, 0), QTime(17, 0)); calendar.addHoliday(QDate(2024, 10, 1)); calendar.addHoliday(QDate(2024, 10, 2)); calendar.addHoliday(QDate(2024, 10, 3)); calendar.addHoliday(QDate(2024, 10, 4)); calendar.addHoliday(QDate(2024, 10, 5)); calendar.addHoliday(QDate(2024, 10, 6)); calendar.addHoliday(QDate(2024, 10, 7)); // 对任务进行拆分 GanttTaskData originalTask {...}; // 原始跨天任务 QListGanttTaskData splitTasks m_ganttScene-splitTaskByCalendar(originalTask, calendar); // 将拆分后的多段任务添加到场景 m_ganttScene-addTasks(splitTasks);内部逻辑splitTaskByCalendar()按工作时段切片每段生成独立GanttTaskData并自动设置isSplitPart true标志。渲染时同源拆分任务条用相同颜色但加细边框区分鼠标悬停显示「第1/3段」提示。5.2 技巧二依赖冲突实时预警红色闪烁气泡提示当用户拖拽任务导致「前置任务结束时间 后置任务开始时间」时系统应即时反馈。源码在GanttScene::checkDependencies()中实现 O(n²) 检查但为避免频繁触发采用防抖策略// ganttscene.h private slots: void onDragEndDebounced(); // 防抖槽函数 private: QTimer *m_dependencyCheckTimer; bool m_isCheckingDependencies; // ganttscene.cpp GanttScene::GanttScene(QObject *parent) : QGraphicsScene(parent) { m_dependencyCheckTimer new QTimer(this); m_dependencyCheckTimer-setSingleShot(true); m_dependencyCheckTimer-setInterval(300); // 300ms 防抖 connect(m_dependencyCheckTimer, QTimer::timeout, this, GanttScene::onDragEndDebounced); } void GanttScene::onDragEndDebounced() { if (m_isCheckingDependencies) return; m_isCheckingDependencies true; QListQPairint, int conflicts; // {fromIndex, toIndex} for (int i 0; i m_tasks.size(); i) { for (int j 0; j m_dependencies.size(); j) { const auto dep m_dependencies[j]; if (dep.fromIndex i dep.toIndex ! i) { const auto fromTask m_tasks[i]; const auto toTask m_tasks[dep.toIndex]; if (fromTask.endTime toTask.startTime) { conflicts.append({i, dep.toIndex}); } } } } if (!conflicts.isEmpty()) { // 触发红色闪烁动画 foreach (auto conflict, conflicts) { m_tasks[conflict.first]-startConflictAnimation(); m_tasks[conflict.second]-startConflictAnimation(); } // 显示气泡提示 showConflictTooltip(conflicts); } m_isCheckingDependencies false; }效果冲突任务条持续 2 秒红色脉冲QPropertyAnimation控制 opacity同时顶部弹出QToolTip“警告任务‘工单#1001’结束时间1月7日16:00晚于依赖任务‘工单#1002’开始时间1月6日09:00请调整”。5.3 一个必须养成的习惯每次修改时间轴范围后强制重置任务条 Z 值这是最容易被忽略的玄学坑。QGraphicsItem的 Z 值决定绘制顺序时间轴Z0、任务条Z1、依赖连线Z2、右键菜单Z3需严格分层。但当你调用setTimeRange()时TimeAxisItem会重新构建刻度项其 Z 值可能被重置为默认 0导致覆盖任务条。血泪经验只要动了时间轴立刻补一句m_ganttScene-setTimeRange(newStart, newEnd); // ← 紧接着强制重置所有 item 的 Z 值 foreach (QGraphicsItem *item, m_ganttScene-items()) { if (dynamic_castTimeAxisItem*(item)) item-setZValue(0); else if (dynamic_castGanttTaskItem*(item)) item-setZValue(1); else if (dynamic_castDependencyLineItem*(item)) item-setZValue(2); }从那以后我每次调setTimeRange()都强制走一遍这个 Z 值重置再没遇到过「时间轴盖住任务」的诡异现场。希望帮到你。本文还有配套的精品资源点击获取