自定义信号的关键不是「怎么声明」,是「参数类型要对上」。 槽的参数少了可以,多了接不上——这一条是「连不上」的头号原因。
上一节用的是内置的 clicked(),这一节自己声明带参数的信号。
监控项目里几乎所有的跨模块通信用都是自定义信号:进度、报警、帧就绪、连接状态。
跨线程传图像时还要注册类型,这一条会在末尾说清。
一、声明就是契约
signals: 下面那一行不是普通函数声明,是「契约」。
| 声明 | 能传什么 | 典型用途 |
|---|---|---|
progress(int, int) |
当前、总量 | 进度条 |
alarm(int, QString) |
级别、内容 | 报警推送 |
frameReady(QImage) |
一张图 | 跨线程传帧 |
这三个覆盖了监控项目九成的场景。
注意第二行用的是 QString 不是 std::string——Qt 的类型系统里只认自己注册过的类型。
用 std::string 会编译不过( moc 不认识它),这是新手最常见的第一个错。
二、参数匹配的三条规则
这条必须记准,D40 的表现在脑子里:
| 信号 | 槽 | 结果 |
|---|---|---|
| (int, int) | (int, int) | 完全匹配 |
| (int, int) | (int) | 允许,第二个丢掉 |
| (int) | (int, int) | 接不上 |
第二行在实际项目里很有用:一个信号带五个参数,多个槽各取所需,不用为每个槽定义不同信号。 第三行反过来了就不行——槽要的东西信号给不出来,只能报错。
报错的形式取决于写法:新式(取地址)是编译错误,
旧式(字符串)是 connect 返回 false,静默失败。
所以「连不上」第一件事:看返回值。
三、跨线程传自定义类型要先注册
这是本节唯一的硬坑,而且报错信息会明确告诉你。
排队连接时,参数要被「打包」进事件队列,所以那个类型必须让 Qt 的元系统认识。 内置类型(int、QString、QImage)都注册过,自定义结构体没有。
| 报错 | 原因 | 修法 |
|---|---|---|
| Cannot queue arguments of type 'MyData' | 没注册 | qRegisterMetaType<MyData>() |
| 连上了但槽不执行 | 注册了但名字不匹配 | 注册时用同一个字符串 |
第一行的修法要在 connect 之前调,放在 main 里最先执行的地方最保险。
第二行更隐蔽:注册了但类型名不一致(比如带命名空间),Qt 认为不是一个类型,
排队时找不到转换器。
四、什么时候该自定义,什么时候不该
信号不是越多越好,多了会让调用关系变乱。
| 情况 | 建议 |
|---|---|
| 只是通知「变了」 | 用内置信号,别有自定义 |
| 需要传业务数据 | 自定义,参数别超过三个 |
| 一个信号带七八个参数 | 改成传一个结构体 |
第三行是经验:参数一多,槽的实现就和信号声明强耦合,改一个字段要改所有槽的签名。 传结构体就只改结构体——这是「稳定接口」的常规做法。
五、动手 Demo:两个自定义信号:一个带进度,一个带结果
声明 progress(int, int) 和 finished(bool, QString),各连一个槽,看参数怎么传进去。
重点是第二条:槽的参数比信号少是允许的。
class Worker(QObject):
progress = Signal()
finished = Signal()
wk.progress.connect(lambda cur, total:
got.append("%d/%d = %.0f%%" % (cur, total,
cur * 100.0 / total)))
wk.finished.connect(lambda ok, msg:
got.append("结束 ok=%s %s" % (ok, msg)))
for i in range(1, 6):
wk.progress.emit(i, 5)
wk.finished.emit(True, "5 件都做完了")
实跑输出:
--------------------------------------------------------------------
自定义信号,参数怎么带
--------------------------------------------------------------------
**内置信号不够用时就得自己声明**,关键是参数类型与顺序,
**两边必须一致,否则连接会在运行时被静默拒绝。**
1/5 = 20%
2/5 = 40%
3/5 = 60%
4/5 = 80%
5/5 = 100%
结束 ok=True 5 件都做完了
**`progress(int, int)` 和 `finished(bool, QString)` 这两行声明就是契约。**
槽的参数比信号少是允许的(多的丢掉),**多了就接不上**。
信号参数 槽参数 结果
----------------------------------------------------
(int, int) (int, int) 完全匹配
(int, int) (int) 允许,第二个丢掉
(int) (int, int) 接不上,运行期报错
**第三行在 Qt5 的新式写法下是编译错误**(`&Class::slot` 取地址,类型不匹配编不过);
**但在旧的字符串写法下是运行期静默失败**——`connect` 返回 false,很多人不看返回值。
**所以「连不上」的第一件事是看 connect 的返回值**,不是怀疑信号没发。
五次进度,从 20% 到 100%,每次两个参数都传对了。finished 那次带了一个布尔和一个字符串。
这就是契约生效的样子:槽收到的东西和声明的一模一样。
注意格式化串里那个双百分号:要输出一个百分号得写两个, 只写一个会抛 ValueError。这是 Python 里的小坑,但它会让整个 Demo 崩掉。
跨线程时这两个信号都要多一步:int、bool、QString 都是内置类型,不用注册;
换成自己的结构体就必须 qRegisterMetaType,否则报「Cannot queue arguments」。
30 秒实验
- 把
finished的槽改成只收一个参数,看还能不能连上——体会「少了可以,多了不行」。 - 把进度信号的参数改成
(int),槽还是收两个,看报什么错。 - 把
progress的 emit 放到一个线程里,主线程收,看输出顺序是不是变得不确定了。
常见误区
- 以为信号里能用
std::string。 moc 不认识,编译不过。要用QString。 - 以为跨线程传什么类型都行。 自定义类型必须
qRegisterMetaType, 否则报「Cannot queue arguments」。 - 以为参数越多越方便。 超过三四个就该改成传结构体,否则改一个字段要改所有槽。
自测三问(点开对答案)
1. 槽的参数比信号少,能连上吗? —— 能,多余的参数被丢掉。反过来(槽要的比信号多)连不上。 2. 跨线程传自定义结构体,为什么报「Cannot queue arguments」? —— 排队连接要把参数打包进事件队列,需要元系统认识这个类型。 用 `qRegisterMetaType` 注册即可,且要在 connect 之前。 3. 信号参数为什么不宜太多? —— 参数一多,槽的实现就和信号声明强耦合,改一个字段要改所有槽。超过三四个就该传结构体。下一节转到图像:Qt 显示一张图,第一个坑是行对齐。
- ☐ (本节完,下一节 03.04.08 Qt显示图像)