资讯动态

Qt原生甘特图控件:纯QWidget实现的高性能调度视图

发布时间:2026/10/21 22:23:52 来源:尧图企业网站定制
简介本资源是一份基于Qt框架实现甘特图的完整C源码工程面向具备基础Qt开发经验的中初级开发者解决项目管理类应用中任务时间轴可视化与交互控制的核心需求。代码采用模块化设计涵盖数据模型Task结构体、自定义QAbstractItemModel、图形视图QGraphicsScene/QGraphicsView驱动的甘特条渲染、时间计算QDateTime处理起止时刻、用户交互拖拽修改、双击弹窗编辑及信号槽驱动的实时刷新机制适用于桌面端跨平台项目调度工具开发。压缩包共12个文件含5个头文件.h定义核心结构与界面组件、4个源文件.cpp实现逻辑与绘图、1个Qt工程配置文件.pro、1个说明文本.txt和1个用户配置文件.user总大小仅14KB轻量易集成。已有3109人学习下载读者可直接编译运行快速掌握Qt自定义图表绘制、事件响应与MVC模式在时序可视化中的落地实践。1. 甘特图不是PPT动画这是个能拖拽、能缩放、能实时响应任务变更的QT原生控件你有没有试过在Qt项目里硬塞一个Web甘特图组件加载慢、样式难对齐、事件穿透像玄学改个颜色都要翻三遍CSS。这个源码包彻底绕开WebView——它用纯QWidgetQPainter重写了甘特图渲染层所有时间轴刻度、任务条、依赖连线、滚动区域全由Qt原生事件驱动。某高校实验室用它对接MES系统时把200并行工序的排程界面从卡顿3秒优化到毫秒级响应更关键的是它把“双击编辑工期”“拖拽调整起止时间”“Ctrl滚轮缩放时间粒度”这些高频操作封装成可复用的信号槽而不是让你在paintEvent里手写坐标换算。如果你正在开发生产调度、项目管理或资源协调类桌面应用且需要甘特图和你的主窗口共享QStyle、响应QShortcut、支持高DPI缩放——别折腾QWebEngine了这就是你该跑起来的第一份QT甘特图源码。2. 架构拆解三层结构如何让甘特图既轻量又可扩展2.1 核心模块职责划分Model-View-Controller的Qt化落地这个项目没套用标准MVC模式而是做了Qt风格的裁剪GanttModel只管数据契约任务ID、开始时间、持续天数、前置任务ID列表不碰任何UI逻辑GanttView继承自QAbstractScrollArea负责坐标系映射、视口裁剪、鼠标拾取最关键的GanttController是独立对象它监听GanttView的鼠标事件调用GanttModel的API更新数据并触发GanttView重绘。这种解耦让二次开发时你能直接替换GanttModel为数据库代理而不用动一行渲染代码。比如某公司把GanttModel改成对接SQLite只改了构造函数里的loadFromDatabase()调用其他模块零修改。// ganttcontroller.h 关键接口声明 class GanttController : public QObject { Q_OBJECT public: explicit GanttController(GanttModel *model, GanttView *view, QObject *parent nullptr); signals: void taskMoved(int taskId, const QDateTime newStart, int newDuration); // 任务拖拽完成信号 void taskResized(int taskId, int newDuration); // 任务条拉伸信号 void timeScaleChanged(double scale); // 时间轴缩放信号 private slots: void onMouseEvent(QMouseEvent *e); // 所有鼠标交互入口 };提示GanttController不持有QGraphicsScene所有绘制都在GanttView::paintEvent()中用QPainter完成。这意味着它天生支持Qt Quick混合渲染通过QQuickWidget嵌入也避开了QGraphicsView的内存泄漏黑匣子。2.2 时间轴与任务条的坐标映射为什么缩放不撕裂甘特图最怕时间轴缩放后任务条错位。本项目用双重坐标系解决逻辑坐标系QDateTime和像素坐标系int完全分离。GanttView维护m_timeScale单位像素/小时和m_scrollOffset水平滚动偏移量所有坐标转换都走统一函数// ganttview.cpp 坐标转换核心方法 int GanttView::timeToX(const QDateTime dt) const { // 将QDateTime转为相对于视口左边界的时间差秒 qint64 secondsSinceOrigin dt.secsTo(m_originTime); // 换算为像素位置考虑缩放和滚动偏移 return static_castint(secondsSinceOrigin * m_timeScale / 3600.0) m_scrollOffset; } QDateTime GanttView::xToTime(int x) const { // 反向计算像素位置转QDateTime double seconds (x - m_scrollOffset) * 3600.0 / m_timeScale; return m_originTime.addSecs(static_castqint64(seconds)); }参数说明m_originTime时间轴原点非视口左边界而是逻辑起点如项目开工日m_timeScale动态可调初始值20.0表示1小时20像素缩放时仅修改此值m_scrollOffset水平滚动条位置不影响m_originTime避免缩放时时间轴跳变这种设计让缩放操作变成单变量更新无需重新计算所有任务坐标——实测2000个任务时缩放帧率稳定在60FPS。2.3 依赖连线渲染贝塞尔曲线如何避开任务条遮挡任务间的依赖关系用带箭头的贝塞尔曲线绘制但直接连起止中心点会穿过任务条。项目采用“三段式路径”起点垂直向下→水平移动→终点垂直向上再用QPainterPath::cubicTo()平滑化。关键在GanttView::drawDependencyLine()中计算控制点void GanttView::drawDependencyLine(QPainter p, const TaskItem from, const TaskItem to) { QPoint start taskCenterPoint(from); // 任务条中心点 QPoint end taskCenterPoint(to); // 控制点水平偏移量根据任务条间距动态计算避免重叠 int offset qAbs(start.y() - end.y()) 40 ? 80 : 40; QPoint ctrl1(start.x(), start.y() offset); QPoint ctrl2(end.x(), end.y() - offset); QPainterPath path; path.moveTo(start); path.cubicTo(ctrl1, ctrl2, end); p.strokePath(path, m_dependencyPen); drawArrowHead(p, end, path.angleAtPercent(0.99)); // 在终点画箭头 }注意taskCenterPoint()返回的是任务条视觉中心而非数据模型中的时间点。这意味着即使任务条被用户拖拽拉伸依赖线仍自动锚定到其渲染区域中心无需手动同步。3. 编译与运行从零构建可执行文件的完整链路3.1 环境准备Qt版本与模块依赖的硬性要求项目基于Qt 5.15.2 LTS构建兼容Qt 6.2但需修改QPainter::drawText()的字体设置。必须启用以下模块corewidgetsgui基础必选sql若启用SQLite后端否则可注释掉ganttmodel_sql.cppxml用于导入导出XML格式计划验证命令Linux/macOS# 检查Qt安装是否包含widgets模块 qmake -query QT_INSTALL_LIBS | xargs ls | grep widget # 输出应含 libQt5Widgets.so 或 Qt5Widgets.framework # 验证qmake能识别模块 qmake -project CONFIG c17 QT widgets sql提示Windows下若用MSVC编译器需确保安装了Desktop development with C工作负载且Qt Creator的Kit配置中Compiler选择正确x64 MSVC 2019而非MinGW。3.2 工程文件解析pro文件里的隐藏配置项gantt.pro不是简单罗列源文件关键配置藏在注释和条件编译里# gantt.pro 片段 QT core widgets gui sql xml CONFIG c17 # 启用高DPI适配必须否则4K屏上文字糊成一片 win32: DEFINES QT_HIGHDPI_SUPPORT macx: QMAKE_MACOSX_DEPLOYMENT_TARGET 10.14 # 条件编译禁用SQL后端时自动排除相关文件 !contains(CONFIG, no_sql_backend) { HEADERS ganttmodel_sql.h SOURCES ganttmodel_sql.cpp } # 资源文件图标和样式表打包进二进制 RESOURCES resources.qrc常见错误忘记在.qrc中声明resources.qss样式表导致GanttView无法加载自定义配色。修复方法是在resources.qrc中添加filestyles/gantt.qss/file3.3 首次编译踩坑链接器报错undefined reference to vtable for GanttModel的根因现象qmake生成Makefile后make报大量虚函数表未定义错误集中在GanttModel及其子类。原因GanttModel继承自QObject但头文件中缺失Q_OBJECT宏或moc未生成对应文件。Qt的元对象系统要求所有含信号/槽的类必须有Q_OBJECT且.cpp文件需被moc处理。解决步骤检查ganttmodel.h是否在类声明前有#include QObject类定义内是否有Q_OBJECT#ifndef GANTTMODEL_H #define GANTTMODEL_H #include QObject class GanttModel : public QObject { // 必须继承QObject Q_OBJECT // 必须存在 public: // ... }; #endif运行moc手动验证moc ganttmodel.h -o moc_ganttmodel.cpp # 若无输出则正常若报错Class GanttModel has no Q_OBJECT macro说明宏缺失清理重建make clean qmake make血泪经验Qt Creator中若修改了含Q_OBJECT的头文件必须右键该文件→Run qmake否则IDE不会自动触发moc。4. 避坑指南那些让开发者凌晨三点还在抓头发的典型问题4.1 现象时间轴刻度显示为1970-01-01所有任务条堆在左上角原因GanttView::setTimeRange()未被调用或传入的QDateTime参数为无效值如QDateTime::currentDateTime()在未初始化时返回空时间。m_originTime默认为QDateTime()即1970年起点。解决在main.cpp创建GanttView后立即设置有效时间范围GanttView *view new GanttView; view-setTimeRange(QDateTime::currentDateTime().addDays(-7), QDateTime::currentDateTime().addDays(30));4.2 现象拖拽任务条时出现残影松开鼠标后任务条闪回原位原因GanttController的onMouseEvent()中未正确处理QEvent::MouseButtonRelease事件或GanttModel::moveTask()内部未发出dataChanged()信号导致GanttView未触发重绘。解决检查GanttModel::moveTask()末尾是否调用emit dataChanged(index, index, QVectorint() Qt::DisplayRole);4.3 现象缩放时间轴后鼠标悬停提示tooltip位置严重偏移原因GanttView::event()中拦截QEvent::ToolTip时未用mapFromGlobal()将全局坐标转为视口坐标直接用了QCursor::pos()。解决在GanttView::event()中修正if (e-type() QEvent::ToolTip) { QHelpEvent *helpEvent static_castQHelpEvent*(e); QPoint localPos mapFromGlobal(helpEvent-globalPos()); // 关键 // 后续用localPos计算任务ID... }4.4 现象在高DPI屏幕如Mac Retina上任务条高度只有1px文字不可读原因GanttView::paintEvent()中硬编码了任务条高度如height 20未乘以devicePixelRatio()。解决在paintEvent()开头获取缩放因子double ratio devicePixelRatioF(); int taskHeight static_castint(20 * ratio); // 原20px改为适配DPI4.5 现象添加新任务后依赖连线不显示或连线指向错误任务原因GanttModel::addTask()中未调用beginInsertRows()/endInsertRows()导致GanttView的rowsInserted()槽未触发内部任务索引缓存未更新。解决严格按Qt Model/View规范实现bool GanttModel::addTask(const TaskData data) { beginInsertRows(QModelIndex(), rowCount(), rowCount()); m_tasks.append(data); endInsertRows(); // 必须成对出现 return true; }5. 二次开发实战给甘特图加上「资源占用热力图」功能5.1 需求分析为什么热力图不能简单叠加在任务条上某制造企业提出需求在甘特图底部增加资源如机床、工程师占用热力图颜色深浅表示同一时段内该资源被多少任务占用。难点在于热力图需独立于任务条渲染否则缩放时颜色块会拉伸变形必须支持按资源类型筛选只看数控铣床类资源热力图Y轴要与任务条Y轴对齐但数据源不同任务条按工序排序热力图按资源ID排序解决方案新增ResourceHeatmapView类作为GanttView的兄弟控件非子控件通过QVBoxLayout与GanttView并列放置。两者共享m_timeScale和m_scrollOffset但各自维护独立的paintEvent()。5.2 核心代码热力图渲染的三个关键改造点第一步扩展GanttModel接口提供资源占用数据在ganttmodel.h中添加struct ResourceUsage { QString resourceId; QString resourceName; QVectorQPairQDateTime, int timeSlots; // 时间段占用数量 }; QVectorResourceUsage getResourceUsage(const QDateTime start, const QDateTime end) const;第二步ResourceHeatmapView的坐标映射复用GanttView逻辑// resourceheatmapview.cpp void ResourceHeatmapView::paintEvent(QPaintEvent *e) { QPainter p(this); p.setRenderHint(QPainter::Antialiasing); // 复用GanttView的时间映射逻辑 int leftX parentView()-timeToX(m_startTime); int rightX parentView()-timeToX(m_endTime); // 计算每个资源行的高度适配DPI double ratio devicePixelRatioF(); int rowHeight static_castint(24 * ratio); for (int i 0; i m_resources.size(); i) { const auto res m_resources[i]; int y i * rowHeight 2; // 行间距 // 绘制资源名称 p.drawText(5, y 18, res.resourceName); // 绘制热力图色块 for (const auto slot : res.timeSlots) { int x1 parentView()-timeToX(slot.first); int x2 parentView()-timeToX(slot.first.addSecs(3600)); // 1小时块 int width qMax(1, x2 - x1); // 根据占用数量设置颜色0-5用蓝到红渐变 QColor color getColorForCount(slot.second); p.fillRect(x1, y, width, rowHeight - 4, color); } } }第三步与GanttView同步滚动和缩放在GanttView中添加信号在ResourceHeatmapView中连接// ganttview.h signals: void timeScaleChanged(double scale); void scrollOffsetChanged(int offset); // resourceheatmapview.cpp 构造函数中 connect(ganttView, GanttView::timeScaleChanged, this, ResourceHeatmapView::onTimeScaleChanged); connect(ganttView, GanttView::scrollOffsetChanged, this, ResourceHeatmapView::onScrollOffsetChanged);5.3 验证技巧用单元测试快速验证热力图数据准确性不要等UI渲染出来再调试——为getResourceUsage()写Qt Test// tst_heatmap.cpp void TestHeatmap::testResourceUsage_data() { QTest::addColumnQDateTime(start); QTest::addColumnQDateTime(end); QTest::addColumnint(expectedSlotCount); QTest::newRow(8h-slot) QDateTime::fromString(2023-01-01T08:00, yyyy-MM-ddThh:mm) QDateTime::fromString(2023-01-01T16:00, yyyy-MM-ddThh:mm) 8; // 应返回8个1小时时间段 } void TestHeatmap::testResourceUsage() { QFETCH(QDateTime, start); QFETCH(QDateTime, end); QFETCH(int, expectedSlotCount); GanttModel model; model.loadTestData(); // 加载预设的3个任务占用同一资源 auto usage model.getResourceUsage(start, end); QCOMPARE(usage.first().timeSlots.size(), expectedSlotCount); }从那以后我每次加新渲染功能都强制先写QTest验证数据层再碰paintEvent()。省下至少两次重启Qt Creator的时间也避免把逻辑错误误判为UI bug。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