☰
Qt QPainter 手绘 Smith 圆图源码:射频匹配调试的轻量级实现
2026/10/21 3:39:47 网站建设 项目流程

简介:这是一份基于 Qt 的史密斯圆图绘制源码,面向学习射频微波电路、需要理解阻抗匹配可视化的开发者与电子工程专业学生。项目以 Qt5.11.3 为环境,借助 QPainter 完成 2D 图形绘制,通过读取 txt 文件中的复数数据点,将阻抗与导纳映射到圆图上,适合作为图形界面编程与射频计算的练手案例。压缩包共 5 个文件,包含 2 个 cpp 源文件、1 个 h 头文件、1 个 pro 工程文件和 1 个 ui 界面文件,整体约 6KB,结构精简,便于快速导入 Qt Creator 编译运行。源码中自定义了 SmithChartWidget 类,涵盖文件读取、数据解析、坐标转换、圆图绘制与标注等环节,读者可据此掌握 QPainter 的 drawLine、drawArc、drawText 等绘图接口,并理解复数运算与几何变换在圆图上的落地方式。目前已有 821 人学习,适合希望把 Qt 绘图与射频知识结合实践的中级开发者参考。

1. 用 QPainter 手搓一张 Smith 圆图:这份源码到底能省掉多少重复劳动

射频链路调试到匹配网络这一步,几乎绕不开 Smith 圆图。可当你想把它塞进自己的 Qt 上位机或者测试工具里时,麻烦就来了:现成控件要么是某个大厂 EDA 套件里的黑匣子,要么是 Python 生态里带着一堆依赖的绘图库,想找一个纯 Qt、只靠 QPainter 就能画出来、还能直接嵌进 QWidget 的源码,翻遍代码托管平台也没几个能开箱即用的。这份 Qt 绘制 Smith chart 源码解决的正是这个场景——它不依赖任何第三方绘图库,核心就是 QWidget 加 QPainter,把等电阻圆、等电抗弧、阻抗/导纳坐标变换全部用几何计算画出来,适合做射频测试工具、天线调试软件、教学演示程序的开发者直接拿去改。你拿到手就能编译运行,看到一张完整的圆图,而不是对着一堆数学公式发愁怎么落笔。

2. 先搞懂圆图怎么变成像素:从反射系数到 QPainter 坐标

2.1 为什么 Smith 圆图能用圆和弧画出来

Smith 圆图的本质是把阻抗平面通过反射系数公式映射到一个单位圆内。归一化阻抗 z = r + jx 对应的反射系数 Γ 是:

Γ = (z - 1) / (z + 1)

这个 Γ 落在复平面的单位圆里,实部对应横轴,虚部对应纵轴。等电阻线 r = 常数 在 Γ 平面上是一个圆,圆心在实轴上,半径随 r 变化;等电抗线 x = 常数 是一段圆弧。所以画圆图不需要什么高深图形学,就是算圆心和半径,然后调 QPainter 的 drawEllipse 和 drawArc。

具体来说,等电阻圆 r 的圆心坐标(在 Γ 平面)是 (r/(1+r), 0),半径是 1/(1+r)。等电抗弧 x 的圆心是 (1, 1/x),半径是 1/|x|。这些公式直接决定了源码里循环的边界和步长。

2.2 坐标系映射:把单位圆塞进 QWidget

QPainter 的坐标原点在左上角,y 轴向下,而 Γ 平面的原点是圆心,y 轴向上。所以必须做一次映射。常见做法是定义一个中心点 center = QPointF(width()/2, height()/2),再定义一个缩放因子 scale = min(width(), height()) * 0.45,留出边距。然后写两个转换函数:

// 将反射系数的实部虚部映射到 QWidget 像素坐标 QPointF SmithChartWidget::gammaToPixel(double gammaReal, double gammaImag) const { // center_ 是控件中心,scale_ 是单位圆半径对应的像素数 double px = center_.x() + gammaReal * scale_; double py = center_.y() - gammaImag * scale_; // y 轴翻转 return QPointF(px, py); }

逻辑说明:gammaReal 和 gammaImag 是反射系数的实部和虚部,范围在 -1 到 1 之间。scale_ 决定了单位圆在屏幕上占多大,一般取控件短边的 45% 左右,留出边距画刻度。y 轴减号是因为 QPainter 的 y 向下增长,而 Γ 平面的虚部向上为正。

参数说明:center_ 在 resizeEvent 里更新,scale_ 也跟着变,这样控件拉伸时圆图自动适配。如果你想让圆图固定大小不随窗口缩放,就把 scale_ 设成常量,但那样在大屏上会显得小。

2.3 画等电阻圆和等电抗弧的循环怎么写

源码里画等电阻圆的部分通常长这样:

// 绘制等电阻圆,r 从 0 到 50,步长根据精度调整 for (double r = 0; r <= 50.0; r += rStep) { double cx = r / (1.0 + r); // 圆心实部 double radius = 1.0 / (1.0 + r); // 半径 QPointF centerPx = gammaToPixel(cx, 0.0); double radiusPx = radius * scale_; painter.drawEllipse(centerPx, radiusPx, radiusPx); }

