Monday, August 01, 2011

QGLWidget 使用 paintEvent 与 paintGL 的区别

QGLWidget 提供了 OpenGL 的封装,通常我们不必写独立的 paintEvent 而只需要重写 paintGL 事件即可。如果需要利用 QPainter 实现基本的 over painting(在 OpenGL 场景上绘制独立的 2D 图案或者文字),我们就有必要在 paintEvent 里面进行修改而不是改 paintGL。

那么应该如何修改 paintEvent 呢(和修改 paintGL 的区别)?我发现主要有下面几点:
  • paintEvent 里面必须创建 QPainter 对象,不论画或者不画 2D 的东西,否则结果不对;我曾很天真的认为如果我不画 2D 的,就没必要创建 QPainter,这是完全不对的。
  • paintEvent 里面需要完全依照先 glPushMatrix 最后 glPopMatrix 的逻辑来写,还需要手工设置 viewport;之后才能利用 QPainter 画图;如果直接写 paintGL 是可以偷懒的;
  • 我原先因为偷懒所以 selection 部分就没有弄完整的画图动作,这在 paintEvent 实现里面是不行的,必须全部实现 OK;

Saturday, October 23, 2010

OpenGL 的工作流程

上图为 OpenGL Programming Guide 中的图片,特此说明。

我们需要理解 OpenGL 的工作机制。OpenGL本身仅仅支持最原始点、线段和平面的绘制,因此多数情况下我们都是准备好 vertex 数据,这些或者组织成为 display list 提交给显卡的 evaluator,这个 evaluator 将点信息转换成为需要描述的面的信息(如法向量等,这个功能在创建复杂的几何形体如曲面等时会经常用到);之后通过设定的 per-vertex 操作,如进行几何变换,计算纹理位置,光源、材料等等;然后是 assemble,获得显示到窗口的投影坐标。这之后就需要将这些计算出来的数据进行 rasterization(像素化,栅格化),同时将纹理信息加入到这些面上,进一步就可以做 fragment 操作(如根据深度去掉被遮挡的、又或者进行 blending 等,一般每种操作对应某种 buffer),最后将结果放到某个 buffer 中(可能是用于显示的,也可能是用于 swap 的)。而一般外部的位图数据,一般会首先经过处理(如放缩、平移、映射等)形成纹理。

编写 OpenGL 的程序要理解清楚,我们所绘制的对象是一个所谓的 OpenGL context,这个 context 里面包括了所有打开的 OpenGL 使用的 buffer 等对象,我们做图时往往需要指定一个 context,做图的过程就是将这个 context 的状态不停的改变,比如我们希望改变渲染的对象,我们会在 GL_RENDER 下切换到矩阵的 GL_MODELVIEW 里面,而如果只是改变投影我们是在 GL_PROJECTION 里面;如果我们需要选择所画的某些对象,我们会切换到 GL_SELECT 这种模式,并通过 GL_PROJECTION 设定我们观看的区域,然后切换回 GL_MODELVIEW 重新绘制,退回 GL_RENDER 的时候就会获得一个被选择的对象列表。所以我们需要搞清楚有些什么样的状态,对应状态下可以进行一些什么操作。

推荐的文档有两个,一个是 OpenGL 红皮书(红宝书?)即 OpenGL Programming Guide,现在已经到第 7 版了,非常经典的书,现在也开始涉及到 OpenGL 3.0 的东西,最新的 4.1 尚未涉及;4.1 相关的主要是 NVIDIA 在推动;这本书网上有电子版,中文版只能说翻译质量一般;另一个自然就是官方的 spec,网上可以免费下载(见这里)。

另外,我们需要理解清楚下面几个概念。
  • OpenGL 与 WGLAGLGLX 的关系:OpenGL 本质上和窗口系统无关,因此如果需要在某些窗体环境中使用 OpenGL 需要这些 GUI 提供某种 OpenGL 扩展,WGL 是 Windows 的扩展,AGL(另外还有 CGL、Cocoa 的 NSOpenGL 类)是苹果的扩展,而 GLX 是 X11 扩展(并且允许 network-transparent 的 OpenGL 渲染)。
  • OpenGL 与 GLU 和 GLUT 之间的关系:前面说了 OpenGL 本身仅仅提供了非常原始的操作;GLU 另外提供了层次纹理(mipmapping)、简单的矩阵操作(更加方便)、polygon tessellation(将曲面分解成为多边形)、二次曲面以及 NURBS 的支持;GLUT 提供的是接口统一跨平台的 GUI 接口。
