简介:这份基于QT的雷达图绘制源代码,面向需要在QT环境中实现自定义数据可视化的开发者,适合学习图形视图框架中的视图与场景组件、自定义图元绘制及交互,可帮助快速掌握自定义绘图流程。压缩包共8个文件,包含cpp源文件、h头文件、pro工程与ui界面,整体仅11KB,便于导入开发环境运行;已有1787人学习下载。源码未使用QT内置雷达函数,自行实现极坐标与直角坐标转换、角度分配、轴长度计算等,基于图形项类定制图元,并用绘图器绘制闭合多边形,信号槽机制支持按钮事件动态刷新。工程基于QT5.14.2与C++11标准,提供轴数、最大值、间隔等调整接口,图形渲染流畅;从界面构建到数学计算、交互联动均有完整演示,便于复用与二次开发,适合作为课程设计与毕业设计参考。
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.cpp | Qt 入口,创建窗口并装配控件 |
| RadarChart.pro | qmake 工程配置,依赖 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 QVector<double>& 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 就行,不需要改绘制代码。我整理了最常用的一组:
| 参数 | 类型 | 说明 | 建议值 |
|---|---|---|---|
| axisCount | int | 轴线数量 / 维度数量 | 3 ~ 12 |
| gridLevels | int | 网格环层数 | 4 ~ 5 |
| chartRadius | double | 雷达图外接圆半径 | 控件短边一半减 40 |
| maxValues | QVector<double> | 各维度最大值 | 按业务量纲分别设置 |
| gridColor | QColor | 网格环与轴线颜色 | 浅灰 #CCCCCC |
| dataBrush | QBrush | 数据多边形填充 | 半透明蓝 |
| dataPenColor | QColor | 数据多边形描边 | 深蓝 |
| labelPadding | int | 标签与图形间距 | 15 ~ 25 |
axisCount 小于 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 通道的 QColor:
QColor 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, [=]() { QVector<double> newValues = fetchLatestMetrics(); radarWidget->setData(dimNames, newValues); }); timer->start(1000);刷新间隔 1000 毫秒是视觉上比较舒服的节奏,太频繁画面跳动,太慢失去实时性。setData 内部只做数据拷贝和合法性检查,重绘交给 Qt 事件循环调度,不会阻塞 UI。如果数据源在别的线程,记得用信号槽连接替代直接调用,跨线程直接调成员函数是高危操作,轻则数据错乱,重则崩溃。
还有一个优化细节:动态刷新时网格和轴线其实没变化,理论上可以画到 QPixmap 缓存里,每次刷新只重绘数据多边形和标签。我在一个数据刷新较快的监控项目里实测过,缓存网格后 CPU 占用明显下降。源码当前是全量重绘,想优化性能可以直接拆:把 paintEvent 里的网格部分抽到离屏 QPixmap,只有 axisCount 或 gridLevels 变化时才重建缓存,数据变化只叠加数据多边形和标签。
从那以后我每次写完绘图控件,都会强制走一遍缩放窗口、改最大值、切中文字体这三个测试,这几步能暴露八成以上的坐标和渲染问题。希望帮到你。
本文还有配套的精品资源,点击获取