Search This Blog

2013-05-14

Qt信号槽机制简介

目录  [+]
以前稍微维护过一个Qt客户端程序,对信号槽机制略有了解。之前MacOSX上的那个QtCreator有点问题,像emit这样的宏都链接不到相应的头文件,我是在网上确认的;跨线程槽函数的调用只是猜测,没看源码也没有别的验证;当时做了一点记录。现在,我在Linux上安装了Qt5.0.2,包括源码,看起来比较方便,看了一部分源码,做了一点debug,整理出这篇文章,算是个了结;另外,我在用KDE,或许会用Qt写一些代码,整理一下还是有益的。
如果只是大略把握,那信号槽机制并不复杂,可以轻松愉快地去了解。
本文没有做深入分析,只是用比较简单直观的方法探究一下,展示的是大概的面貌,因为我不想写一篇很长很吓人的文章,有了大略的把握,再加上一定的方法,以后想深入了解也方便。本文假设读者了解信号槽的写法。

1 Q_OBJECT

The Q_OBJECT macro must appear in the private section of a class definition that declares its own signals and slots or that uses other services provided by Qt's meta-object system.
这是宏,在qobjectdefs.h定义。
/* qmake ignore Q_OBJECT */
#define Q_OBJECT \
public: \
    Q_OBJECT_CHECK \
    static const QMetaObject staticMetaObject; \
    virtual const QMetaObject *metaObject() const; \
    virtual void *qt_metacast(const char *); \
    QT_TR_FUNCTIONS \
    virtual int qt_metacall(QMetaObject::Call, int, void **); \
private: \
    Q_DECL_HIDDEN_STATIC_METACALL static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **); \
    struct QPrivateSignal {};
这里添加的方法是在相应的moc_xyz.cpp实现的。

2 signals, slots, emit

这些都是宏,在qobjectdefs.h定义。
# if defined(QT_NO_KEYWORDS)
#  define QT_NO_EMIT
# else
#   ifndef QT_NO_SIGNALS_SLOTS_KEYWORDS
#     define slots
#     define signals public
#   endif
# endif
# define Q_SLOTS
# define Q_SIGNALS public
# define Q_PRIVATE_SLOT(d, signature)
# define Q_EMIT
#ifndef QT_NO_EMIT
# define emit
#endif
emit跟slots都是空,增加代码可读性而已。signals定义为了public,因为信号函数需要在外部访问。

3 SIGNAL, SLOT

SIGNAL跟SLOT没法在源码点开,在Qt5.0.2/5.0.2/Src/qtbase/src/corelib/kernel,"grep SIGNAL *.h"就能找到。
这些都是宏,在qobjectdefs.h定义。
Pre[-]
Q_CORE_EXPORT const char *qFlagLocation(const char *method);

#ifndef QT_NO_META_MACROS
#ifndef QT_NO_DEBUG
# define QLOCATION "\0" __FILE__ ":" QT_STRINGIFY(__LINE__)
# ifndef QT_NO_KEYWORDS
#  define METHOD(a)   qFlagLocation("0"#a QLOCATION)
# endif
# define SLOT(a)     qFlagLocation("1"#a QLOCATION)
# define SIGNAL(a)   qFlagLocation("2"#a QLOCATION)
#else
# ifndef QT_NO_KEYWORDS
#  define METHOD(a)   "0"#a
# endif
# define SLOT(a)     "1"#a
# define SIGNAL(a)   "2"#a
#endif // QT_NO_DEBUG
#define QMETHOD_CODE  0                        // member type codes
#define QSLOT_CODE    1
#define QSIGNAL_CODE  2
#endif // QT_NO_META_MACROS

qglobal.h
#define QT_STRINGIFY2(x) #x
#define QT_STRINGIFY(x) QT_STRINGIFY2(x)

SIGNAL跟SLOT产生字符串,这跟connect函数的定义相符。

4 信号槽函数的连接

4.1 QObject::connect

bool connect ( const QObject * sender, const char * signal, const QObject * receiver, const char * method, Qt::ConnectionType type = Qt::AutoConnection )
这是绑定信号槽的方法。

