Showing posts with label Qt. Show all posts
Showing posts with label Qt. Show all posts

Monday, October 11, 2010

Qt 的 OpenGL 支持

首先需要知道 OpenGL 是一个渲染 3D 图像的接口,其定义与语言几乎无关。我们这里直接在 C/C++ 里面使用对应的 OpenGL 语言。说到头,OpenGL 只是渲染一个图像,因此很多做图的原语被反复的使用。我们知道的 Mesa 本质上是实现了一个 CPU 版本(以及少量显卡硬件支持)的 OpenGL。为什么需要 OpenGL 呢?如果每个显卡都有自己的一套渲染方案,但提供给用户的 API 不同,则每个图形应用程序都需要针对特定的显卡进行编写。而如果显卡提供了一致的标准,编写程序的时候则会轻松很多。但是这要求显卡提供商实现两部分内容。一部分是驱动程序,被 OS 隐藏起来,用于直接与硬件打交道;另一部分是用来向 OS 提交请求的用户 API 接口。这样显卡提供商一方面需要尽可能的通过自己显卡的硬件环境优化 OpenGL 的渲染实现,也需要通过 marshalling 减少从用户态转换到内核态的次数(避免每次 OpenGL 调用都 involve 一次转换)。这样将一些 rendering 的工作(特别是大量 3D 渲染)从 CPU 转移到 GPU 之后就能加速应用程序。

第二,Qt 对 OpenGL 的支持在于它提供了一个用于显示 OpenGL 渲染结果的 widget,即 QGLWidget,如果我们需要显示 OpenGL 的渲染结果,我们就应该继承 QGLWidget,然后实现它三个方法:initializeGL() 用于初始化 OpenGL,通常设置一些前期的背景色、光照等等;paintGL() 用于渲染整个场景;resizeGL() 用于在 widget 大小变化的时候产生合理的 view。

第三,如果需要设置动画效果,本质上是在 paintGL() 过程中加入某些变化的参数,这样每次调用 paintGL() 的时候产生不同的渲染结果。实现动画需要调用 QTimer 之类的定时器,而这些对象并不能访问前面的这些方法(因为是 protected 的),因此一般调用含有 updateGL() 方法的 slot,将 QTimer 的某些 signal 与之 connect 即可。

第四,如果需要加入鼠标和键盘的操作,应该设置 mousePressEvent()、mouseMoveEvent() 等事件,将需要的方法绑定到对应的事件中。

总而言之,OpenGL 的使用并不神秘,只是我们需要进一步理解其中的一些基本概念。下面是一个简单的 OpenGL 程序,可以看到前面第二点里面所说的三个要素,我们在后面的分析中会逐步加深。下面是继承 QGLWidget 类的声明
#ifndef GLWIDGET_H
#define GLWIDGET_H

#include <QGLWidget>
#include <QVector>
#include <gl.h>

class GLWidget : public QGLWidget {
 public:
  GLWidget( QWidget* = NULL, const QGLWidget* = NULL,
            Qt::WindowFlags = Qt::Widget ) ;

 protected:
  void initializeGL();
  void paintGL();
  void resizeGL(int width, int height);

 private:
  QVector<GLuint> stars ;
} ;

#endif
下面是相关实现,
#include "glwidget.h"

GLWidget::GLWidget( QWidget* parent, const QGLWidget* shared,
                    Qt::WindowFlags flags)
  : QGLWidget( parent, shared, flags ), stars(1) {
}

void
GLWidget::initializeGL() {
  QGLWidget::initializeGL() ;
  glClearColor(0.0, 0.0, 0.0, 0.0);
  glClearDepth( 1.0 ) ;
}

void
GLWidget::paintGL() {
  glClearColor( 0.0, 0.0, 0.0, 0.0 ) ;
  glClear (GL_COLOR_BUFFER_BIT);

  glLoadIdentity() ;
  glColor3f (1.0, 1.0, 1.0);
  glOrtho(0.0, 1.0, 0.0, 1.0, -1.0, 1.0);
  glBegin(GL_POLYGON);
    glVertex3f( 0.25, 0.25, .0 ) ; 
    glVertex3f( 0.75, 0.25, .0 ) ; 
    glVertex3f( 0.75, 0.75, .0 ) ; 
    glVertex3f( 0.25, 0.75, .0 ) ;
  glEnd();
  glFlush();
}

void
GLWidget::resizeGL(int w, int h ) {
  glViewport( 0, 0, w, h ) ;
  glMatrixMode( GL_PROJECTION ) ;
  glLoadIdentity() ;
}
我想后面我们应该会更关注 OpenGL 本身,而不仅仅在 Qt 上。

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

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。

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 结构。