☰
Qt QPainter实现Smith圆图阻抗匹配交互控件源码
2026/10/26 10:47:56 网站建设 项目流程

简介:面向射频微波电路学习与Qt图形开发练习的Smith chart绘制源码,基于Qt5.11.3与QPainter实现,可从txt文件读取复数阻抗数据,完成坐标转换后在圆图上绘制曲线、圆环、弧线与数值标注,适合用于理解阻抗匹配的可视化过程,也可作为Qt自定义绘图控件的参考范例。压缩包共5个文件,包含2个cpp源文件、1个h头文件、1个pro工程文件和1个ui界面文件,整体约6KB,其中界面文件负责组织主窗口交互,源文件承载读取、转换与绘制逻辑,体量轻巧,便于导入Qt Creator查阅与编译。已有822人学习下载,说明其在相关学习场景中具有一定参考价值。源码重点展示QFile与QTextStream读取解析、复数数据到圆图坐标的映射、paintEvent刷新以及QPainter绘图接口的组织方式,对中初级Qt开发者、电子工程学生和需要实现Smith圆图界面的工程师均有借鉴意义。

1. 调试匹配时最没用的动作,是在纸上徒手画圆图

调一段匹配网络,真正卡住人的往往不是算不出电容电感的取值,而是算完之后不知道下一步该往圆图的哪个方向挪:手里那串阻抗数字变成屏幕上的一个点之后,它离圆心还有多远、往哪个方向移动才能让驻波比降下来,心里没底。这份 Qt 源码解决的就是这件事:一个 QWidget 子类,用 QPainter 把等电阻圆、等电抗弧、单位圆外框一级一级画出来,鼠标在圆图里点一下,旁边立刻给出归一化阻抗、反射系数模值和驻波比。整个工程没有任何第三方图表依赖,纯 Qt Widgets 加 QPainter,源码量很小,新手拿来当 Qt 绘图练习合适,做射频工具的人拿它当阻抗匹配小工具的底座也合适。

2. 项目定位与技术选型:为什么是 QPainter 手撸,而不是 QtCharts

拿到这份源码,第一眼容易误判成"一个画图的 Demo"。实际上它把 Smith chart 拆成了三层:几何层负责 Γ 平面到像素的映射,绘制层负责生成圆和弧的 QPainterPath,交互层负责把鼠标坐标反算回阻抗。三层各自独立,换主题、换坐标系、加数据轨迹都不用动其他两层。这种拆分方式决定了它不适合塞进 QtCharts 那套"数据序列 → 坐标轴 → 渲染器"的模型里,因为圆图的坐标轴根本不是线性的,它是一张把整个右半阻抗平面压缩进单位圆的保角映射图。

2.1 目录结构与模块职责

源码的目录组织基本按上面三层铺开,我按常见 Qt Widgets 工程的惯例把它梳理成下面这个样子,实际文件命名可能略有出入,但职责划分是对得上的:

smithchart/ ├── smithchart.pro # qmake 工程,也可以直接换成 CMakeLists.txt ├── src/ │ ├── main.cpp # 只负责起窗口、塞进一个 SmithChartWidget │ ├── smithchartwidget.h/.cpp # 主控件:paintEvent、鼠标事件、缓存管理 │ ├── smithgeometry.h/.cpp # Γ 平面 <-> 像素 的双向变换,全篇唯一真源 │ ├── smithgridbuilder.h/.cpp # 生成等 r 圆、等 x 弧的 QPainterPath │ ├── smiththeme.h # 线宽、颜色、字号、留白比例 │ └── smithchartexporter.h/.cpp # PNG / SVG / PDF 导出 ├── res/ │ └── theme_dark.json # 暗色主题,只在启动时读一次 └── examples/ └── sweep_demo.cpp # 灌一组扫频数据,看轨迹怎么走

这个划分里最关键的一条纪律是:任何地方要算像素坐标,都必须调SmithGeometry,不准在业务代码里手写center.x() + re * radius。我见过太多圆图项目在导出模块里偷偷重算了一遍坐标,结果导出的图跟屏幕上差几个像素,查半天以为是抗锯齿的问题。

