Qt实现Smith Chart绘制

AI助手已提取文章相关产品:

Qt 绘制 Smith Chart 源码技术分析

在高频电路设计和射频系统调试中,工程师常常面临一个看似简单却极具挑战的问题:如何直观地理解阻抗随频率变化的行为?传统的复数计算或极坐标图虽然精确,但缺乏空间直觉。而史密斯图(Smith Chart)正是为此诞生的——它把复杂的阻抗变换变成了“走迷宫”般的图形操作。

如今,越来越多的矢量网络分析仪(VNA)、天线调谐器和嵌入式射频测试设备需要在本地界面实时显示S参数轨迹。MATLAB固然强大,但在工业级软件中,我们更希望拥有自主可控、轻量高效且可深度定制的可视化方案。Qt 作为跨平台 C++ GUI 框架,凭借其成熟的 QPainter 绘图系统和良好的性能表现,成为实现这一目标的理想选择。

本文不依赖任何第三方图表库,从零构建一个基于 Qt 的 Smith Chart 控件。整个过程不仅涉及数学映射与几何绘制,还包括交互逻辑的设计与工程化考量。最终成果是一个可以直接集成到实际项目中的高精度、可交互控件。


要让计算机画出 Smith 图,首先要搞清楚它的本质是什么。很多人把它当作“圆圈组成的图案”,但真正关键的是背后的保角映射原理。

Smith Chart 的核心思想是将归一化阻抗 $ z = r + jx $(其中 $ r = R/Z_0, x = X/Z_0 $)通过 Möbius 变换映射到反射系数平面:

$$
\Gamma = \frac{z - 1}{z + 1}
$$

这个公式看似简单,却完成了从右半复平面(所有稳定负载)到单位圆内部的一一对应。反过来也能还原:

$$
z = \frac{1 + \Gamma}{1 - \Gamma}
$$

在这个映射下, 等电阻线 等电抗线 不再是直线,而是变成一组正交的圆弧:

  • 等电阻圆 :圆心位于实轴 $ (r/(1+r), 0) $,半径为 $ 1/(1+r) $
  • 等电抗圆 :圆心在 $ (1, 1/x) $ 或 $ (1, -1/x) $,半径为 $ |1/x| $

这些圆弧共同构成了 Smith Chart 的网格骨架。特别值得注意的是:
- 实轴上的水平线代表纯电阻;
- 上半圆对应感性负载($ x > 0 $),下半圆为容性($ x < 0 $);
- 单位圆边界表示全反射(开路或短路);
- 中心点 $ (0,0) $ 表示完全匹配($ Z = Z_0 $)。

这种结构使得我们在做阻抗匹配时,可以通过沿着圆弧“移动”来模拟添加串联电感/电容或使用传输线的效果。


那么,在 Qt 中该如何把这些数学曲线精准地画出来?

常见的做法有三种:使用 QCustomPlot 扩展插件、基于 QGraphicsView 构建场景图,或者直接用 QPainter 在 QWidget 上绘制。每种方式各有优劣:

  • QCustomPlot 虽然支持丰富的二维图表,但原生并不包含 Smith Chart 支持,需自行扩展坐标系,灵活性受限。
  • QGraphicsView 提供强大的缩放、图元管理和动画能力,适合大型可视化系统,但架构复杂,对于只需要一个独立控件的小型应用来说显得“杀鸡用牛刀”。
  • QPainter + paintEvent 则提供了最底层的控制权,绘图效率高,资源占用低,非常适合嵌入式设备或对启动速度敏感的应用。

因此,本文采用第三种方式:继承 QWidget ,重写 paintEvent ,利用 QPainter 直接绘制所有元素。这种方式虽需手动处理坐标变换和事件响应,但也正因如此,才能做到极致优化和完全定制。


下面是核心类的基本定义:

class SmithChart : public QWidget
{
    Q_OBJECT

public:
    explicit SmithChart(QWidget *parent = nullptr);

protected:
    void paintEvent(QPaintEvent *event) override;
    void mousePressEvent(QMouseEvent *event) override;
    void mouseMoveEvent(QMouseEvent *event) override;

private:
    void drawGrid(QPainter &painter);
    void drawImpedancePoint(QPainter &painter, double r, double x);
    QPointF gammaToPixel(double re_gamma, double im_gamma);

    double m_centerX, m_centerY;
    double m_radius;
    double m_hoverR = 0.0, m_hoverX = 0.0;
    bool m_showCursor = false;
};