4.2 enum Qt::ConnectionType

This enum describes the types of connection that can be used between signals and slots. In particular, it determines whether a particular signal is delivered to a slot immediately or queued for delivery at a later time.
最常用的几个选项:
ConstantValueDescription
Qt::AutoConnection0(default) If the signal is emitted from a different thread than the receiving object, the signal is queued, behaving as Qt::QueuedConnection. Otherwise, the slot is invoked directly, behaving as Qt::DirectConnection. The type of connection is determined when the signal is emitted.
Qt::DirectConnection1The slot is invoked immediately, when the signal is emitted.
Qt::QueuedConnection2The slot is invoked when control returns to the event loop of the receiver's thread. The slot is executed in the receiver's thread.
槽函数可以在不同的线程被执行,默认是在接收者所在线程执行。

5 槽函数如何被调用

emit一个信号函数之后,槽函数是怎么被调用到的呢?由于槽函数可以在不同的线程被执行,不可能通过直接回调来实现。
我打开的是Qt的AddressBook例子,在addresswidget.cpp的AddressWidget::addEntry()方法加了断点,debug。

5.1 mainwindows.cpp # MainWindow::createMenus()

addAct = new QAction(tr("&Add Entry..."), this);
toolMenu->addAction(addAct);
connect(addAct, SIGNAL(triggered()), addressWidget, SLOT(addEntry()));
triggered()是信号函数,addEntry()是槽函数,做了绑定。

5.2 调用堆栈

我在菜单点了"Add Entry",触发了断点。先看看堆栈,做个大略了解。
Pre[-]
0    AddressWidget::addEntry    addresswidget.cpp    64
1    AddressWidget::qt_static_metacall    moc_addresswidget.cpp    89
2    QMetaObject::activate(QObject*, int, int, void**)    libQt5Core.so.5
3    QAction::triggered(bool)    libQt5Widgets.so.5
4    QAction::activate(QAction::ActionEvent)    libQt5Widgets.so.5
5    ??    libQt5Widgets.so.5
6    ??    libQt5Widgets.so.5
7    QMenu::mouseReleaseEvent(QMouseEvent*)    libQt5Widgets.so.5
8    QWidget::event(QEvent*)    libQt5Widgets.so.5
9    QMenu::event(QEvent*)    libQt5Widgets.so.5
10    QApplicationPrivate::notify_helper(QObject*, QEvent*)    libQt5Widgets.so.5
11    QApplication::notify(QObject*, QEvent*)    libQt5Widgets.so.5
12    QCoreApplication::notifyInternal(QObject*, QEvent*)    libQt5Core.so.5
13    QApplicationPrivate::sendMouseEvent(QWidget*, QMouseEvent*, QWidget*, QWidget*, QWidget**, QPointer<QWidget>&, bool)    libQt5Widgets.so.5
14    ??    libQt5Widgets.so.5
15    ??    libQt5Widgets.so.5
16    QApplicationPrivate::notify_helper(QObject*, QEvent*)    libQt5Widgets.so.5
17    QApplication::notify(QObject*, QEvent*)    libQt5Widgets.so.5
18    QCoreApplication::notifyInternal(QObject*, QEvent*)    libQt5Core.so.5
19    QGuiApplicationPrivate::processMouseEvent(QWindowSystemInterfacePrivate::MouseEvent*)    libQt5Gui.so.5
20    QGuiApplicationPrivate::processWindowSystemEvent(QWindowSystemInterfacePrivate::WindowSystemEvent*)    libQt5Gui.so.5
21    QWindowSystemInterface::sendWindowSystemEventsImplementation(QFlags<QEventLoop::ProcessEventsFlag>)    libQt5Gui.so.5
22    ??    gcc_64/plugins/platforms/libqxcb.so
23    g_main_context_dispatch    /usr/lib64/libglib-2.0.so.0
24    ??    /usr/lib64/libglib-2.0.so.0
25    g_main_context_iteration    /usr/lib64/libglib-2.0.so.0
26    QEventDispatcherGlib::processEvents(QFlags<QEventLoop::ProcessEventsFlag>)    libQt5Core.so.5
27    QEventLoop::exec(QFlags<QEventLoop::ProcessEventsFlag>)    libQt5Core.so.5
28    QCoreApplication::exec()    libQt5Core.so.5
29    main    main.cpp    51
第2到4行是重点,大概的过程应该有数了。

