上一篇讲元对象系统与信号槽,这一篇解决”界面怎么做才不翻车”:布局与控件的正确用法、QSS 样式、事件系统、QPainter 实时绘图、线程与界面。延续调参上位机的场景——做完这一篇,你应该能独立写出一个带实时曲线、可换肤、不卡死的上位机界面。
一、控件的本质:数据到界面的映射层
控件不是”画出来的按钮”,而是数据模型的投影:QSpinBox 显示并编辑一个数值,QCheckBox 映射一个 bool,QComboBox 映射一个枚举。理解这一点,选控件的依据就变成了”我的数据是什么类型”:
| 数据类型 | 控件 | 关键信号(选对信号!) |
|---|---|---|
| 连续数值 | QDoubleSpinBox / QSlider |
valueChanged(double) |
| 整数枚举 | QComboBox |
currentIndexChanged(int)(activated 只在用户操作时触发) |
| 开关量 | QCheckBox / QRadioButton |
toggled(bool)(clicked 在编程 setChecked 时不触发) |
| 短文本 | QLineEdit |
textEdited(仅用户输入)vs textChanged(含 setText) |
| 多行日志 | QPlainTextEdit |
appendPlainText 追加,避免频繁 setText 全量重排 |
坑:信号选错 = 数据不同步。 用
QLineEdit::textChanged做”输入即下发”,会在程序自己setText()回显时再次触发下发,形成回环;用textEdited只响应真实用户输入。同理QComboBox的activated不响应代码设置。
二、UI 设计的两种方式
2.1 Designer 可视化 + 自定义控件提升
Designer 适合静态布局:拖拽控件 → 右键”转到槽”自动生成连接骨架 → 自定义连接写在构造函数里。
真正的进阶是自定义控件提升(笔记原话流程):从基类派生自己的控件类,在 Designer 里拖一个和基类同类的占位控件,右键 → 提升为 → 填自定义类名 → 编译运行后占位控件被你的类覆盖:
1 | // 自绘控件:带刻度盘的仪表 |
提升后 Designer 里继续正常布局,运行时行为完全由你的类决定——“界面所见 = 代码所得”。
2.2 代码化动态创建
运行时才知道有多少行参数时用代码创建:
1 | // 从参数表动态生成一行行"名称 + 输入框" |
生成代码和 Designer 代码可以混用:.ui 里放固定框架,动态部分在构造函数里 addWidget。注意动态控件也要 new 并挂父对象(进对象树)。
三、布局系统:为什么手动 move 没用
布局(QHBoxLayout/QVBoxLayout/QGridLayout)接管了子控件的位置和大小,算法基于三个东西:
- sizeHint:控件”想要”多大(按钮按文字+边距算);
- sizePolicy:空间富余/不足时如何伸缩——
Fixed(不动)、Minimum(可大不可小)、Preferred(可大可小,优先 sizeHint)、Expanding(主动抢占多余空间); - stretch 因子:多余空间按比例分配。
1 | layout->addWidget(plotWidget, 1); // 曲线区域:占 1 份弹性空间 |
坑:布局里的控件
move()/setGeometry()无效——布局会立刻重新计算覆盖你的坐标。要精确定位只有两条路:不用布局直接setGeometry(窗口 resize 时不自动调整),或者用弹簧 + stretch 控制相对位置。QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum)是”只占水平弹性、竖直不变”的弹簧。
窗口缩放不塌的秘诀:关键信息用 setMinimumSize 保底,弹性区域(曲线、日志)设 Expanding,固定控件(按钮)保持 Fixed。调参面板里”曲线必须能拉大、按钮不能变形”就是这个策略。
四、QSS:样式与逻辑分离
4.1 选择器与伪状态
1 | QPushButton { |
应用:qApp->setStyleSheet(...) 全局 / widget->setStyleSheet(...) 局部。局部样式会覆盖全局(就近原则)。
4.2 动态属性选择器:暗色模式的正确姿势
QSS 支持按 Q_PROPERTY 动态属性选择(属性变化后需 unpolish/polish 刷新):
1 | // 切换主题 |
1 | QMainWindow[theme="dark"] { background-color: #2b2b2b; color: #eeeeee; } |
机器人上位机常在强光/暗室两种环境用,主题切换是刚需。别用”每个控件 setStyleSheet 覆盖”实现换肤——改一处漏一处;动态属性 + 统一 QSS 是唯一可控的方案。
五、事件系统:从回调到拦截
5.1 事件怎么流动
1 | 系统事件 → QApplication::notify → 目标 QObject::event → 具体 handler(如 mousePressEvent) |
event()是总入口,返回 true 表示事件已处理;- 具体 handler 处理后调
event->accept();ignore()会让事件向上冒泡给父控件(比如子控件不处理的按键给父窗口); - 与信号槽的区别:事件是”框架发给你的”,信号是”你发给别人的”。
5.2 事件过滤器:全局拦截
1 | bool MainWindow::eventFilter(QObject *obj, QEvent *event) override { |
典型用途:回车/空格全局快捷键、屏蔽某控件的右键菜单、鼠标事件转发(拖拽整个无边框窗口)。
5.3 无边框窗口拖动(实战)
1 | void mousePressEvent(QMouseEvent *e) override { |
六、QPainter 绘图:实时曲线怎么做
6.1 paintEvent 的原理
绘制只能发生在 paintEvent 里,外部不能直接画。要刷新就调 update()(异步,多个 update 合并成一次重绘,推荐)或 repaint()(同步立即重绘,可能闪烁)。
1 | void GaugeWidget::paintEvent(QPaintEvent *) override { |
要点:translate/rotate/save/restore 让”画指针”变成”旋转画布再画一条竖线”——云台角度仪表盘、imu 姿态球都是这个套路。
6.2 实时曲线:环形缓冲 + 定时刷新
高频数据(如 1kHz 的 PID 反馈)不能每次到数据就 update——重绘开销大且肉眼 60fps 就到顶了。正确做法:
1 | // 采集线程只往环形缓冲写 |
这就是”曲线不卡、CPU 不爆”的全部秘密:数据写入与绘制解耦,绘制频率固定(~30fps),数据频率随便多高。PlotJuggler、rqt_plot 也是这个模型。
七、定时器:QTimer 的精度边界
1 | QTimer *t = new QTimer(this); |
精度事实:QTimer 依赖事件循环,实际触发间隔 = 设定值 ± 数毫秒(系统调度 + 事件队列排队)。结论:
- 界面刷新、心跳、超时检测:QTimer 足够;
- 机器人 1kHz 控制周期:绝对不能用 QTimer。控制循环必须跑在实时线程(或专用高优先级线程)里,界面只负责显示;
- 用 QTimer 做”超时保护”时留裕量:串口 100ms 超时,定时器设 120ms,别卡在临界值上。
八、文件与配置
8.1 文本读写与编码
1 | QFile f(path); |
坑:Windows 下记事本存的中文是 GBK/带 BOM,
setCodec("UTF-8")读出来是乱码。要么统一约定 UTF-8,要么读 BOM 判断编码。跨平台上位机(Windows 调参 + Linux 工控机)建议配置文件一律 UTF-8。
8.2 参数持久化:QSettings 与 JSON
1 | // ini 式配置:记住上次的窗口位置、PID 初值 |
九、线程与界面:不卡死的铁律
铁律 1:控件属于创建它的线程,跨线程操作控件 = 未定义行为。
铁律 2:耗时操作(文件、网络、串口读写、算法)放工作线程,结果用信号回传。
1 | class Worker : public QObject { |
- 槽在接收者所在线程执行(QueuedConnection),所以
doHeavyWork跑在工作线程,finished的回调跑回 UI 线程——信号槽自动完成了线程切换; - 别用
QThread子类重写run()+ 裸指针传控件,那是教科书式的崩溃写法; - 工作线程里
emit的信号,槽里若操作控件,务必确认连接是 Queued(跨线程自动就是)。
十、踩坑清单
| 现象 | 原因 | 解决 |
|---|---|---|
| 布局里 move() 无效 | 布局接管几何管理 | 用弹簧/stretch,或放弃布局 |
| setText 后 textChanged 又触发一次 | 信号选错 | 用户输入用 textEdited |
| 曲线一卡一卡 | 每个数据点都 update() | 环形缓冲 + 30ms 定时刷新 |
| 界面点按钮后卡死 | 槽里同步做了耗时操作 | 工作线程 + 信号回传 |
| 窗口 resize 后控件乱飞 | 没有 sizePolicy/stretch | Fixed 固定、Expanding 弹性 |
| 中文全乱码 | 编码没设/文件是 GBK | setCodec(“UTF-8”) + 统一约定 |
| 事件过滤器没生效 | 忘了 installEventFilter | 检查安装与返回值 |
| 定时器不准 | 把 QTimer 当硬实时 | 控制循环进专用线程 |
最后一句:控件、布局、样式、事件、绘图、线程——六个主题背后只有一个原则:界面层永远是被动的投影层,数据与逻辑在它下面。守住这条线,上位机怎么写都不乱。