2.2 三种画法的取舍

Qt 生态里画这种图有三条路,选错路后面全是被动:

方案适合什么用在 Smith chart 上的问题
QtCharts折线、柱状、饼图这类直角坐标数据极坐标和自定义网格都得重写QAbstractSeries,等于绕远路
QCustomPlot需要缩放、拖拽、图例的科学绘图单文件几千行,为了画静态网格引进来太重,且它的 layer 模型和圆图的"底图+覆盖层"不是一回事
QPainter 直绘图形本身有明确数学定义、交互逻辑简单要自己处理 DPR、裁剪、缓存,但可控性最强

圆图的几何是闭式解,等电阻圆和等电抗弧都能用圆心加半径精确描述,没有任何"采样点连成线"的近似需求。这种情况下用图表库反而是负资产——库帮你做的事(自动刻度、自动布局)你一件都用不上,库限制你做的事(自定义裁剪、分层缓存)你件件都要绕。

2.3 坐标系约定:先把 Γ 平面钉死

写这类代码最容易翻车的地方不是画圆,而是坐标系没约定清楚。这份源码里只认一套约定:

  • 归一化阻抗z = r + jx,r ≥ 0,x可正可负;
  • 反射系数Γ = (z - 1) / (z + 1),落在单位圆内;
  • 像素坐标 y 轴向下,而 Γ 平面的虚部向上,必须翻转一次。

翻转这件事看着简单,但它是后面所有"鼠标点偏半格""上下半圆对调"类 bug 的总根源。所以SmithGeometry里干脆只暴露两个函数,让翻转只发生在一个地方:

// smithgeometry.h struct SmithGeometry { QPointF center; // 单位圆圆心,像素坐标 qreal radius = 1.0; // 单位圆半径,像素 QPointF toPixel(const QPointF &gamma) const { // Γ 的虚轴向上,屏幕 y 向下,这里做唯一一次翻转 return QPointF(center.x() + gamma.x() * radius, center.y() - gamma.y() * radius); } QPointF toGamma(const QPointF &pixel) const { return QPointF((pixel.x() - center.x()) / radius, (center.y() - pixel.y()) / radius); } };

toPixel和toGamma互为逆运算,radius同时承担缩放和单位化两个角色。参数上唯一要注意的是radius不能为 0——控件还没布局完时width()或height()可能是 0,直接算会得到除零。稳妥做法是在resizeEvent里判断尺寸小于阈值就直接跳过重算,让几何保持上一次的有效值。

3. 核心绘制链路:从反射系数到屏幕上一圈一线

圆图好看不好看不重要,重要的是每一圈的位置必须能被独立验证。这一章把绘制拆成四步:先算几何,再生成路径,再处理设备像素比,最后做缓存分层。每一步都能单独拿出来对答案,出问题时也能快速定位到底是数学错了还是渲染错了。

3.1 等电阻圆:圆心在实轴上的一族圆

等电阻圆的推导不复杂,但结论必须记准。给定归一化电阻r,对应的 Γ 平面轨迹是一个圆:

  • 圆心:(r / (1 + r), 0)
  • 半径:1 / (1 + r)

代入几个特殊值验证一下就知道对不对:r = 0时圆心在原点、半径 1,正好是整个单位圆(纯电抗边界);r = 1时圆心在(0.5, 0)、半径0.5,左端点过原点、右端点过(1, 0);r → ∞时圆心趋近(1, 0)、半径趋近 0,退化成开路点。三个极端值都对上了,中间值基本不会错。

// smithgridbuilder.cpp QPainterPath SmithGridBuilder::buildResistanceCircle(qreal r, const SmithGeometry &geo) { // r 为归一化电阻,等于 0 时退化成单位圆本身 const qreal cx = r / (1.0 + r); // Γ 平面圆心 x const qreal rad = 1.0 / (1.0 + r); // Γ 平面半径 const QPointF cPix = geo.toPixel(QPointF(cx, 0.0)); const qreal rPix = rad * geo.radius; QPainterPath path; path.addEllipse(cPix, rPix, rPix); return path; }

