Qt自绘雷达图与摇杆图控件:可配置方向与工程避坑

发布时间:2026/10/28 12:12:44
Qt自绘雷达图与摇杆图控件:可配置方向与工程避坑 简介一份面向C与Qt开发者的自定义绘图控件示例包聚焦雷达图与摇杆图两类交互图形的实现适合需要学习QPainter绘制、控件封装与方向扩展的中级开发者参考。工程基于VS2019与Qt5.15.2搭建未引入第三方库采用CMake管理构建流程便于对照环境验证编译与运行。压缩包共10个文件包含5个cpp源文件、4个头文件和1个txt说明文件整体约13KB代码量精简但结构完整注释可用于梳理绘制逻辑与模块职责。已有523人学习说明其在Qt图形界面练习场景中具有一定参考价值。读者可从中了解雷达图坐标与图形绘制、摇杆图方向控制与交互响应的组织方式并依据自身需求增加方向或调整控件参数也可将工程作为CMake与Qt结合的小项目模板用于课程设计、界面练习或二次开发前的技术验证。1. 雷达图加摇杆图方向数量是配置出来的不是写死的调一个方向输入设备时最烦的不是算法本身是看着一屏浮点数字判断它到底偏了多少。摇杆输出一对 x/y雷达图那六七个维度也是一串小数日志里滚过去全是 0.37、-0.82谁也没法一眼看出结果是右前还是右前偏上十五度。这个 C 组件包要解决的就是这件事一个雷达图控件加一个摇杆图控件雷达图把多维评分铺成多边形摇杆图把二维方向连同死区、吸附方向一起画出来。方向数量没写死八方向、十六方向、只留上下左右都是配置表里几行数据的事加方向区不用动绘制代码。2. 工程骨架与选型自绘控件比嵌图表库更好改选QWidget自绘而不是塞一个第三方图表库理由很实际。雷达图的轴数会变、摇杆图的方向扇区会变、配色跟着主题走图表库要么只支持固定轴数要么改一个扇形高亮得翻它的内部绘制流程。自绘的代价是要自己算极坐标、自己处理高 DPI收益是每一根线都在自己手里改起来没有黑匣子。组件全部基于QPainter只依赖 Qt Widgets 模块纯计算部分角度换算、方向命中不碰任何 Qt 类型抽出来放进标准库就能单测。2.1 目录怎么分绘制、方向配置、demo 三层拆开目录按职责切不按文件类型切。include/只放对外头文件src/放实现demo/是一个能直接跑的宿主窗口tests/里放不依赖 GUI 的几何计算用例。方向配置单独抽成direction_profile是因为它是两个控件共用的雷达图的轴标签顺序、摇杆图的扇区命中都吃同一套「角度 名称 业务码」的数据结构放在一起改一处生效两处。radar-joystick-widgets/ ├── CMakeLists.txt ├── include/ │ ├── radar_chart.h # 雷达图控件对外只有 setLabels / setValues │ ├── joystick_pad.h # 摇杆图控件对外只有 setProfile / 信号 │ ├── direction_profile.h # 方向配置表纯数据 命中算法 │ └── theme.h # 配色与线宽的集中定义 ├── src/ │ ├── radar_chart.cpp │ ├── joystick_pad.cpp │ ├── direction_profile.cpp │ └── theme.cpp ├── demo/ │ ├── main.cpp │ └── demo_window.cpp # 左边雷达图右边摇杆图拖鼠标看联动 └── tests/ └── test_geometry.cpp # 角度归一化、跨零度命中的用例direction_profile.h里只有struct DirectionDef和几个静态工厂方法没有任何QWidget依赖这个边界要守住。一旦方向命中算法里混进了「当前控件宽度」这种东西单测就没法写了后面加方向也只能靠肉眼在界面上试。2.2 坐标系与尺寸契约先定死半径和角度约定再动手两个控件都涉及角度最容易翻车的地方是约定不统一。雷达图从正上方起、顺时针铺轴摇杆图的方向角也定成正上方为 0、顺时针为正。所有内部角度统一用「从正上方顺时针的度数」只在跟std::atan2、std::cos打交道的那一行做一次转换。屏幕上 y 轴向下数学角逆时针这两个反向叠在一起少写一个负号整张图就上下颠倒。半径同样要有契约控件短边的固定比例不写死像素。这样宿主窗口拉伸时图形跟着缩放不会出现「窗口一大图缩在左上角」的尴尬。// include/radar_chart.h节选 class RadarChart : public QWidget { Q_OBJECT public: explicit RadarChart(QWidget *parent nullptr); // 轴标签和数值必须等长长度不一致的调用会被直接丢弃 void setAxisLabels(const QStringList labels); void setValues(const QVectordouble values); // 每项约定在 0.0 ~ 1.0 protected: void paintEvent(QPaintEvent *e) override; private: QPointF pointAt(const QPointF c, double r, int index, int count) const; QStringList m_labels; QVectordouble m_target; // 目标值 QVectordouble m_display; // 当前显示值动画用 QTimer m_animTimer; double m_radiusRatio 0.38; // 半径占控件短边的比例 };m_radiusRatio取 0.38 是留标签空间用的。轴标签画在顶点外侧比例给到 0.45 以上标签就会被控件边界裁掉半截。这个数不是玄学是按「标签平均宽度约等于半径的 0.3 倍」倒推出来的经验值字号调大时同步往下调。2.3 跑起来的最小步骤CMake 配好就能看到两个控件工程用 CMake 管理控件类带Q_OBJECTAUTOMOC必须开否则信号槽在链接期报 undefined reference这个坑每年都有人踩。cmake_minimum_required(VERSION 3.16) project(dash_widgets LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) # 控件里有 Q_OBJECT不开就链接失败 # Qt6 环境换成 find_package(Qt6 COMPONENTS Widgets) find_package(Qt5 REQUIRED COMPONENTS Widgets) add_library(dashwidgets STATIC src/radar_chart.cpp src/joystick_pad.cpp src/direction_profile.cpp ) target_include_directories(dashwidgets PUBLIC include) target_link_libraries(dashwidgets PUBLIC Qt5::Widgets) add_executable(demo demo/main.cpp demo/demo_window.cpp) target_link_libraries(demo PRIVATE dashwidgets)构建就三行命令cmake -B build -DCMAKE_PREFIX_PATH你的 Qt 安装前缀、cmake --build build -j、./build/demo。CMAKE_PREFIX_PATH是新手第一个卡点系统里装了 Qt 但 CMake 找不到时报错是Could not find a package configuration file provided by Qt5把前缀指到Qt/5.x/gcc_64那一层就好别指到bin目录。// demo/main.cpp #include QApplication #include demo_window.h int main(int argc, char **argv) { // 这行必须在 QApplication 构造之前放后面不生效 QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QApplication app(argc, argv); DemoWindow w; w.resize(900, 520); // 默认尺寸下两个控件并排刚好 w.show(); return app.exec(); }demo 窗口里把两个控件塞进一个QHBoxLayout摇杆的moved信号连到雷达图前两个轴上拖鼠标能看到雷达图实时变形这一步跑通说明构建链路和绘制链路都没问题。Qt6 里AA_EnableHighDpiScaling已经是默认行为这行留着也不会报错只是没作用。3. 雷达图核心链路从一组归一化数据到一个多边形雷达图的绘制链路是「数据归一化 → 极坐标换算 → 网格 → 轴线 → 数据面 → 标签」顺序不能乱后画的盖前面的。整条链路里最贵的不是绘制是换算里的符号处理一个 y 轴方向写反图就是镜像的而且看上去还挺像那么回事不仔细看发现不了。3.1 极坐标到屏幕坐标一个减号决定方向对不对所有顶点都由同一个函数算出来网格、轴线、数据面共用它。这样只要这个函数对三层图形就一定是严格同心的不会出现数据多边形比网格偏半格的情况。// src/radar_chart.cpp QPointF RadarChart::pointAt(const QPointF c, double r, int index, int count) const { // 从正上方开始顺时针均匀铺开 const double deg 90.0 - index * 360.0 / count; const double rad qDegreesToRadians(deg); // y 前面是减号屏幕坐标 y 轴向下数学角逆时针两个反向叠一起 return QPointF(c.x() r * std::cos(rad), c.y() - r * std::sin(rad)); }deg从 90 度起算是因为数学角 0 度指向正右方减 90 才挪到正上方index * 360 / count是顺时针走所以要减不要加加就成了逆时针标签顺序和配置表对不上。r传进来的是像素半径调用方乘好比例再传函数本身不关心缩放。提示改这个函数之后先用 4 个轴、值全为 1.0 跑一遍看是不是一个正菱形。3 个轴看不出问题4 个轴镜像了也容易被误认为是对的。3.2 网格与轴线环数和轴数分开控制网格是若干个同心正多边形不是圆。环数放在主题里轴数跟着标签走。两层循环写反了就会画出「每个环只有一条边」的怪东西。void RadarChart::paintEvent(QPaintEvent *) { QPainter p(this); p.setRenderHint(QPainter::Antialiasing, true); // 不开这个斜边全是锯齿 const int n m_labels.size(); // 少于三条轴构不成多边形长度不一致直接不画别硬算 if (n 3 || m_display.size() ! n) return; const QPointF c(width() / 2.0, height() / 2.0); const double r std::min(width(), height()) * m_radiusRatio; // 1) 网格rings 圈同心多边形 p.setPen(QPen(m_theme.gridColor, 1)); for (int ring 1; ring m_theme.rings; ring) { QPolygonF poly; for (int i 0; i n; i) poly pointAt(c, r * ring / m_theme.rings, i, n); p.drawPolygon(poly); } // 2) 轴线圆心到每个顶点 for (int i 0; i n; i) p.drawLine(c, pointAt(c, r, i, n)); // 3) 数据面值 clamp 到 0~1超范围不会画到控件外 QPolygonF data; for (int i 0; i n; i) data pointAt(c, r * std::clamp(m_display[i], 0.0, 1.0), i, n); p.setBrush(m_theme.fillColor); p.setPen(QPen(m_theme.lineColor, 2)); p.drawPolygon(data); }m_theme.rings一般取 3 到 5。环太密低值区的多边形和小格子挤在一起看不清环太少读者没法估读数值。std::clamp那一行不是防御性编程的洁癖外部数据源偶尔会吐出 1.02 这种浮点误差值不裁的话顶点跑到控件外面Qt 不会报错就是画花了。轴标签的位置在顶点再往外推一段用QFontMetrics量文本宽度做左右对齐右半边的标签左对齐、左半边的右对齐、正上方的居中对齐。不做这个区分标签会盖到图形上。3.3 数据多边形填充透明度比颜色更重要填充用半透明色描边用不透明色两者分开配置。填充不透明会把网格线全糊掉读者就看不出当前值大概在几档。描边线宽给 2 px配合抗锯齿在浅色背景上也清晰。数据点是否画小圆点看使用场景。监控面板上不画密密麻麻一坨调试工具里画能快速定位是哪个维度没数据。这个开关放在主题里不要去改绘制主流程。3.4 数值刷新动画缓动系数与定时器的生命周期数据是外部推进来的直接update()会让多边形瞬间跳变看不出变化方向和幅度。加一个指数缓动显示值向目标值逼近视觉上能看出来「哪个轴在涨」。void RadarChart::setValues(const QVectordouble values) { if (values.size() ! m_labels.size()) return; // 长度不匹配丢弃避免越界 m_target values; if (!m_animTimer.isActive()) m_animTimer.start(); // 按需启动不常开 } void RadarChart::onAnimTick() { const double k 0.22; // 缓动系数0.15~0.30 之间手感比较自然 bool moving false; for (int i 0; i m_display.size(); i) { const double d m_target[i] - m_display[i]; if (std::fabs(d) 1e-3) { // 差值小于千分之一认为到位 m_display[i] d * k; moving true; } else { m_display[i] m_target[i]; // 直接吸附避免永远差一点点 } } update(); if (!moving) m_animTimer.stop(); // 到位立刻停别让 16ms 定时器常驻 }k越大收敛越快、越生硬。0.22 对应大约 200 ms 收敛跟人的视觉残留接近。定时器周期给 16 ms约 60 帧不要给 5 ms——屏幕刷不了那么快多出来的只是在烧 CPU。1e-3这个阈值必不可少浮点指数逼近永远不会精确相等没有吸附判断定时器就停不下来。4. 摇杆图核心链路方向扩展的关键在配置表不在绘制摇杆图看上去比雷达图简单其实逻辑分支更多要判死区、要判超界、要判方向、要画扇区高亮还要把归一化结果和方向码一起抛给业务层。把这四件事拆清楚加方向就只是往配置表里塞一行数据。4.1 圆心、半径与死区三个数决定手感圆心和半径每次用时现算不缓存。缓存的写法在resizeEvent里补一处更新也能跑但只要有人重写了布局而忘了调中心点就偏了这属于「当时能跑、半年后炸」的写法。void JoystickPad::updateFromPos(const QPoint pos) { const QPointF c(width() / 2.0, height() / 2.0); const double r std::min(width(), height()) * 0.42; // 留出边缘描边空间 double dx pos.x() - c.x(); double dy c.y() - pos.y(); // 屏幕 y 向下翻成数学方向 const double dist std::hypot(dx, dy); if (dist m_deadZone * r) { // 死区内一律视为回中 m_active -1; emit moved(0.0, 0.0, QString()); update(); return; } // 超界时把输出裁到单位圆上但摇杆头仍画在圆周 double nx dx / r, ny dy / r; const double len std::hypot(nx, ny); if (len 1.0) { nx / len; ny / len; } const double angleFromTop std::fmod(90.0 - qRadiansToDegrees(std::atan2(dy, dx)) 360.0, 360.0); m_active m_profile.hitTest(angleFromTop); emit moved(nx, ny, m_active 0 ? m_profile.at(m_active).code : QString()); update(); }m_deadZone是半径的比例取 0.08 到 0.15。太小手指微抖就触发方向变化输出一直在跳太大轻推推不动用户以为控件卡了。std::hypot比sqrt(dx*dxdy*dy)稳中间过程不会溢出虽然这里数值很小用不上但养成习惯没坏处。注意dy c.y() - pos.y()这里的减法方向要和绘制时统一。一旦某一处用了pos.y() - c.y()方向就会整体上下翻转而摇杆头的位置却是对的排查起来很费时间。4.2 方向配置表从 8 方向扩到任意角度的做法方向不是if-else堆出来的是一条条DirectionDef。angle是扇区中心角span是覆盖宽度code是给业务层的稳定标识字符串或枚举都行别用显示名当 key显示名会跟着语言变。// include/direction_profile.h struct DirectionDef { QString name; // 显示名如 右上只用于画标签 QString code; // 业务标识如 NE不要用显示名做 key double angle; // 扇区中心角正上方为 0顺时针为正 double span; // 扇区覆盖宽度单位度 }; class DirectionProfile { public: static DirectionProfile make(int count, const QStringList names, const QStringList codes); static DirectionProfile eightWay(); // 45 度一格 static DirectionProfile sixteenWay(); // 22.5 度一格 static DirectionProfile fourWay(); // 只留上下左右四角是空洞 int hitTest(double angleFromTop) const; // 命中返回下标未命中返回 -1 const DirectionDef at(int i) const { return m_dirs.at(i); } int count() const { return m_dirs.size(); } private: QVectorDirectionDef m_dirs; };make的实现就是均匀铺开count个方向、每个360/count度中心角从 0 开始按360*i/count递增。DirectionProfile DirectionProfile::make(int count, const QStringList names, const QStringList codes) { DirectionProfile p; for (int i 0; i count; i) { DirectionDef d; d.angle 360.0 * i / count; // 均匀铺开0 度在正上方 d.span 360.0 / count; // 等宽扇区相邻扇区首尾相接 d.name names.value(i); d.code codes.value(i); p.m_dirs.append(d); } return p; }names的顺序必须从正上方开始顺时针上、右上、右、右下、下、左下、左、左上。顺序错了图还是能画只是「上」跑到右边去了这种错误靠肉眼看图不一定立刻发现最好在 demo 窗口里把八个方向的 code 打到状态栏上转一圈核对一遍。span不是必须等于360/count。想做成「只认四个正方向、四角留空」的手感把 4 个方向的span设成 60 度、间隔 90 度铺开hitTest自然在四个角落返回 -1业务层收到空 code 就当无方向处理。不用改任何绘制代码。4.3 归一化输出与吸附用 remainder 处理跨零度方向判定最容易翻车的地方是 0 度边界。359 度和 1 度在数值上差了 358实际只差 2 度。直接相减取绝对值359 度会被判成「左」而不是「上」。int DirectionProfile::hitTest(double angleFromTop) const { for (int i 0; i m_dirs.size(); i) { const double half m_dirs[i].span * 0.5; // remainder 把差值折到 [-180, 180]跨 0 度不会误判 const double diff std::fabs( std::remainder(angleFromTop - m_dirs[i].angle, 360.0)); if (diff half) return i; } return -1; // 扇区之间有空洞或者角度落在配置外 }std::remainder(x, 360)返回的是离x最近的360整数倍之差结果落在[-180, 180]。用它跟fmod的区别在于fmod(359 - 1, 360)得 358remainder(359 - 1, 360)得 -2取绝对值就是 2正好是真实角距。这个函数是整套方向命中的地基改坏了所有方向都会在边界附近抖动。命中之后是否「吸附」是另一个决定。不吸附摇杆头画在真实位置方向码只做高亮提示手感连续。吸附摇杆头画到扇区中心角上手感是格子的适合方向盘式的离散输入。两种都保留用setSnapToSector(bool)控制绘制时决定m_tip用真实位置还是吸附位置。4.4 输入源对接鼠标、键盘、外部数据流共用一条更新路径不管输入从哪来都收敛到updateFromPos一个入口。鼠标拖拽在mousePressEvent/mouseMoveEvent里调它外部的串口数据、手柄事件、网络包把原始值换算成控件内坐标后同样调它。省得每个来源各写一套边界判断最后四处不一致。void JoystickPad::mousePressEvent(QMouseEvent *e) { if (e-button() Qt::LeftButton) updateFromPos(e-pos()); } void JoystickPad::mouseMoveEvent(QMouseEvent *e) { // 只在按下状态跟随避免鼠标划过就改值 if (e-buttons() Qt::LeftButton) updateFromPos(e-pos()); } void JoystickPad::mouseReleaseEvent(QMouseEvent *) { // 松手回中业务上多半希望摇杆是弹簧式的 emit moved(0.0, 0.0, QString()); m_active -1; update(); }外部数据流进来时不要直接调updateFromPos那个函数吃的是像素坐标。给它加一个setNormalized(double nx, double ny)的重载内部反算出像素位置再走同一条路径边界裁剪、死区、方向判定全都复用。串口一帧 50 字节、一秒来 100 帧的时候还得加一层节流把最近一帧存下来用 16 ms 的定时器统一消费不然事件队列会被绘制请求塞满界面直接卡成幻灯片。5. 避坑与排查这几种现象我都在真机上见过控件本身的代码量不大问题基本出在「环境差异」和「边界条件」上。下面五条是按踩坑频率排的现象都很好认原因往往藏在看不见的地方。5.1 高 DPI 屏上线条发虚、多边形错位现象1080p 屏上一切正常换到 2K 或 4K 屏之后网格线变成两像素宽的灰糊糊一条数据多边形的顶点和网格顶点对不齐偏了半个像素。原因绘制时手动乘了devicePixelRatio。QPainter在开启高 DPI 缩放后坐标已经是逻辑坐标会自己乘一次代码再乘一次就等于放大了两倍位置全部漂移。另一半原因是用了QPainter::TextAntialiasing却没开Antialiasing直线在缩放后走了非整数坐标。解决绘制代码里一律用逻辑坐标width()、height()返回什么就用什么不碰devicePixelRatio。需要精确对齐像素的水平垂直线用painter.drawLine(QPointF(x 0.5, y1), QPointF(x 0.5, y2))补半像素。开启AA_EnableHighDpiScaling之后就别在paintEvent里做任何 DPI 换算这条守住了这类问题基本绝迹。5.2 角度在 0 度附近跳变方向来回闪现象摇杆慢慢从正上方往左偏方向码在「上」和「左上」之间反复横跳摇杆头的位置却是平滑移动的。原因角距用了直接相减取绝对值。359 度和 1 度差了 358被判成最远而不是最近扇区命中的结果就乱了。另一种情形是只写了fmod没处理负数fmod(-2, 360)得 -2直接拿去和half比大小负数永远小于half所有角度都被判成命中第一个方向。解决统一用std::remainder(a - b, 360.0)再取绝对值。写完拿359.9、0.1、180.0三个值跑单测这三个点过了边界基本就没问题。扇区宽度之和小于 360 时hitTest返回 -1 是正常行为业务层要能接住空方向不能把它当错误。5.3 定时器常开把 CPU 拉满现象界面静止不动任务管理器里进程占用稳定在 8% 到 15%笔记本风扇一直转。原因动画定时器在构造函数里start()了之后再也不停。每 16 ms 触发一次update()不管数据有没有变整张图重画一遍。雷达图轴数一多抗锯齿多边形的绘制开销并不便宜。解决改成按需启动、到位即停。setValues里只在定时器没跑的时候start()onAnimTick里检测到所有分量都收敛就stop()。再加一道保护新值和当前目标值完全相同时直接 return不重启动画。改完之后静止状态的 CPU 占用会掉到 0.5% 以下。5.4 窗口 resize 之后中心点跑偏现象窗口拉大摇杆的圆还在中间但拖动时摇杆头的位置和鼠标对不上越往边角偏得越多。原因圆心和半径在构造函数里算了一次存成成员变量resizeEvent里没更新。控件刚创建时尺寸是默认值比如 640×480后面布局把它拉到 900×520缓存值就过期了。雷达图的m_radiusRatio如果也按同样方式缓存像素半径会出现同样的问题。解决圆心和半径做成「用时现算」的局部变量updateFromPos和paintEvent各算一次两处必须用同一个公式。真要缓存就在resizeEvent里统一刷新但更稳的做法是干脆不缓存——这两个值的计算成本就是两次除法和一次std::min完全可以忽略。5.5 子线程直接调 update 导致花屏或崩溃现象数据从串口或网络线程推过来接上之后偶发崩溃堆栈停在绘制相关的地方重跑又复现不了。原因QWidget::update()和paintEvent必须在 GUI 线程执行。工作线程里直接调等于跨线程碰QPainter和底层绘制资源属于未定义行为。这类问题偶发性极强本地跑一天不炸换台机器十分钟就崩。解决工作线程只负责把数据塞进队列用信号槽的Qt::QueuedConnection投递到控件所在线程或者用QMetaObject::invokeMethod(this, ..., Qt::QueuedConnection)。投递频率高的时候先做一次合并工作线程只更新一个std::atomic的「最新值」GUI 线程用 16 ms 定时器去取这样既保证了线程安全也不会把事件队列撑爆。6. 二次开发加方向、换皮肤、接真实数据拿到这份代码之后最常见的三种改造改动面都不大但每一处都有个容易忽略的点。新增方向类型。加一个twelveWay()之类的工厂方法或者直接调make(12, names, codes)names和codes按正上方起顺时针排满 12 项。改动只落在direction_profile.cpp雷达图和摇杆图的绘制代码一行不动。要注意 12 方向的扇区宽是 30 度如果业务上希望「正方向宽、斜方向窄」就在make之后逐条改span别去动hitTest。验证方法是写个循环从 0 到 359 逐度调hitTest把结果打出来看覆盖是否连续、有没有空洞。改造点需要动的地方主要风险方向数量变化DirectionProfile::make的入参名称顺序错位图上方向全反扇区不等宽逐条修改DirectionDef::span留出空洞后业务层没处理 -1换配色theme.h里的颜色常量填充色不透明盖住网格关闭吸附setSnapToSector(false)摇杆头画在真实位置高亮仍按扇区接外部数据新增setNormalized重载在子线程直接调 update换一套皮肤。颜色、线宽、环数、字体全收进Theme结构体控件只读不写。换肤就是构造一个新Theme传进setTheme然后update()。这里有个细节填充色必须带 alpha不透明填充会把网格线完全遮住图上只剩一个色块读者没法估读数值。描边色和填充色要在同一色系里拉开明度差不然低值区几乎看不见。接一路真实数据。外部输入统一走setNormalized(nx, ny)内部反算像素位置复用同一条判定路径。工作线程推数据时用原子变量存最新值GUI 侧 16 ms 定时器消费这个模式在上面第 5.5 条里已经踩过一次。雷达图那边同理setValues只接受与标签等长的数组长度不一致直接丢弃别做自动补齐——补齐会把「数据源出错了」这件事悄悄吞掉等发现时已经错了半个月。// 工作线程侧只存不算绝不碰控件 void Worker::onFrame(const Frame f) { m_latestX.store(f.nx, std::memory_order_relaxed); m_latestY.store(f.ny, std::memory_order_relaxed); m_dirty.store(true, std::memory_order_release); } // GUI 线程侧16ms 定时器统一消费 void DemoWindow::onPoll() { if (!m_dirty.exchange(false, std::memory_order_acquire)) return; m_pad-setNormalized(m_latestX.load(std::memory_order_relaxed), m_latestY.load(std::memory_order_relaxed)); }验证方法。几何计算那部分不依赖 GUItests/test_geometry.cpp里直接断言hitTest(359.9)和hitTest(0.1)必须命中同一个方向pointAt在index0时返回的 y 必须小于圆心 y四方向配置在 45 度处必须返回 -1。这三个用例写完后面每次改角度逻辑先跑一遍比在界面上拿鼠标转圈靠谱得多。我第一次写这套东西的时候为了省事把圆心和半径缓存成了成员变量demo 窗口固定大小跑了两个星期一点问题没有。后来有人把它嵌进一个可拖拽分栏的布局里右栏一拉宽摇杆头就和鼠标差出去半屏。那天下午我盯着代码看了四十分钟才反应过来构造函数里那几个值早就过期了。从那以后我每次写自绘控件圆心、半径、角度这类跟尺寸相关的量一律用时现算宁可多两次除法也不留一个会过期的缓存。希望这些能帮你少走一段弯路。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询