使用元对象编译器

Qt 3.0.5

主页 | 所有的类 | 主要的类 | 注释的类 | 分组的类 | 函数

使用元对象编译器

元对象编译器,朋友中的moc,是处理Qt的C++扩展的程序。

元对象编译器读取一个C++源文件。如果它发现其中的一个或多个类的声明中含有Q_OBJECT宏,它就会给这个使用Q_OBJECT宏的类生成另外一个包含元对象代码的C++源文件。尤其是,元对象代码对信号/槽机制、运行时类型信息和动态属性系统是需要的。

一个被元对象编译器生成的C++源文件必须和这个类的实现一起被编译和连接(或者它被包含到(#include)这个类的源文件中)。

如果你是用qmake来生成你的Makefile文件,当需要的时候,编译规则中需要包含调用元对象编译器,所以你不需要直接使用元对象编译器。关于元对象编译器的更多的背景知识,请看为什么Qt不用模板来实现信号和槽?

用法

元对象编译器很典型地和包含下面这样情况地类声明地输入文件一起使用:

    class MyClass : public QObject
    {
        Q_OBJECT
    public:
        MyClass( QObject * parent=0, const char * name=0 );
        ~MyClass();

    signals:
        void mySignal();

    public slots:
        void mySlot();

    };

除了上述提到地信号和槽,元对象编译器在下一个例子中还将实现对象属性。Q_PROPERTY宏声明了一个对象属性,而Q_ENUMS 声明在这个类中的属性系统中可用的枚举类型的一个列表。在这种特殊的情况下,我们声明了一个枚举类型属性Priority,也被称为“priority”,并且读函数为priority(),写函数为setPriority()

    class MyClass : public QObject
    {
        Q_OBJECT
        Q_PROPERTY( Priority priority READ priority WRITE setPriority )
        Q_ENUMS( Priority )
    public:
        MyClass( QObject * parent=0, const char * name=0 );
        ~MyClass();

        enum Priority { High, Low, VeryHigh, VeryLow };
        void setPriority( Priority );
        Priority priority() const;
    };

属性可以通过Q_OVERRIDE宏在子类中进行修改。Q_SETS宏声明了枚举变量可以进行组合操作,也就是说可以一起读或写。另外一个宏,Q_CLASSINFO,用来给类的元对象添加名称/值这样一组数据:

    class MyClass : public QObject
    {
        Q_OBJECT
        Q_CLASSINFO( "Author", "Oscar Peterson")
        Q_CLASSINFO( "Status", "Very nice class")
    public:
        MyClass( QObject * parent=0, const char * name=0 );
        ~MyClass();
    };

这三个概念:信号和槽、属性和元对象数据是可以组合在一起的。

元对象编译器生成的输出文件必须被编译和连接,就像你的程序中的其它的C++代码一样;否则你的程序的连编将会在最后的连接阶段失败。出于习惯,这种操作是用下述两种方式之一解决的:

方法一:类的声明放在一个头文件(.h文件)中

如果在上述的文件myclass.h中发现类的声明,元对象编译器的输出文件将会被放在一个叫moc_myclass.cpp的文件中。这个文件将会像通常情况一样被编译,作为对象文件的结果是moc_myclass.o(在Unix下)或者moc_myclass.obj(在Windows下)。这个对象接着将会被包含到一个对象文件列表中,它们将会在程序的最后连编阶段被连接在一起。

方法二:类的声明放在一个实现文件(.cpp文件)中

如果上述的文件myclass.cpp中发现类的声明,元对象编译器的输出文件将会被放在一个叫myclass.moc的文件中。这个文件需要被实现文件包含(#include),也就是说myclass.cpp需要包含下面这行
    #include "myclass.moc"
放在所有的代码之后。这样,元对象编译器生成的代码将会和myclass.cpp中普通的类定义一起被编译和连接,所以方法一中的分别编译和连接就是不需要的了。

方法一是常规的方法。方法二用在你想让实现文件自包含,或者Q_OBJECT类是内部实现的并且在头文件中不可见的这些情况下使用。

Makefile中自动使用元对象编译器的方法

除了最简单的测试程序之外的任何程序,建议自动使用元对象编译器。在你的程序的Makefile文件中加入一些规则,make就会在需要的时候运行元对象编译器和处理元对象编译器的输出。

我们建议使用Trolltech的自由makefile生成工具,qmake,来生成你的Makefile。这个工具可以识别方法一和方法二风格的源文件,并建立一个可以做所有必要的元对象编译操作的Makefile。

另一方面如果,你想自己建立你的Makefile,下面是如何包含元对象编译操作的一些提示。

对于在头文件中声明了Q_OBJECT宏的类,如果你只使用GNU的make的话,这是一个很有用的makefile规则:

    moc_%.cpp: %.h
            moc $< -o $@

如果你想更方便地写makefile,你可以按下面的格式写单独的规则:

    moc_NAME.cpp: NAME.h
            moc $< -o $@

你必须记住要把moc_NAME.cpp添加到你的SOURCES(你可以用你喜欢的名字替代)变量中并且把moc_NAME.o或者moc_NAME.obj添加到你的OBJECTS变量中。

(当我们给我们的C++源文件命名为.cpp时,元对象编译器并不留意,所以只要你喜欢,你可以使用.C、.cc、.CC、.cxx或者甚至.c++。)

对于在实现文件(.cpp文件)中声明Q_OBJECT的类,我们建议你使用下面这样的makefile规则:

    NAME.o: NAME.moc

    NAME.moc: NAME.cpp
            moc -i $< -o $@

这将会保证make程序会在编译NAME.cpp之前运行元对象编译器。然后你可以把

    #include "NAME.moc"

放在NAME.cpp的末尾,这样在这个文件中的所有的类声明被完全地知道。

调用元对象编译器moc

这里是元对象编译器moc所支持地命令行选项:

-o file
将输出写到file而不是标准输出。
-f
强制在输出文件中生成#include声明。文件的名称必须符合正则表达式\.[hH][^.]*(也就是说扩展名必须以H或h开始)。这个选项只有在你的头文件没有遵循标准命名法则的时候才有用。
-i
不在输出文件中生成#include声明。当一个C++文件包含一个或多个类声明的时候你也许应该这样使用元对象编译器。然后你应该在.cpp文件中包含(#include)元对象代码。如果-i和-f两个参数都出现,后出现的有效。
-nw
不产生任何警告。不建议使用。
-ldbg
把大量的lex调试信息写到标准输出。
-p path
使元对象编译器生成的(如果有生成的)#include声明的文件名称中预先考虑到path/。
-q path
使元对象编译器在生成的文件中的qt #include文件的名称中预先考虑到path/。

你可以明确地告诉元对象编译器不要解析头文件中的成分。它可以识别包含子字符串MOC_SKIP_BEGIN或者MOC_SKIP_END的任何C++注释(//)。它们正如你所期望的那样工作并且你可以把它们划分为若干层次。元对象编译器所看到的最终结果就好像你把一个MOC_SKIP_BEGIN和一个MOC_SKIP_END当中的所有行删除那样。

诊断

元对象编译器将会警告关于学多在Q_OBJECT类声明中危险的或者不合法的构造。

如果你在你的程序的最后连编阶段得到连接错误,说YourClass::className()是未定义的或者YourClass缺乏vtbl,某样东西已经被做错。绝大多数情况下,你忘记了编译或者#include元对象编译器产生的C++代码,或者(在前面的情况下)没有在连接命令中包含那个对象文件。

限制

元对象编译器并不展开#include或者#define,它简单地忽略它所遇到的所有预处理程序指示。这是遗憾的,但是在实践中它通常情况下不是问题。

元对象编译器不处理所有的C++。主要的问题是类模板不能含有信号和槽。这里是一个例子:

    class SomeTemplate<int> : public QFrame {
        Q_OBJECT
        ...
    signals:
        void bugInMocDetected( int );
    };

次重要的是,后面的构造是不合法的。所有的这些都可以替换为我们通常认为比较好的方案,所以去掉这些限制对于我们来说并不是高优先级的。

多重继承需要把QObject放在第一个

如果你使用多重继承,元对象编译器假设首先继承的类是QObject的一个子类。也就是说,确信仅仅首先继承的类是QObject。

    class SomeClass : public QObject, public OtherClass {
        ...
    };

(这个限制几乎是不可能去掉的;因为元对象编译器并不展开#include或者#define,它不能发现基类中哪个是QObject。)

函数指针不能作为信号和槽的参数

在你考虑使用函数指针作为信号/槽的参数的大多数情况下,我们认为继承是一个不错的替代方法。这里是一个不合法的语法的例子:

    class SomeClass : public QObject {
        Q_OBJECT
        ...
    public slots:
        // 不合法的
        void apply( void (*apply)(List *, void *), char * );
    };

你可以在这样一个限制范围内工作:

    typedef void (*ApplyFunctionType)( List *, void * );

    class SomeClass : public QObject {
        Q_OBJECT
        ...
    public slots:
        void apply( ApplyFunctionType, char * );
    };

有时用继承和虚函数、信号和槽来替换函数指针是更好的。

友声明不能放在信号部分或者槽部分中

有时它也许会工作,但通常情况下,友声明不能放在信号部分或者槽部分中。把它们替换到私有的、保护的或者公有的部分中。这里是一个不合法的语法的例子:

    class SomeClass : public QObject {
        Q_OBJECT
        ...
    signals:
        friend class ClassTemplate<char>; // 错的
    };

信号和槽不能被升级

把继承的成员函数升级为公有状态这一个C++特征并不延伸到包括信号和槽。这里是一个不合法的例子:

    class Whatever : public QButtonGroup {
        ...
    public slots:
        void QButtonGroup::buttonPressed; // 错的
        ...
    };

QButtonGroup::buttonPressed()槽是保护的。

C++测验:如果你试图升级一个被重载的保护成员函数将会发生什么?

  1. 所有的函数都被重载。
  2. 这不是标准的C++。

类型宏不能被用于信号和槽的参数

因为元对象编译器并不展开#define,在信号和槽中类型宏作为一个参数是不能工作的。这里是一个不合法的例子:

    #ifdef ultrix
    #define SIGNEDNESS(a) unsigned a
    #else
    #define SIGNEDNESS(a) a
    #endif

    class Whatever : public QObject {
        ...
    signals:
        void someSignal( SIGNEDNESS(int) );
        ...
    };

不含有参数的#define将会像你所期望的那样工作。

嵌套类不能放在信号部分或者槽部分,也不能含有信号和槽

这里是一个例子:

    class A {
        Q_OBJECT
    public:
        class B {
        public slots:   // 错的
            void b();
            ...
        };
    signals:
        class B {       // 错的
            void b();
            ...
        }:
    };

构造函数不能用于信号部分和槽部分

为什么一个人会把一个构造函数放到信号部分或者槽部分,这对于我们来说都是很神秘的。你无论如何也不能这样做(除去它偶尔能工作的情况)。请把它们放到私有的、保护的或者公有的部分中,它们本该属于的地方。这里是一个不合法的语法的例子:

    class SomeClass : public QObject {
        Q_OBJECT
    public slots:
        SomeClass( QObject *parent, const char *name )
            : QObject( parent, name ) { } // 错的
        ...
    };

属性的声明应该放在含有相应的读写函数的公有部分之前

在包含相应的读写函数的公有部分之中和之后声明属性的话,读写函数就不能像所期望的那样工作了。元对象编译器会抱怨不能找到函数或者解析这个类型。这里是一个不合法的语法的例子:

    class SomeClass : public QObject {
        Q_OBJECT
    public:
        ...
        Q_PROPERTY( Priority priority READ priority WRITE setPriority ) // 错的
        Q_ENUMS( Priority ) // 错的
        enum Priority { High, Low, VeryHigh, VeryLow };
        void setPriority( Priority );
        Priority priority() const;
        ...
    };

根据这个限制,你应该在Q_OBJECT之后,在这个类的声明之前声明所有的属性:

    class SomeClass : public QObject {
        Q_OBJECT
        Q_PROPERTY( Priority priority READ priority WRITE setPriority )
        Q_ENUMS( Priority )
    public:
        ...
        enum Priority { High, Low, VeryHigh, VeryLow };
        void setPriority( Priority );
        Priority priority() const;
        ...
    };


Copyright © 2002 Trolltech Trademarks 译者:Cavendish
Qt 3.0.5版