这里geo.toPixel只用来定圆心,半径单独乘geo.radius做缩放,因为toPixel里的 y 翻转对纯水平位移没有影响,但对半径也没有意义。rPix直接是"Γ 平面半径 × 像素半径",别想着套toPixel再减一次,那样会多算一遍偏移。

调用侧通常会遍历一组固定档位,而不是连续取值。工程上没必要把r从 0 画到 100 连续铺满,档位取得太密只会让线条糊成一片:

// 常用档位:低频段细分,高频段粗分,视觉上疏密才均匀 static const qreal kResistanceTicks[] = { 0.0, 0.1, 0.2, 0.3, 0.5, 0.7, 1.0, 1.5, 2.0, 3.0, 5.0, 10.0, 20.0, 50.0 }; for (qreal r : kResistanceTicks) { const bool major = qFuzzyCompare(r, 1.0) || qFuzzyIsNull(r); p.setPen(major ? m_theme.majorPen : m_theme.minorPen); p.drawPath(buildResistanceCircle(r, geo)); }

major那一行把r = 0(外圈)和r = 1(过圆心)单独提出来加粗,是圆图可读性的关键。没有这两条基准线,中间几十条细线看起来完全一样粗,用户没法快速判断某个点大概在什么量级。

3.2 等电抗弧:圆心跑到单位圆外面去了

等电抗的公式比等电阻稍微反直觉一点:x对应的轨迹圆心是(1, 1/x),半径是1/|x|。注意圆心不在单位圆内,而是落在Γ = 1这条竖线上,且|x|越小圆心跑得越远。x → 0时圆心趋于无穷远,圆弧退化成实轴;x → ∞时圆心回到(1, 0)、半径趋近 0,退化成一个点。

正因为圆心在外面,直接算起止角度会很麻烦,得解两圆交点。更省事的做法是把整圆加进路径,再用单位圆做一次裁剪——QPainter 的setClipPath本来就支持路径裁剪,性能上完全够用:

