Sunday, May 03, 2009

Gtkmm v.s. Qt

Gtkmm 自己的手册上也对 Qt 进行了比较,另外在 FAQ 里面也有所阐述,我们在这里记录一下:

Qt 产生的时代比较早,那个时候 C++ 标准尚未成熟,而 Gtkmm 产生较晚,那个时候 C++ 标准和 STL 都比较成熟。使用 gtkmm,使用的方式更偏向于 C++ 的风格,可以使用继承、多态,并且由于 gtkmm 使用的机制(libsigc++,在对象里 composite signal 对象)能更好的利用 C++ 编译实现 type-safe,而不是依赖于运行时检查类型匹配。

在内存管理上,gtkmm 之于 Qt 稍微灵活一些,一般说来 Qt 里面通过 QWidget 一线继承下来的有一个所谓的 parent,这使得我们 new 出来的对象可以不必手工 delete,因为通过 parent 的释放,这个 QWidget 也将被自动的释放。但是这样做了,我们就不能通过自己的代码改变该 QWidget 的生存。gtkmm 提供了额外的方式,Gtk::manage()。

从继承的方式我们也知道,Qt 通过额外的 QMetaObject,这使我们在继承的时候不能遗忘 Q_OBJECT 宏以维持正常的 signal/slot 机制,同时在该分支中希望做多继承其实不必要。换言之 Qt 的设计更偏向于 Java 一些。gtkmm 虽然没用这些比较 tricky 的地方,但是也会因此丧失一些 Qt 的优点。比如 Qt 不仅仅包含一整套 GUI 设计的类库、工具,所有的外围部件,如 XML、网络等都使用的统一的设计,相同的通讯机制。而 gtkmm 充其量只算把 GUI 设计部分解决了,如果需要其他的功能,需要使用别的库,这在一定程度上对程序员提出了更高的要求,因为很可能必须自己写一个 wrapper(如果是一个 C 库,不具备 signal/slot 的能力),又或者是具有 signal/slots 能力的库,如何将两部分整合在一起呢?

有一点 Qt 通过 QMetaObject 实现的,如 signal/slot 的 auto connection,我们只需要为某个 GUI 设计好界面,然后通过多继承也好,UiTools 直接读取 ui 文件也好,不需要额外的代码,signal/slots 就连接好了,不过这也限制了这类 signal/slots 对象必须是 QObject。gtkmm 应该是不能这样实现自动连接,但是 ligsigc++ 允许将 signal 连接到普通的函数或者成员函数,这也使得我们能够比较方便的实现 signal/slot 的连接过程。

gtkmm 可能更加接近 STL,有人认为这是好事,因为标准就是好;也有人不这么认为,因为相反提供的功能比较有限,不如 Qt 很多类更适合编程人员的上手。

gtkmm 的外围工具不如 Qt 那么专门,比如 qmake、uic 和 moc;我们更常用的 pkg-config 和 autotools 将和 gtkmm 更好的协同工作。当然 Qt 的 qmake 使用 pkg-config 的也相当容易了。

Qt 似乎多线程方面做得更好一些,很多关键性的部分都有 mutex 等出现保证原子操作性,而 gtkmm 在这部分似乎做的还差的很远,因为 FAQ 里面说道倾向用单线程 -,-b

关于 qmake 的一个诡异的事情

不知道为什么,巨汗,本来以为那个 scope 可以很容易实现在 debug 和 release 版本下把目标文件放在不同的目录下,只需要

debug {
 DESTDIR = debug
}
release {
  DESTDIT = release
}
就完了,结果非常的 frustrating,网上看见不少人跟我有类似的抱怨,就是写了但不 work,比如有的是希望 LIBS 不一样什么的,但是居然有人说没这回事,可是在我机器上这个事情发生了 N 次了。你看看生成的 Makefile.Debug 和 Make.Release 好了,这样写最后 target 都是 release/$$TARGET,天知道为啥这个 scope 就被忽视了。

最后现在用下面的方法“解决”了,其实不是解决吧,因为我也不知道为啥。

CONFIG(debug,debug|release) {
  DESTDIR = debug
} else {
  DESTDIR = release
}
最囧的是,必须把这句话放在 CONFIG 变量前面... 我到...

Friday, April 24, 2009

QCoreApplication 和 QApplication

QCoreApplication 是为了编写 console 下 Qt 程序设计的类,提供 event loop,QApplication 继承了它,提供 GUI 下的的 event loop。我们先看看 QCoreApplication,这个类应该是一个 singleton,在其构造函数中调用的 init() 里面有如下代码

Q_ASSERT_X(!self, "QCoreApplication", "there should be only one application object");
QCoreApplication::self = this;
注意与此对应,event dispatcher 对于一个线程也应该是唯一的,
// use the event dispatcher created by the app programmer (if any)
if (!QCoreApplicationPrivate::eventDispatcher)
  QCoreApplicationPrivate::eventDispatcher = d->threadData->eventDispatcher;
// otherwise we create one
if (!QCoreApplicationPrivate::eventDispatcher)
  d->createEventDispatcher();
Q_ASSERT(QCoreApplicationPrivate::eventDispatcher != 0);

if (!QCoreApplicationPrivate::eventDispatcher->parent())
  QCoreApplicationPrivate::eventDispatcher->moveToThread(d->threadData->thread);

