Qt 学习之路 2(19):事件的接受与忽略

版本:

  1. 2012-09-29
  2. 2013-04-23 更新有关accept()ignore()函数的相关内容。
  3. 2013-12-02 增加有关accept()ignore()函数的示例。

上一章我们介绍了有关事件的相关内容。我们曾经提到,事件可以依情况接受和忽略。现在,我们就来了解下有关事件的更多的知识。

首先来看一段代码:

//!!! Qt5
// ---------- custombutton.h ---------- //
class CustomButton : public QPushButton
{
    Q_OBJECT
public:
    CustomButton(QWidget *parent = 0);
private:
    void onButtonCliecked();
};

// ---------- custombutton.cpp ---------- //
CustomButton::CustomButton(QWidget *parent) :
    QPushButton(parent)
{
    connect(this, &CustomButton::clicked,
            this, &CustomButton::onButtonCliecked);
}

void CustomButton::onButtonCliecked()
{
    qDebug() << "You clicked this!";
}

// ---------- main.cpp ---------- //
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    CustomButton btn;
    btn.setText("This is a Button!");
    btn.show();

    return a.exec();
}

这是一段简单的代码,经过我们前面一段时间的学习,我们已经能够知道这段代码的运行结果:点击按钮,会在控制台打印出“You clicked this!”字符串。这是我们前面介绍过的内容。下面,我们向CustomButton类添加一个事件函数:

// CustomButton
...
protected:
    void mousePressEvent(QMouseEvent *event);
...

// ---------- custombutton.cpp ---------- //
...
void CustomButton::mousePressEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        qDebug() << "left";
    } else {
        QPushButton::mousePressEvent(event);
    }
}
...

我们重写了CustomButtonmousePressEvent()函数,也就是鼠标按下。在这个函数中,我们判断如果鼠标按下的是左键,则打印出来“left”字符串,否则,调用父类的同名函数。编译运行这段代码,当我们点击按钮时,“You clicked this!”字符串不再出现,只有一个“left”。也就是说,我们把父类的实现覆盖掉了。由此可以看出,父类QPushButtonmousePressEvent()函数中肯定发出了clicked()信号,否则的话,我们的槽函数怎么会不执行了呢?这暗示我们一个非常重要的细节:当重写事件回调函数时,时刻注意是否需要通过调用父类的同名函数来确保原有实现仍能进行!比如我们的CustomButton类,如果像我们这么覆盖函数,clicked()信号永远不会发生,你连接到这个信号的槽函数也就永远不会被执行。这个错误非常隐蔽,很可能会浪费你很多时间才能找到。因为这个错误不会有任何提示。这一定程度上说,我们的组件“忽略”了父类的事件,但这更多的是一种违心之举,一种错误。

通过调用父类的同名函数,我们可以把 Qt 的事件传递看成链状:如果子类没有处理这个事件,就会继续向其父类传递。Qt 的事件对象有两个函数:accept()ignore()。正如它们的名字一样,前者用来告诉 Qt,这个类的事件处理函数想要处理这个事件;后者则告诉 Qt,这个类的事件处理函数不想要处理这个事件。在事件处理函数中,可以使用isAccepted()来查询这个事件是不是已经被接收了。具体来说:如果一个事件处理函数调用了一个事件对象的accept()函数,这个事件就不会被继续传播给其父组件;如果它调用了事件的ignore()函数,Qt 会从其父组件中寻找另外的接受者。

事实上,我们很少会使用accept()ignore()函数,而是像上面的示例一样,如果希望忽略事件(所谓忽略,是指自己不想要这个事件),只要调用父类的响应函数即可。记得我们曾经说过,Qt 中的事件都是 protected 的,因此,重写的函数必定存在着其父类中的响应函数,所以,这个方法是可行的。为什么要这么做,而不是自己去手动调用这两个函数呢?因为我们无法确认父类中的这个处理函数有没有额外的操作。如果我们在子类中直接忽略事件,Qt 会去寻找其他的接收者,该子类的父类的操作会被忽略(因为没有调用父类的同名函数),这可能会有潜在的危险。为了避免自己去调用accept()ignore()函数,而是尽量调用父类实现,Qt 做了特殊的设计:事件对象默认是 accept 的,而作为所有组件的父类QWidget的默认实现则是调用ignore()。这么一来,如果你自己实现事件处理函数,不调用QWidget的默认实现,你就等于是接受了事件;如果你要忽略事件,只需调用QWidget的默认实现。这一点我们前面已经说明。下面可以从代码级别来理解这一点,我们可以查看一下QWidgetmousePressEvent()函数的实现:

