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

界面编程的九成代码都是「控件 A 变了,通知控件 B」。 一个按钮点三次,两个视图各更新三次——这就是标准信号槽的全部。

📝 本节和前面几节的关系

上一节讲了实现原理,这一节回到用法,做最小的一个界面。 用 Python 模拟,因为沙箱里没有 Qt;但信号、槽、事件循环三层结构一个不少。 把它套到 Qt 上,只是把 Signal 换成 signals: 声明,逻辑完全一样。

一、三层结构

事件循环、分发器、槽——三层,缺一不可。

层 做什么 卡住会怎样
事件队列 收系统消息 界面完全不响应
分发器 把消息转成信号 点了没反应
槽 处理信号 界面假死

三行是同一件事的三个位置,排查「界面卡住」就从这里往下切: 队列满 → 系统层面卡;分发器慢 → 点了没反应;槽慢 → 假死。 D39 的按钮和标签之间只隔了一个信号,正好是最小的三层样例。

二、「值没变就不发」这一句

D39 的 setText 里有一句比较,这是标准做法。

写法 后果
不比较,每次都 emit 无谓重绘,可能死循环
比较后再 emit 只在真变化时通知

第二行能防死循环:如果两个控件互相绑定(A 变通知 B,B 变通知 A),不比较就会无限来回发。 Qt 自己的 setText 内部也做了这个比较,所以你写不写都行;但自定义 setter 一定要写。

三、多视图怎么同步

D39 里两个视图都连了同一个信号,所以都更新。

这就是 MVC 里「模型一变,所有视图都刷新」的最小实现:

角色 对应什么
模型 那个 Label(持有数据)
视图 两个 lambda(只管显示)
控制器 Button(改数据)

三个角色之间只有一条信号线,模型不知道有几个视图——这就是解耦。 监控客户端的多画面预览就是这么做的:一路信号,N 个窗口订阅。

四、什么时候不该用信号槽

它也有不适用的时候,别当成万能药。

场景 该用什么
同步要结果 直接函数调用
高频调用(每帧) 直接调用,省掉派发开销
一对一的强依赖 直接调用,读代码更清楚

第二行值得解释:30 fps 每帧都走一遍队列,派发开销不大但会打乱时序, 而且调试时堆栈会变得很深。 所以「每帧」的东西通常直接调用,只有「状态变化」才用信号。

五、动手 Demo:按钮点三次,两个视图各更新三次

用 Signal 做一条 Chain:按钮 clicked → 标签 setText → 标签 changed → 两个视图。 中间那一层 setText 带「值变了才发」的判断,这是标准写法。

class Label(QObject):
    changed = Signal()

    def setText(self, s):
        if s != self.text:        # 值没变就不发
            self.text = s
            self.changed.emit(s)

lb.changed.connect(lambda s: views.append("视图1: " + s))
lb.changed.connect(lambda s: views.append("视图2: " + s))
bt.clicked.connect(lambda: lb.setText("已点 %d 次" % n))

bt.clicked.emit()
bt.clicked.emit()
bt.clicked.emit()

实跑输出:

--------------------------------------------------------------------
一个按钮一个标签,标准信号槽怎么搭
--------------------------------------------------------------------
**界面编程的九成代码都是「控件 A 变了,通知控件 B」。**这就是信号槽要解决的全部问题。

  点三次按钮,两个视图各收到三次:
    视图1: 已点 1 次
    视图2: 已点 1 次
    视图1: 已点 2 次
    视图2: 已点 2 次
    视图1: 已点 3 次
    视图2: 已点 3 次

  **注意「值没变就不发」这个判断**:`setText` 里先比较,相同就不 emit。
  **少了这一句会有两个后果**:一是不必要的重绘,二是「A 改成 B、B 又改回 A」时会来回发,
  如果两边都这样连,就成死循环。**Qt 的 setText 内部也做了同样的比较。**

**事件循环就是「不停地取消息、发信号」**:

  层           做什么             卡住会怎样
  --------------------------------------------------
  事件队列     收系统消息         界面完全不响应
  分发器       把消息转成信号     点了没反应
  槽           处理信号           界面假死

  **三行是同一件事的三个位置**,排查「界面卡住」就从这里往下切。

点三次,两个视图各收到三次,交替出现。顺序是「视图1 先、视图2 后」——按连接顺序。 这个顺序不是巧合:信号内部就是列表,从头遍历。想让某个视图先更新,就让它先连。

那句「值没变就不发」在三次点击里没起作用(值每次都变),但它防的是另一种情况: 同一句话被设两次。 那种情况下不比较就会多发一次 changed,如果下游有重绘或者写日志,就是无谓开销。

三层结构在这段代码里很清楚:bt.clicked.emit() 是消息,lambda 是分发,setText 是槽。 Qt 里一模一样,只是多了个事件循环在背后转。

30 秒实验

  1. 把 setText 里的比较去掉,再点三次,看调用次数变不变——值每次都不一样,所以不变。
  2. 改成连续两次设同一个值,看有没有比较的差别。
  3. 把两个视图的连接顺序调换,看输出顺序变不变。

常见误区

  1. 以为一个信号只能连一个槽。 能连任意多个,按连接顺序依次调用。
  2. 以为 setText 里不用比较。 Qt 自带的确实做了比较,但你写的自定义 setter 没有, 会引发无谓重绘甚至死循环。
  3. 以为所有通信都该用信号槽。 每帧的高频调用、需要同步取结果的,直接调用更清楚也更快。
自测三问(点开对答案) 1. 事件循环的三层是什么?哪一层卡住会假死? —— 事件队列、分发器、槽。槽卡住会假死,队列卡住是系统层面不响应。 2. 为什么 setText 里要判断「值变没变」? —— 防止两个控件互相绑定时无限来回发信号,也避免无谓重绘。 3. 多个视图订阅同一个信号,模型需要知道有几个视图吗? —— 不需要。模型只管 emit,谁订阅它不知道——这就是解耦。

下一节做自定义信号:内置的不够用时,自己的信号怎么声明、参数怎么带。

相关笔记