后面我们会学习红皮书和 GLU 方面的东西。

Friday, October 22, 2010

OpenGL Utility Toolkit 使用指南

嗯,这个东西非常轻量化,如果希望写一些 OpenGL 的测试程序非常合适。OpenGL Utility Toolkit 简称 GLUT,虽然并不是开源软件,但是多数平台下面都有,并且接口一致简洁。这里有它的主页,上面有文档和源代码。

初始化

void glutInit( int argc, char* argv[] ) ;
用于初始化 glut 程序,会处理一些相关的选项。

void glutInitWindowSize( int width, int height ) ;
void glutInitWindowPosition( int x, int y ) ;
初始化窗口的大小和位置。

void glutInitDisplayMode( unsigned int mode ) ;
支持 GLUT_ 系模式,主要是设置颜色支持,打不打开各种 buffer;

事件处理

void glutMainLoop( void ) ;
事件循环。

窗口管理

int glutCreateWindow( char* name ) ;
创建窗口,返回窗口编号;

int glutCreateSubWindow( int win,
    int x, int y, int width, int height ) ;
在给定窗口内,创建子窗口。

void glutSetWindow( int win ) ;
int glutGetWindow( void ) ;
选择或者获得当前(用于做图的)窗口;

void glutDestroyWindow( int win ) ;
释放窗口;

void glutPostRedisplay( void ) ;
要求重绘当前窗口;

void glutSwapBuffer( void ) ;
如果当前窗口使用了 double buffering 就进行 buffer 交换;

void glutPositionWindow( int x, int y ) ;
重新放置当前窗口;

void glutReshapeWindow( int width, int height ) ;
改变当前窗口大小;

void glutFullScreen( void ) ;
将当前窗口切换到全屏;

void glutFullScreen( void ) ;
切换到全屏。

void glutPushWindow( void ) ;
void glutPushWindow( void ) ;
改变 window 堆积的顺序。

void glutShowWindow( void ) ;
void glutHideWindow( void ) ;
void glutIconifyWindow( void ) ;
显示、隐藏与最小化窗口。

void glutSetWindowTitle( char* name ) ;
void glutSetIconTitle( char* name ) ;
设定窗口和图标的标题。

void glutSetCursor( int cursor ) ;
设定鼠标形状;

overlay 管理

所谓的 overlay 指在 OpenGL 窗口上覆盖的另一个 OpenGL 绘制区域。

void glutEstablishOverlay( void ) ;
在当前窗口上创建 overlay;

void glutUseOverlay( GLenum ) ;
设定使用原始窗口(GL_NORMAL)还是 overlay(GL_OVERLAY);

void glutRemoveOverlay( void ) ;
移除所创建的 overlay;

void glutPostOverlayRedisplay( void ) ;
重绘 overlay。

void glutShowOverlay( void ) ;
void glutHideOverlay( void ) ;
现实或者隐藏 overlay;

菜单管理

int glutCreateMenu( void (*func)(int val) ) ;
创建菜单,使用的 callback 函数在菜单被点击后会调用,传入的是所点的菜单项。返回菜单 id;

void glutSetMenu( int menu ) ;
int glutGetMenu(void) ;
设定 menu 以及获得使用的 menu 编号;

void glutDestroyMenu( void ) ;
析构 menu 对象。

void glutAddMenuEntry( char* name, int value ) ;
void glutAddSubMenu( char* name, int value ) ;
添加(子)菜单项。

void glutChangeToMenuEntry( int entry, char* name, int index ) ;
void glutChangeToSubMenuEntry( int entry, char* name, int index ) ;
设定(子)菜单的新的索引值(传递给 callback 函数用的);

void glutRemoveMenuItem( int entry ) ;
删掉菜单项。