//!!! Qt5
void QWidget::mousePressEvent(QMouseEvent *event)
{
    event->ignore();
    if ((windowType() == Qt::Popup)) {
        event->accept();
        QWidget* w;
        while ((w = QApplication::activePopupWidget()) && w != this){
            w->close();
            if (QApplication::activePopupWidget() == w)
                w->hide(); // hide at least
        }
        if (!rect().contains(event->pos())){
            close();
        }
    }
}

这段代码在 Qt4 和 Qt5 中基本一致(区别在于activePopupWidget()一行,Qt4 的版本是qApp->activePopupWidget())。注意函数的第一个语句:event->ignore(),如果子类都没有重写这个函数,Qt 会默认忽略这个事件,继续寻找下一个事件接收者。如果我们在子类的mousePressEvent()函数中直接调用了accept()或者ignore(),而没有调用父类的同名函数,QWidget::mousePressEvent()函数中关于Popup判断的那段代码就不会被执行,因此可能会出现默认其妙的怪异现象。

针对accept()ignore(),我们再来看一个例子:

class CustomButton : public QPushButton
{
    Q_OBJECT
public:
    CustomButton(QWidget *parent) : QPushButton(parent)
    {
    }
protected:
    void mousePressEvent(QMouseEvent *event)
    {
        qDebug() << "CustomButton";
    }
};

class CustomButtonEx : public CustomButton
{
    Q_OBJECT
public:
    CustomButtonEx(QWidget *parent) : CustomButton(parent)
    {
    }
protected:
    void mousePressEvent(QMouseEvent *event)
    {
        qDebug() << "CustomButtonEx";
    }
};

class CustomWidget : public QWidget
{
    Q_OBJECT
public:
    CustomWidget(QWidget *parent) : QWidget(parent)
    {
    }
protected:
    void mousePressEvent(QMouseEvent *event)
    {
        qDebug() << "CustomWidget";
    }
};

class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    MainWindow(QWidget *parent = 0) : QMainWindow(parent)
    {
        CustomWidget *widget = new CustomWidget(this);
        CustomButton *cbex = new CustomButton(widget);
        cbex->setText(tr("CustomButton"));
        CustomButtonEx *cb = new CustomButtonEx(widget);
        cb->setText(tr("CustomButtonEx"));
        QVBoxLayout *widgetLayout = new QVBoxLayout(widget);
        widgetLayout->addWidget(cbex);
        widgetLayout->addWidget(cb);
        this->setCentralWidget(widget);
    }
protected:
    void mousePressEvent(QMouseEvent *event)
    {
        qDebug() << "MainWindow";
    }
};

这段代码在一个MainWindow中添加了一个CustomWidget,里面有两个按钮对象:CustomButtonCustomButtonEx。每一个类都重写了mousePressEvent()函数。运行程序点击 CustomButtonEx,结果是

CustomButtonEx

这是因为我们重写了鼠标按下的事件,但是并没有调用父类函数或者显式设置accept()ignore()。下面我们在CustomButtonExmousePressEvent()第一行增加一句event->accept(),重新运行,发现结果不变。正如我们前面所说,QEvent默认是accept的,调用这个函数并没有什么区别。然后我们将CustomButtonExevent->accept()改成event->ignore()。这次运行结果是

CustomButtonEx
CustomWidget

ignore()说明我们想让事件继续传播,于是CustomButtonEx的父组件CustomWidget也收到了这个事件,所以输出了自己的结果。同理,CustomWidget又没有调用父类函数或者显式设置accept()ignore(),所以事件传播就此打住。这里值得注意的是,CustomButtonEx的事件传播给了父组件CustomWidget,而不是它的父类CustomButton事件的传播是在组件层次上面的,而不是依靠类继承机制。