QPainterPath SmithGridBuilder::buildReactanceArc(qreal x, const SmithGeometry &geo) { Q_ASSERT(!qFuzzyIsNull(x)); // x = 0 就是实轴,单独画 const qreal cx = 1.0; // Γ 平面圆心 x 恒为 1 const qreal cy = 1.0 / x; // x 为负时圆心在下半平面 const qreal rad = 1.0 / qAbs(x); const QPointF cPix = geo.toPixel(QPointF(cx, cy)); const qreal rPix = rad * geo.radius; QPainterPath path; path.addEllipse(cPix, rPix, rPix); return path; // 超出单位圆的部分由外层 clip 掉 }

x为负时cy自动落到下半平面,rad靠qAbs保证为正(addEllipse传负半径会画不出东西,这是个静默失败,调试时很容易被忽略)。

实际绘制时,整组弧一次性塞进一个QPainterPath再统一drawPath,比逐条drawEllipse快不少,因为抗锯齿只需要在合并后的路径上跑一遍边界:

QPainterPath arcs; for (qreal x : kReactanceTicks) { arcs.addPath(buildReactanceArc(x, geo)); arcs.addPath(buildReactanceArc(-x, geo)); // 下半平面对称 } p.setClipPath(unitCirclePath(geo)); p.setPen(m_theme.minorPen); p.drawPath(arcs);

上下对称这一点可以省一半循环,但要注意x = 0不能进这个循环,它是实轴,得单独用一条直线画,否则1/0会产生 inf,addEllipse收到 inf 参数后行为未定义,通常是整条路径消失。

3.3 设备像素比:高 DPI 屏上不糊的关键一行

Qt5 之后高 DPI 屏普及,圆图最容易出现的问题就是"在 1080p 上很清楚,换到 4K 笔记本屏幕就发虚"。原因几乎总是同一个:用QPixmap做缓存时没设devicePixelRatio。一张逻辑尺寸 800×800 的 pixmap,在 DPR=2 的屏幕上会被拉伸到 1600×1600 显示,等于把图放大了一倍,线条自然糊。

void SmithChartWidget::rebuildGridCache() { const qreal dpr = devicePixelRatioF(); // 物理像素尺寸 = 逻辑尺寸 × dpr,必须按物理尺寸申请 m_gridCache = QPixmap((QSizeF(size()) * dpr).toSize()); m_gridCache.setDevicePixelRatio(dpr); // 少了这一行,4K 屏上必糊 m_gridCache.fill(m_theme.background); QPainter p(&m_gridCache); p.setRenderHint(QPainter::Antialiasing, true); p.setRenderHint(QPainter::TextAntialiasing, true); // 所有几何按逻辑坐标算,Painter 会自动乘 dpr p.setClipPath(unitCirclePath(m_geo)); drawResistanceFamily(p); drawReactanceFamily(p); p.setClipping(false); drawBoundary(p); // 外圈、实轴、圆心十字,不受裁剪影响 m_gridDirty = false; }

这里有个容易踩的细节:设了devicePixelRatio之后,QPainter的坐标系仍然是逻辑坐标,你写的geo.center和geo.radius不需要乘 dpr。如果手动乘了,图会缩小到原来的四分之一,然后你会怀疑是不是 DPR 没生效——其实是被乘了两遍。

3.4 分层缓存:静态网格只画一次

圆图的网格是死的,只有 marker、扫频轨迹、鼠标读数在变。把网格缓存成QPixmap,每次paintEvent只贴图加覆盖层,是这类控件最基本的优化:

void SmithChartWidget::paintEvent(QPaintEvent *) { QPainter p(this); p.setRenderHint(QPainter::Antialiasing, true); // 尺寸或主题变了才重建,平时只做一次内存拷贝 if (m_gridDirty || m_gridCache.size() != (QSizeF(size()) * devicePixelRatioF()).toSize()) rebuildGridCache(); p.drawPixmap(0, 0, m_gridCache); // 底层:静态网格 p.save(); p.setClipPath(unitCirclePath(m_geo)); // 覆盖层限制在圆内 drawTrace(p); // 扫频轨迹 drawMarkers(p); // 标记点 p.restore(); drawReadout(p); // 读数画在圆外,不裁剪 }

m_gridDirty在resizeEvent、主题切换、z0变化时置位。判断缓存尺寸那一行是防御性的:有些窗口管理器在拖动边框时不会及时触发resizeEvent,导致缓存尺寸和控件尺寸短暂不一致,贴图会出现撕裂。加上这个判断基本就稳了。

4. 鼠标交互与数据回读:点一下就能读出阻抗和驻波比

画得再漂亮,不能交互就只是个图片。这部分代码量不大,但涉及坐标反算、命中测试和状态管理,是二次开发时改动最频繁的一块。

4.1 从像素反算回 Γ,再做边界判定

鼠标事件拿到的是控件坐标,先转成 Γ,再判断是否落在单位圆内。注意这里是开方比较而不是直接比较平方,因为QLineF::length()已经带了一次开方,量级很小,性能无所谓,可读性更重要:

void SmithChartWidget::mousePressEvent(QMouseEvent *e) { if (e->button() != Qt::LeftButton) { QWidget::mousePressEvent(e); return; } const QPointF g = m_geo.toGamma(e->pos()); // 圆外点击直接忽略,否则会算出负电阻这种物理上不存在的值 if (QLineF(QPointF(0, 0), g).length() > 1.0) return; m_dragging = true; upsertMarker(g); // 命中已有 marker 就复用,否则新建 update(); // 只重绘覆盖层 }

upsertMarker里做命中测试时,阈值不要用固定像素值,应该按控件尺寸换算,否则窗口一大,原本 8 像素的抓取半径就显得太窄:

static const qreal kHitRadiusPx = 10.0; const qreal hitRadiusGamma = kHitRadiusPx / m_geo.radius;

m_geo.radius是像素半径,除一下就把像素阈值换算到了 Γ 平面,窗口缩放时抓取手感保持一致。

4.2 Marker 结构与阻抗回算

Marker 用值类型存,避免指针管理带来的生命周期问题。一个 marker 只需要存 Γ 坐标,阻抗和驻波比都是它的派生量,不存副本:

struct Marker { QPointF gamma; // 唯一真源:反射系数 QColor color = Qt::red; QString label; // 用户可编辑的短标签 bool locked = false; // 锁定后不参与拖拽命中 };

回算函数是整份源码里最值得记住的一段,因为它把三个常用量一次性算全了:

// 由 Γ 反算归一化阻抗与实际阻抗 static void gammaToImpedance(const QPointF &g, qreal z0, qreal *rNorm, qreal *xNorm, qreal *vswr, qreal *gammaMag) { const qreal g2 = g.x() * g.x() + g.y() * g.y(); const qreal den = 1.0 - 2.0 * g.x() + g2; // |1 - Γ|² if (qFuzzyIsNull(den)) { // Γ = (1,0),对应开路 *rNorm = *xNorm = std::numeric_limits<qreal>::infinity(); *vswr = std::numeric_limits<qreal>::infinity(); *gammaMag = 1.0; return; } *rNorm = (1.0 - g2) / den; *xNorm = 2.0 * g.y() / den; *gammaMag = std::sqrt(g2); *vswr = (1.0 + *gammaMag) / (1.0 - *gammaMag); } // 实际阻抗 = 归一化阻抗 × 参考阻抗 // 调用侧:qreal R = rNorm * z0; qreal X = xNorm * z0;

den是|1 - Γ|²的展开,qFuzzyIsNull那一支必须保留——用户把点拖到最右端(1, 0)时就会命中,不处理的话会得到0/0的 NaN,界面上直接显示nan,非常难看。

4.3 VSWR 圆、Q 圆与导纳图叠加

等驻波比圆最简单:圆心固定在原点,半径就是|Γ|。拖动 marker 的时候跟着变,视觉上立刻能看出"我这个点对应的驻波比圈有多大"。

Q 圆稍微绕一点,但推导下来是个很干净的结果。等 Q 定义为Q = |x| / r,代入 Γ 的参数表达式整理后得到:

Γx² + (Γy + 1/Q)² = 1 + 1/Q²

也就是圆心(0, -1/Q)、半径sqrt(1 + 1/Q²)的一族圆,全部经过(1, 0)和(-1, 0)两个点。落到代码里:

QPainterPath buildQCircle(qreal Q, const SmithGeometry &geo) { const qreal cy = -1.0 / Q; const qreal rad = std::sqrt(1.0 + 1.0 / (Q * Q)); const QPointF cPix = geo.toPixel(QPointF(0.0, cy)); const qreal rPix = rad * geo.radius; QPainterPath path; path.addEllipse(cPix, rPix, rPix); return path; // 同样靠单位圆裁剪 }

导纳图的叠加更省事:y = 1/z,对应Γ_y = -Γ_z,也就是整个图关于原点中心对称。所以画导纳圆图不需要推导新的公式,把每个Γ乘 -1 再走一遍同一套绘制流程就行:

// 导纳模式下,所有几何点取反,绘制代码完全复用 QPointF mirror(const QPointF &g) { return QPointF(-g.x(), -g.y()); }

这里要注意的坑是:叠加导纳图的时候,刻度标签也得跟着镜像,但文字本身不能镜像(否则字是反的)。改成把标签位置镜像、文字用正常drawText绘制,就对了。

4.4 导出 PNG / SVG / PDF

导出最大的陷阱是"直接grab()控件"。这样导出的图分辨率跟屏幕一样,放到报告里一放大就糊。正确做法是让控件暴露一个renderTo(QPainter*, QRectF),按目标矩形重算几何再画一遍:

bool SmithChartExporter::toPng(SmithChartWidget *w, const QString &path, const QSize &targetPx, qreal dpr) { QImage img(targetPx, QImage::Format_ARGB32_Premultiplied); img.setDevicePixelRatio(dpr); img.fill(w->theme().background); QPainter p(&img); p.setRenderHint(QPainter::Antialiasing, true); // renderTo 内部会按传入的矩形重算 SmithGeometry,不依赖控件当前尺寸 w->renderTo(&p, QRectF(QPointF(0, 0), QSizeF(targetPx) / dpr)); return img.save(path, "PNG"); }

targetPx / dpr这一步是把物理像素换算回逻辑尺寸,保证线条粗细在导出图里跟屏幕上观感一致。如果不做这个换算,导出 4000×4000 的图时线宽仍然是 1 像素,看起来像头发丝。

SVG 走QSvgGenerator,流程几乎一样,只是要把setResolution设成96 * dpr,否则某些阅读器打开时尺寸会缩水一半。PDF 则用QPrinter设setOutputFormat(QPrinter::PdfFormat),再setPageSize(QPageSize(QPageSize::A4)),把圆图按比例居中画进页面——射频报告里贴圆图基本都用 PDF,矢量格式放大不糊。

5. 避坑与排查:圆图"画出来了但不对"的五种典型现场

这段是血泪经验。圆图这类几何密集型控件,错了往往不会崩,只是"看起来怪怪的",靠肉眼很难判断是数学错了还是渲染错了。下面五条按现象归类,基本覆盖了我见过的绝大多数情况。

5.1 几何变形类:圆不圆、位置偏

现象一:窗口拉宽之后,圆图变成椭圆。

原因很直接:半径用了width() / 2和height() / 2分别算,宽高不等就成了椭圆。圆图必须严格是正圆,因为 Γ 平面的单位圆在几何上就是圆的,压扁之后所有角度关系全错。

解决办法是把半径和圆心统一到短边:

void SmithChartWidget::updateGeometry() { const qreal margin = 0.06; // 四周留白,给刻度标签腾位置 const qreal side = qMin(width(), height()) * (1.0 - 2.0 * margin); m_geo.radius = side * 0.5; m_geo.center = QPointF(width() * 0.5, height() * 0.5); }

现象二:靠近开路端的弧线整条消失,或者画成一条垂直的直线。

原因是x取了很小的值(比如 0.01),1/x得 100,圆心跑到(1, 100),半径 100。这个圆的绝大部分在单位圆外,只有极短一段在圆内。如果没做裁剪,它会画出控件外面去;如果做了裁剪但 Qt 的路径精度不够,可能出现整条路径被丢弃。稳妥做法是给档位设下限,|x| < 0.05的档位直接跳过不画——那个区域视觉上已经和实轴重合,画了也看不出区别。

5.2 渲染质量类:高 DPI 发虚、缩放卡顿

现象三:换到高分屏后,网格线变成两三个像素宽的灰带。

前面 3.3 节说过,QPixmap缓存没设devicePixelRatio是头号嫌疑。排查方法很土但有效:在rebuildGridCache里打一行日志,把m_gridCache.size()和size()都打出来,如果两个值相等而 DPR 是 2,那就百分百是这个原因。修完之后记得把m_gridDirty在QEvent::ScreenChangeInternal时也置位,否则把窗口从 1080p 显示器拖到 4K 显示器上,缓存不会自动重建,还是糊的。

现象四:拖动窗口边框时圆图闪烁,CPU 占用飙高。

原因是没有缓存,每次resizeEvent都触发完整的网格重建,而拖动边框时resizeEvent一秒能触发几十次。除了加缓存,还有一个技巧是在resizeEvent里做防抖:用一个QTimer::singleShot(50, ...)延迟重建,拖动过程中先拉伸旧图,停手后再重画。观感上会有一瞬间的模糊,但流畅度提升非常明显。

5.3 交互与导出类:点偏、导出的和屏幕不一样

现象五:鼠标点在某个交点上,读数显示的阻抗和预期差了一点。

先排查是不是翻转错了。用三个已知点验证:Γ = (0, 0)应该是z = 1(圆心),Γ = (-1, 0)是短路,Γ = (1, 0)是开路。如果点在圆心附近读数正常,但上下半平面反了,那就是某处手写了+ gamma.y() * radius,漏了负号。第二个可能是控件被放进了QScrollArea,e->pos()给的是相对控件自身的坐标,但绘制时用的可能是相对 viewport 的——这种嵌套场景下统一用mapTo转换一次,别无他法。

现象六:导出的 PNG 里,圆图的线宽和屏幕上不一样,或者标签位置错位。

标签位置错位通常是QFontMetrics的问题:标签位置是在paintEvent里用当前 painter 的字体度量算的,导出时换了 painter、换了 DPI,度量结果就变了。解决办法是把标签位置的计算抽成一个不依赖 painter 的纯函数,输入是几何和字号,输出是偏移量,导出时用同一套逻辑重算一遍。线宽不一致则是因为用了QPen(Qt::black, 1)这种整型宽度,在缩放后的 painter 上会被取整。改成QPen配合setWidthF(1.2)并开启Qt::AA_UseHighDpiPixmaps,问题基本消失。

6. 二次开发:把它改成你自己的阻抗匹配小工具

源码本身的完成度够当底座,但真正用起来,每个人都会想加东西。我按改动成本从低到高排了几条,顺手把验证方法也写进去。

第一条,接扫频数据画轨迹。从矢网导出的 Touchstone 文件里读 S11,逐点转成 Γ 直接塞进m_trace,drawTrace里连成QPainterPath就行。这里唯一要注意的是频率着色——单色轨迹看不出频率走向,用QConicalGradient或者按索引插值颜色,能一眼看出低频到高频的移动方向。改动量大概三十行,收益最大。

第二条,加 L 型匹配网络的逐步演示。串联元件在圆图上走等电阻圆,并联元件走等电导圆(也就是导纳图里的等电阻圆)。所以只要把 4.3 节的镜像逻辑封装成"当前处于阻抗视图还是导纳视图",加一个元件就把 marker 沿对应圆移动一次,交互上会非常直观。这个改动需要重构一下Marker的移动逻辑,把"沿圆移动"抽象成一个函数,别在事件处理里硬编码。

第三条,换主题。源码里颜色如果散落在各处,先集中到SmithTheme结构体里,再配一个 JSON 读取。暗色主题最容易翻车的地方是网格线:亮色主题下#CCCCCC的细线在暗背景上几乎看不见,得把线宽和亮度同时调,别只改颜色。

改完之后怎么验证?别靠肉眼。准备一张自检表,每次改完跑一遍:

输入 Γ期望归一化 z期望 VSWR用途
(0, 0)1 + j01.000圆心,匹配点
(-1, 0)0 + j01.000(此处 VSWR 定义失效,应显示短路)短路端
(1, 0)∞∞开路端,检查除零分支
(0.2, 0.4)1 + j12.618上半平面任意点,验翻转
(-0.6, 0)0.25 + j04.000实轴左半段

(0.2, 0.4)那一行是最好用的:z = (1+Γ)/(1-Γ) = (1.2+0.4j)/(0.8-0.4j),分子分母同乘(0.8+0.4j)得(0.96 + 0.48j + 0.32j - 0.16) / 0.8 = (0.8 + 0.8j)/0.8 = 1 + j,|Γ| = sqrt(0.04+0.16) = 0.4472,VSWR = 1.4472/0.5528 = 2.618。三个数能同时对上,说明正变换、逆变换、裁剪、翻转全都正确。我一般会把这个表写成单元测试,而不是手工点一遍,因为手工点很容易点偏一点点,然后对着 0.001 的误差怀疑人生。

还有一个我自己踩过的坑值得单独说:改z0的时候,圆图的几何一个像素都不该变,变的只有读数里的阻抗数值。如果改完z0发现圆图跟着缩放了,那一定是把z0掺进了SmithGeometry的半径计算里。归一化坐标和物理量必须彻底分开,这条线一旦模糊,后面加什么都别扭。

从那以后我每次动这个项目的几何层,都会先把上面那张自检表跑一遍再提交,哪怕只改了一个线宽常量。圆图这东西看久了会骗眼睛,只有数字不骗人。希望帮到你。

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

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

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

立即咨询