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)
接口动态调整网格。
在工程实践中,有几个关键点容易被忽视但至关重要:
-
浮点精度问题
所有中间计算建议使用double类型。曾有开发者用float导致高阻抗区域圆弧明显偏移,尤其在接近单位圆边缘时误差放大。 -
性能优化策略
- 网格是静态的,除非窗口尺寸改变,否则无需每帧重绘。可考虑缓存为QImage,仅在resizeEvent后更新。
- 动态数据点(如扫频动画)应单独绘制,避免重复渲染整个网格。 -
可维护性设计
将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); -
验证方法
必须进行基准测试:
- 输入已知阻抗验证输出位置是否正确:- 短路(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),仅供参考

被折叠的 条评论
为什么被折叠?



