1. 背景
在使用Qt的控件时,我们大概率会使用Qt的信号与槽(signal-slot)的机制来实现自己的UI交互逻辑。由于Qt内置控件的信号种类是有限的,我们常常会遇到如下窘境:
以常见的QComboBox
控件为例,它提供了一个非常实用的信号void currentIndexChanged(int index)
,每当该控件中的当前选项通过用户交互或以编程方式更改时,都会发送此信号。注意到,当我们调用void QComboBox::setCurrentIndex(int index)
时,也会导致该信号被触发。那么,假设有这么一个场景,我们使用了一个只含有两个选项的QComboBox
(0:Item1和1:Item2),每当用户变更这个选项时,我们会在对应的slot中触发相应的业务逻辑,例如有下面的槽函数:
void MainWindow::on_testCbB_currentIndexChanged(int index)
{
qDebug() << "do something for new index: " << index;
}
同时,我们还可能通过程序来设置QComboBox
选项(例如一个很典型的使用场景,即从配置文件中恢复控件的当前选项),并且不希望它进行任何业务处理。为了简化说明,本文以点击一个按钮为例来模拟通过程序设置的场景,按钮的槽函数如下:
void MainWindow::on_testBtn_clicked()
{
ui->testCbB->setCurrentIndex(ui->testCbB->currentIndex() == 0 ? 1 : 0);
}
在槽函数中,我们简单地在QComboBox
的两个选项之间进行切换,每点击一次切换一下。
最终的UI界面如下:
我手动从QComboBox
中选择了Item2,然后点击了一次Switch Item按钮,毫无疑问,最终会得到下面的日志输出:
do something for new index: 1
do something for new index: 0
但这并不是我们想要的结果,我们真正想要的是只有第一行日志的输出。
2. 解决方法
我们首先能想到的一种解决方案就是使用disconnect()
函数来断开信号与槽的连接。为了不影响其他控件的响应,我们就需要在每次断开时,指定被断开的信号和槽,以及对应的发送者和接收者。并且在完成操作后重新连接。这一波操作太不优雅了。
实际上,或许Qt的开发人员考虑到了这么一种使用场景,所以在所有控件的基类QObject
中,提供了bool QObject::blockSignals(bool block)
函数,官方对该函数的说明如下:
If block is true, signals emitted by this object are blocked (i.e., emitting a signal will not invoke anything connected to it). If block is false, no such blocking will occur.
The return value is the previous value of signalsBlocked().
Note that the destroyed() signal will be emitted even if the signals for this object have been blocked.
Signals emitted while being blocked are not buffered.
简而言之,我们可以通过这个接口暂时屏蔽掉指定对象的所有信号(destroyed()
信号除外),并且在信号屏蔽期间产生的信号都不会被缓存起来(以防止恢复时还是触发了槽函数)。
很好,这个函数完美解决了这个场景下的问题。我们使用该函数改造一下按钮控件的clicked槽函数:
void MainWindow::on_testBtn_clicked()
{
const bool wasBlocked = ui->testCbB->blockSignals(true);
ui->testCbB->setCurrentIndex(ui->testCbB->currentIndex() == 0 ? 1 : 0);
ui->testCbB->blockSignals(wasBlocked);
}
然后点击几次按钮,好了,UI发生了变化,但是并不会产生日志了(即我们为QComboBox
连接的on_testCbB_currentIndexChanged()
槽函数不会被调用了)。
到了这里,似乎问题已经被彻底解决了。不,再仔细看看,是不是发现这玩意儿跟临界区有那么一点点相似?在我们这个例子中,按钮的处理逻辑非常简单。然而,在实际项目中,处理逻辑肯定比这个复杂得多,一旦程序流程变得复杂,我们就很容易犯错,比如,我们可能调用了ui->testCbB->blockSignals(true)
对该控件的信号进行了屏蔽,但是在后续的某个分支处理后或者发生了运行时异常,最终使得我们并没有解除屏蔽就退出了流程,那么该控件后续的所有的信号就都失效了。(这像极了我们使用互斥锁等多线程同步机制时的场景)因此,我们自然而然就想到可以使用RAII( Resource Acquisition Is Initialization)手法来改进这个实现。
等等,先别自己动手造轮子,其实Qt已经提供了这种实现手法,即QSignalBlocker
。这个类在Qt 5.3中引入。用法很简单,还是以我们这个场景为例:
void MainWindow::on_testBtn_clicked()
{
QSignalBlocker blocker(ui->testCbB);
// signal blocked
ui->testCbB->setCurrentIndex(ui->testCbB->currentIndex() == 0 ? 1 : 0);
} // signal resumed
QSignalBlocker blocker(ui->testCbB)
等价于下列代码:
const bool wasBlocked = ui->testCbB->blockSignals(true);
// signals blocked
// do something ...
ui->testCbB->blockSignals(wasBlocked);