资讯动态

Qt雷达图绘制源码:坐标变换、归一化与动态交互

发布时间:2026/10/14 3:26:29 来源:尧图企业网站定制
简介这份基于QT的雷达图绘制源代码面向需要在QT环境中实现自定义数据可视化的开发者适合学习图形视图框架中的视图与场景组件、自定义图元绘制及交互可帮助快速掌握自定义绘图流程。压缩包共8个文件包含cpp源文件、h头文件、pro工程与ui界面整体仅11KB便于导入开发环境运行已有1787人学习下载。源码未使用QT内置雷达函数自行实现极坐标与直角坐标转换、角度分配、轴长度计算等基于图形项类定制图元并用绘图器绘制闭合多边形信号槽机制支持按钮事件动态刷新。工程基于QT5.14.2与C11标准提供轴数、最大值、间隔等调整接口图形渲染流畅从界面构建到数学计算、交互联动均有完整演示便于复用与二次开发适合作为课程设计与毕业设计参考。1. 基于QT的雷达图绘制一份能直接跑的绘图源码做数据展示时多维指标的效果图最容易翻车的环节就是雷达图轴多、值杂、网格套网格。用 QPainter 一点点画坐标算错一两个像素整张图就歪了标签重叠更是常态。这份基于 QT 的雷达图绘制源代码把网格环、轴线、数据多边形、标签四层绘制逻辑封装成了一个独立控件给一组维度和一组数值就能出图支持颜色、刻度、叠加区域等参数动态调整还能把点击事件通过信号槽抛出去。适合 Qt 开发者做项目里的指标看板、能力评估模块也适合刚接触 QPainter 的人拿来做绘图入门参照——代码量不大但坐标系、数值归一化、重绘机制这些 Qt 绘图的关键知识点全在里面当开发语言的文档资料来翻也够用。新手照着改参数就能复现出自己项目里的雷达图熟手则可以把控件直接拖进现有工程改样式。2. 雷达图绘制原理与坐标变换从极坐标到屏幕坐标的关键映射2.1 QPainter 的绘图基础与坐标系先别急着抄代码把坐标系搞明白比什么都重要。QPainter 默认的坐标原点在窗口左上角x 轴向右y 轴向下这和数学里的直角坐标系正好相反。雷达图本质是极坐标系下的图形每个维度对应一条从中心向外辐射的轴线数值大小映射成半径。所以第一步是把极坐标角度、半径转换成 QPainter 能用的屏幕坐标x, y。常见做法是先把绘图表面坐标原点平移到控件中心相当于做一个坐标系平移。我一般不太喜欢改 QPainter 的世界矩阵而是写一个独立的角度转坐标函数把所有计算集中在函数里。后面加鼠标交互、加数据标签都复用同一个函数只改这一处血泪经验告诉我这样最不容易出乱子。绘制开始前还有一个必须做的事开抗锯齿。painter.setRenderHint(QPainter::Antialiasing, true);不开这行斜线边缘全是锯齿雷达图的轴线全是斜的观感会差一大截。这行代码的顺序也有讲究必须在任何 drawLine、drawPolygon 之前设置否则不生效。注意 QPainter 的渲染提示是绘制状态的一部分每条绘制路径的样式画笔颜色、宽度、填充都需要在 draw 调用前设置好这条规则在 Qt 里是硬性的跟记忆里其他绘图库先设置再绘制的流程一致。2.2 雷达图的角度模型与坐标换算雷达图有 N 个维度就有 N 条轴线相邻两条轴线之间的夹角是均匀的也就是 2π / N。关键的一个约定是第一条轴线从正上方开始也就是角度为 -π/2 的位置然后按顺时针方向依次排布。如果不做这个偏移第一条轴线会指向右侧 0 度方向图形整体跟着转 90 度看起来就躺下来了。坐标换算的核心函数长这样#include cmath QPointF RadarWidget::polarToCartesian(double angle, double radius) { double x centerX radius * std::cos(angle); double y centerY radius * std::sin(angle); return QPointF(x, y); } double RadarWidget::axisAngle(int index) { // 起始角度 -PI/2 保证第一条轴线指向正上方 return -M_PI / 2 index * 2 * M_PI / axisCount; }axisAngle 计算出第 index 条轴线对应的角度polarToCartesian 把角度和半径变成屏幕坐标。centerX、centerY 是控件中心的坐标radius 是当前点到中心的距离。注意 std::cos 和 std::sin 接收的是弧度而不是角度用角度制的人第一次写这函数十有八九会算出诡异结果——图形缩成一个点或者完全错位这种问题很难肉眼定位建议在函数入口写个调试日志输出几个关键坐标值配合 QPainter 的 drawPoint 把中心点和各轴顶点画出来一眼就能看出哪里歪了。2.3 数值归一化与半径映射原始业务数据往往量纲不同比如某项指标满分是 100另一项满分是 5不能直接用原始值去绘图得先归一化。源码里默认的做法是每个维度的值除以该维度的最大值得到一个 0 到 1 之间的比例再乘以雷达图的外接圆半径。double normalized values[axis] / maxValues[axis]; double radius normalized * chartRadius;这个逻辑本身很简单但有两个坑。第一个坑是 maxValue 传 0 的问题除以 0 在浮点运算里得到 inf画出来是乱线必须在 setData 入口做合法性检查任何维度最大值不大于 0 时直接拒绝更新数据并给出提示。第二个坑是不同维度最大值不一致的处理应该按维度分别归一化而不是统一用全局最大值否则数值小的维度在图里永远贴着内圈看不出差异。这里还有一层容易被忽略的语义归一化只是把数据压到 0 到 1 区间如果某维度数据超过预设最大值radius 会大于 chartRadius图形直接冲出网格外。所以最大值参数要按业务上限留出 10% 到 20% 余量别把满分值卡得太死。3. 源码结构与核心绘制流程QPainter 画网格、轴线与数据多边形3.1 源码包的文件构成这套源码的核心是一个继承自 QWidget 的 RadarWidget 控件类文件组织如下文件作用RadarWidget.h控件类声明、公开参数接口、信号定义RadarWidget.cpp绘制逻辑实现、事件重写、参数变更处理main.cppQt 入口创建窗口并装配控件RadarChart.proqmake 工程配置依赖 Qt Widgets 模块RadarWidget.h 里暴露的接口集中在几个 set 方法和一个 setData 方法。data 存的是维度名称列表和对应数值列表。源码把数据源和绘制逻辑解耦了外部只需要调用 setData 把最新数据传进去控件内部自动触发 update 请求重绘不需要外部手动调 repaint。这个设计对后面接实时数据流很重要因为 paintEvent 由 Qt 事件循环调度连续多次 setData 会被合并成一次重绘避免了高频刷新下的闪烁。3.2 网格环与轴线的绘制顺序绘制顺序决定图层关系。雷达图从底层到顶层依次是背景、网格环、轴线、数据多边形、标签。顺序反了会互相遮挡比如先画多边形再画网格网格线就会压在多边形上面看着不干净。这层顺序在源码里是写在 paintEvent 里的五个独立方法调用每个方法职责单一改动某个环节不影响其他环节。网格环不是圆环是正多边形环。雷达图每个维度对应一个顶点同一半径上的相邻顶点用直线连接起来形成一圈一圈的多边形网格。画 N 层网格就循环 N 次每层半径从内到外递增void RadarWidget::drawGrid(QPainter painter) { painter.setPen(QPen(gridColor, 1)); for (int ring 1; ring gridLevels; ring) { double ringRadius chartRadius * ring / gridLevels; QPolygonF ringPolygon; for (int axis 0; axis axisCount; axis) { double angle axisAngle(axis); ringPolygon polarToCartesian(angle, ringRadius); } painter.drawPolygon(ringPolygon); } }外层循环控制网格层级内层循环按轴数收集顶点最后 drawPolygon 一次闭合。QPolygonF 追加顶点用 运算符这是 Qt 容器惯用法比先 resize 再赋值更直观。网格层数 gridLevels 一般取 4 或 5太多了图显得密太少了看不出刻度关系。如果网格环想画成虚线风格改用 QPen(Qt::DashLine) 即可但注意虚线在斜线上的间距不均匀这是 QPainter 的固有行为不做处理的话视觉上会有轻微抖动感。轴线是从中心到最外圈半径的直线循环 axisCount 次每条线从中心点到该轴方向的最外圈坐标drawLine 即可。最后一根轴和最外圈网格的顶点正好重合没必要重复计算坐标可以直接复用 2.2 节里的 polarToCartesian 函数。3.3 数据多边形与标签绘制数据多边形是所有维度归一化半径对应的顶点连成的封闭多边形是雷达图的核心视觉元素void RadarWidget::drawDataPolygon(QPainter painter, const QVectordouble values) { QPolygonF dataPolygon; for (int axis 0; axis axisCount; axis) { double radius values[axis] / maxValues[axis] * chartRadius; dataPolygon polarToCartesian(axisAngle(axis), radius); } painter.setBrush(dataBrush); painter.setPen(QPen(dataPenColor, 2)); painter.drawPolygon(dataPolygon); }每个轴的数据点位置由该维度的归一化半径决定连起来是一条折线闭环。填充色和描边色分开设置有填充时视觉上更像面积图只描边则更清爽。画多组数据时不同组的多边形叠加区域会互相遮盖建议每组用不同的半透明填充色这样重叠部分能透出下层颜色对比效果更好。填充色的 alpha 值保持在 60 到 100 之间比较合适我后面在第四章单独展开。标签绘制相对麻烦因为文字有宽高直接把坐标放在轴线外沿会被网格盖住或超出边界。常见做法是取轴线的外圈坐标作为锚点再用 QFontMetrics 测量文字尺寸做偏移修正QFontMetrics fm(painter.font()); int textWidth fm.horizontalAdvance(label); int textHeight fm.height(); QPointF anchor polarToCartesian(axisAngle(axis), chartRadius labelPadding); // 上半区文字居中下半区文字居左避免超出控件边界标签方向也有讲究左侧几条轴的标签放在锚点右侧避免文字向左延伸超出控件范围右侧的则放在锚点左侧正上方和正下方的标签直接居中。这套偏移逻辑看起来琐碎却是标签不越界、不重叠的关键。维度名称较长时建议在 setData 里做截断处理超过 8 个字符就用省略号别指望绘图层能自动换行QPainter 没有自动换行的概念。4. 参数配置与样式定制颜色、刻度、叠加区域的调整方法4.1 核心参数一览源码把可调参数都做成了公开属性直接 set 就行不需要改绘制代码。我整理了最常用的一组参数类型说明建议值axisCountint轴线数量 / 维度数量3 ~ 12gridLevelsint网格环层数4 ~ 5chartRadiusdouble雷达图外接圆半径控件短边一半减 40maxValuesQVectordouble各维度最大值按业务量纲分别设置gridColorQColor网格环与轴线颜色浅灰 #CCCCCCdataBrushQBrush数据多边形填充半透明蓝dataPenColorQColor数据多边形描边深蓝labelPaddingint标签与图形间距15 ~ 25axisCount 小于 3 时雷达图退化成直线或三角形语义上不再成立源码里建议加个断言拦截。chartRadius 不要在构造时写死应该在 resizeEvent 里根据控件实际大小重算否则窗口拉大图不变四周留大片空白窗口变小又会被裁掉。maxValues 是 QVector每个维度独立设置不要图省事统一用一个值前面 2.3 节说过原因。4.2 样式定制从控件背景到刻度值默认样式是灰网格加深蓝数据适合多数通用场景。业务项目里通常要改配色源码支持给每个部件单独设置颜色。我一般习惯把背景设为浅色甚至透明让雷达图嵌进现有页面的配色体系而不是自带一块白色底板。设置背景色有两条路一是重写 paintEvent 里先 fillRect 再画图二是调用 setAttribute(Qt::WA_TranslucentBackground) 后用 QPainter 画透明背景。第二条路适合做半透明浮层配合 QGraphicsOpacityEffect 能做出整体淡入效果但注意设置了透明度属性后某些系统上绘制内容会丢失需要及时 update 触发重绘这属于平台相关行为跨平台交付时要实测。刻度值的显示是另一个隐蔽细节网格环画了但默认不标数值看起来只是个几何图形。补数值的做法是在每条轴线上等距放置刻度文字位置按 gridLevels 均分。注意刻度文字不要每层都画5 层网格乘 8 条轴就是 40 个数字全是噪点。我一般只标最外层数值或者只标内外两层中间层用点标记代替文字。4.3 多组数据与区域叠加雷达图一个常见需求是同时展示多组数据做对比比如两个季度的指标对比、不同方案的能力评估。源码的 setData 接口设计成一次传一组多组场景就要扩展成传二维数组或者连续调用多次绘制函数。我实测中更推荐后者绘制函数接收一个 values 列表和一个对应的样式结构体循环里换画笔和填充即可。多组数据绘制时填充色的透明度是重点。完全实心的两个多边形叠在一起后面的直接被盖住对比无从谈起。建议用带 alpha 通道的 QColorQColor fillColor QColor(41, 128, 185, 80); // alpha 通道 80 QBrush brush(fillColor); painter.setBrush(brush);alpha 值 60 到 100 之间比较合适太低看不清色块太高失去叠加语义。叠加区域的显示顺序也有讲究先画数值较大但需要完整展示的一组再画数值较小的这样第二组的完整轮廓压在上面对比更直观。如果你希望强调的是较大的一组反过来即可。这套逻辑和 GIS 里图层叠加的思维一致做视觉层次时先想清楚哪一层是信息主体。图例放在雷达图右下角即可用一个简单 QRectF 区域画色块和文字不需要引入重量级组件。图例位置要避开轴线最密集的区域否则标签和图例会打架。5. 常见问题与避坑排查坐标偏移、抗锯齿与字体乱码这一章是踩坑记录每一条都对应一个真实翻车现场按现象 → 原因 → 解决展开。5.1 图形整体偏移不在控件中心现象雷达图画出来整体偏向左下角或右上角网格环明明是对称的但中心不在控件几何中心。原因centerX、centerY 用的是初始化时的固定值而控件实际渲染区域可能被布局、边距影响更常见的原因是忘记在 resizeEvent 里更新中心点和半径窗口尺寸变化后旧的中心坐标残留图形就偏了。解决在 resizeEvent 里重新计算 centerX、centerY 和 chartRadius 并调用 update()。初始化时也主动触发一次几何计算把 center 的计算放进一个私有方法里paintEvent 和 resizeEvent 都调用它。从那以后我每次写绘图控件都强制把几何计算和绘制分离几何只在一处更新。5.2 第一条轴线不在正上方现象雷达图整体旋转了 90 度第一条轴线指向正右方看起来像躺倒了。原因axisAngle 函数没有做 -π/2 的起始角度偏移直接用了 index * 2π / N导致 0 度角指向 x 轴正方向也就是右侧。解决角度基准加上 -M_PI / 2。如果后续产品需求要支持角度微调把起始角度设计成可配置参数默认 -π/2预留一个 setStartAngle 接口。注意调整起始角度后标签偏移逻辑也要跟着改否则标签的左右判定会错位。5.3 线条有锯齿斜线边缘发虚现象网格环和轴线的斜边呈阶梯状把窗口放大后更明显整体显得粗糙。原因没有开启 QPainter::Antialiasing 渲染提示。抗锯齿是 QPainter 最容易被忽略的开关Qt 默认关闭只有显式设置才生效。解决在 paintEvent 最开始设置 painter.setRenderHint(QPainter::Antialiasing, true)。如果画布上同时有图形和文字文字抗锯齿可以单独用 QPainter::TextAntialiasing 控制避免图形抗锯齿导致文字边缘发糊。这两种抗锯齿的代价都是性能下降静态图无所谓高频刷新时要评估。5.4 中文标签乱码现象维度名称是中文时图上显示成方块或问号英文正常。原因多发生在旧版 Qt 或运行环境缺少中文字体时默认字体族不支持中文字形还有一种情况是源文件编码不是 UTF-8字符串在编译阶段就坏了跟运行环境无关。解决在构造时指定字体族为 Microsoft YaHei 或 WenQuanYi Micro Hei并用 QFontDatabase 检查字体是否存在不存在时回退到系统默认。代码文件统一保存为 UTF-8.pro 文件里加一行兜底配置防止跨平台编译时编码错乱。这套组合拳基本能把乱码问题堵死。5.5 窗口缩放后图形变形或被截断现象拖动窗口变大图还是原来尺寸四周留白窗口变小图形边缘被裁掉。原因chartRadius 是构造时写死的常量没有跟随控件尺寸变化。雷达图本质是圆形布局半径必须取控件短边的一半再减去标签边距写死必出问题。解决在 resizeEvent 里用 qMin(width, height) 计算半径基准减去固定 padding一般 30 到 50 像素同时更新中心坐标并 update()。resizeEvent 里不要做重量级操作几何运算就几行开销可以忽略。如果控件放在布局里还要注意布局可能触发多次 resizeEvent幂等写法能避免重复计算带来的抖动。6. 进阶鼠标交互与动态数据刷新技巧静态雷达图做完后业务上最想要的是点某个维度看具体数值。实现思路是重写 mousePressEvent把鼠标位置和每个轴顶点坐标比较距离小于阈值就判定命中void RadarWidget::mousePressEvent(QMouseEvent* event) { QPointF pos event-position(); // Qt 6 写法Qt 5 用 event-pos() double minDist 20.0; int hitAxis -1; for (int axis 0; axis axisCount; axis) { QPointF point polarToCartesian(axisAngle(axis), dataRadius(axis)); double dist QLineF(pos, point).length(); if (dist minDist) { minDist dist; hitAxis axis; } } if (hitAxis 0) emit axisClicked(hitAxis, axisNames[hitAxis], dataValues[hitAxis]); QWidget::mousePressEvent(event); }命中阈值 20 像素是参考触屏容错范围定的鼠标操作可以收紧到 10。dataRadius 要取该维度当前数据的归一化半径而不是最外圈半径否则点的是数据点位置提示的却是外圈刻度值会误导用户。命中的反馈我一般会画一个小圆圈标注高亮状态存成员变量触发 update 重绘即可。实时监控场景下雷达图数据每秒变化比如系统负载的五维指标。做法是 QTimer 定时灌新数据控件内部只做 setData 加 update不涉及线程QTimer* timer new QTimer(this); connect(timer, QTimer::timeout, this, []() { QVectordouble newValues fetchLatestMetrics(); radarWidget-setData(dimNames, newValues); }); timer-start(1000);刷新间隔 1000 毫秒是视觉上比较舒服的节奏太频繁画面跳动太慢失去实时性。setData 内部只做数据拷贝和合法性检查重绘交给 Qt 事件循环调度不会阻塞 UI。如果数据源在别的线程记得用信号槽连接替代直接调用跨线程直接调成员函数是高危操作轻则数据错乱重则崩溃。还有一个优化细节动态刷新时网格和轴线其实没变化理论上可以画到 QPixmap 缓存里每次刷新只重绘数据多边形和标签。我在一个数据刷新较快的监控项目里实测过缓存网格后 CPU 占用明显下降。源码当前是全量重绘想优化性能可以直接拆把 paintEvent 里的网格部分抽到离屏 QPixmap只有 axisCount 或 gridLevels 变化时才重建缓存数据变化只叠加数据多边形和标签。从那以后我每次写完绘图控件都会强制走一遍缩放窗口、改最大值、切中文字体这三个测试这几步能暴露八成以上的坐标和渲染问题。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