整个控件的关键在于 paintEvent 的实现。每次重绘时,先确定图表中心和最大可用半径:

void SmithChart::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing); // 抗锯齿

    m_centerX = width() / 2.0;
    m_centerY = height() / 2.0;
    m_radius = qMin(m_centerX, m_centerY) * 0.9; // 留边距

    painter.translate(m_centerX, m_centerY); // 原点移到中心

接着绘制背景和网格。这里有个重要细节: 不能简单调用 drawCircle 画整圆 ,因为部分等电抗圆只存在于上半或下半平面。我们必须使用 drawArc 并正确设置起始角度和跨度。

以等电阻圆为例:

for (int i = 0; i <= 10; ++i) {
    double r = i * 0.2;
    if (r == 0) continue; // r=0 是左边界圆,单独处理?

    double centerU = r / (1 + r);
    double radiusU = 1.0 / (1 + r);

    QRectF rect(-radiusU * m_radius, -radiusU * m_radius,
                2 * radiusU * m_radius, 2 * radiusU * m_radius);
    rect.moveCenter(QPointF(centerU * m_radius, 0));

    painter.drawArc(rect, 0, 5760); // Qt角度单位是1/16度
}

而对于等电抗圆,则需分别绘制正负值,并注意它们只覆盖半个圆周:

for (int i = 1; i <= 16; ++i) {
    double signs[] = {-1.0, 1.0};
    for (double sign : signs) {
        double x = sign * i * 0.5;
        double centerY = 1.0 / x;
        double radiusY = fabs(1.0 / x);

        QRectF rect(-m_radius, (-radiusY + centerY) * m_radius,
                    2 * m_radius, 2 * radiusY * m_radius);
        int startAngle = (centerY > 0) ? 0 : 180 * 16;
        int spanAngle = 180 * 16;

        painter.drawArc(rect, startAngle, spanAngle);
    }
}

你可能会问:为什么不用 drawEllipse ?因为 drawArc 允许我们只画半圆,避免超出单位圆范围造成视觉干扰。这也是专业工具常用的手法。

此外,外圈通常标注反射系数相位角(0°~360°)。我们可以每隔30°画一条刻度线:

QPen anglePen(Qt::blue);
anglePen.setWidthF(0.8);
painter.setPen(anglePen);
QRectF outerCircle(-m_radius, -m_radius, 2*m_radius, 2*m_radius);
for (int deg = 0; deg < 360; deg += 30) {
    double rad = deg * M_PI / 180.0;
    QPointF p1(cos(rad), sin(rad));
    QPointF p2 = p1 * 0.98;
    painter.drawLine(p1 * m_radius, p2 * m_radius);
}

至于数据点的绘制,关键是实现 $ \Gamma $ → 屏幕坐标 的转换函数:

QPointF SmithChart::gammaToPixel(double re_gamma, double im_gamma)
{
    double mag = sqrt(re_gamma*re_gamma + im_gamma*im_gamma);
    if (mag > 1.0) return QPointF(); // 超出单位圆

    double x = re_gamma * m_radius;
    double y = -im_gamma * m_radius; // Qt Y轴向下,需翻转
    return QPointF(x, y);
}

有了这个函数,就可以轻松将任意阻抗点画上去:

void SmithChart::drawImpedancePoint(QPainter &painter, double r, double x)
{
    std::complex<double> z(r, x);
    std::complex<double> gamma = (z - 1.0) / (z + 1.0);
    QPointF pos = gammaToPixel(gamma.real(), gamma.imag());

    if (!pos.isNull()) {
        painter.setBrush(Qt::red);
        painter.setPen(Qt::darkRed);
        painter.drawEllipse(pos, 4, 4); // 小圆点
    }
}

交互功能是提升用户体验的关键。比如用户点击图上某点,能否反向查出对应的阻抗值?

完全可以。通过鼠标事件获取屏幕坐标后,先转换为归一化的反射系数:

void SmithChart::mousePressEvent(QMouseEvent *event)
{
    QPoint pixelPos = event->pos();
    QPointF relPos(pixelPos.x() - m_centerX, -(pixelPos.y() - m_centerY)); // Y翻转
    double normX = relPos.x() / m_radius;
    double normY = relPos.y() / m_radius;

    double magSq = normX*normX + normY*normY;
    if (magSq > 1.0) return; // 超出单位圆

    std::complex<double> gamma(normX, normY);
    std::complex<double> z = (1.0 + gamma) / (1.0 - gamma);

    m_hoverR = z.real();
    m_hoverX = z.imag();
    m_showCursor = true;

    update(); // 触发重绘以显示光标信息
}

