尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于Qt的史密斯圆图控件开发:从数学映射到S11实时显示

基于Qt的史密斯圆图控件开发:从数学映射到S11实时显示 简介基于Qt5.11.3与QPainter实现史密斯圆图绘制的小程序源码面向射频/微波电路设计人员、电子工程学习者及需要做数据可视化的Qt开发者适合有一定C基础、希望深入理解Qt 2D绘图机制的人群。包内共5个文件2个cpp实现主要功能与入口1个h声明核心类接口1个pro配置Qt工程属性1个ui定义主窗口界面布局整体压缩后仅6KB结构清晰轻量。核心逻辑围绕自定义SmithChartWidget展开从txt文件中逐行读取复数阻抗数据解析后通过坐标变换映射到圆图坐标系并调用QPainter绘图原语绘制坐标网、标注与阻抗曲线覆盖文件读取、字符串拆分、浮点转换、几何映射和界面刷新等关键环节。已有819人学习下载可作为理解史密斯圆图原理、掌握QPainter实际应用的参考范例替换数据文件即可扩展到VSWR、导纳圆图等场景对入门射频可视化开发具有直接帮助。 做射频测试项目时上位机里免不了要把S11反射系数实时显示到Smith chart史密斯圆图上。网上搜了一圈现成控件要么收费要么嵌入Qt时各种别扭最后干脆自己动手写了一套基于Qt的绘制源码。今天就把这套Smith chart控件的完整实现思路整理出来从背后的数学映射、关键的等电阻/等电抗圆绘制到鼠标交互、与时域转频域数据联动以及我实际开发中踩过的那些坑一次性说清楚。适合正在做阻抗匹配、天线调试、网络分析仪上位机或者需要在Qt里自绘专业工程图的朋友参考。1. 史密斯圆图绘制前必须搞懂的数学和坐标映射1.1 从反射系数到圆图方程很多人一看Smith chart上密密麻麻的圆弧就头大其实它本质上就是一个复平面只不过把反射系数 \Gamma 和归一化阻抗 z 做了双射变换。核心公式是z r jx (1 \Gamma) / (1 - \Gamma)反过来就是\Gamma (z - 1) / (z 1)其中 \Gamma \Gamma_r j\Gamma_i 表示反射系数r 是归一化电阻x 是归一化电抗。把复数运算展开就能得到两条重要的轨迹方程等电阻圆圆心在 (\frac{r}{r1}, 0)半径为 \frac{1}{r1}。也就是说不同数值的 r画出来是一簇圆心沿横轴分布、大小不一的圆。r0时是单位圆本身r趋于无穷时缩成右侧端点(1,0)。等电抗圆圆心在 (1, \frac{1}{x})半径为 |\frac{1}{x}|。x0 时圆心在横轴上方我们取下半圆弧x0 时圆心在横轴下方取上半圆弧。所以电抗圆其实只画属于单位圆内部的那部分弧线。这两簇线是Smith chart的骨架。我当时第一次画电抗圆时偷懒直接drawEllipse结果圆图里全是完整的电抗圆完全没法看。记住电抗圆只保留单位圆内部的弧不是整个圆。1.2 Qt坐标系与工程坐标的换算Qt的绘图坐标是左上角为原点、y轴向下而反射系数复平面的实部和虚部范围一般在[-1, 1]y轴向上。如果不做换算画出来的圆图是上下颠倒的。我的做法是定义一个绘制中心点 center 和绘制半径 radius把复平面坐标映射到屏幕像素屏幕.x center.x Γr * radius 屏幕.y center.y - Γi * radius反推鼠标位置对应的反射系数Γr (屏幕.x - center.x) / radius Γi (center.y - 屏幕.y) / radius这个公式看着简单但整个控件的正确性都建立在它上面。我建议把这两个映射单独封装成 toScreen 和 fromScreen 两个函数后续画网格、画数据点、做鼠标悬停读取阻抗值全都走这两个函数避免各处重复计算导致不一致。还有个细节容易被忽略如果控件的宽和高不相等直接 radius width / 2 会把圆拉成椭圆。最稳妥的做法是取宽高较小值的一半再减去边距然后让圆心居中。虽然这样两侧会留白但至少保证圆是正的。2. 绘图技术选型为什么我不建议直接用QCustomPlot画2.1 三种常见方案的对比在Qt里画Smith chart最常见的路线有三条纯QPainter在自定义QWidget里画。自己处理坐标系、圆弧裁剪、交互事件优点是完全可控、轻量、无第三方依赖缺点是写圆弧裁剪时有点绕用QCustomPlot加自定义图层。QCustomPlot本身是画2D曲线的好手但它默认的坐标轴和图层结构是为线性直角坐标系设计的。要在它之上画Smith chart等于是强行把圆图坐标塞进一个直角坐标系统里缩放和鼠标坐标反算会很别扭用QGraphicsView/QGraphicsScene框架。适合做复杂图元和交互比如每个marker、每段曲线都是独立item方便拾取。缺点是性能上如果S参数点很多、item数量巨大需要自己做合并复杂度偏高。我最终选了第一种纯QPainter画。原因很简单Smith chart的网格是静态的数据轨迹是动态的。网格可以在resizeEvent里离线生成一张QPixmap缓存数据层每次paintEvent重绘即可。这个架构下QPainter的效率和灵活性已经完全可以满足需求没必要引入QCustomPlot或QGraphicsView这些更重的框架。2.2 选型时还要考虑交互需求如果只是在程序里显示一张静态Smith chart截图方案随便选差别不大。但真实项目里总会遇到这些需求鼠标悬停显示某个点的阻抗、拖动查看不同频点、放大某个区域、叠加多条轨迹。这些交互对控件架构是有要求的。QPainter方案下交互靠重写mousePressEvent、mouseMoveEvent、paintEvent完成。每帧绘制时把鼠标位置通过 fromScreen 反算成反射系数再换算成阻抗然后显示到状态栏或tooltip。整个过程不涉及昂贵的GUI框架操作60帧刷新毫无压力。这个方案特别适合嵌入式设备或麒麟等国产化Linux系统因为不依赖额外图形库离线安装Qt后直接编译就能跑。如果后续要做频域曲线、时域波形的联动显示我会在Smith chart旁边各放一个QCustomPlot控件来做二维线性图Smith chart继续用自绘控件两者通过一个 DataProvider 数据源共享S参数数据谁有更新就发信号互不干扰。这也是我最终确定纯QPainter方案的原因之一边界清晰。3. 核心源码实现一步步画出完整的Smith chart3.1 搭建控件框架先写一个自定义的SmithChartWidget继承QWidget。关键成员变量有绘制中心点center、绘制半径radius、S参数数据vectorcomplex 、频率点vector 、当前鼠标位置对应的阻抗值等。控件重写三个关键函数paintEvent用于绘制resizeEvent用于重建坐标缓存和网格缓存mouseMoveEvent用于实时反算阻抗值。// smithchartwidget.h class SmithChartWidget : public QWidget { Q_OBJECT public: explicit SmithChartWidget(QWidget* parent nullptr); void setS11Data(const QVectorstd::complexdouble s11, const QVectordouble freqMHz); protected: void paintEvent(QPaintEvent* event) override; void resizeEvent(QResizeEvent* event) override; void mouseMoveEvent(QMouseEvent* event) override; private: QPointF toScreen(std::complexdouble gamma) const; std::complexdouble fromScreen(const QPointF pos) const; void rebuildGridCache(); void drawGrid(QPainter painter); void drawDataTrace(QPainter painter); void drawMarker(QPainter painter, int index); private: QPointF m_center; double m_radius 200.0; QVectorstd::complexdouble m_s11; QVectordouble m_freq; QPixmap m_gridCache; double m_cursorR 0.0, m_cursorX 0.0; };3.2 绘制外圆、等电阻圆和等电抗圆绘制Smith chart骨架是整个控件里最见功底的部分。先画背景和外圆然后画等电阻圆和等电抗圆。我在paintEvent里做了一个组装但真正画网格的耗时操作被放到了rebuildGridCache里先把网格渲染到QPixmap这样鼠标移动时只重绘数据层性能会好很多。void SmithChartWidget::drawGrid(QPainter painter) { // 先画背景避免截图或系统主题下出现黑底 painter.fillRect(rect(), Qt::white); painter.setPen(QPen(Qt::black, 2)); painter.drawEllipse(m_center, (int)m_radius, (int)m_radius); // 等电阻圆 double resistors[] {0.0, 0.2, 0.5, 1.0, 2.0, 5.0}; for (double r : resistors) { double c r / (r 1.0); double rad 1.0 / (r 1.0); // 实心圆只画轮廓 painter.setPen(QPen(Qt::gray, 0, Qt::DashLine)); painter.drawEllipse(toScreen(std::complexdouble(c, 0)), rad * m_radius, rad * m_radius); } // 等电抗圆只画单位圆内部弧线 double reactances[] {0.2, 0.5, 1.0, 2.0, 5.0}; // 先设置裁剪区域确保弧线不超出单位圆 painter.save(); painter.setClipPath(unitCirclePath()); for (double x : reactances) { double cy 1.0 / x; double rr std::abs(cy); QPointF center(1.0, cy); // 上半平面取上半圆弧下半平面取下半圆弧 int startAngle 0; int spanAngle 0; if (x 0) { startAngle 0 * 16; spanAngle 180 * 16; } else { startAngle 180 * 16; spanAngle 180 * 16; } painter.setPen(QPen(Qt::blue, 1)); painter.drawArc(toScreen(center), rr * m_radius, rr * m_radius, startAngle, spanAngle); } painter.restore(); }这段代码里有三个细节我要单独强调。第一drawArc的起止角度用的是1/16度为单位所以90度要写成90*16很多人第一次画弧都会在这里翻车第二等电抗圆的圆心在横轴1.0处半径 \frac{1}{x} 其实会超出单位圆很多如果不做裁剪画面会乱成一团第三正电抗画上半弧负电抗画下半弧这个正负判断非常容易搞反。3.3 将S11数据映射到圆图上网格画完接下来就是把S参数数据映射上去。数据来源一般是网络分析仪的S1P/Touchstone文件或者仿真软件导出的反射系数。这里面每个频率点对应一个复数 \Gamma(f)直接用之前封装的toScreen转成屏幕坐标即可。void SmithChartWidget::drawDataTrace(QPainter painter) { if (m_s11.size() 2) return; QPainterPath tracePath; for (int i 0; i m_s11.size(); i) { QPointF p toScreen(m_s11[i]); if (i 0) tracePath.moveTo(p); else tracePath.lineTo(p); } painter.setPen(QPen(Qt::red, 2)); painter.setRenderHint(QPainter::Antialiasing, true); painter.drawPath(tracePath); // 标记当前频点 if (m_markerIndex 0 m_markerIndex m_s11.size()) { QPointF mp toScreen(m_s11[m_markerIndex]); painter.setBrush(Qt::red); painter.setPen(Qt::NoPen); painter.drawEllipse(mp, 6, 6); } }如果你是在做时域反射计或者雷达回波分析拿到的是时域采样数据那就需要先做FFT转换把时域波形转到频域得到每个频点的反射系数复数。热词里提到的kissfft就很适合干这个它是轻量级的FFT库嵌到Qt工程里不啰嗦。大致流程是采集时域数据 - kissfft做复数FFT - 取每个频点的复数幅值 - 按频率顺序喂给SmithChartWidget。这样你就能看到回波损耗随频率扫过的阻抗轨迹。我实际测试时如果用1GHz采样率采了1024个点时域数据FFT之后频率分辨率接近1MHz频点稀疏时直接连线会显得很生硬。这种情况下我一般建议在数据层做插值至少保证轨迹连续平滑不然在圆图上看起来像折线图。4. 交互与扩展让Smith chart真正用起来4.1 鼠标悬停实时读取阻抗值静态圆图好看但不好用。射频工程师拿到圆图第一反应肯定是把鼠标挪上去看看某个点对应的归一化电阻和电抗是多少。所以我在mouseMoveEvent里做了坐标反算。void SmithChartWidget::mouseMoveEvent(QMouseEvent* event) { std::complexdouble gamma fromScreen(event-pos()); double mag std::abs(gamma); if (mag 0.999999) { m_cursorR std::numeric_limitsdouble::infinity(); m_cursorX std::numeric_limitsdouble::infinity(); } else { // z (1Γ)/(1-Γ) std::complexdouble z (1.0 gamma) / (1.0 - gamma); m_cursorR z.real(); m_cursorX z.imag(); } update(); // 触发重绘实时刷新状态栏 }translate到状态栏或tooltip展示形如 归一化阻抗 1.25 j0.43反射系数模 0.18驻波比VSWR 1.44。这几个参数对射频工程师来说是很直接的判读信息。注意判断模值是否接近1因为圆图边界上 \Gamma 接近1时阻抗趋向无穷大分母接近0不处理会直接得到异常大的数值。更进一步可以在图上画Marker。鼠标点击某个频率点记录频点索引然后在对应位置画一个高亮圆点同时绘制一条从圆心到该点的放射线直观显示反射系数的相位。这样在调试天线阻抗匹配时很容易看出某个谐振点落在哪个电抗区。4.2 与时域波形、频域曲线的联动扩展很多测试软件并不只有Smith chart一个视图通常还有时域波形和频域S参数曲线。Qt里实现这种多视图联动我的建议是Smith chart用自绘控件时域/频域曲线用QCustomPlot。共享同一个数据模型当用户在主曲线图上拖动光标时通过信号槽同步更新Smith chart上的Marker频点。时域数据到频域数据之间的转换用kissfft这类FFT库实现。把时域回波信号做FFT之后得到复数频谱提取出感兴趣频段的反射系数换算成阻抗就能直接画到Smith chart上。我做环境搭建时在麒麟x86系统上离线安装过Qt 5.15.2建议用国内镜像下载离线包装完再把kissfft源码塞进工程子目录整套环境很干净没有额外依赖。这整套流程下来你手里的工具就不只是画图而是一个初具雏形的射频测量上位机。5. 常见问题与排查技巧实录5.1 高频踩坑速查表我在写这个控件时几乎每个环节都踩过坑整理成下面这张表按出现频率排序问题现象根本原因解决方案圆被画成扁平椭圆宽高不等时半径计算错误用min(width, height)/2减去边距作为radius不要用width/2图像上下颠倒Qt y轴方向与数学坐标相反屏幕y center.y - Γi * radius电抗圆画出完整圆缺少单位圆裁剪或未判断正负设置QPainterPath裁剪路径正x取上半弧负x取下半弧曲线严重锯齿未开启抗锯齿painter.setRenderHint(QPainter::Antialiasing)缩放后线宽变形QPainter变换作用在画笔上设置QPen::setCosmetic(true)让线宽不随缩放变化鼠标读数漂移坐标反算与正算函数不一致强制所有映射只走toScreen和fromScreen两个函数数据点都在圆外输入数据模值大于1检查S11数据是否包含有源数据或做归一化处理5.2 性能优化和建议S参数点少时每帧重绘无所谓。但如果你从VNA导入几千个频点的数据再叠加鼠标悬停实时刷新效率问题就出来了。我的做法是把网格层坐标系、等电阻圆、等电抗圆渲染到QPixmap缓存resizeEvent里重建一次鼠标移动时只重绘数据轨迹层上层叠加QPixmap和动态图元。还有一个容易被忽略的坑paintEvent里不要做任何耗时计算比如读文件、解析数据、排序等这些应该在数据加载线程里完成通过信号触发update()重绘。否则界面卡顿是小事某些场景下还可能被系统判定为无响应。最后分享一个调试小技巧正式接入真实数据之前先用一个解析解验证圆图正确性。比如并联一个50欧姆电阻接在50欧姆系统上理论S11应该是0在圆心处显示一个点串联一个1pF电容它的轨迹应该落在等电抗圆下半平面。如果这些标准场景都对再接入实测数据就心里有底了。我自己写的这套源码就是这样一步步从圆骨架到数据轨迹调试过来的整个过程比想象中费时但也比预想中值得。这个控件后续我还会继续扩展比如Touchstone文件导入导出、多轨迹对比显示、自动阻抗匹配建议这些功能到时候再和大家分享。本文还有配套的精品资源点击获取
返回列表