弄清 QObject —— 元对象模型的核心类

概述

QObject 类不仅是所有 Qt 对象的基类,还是“Qt 中的对象模型(Object Model)”的核心类。我们知道元对象模型最重要的功能就是“信号-槽”机制,该机制使 Qt 对象之间可以进行无缝的交互数据,进行通信。那么只要是 QObject 及其子类,都可以使用信号-槽机制。

QObject 如果有父对象,必须在同一个线程中

CPU的亲和性, 就是进程要在指定的 CPU 上尽量长时间地运行而不被迁移到其他处理器,也称为CPU关联性;再简单的点的描述就将制定的进程或线程绑定到相应的cpu上;在多核运行的机器上,每个CPU本身自己会有缓存,缓存着进程使用的信息,而进程可能会被OS调度到其他CPU上,如此,CPU cache命中率就低了,当绑定CPU后,程序就会一直在指定的cpu跑,不会由操作系统调度到其他CPU上,性能有一定的提高。

有一个知识点需要我们明白,就是一个 QObject 对象是具有线程亲和性(Thread Affinity)的。什么意思呢?就是这个类创建的对象收到信号或者事件的时候,处理信号的槽函数以及处理事件的某某函数也是和这个对象在一个线程中运行。说白了,我在哪个国家里收到命令,我就在哪个国家去执行任务。

如果调用 moveToThread() 函数把 QObject 对象移动到其他线程了,那原来的信号可就找不到你了,那么槽函数也就不会再执行。哦,对了,移动到其他线程也称为改变了这个对象的线程亲和性。默认情况下你在哪个线程创建 QObject 对象,那这个对象就属于哪个线程。查询 QObject 对象的亲和性用 thread() 函数。

因为 QObject 对象必须和父对象在同一个线程中,因此:

  • 如果两个 QObject 位于不同线程中,他们不能互相指定为父子关系,即调用 setParent() 将失败
  • 移动 QObject 到另一个线程中,其所有子对象也都自动移过去
  • 如果某个 QObject 对象指定了父对象,则调用 moveToThread() 函数将会失败
  • QThread::run() 中创建的 QObject 对象不能成为 QThread 对象的子对象,因为 QThread 不存在于 QThread::run() 中的线程。

QObject 类的成员变量是否为其子对象问题

根据官方文档说明,QObject 的成员变量默认不是其子对象,需要调用 setParent() 函数来手动指定,或者成员变量定义的时候构造函数中指明。否则的话,当 QObject 对象移动到其他线程后,成员变量可是不动的保留在旧的线程中。示例代码如下:

// Widget.h
Class Widget : public QWidget
{
    Q_OBJECT
public:
    Widget(QWidget *parent = 0);
    ~Widget();
private:
    QLabel *label;
}
// Widget.cpp
Widget::Widget(QWidget *parent) :
    Widget(parent)
{
    // 指定父对象的一种方法
    label = new QLabel(this);

    // 指定父对象的第二种方法
    label = new QLabel();
    label->setParent(this);
}

 

我们创建了一个 Widget 类,继承于 QWidget(QObject 的子类),那么 Widget 自然就是 QObject的子孙。里面有个成员变量 label,默认不是 Widget 的子对象。只有在 cpp 文件中定义时在构造函数中指定或者显示的调用 setParent() 才是 Widget 的子孙。

禁用拷贝构造函数、禁用赋值运算符

在“Qt 中的对象模型(Object Model)”中已经讲过这部分内容。只要记住用 QObject 的地方用指针比较好就可以了。

QObject 可以干什么?

和 QObject 类相关的有对象信息、信号槽、事件、计时器、线程和翻译。本节内容挑选一些不容易理解的点来讲,否则全部讲了会显得很累赘。

  • 安全的删除对象 - deleteLater() 函数
// 安全析构
void deleteLater()

这个 deleteLater() 函数主要是往事件循环队列里发送一条“把我删除”的信息。那为啥不直接删除呢?因为有些时候我虽然用完这个对象了,但其他组件可能还依赖它,那么等所有事儿都干完了,控制权会回到事件循环队列中,此时再安全的删除会更好。

启动事件循环的方法就是调用 QCoreApplication :: exec() 函数,我们在 main() 中见的多了。在启动事件循环前要是调用 deleteLater(),你一起动循环就会被删除,我是没见过有谁这样操作。从 Qt 4.8以后,你要是开辟一个线程,即使没在这个线程里开事件循环,也会安全删除。

既然是往事件循环队列里插入“把我删除”,那这个函数可以多次调用没关系,反正最终只会有一个去执行。这好比 sendEvent 是立马发送事件,而 postEvent 是发送到一个队列里,postEvent 可以多次调用。类似的还有绘图中的 repaint() 和 update(),都是一样的道理。

  • 有信号连接/断开时我们可以做些什么? - connectNotify等函数
// 有信号连接时自动调用的函数
virtual void connectNotify(const QMetaMethod &signal)
// 有信号断开时自动调用的函数
virtual void disconnectNotify(const QMetaMethod &signal)

// 示例
if(signal == QMetaMethod :: fromSignal(&MyObject :: valueChanged)){
    ....
}

这个 connectNotify() 函数啊,当有别人连接到 QObject 的信号时就会被自动的调用。当然这函数是虚函数,啥都没写,所以平时你也感觉不出什么。但是如果你重新实现了这个函数的话,只要有信号连接,你就可以在这里干些什么。比如别人连我时,我就打印“呀,有人连我了”,哈哈。

