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

信号就是「我发生了」,它不知道谁在听,也不关心有没有人听。 它是观察者模式,二十行 Python 就能写完——写完你就不再觉得它神秘。

📝 本节和前面几节的关系

SDL 那条线到这里结束,剩下七节讲 Qt 的信号槽机制。 先讲信号(发出方),再讲槽(接收方),最后讲关联(怎么接上), 这三节连起来就是一套完整的通知机制。 为什么在监控项目里要用它:界面、采集、录像、报警四个模块之间的通信,全靠这套东西解耦。

一、它解决什么问题

假设没有信号,界面怎么知道采集出错了?只能轮询或者直接调用。

做法 耦合度 问题
直接调用 高 界面要认识采集模块
轮询标志位 中 延迟大,浪费 CPU
信号槽 低 谁都不用认识谁

第三行是全部价值:采集模块只管 emit, 它不需要知道有没有界面、界面长什么样、甚至有没有界面。 这就是「解耦」两个字的具体含义——编译期不依赖,所以可以单独测试、单独替换。

二、三个特性,三个坑

D34 验出来的三条,每条都对应一个常见误解:

特性 含义 误解
只发不管 没人听就静默丢弃 以为没连上=没发出去
一对多 按登记顺序挨个调 以为顺序不确定
返回值被丢 拿不到槽的返回值 以为能靠返回值传结果

第一行最容易让人怀疑人生:写了 emit 界面没反应,先查连接,别查信号。 因为信号发出去是「无条件的」,Qt 不检查有没有接收者,也不返回「发给了几个人」。

第三行解释了一个设计选择:Qt 的槽不返回业务值,要传结果就传引用参数(int &result)。 这是 C++ 的常规做法,不是 Qt 的怪癖。

三、唯一连接:为什么需要去重

D34 里同一个函数连两次,第二次被拦下(返回 False)。

这个行为在 Qt 里是默认的——同一个信号对同一个槽,重复 connect 不会重复触发。 要重复触发得显式用 Qt::UniqueConnection 的反面。

但要注意反过来的情况:两个不同的函数对象(比如两个 lambda)连同一个信号, Qt 认为是两个槽,会各调一次。 Python 里尤其容易踩:obj.method 每次取都生成新的绑定方法对象, 所以「看起来是同一个」其实不是。 这一点在动态语言里比 C++ 更容易出问题——C++ 的函数指针比较是地址比较, Python 的绑定方法比较是「同对象同函数」。

四、信号能不能带参数

能,而且必须声明类型(3.4.7 会专门试)。

声明 能传什么
clicked() 什么都不传
progress(int, int) 两个整数
frameReady(QImage) 一张图(要注册类型)

第三行的括号里那句话是坑:跨线程排队时,自定义类型必须先 qRegisterMetaType, 否则信号发得出去、槽收不到。 这个错的报错信息是「Cannot queue arguments of type 'QImage'」,看到这句就知道是没注册。

五、动手 Demo:二十行写完一个信号,把它该有的行为全试一遍

用一个回调列表实现 Signal,试三件事:按顺序调用、断开后不再调、重复连接被拦下。 这三件事就是 Qt 信号的全部行为。

class Signal(object):
    def __init__(self, unique=True):
        self._slots = []
        self._unique = unique

    def connect(self, fn):
        if self._unique and fn in self._slots:
            return False
        self._slots.append(fn)
        return True

    def emit(self, *a):
        for fn in list(self._slots):
            fn(*a)
        return len(self._slots)

clicked = Signal()
clicked.connect(lambda: print("A"))
clicked.connect(lambda: print("B"))
clicked.emit()          # A 然后 B

实跑输出:

--------------------------------------------------------------------
信号就是「我发生了,谁来听」
--------------------------------------------------------------------
**Qt 的信号没有任何魔法,它就是一个回调列表:有人登记,有人挨个调用。**
**把它写成二十行 Python,Qt 那一套就褪掉神秘感了。**

  三个槽登记后发一次信号,**按登记顺序调用,返回调用了 3 个**:
    调用顺序:A → B → C

  **顺序是「先进先调」,不是「随机」**——这一点在槽之间有依赖时很关键(比如先算后画)。

  全部断开后再发,调用 0 个,日志 `空`——**没人听了。**

  同一个函数连两次,第二次返回 **False**(被拦下)
  实际调用次数:**1**——这就是 Qt 的 `Qt::UniqueConnection`

**信号有三个必须知道的特性:**

  特性           含义                       用错会怎样
  --------------------------------------------------------------------------
  只发不管       发出后不知道有没有人接     没连接时静默丢弃,不报错
  一对多         一个信号可以连多个槽       顺序在前的先跑,别依赖反过来
  返回值被丢     槽的返回值拿不到           要结果就得用回调参数

  **第一行最容易让人迷惑**:写了 `emit` 却没反应,
  **先怀疑没连上,别怀疑信号没发**——Qt 里发不存在的信号会编译报错,但连不上的槽是运行期静默的。
  **第三行解释了一个常见需求**:想让槽「返回一个值」,Qt 的做法是传一个引用参数出去,
  而不是用返回值。

三个槽按登记顺序调:A → B → C。 顺序是确定的,这一点在有依赖时很重要(先算后画)。

全部 disconnect() 之后再发,调用 0 个,日志是空的。 这就是「只发不管」——不报错、不抛异常,什么都不发生。

同一个函数连两次,第二次返回 False,实际只调用 1 次。这正是 Qt 的默认行为。 但注意我用的是同一个函数对象 cb——如果写成两个 lambda: ...,就是两个对象,会调两次。 Python 里这个区别很隐蔽。

30 秒实验

  1. 把 unique 改成 False,同一个函数连两次,看是不是调两次。
  2. 在 emit 里加一句 print(len(self._slots)),看断开前后这个数怎么变。
  3. 让某个槽抛异常,看后面的槽还执行不执行——这个行为决定了你要不要在槽里加 try。

常见误区

  1. 以为信号发了就一定有人收到。 没连接时静默丢弃,不报错、不抛异常、不返回失败。
  2. 以为 obj.method 每次都是同一个槽。 Python 里每次取都生成新的绑定方法,Qt 认成两个槽, 会调两次。
  3. 以为槽能返回值。 Qt 的槽不返回业务值,要传结果得用引用参数。
自测三问(点开对答案) 1. 信号发出后,Qt 会检查有没有接收者吗? —— 不会。没连接时静默丢弃。所以「写了 emit 没反应」先查 connect。 2. 同一个槽连两次会触发几次? —— 默认一次(Qt 做了去重)。但 Python 里两个不同的 lambda 会被当成两个槽。 3. 为什么槽不能靠返回值传结果? —— 因为一个信号可能连多个槽,返回值该用哪个说不清。Qt 的做法是传引用参数出去。

下一节讲槽:信号是谁都能发,槽是「什么都能当」。

相关笔记