此时可以在 paintEvent 中追加一段代码,绘制十字光标并显示数值标签:

if (m_showCursor) {
    QPen crossPen(Qt::red, 1, Qt::DashLine);
    painter.setPen(crossPen);
    painter.drawLine(-m_radius, 0, m_radius, 0);
    painter.drawLine(0, -m_radius, 0, m_radius);

    painter.setPen(Qt::black);
    painter.setFont(QFont("Arial", 10));
    QString text = QString("Z = %1 + j%2 Ω").arg(m_hoverR, 0, 'f', 2).arg(m_hoverX, 0, 'f', 2);
    painter.drawText(10, -m_radius + 20, text);
}

这已经具备了基本的交互能力。进一步还可以增加:
- 多点标记与轨迹追踪
- 频率标签悬停提示
- 键盘导航支持(如方向键微调)
- 深色主题适配(修改画笔颜色)


在实际系统中,Smith Chart 往往不是孤立存在的。典型架构如下:

[射频前端]
     ↓ (SPI/I2C/USB)
[VNA模块] ——→ [嵌入式处理器 / PC]
                  ↓
          [Qt应用程序] ——→ SmithChart Widget
                  ↑
        [用户输入:频率切换、校准]

假设你正在开发一款便携式阻抗分析仪,主控接收到 VNA 模块传来的 S11 数据(例如幅度 0.6,相位 60°),你需要将其转换为复数形式:

$$
\Gamma = 0.6 \cdot (\cos60^\circ + j\sin60^\circ) = 0.3 + j0.52
$$

然后调用绘图接口:

std::complex<double> gamma(0.3, 0.52);
QPointF pos = gammaToPixel(gamma.real(), gamma.imag());
// 添加到点序列并刷新

如果支持不同特性阻抗(如 Z0=75Ω 视频系统),只需在计算前做归一化处理即可。甚至可以设计一个 setZ0(double z0) 接口动态调整网格。


在工程实践中,有几个关键点容易被忽视但至关重要:

  1. 浮点精度问题
    所有中间计算建议使用 double 类型。曾有开发者用 float 导致高阻抗区域圆弧明显偏移,尤其在接近单位圆边缘时误差放大。

  2. 性能优化策略
    - 网格是静态的,除非窗口尺寸改变,否则无需每帧重绘。可考虑缓存为 QImage ,仅在 resizeEvent 后更新。
    - 动态数据点(如扫频动画)应单独绘制,避免重复渲染整个网格。

  3. 可维护性设计
    SmithChart 封装成独立 .h/.cpp 文件,提供清晰 API:
    cpp void addPoint(double freq, const std::complex<double>& gamma); void clearPoints(); void setZ0(double z0); void exportToPNG(const QString& path);

  4. 验证方法
    必须进行基准测试:
    - 输入已知阻抗验证输出位置是否正确:

    • 短路(0Ω)→ $ \Gamma = -1 $ → 左端点
    • 开路(∞Ω)→ $ \Gamma = +1 $ → 右端点
    • 匹配(Z0)→ $ \Gamma = 0 $ → 中心
    • 与 ADS 或 MATLAB 生成的标准图对比一致性

最终,这套方案的价值远不止于“能画出一个图”。它意味着你可以摆脱商业软件授权限制,在没有 MATLAB 许可证的情况下依然构建专业的射频调试工具。更重要的是,整个过程加深了对 Smith Chart 数学本质的理解——当你亲手写出每一个圆弧方程时,那些原本抽象的概念 suddenly become tangible。

这种从数学到像素的完整闭环,正是嵌入式 GUI 开发的魅力所在。无论是用于教学演示、产品原型还是量产仪器,这样一个轻量、可靠、可扩展的 Smith Chart 控件,都能成为你射频工具箱中的得力成员。

未来还可在此基础上拓展:
- 支持双端口分析(S11 + S22 同屏显示)
- 添加匹配轨迹动画(模拟添加元件后的路径)
- 集成稳定性圆、噪声圆等高级功能

而这,仅仅是从一个 QWidget 和几行 QPainter 代码开始的。

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

您可能感兴趣的与本文相关内容

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值