Showing posts with label meta object model. Show all posts
Showing posts with label meta object model. Show all posts

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 产生的代码会自动调用该函数。

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 产生的?