📹 第三章 FFmpeg二次开发采集并预览本地摄像头
📄 一句话概括

自定义信号的关键不是「怎么声明」,是「参数类型要对上」。 槽的参数少了可以,多了接不上——这一条是「连不上」的头号原因。

📝 本节和前面几节的关系

上一节用的是内置的 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 秒实验

  1. 把 finished 的槽改成只收一个参数,看还能不能连上——体会「少了可以,多了不行」。
  2. 把进度信号的参数改成 (int),槽还是收两个,看报什么错。
  3. 把 progress 的 emit 放到一个线程里,主线程收,看输出顺序是不是变得不确定了。

常见误区

  1. 以为信号里能用 std::string。 moc 不认识,编译不过。要用 QString。
  2. 以为跨线程传什么类型都行。 自定义类型必须qRegisterMetaType, 否则报「Cannot queue arguments」。
  3. 以为参数越多越方便。 超过三四个就该改成传结构体,否则改一个字段要改所有槽。
自测三问(点开对答案) 1. 槽的参数比信号少,能连上吗? —— 能,多余的参数被丢掉。反过来(槽要的比信号多)连不上。 2. 跨线程传自定义结构体,为什么报「Cannot queue arguments」? —— 排队连接要把参数打包进事件队列,需要元系统认识这个类型。 用 `qRegisterMetaType` 注册即可,且要在 connect 之前。 3. 信号参数为什么不宜太多? —— 参数一多,槽的实现就和信号声明强耦合,改一个字段要改所有槽。超过三四个就该传结构体。

下一节转到图像:Qt 显示一张图,第一个坑是行对齐。

相关笔记