逻辑说明:r 是归一化电阻值,从 0 开始,0 对应单位圆本身(半径 1,圆心在原点),r 越大圆越小、越靠右。rStep 控制画多少条圆,一般取 0.5 或 1.0,太密了屏幕糊成一片,太疏了读数不准。

参数说明:r 的上限 50 是常见选择,因为 r > 50 的圆已经小到肉眼难辨,画出来只是右边缘一坨。如果你的应用需要高阻抗匹配,可以调到 100,但步长也要相应调整。

等电抗弧稍微麻烦一点,因为 x 有正负,正电抗是上半弧,负电抗是下半弧。源码里一般分两段循环,x 从 0.1 到 50 画上半弧,再从 -0.1 到 -50 画下半弧。画弧用 drawArc,需要算起始角和跨度角,这里不展开公式,源码里都有注释。

3. 把源码跑起来:编译、嵌入和交互改造

3.1 最小可运行工程的搭建步骤

拿到源码后,第一件事是确认 Qt 版本。这份源码通常基于 Qt 5 或 Qt 6 的 Widgets 模块,不依赖 Qt Charts 或任何第三方库。用 qmake 或 CMake 都能编。我一般用 CMake,因为 Qt 6 默认推 CMake。

步骤一:新建一个 Qt Widgets Application 工程,把源码里的 smithchartwidget.h 和 smithchartwidget.cpp 拷进项目目录。

步骤二:在 CMakeLists.txt 里确保找到 Widgets 模块:

find_package(Qt6 REQUIRED COMPONENTS Widgets) target_link_libraries(your_app PRIVATE Qt6::Widgets)

步骤三:在主窗口里实例化 SmithChartWidget,塞进布局:

// 在主窗口构造函数里 SmithChartWidget *chart = new SmithChartWidget(this); QVBoxLayout *layout = new QVBoxLayout(centralWidget()); layout->addWidget(chart);

逻辑说明:SmithChartWidget 继承自 QWidget,重写了 paintEvent,所以只要放进布局就能显示。不需要手动调 update,Qt 会在窗口显示时自动触发重绘。

参数说明:如果你的主窗口用了 QSplitter 或 QDockWidget,注意给圆图控件设一个最小尺寸,否则拖拽分割条时可能被压成一条线,圆图就看不见了。setMinimumSize(300, 300) 是个稳妥的起点。

3.2 怎么把实测阻抗点画上去

光有底图不够,实际用的时候要把测到的阻抗点标在图上。源码一般会留一个 addImpedancePoint 接口,或者你可以自己加。核心就一步:把阻抗转成反射系数,再转像素。

// 添加一个归一化阻抗点并触发重绘 void SmithChartWidget::addImpedancePoint(double r, double x) { // 阻抗转反射系数 std::complex<double> z(r, x); std::complex<double> gamma = (z - 1.0) / (z + 1.0); points_.append(gammaToPixel(gamma.real(), gamma.imag())); update(); // 请求重绘 }

逻辑说明:std::complex 做复数运算比手写实部虚部分开算清爽得多。gamma 算出来后直接映射到像素坐标,存进 points_ 容器。paintEvent 里遍历 points_ 画小圆点或十字标记。

参数说明:r 和 x 必须是归一化值。如果你手头是 50 欧姆系统的实际阻抗,先除以 50 再传进来。比如 75 欧姆就是 r = 1.5,x = 0。忘了归一化是新手最常见的翻车点,画出来的点全挤在圆图右边。

3.3 频率扫描轨迹怎么画

单点不够直观,扫频测试要画轨迹。源码里如果有 drawTrajectory 之类的函数,原理就是把一串频率点对应的阻抗依次转成像素,然后用 QPainterPath 连起来。

// 绘制频率扫描轨迹 QPainterPath path; for (const auto &zp : sweepData) { std::complex<double> z(zp.r, zp.x); std::complex<double> gamma = (z - 1.0) / (z + 1.0); QPointF pt = gammaToPixel(gamma.real(), gamma.imag()); if (path.isEmpty()) path.moveTo(pt); else path.lineTo(pt); } painter.setPen(QPen(Qt::red, 2)); painter.drawPath(path);

逻辑说明:sweepData 是一个结构体数组,每个元素存一个频点的归一化阻抗。QPainterPath 负责把离散点连成光滑折线。setPen 设红色粗线,跟底图的黑色细线区分开。

参数说明:如果扫频点很密(比如 1001 个点),画出来的轨迹会很平滑,但重绘耗时增加。我一般会在数据量超过 500 点时做一次抽稀,或者用 QPainter 的 Antialiasing 抗锯齿来改善观感,代价是 CPU 占用略高。

4. 避坑与排查:圆图画歪了先查这几处

4.1 圆图变成椭圆

现象:窗口拉宽后,单位圆变成横向椭圆,等电阻圆也跟着变形。

原因:scale_ 用了 width() 和 height() 分别缩放,或者 gammaToPixel 里 x 和 y 用了不同的缩放因子。

解决:scale_ 必须取 min(width(), height()) 的同一个值,保证 x 和 y 方向缩放一致。检查 gammaToPixel 里是不是误用了 width()/2 和 height()/2 分别做偏移。