void glutAttachMenu( int button ) ;
void glutDetachMenu( int button ) ;
将按钮关联到菜单。

注册 callback 函数

void glutDisplayFunc( void (*func)(void) ) ;
用于绘制窗口的 callback 函数。一般会把所有的画图部分放在这个函数里面,通过几个参数控制最后的行为。

void glutOverlayDisplayFunc( void (*func)(void) ) ;
设定 overlay 的绘图过程。

void glutReshapeFunc( void (*func)( int width, int height) ) ;
设定窗口大小改变后的行为。

void glutKeyboardFunc( void (*func)(unsigned char key, int x, int y) ) ;
设定响应键盘的 callback 函数。

void glutMouseFunc( void (*func)(int button, int state, int x, int y) ) ;
设定响应鼠标的函数。

void glutMotionFunc( void (*func)(int x, int y) ) ;
void glutPassiveMotionFunc( void (*func)(int x, int y) ) ;
设定响应鼠标按下后行为与一般移动行为的 callback 函数。

void glutEntryFunc( void (*func)( int state) ) ;
鼠标进入、退出窗口时触发;

void glutSpecialFunc( void (*func)(int key, int x, int y) ) ;
响应特殊按键;

void glutSpaceballMotionFunc( void (*func)(int x, int y, int z) ) ;
void glutSpaceballRotateFunc( void (*func)(int x, int y, int z) ) ;
void glutSpaceballButtonFunc( void (*func)(int button, int state ) ) ;
滚动球的移动、旋转和点击行为;

void glutButtonBoxFunc( void (*func)(int button, int state) ) ;
点击按钮的行为;

void glutDialsFunc( void (*func)(int dial, int value) ) ;
设置 dial 和 button 的行为。

void glutTabletMotionFunc( void (*func)( int x, int y) ) ;
void glutTabletButtonFunc( void (*func)( int button, int state, int x, int y) ) ;
设置响应平板操作的函数;

void glutMenuStatusFunc( void (*func)(int status, int x, int y) ) ;
void glutMenuStateFunc( void (*func)(int status) ) ;
设置菜单的行为;

void glutIdleFunc( void (*func)(void) ) ;
如果无事可做就会被调用的 callback 函数;

void glutTimerFunc( unsigned int msecs, void (*func)(int value), int value ) ;
设定计时器;

色彩索引管理

通过 glutGet( GLUT_WINDOW_COLORMAP_SIZE ) 可以获得可使用的 pallete 大小,之后可以用
void glutSetColor( int cell, GLfloat r, GLfloat g, GLfloat b ) ;
GLfloat glutGetColor( int cell, int component ) ;
设定或者获得每个调色板的颜色;

void glutCopyColormap( int win ) ;
可以复制指定窗口的 colormap;

状态获得

可以用
void glutGet( GLenum state ) ;
获得需要的状态;

void glutLayerGet( GLenum info ) ;
获得当前 layer 的信息;

void glutDeviceGet( GLenum info ) ;
获得设备相关信息;

void glutGetModifiers( void ) ;
获得 shift、ctrl 和 alt 的状态;

void glutExtensionSupported( char* extentsion ) ;
返回某些 OpenGL 扩展是否被支持;

字体支持

void glutBitmapCharacter( void* font, int chararacter ) ;
字体中某个字符的 OpenGL 渲染;

int glutBitmapWidth( GLUTbitmapFont font, int chararacter ) ;
返回字体宽度;

void glutStrokeCharacter( void* font, int chararacter ) ;
使用轮廓字体;

int glutBitmapWidth( GLUTstrokeFont font, int chararacter ) ;
返回轮廓字体宽度;

几何体的渲染

这也是我需要好生研究一下 src code 的部分。

void glutSolidSphere( GLdouble radius, GLint slices, GLint stacks ) ;
void glutWireSphere( GLdouble radius, GLint slices, GLint stacks ) ;
画球;

void glutSolidCube( GLdouble size ) ;
void glutWireCube( GLdouble size ) ;
画正方体;

void glutSolidCone( GLdouble base, GLdouble height, GLint slices, GLint stacks ) ;
void glutWireCone( GLdouble base, GLdouble height, GLint slices, GLint stacks ) ;
锥体,base 设定的是底面圆的半径;