d->threadData->eventDispatcher = QCoreApplicationPrivate::eventDispatcher;
类似的,使用 QApplication 时,我们建立了 event dispatcher,创建了 widget 并 QWidget::show() 了,当我们调用 QApplication::exec() 时,进入消息循环,可是消息如何发给我们的 widget 的呢?我们知道前面的 main.cpp 里面甚至没有 QApplication 和 QWidget 的任何关联。 事实是这样的,我们建立一个 QCoreApplication(包括一个 QApplication)对象时,同时建立的 QEventDispatcher 为本 thread 做好了接受 Events 的准备,QWidget 负责创建一个窗体,创建这个部分需要调用底层的 xlib,这时 QEventDispatcher 在该线程某个 xlib 创建的部分收到 X11 的事件后就会获得 event,QEventDispatcherX11::processEvents() 将某一部分 event (用户输入事件)放到一个队列中,还有一部份交给 QApplication::x11PrcessEvent(),该函数负责找到对应的 QWidget 并按照事件类型通过 QWidget::x11Event() 发送过去,注意该函数默认返回 false,
bool QWidget::x11Event(XEvent *)
{
    return false;
}
这是为了让程序员需要的时候自己处理 X11 的事件,因此可以继承 QWidget 覆盖这个函数。我们看看进入消息队列的那部分,
switch (event.type) {
  case ButtonPress:
  case ButtonRelease:
  case MotionNotify:
  case XKeyPress:
  case XKeyRelease:
  case EnterNotify:
  case LeaveNotify:
    d->queuedUserInputEvents.append(event);
    continue;
  // ...
可见我们需要看看 QEventDispatcherX11Private 维护的 queuedUserInputEvents 队列是如何被处理的,
class QEventDispatcherX11Private : public QEventDispatcherUNIXPrivate
{
    Q_DECLARE_PUBLIC(QEventDispatcherX11)
public:
    inline QEventDispatcherX11Private()
        : xfd(-1)
    { }
    int xfd;
    QList<XEvent> queuedUserInputEvents;
};
这是一个链表,事实上 QApplication 调用 processEvents() 时,即调用了 QEventDispatcher::processEvents()。这只是处理消息的函数,我们仍然不清楚该函数如何被调用的,在 QCoreApplication::exec() 里,另外一个东西就是 QEventLoop,在 QEventLoop::exec() 里面我们找到了答案,
// remove posted quit events when entering a new event loop
if (qApp->thread() == thread())
  QCoreApplication::removePostedEvents(qApp, QEvent::Quit);

while (!d->exit)
  processEvents(flags | WaitForMoreEvents | EventLoopExec);
这就是核心的部分。

上面的过程比较混乱,这里重新总结下,建立的 QCoreApplication/QApplication 建立了一个 QEventDispatcher,然后建立了用户界面后就可以响应用户输入了,调用了 QApplication::exec() 后,这部分 X event 通过 Xlib 发送过来,由 QEventLoop 处理消息,调用的是 QEventDispatcher::processEvents() 维护消息队列,该函数会通过 QApplication::x11ProcessEvent() 将消息发送给对应的 QWidget,也可以通过设置用户自己处理 XEvent,这会发送给那个 QWidget 对应的 x11Event() 函数处理。

这也就是为什么写多线程程序时,如果希望在新的线程使用消息机制,也需要在 QThread::run() 里面调用 QThread::exec() 维护消息队列。

Qt 的线程和线程通信

传统的多线程程序都是通过 POSIX 的线程库 pthread 实现的,而 Qt 为用户提供的线程就是对 pthread 的再次封装,这个封装的类就是 QThread 直接继承 QObject 而来。

class Q_CORE_EXPORT QThread : public QObject
{
public:
    static Qt::HANDLE currentThreadId();
    static QThread *currentThread();
    static int idealThreadCount();
    static void yieldCurrentThread();

    explicit QThread(QObject *parent = 0);
    ~QThread();

    enum Priority {
        IdlePriority,

        LowestPriority,
        LowPriority,
        NormalPriority,
        HighPriority,
        HighestPriority,

        TimeCriticalPriority,

        InheritPriority
    };

    void setPriority(Priority priority);
    Priority priority() const;

    bool isFinished() const;
    bool isRunning() const;

    void setStackSize(uint stackSize);
    uint stackSize() const;

    void exit(int retcode = 0);

public Q_SLOTS:
    void start(Priority = InheritPriority);
    void terminate();
    void quit();

public:
    // default argument causes thread to block indefinately
    bool wait(unsigned long time = ULONG_MAX);

Q_SIGNALS:
    void started();
    void finished();
    void terminated();

protected:
    virtual void run();
    int exec();

    static void setTerminationEnabled(bool enabled = true);

    static void sleep(unsigned long);
    static void msleep(unsigned long);
    static void usleep(unsigned long);

#ifdef QT3_SUPPORT
public:
    inline QT3_SUPPORT bool finished() const { return isFinished(); }
    inline QT3_SUPPORT bool running() const { return isRunning(); }
#endif

protected:
    QThread(QThreadPrivate &dd, QObject *parent = 0);

private:
    Q_OBJECT
    Q_DECLARE_PRIVATE(QThread)

    static void initialize();
    static void cleanup();

    friend class QCoreApplication;
    friend class QThreadData;
};
编程人员主要通过继承 QThread 类,并自己重新实现 QThread::run(),我们来看该类在哪里留下的“钩子”,这个原始的 run() 做了什么呢?
void QThread::run()
{
    (void) exec();
}

int QThread::exec()
{
    Q_D(QThread);
    d->mutex.lock();
    d->data->quitNow = false;
    QEventLoop eventLoop;
    d->mutex.unlock();
    int returnCode = eventLoop.exec();
    return returnCode;
}
可见默认情况就是进入一个事件循环。

所谓的 event loop 是每一个 thread 都需要进行的任务,这个过程将处理外部环境发给自己的 event,Qt 将各种 event 进行了包装,用 QEvent 类进行了封装,不同的事件就是 QEvent 的子类,很多类都具有处理 event 的能力,比如 QWidget,下面是 QWidget 实现的事件,

protected:
    // Event handlers
    bool event(QEvent *);
    virtual void mousePressEvent(QMouseEvent *);
    virtual void mouseReleaseEvent(QMouseEvent *);
    virtual void mouseDoubleClickEvent(QMouseEvent *);
    virtual void mouseMoveEvent(QMouseEvent *);
#ifndef QT_NO_WHEELEVENT
    virtual void wheelEvent(QWheelEvent *);
#endif
    virtual void keyPressEvent(QKeyEvent *);
    virtual void keyReleaseEvent(QKeyEvent *);
    virtual void focusInEvent(QFocusEvent *);
    virtual void focusOutEvent(QFocusEvent *);
    virtual void enterEvent(QEvent *);
    virtual void leaveEvent(QEvent *);
    virtual void paintEvent(QPaintEvent *);
    virtual void moveEvent(QMoveEvent *);
    virtual void resizeEvent(QResizeEvent *);
    virtual void closeEvent(QCloseEvent *);
#ifndef QT_NO_CONTEXTMENU
    virtual void contextMenuEvent(QContextMenuEvent *);
#endif
#ifndef QT_NO_TABLETEVENT
    virtual void tabletEvent(QTabletEvent *);
#endif
#ifndef QT_NO_ACTION
    virtual void actionEvent(QActionEvent *);
#endif

#ifndef QT_NO_DRAGANDDROP
    virtual void dragEnterEvent(QDragEnterEvent *);
    virtual void dragMoveEvent(QDragMoveEvent *);
    virtual void dragLeaveEvent(QDragLeaveEvent *);
    virtual void dropEvent(QDropEvent *);
#endif

    virtual void showEvent(QShowEvent *);
    virtual void hideEvent(QHideEvent *);

#if defined(Q_WS_MAC)
    virtual bool macEvent(EventHandlerCallRef, EventRef);
#endif
#if defined(Q_WS_WIN)
    virtual bool winEvent(MSG *message, long *result);
#endif
#if defined(Q_WS_X11)
    virtual bool x11Event(XEvent *);
#endif
#if defined(Q_WS_QWS)
    virtual bool qwsEvent(QWSEvent *);
#endif

    // Misc. protected functions
    virtual void changeEvent(QEvent *);
这里看起来很复杂,主要两种,一个是 QWidget::event() 本身,另一个是各种虚函数 *Event 用于处理不同的 event,其实是一种 event handler,我们通过继承 QWidget 实现不同的 event handler 就可以获得不同的 component 了,比如 QAbstractButton 需要重写 paintEvent,使得该区域没有被按下的时候凸起,而按下后变成凹下的样子,另外,在 QWidget::event() 的实现中我们看见如下实现,
switch (event->type()) {
case QEvent::MouseMove:
  mouseMoveEvent((QMouseEvent*)event);
  break;
// ...
}
这就是下“钩子”的地方,Qt 通过调用 event() 方法,响应向一个 QObject 发送一个需要响应的事件(调用者利用 QApplication::sendEvent() 方法发送),event() 处理后返回 true,否则返回 false,event() 函数进一步利用 virtual function *Event() 函数分类处理该事件,因此要改变一个 QWidget 响应某事件就需要 override 这些事件的 event handler。而为了让用户更好的处理这些事件而不干预内部的实现,在 event handler 里面会 emit signal,比如
void QAbstractButton::mousePressEvent(QMouseEvent *e)
{
    Q_D(QAbstractButton);
    if (e->button() != Qt::LeftButton) {
        e->ignore();
        return;
    }
    if (hitButton(e->pos())) {
        setDown(true);
        repaint(); //flush paint event before invoking potentially expensive operation
        QApplication::flush();
        d->emitPressed();
        e->accept();
    } else {
        e->ignore();
    }
}
这样,用户就可以利用这个 signal 而不是事件本身来通知别的 component 了。如果是需要通过 QApplication::sendEvent() 那么必须重新 implement 对应的 event handler,也就是必须继承该 class,这是非常繁琐的(每个控件都是库里面的衍生体 -,-b)。

我们接着看 QThread 如何调用 run(),这部分到不是在 [qt]/src/corelib/qthread.cpp 里定义的,是在 qthread_unix.cpp 里面,这相当于是跟实现的 OS 相关的代码,因此分成了几个文件,QThread::start() 调用了 run(),

int code =
        pthread_create(&d->thread_id, &attr, QThreadPrivate::start, this);
void *QThreadPrivate::start(void *arg)
{
    pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
    pthread_cleanup_push(QThreadPrivate::finish, arg);

    QThread *thr = reinterpret_cast<QThread *>(arg);
    QThreadData *data = QThreadData::get2(thr);

    pthread_once(¤t_thread_data_once, create_current_thread_data_key);
    pthread_setspecific(current_thread_data_key, data);

    data->ref();
    data->quitNow = false;

    // ### TODO: allow the user to create a custom event dispatcher
    if (QCoreApplication::instance())
        createEventDispatcher(data);

    emit thr->started();
    pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
    pthread_testcancel();
    thr->run();

    pthread_cleanup_pop(1);
    return 0;
}
这个程序首先把 QThreadPrivate::finish() 压栈(等线程结束后清理),设置好线程特性,然后建立 event dispatcher,触发 QThread::started() 信号,然后就调用 QThread::run() 了。最后等待该部分结束后,就会通过前面压栈的 finish() 完成清理工作。

因此尽管 QThread 这个对象在主线程中创建,但是实际上 QThread::run() 是在新的线程里面运行的,这里涉及到很重要的问题就是 reentrant 和 thread-safe。一个函数/类称为 reentrant 如果可以同时被若干线程调用,一般来说就是说他不会改变内部所依赖的数据,有的函数通过一个 static 成员计算结果,而该成员在计算中会被反复修改,因此一个线程调用该函数后,另一个线程就不可以继续调用。所谓的 thread-safe 就是说,某函数/类尽管需要访问 shared data,但是通过串行化访问,保证在不同线程调用时使得每个操作都是安全的。为了保证这点,Qt 提供了我们 QMutex、QReadWriteLock、QSemaphore 和 QWaitCondition 这些用于保证访问的 thread-safety 和同步我们的线程。

所谓 mutex(mutual exclusive)就是保证访问是 serialized 的,创建一个 QMutex 对象,所有合法的请求应该都遵守首先使用 QMutexLocker 将该 mutex 锁定,然后才能访问数据的原则,这时因为 QMutex 只能被 lock 一次,后续请求的线程将被挂起直到前面锁定的线程解除。可是 QMutex 无法区分读、写的不同,因为往往读操作可以多线程并行,而写操作必须串行化,这可以用 QReadWriteLock。另外 QMetaphore 是对一批资源的控制,比如 Qt 提供的例程,

#include <QtCore>

#include <stdio.h>
#include <stdlib.h>

const int DataSize = 100000;
const int BufferSize = 8192;
char buffer[BufferSize];

QSemaphore freeBytes(BufferSize);
QSemaphore usedBytes;

class Producer : public QThread
{
public:
  void run();
};

void Producer::run()
{
  qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));
  for (int i = 0; i < DataSize; ++i) {
    freeBytes.acquire();
    buffer[i % BufferSize] = "ACGT"[(int)qrand() % 4];
    usedBytes.release();
  }
}