4.2 圆图边缘被裁掉

现象:单位圆的最左最右或者最上最下贴边甚至超出控件边界。

原因:scale_ 取太大,比如用了 min(width(), height()) * 0.5,没留边距。

解决:改成 0.45 或 0.42,留出画刻度和标签的空间。如果控件本身很扁,比如宽 800 高 200,那 scale_ 会被高度限制,圆图只占中间一小块,这是正常的,要么调整布局给圆图更多高度,要么接受。

4.3 阻抗点位置全错

现象:明明算好的阻抗,画上去跑到圆图外面去了。

原因:忘了归一化,或者反射系数公式写错,比如把 (z-1)/(z+1) 写成了 (z+1)/(z-1)。

解决:先确认输入阻抗是否除以了系统阻抗(通常是 50)。再检查公式,可以用 z = 1(匹配点)验证,反射系数应该是 0,点应该落在圆心。如果落在无穷远,公式肯定反了。

4.4 重绘闪烁

现象:拖动窗口或者频繁更新数据时,圆图闪烁厉害。

原因:没有开双缓冲,或者 paintEvent 里做了太多重复计算。

解决:Qt 5 以上默认开双缓冲,如果还闪,检查是不是在 paintEvent 里反复创建 QPainterPath 或重复计算等电阻圆。把静态底图缓存成 QPixmap,paintEvent 里先 drawPixmap 再画动态点,能大幅降低闪烁。

4.5 高 DPI 屏上线条发虚

现象:在 4K 屏或缩放 150% 的屏幕上,圆图线条模糊,刻度看不清。

原因:Qt 的高 DPI 缩放没处理好,像素坐标算了小数但没对齐。

解决:在 main 函数里设置 QApplication::setAttribute(Qt::AA_EnableHighDpiScaling),Qt 5.6 以上支持。另外画线时用 QPainter::setRenderHint(QPainter::Antialiasing) 让线条平滑,但刻度线如果要求锐利,可以关掉抗锯齿单独画。

5. 进阶:让圆图支持导纳模式和鼠标读数

5.1 导纳圆图的旋转技巧

Smith 圆图有阻抗和导纳两种模式。导纳圆图其实就是把阻抗圆图旋转 180 度,因为导纳 y = 1/z,反射系数变成 -Γ。实现上不需要重画一套圆,只要在 gammaToPixel 里把实部虚部都取反,或者直接在 paintEvent 里对 QPainter 做 rotate(180) 再画。但注意刻度标签也要跟着转,不然读数会乱。我一般会在控件上加一个 toggle 按钮,切换时只改一个 bool 成员,重绘时根据这个 bool 决定是否取反。

5.2 鼠标移动显示阻抗值

这是提升工具可用性的关键一步。重写 mouseMoveEvent,把鼠标像素坐标反算成反射系数,再反算成阻抗。

void SmithChartWidget::mouseMoveEvent(QMouseEvent *event) { // 像素坐标反算反射系数 double gammaReal = (event->pos().x() - center_.x()) / scale_; double gammaImag = (center_.y() - event->pos().y()) / scale_; // 反射系数反算阻抗 std::complex<double> gamma(gammaReal, gammaImag); std::complex<double> z = (1.0 + gamma) / (1.0 - gamma); // 发射信号或更新标签显示 z.real() 和 z.imag() emit impedanceHovered(z.real(), z.imag()); }

逻辑说明:反算公式是正算的逆运算。先减中心偏移再除缩放因子得到 Γ,再用 z = (1+Γ)/(1-Γ) 得到归一化阻抗。注意分母 1-Γ 在 Γ 接近 1 时会趋近零,对应阻抗无穷大,实际代码里要加一个保护,比如 |1-Γ| < 1e-6 时直接返回一个极大值或跳过。

参数说明:emit 的信号可以连到状态栏或浮动提示框。如果不想用信号槽,也可以直接 setToolTip 更新文本,但 tooltip 有延迟,体验不如实时标签。

5.3 等驻波比圆和品质因数弧

进阶用法里常要画等 VSWR 圆和等 Q 弧。等 VSWR 圆是以原点为圆心的同心圆,半径由 VSWR 决定:|Γ| = (VSWR-1)/(VSWR+1)。画法跟等电阻圆一样,只是圆心固定在原点。等 Q 弧是连接原点和某个阻抗点的弧线,Q = |x|/r,画的时候用 drawArc 指定角度范围。这两组曲线源码里不一定有,但照着等电阻圆的套路加进去不难,关键是别跟已有的圆混淆,用不同颜色或虚线区分。

5.4 一个我踩过的坑:浮点累积误差

画等电阻圆时如果用 r += 0.1 这种浮点步进,循环到后面 r 可能变成 49.999999 而不是 50,导致最后一条圆画不出来或者位置偏一丝。血泪经验是改用整数循环再乘步长:

for (int i = 0; i <= 500; ++i) { double r = i * 0.1; // ... }

这样 r 的精度可控,不会因为累积误差漏画。从那以后我每次写这种等步长循环都强制走整数索引,再也没翻过车。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询