void glutSolidTorus( GLdouble innerRadius, GLdouble outerRadius, GLint nsides, GLint rings ) ;
void glutWireTorus( GLdouble innerRadius, GLdouble outerRadius, GLint nsides, GLint rings ) ;
圆环;

void glutSolidDodecahedron( void ) ;
void glutWireDodecahedron( void ) ;
正 12 面体,每个面是五边形;

void glutSolidOctahedron( void ) ;
void glutWireOctahedron( void ) ;
正八面体,每个面是三角形;

void glutSolidTetrahedron( void ) ;
void glutWireTetrahedron( void ) ;
正四面体,每个面是三角形;

void glutSolidIcosahedron( void ) ;
void glutWireIcosahedron( void ) ;
二十面体;

void glutSolidTeapot( GLdouble size ) ;
void glutWireTeapot( GLdouble size ) ;
茶壶;

其他内容

其实 GLUT 的实现有很多免费的版本,如 freeglutOpenGLUT。而 OpenGL 本身也有一个开源的实现,即 mesa,想看源代码,可能更多的应该是 GLU,毕竟 GLUT 提供的主要是窗口操作方面的东西。写轻量级的 OpenGL 程序比较合适。

Saturday, October 16, 2010

depth buffer 与 blending buffer

OpenGL 通过 depth buffer 维护最后显示在屏幕上的像素,深度上较大的像素会被较小的覆盖;blending buffer 主要是为了渲染半透明的效果,主要是为了将不同层次的能够融合在一起。两个 buffer 作用的顺序是先 depth 后 blend。

这导致了一个问题,如果我们打开了两个 buffer,那么深度测试后被遮挡的物体将被剔除,进入 blending buffer 的时候不存在 blending 的问题了。因此 blending 失效。

解决的方案是,打开 depth buffer,先画不需要 blending 的物体,然后自己计算这些半透明物体的深度,并按照深度从深到浅画出。

相关的讨论见 NeHe 的 tutorial 以及 OpenGL 官方的 FAQ

billboard 的 OpenGL 实现

原文见这里

基本想法是两个:

要不我把当前的 MODELVIEW 矩阵拿出来,把旋转去掉,这只需要把那个矩阵的 3x3 部分变成单位阵就行了,因为把矩阵理解成为对当前点的变换可以发现我们仅仅需要平移到当前点,然后画需要的 billboard 就好了。

要不我把当前的 MODELVIEW 矩阵拿出来,3x3 那部分里面每行相当于是在 x、y、z 上的投影,因此只需要取前两行作为方向计算该处物体的坐标就行了。

一般 billboard 都是矩形,因此计算量不算很大,后者需要用 CPU 计算所有的 billboard 坐标,如果 billboard 较少比较适合;前者实际上需要将矩阵从显存放到内存,然后写回去,如果 billboard 计算量太小则不太适合。

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

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

Wednesday, February 28, 2007

A detailed exploration of gears.c

As soon as I saw gears.c, I decided to make it my first experiment of the library. Here is a snapshot of the program.