class Consumer : public QThread
{
public:
  void run();
};

void Consumer::run()
{
  for (int i = 0; i < DataSize; ++i) {
    usedBytes.acquire();
    fprintf(stderr, "%c", buffer[i % BufferSize]);
    freeBytes.release();
  }
  fprintf(stderr, "\n");
}

int main(int argc, char *argv[])
{
  QCoreApplication app(argc, argv);
  Producer producer;
  Consumer consumer;
  producer.start();
  consumer.start();
  producer.wait();
  consumer.wait();
  return 0;
}
有人可能会抱怨,那个 fprintf() 会不会不是 atomic 的呢?
The POSIX standard requires that by default the stream operations are atomic. I.e., issuing two stream operations for the same stream in two threads at the same time will cause the operations to be executed as if they were issued sequentially. The buffer operations performed while reading or writing are protected from other uses of the same stream. To do this each stream has an internal lock object which has to be (implicitly) acquired before any work can be done.
But there are situations where this is not enough and there are also situations where this is not wanted. The implicit locking is not enough if the program requires more than one stream function call to happen atomically. One example would be if an output line a program wants to generate is created by several function calls. The functions by themselves would ensure only atomicity of their own operation, but not atomicity over all the function calls. For this it is necessary to perform the stream locking in the application code.
而 QWaitCondition 主要用于多个线程的同步,比如创建一个该对象,几个线程调用 QWaitCondition::wait( &mutex ),另外的通过 QWaitCondition::wakeAll() 或者 QWaitCondition::wakeOne() 唤醒。

最后我们谈一下 signal/slot 机制和多线程的关系。尽管 QObject 保证是 reentrant 的(QWidget 一般不是的),但是我们编程的时候要注意,一个 QObject 的的子对象(通过 QObject::QObject( QObject *parent) 构造)必须在同一个线程里面产生;另外,对事件驱动的如 QTimer 和 network 一些类,启动的代码必须在同一个线程里(这意味着如果在一个QThread 子类里面创建一个 QTimer,其实是不能在 QThread::run() 里面启动该 timer,因为创建 QThread 对象是在另外一个线程里面,只有 QThread::run() 的那部分在新的线程里面执行);在一个线程里面创建的对象必须在线程被释放 QThread 前释放。

一个 QObject 创建后存在于一个线程中,我们可以用 QObject::moveToThread() 把该对象“置于”另外的线程,但是该 QObject 不能有 parent。这时候从一个线程调用另外一个线程的方法可以认为不是 thread safe 的,因为可能该对象正在处理事件,而比如调用析构函数就会产生严重问题(因此最好调用 QObject::deleteLater() 函数),这也就是我们需要注意信号发送的方式的问题,QObject::connect() 允许我们使用所谓 queued connection。低层一点的 QCoreApplication::sendEvent() 其实也应该在这种情况被替换为 QCoreApplication::postEvent()。这样避免在本线程重复调用某个 QObject 的代码,产生 thread safety 的问题。

我们将在下面研究 QApplication 和 QCoreApplication 类,分析 sendEvent()、postEvent() 以及 Qt 如何实现的 queued connection 的。

Wednesday, April 22, 2009

做一个简单的显示图片的 widget

需要明白的是 Qt 实现图片等显示需要一个所谓的 QPaintDevice,我们通过 QPaintEngine 在上面作图,而 QPainter 实现比较高层次的作图动作。对于一些特殊的应用,比如需要渲染 OpenGL 场景,原始的 QPaintEngine 并不适用,注意一般说来 QPaintEngine 是一个 static 对象,我们看看相关一些定义。首先了解一下下面这一段代码,来自 qglobal.h

#define Q_GLOBAL_STATIC(TYPE, NAME)                              \
    static TYPE *NAME()                                          \
    {                                                            \
        static TYPE this_##NAME;                                 \
        static QGlobalStatic<TYPE > global_##NAME(&this_##NAME); \
        return global_##NAME.pointer;                            \
    }
template <typename T>
class QGlobalStatic
{
public:
    T *pointer;
    inline QGlobalStatic(T *p) : pointer(p) { }
    inline ~QGlobalStatic() { pointer = 0; }
};
可见这定义了一个函数,该函数内有一个 static 对象,叫 this_NAME,然后通过模板类产生一个指向该对象的指针。我们在 qwidget.h 里面看到
Q_GLOBAL_STATIC(QX11PaintEngine, qt_widget_paintengine)
,在 qgl.cpp 中发现
Q_GLOBAL_STATIC(QGL2PaintEngineEx, qt_gl_engine)
Q_GLOBAL_STATIC(QOpenGLPaintEngine, qt_gl_engine)
因此,实际上我们平时在 Linux 下使用的 Engine 是 qt_widget_paintengine(QX11PaintEngine),而有两种 OpenGL 的 Engine。

那么使用不同的 Engine 获益主要在于某些 engine 可以通过特殊的硬件,如显卡,对特别的应用实现高速的渲染。因此,普通的 QWidget 如果想具有 OpenGL 的渲染能力需要将其 paint engine 换成 qt_gl_engine,这是通过构造函数里面

QGLWidget::QGLWidget(QWidget *parent, const QGLWidget* shareWidget, Qt::WindowFlags f)
    : QWidget(*(new QGLWidgetPrivate), parent, f | Qt::MSWindowsOwnDC)
{
    Q_D(QGLWidget);
    setAttribute(Qt::WA_PaintOnScreen);
    setAttribute(Qt::WA_NoSystemBackground);
    setAutoFillBackground(true); // for compatibility
    d->init(new QGLContext(QGLFormat::defaultFormat(), this), shareWidget);
}
实现的,这里 QGLContext 在该 QPainterDevice 上建立了 OpenGL 的 context,这是 enable OpenGL 的重要步骤,这个实现也是通过 QGLContextPrivate 的 init() 方法实现的。

有了这些底层的准备工作之后,我们后面还需要做很多事情。虽然 QWidget 提供了一个基本的舞台,但是并没有很多专门性的用途,因此,很多我们使用的 component 都是从 QWidget 里面继承的,比如 QLabel,它本身是一个可以显示 image 的 component。我们可以借鉴它的代码,

void QLabel::setPixmap(const QPixmap &pixmap)
{
    Q_D(QLabel);
    if (!d->pixmap || d->pixmap->cacheKey() != pixmap.cacheKey()) {
        d->clearContents();
        d->pixmap = new QPixmap(pixmap);
    }

    if (d->pixmap->depth() == 1 && !d->pixmap->mask())
        d->pixmap->setMask(*((QBitmap *)d->pixmap));

    d->updateLabel();
}
注意这里使用了一个 QLablePrivate 类作为 QPixmap 的存储,最后调用 QLabelPrivate::updateLabel() 实际上是重画这个 QWidget。那么如何重画呢,其实这时候需要响应所谓的 QPaintEvent,这会调用 QWidget::paintEvent(),我们后面来看看所谓的 event 到底是什么。因此实现一个自己需要的样子的 widget 就只需要把这个 protected 虚函数 overriden 即可。

那么,对于比如说 QGLWidget 而言,如果需要渲染场景、改变场景,是否应该对应到对应到 paint event 呢?我们注意到该 widget 声明中,

protected:
    virtual void initializeGL();
    virtual void resizeGL(int w, int h);
    virtual void paintGL();

    void paintEvent(QPaintEvent*);
我们来看看是怎么回事,在实现文件中,
void QGLWidget::paintEvent(QPaintEvent *)
{
    if (updatesEnabled()) {
        glDraw();
        updateOverlayGL();
    }
}
可见直接调用 OpenGL 函数重绘场景,而另外三个函数提供给需要自己创建自己的 widget 的用户继承该类进行 overridden。因此,我们得出一个基本的事情,就是当我们收到 paint event 时,正常情况是调用 paint engine 重绘,一旦我们需要改变一个 QWidget 对其 paintEvent() 进行重载,QGLWidget 提供的另外三个函数只是为了方便实现一些简单的功能留下来的“钩子”,比如 initializeGL 将在 QGLWidget::glInit() 中调用,初始化 GL 环境后创建初始场景,QGLWidget::glDraw() 中调用 paintGL(),因此响应 paint event 的时候我们可以通过 override paintGL() 更改场景。那么实现 OpenGL 动画的关键就是通过几个参数确定状态,然后在 paintGL() 里面将场景更新,而在一个 QTimer 中依照一定的时间间隔更新参数。

现在我们实现一个简单的显示图片的的 widget,根据前面的知识,我们创建如下的 qmake 配置文件,

TEMPLATE = app
CONFIG += qt debug_and_release
DEPENDPATH += .
INCLUDEPATH += .

