我见过不少 Qt 项目,最开始都很清爽:拖几个按钮,连几个槽函数,功能很快就跑起来了。尤其是工业上位机、设备调试工具、桌面客户端这种项目,老板一句“先把流程跑通”,开发很容易顺手把代码写进按钮槽里。
一开始确实舒服。
void MainWindow::on_btnStart_clicked() { ui->btnStart->setEnabled(false); db.open(); tcpClient->sendStartCommand(); worker->startCollect(); log("start collect"); }这段代码看着很正常,甚至很“高效”。问题是,项目一旦开始长肉,它就会变味。今天按钮点击要判断权限,明天要写数据库,后天要兼容离线模式,再后来还要加 UDP 广播、串口重连、异常恢复。最后一个槽函数三百行,里面有 UI、有通信、有状态机、有业务规则。这时候你改的已经不是按钮逻辑,而是在拆炸弹。
界面代码真正该做什么?我的经验是:UI 负责表达状态和接收操作,业务负责决定事情怎么发生。Qt 的信号槽很方便,但方便不代表所有逻辑都该塞进 QWidget。按钮点击只是一个入口,不应该变成整个业务流程的根据地。
项目里我一般会把流程往外挪一层,比如做一个 Service 或 Controller:
connect(ui->btnStart, &QPushButton::clicked, controller, &CollectController::start); connect(controller, &CollectController::stateChanged, this, &MainWindow::updateState);这几行解决的不是“代码好不好看”的问题,而是解决后期能不能改。采集流程以后要换成定时启动、远程指令启动、插件触发启动,都不用再复制 MainWindow 里的槽函数。业务逻辑脱离界面后,入口可以变,规则不用跟着散。
很多人会说,项目没那么复杂,分层是不是过度设计?我的判断是:如果只是一次性小工具,直接写没问题。但只要涉及串口通信、TCP 长连接、多线程采集、数据库读写、配置管理、日志系统,基本就别赌了。这些东西天然会变,变的时候最怕它们都绑在 UI 上。
UI 和业务混在一起,还有一个隐蔽问题:线程更难管。比如 Worker 在子线程采集数据,MainWindow 槽函数里直接操作 Worker、数据库和界面控件,一旦对象生命周期没理清,退出时就容易出现卡死、野指针、跨线程调用警告。界面层越重,线程收尾越乱。
常见坑或经验提醒
第一个坑,是把MainWindow写成“上帝类”。所有对象都挂在它下面,所有逻辑都从它调度,最后谁也不敢动它。
第二个坑,是用 UI 状态当业务状态。比如按钮是否 enabled、文本框内容是什么,直接决定设备流程。界面一改,业务跟着坏。
第三个坑,是为了省一个类,复制十段槽函数。开始按钮、菜单按钮、快捷键、远程命令都写一遍启动逻辑,半年后四处补 bug。
第四个坑,是觉得信号槽能自动解耦。其实连接关系乱了以后,信号槽也会变成隐形债务。解耦不是少写调用,而是让职责边界清楚。
我现在写 Qt 项目有个很朴素的习惯:MainWindow 里尽量只放界面刷新、用户交互、弹窗提示;业务流程放到独立对象;耗时任务放到 Worker;数据读写封装到 Repository 或 Manager。名字可以不讲究,但边界一定要有。
Qt 开发最怕的不是代码丑,而是代码前期看起来很顺手,后期每个需求都要翻旧账。按钮槽函数里塞满业务代码,确实能让第一版快一点,但它省下来的时间,后面大概率会以加班、回归 bug 和“不敢改”的形式还回来。界面不是业务垃圾桶,槽函数也不是流程编排中心。这点早点认,项目会轻很多。