So, let's focus on its source.
int
main (int argc, char *argv[])
{
GdkGLConfig *glconfig;
GtkWidget *window;
GtkWidget *vbox;
GtkWidget *drawing_area;
GtkWidget *button;

gtk_init (&argc, &amp;amp;amp;argv);
gtk_gl_init (&argc, &argv);
glconfig = gdk_gl_config_new_by_mode (GDK_GL_MODE_RGB    |
                                      GDK_GL_MODE_DEPTH  |
                                      GDK_GL_MODE_DOUBLE);
if (glconfig == NULL) {
  g_print ("*** Cannot find the double-buffered visual.\n");
  g_print ("*** Trying single-buffered visual.\n");

  glconfig = gdk_gl_config_new_by_mode (GDK_GL_MODE_RGB   |
                                        GDK_GL_MODE_DEPTH);
  if (glconfig == NULL) {
    g_print ("*** No appropriate OpenGL-capable visual found.\n");
    exit (1);
  }
}

window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
gtk_window_set_title (GTK_WINDOW (window), "gears");
gtk_container_set_reallocate_redraws (GTK_CONTAINER (window), TRUE);

g_signal_connect (G_OBJECT (window), "delete_event",
                  G_CALLBACK (gtk_main_quit), NULL);

vbox = gtk_vbox_new (FALSE, 0);
gtk_container_add (GTK_CONTAINER (window), vbox);
gtk_widget_show (vbox);

drawing_area = gtk_drawing_area_new ();
gtk_widget_set_size_request (drawing_area, 300, 300);

gtk_widget_set_gl_capability (drawing_area,
                              glconfig,
                              NULL,
                              TRUE,
                              GDK_GL_RGBA_TYPE);

gtk_widget_add_events (drawing_area,
                       GDK_VISIBILITY_NOTIFY_MASK);

g_signal_connect_after (G_OBJECT (drawing_area), "realize",
                        G_CALLBACK (init), NULL);
g_signal_connect (G_OBJECT (drawing_area), "configure_event",
                  G_CALLBACK (reshape), NULL);
g_signal_connect (G_OBJECT (drawing_area), "expose_event",
                  G_CALLBACK (draw), NULL);
g_signal_connect (G_OBJECT (drawing_area), "map_event",
                  G_CALLBACK (map), NULL);
g_signal_connect (G_OBJECT (drawing_area), "unmap_event",
                  G_CALLBACK (unmap), NULL);
g_signal_connect (G_OBJECT (drawing_area), "visibility_notify_event",
                  G_CALLBACK (visible), NULL);

g_signal_connect_swapped (G_OBJECT (window), "key_press_event",
                          G_CALLBACK (key), drawing_area);

gtk_box_pack_start (GTK_BOX (vbox), drawing_area, TRUE, TRUE, 0);

gtk_widget_show (drawing_area);
button = gtk_button_new_with_label ("Quit");

g_signal_connect (G_OBJECT (button), "clicked",
                  G_CALLBACK (gtk_main_quit), NULL);

gtk_box_pack_start (GTK_BOX (vbox), button, FALSE, FALSE, 0);
gtk_widget_show (button);
gtk_widget_show (window);

gtk_main ();
return 0;
}
Here we will explain the whole thing step by step. There are two init functions in the beginning, which extracts those options they care so that the rest are what the application really cares about.

Friday, February 09, 2007

The relationship of several libraries

As is mentioned in the web sites of GTK+ and Gtkmm, there are several packages which are highly related to the topics here. Their relationship is of utmost importance to the understanding of the mechanisms of GTK+ and Gtkmm.

GTK+ is originally GTK, GIMP ToolKit. There are three supportive libraries, GLib, Pango and ATK. GLib provides lots of related functions that GTK+ widgets rely on, such signal system(here the term signal is different from the one Linux OS uses to send to processes). Pango handles layout and rendering texts. And ATK contains interfaces for accessibility.

However, the term GDK, GIMP Drawing Kit, can be found from time to time in the related materials. GDK is simply a wrapper for Xlib and if familiar with Xlib, it is not that essential here.

Gtkmm, a C++ wrapper of GTK+, provides a different developing model. It relies on libsigc++, glibmm. Here libsigc++ is a C++ flavor of realization of signal system.

GNOME project prospers on the GTK+. And based on Gtkmm, there is also a gnomemm library. Glade is a tool for creating GUI of GTK+ and libglade provides an alternative with XML as the GUI descriptor and dynamic creation of GUI. Not surprisingly, there will be a libglademm.

In order to compile several programs listed later, we need several packages:

  • GTK: gtk2, gtk2-devel
  • gtkglext: gtkglext, gtkglext-devel
  • gtkmm: gtkmm24, gtkmm24-devel
  • libglade: libglade, libglade-devel

A blueprint

Here are several things that I have decided to read. The progress will be exhibited here.

GTK+, a light GUI library, we have a pieve of tutorial for GTK+-2.0 and related FAQ. A C++ wrapper is provided, with the name Gtkmm. They kindly give us an API documentation and its FAQ. There is an OpenGL interface for GTK+, called GtkGLExt. There are few materials on it yet. I have to find more later on.