TARGET = test_imagewidget
SOURCES = main.cpp imagewidget.cpp
HEADERS = imagewidget.hpp

debug {
  DESTDIR = debug
}

release {
  DESTDIR = release
}

DESTDIR_TARGET = $$DESTDIR
首先我们看看我们的 ImageWidget 类,
#ifndef IMAGEWIDGET_HPP
#define IMAGEWIDGET_HPP

#include <QWidget>
#include <QImage>

class ImageWidget : public QWidget {
  Q_OBJECT
  QImage *img ;
protected:
  void paintEvent( QPaintEvent * ) ;
public:
  ImageWidget( QWidget* = 0 ) ;
  void setImage( QImage * ) ;
  const QImage *getImage() const ;
} ;

#endif
我们通过一个 QImage 的指针用于作图,其实用 QPixmap 可能更好。下面是实现的代码,根据以上分析,我们修改 QWidget::paintEvent() 实现显示功能,
#include "imagewidget.hpp"
#include <QPainter>

ImageWidget::ImageWidget( QWidget* pa ) : QWidget::QWidget( pa )
{
  img = NULL ;
}

void
ImageWidget::setImage( QImage *image )
{
  img = image ;
}

const QImage*
ImageWidget::getImage() const
{
  return img ;
}

void
ImageWidget::paintEvent( QPaintEvent * )
{
  if( img != NULL ) {
    QPainter p( this ) ;
    p.drawImage( QPoint( 0, 0 ), img -> scaled( size() ) ) ;
  }
}
值得注意的是如果我们考虑实现 seam carving 这种东西,我们可能需要对 resizeEvent 进行 override。最后下面是主调文件,
#include <QApplication>
#include <QImage>
#include "imagewidget.hpp"

int
main( int argc, char *argv[] )
{
  QApplication app( argc, argv ) ;
  QImage img ;
  if( argc == 2 )
    img.load( argv[1] ) ;
  else
    return 1 ;

  ImageWidget widget ;
  widget.setImage( &img ) ;
  widget.show() ;

  return app.exec() ;
}

那么很显然,如果我们有一个图片序列,可以用 QTimer 调用 ImageWidget::setImage() 设置图片,然后就可以获得我们需要的结果了。因此从一个高层角度来看,我们去实现一个 image buffer,然后通过 QTimer 从中获得需要的图片,另一个线程更新该 buffer 即可。但是事实上 video 是这样的么?

qmake 简介

qmake 是 Qt 默认的一个辅助建立 Makefile 的程序,我们知道,Qt 要用 moc、uic 等等预编译程序,而手工书写相关的编译规则会比较繁琐。另外由于 Qt 使用了模块化的设计,编译的时候需要连接到不同的动态链接库上,自己写 Makefile 会比较麻烦。因此一种标准的做法是使用一个程序产生 Makefile。这里 Qt 提供了 qmake,GNU 传统使用的那就是 autoconf/automake 系列,另外还有所谓 cmake。据说 gtkmm 那边使用的是 auto 系列,和 GNU 传统保持的更好。比较还是等两者都比较清楚之后再来做出。

qmake 依靠的是一个 .pro 文件产生输出的 Makefile 或者 Visual Studio 的工程文件的,比较土的使用方式就是直接调用 qmake -project 产生一个基本的 pro 文件,然后调用 qmake 产生 Makefile(看 man page 如何做 VS 的)。过程很简单,之后有了 Makefile 就可以直接 Make 了。

.pro 文件的内容其实就是一些变量的赋值,其实和 Makefile 的语法多多少少相像的,如 = 赋值,+= 是在原来值后添加新的值,变量展开是 $$(Makefile 是 $(var),这不大一样)。那么里面有些什么常用的变量呢?

  • TEMPLATE 表示 qmake 使用什么模板产生 Makefile,可以用 app 表示应用程序(默认),lib 表示产生库,这会影响其他的一些变量,比如使用 app 时 CONFIG 变量可以选择 windows(表示 Windows GUI 程序)或者 console(命令行程序),选择 lib 时,CONFIG 可以选择 dll(动态链接库)、staticlib(静态库)、plugin(插件)。另外有 subdirs 可以把需要编译的目标放在子目录中(用 SUBDIRS 说明),每个子目录里面有对应的 pro 文件;vcapp 和 vclib 是对应的 visual studio 版本。
  • TARGET 目标文件,也就是最后生成的可执行文件(这当然是 app 模板啦),相关的还有 TARGET_EXT 表示扩展名(Windows 里面用的),TARGET_x.y.z 是带版本号的目标文件。
  • DEPENDPATH 是需要解决依赖关系时(一般都是通过 include 产生的)搜索的路径。
  • INCLUDEPATH 很明显就是使用 -I 命令添加的路径,一般来说使用 CONFIG 变量加载了 Qt 的模块后,该变量就会含有对应的目录,这样写 #include 时就不必写相对目录(如 QtUiTools)了。
  • HEADERS 包含需要的头文件。
  • SOURCES 需要编译的源文件。
  • CONFIG 是最重要的变量之一了,调试和发布版本一般编译选项不同,可以用 debug、release 和 debug_and_release 三种,这会产生三个同名状态,比如可以用不同的 TARGET 或者 DESTDIR(TARGET 放的地方)。build_all 是把 Makefile 的默认编译目标做成编译所有的包括 debug 和 release。ordered 使得使用 subdirs 的 template 的时候依照顺序编译。warn on/off 是编译器警告打开多少的开关。qt 表示是一个 Qt 的程序。thread 表示是多线程程序。x11/windows/console/dll/staticlib... 和 TEMPLATE 表达意思一样。resources 表示需要调用 rcc 对资源编译。下面一些是对编译器相关选项的打开,3dnow、exceptions、mmx、rtti、stl、sse、sse2,对 windows 和 mac 下面还有一些自己的选项。另外如果是小写的名字可以做为一个条件,后面会讲不同条件下编译的情况。使用别的库,可用 pkg-config 配置的话可以用 link_pkgconfig,然后 PKGCONFIG 指定这些库的名字。
  • QT 指定使用的模块,比如使用各个 module 的话可以 QT += mod1 mod2,模块有 core、gui(这两个默认就有)、network、opengl、phonon(多媒体)、sql、svg、xml、webkit、 qt3support。
  • DEFINES 包含一些 -D 传递的宏。
  • FORMS 指定 .ui 文件。
  • RESOURCES 指定资源。
  • LIBS 指定其他 -l -L 的参数。

qmake 有所谓的 scope,比如可以用 debug、release、win32、unix、macx 判断环境,最经典的用法

debug {
  DESTDIR=debug
}
release {
  DESTDIR=release
}
,另外嵌套的判断可以用 : 连接,如 win32:debug 就表示
win32 {
  debug {
  }
}
另外在条件前加 ! 表示不在该范围内,条件可以 | 连接表示或。

除了 += 以外,还有 -=(去掉),*=(如果没有则加入),~=(替换,使用 s/ / 语法)。

变量除了 $$ 获得值,也可以 $${},这种格式可以接在另一个字符串后边,$$() 可以获得环境变量,$$[] 可以获得 Qt 的信息。

qmake 还有一些函数,如 message/warning/error 将在命令行上输出一条消息,dirname/basename 和系统命令一样,include 读入另一个 .pro 文件,CONFIG() 可以测试 CONFIG 里面互斥的如 debug/release,system 调用系统命令,等。

通常我们都不必要写非常复杂的 .pro 文件,因此,我们将在必要的时候深化对此的学习。

Sunday, April 19, 2009

Qt 的 GUI 设计

