
简介面向射频微波电路设计、课程学习及Qt进阶开发人群这份资源实现了基于QPainter的史密斯圆图绘制小程序用于将txt文件中的复数阻抗数据直观映射到圆图上辅助阻抗匹配分析。压缩包共5个文件包括2个cpp、1个pro、1个h和1个ui分别对应主窗口逻辑、控件绘制、工程配置、头文件和界面布局整体仅6KB代码量少适合快速阅读与二次开发。已有819人学习下载。源码完整演示了使用QFile读取文本、坐标变换、调用drawLine/drawArc绘制圆图曲线以及drawText标注的完整流程并封装了SmithChartWidget类配合主窗口菜单即可打开数据文件直接运行观察结果。对于想掌握Qt图形绘制、文件操作或深入理解史密斯圆图的开发者这是一份轻量但完整的参考实现兼具学习与工程价值。 做射频调试的人应该都经历过这个尴尬MATLAB里几行代码就能把S参数画成漂亮的史密斯圆图可一旦想把圆图做进自己的工具软件、嵌入手持设备或者跟串口数据实时对接那套带授权费的方案就完全不灵了。我去年在做一套射频测试辅助工具时需要把时域脉冲转成频谱再从频域取阻抗值映射到圆图上当时翻遍了现成的Qt控件库没有一个能直接用的。试过拿QCustomPlot硬改折腾两天之后放弃了——不如直接用QPainter从零画一个。这篇文章就把“Qt绘制Smith chart源码”这件事彻底讲透史密斯圆图两族曲线背后的数学关系、为什么最终选QPainter而不是QCustomPlot或QGraphicsView、核心类的源码怎么组织、以及接入Touchstone文件、串口数据和鼠标交互时那些容易踩的坑。适合想把圆图控件做进自己Qt工程或者想彻底搞懂这张经典图表底层逻辑的人。1. 为什么是Qt而不是MATLAB/Python工具链的现实考量很多人第一个问题是画个圆图而已至于自己写源码吗这个问题的答案取决于你的使用场景。如果只是出报告、做离线分析那MATLAB或者Python的schelbert包确实更省事输入一组S参数直接出图谁都不会拦你。但我要做的是桌面级调试工具需要满足三个额外条件跨平台部署、跟C采集代码无缝衔接、不用给运行时环境交授权费。Qt在这三个维度上都站得住它的QPainter绘图引擎性能足够一个圆图控件在普通嵌入式主板上也能跑出流畅的帧率这在调试射频硬件时很关键。还有一个很实际的原因Qt控件可以被当成普通QWidget嵌进任何界面布局里。我可以把它放在QLabel旁边、塞进QSplitter、跟QCustomPlot的波形图并排显示甚至直接编译进ARM板子的显示程序。对做射频硬件或者嵌入式开发的人来说这种嵌入能力比绘图本身更值钱。那为什么不干脆用QCustomPlot它的确自带一些特殊轴类型和图层机制看起来很适合做科学绘图。但我试验下来的结论是QCustomPlot的设计目标还是“矩形坐标系下的函数曲线图”它的坐标轴、刻度、网格机制都是围绕笛卡尔坐标体系做的。要改成史密斯圆图这种以单位圆为边界的非矩形坐标空间需要绕很大一圈去骗坐标轴系统换来换去反而容易出错。相比之下QPainter提供的自由绘制能力恰恰是最直接的工具圆图网格又不需要坐标轴刻度系统自己算画起来一点都不亏。技术栈上我只用了Qt Widgets模块加QPainter核心代码不依赖任何第三方库。主窗口里给QCustomPlot留一个位置显示时域波形给自制控件留一个位置显示圆图两边通过信号槽同步整个工具跑起来很清爽。2. 史密斯圆图的数学坐标体系画图之前必须先算清楚的事写源码之前先把数学底子铺好。圆图不是随便画几圈弧线它本质上是“反射系数平面”上的一个映射图。反射系数的定义是Γ (Z - Z0) / (Z Z0)Z是负载阻抗Z0是参考阻抗通常50Ω。Γ是一个复数可以拆成实部U和虚部VΓ U jV把它们画在直角坐标系里横轴是U纵轴是V整个圆图的范围是单位圆|Γ| 1。单位圆内每一个点都对应一个唯一的阻抗值Z。史密斯圆图的巧妙之处在于它把整个右半阻抗平面映射进了一个单位圆这样一来无限大的阻抗区域被压缩成有限大小看阻抗匹配就非常直观。网格上的每一组圆其实是通过固定阻抗实部或虚部得到的曲线。设归一化阻抗z r jx这里r是归一化电阻x是归一化电抗代入反射系数公式Γ (z - 1) / (z 1)把实部虚部分开固定r不变、让x变化得到一族圆这就是等电阻圆固定x不变、让r变化得到另一族圆这就是等电抗圆。它们的具体公式为等电阻圆(U - r/(r1))² V² (1/(r1))²所以圆心在(r/(r1), 0)半径为1/(r1)。等电抗圆(U - 1)² (V - 1/x)² (1/x)²所以圆心在(1, 1/x)半径为|1/x|。注意电抗为正值时圆心在单位圆上方电抗为负值时在下方而且所有等电抗圆都经过点(1, 0)也就是Γ 1那个点对应阻抗无穷大。这几个参数直接决定了网格的绘制代码我把常用值列成一张表方便对照归一化电阻r等电阻圆圆心(U)等电阻圆半径0010.20.1670.8330.50.3330.66710.50.520.6670.33350.8330.167归一化电抗x等电抗圆圆心(1, 1/x)等电抗圆半径0.2(1, 5)50.5(1, 2)21(1, 1)12(1, 0.5)0.55(1, 0.2)0.2从表里能看出一个非常重要的现象电抗越接近0等电抗圆的半径越大比如x0.2时半径是5圆心在纵轴方向上离单位圆很远。这就是为什么绘制网格时必须做裁剪否则大圆弧会冲出控件边界。坐标变换也是绕不开的一步。Qt的像素坐标系Y轴朝下圆图的数学坐标Y轴朝上二者正好相反。我最初直接手写转换函数后来发现QPainter自带矩阵变换可以一步解决这个技巧后面源码部分细说。3. 三种绘图方案的取舍QPainter、QCustomPlot与QGraphicsView做控件前我把Qt生态里能用的三条路线都试了一遍这里直接说结论省得你在同样的问题上浪费工时。第一条路线是QPainter直接绘制。这是最底层、最灵活的方式所有图形都在paintEvent里完成。网格是画圆数据轨迹是画连续多段线标记是画小圆点它天然适合这种“图形元素不多但坐标系特殊”的场景。性能上即使数据点达到几千个用QPolygonF一次drawPolyline也能轻松跑满60帧。代价是所有交互逻辑都得自己写缩放、悬停、拾取都没有现成的。第二条路线是QCustomPlot。它的优势是坐标轴、刻度、图例、鼠标拖拽缩放一整套都做好了特别适合时域波形和频谱图。但它的轴系统和数据映射方式高度绑定笛卡尔坐标要拿它画史密斯圆图得把圆图的数据“伪装”成在普通直角坐标里能显示的曲线虽然最终也能画出来但网格是假曲线拼的坐标轴信息也对不上改动成本比直接画高一个数量级。所以我最后的方案是波形和频谱用QCustomPlot圆图用QPainter两者各司其职。第三条路线是QGraphicsView。它是为“图形对象交互”设计的每个圆、每条线都可以是独立的QGraphicsItem天然支持选中、拖拽、捏合。听起来很美好但实际开发中我发现两个问题一是圆图网格里等电阻圆和等电抗圆数量不少都用独立Item管理会徒增复杂度二是QGraphicsView的坐标系统同样基于矩形场景单位圆的特殊映射还是要绕。除非你要做教学演示软件需要用户拖拽某个圆上的节点然后实时计算匹配网络否则没必要上这个复杂度。三条路线的对比我整理成表格方案绘制性能交互能力坐标轴支持定制自由度上手成本圆图匹配度QPainter高需手写无最高低高QCustomPlot高强强笛卡尔中中低QGraphicsView中强无高高中实践上我推荐QPainter为主路线如果你只是做数据展示可以配合QCustomPlot画波形但圆图部分不要勉强塞进去。4. 核心源码拆解从反射系数到像素坐标的完整链路先看头文件里需要哪些成员。我这个SmithChartWidget的骨架是这样的// smithchartwidget.h #ifndef SMITHCHARTWIDGET_H #define SMITHCHARTWIDGET_H #include QWidget #include QVector #include complex class SmithChartWidget : public QWidget { Q_OBJECT public: explicit SmithChartWidget(QWidget *parent nullptr); // 输入一组复数阻抗自动换算成反射系数 void setImpedanceData(const QVectorstd::complexdouble zPoints, const QVectordouble frequencies); // 输入直接给反射系数轨迹例如Touchstone里的S11 void setReflectionData(const QVectorstd::complexdouble gammaPoints); void setGridVisible(bool visible); void setTraceVisible(bool visible); protected: void paintEvent(QPaintEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void wheelEvent(QWheelEvent *event) override; private: void drawUnitCircle(QPainter painter, const QPointF center, double R); void drawGrid(QPainter painter, const QPointF center, double R); void drawTrace(QPainter painter, const QPointF center, double R); void drawMarkersText(QPainter painter, const QPointF center, double R); std::complexdouble pixelToGamma(const QPointF pos) const; QVectorstd::complexdouble m_gammaPoints; // 反射系数轨迹 QVectordouble m_frequencies; bool m_gridVisible true; bool m_traceVisible true; double m_zoomLevel 1.0; }; #endif // SMITHCHARTWIDGET_H接口设计上控件只认反射系数因为圆图的横纵坐标本来就是Γ的实部和虚部。如果上层拿到的是阻抗通过下面这对转换函数换算// 阻抗转反射系数 std::complexdouble zToGamma(const std::complexdouble z, double z0 50.0) { return (z - z0) / (z z0); } // 反射系数转阻抗鼠标悬停显示用 std::complexdouble gammaToZ(const std::complexdouble gamma, double z0 50.0) { return z0 * (1.0 gamma) / (1.0 - gamma); }到这里公式还很简单真正的关键在paintEvent里的坐标映射。我最开始是手动把每个数学坐标换算成像素坐标后来发现QPainter自带的坐标变换能省掉大半工作量——先沿窗体中心翻转Y轴之后所有绘制逻辑都直接用数学坐标void SmithChartWidget::paintEvent(QPaintEvent *) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); painter.fillRect(rect(), QColor(250, 250, 250)); const double baseR std::min(width(), height()) / 2.0 * 0.98; const double R baseR * m_zoomLevel; const QPointF center(width() / 2.0, height() / 2.0); // 翻转Y轴坐标之后画的坐标系以圆心为原点且Y轴向上 painter.save(); painter.translate(center); painter.scale(1.0, -1.0); painter.translate(-center); drawUnitCircle(painter, center, R); if (m_gridVisible) { drawGrid(painter, center, R); } if (m_traceVisible) { drawTrace(painter, center, R); } painter.restore(); // 文本标注必须在非翻转状态下绘制否则字是镜像的 drawMarkersText(painter, center, R); }这里必须多说一句text在翻转的坐标系里画出来是反的这是所有自绘控件最经典的坑。我的做法是把网格、曲线这些几何图形放在翻转状态下画文字标注统一放到restore之后画两侧坐标都是像素坐标这样文字永远正着显示。网格绘制照着那两张表来写就行void SmithChartWidget::drawGrid(QPainter painter, const QPointF center, double R) { // 等电阻圆蓝色虚线 QColor rColor(30, 80, 200, 120); painter.setPen(QPen(rColor, 1.0, Qt::DotLine)); for (double r : {0.0, 0.2, 0.5, 1.0, 2.0, 5.0}) { double cx r / (r 1.0); double cy 0.0; double radius 1.0 / (r 1.0); painter.drawEllipse(QPointF(center.x() cx * R, center.y() cy * R), radius * R, radius * R); } // 等电抗圆红色虚线注意区分正负电抗 QColor xColor(200, 50, 50, 120); painter.setPen(QPen(xColor, 1.0, Qt::DashLine)); for (double x : {0.2, 0.5, 1.0, 2.0, 5.0, -0.2, -0.5, -1.0, -2.0, -5.0}) { double cx 1.0; double cy 1.0 / x; double radius std::abs(1.0 / x); painter.drawEllipse(QPointF(center.x() cx * R, center.y() cy * R), radius * R, radius * R); } }可以看到翻转坐标后的代码非常直观几乎就是在照着数学公式写。等电抗圆没有裁剪的话x0.2时半径高达5R画出来的弧线会跑出控件很远所以绘制网格前一定要把绘制区域裁剪到单位圆内部。我通常这样加裁剪QPainterPath clipPath; clipPath.addEllipse(center, R, R); painter.setClipPath(clipPath);裁剪要加在drawGrid里但只对网格生效数据轨迹我有时反而希望保留单位圆外的可见性方便排查异常数据所以一般把裁剪限制在网格绘制的小作用域里。数据轨迹的绘制反而不复杂把每个Γ点直接映射成逻辑坐标丢进QPolygonF之后一次drawPolylinevoid SmithChartWidget::drawTrace(QPainter painter, const QPointF center, double R) { if (m_gammaPoints.isEmpty()) return; QPolygonF poly; poly.reserve(m_gammaPoints.size()); for (const auto gamma : m_gammaPoints) { poly QPointF(center.x() gamma.real() * R, center.y() gamma.imag() * R); } painter.setPen(QPen(QColor(0, 160, 0, 220), 2.0)); painter.drawPolyline(poly); // 起点和终点用不同颜色做标记 if (!poly.isEmpty()) { painter.setBrush(Qt::green); painter.drawEllipse(poly.first(), 5, 5); painter.setBrush(Qt::red); painter.drawEllipse(poly.last(), 5, 5); } }很多初学者会在这里写循环然后逐个drawPoint数据少时无所谓数据一多性能就会很难看。用QPolygonF合并成一条多段线是QPainter绘制大量连续点的标准做法。5. 让圆图“活”起来数据接入、实时刷新与交互操作只有静态网格和一条静态曲线圆图只是张图片。真正让它变成调试工具的是数据接入和交互。最常见的输入数据源是Touchstone文件。网络分析仪导出的S1P文件里S11本来就是反射系数直接作为Γ点即可。我写了一个简化版解析函数bool loadTouchstoneS1P(const QString filePath, QVectordouble frequencies, QVectorstd::complexdouble gamma) { QFile file(filePath); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return false; QTextStream in(file); while (!in.atEnd()) { QString line in.readLine().trimmed(); if (line.isEmpty() || line.startsWith(!) || line.startsWith(#)) continue; QStringList parts line.split(QRegularExpression(\\s)); if (parts.size() 3) continue; double freq parts[0].toDouble(); double mag parts[1].toDouble(); double ang parts[2].toDouble(); // 单位是度 double rad qDegreesToRadians(ang); gamma.append(std::polar(mag, rad)); frequencies.append(freq); } return true; }注意S1P文件头部的#行里可能标注了频率单位Hz/kHz/MHz和数据格式DB/MA/RI实际工程代码需要解析这个字段。上面这段只是让你先把流程跑通。如果你是做嵌入式或硬件联调数据一般走串口。我的做法是用QSerialPort接收字节流按换行符拆帧解析出频点和S11复数后通过信号槽提交到主线程调用setReflectionData然后update()。刷新频率控制在10-20Hz就够了圆图变化本身是平滑的没必要用太快的定时器去刷UI。交互方面我实现了鼠标悬停显示阻抗值和滚轮缩放两个基本功能。滚轮缩放要注意缩放中心最简做法是以圆心为缩放中心void SmithChartWidget::wheelEvent(QWheelEvent *event) { double factor event-angleDelta().y() 0 ? 1.2 : 1.0 / 1.2; m_zoomLevel std::clamp(m_zoomLevel * factor, 1.0, 20.0); update(); }鼠标悬停时把像素坐标反算回Γ再换算成阻抗用QToolTip或状态栏显示void SmithChartWidget::mouseMoveEvent(QMouseEvent *event) { if (!this-rect().contains(event-pos())) return; QPointF p event-pos(); double baseR std::min(width(), height()) / 2.0 * 0.98; double px (p.x() - width() / 2.0) / baseR; double py (height() / 2.0 - p.y()) / baseR; // 注意Y轴反转 std::complexdouble gamma(px, py); std::complexdouble z gammaToZ(gamma); setToolTip(QString(Γ%1, %2 Z%3%4Ω) .arg(gamma.real(), 0, f, 3) .arg(gamma.imag(), 0, f, 3) .arg(z.real(), 0, f, 1) .arg(z.imag() 0 ? j : -j) .arg(std::abs(z.imag()), 0, f, 1)); update(); }需要注意由于painter翻转了Y轴手动把像素坐标换算回数学坐标时Y方向也要手动取反否则悬停显示的位置永远上下颠倒。如果你的工具还需要显示时域波形和频谱圆图控件旁边的QCustomPlot依然可以发挥价值。我之前在同一个面板里左边放QCustomPlot显示时域波形右边放自己写的SmithChartWidget显示阻抗轨迹中间用kissfft做时域到频域转换。这样的结构下每个控件只负责自己最擅长的一块代码之间通过数据模型解耦改起来很轻松。6. 我踩过的几个坑与审查清单这部分记录下来是因为每一个坑都实际花掉过我几个小时。第一个是Y轴方向导致的圆图上下镜像。这个问题在翻转Y轴时很容易被掩盖但悬停显示那里如果你忘了手动取反出来的数据就是错的。排查办法非常简单鼠标放到单位圆上半平面正常应该显示正电抗如果显示负电抗说明Y轴转换出问题了。第二个是QPainter绘制大半径圆时的精度问题。等电抗圆x0.2时半径是5R用drawEllipse传QRectF时矩形尺寸会非常大个别情况下圆会渲染出明显的棱角甚至残缺。我的解决办法是绘制网格前先裁剪到单位圆内此外把抗锯齿打开视觉上会好很多。第三个是文本镜像问题。翻转坐标系下drawText画出的字全部左右颠倒。曾经有段时间我为了让文字不镜像在每个绘图状态下反复切换坐标变换代码又乱又容易错。后来改成“几何图形在翻转态绘制文字统一回正常坐标系绘制”这个约定问题彻底消失。第四个是曲线超出单位圆时的表现。反射系数的模应当小于等于1但实际测量数据里经常混入噪声点或者某些有源电路的|Γ|大于1导致曲线尾巴伸到圆外。如果不做处理圆图边界会有意料之外的长线容易误导判断。我现在提供了开关默认把轨迹裁剪到单位圆内排查异常时可以临时关闭裁剪观察原始数据。第五个是缩放后线宽和网格密度的问题。鼠标滚轮把网格放大后如果网格线条数量不变视觉上会显得特别疏。我暂时没有做动态增删网格线而是把缩放上限控制在20倍实测对绝大多数调试场景都够用。如果你确实需要深度缩放可以考虑根据缩放级别动态选择要显示的电阻/电抗值集合。最后检查清单列在这里照着自查一遍基本不会翻车网格线画完是否裁剪到单位圆内悬停显示时Y轴是否手动翻转文本绘制是否放在翻转坐标系之外大半径电抗圆是否出现渲染残破数据点超界时曲线是否具备可控的裁剪行为mouseTracking是否打开没打开的话mouseMoveEvent不会持续触发。做完这个控件后我最大的感受是史密斯圆图这种非常规坐标系的图形真正难的从来不是Qt本身而是动手之前有没有把数学关系理清楚。等电阻圆和等电抗圆的圆心、半径一旦写在纸上后面所有代码都只是把公式翻译成QPainter调用而已。希望这些源码思路能帮你节省掉我当初踩坑的时间。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。