5.3 addresswidget.h # slots

槽函数声明:
    Q_OBJECT

public slots:
    void addEntry();
    void addEntry(QString name, QString address);
    void editEntry();
    void removeEntry();

5.4 moc_addresswidget.cpp # qt_static_metacall

moc文件是Qt5.0.2/5.0.2/gcc_64/examples/widgets/itemviews/build-addressbook-Desktop_Qt_5_0_2_GCC_64bit-Debug/moc_addresswidget.cpp,摘录部分源码:
Pre[-]
void AddressWidget::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
    if (_c == QMetaObject::InvokeMetaMethod) {
        AddressWidget *_t = static_cast<AddressWidget *>(_o);
        switch (_id) {
        case 0: _t->selectionChanged((*reinterpret_cast< const QItemSelection(*)>(_a[1]))); break;
        case 1: _t->addEntry(); break;
        case 2: _t->addEntry((*reinterpret_cast< QString(*)>(_a[1])),(*reinterpret_cast< QString(*)>(_a[2]))); break;
        case 3: _t->editEntry(); break;
        case 4: _t->removeEntry(); break;
        default: ;
        }
    } else if (_c == QMetaObject::IndexOfMethod) {
        int *result = reinterpret_cast<int *>(_a[0]);
        void **func = reinterpret_cast<void **>(_a[1]);
        {
            typedef void (AddressWidget::*_t)(const QItemSelection & );
            if (*reinterpret_cast<_t *>(func) == static_cast<_t>(&AddressWidget::selectionChanged)) {
                *result = 0;
            }
        }
    }
}
比较直观,不解释了。
addresswidget也有信号函数,原理是跟上面一样的。

5.5 addresswidget.h # signals

信号函数声明:
signals:
    void selectionChanged (const QItemSelection &selected);

5.6 moc_addresswidget.cpp # selectionChanged

// SIGNAL 0
void AddressWidget::selectionChanged(const QItemSelection & _t1)
{
    void *_a[] = { 0, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };
    QMetaObject::activate(this, &staticMetaObject, 0, _a);
}
selectionChanged函数是生成的,同样调用了QMetaObject::activate。
Qt::DirectConnection就到此为止了,下面是Qt::QueuedConnection相关的部分。

QMetaObject是个结构体,定义是在include/QtCore/qobjectdefs.h,实现在好几个类,其中activate方法是在qobject.cpp(在Qt5.0.2/5.0.2/Src/qtbase/src/corelib/kernel目录,用grep "::activate(" *.cpp搜索)。

5.7 QMetaObject::activate

这个方法很长,不全部列举了。
if ((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
    || (c->connectionType == Qt::QueuedConnection)) {
    queued_activate(sender, signal_index, c, argv ? argv : empty_argv);
    continue;
}

5.8 queued_activate

static void queued_activate(QObject *sender, int signal, QObjectPrivate::Connection *c, void **argv)
{
// other code...
    QMetaCallEvent *ev = c->isSlotObject ?
        new QMetaCallEvent(c->slotObj, sender, signal, nargs, types, args) :
        new QMetaCallEvent(c->method_offset, c->method_relative, c->callFunction, sender, signal, nargs, types, args);
    QCoreApplication::postEvent(c->receiver, ev);
}
给receiver发了一个QMetaCallEvent。QCoreApplication::postEvent会把event给receiver所在的线程。线程应该会在事件循环当中调用相应的方法,这部分代码没看。

6 深入阅读

http://qt-project.org/doc/qt-4.8/signalsandslots.html
Qt Internals & Reversing - Author: Daniel Pistelli - www.ntcore.com

=文章版本=

20130513

No comments:

Post a Comment