Qt/C++ 临时屏蔽控件信号(signal)的实用方法

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界面如下:

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);

3. Reference

  1. QSignalBlocker Class
  • 8
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
QT提供了对串口通信的支持,可以方便地实现串口调试助手。下面是一个简单的QT串口调试助手的设计思路和功能描述: 1. 创建一个串口类,用来发送和接收数据; 2. 创建两个文本框和几个按钮来实现数据的接收和发送,并且设置串口通信的参数; 3. 实现针对串口数据的解析; 4. 实现波形数据绘制和文件操作。 具体实现步骤如下: 1. 创建一个QT工程,使用widget作为默认控件; 2. 在widget.h中添加相关的函数和变量,指针申明; 3. 在widget.cpp中实现具体的函数功能; 4. 实现定时接收串口数据的功能。 以下是一个简单的QT串口调试助手的示例代码,其中使用QSerialPort类实现了串口通信功能,并使用QTimer类实现了定时接收串口数据的功能: ```cpp #include "widget.h" #include "ui_widget.h" #include <QtSerialPort/QSerialPort> #include <QtSerialPort/QSerialPortInfo> #include <QTimer> Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) { ui->setupUi(this); // 初始化串口 serialPort = new QSerialPort(this); serialPort->setBaudRate(QSerialPort::Baud9600); serialPort->setDataBits(QSerialPort::Data8); serialPort->setParity(QSerialPort::NoParity); serialPort->setStopBits(QSerialPort::OneStop); serialPort->setFlowControl(QSerialPort::NoFlowControl); // 显示可用串口列表 foreach (const QSerialPortInfo &info, QSerialPortInfo::availablePorts()) { ui->comboBox_serialPort->addItem(info.portName() + " : " + info.description()); } // 连接信号槽 connect(ui->pushButton_openSerial, SIGNAL(clicked()), this, SLOT(openSerialPort())); connect(ui->pushButton_closeSerial, SIGNAL(clicked()), this, SLOT(closeSerialPort())); connect(ui->pushButton_sendData, SIGNAL(clicked()), this, SLOT(sendData())); connect(serialPort, SIGNAL(readyRead()), this, SLOT(readData())); connect(&timer, SIGNAL(timeout()), this, SLOT(timeout())); // 设置定时器 timer.start(100); } Widget::~Widget() { delete ui; } void Widget::openSerialPort() { // 打开串口 serialPort->setPortName(ui->comboBox_serialPort->currentText().split(" : ").first()); if (serialPort->open(QIODevice::ReadWrite)) { ui->label_status->setText("串口已打开"); } else { ui->label_status->setText("打开串口失败"); } } void Widget::closeSerialPort() { // 关闭串口 if (serialPort->isOpen()) { serialPort->close(); ui->label_status->setText("串口已关闭"); } } void Widget::sendData() { // 发送数据 serialPort->write(ui->lineEdit_sendData->text().toUtf8()); } void Widget::readData() { // 读取数据 QByteArray data = serialPort->readAll(); ui->textEdit_receiveData->append(data); } void Widget::timeout() { // 定时读取数据 readData(); } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值