接下来我们继续测试,在CustomWidgetmousePressEvent()中增加QWidget::mousePressEvent(event)。这次的输出是

CustomButtonEx
CustomWidget
MainWindow

如果你把QWidget::mousePressEvent(event)改成event->ignore(),结果也是一样的。这正如我们前面说的,QWidget的默认是调用event->ignore()

在一个特殊的情形下,我们必须使用accept()ignore()函数,那就是窗口关闭的事件。对于窗口关闭QCloseEvent事件,调用accept()意味着 Qt 会停止事件的传播,窗口关闭;调用ignore()则意味着事件继续传播,即阻止窗口关闭。回到我们前面写的简单的文本编辑器。我们在构造函数中添加如下代码:

//!!! Qt5
...
textEdit = new QTextEdit(this);
setCentralWidget(textEdit);
connect(textEdit, &QTextEdit::textChanged, [=]() {
    this->setWindowModified(true);
});

setWindowTitle("TextPad [*]");
...

void MainWindow::closeEvent(QCloseEvent *event)
{
    if (isWindowModified()) {
        bool exit = QMessageBox::question(this,
                                      tr("Quit"),
                                      tr("Are you sure to quit this application?"),
                                      QMessageBox::Yes | QMessageBox::No,
                                      QMessageBox::No) == QMessageBox::Yes;
        if (exit) {
            event->accept();
        } else {
            event->ignore();
        }
    } else {
        event->accept();
    }
}

setWindowTitle()函数可以使用 [] 这种语法来表明,在窗口内容发生改变时(通过setWindowModified(true)函数通知),Qt 会自动在标题上面的 [] 位置替换成 * 号。我们使用 Lambda 表达式连接QTextEdit::textChanged()信号,将windowModified设置为 true。然后我们需要重写closeEvent()函数。在这个函数中,我们首先判断是不是有过修改,如果有,则弹出询问框,问一下是否要退出。如果用户点击了“Yes”,则接受关闭事件,这个事件所在的操作就是关闭窗口。因此,一旦接受事件,窗口就会被关闭;否则窗口继续保留。当然,如果窗口内容没有被修改,则直接接受事件,关闭窗口。