之所以这个用的不多是这种设计有悖面向对象思想,耦合了呗。所以一般不用,你也可以扩展思维玩出花样。

  • 调试利器,有多少人连我啊? - receivers() 函数
int receivers(const char *signal) const

// 示例
if (receivers(SIGNAL(valueChanged(QByteArray))) > 0) {
    ....
}

这个函数也不常用,有悖面向对象思想。但有时候工程浩大了,我们也不清楚有多少信号、槽函数和我得某某信号连接,这时候就可以调用该函数来确定。语法上,指定某信号用 SIGNAL 宏去指定。

  • 到底是谁在连我? - sender() 函数
QObject *sender() const

虽然这函数也是有悖面向对象思想,但用的地方还挺多,尤其是在 QObject 的槽函数中。比如有一个颜色面板,选择一个颜色然后设置文字颜色,肯定是信号槽形式。那我槽中怎么知道我点了哪个颜色呢?这时候在这个槽中就可以用 sender() 来确定是哪个人发出的信号传到我这里了,还是很实用的。

  • 我该怎么处理接收的事件? - event() 函数
virtual bool event(QEvent *e)

// 示例:调用父类函数
if (ev->type() == QEvent::PolishRequest) {
    // 只处理 PolishRequest 事件,其他不处理
    doThings();
    return true;
} else  if (ev->type() == QEvent::Show) {
    // 不仅处理 Show 事件,其他事件交给父类处理
    doThings2();
    QWidget::event(ev);
    return true;
}

// 确保剩余的事件交给父类处理
return QWidget::event(ev);

所有的事件发生后都可以用该函数来处理。虽然 Qt 已经在这个函数中写了一些代码,但同时 Qt 把这个函数搞成 virtual 虚函数意味着我们可以自定义处理的过程。

你要是处理了某事件,记得返回 ture。

另一个需要注意的是记得调用父类的 event() 函数来避免遗漏事件。比如 Qt 已经在该函数里写了处理鼠标、处理动画、处理绘图等事件,你要是写不全,最好是调用父类函数来处理剩余的事件。

有关 Qt 的事件处理会在以后的文章中讲解,QObject 的事件过滤器函数 eventFilter() 等就不赘述了。

  • 自发启动计时器 - startTimer() 函数
int startTimer(int interval, Qt::TimerType timerType = Qt::CoarseTimer)
int startTimer(std::chrono::milliseconds time, Qt::TimerType timerType = Qt::CoarseTimer)

// 示例
class MyObject : public QObject
  {
      Q_OBJECT

  public:
      MyObject(QObject *parent = 0);

  protected:
      void timerEvent(QTimerEvent *event);
  };

  MyObject::MyObject(QObject *parent)
      : QObject(parent)
  {
      startTimer(50);     // 50-millisecond timer
      startTimer(1000);   // 1-second timer
      startTimer(60000);  // 1-minute timer

      using namespace std::chrono;
      startTimer(milliseconds(50));
      startTimer(seconds(1));
      startTimer(minutes(1));

      // since C++14 we can use std::chrono::duration literals, e.g.:
      startTimer(100ms);
      startTimer(5s);
      startTimer(2min);
      startTimer(1h);
  }

  void MyObject::timerEvent(QTimerEvent *event)
  {
      qDebug() << "Timer ID:" << event->timerId();
  }

 

一般我们用 QTimer 类来创建个计时器,QTimer 本身就是 QObejct 的子类。其实 QObject 本身就自带计时器功能,调用 startTimer() 函数就启动了,而每一次的计时动作都会发出 QTimerEvent 事件。这样你可以重新实现 timerEvent() 函数来决定每一次的计时想要干什么。

终止计时器的函数是 killTimer()。

  • 国际化必备 - tr() 函数
QString tr(const char *sourceText, const char *disambiguation = Q_OBJECT, int n = Q_OBJECT)

// 示例
void MainWindow::createActions()
{
    QMenu *fileMenu = menuBar()->addMenu(tr("&File"));
    ...
}

 

这一部分要展开说挺多的,会在以后的文章专门讲解 Qt 的国际化翻译。这里只需记住将翻译的字符串用 tr() 函数包裹起来即可。

相关的宏

  • 启用 Meta-System 一系列功能:Q_OBJECT
  • 配置属性:Q_PROPERTY(...)
  • 禁止缩窄转换:QT_NO_NARROWING_CONVERSIONS_IN_CONNECT
  • 设置类的额外信息:Q_CLASSINFO(Name, Value)
  • 禁用拷贝构造函数、赋值操作符:Q_DISABLE_COPY(Class)
  • 第三方使用信号槽:Q_EMIT
  • 第三方使用信号:Q_SIGNAL/Q_SIGNALS
  • 第三方使用槽函数:Q_SLOT/Q_SLOTS
  • 向 Meta-System 注册枚举:Q_ENUM( ...)/Q_ENUM_NS( ...)
  • 向 Meta-System 注册flag:Q_FLAG( ...)/Q_FLAG_NS( ...)
  • 轻量级 Q_OBJECT:Q_GADGET
  • 向 Qt 标记实现了哪些接口:Q_INTERFACES( ...)
  • 向 Meta-System 注册方法函数:Q_INVOKABLE
  • 给某个函数加个版本:Q_REVISION
  • 设置对象名:Q_SET_OBJECT_NAME(Object)

以上就是有关 QObject 类的心得体会,还有一些内容涉及的面太广,会在以后的专题文章中讲解。

 

原文链接:https://zhuanlan.zhihu.com/p/43598693

posted on 2022-10-09 14:36  斗战胜佛美猴王  阅读(175)  评论(0编辑  收藏  举报