最早接触到这类设计其实是从 Borland C++ Builder 开始的,作为一个所谓的快速开发工具,其实我对其实现界面设计那块到现在都没有清晰的理解。后来接触了 Java 一段时间,那时候只懂得自己设计界面就是继承一个类,如主窗口或者 applet,然后在该类中添加很多其他的 component 作为其 protected 成员。可是很少考虑到怎么更方便的设计。因此,可以说接触到第一个这种设计思想的 GUI 库就是在 Qt 了。 不得不说 Qt 其实和 Java 很像,虽然说 Qt 是 C++ 写成,但是注意到它其实是单一祖先 QObject 一脉相承,通过 QMetaObject 实现的 RTTI,这多多少少和 Java单一祖先一致,但是 Qt 不排斥使用其他的 C++ class,只是失去了 signal/slot 机制。在 moc 的 man page 里面,其实介绍了 Qt 实现的种种局限性:
  • 我们无法使用 template 继承 QObject,换言之,下面代码无法被 moc 转换成为有效的 C++ compiler 可编译代码
    template
    class TemplateClass : public QObject {
      Q_OBJECT
      // ...
    public slots:
      // ...
    } ;
  • 使用 multiple inheritance 必须把 QObject 或者其子类放在第一个父类的位置(因为使用 Q_OBJECT 需要覆盖),然后继承别的类。颠倒后,如 g++ 会抱错,如
    multiclass.hpp:17: Warning: Class MultiClass inherits from two QObject subclasses NoneQtClass and QObject. This is not supported!
    moc_multiclass.cpp:39: error: ‘staticMetaObject’ is not a member of ‘NoneQtClass’
    moc_multiclass.cpp: In member function ‘virtual void* MultiClass::qt_metacast(const char*)’:
    moc_multiclass.cpp:55: error: ‘qt_metacast’ is not a member of ‘NoneQtClass’
    moc_multiclass.cpp: In member function ‘virtual int MultiClass::qt_metacall(QMetaObject::Call, int, void**)’:
    moc_multiclass.cpp:60: error: ‘qt_metacall’ is not a member of ‘NoneQtClass’
    make: *** [moc_multiclass.o] Error 1
    MultiClass 继承 QObject 和 NoneQtClass,插入的 Q_OBJECT 展开后,因为将 NoneQtClass 放在第一位后 MultiClass 的结构已经不是 QObject 在前面所以创建 staticMetaObject 出错,后面虚函数表也因为在后面所以导致使用的其实是前面那个 class 的 vtable。
  • 函数指针不能作为 signal/slot 的参数,这个可以用 typedef 克服,如
    class SomeClass : public QObject {
      Q_OBJECT
      //...
    public slots:
      // illegal
      void apply( void (*apply)(List *, void *), void * );
    };
    将被认为非法(主要是判断参数类型时会失败,记得 QMetaObject 存下来的是什么信息么?),但是
    typedef void (*ApplyFunctionType)( List *, void * );
    
    class SomeClass : public QObject {
      Q_OBJECT
      //...
    public slots:
      void apply( ApplyFunctionType, char * );
    };
    是可行的。
  • 友元声明最好不要放在 signal 和 slot 声明中,这很明显,因为多数情况下虽然根据宏替换 signals: 被替换为 protected:,slots 被替换为空,但是编译器处理 friend 可能并不完全这样无关的处理,理论上说标准的编译器应该能 work,如 g++ 4.3.3.
  • signal 和 slot 不能被 upgrade,这是因为 moc 需要一个完整的函数声明,而提升的时候声明是不完整的,如
    class SlotClass : public QObject {
      Q_OBJECT
    protected slots:
      int getValue() ;
    } ;
    
    class UpgradeSlotClass : public SlotClass {
      Q_OBJECT
    public slots:
      Slot::getValue ;
    } ;
    其中的 Slot::getValue 的提升在正常的情况是被允许的,可见报错的是 moc,
    /usr/bin/moc-qt4 -DQT_NO_DEBUG -DQT_GUI_LIB -DQT_CORE_LIB -DQT_SHARED -I/usr/share/qt4/mkspecs/linux-g++ -I. -I/usr/include/qt4/QtCore -I/usr/include/qt4/QtGui -I/usr/include/qt4 -I. -I. -I. upgradeslotclass.hpp -o moc_upgradeslotclass.cpp
    upgradeslotclass.hpp:15: Error: Not a signal or slot declaration
  • signal 和 slot 的参数不能使用宏,这是因为 moc 不展开宏。
  • 嵌套类声明不应该出现在 signal 或者 slot 里面,也是因为 moc 不能处理的原因。
  • 构造函数不应出现在 signal 和 slot 里面,虽然是函数,但是没有必要。
  • Q_PROPERTY 宏声明属性时应在包含其读写函数的 public 节之前,写在同一个 public 里面不允许,注意
    #define Q_PROPERTY(text)
    其实在类声明时这句话没有任何作用,只是 moc 编译会产生对应的代码,而 moc 要求这部分必须以类似下面的方式书写
    class PropertyClass : public QObject {
      Q_OBJECT
      Q_PROPERTY( int value READ getValue WRITE setValue )
      int value ;
    public:
      PropertyClass() ;
      void setValue( int = 0 ) ;
      int getValue() const ;
    } ;
    这部分对应 moc 会产生如下代码
    int PropertyClass::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
    {
      _id = QObject::qt_metacall(_c, _id, _a);
      if (_id < _c ="=" _v =" _a[0];" _c ="=" _v =" _a[0];" _c ="=" _c ="=" _c ="=" _c ="=" _c ="=" _c ="=">

为什么在 QMetaObject 中提供支持 property 以及 Q_CLASSINFO 这些看起来完全没必要的东西呢?后者比如

Q_CLASSINFO("Version", "3.0.0")
这些其实从一个方面是对 GUI 设计程序提供支援。Qt 提供的 GUI 设计程序叫 Qt Designer(似乎 Qt Creator 也能 design?),这是一个图形界面,将需要的 widget 拖到窗口上,用适当的 layout 组织起来,在每一个 widget 被点中的时候有一个 property editor,可以在这里设置前面使用 Q_PROPERTY 声明并且是 DESIGNABLE 为 true 的属性,这就是所谓的 widget editing mode。另外还有 signal and slot editing mode,这里可以直接把发出 signal 的 widget 拖向 slot 的 widget,这会产生一根箭头,然后填写对应的 signal 和 slot 即可。在 buddy editing mode 里,我们将一些原则上不接受键盘响应 widget 拖向相关接受键盘响应的 widget,这将让他们具有等效力的处理键盘的能力。在 tab order mode 里面我们设置按 TAB 时遍历的顺序。另外还有 resource editor 供我们管理资源,如使用的图片,action editor 让我们编辑菜单上的 action(最后将菜单项 or 工具栏与之连接)。新加入的 QUiLoader 类允许 Qt 能像 glade 一样处理 XML 文件描述的界面,并动态生成。

Qt 提供的当然远远不止仅仅一些 widget(与 gtkmm 相比),它还提供了对 accessibility、数据库 SQL、网络模块等等的支持,这个我们会在后文讨论。这里集中讨论 Qt designer 一些设计上的特性。

Qt Designer 里面可以预览 skin、添置自己的类似 CSS 的 stylesheet 将 widget 改变样式,这可以用 settings -> preference -> form 里面的 preview 打开,或者直接点某个 widget 的 styleSheet 自己添加。Qt 里面常用的 container 有 Frame、GroupBox、StackedWidget、TabWidget、ToolboxWidget、DockWidget,我们可以在里面放其他的 widget。

Qt 设计菜单和工具栏很简单,界面和功能是分开设计的,界面在编辑状态依照要求点击对应的地方就可以增加菜单项,菜单里面 & 可以产生一个键盘操作,如 &File 就会使得当菜单打开后按 F 调用该菜单。类似的,添加工具栏后可以在上面增加一些按钮。功能是用 Action Editor 编辑的。

下面我们来看如何利用 Qt Designer 设计的界面和我们的程序结合起来。首先要了解 Qt Designer 输出的 .ui 文件最后会被如何处理,这是 uic 程序所处理的,这会产生一个 .h 文件,比如 mywindow.ui 会产生一个 ui_mywindow.h 文件,该文件包含两个部分,一个是 Ui_MyWindow 类,这个类其实仅仅包含除了顶层 widget 以外所有的 widgets,比如我们的窗口用 QMainWindow 或者 QWidget 类,里面有若干 layout、button 等,那么产生的 Ui_MyWindow 类含有除了 QMainWindow 或 QWidget 外所有其他 components 的指针,并通过 setupUi( QMainWindow *) 方法将我们设计的界面呈现在指定的顶层 widget 上。另外在 Ui 名域空间声明了一个 MyWindow 类,继承 Ui_MyWindow,这是为了避免 namespace pollution。因此,预览我们窗体最简单的方法是用如下代码,

#include "ui_main.h"

int
main( int argc, char *argv[] )
{
  QApplication app( argc, argv ) ;
  QMainWindow *widget = new QMainWindow ;
  Ui::MainWindow ui ;
  ui.setupUi( widget ) ;
  widget -> show() ;
  return app.exec() ;
}
我们会在后面详细分析这里面相关代码,这里注意通过 new 产生一个 QMainWindow,然后注意用 Ui::MainWindow 产生一个仅仅生成代码的类实体,并且调用 setupUi() 方法在 widget 上产生我们需要的界面。最后调用 widget 的 show() 方法,并用 app.exec() 开始进入消息循环。

这里插一段关于 widget 释放的问题,注意 ui 里面通过调用 new 产生的控件在 widget 被摧毁的时候一起被摧毁,ui 里面并没有析构这些,因为其实最后都是 dangling pointer,这是很危险的,千万不要多做这一步。另外关闭窗口时会触发 QMainWindow::close() 这个 slot,这会析构窗体(如果设置 QWidget::DeleteOnClose 属性)。

可是我们在 Qt Designer 里面最多加入很简单的 signal/slot 连接,还有很多功能我们并不是在 Qt Designer 里面实现的,因此,我们需要用别的方式才能在不修改这部分自动生成代码的基础上实现自己的功能。最简单的办法就是单继承窗体类,把界面作为一个私有成员,

class MyWindow : public QMainWindow {
  // ...
private:
  Ui::MyWindow ui ;
} ;
这样在构造 MyWindow::MyWindow() 时通过 ui(this) 就可以创建自己的界面,然后进一步通过 QObject::connect() 等函数修改 signal/slot 连接。但是连接必须通过成员 ui 进行。

更方便的做法是使用多重继承,

class MyWindow : public QMainWindow, private Ui::MyWindow {
  // ...
public:
  MyWindow( QWidget *p ) : QMainWindow(p), Ui::MyWindow() {
    setupUi( this ) ;
    // ...
  }
} ;
这时我们可以直接对 MyWindow::component 进行 connect。不过值得注意的是 connect 只能在 QObject 之间进行,无法传递给非 QObject 对象,也无法 connect 到一般的函数。

另外一种就是和 glade 实现的类似,

QWidget* TextFinder::loadUiFile() {
  QUiLoader loader;

  QFile file(":/forms/textfinder.ui");
  file.open(QFile::ReadOnly);

  QWidget *formWidget = loader.load(&file, this);
  file.close();

  return formWidget;
 }
注意这样动态加载的不能使用 formWidget -> member 的形式调用,我们应该利用 QObject::objectName() 来搜索获得对应的地址,如
ui_findButton = qFindChild<QPushButton*>(this, "findButton");
我们后面会比较 gtkmm 在实现类似的功能上的区别。

创建 connection 最土的办法就是手工创建,因为前面说必须 connect 到一个 QObject 上,所以最直接的做法如下

ImageDialog::ImageDialog(QWidget *parent)
     : QDialog(parent)
{
  setupUi(this);
  okButton->setAutoDefault(false);
  cancelButton->setAutoDefault(false);
  // ...
  connect(okButton, SIGNAL(clicked()), this, SLOT(checkValues()));
 }
把 slot 就放在多重继承 QObject 的 private slots 里,但是 Qt 可以利用 QMetaObject 实现自动的连接,这是用约定的 on_objectName_signal 定义的 slots,用 QMetaObject::connectSlotsByName(QObject *) 连接,如多重继承的情况下,取 this,而从 ui 文件产生的界面用 QUiLoader::load() 返回的 widget 指针。一般用 uic 产生的代码会自动调用该函数。

Saturday, April 18, 2009

再看 Qt 的 QMetaObject

首先我们看看 Q_OBJECT 展开变成了什么,在 qobjectdefs.h 文件中有
#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_OBJECT_CHECK (插入了一个 qt_check_for_QOBJECT_macro 的 template function),然后是全局常量 QMetaObject 对象,因此可以用 QClassname::staticMetaObject 直接访问,另外提供了两个接口函数 metaObject() 用于不同的 class 返回自己的 staticMetaObject、qt_metacast() 用于转换,我们在 moc 产生的文件里面可以找到这两个接口的实现,
const QMetaObject *Counter::metaObject() const
{
 return &staticMetaObject;
}

void *Counter::qt_metacast(const char *_clname)
{
 if (!_clname) return 0;
 if (!strcmp(_clname, qt_meta_stringdata_Counter))
   return static_cast<void*>(const_cast< Counter*>(this));
 return QObject::qt_metacast(_clname);
}
后者很明显,如果需要转换的名字 _clname 是自己的类名,就把自己的指针通过转换成 void* 传回去,否则调用 QOject::qt_metacast(),其实就是看是不是 QObject 了,否则就返回 0 了。另外 QT_TR_FUNCTIONS 是对应的 i18n 的函数,我们后面再看。最后还有一个 qt_metacall 的接口,实现如下
int Counter::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
 _id = QObject::qt_metacall(_c, _id, _a);
 if (_id < 0)
   return _id;
 if (_c == QMetaObject::InvokeMetaMethod) {
   switch (_id) {
   case 0:
     valueChanged((*reinterpret_cast< int(*)>(_a[1])));
     break;
   case 1:
     setValue((*reinterpret_cast< int(*)>(_a[1])));
     break;
   default: ;
   }
   _id -= 2;
 }
 return _id;
}
这个函数起到一个中间作用,可以间接的调用成员方法,我们来仔细看看 QMetaObject,同一个文件里面有该结构的定义,我们只看这一部分,
struct Q_CORE_EXPORT QMetaObject
{
 const char *className() const;
 const QMetaObject *superClass() const;

 QObject *cast(QObject *obj) const;

 // ...

 struct { // private data
   const QMetaObject *superdata;
   const char *stringdata;
   const uint *data;
   const void *extradata;
 } d;
} ;
注意 Couter 类 QMetaObject 的初始化,
const QMetaObject Counter::staticMetaObject = {
 { &QObject::staticMetaObject, qt_meta_stringdata_Counter,
  qt_meta_data_Counter, 0 }
} ;
下面我们着重看看几个与 signal/slot 相关的代码,首先就是 [qt]/src/corelib/kernel/qobject.cpp 文件中关于 QObject::connect() 函数的代码,
bool QObject::connect(const QObject *sender, const char *signal,
                  const QObject *receiver, const char *method,
                  Qt::ConnectionType type)
{
 {
   const void *cbdata[] = { sender, signal, receiver, method, &type };
   if (QInternal::activateCallbacks(QInternal::ConnectCallback, (void **) cbdata))
     return true;
 }

 // checking sender, receiver, compatability of signal and slot

 QMetaObject::connect(sender, signal_index, receiver, method_index, type, types);
 const_cast<QObject*>(sender)->connectNotify(signal - 1);
 return true;
}
这里首先调用了 QInternal 这个 namespace 里面 activateCallbacks 这个函数,然后根据 QMetaObject 信息检查了 sender、receiver 以及对应 signal/slots 的匹配性,此时已经把 signal/slot 字符串转换成为了对应的 index,然后调用 QMetaObject::connect() 完成连接,最后的 QObject::connectNotify() 以及另外的 QObject::disconnectNotify() 其实是一个 signal。QInternal::activaeCallback() 在 [qt]/src/corelib/global/qglobal.cpp 中定义,
bool QInternal::activateCallbacks(Callback cb, void **parameters)
{
 Q_ASSERT_X(cb >= 0, "QInternal::activateCallback()", "Callback id must be a valid id");

 QInternal_CallBackTable *cbt = global_callback_table();
 if (cbt && cb < cbt->callbacks.size()) {
   QList<qInternalCallback> callbacks = cbt->callbacks[cb];
   bool ret = false;
   for (int i=0; i<callbacks.size(); ++i)
     ret |= (callbacks.at(i))(parameters);
   return ret;
 }
 return false;
}
这可以看出来调用该函数去检查一个 global_callback_table(),如果查到一个匹配的 signal/slot 就返回 true,否则返回 false,换言之 QObject::connect() 通过这个函数判定需不需要调用 QMetaObject::connect 创建新的连接。这个 callback table 本质是什么呢?同一个文件里面有
struct QInternal_CallBackTable {
QVector<QList<qInternalCallback> > callbacks;
};
所以,这是一个链表的动态数组(Orz... 本来以为就是个链表),而 qInternalCallback 是一个在 [qt]/src/corelib/global/qnamespace.h 中定义的函数指针,
typedef bool (*qInternalCallback)(void **);
现在我们可以猜测到在 QMetaObject::connect() 调用中我们会维护这个 callback table。在 [qt]/src/corelib/kernel/qmetaobject.cpp 我们有幸找到了如下代码:
bool QMetaObject::connect(const QObject *sender, int signal_index,
                      const QObject *receiver, int method_index, int type, int *types)
{
 QObject *s = const_cast<QObject *<(sender);
 QObject *r = const_cast<QObject *<(receiver);

 QOrderedMutexLocker locker(&s->d_func()->threadData->mutex,
                            &r->d_func()->threadData->mutex);

#if defined(Q_CC_HPACC) && defined(QT_ARCH_PARISC)
 QObjectPrivate::Connection c;
 c.receiver = r;
 c.method = method_index;
 c.connectionType = type;
 c.argumentTypes = types;
#else
 QObjectPrivate::Connection c = { r, method_index, type, Q_BASIC_ATOMIC_INITIALIZER(types) };
#endif
 s->d_func()->addConnection(signal_index, &c);
 r->d_func()->refSender(s, signal_index);

 if (signal_index < 0)
   sender->d_func()->connectedSignals = ~0u;
 else if (signal_index < 32)
   sender->d_func()->connectedSignals |= (1 << signal_index);

 return true;
}
这段代码中使用了防止多线程操作引起问题的 mutex,sender 和 receiver 双方都有一个结构来保证这个通讯机制,sender 是通过 addConnection,receiver 通过 refSender(),这里并没有我们猜测的 global callback table。现在我们有两个问题,一个是 sender 通过 addConnection() 和 receiver 通过 refSneder() 记录了一些什么,用什么数据结构储存,另一个是 global callback table 是做什么用的。

我们先来看看 global callback table,不难发现,该 table 是 QInternal 类(没有成员,提供了一个接口)的方法维护的,主要有 QInternal::registerCallback()、QInternal::unregisterCallback()、QInternal::activateCallbacks()、QInternal::callFunction(),意思我想都很清楚,可是 grep 了一圈,似乎只在某些调试部分看到了调用该函数的地方,莫非这是用来调试的代码?

我们来看看 QObject->d_func() 返回的是什么。

#define Q_DECLARE_PRIVATE(Class) \
  inline Class##Private* d_func() { return reinterpret_cast<Class##Private *>(d_ptr); } \
  inline const Class##Private* d_func() const { return reinterpret_cast<const Class##Private *>(d_ptr); } \
  friend class Class##Private;

#define Q_DECLARE_PRIVATE_D(Dptr, Class) \
  inline Class##Private* d_func() { return reinterpret_cast<Class##Private *>(Dptr); } \
  inline const Class##Private* d_func() const { return reinterpret_cast<const Class##Private *>(Dptr); } \
  friend class Class##Private;

#define Q_DECLARE_PUBLIC(Class)                                    \
  inline Class* q_func() { return static_cast<Class *>(q_ptr); } \
  inline const Class* q_func() const { return static_cast<const Class *>(q_ptr); } \
  friend class Class;

#define Q_D(Class) Class##Private * const d = d_func()
#define Q_Q(Class) Class * const q = q_func()
记得 QObject 的定义么?
class Q_CORE_EXPORT QObject
{
  Q_OBJECT
  Q_PROPERTY(QString objectName READ objectName WRITE setObjectName)
  Q_DECLARE_PRIVATE(QObject)
这样 QObject 通过 d_func() 返回 d_ptr 这个指针,这是怎么一回事呢?我们知道这个宏在 QObject 内部定义了一个 QObjectPrivate 的类,并且留下了一个指针
protected:
   QObjectData *d_ptr;
在 QObject 构造的时候创建了这个对象,并在析构的时候释放,由于是 friend class,可以对 QObject 无限制访问,
QObject::QObject(QObject *parent) : d_ptr(new QObjectPrivate)
QObject::~QObject()
{
   Q_D(QObject);
   // clear all signal receivers
   emit destroyed(this);
   // clear all signal senders
   delete d;
   d_ptr = 0;
}
可见真正管理 signal/slots 的是 QObjectPrivate 类,下面是它的主要成员函数,
//constructor and destructor
QObjectPrivate::QObjectPrivate(int version) ;
QObjectPrivate::~QObjectPrivate() ;
int *QObjectPrivate::setDeleteWatch(QObjectPrivate *d, int *w) ;
void QObjectPrivate::resetDeleteWatch(QObjectPrivate *d, int *oldWatch, int deleteWatch) ;
void QObjectPrivate::sendPendingChildInsertedEvents() ;
void QObjectPrivate::removePendingChildInsertedEvents(QObject *child) ;
bool QObjectPrivate::isSender(const QObject *receiver, const char *signal) const ;
QObjectList QObjectPrivate::receiverList(const char *signal) const ;
QObjectList QObjectPrivate::senderList() ;
// connection list manipulation
void QObjectPrivate::addConnection(int signal, Connection *c) ;
void QObjectPrivate::removeReceiver(int signal, QObject *receiver) ;
void QObjectPrivate::cleanConnectionLists() ;
// sender list manipulation
void QObjectPrivate::refSender(QObject *sender, int signal) ;
void QObjectPrivate::derefSender(QObject *sender, int signal) ;
void QObjectPrivate::removeSender(QObject *sender, int signal) ;
QObjectPrivate::Sender *QObjectPrivate::setCurrentSender(QObject *receiver, Sender *sender) ;
void QObjectPrivate::resetCurrentSender(QObject *receiver, Sender *currentSender, Sender *previousSender) ;
void QObjectPrivate::clearGuards(QObject *object) ;
那么我们如何存储数据的呢?在 [qt]/src/corelib/kernel/qobject_p.h 里,有该类的声明,
class Q_CORE_EXPORT QObjectPrivate : public QObjectData {
 // ...
public:
 QList pendingChildInsertedEvents;
 struct Sender {
   QObject *sender;
   int signal;
   int ref;
 };
 Sender *currentSender;
 QList<QPointer<QObject> > eventFilters;

 struct Connection {
   QObject *receiver;
   int method;
   uint connectionType : 3; // 0 == auto, 1 == direct, 2 == queued, 4 == blocking
   QBasicAtomicPointer<int> argumentTypes;
 };
 typedef QList<Connection> ConnectionList;
 QObjectConnectionListVector *connectionLists;

 QList<Sender> senders;
} ;
至此,我们已经找到了 Qt 实现 signal/slot 机制的所有需要知道的东西。

记得下面 moc 生成的代码

void Counter::valueChanged(int _t1)
{
   void *_a[] = { 0, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };
   QMetaObject::activate(this, &staticMetaObject, 0, _a);
}
这里重要的就是 QMetaObject::activate() 函数,其实它就是依次激活 senders 里面的函数,
void QMetaObject::activate(QObject *sender, int from_signal_index, int to_signal_index, void **argv) ;
void QMetaObject::activate(QObject *sender, int signal_index, void **argv) ;
void QMetaObject::activate(QObject *sender, const QMetaObject *m, int local_signal_index, void **argv) ;
void QMetaObject::activate(QObject *sender, const QMetaObject *m, int from_local_signal_index, int to_local_signal_index, void **argv) ;
这里调用的是第三个,他们最终都是用第一个实现的,下面是实现的基本代码,
void QMetaObject::activate(QObject *sender, int from_signal_index, int to_signal_index, void **argv)
{
   if (sender->d_func()->blockSig)
       return;
   // ...
   for (int signal = from_signal_index;
        (signal >= from_signal_index && signal <= to_signal_index) || (signal == -2);
        (signal == to_signal_index ? signal = -2 : ++signal))
   {
       int count = connectionLists->at(signal).count();
       for (int i = 0; i < count; ++i) {
           // signal type and etc
           receiver->qt_metacall(QMetaObject::InvokeMetaMethod, method, argv ? argv : empty_argv);
           // ...
       }
   //...
   }
   // ...
   --connectionLists->inUse;
   // ...
}

我们来回顾一下,使用 Qt 实现 signal/slots 机制是通过继承 QObject,并在类声明时加入 Q_OBJECT,该宏嵌入一个 QMetaObject 作为整个类实现 Qt 的 RTTI 机制的基础。继承 QObject 的同时使得该类含有一个 d_ptr 指向 QObject 共有的一个友元类 QObjectPrivate,每一个对象都会创建一个 QObjectPrivate 的对象来管理自己 signal/slot。对用户而言,通过声明 signal 其实是实现一个 protected function,它由 moc 生成程序,而 slot 本身就是一般的函数。signal 和 slot 通过调用 QObject::connect()/disconnect() 连接/切断连接,该函数验证连接的合理性(通过 QMetaObject 里面存放的字符串信息,如类名、父类、signal 和 slot 名称和参数),然后调用 QMetaObject::connect() 该方法会使用 mutex 保证操作的线程稳定性,访问两个对象的 d_ptr 指向的 QObjectPrivate 对象,对 sender 添加 connectionLists(一个链表),对 receiver 添加 senders(一个链表)。调用 signal 使用的 emit 其实什么都不是,可以直接调用该 signal 的函数也行,这会调用 QMetaObject::activate() 调用连接上的 slots。

Friday, April 17, 2009

Gtkmm 实现信号使用 libsigc++

这个 libsigc++ 只有一个终极目标,通过 C++ 的 template 机制实现 object 之间 signal/slot 功能。与 Qt 依赖 QMetaObject 不同在于,前者 hack C++ 的 template,而后者其实通过什么字符串等实现的,我们在这里仅仅实现一个简单的通信例子,介绍使用方法。我们将在后面的文章比较这两种实现需要的开销,细节。 整体上来看 libsigc++ 实现通信机制要简单很多,下面是 counter.hpp:
#ifndef COUNTER_HPP
#define COUNTER_HPP
#include <iostream>
#include <sigc++/sigc++.h>

class Counter {
int value ;
public:
sigc::signal<void, int> valueChanged ;
void setValue( int ) ;
Counter( int = 0 ) ;

friend std::ostream& operator<< ( std::ostream&, const Counter& ) ;
} ;

#endif
这里和 Qt 不同之处是需要通过 composite 一个 sigc::signal 的对象从而使得该类获得 signal/slot 处理能力,Qt 是依靠继承 QObject 获得(使用 Q_OBJECT 只是为了获得新的 QMetaObject)。下面是实现代码 counter.cpp:
#include <iostream>
#include "counter.hpp"

void
Counter::setValue( int val )
{
value = val ;
// emit the signal
valueChanged.emit( value ) ;
}

Counter::Counter( int val ) : value( val ) {}

std::ostream&
operator<< ( std::ostream& os, const Counter& c )
{
return os << c.value ;
}
这里通过 sigc::signal::emit 方法或者 sigc::signal::operator() 发送信号。最后是主文件 test_sigc.cpp:
#include <iostream>
#include <iomanip>
#include "counter.hpp"

using namespace std ;

int
main( int argc, char *argv[] )
{
Counter a, b( 10 ) ;

cout << "We have two counters, " << a
     << " and " << b << endl ;

a.valueChanged.connect( sigc::mem_fun( b, &Counter::setValue ) ) ;

a.setValue( 12 ) ;
cout << "We have two counters, " << a
     << " and " << b << endl ;

b.setValue( 5 ) ;
cout << "We have two counters, " << a
     << " and " << b << endl ;

return 0 ;
}
这里调用了 sigc::signal::connect 方法连接到一个 slot上,该 slot 是使用 sigc::mem_fun 产生的一个调用指定对象成员函数的 functor。其实 libsigc++ 提供了远远不止这些功能,我们将在下面仔细介绍其他的功能。 编译 libsigc++ 程序可以用 pkg-config,如
$ g++ -o test_sigc test_sigc.o counter.o `pkg-config sigc++-2.0 --libs`
通过以上例子我们可以大致获得一个使用该库的印象,需要 signal 功能的类通过 composition 或者继承(感觉似乎不大好,也许应该 protected 继承?)加入 sigc::signal<> 对象,该模板第一个参数是 signal 返回值,后面写上的话是 signal 的参数,可见通过 template 的检查保证了 type safe。之后使用 sigc::signal::connect 连接到一个 slot 上,该 slot 可以用 sigc::mem_fun() 创建(使用对象的成员函数)或者 sigc::fun_ptr() 创建(使用一般函数地址)。在该库编写的时候合适的地方通过调用 sigc::signal::emit() (或者重载的 operator())发出 signal。 简而言之,C++ 的 virtual function 为对象在继承过程中实现多态提供了基础,是一个同类函数同名操作的函数钩子;signal/slot 机制是为了对象之间互相通信留下的函数钩子。只是前者通过 vtable 很容易实现,后者需要依赖 template 和 functor 等比较高级的方式实现。

Thursday, April 16, 2009

Qt 如何实现的 Meta Object

前面提到了 Qt 实现的一个关键性技术,signal/slot。由于 Qt 使用了一个 moc 预处理,因此我们肯定会想知道 Qt 如何通过 moc 实现前面所说对于程序员的优点的呢?我们先通过一个简单的例子看看如何使用 QObject 这一些概念实现最基本的调用过程。然后我们探讨 Qt 实现这种扩展的方式是什么,后面因为要与 Gtkmm 的实现互相比较,而 Gtkmm 使用的是 libsigc++ 实现的对象通信机制,而且不使用预编译,因此可以发现很多有意思的东西。 下面这个例子声明了一个简单的 QObject 的子类,counter.hpp 文件
#ifndef COUNTER_HPP
#define COUNTER_HPP
#include <iostream>
#include <qobject.h>

class Counter : public QObject {
Q_OBJECT
int value ;
signals:
void valueChanged( int ) ;
public slots:
void setValue( int ) ;
public:
Counter( int = 0 ) ;

friend std::ostream& operator<< ( std::ostream&, const Counter& ) ;
} ;
#endif
我们可以看见我们需要通过继承 QObject 类,并且在 private 段声明 Q_OBJECT,signal 和 slots 其实对应的可以理解为 signal 是一个 callback 函数的链表(因为可以一个 signal 激发多个 slots),而 slots 就是常意的函数,因此也分 public/protected/private,可以继承、可以重载。下面是以上 Counter 类的实现,counter.cpp 文件
#include <iostream>
#include "counter.hpp"

void
Counter::setValue( int val )
{
value = val ;
// emit the signal
emit valueChanged( value ) ;
}

Counter::Counter( int val ) : value( val ) {}

std::ostream&
operator<< ( std::ostream& os, const Counter& c )
{
return os << c.value ;
}
然后下面是调用的主程序 main.cpp 文件,
#include <iostream>
#include <iomanip>
#include "counter.hpp"

using namespace std ;

int
main( int argc, char *argv[] )
{
Counter a, b( 10 ) ;

cout << "We have two counters, " << a
     << " and " << b << endl ;

QObject::connect( &a, SIGNAL( valueChanged(int) ),
                  &b, SLOT( setValue(int) ) ) ;

a.setValue( 12 ) ;
cout << "We have two counters, " << a
     << " and " << b << endl ;

b.setValue( 5 ) ;
cout << "We have two counters, " << a
     << " and " << b << endl ;

return 0 ;
}
我们需要首先在该目录中使用 qmake -project 生成一个 .pro 文件,该文件含有工程细节,然后使用 qmake 产生 Makefile,最后 make 就可以产生可执行文件了。我们看到在主程序中调用 QObject::connect 将一个 signal 和一个 slot 连接,这导致我们触发 a.setValue() 的时候 b 的相关函数也被调用了。 那么我们继续看看 make 之后出现了什么。除了目标代码和执行文件以外,还有一个 moc_counter.cpp,这是使用 moc 产生的一个中间文件,稍微研究该文件,我们就不难发现 Qt 实现这一过程的要点: 继承 QObject 是为了使用 QObject 里面定义的信号方面的函数,如 QObject::connect() 和 QObject::disconnect()、判定继承 QObject::inherits()、支持国际化 QObject::tr() 和 QObject::trUtf8()、属性 QObject::setProperty() 和 QObject::property(),而 Q_OBJECT 宏(定义在 [include]/qt4/QtCore/qobjectdefs.h)是为了在原来这个类中重新插入一个 const static 的 QMetaObject,这个将覆盖父类对应的 QMetaObject,该 QMetaObject 里面含有实现比 C++ 的 RTTI 更丰富功能以及 qobject_cast 等等的一个对象,因为该对象创建后不需要修改,整个 class 公用,所以是 const static 的,那个 moc_*.cpp 里面实际上就含有该 MetaObject 的初始化代码,编译的时候和自己实现的 .o 一起编译,最后连接的时候加入到可执行文件里面。 这个 MetaObject 的初始化 code 是
const QMetaObject Counter::staticMetaObject = {
{ &QObject::staticMetaObject, qt_meta_stringdata_Counter,
  qt_meta_data_Counter, 0 }
};
其中 string 的部分就是 slot 等,其实是以字符串形式存储的,当使用 SLOT/SIGNAL 宏的时候,应该是通过查寻字符串(这两个 macro 其实把代码转换成字符串传递给对应的函数)。 在 [include]/qt4/QtCore/qobjectdefs.h 里面有 MetaObject 类的定义,很多 QObject 的函数都是依赖这里面的函数的,比如 QMetaObject::connect()、QMetaObject::className()、QMetaObject::superClass() 等。
通过预处理,同时把 signals 替换为 protected 而把 * slots 的 slots 去掉。我们可以通过 g++ -E 获得预处理后的源文件,我们会发现 signal 对应的 protected 部分没有实现代码,但是在生成的执行代码中却有,这部分难道是 template 产生的?

Qt 的辩词

Qt 是一个跨平台的 C++ GUI 库,曾经稍微了解了一点,现在重新开始向技术方靠拢,所以再次开始研究这些库,写一些程序准备找工作。我打算比较学习 Qt 和 Gtkmm 这两个 C++ 的库,后者因为依赖于 Gtk+那一套东西,可能学习起来更花时间,于是从 Qt 开始好了。

Qt 的一个特点是提供的 widget 有一个公共的祖先 class,就是 QObject,这并不是一个常意下的 C++ object model,Qt 使用了一个预编译程序 Meta object compiler(简称 moc)扩展了 C++ 的模型,程序员看到的是一个扩展语法下的 class declaration,而 moc 将这个扩展结构解析成为标准 C++ 模型。使用这个方式,而不是 Gtkmm 直接使用 C++ 的方式实现的 GUI library 有他自己的理由

语法

使用 Qt 对 C++ 语法的扩展能够更加简洁、直观的表达 GUI 里面事件驱动过程。其实主要是让新手建立起 signal/slot 的概念,这是 Qt 编程的关键部分。类似扩展语法的如 Borland C++ Builder 也通过类似的手法增强了 C++ object model。

代码自动生成

moc 自动的将 Qt object model 转换成为标准的 C++ model,为程序员提供了一个透明的机制,很多实现相当功能的 C++ 编程技巧(如 procted 成员)、多线程需要注意的保护机制等,将由 moc 自动用一种规范化的方式提供。其实总觉得使用了这种 generator 使得 C++ 编程变得 ugly,但是事实上也可以认为节省了很多精力,可以集中在编程本身,而不是实现这些功能的细节上。另外 Qt 的界面产生程序 user interface compiler (uic)都是 code generator 的典型应用。

GUI 是动态的

C++ 的 object model 是静态的,这意味着一定程度上对 GUI 编程需求的不满足。事实上有很多方式来解决这个问题,Qt 提供的是对程序员透明的,为 QObject 可以动态的添加 property、改变 signal/slot,并且保证 type-safe。

调用开销不是所有的

我们为了获得以上 Qt 提供的所有便捷性的时候应该知道比起使用 template 实现的版本而言,Qt 这一套机制开销大约是前者的 10 倍,但是一般如果使用该功能不是非常频繁(在某个内层循环里面拼命发 signal),这部分一般不会成为瓶颈,往往是程序实现的某些功能本身是开销的主要来源。

其他

使用国际化(i18n)程序需要使用 tr() 以及比 C++ 更好的 RTTI,qobject_cast 都需要使用 moc。

我们将先探索一下 Qt 的 signal/slot 结构。