149 Comments

  1. Anonymous 2012年10月2日
    • DevBean 2012年10月2日
      • Anonymous 2012年10月2日
      • li 2016年5月1日
        • li 2016年5月1日
  2. tony 2012年10月6日
    • DevBean 2012年10月6日
      • xuefu 2013年3月14日
        • 豆子 2013年3月15日
          • 渡世白玉 2013年4月16日
  3. longchisihai 2012年12月30日
    • DevBean 2012年12月31日
  4. longchisihai 2012年12月31日
    • DevBean 2012年12月31日
      • xuefu 2013年3月15日
  5. cwq 2013年1月25日
    • 豆子 2013年1月25日
  6. JiaPan 2013年1月30日
    • 豆子 2013年1月31日
      • JiaPan 2013年1月31日
        • great 2013年2月1日
        • 豆子 2013年2月5日
    • 春云者 2017年5月6日
  7. qingxp9 2013年4月22日
    • 豆子 2013年4月23日
      • qingxp9 2013年4月23日
        • 豆子 2013年4月24日
          • qingxp9 2013年4月26日
          • 豆子 2013年4月27日
        • hysteria 2013年12月2日
          • Alien You 2017年4月30日
  8. Jakes 2013年4月27日
    • 豆子 2013年4月28日
  9. guiji16 2013年5月30日
    • Rice 2013年9月23日
      • 豆子 2013年9月24日
  10. enockipp 2013年9月15日
  11. hysteria 2013年12月1日
    • hysteria 2013年12月1日
      • 豆子 2013年12月2日
    • 豆子 2013年12月2日
  12. 付强 2013年12月3日
    • 付强 2013年12月3日
      • 付强 2013年12月3日
      • 豆子 2013年12月3日
  13. 付强 2013年12月3日
    • 付强 2013年12月3日
  14. xiaoziwen 2013年12月18日
    • 豆子 2013年12月18日
  15. sgnannan 2014年1月8日
    • 豆子 2014年1月9日
  16. 2014年2月18日
    • 豆子 2014年2月18日
  17. 2014年2月19日
    • 豆子 2014年2月19日
  18. 大灰狼嘎嘎 2014年3月2日
  19. Const_Lin 2014年3月7日
    • 豆子 2014年3月10日
  20. Const_Lin 2014年3月7日
    • 豆子 2014年3月10日
  21. 橘子 2014年4月1日
    • 豆子 2014年4月2日
  22. ksn13 2014年5月8日
    • 豆子 2014年5月8日
  23. darkhandz 2014年5月10日
  24. darkhandz 2014年5月11日
  25. pubemail 2014年5月31日
    • pubemail 2014年5月31日
      • 豆子 2014年6月3日
  26. rainc 2014年6月14日
    • 豆子 2014年6月18日
      • shandy 2016年10月17日
  27. Habor 2014年7月17日
  28. Geek 2014年9月16日
    • 豆子 2014年9月18日
  29. jmgs 2014年12月30日
  30. 双人鱼 2015年1月28日
  31. xiaomxxx 2015年3月9日
    • 豆子 2015年3月9日
  32. 伶仃听雨客 2015年4月13日
    • 豆子 2015年4月21日
      • Lucky 2015年4月27日
        • 豆子 2015年6月3日
  33. laoreja 2015年4月14日
  34. laoreja 2015年4月14日
  35. xie 2015年5月13日
    • 豆子 2015年6月3日
  36. Ycoronene 2015年9月2日
    • 豆子 2015年9月2日
      • shandy 2016年10月17日
  37. markzzz 2015年9月10日
    • 豆子 2015年9月10日
      • markzzz 2015年9月11日
  38. 王先先 2015年9月23日
  39. Crazydoudou 2015年10月29日
  40. 孙江涛 2015年11月12日
  41. hlx1996 2015年11月25日
    • 豆子 2015年11月26日
      • shandy 2016年10月17日
      • shandy 2016年10月17日
        • 豆子 2016年10月17日
  42. 善良超哥哥 2015年12月3日
  43. 善良超哥哥 2015年12月3日
    • 善良超哥哥 2015年12月3日
  44. yeyiliang 2015年12月10日
  45. 2015年12月11日
    • 2015年12月11日
  46. lipanlin 2015年12月13日
  47. 12 2016年1月16日
    • 豆子 2016年1月17日
  48. SHIHUA 2016年4月3日
    • SHIHUA 2016年4月3日
      • 豆子 2016年4月6日
  49. 小新 2016年5月16日
    • 普法居士 2016年5月19日
  50. 普法居士 2016年5月19日
  51. 司徒 2016年8月3日
    • 豆子 2016年8月3日
      • 小新 2016年8月3日
        • 豆子 2016年8月3日
  52. 瞬间 2016年9月6日
    • 豆子 2016年9月8日
  53. bin 2016年9月7日
    • 豆子 2016年9月8日
  54. William 2016年10月21日
  55. firephoenix 2016年12月6日
  56. firephoenix 2016年12月6日
  57. Zed 2016年12月8日
    • 豆子 2016年12月10日
  58. Ollog 2017年2月14日
  59. 暴雪 2017年4月6日
    • 豆子 2017年4月8日
  60. Alien You 2017年4月30日
    • 豆子 2017年5月6日
  61. 小浩 2017年5月5日
  62. angon 2017年7月24日
  63. ben 2017年8月8日
  64. ben 2017年8月8日
  65. yhw 2018年8月10日
    • 豆子 2018年8月11日
  66. 无忧无虑的兔兔 2019年5月12日
    • 无忧无虑的兔兔 2019年5月12日
  67. zz 2019年7月26日
    • zz 2019年7月26日
  68. ct 2019年8月13日
    • 豆子 2019年8月16日
  69. 燎原 2019年8月15日

Leave a Reply