本节和下一节都是讲同一个元对象系统综合示例,因为内容很多,所以作为两节来讲解。本节首先介绍 Qt 元对象类 QMetaObject(主要从 Qt 帮助文档翻译来的),然后根据之前 4.4.2 普通属性示例的修改版,来讲解 Qt 工具集自动生成的 ui_*.h 代码,学习 uic 工具根据 ui 文件所生成的代码,以后如果需要手动编写构建图形界面的代码,原理也是类似的,可以举一反三。下一节专门讲解 moc_*.cpp 里面的代码,深入了解信号和槽机制的内部原理,并且加入了一些 Qt 核心类的源代码、流程框图来理解从信号触发到槽函数调用的过程。
本章 4.5 和 4.6 两节都是讲解 Qt5 元对象系统内部机理的,之前国外大神写过 Qt4 内幕和逆向“Qt Internals & Reversing”,推荐读者阅读该文章,并且本节很多内容也是从这篇文章学来的,链接如下:
http://www.codeproject.com/Articles/31330/Qt-Internals-Reversing
或者打开这个网址: http://www.ntcore.com/files/qtrev.htm
国内也有类似的中文文章,推荐这两篇:
Qt一些细节内幕: http://blog.csdn.net/liangkaiming/article/details/5799752
解析Qt的信号-槽机制是如何工作的: http://blog.csdn.net/newthinker_wei/article/details/22701695
1. QMetaObject类
QMetaObject 是实现元对象系统的关键类,包含 Qt 对象的元信息,可以在 Qt 帮助文档检索关于它的资料。每个 QObject 派生类都有一个 QMetaObject 实例,保存该派生类的元信息,可以通过 QObject::metaObject() 获取元对象。QMetaObject 提供了这些公有函数:
- className() 返回类的名称字符串。
- superClass() 返回基类的元对象。
- method() 和 methodCount() 提供类的元方法的信息(元方法包括信号、槽和其他 invokable 成员函数 )。
- enumerator() 和 enumeratorCount() 提供类里定义的枚举类型信息。
- propertyCount() 和 property() 提供类的属性信息。
- constructor() 和 constructorCount() 提供类的元构造函数信息。
另外还有多个索引函数,能根据字符串名称检索元构造函数、元方法、枚举类型、属性等,函数名为: indexOfConstructor(), indexOfMethod(), indexOfEnumerator() 和 indexOfProperty() 。上一节讲过类的附加信息等函数,也是元对象类提供的。
下面首先解释一下元方法,信号和槽函数之前都是见到了,而 invokable 成员函数是指使用 Q_INVOKABLE 前缀声明的类成员函数,如:
class Window : public QWidget { Q_OBJECT public: Window(); void normalMethod(); Q_INVOKABLE void invokableMethod(); };
|
Q_INVOKABLE 前缀声明的函数和信号、槽等名称,都会由 moc 工具处理成字符串,保存到类的静态数据里面,后面会讲到。这些元方法都可以通过 QMetaObject::invokeMethod() 来调用,才称之为 invokable 。
因为各个类的元方法的声明都不一样,如何通过统一的接口在运行时调用元方法呢?这就是QMetaObject::invokeMethod() 函数干的活,它根据元方法的名称字符串和参数列表来统一调用元方法。该静态函数有多个重载,下面给出它的第一个声明,其他的重载是差不多的:
bool QMetaObject::invokeMethod(QObject * obj, const char * member, Qt::ConnectionType type, QGenericReturnArgument ret, QGenericArgument val0 = QGenericArgument( 0 ), QGenericArgument val1 = QGenericArgument(), QGenericArgument val2 = QGenericArgument(), QGenericArgument val3 = QGenericArgument(), QGenericArgument val4 = QGenericArgument(), QGenericArgument val5 = QGenericArgument(), QGenericArgument val6 = QGenericArgument(), QGenericArgument val7 = QGenericArgument(), QGenericArgument val8 = QGenericArgument(), QGenericArgument val9 = QGenericArgument())
|
因为是静态函数,所以它第一个参数手动传了需要调用元方法的对象指针;第二个参数是元方法函数的名称字符串;第三个是关联类型,就是信号和槽函数关联时用的类型; 第四个参数是元方法的返回值;接下来是编号从 0 到 9 的 10 个元方法参数。QGenericArgument 是 Qt 内部使用的辅助类,专门用于元方法返回值和参数的传递,它有两个公有函数,name() 获取参数类型字符串,data() 获取 void * 保存的参数数值,另外不能直接调用它的构造函数,而应该用 Q_ARG() 宏:
QGenericArgument Q_ARG( Type, const Type & value)
|
其次,enumerator() 和 enumeratorCount() 针对的是类里使用 Q_ENUMS 声明枚举类型,因为有些元方法会使用枚举类型,枚举类型的名称与数值是不一样的,比如属性系统通过名称字符串寻找对应的元方法时,也需要枚举类型的字符串形式。 Q_ENUMS() 宏就是把枚举类型的字符串形式也保存到类的静态数据里面,枚举类型声明示例:
class MyClass : public QObject { Q_OBJECT Q_PROPERTY(Priority priority READ priority WRITE setPriority NOTIFY priorityChanged) Q_ENUMS(Priority) public: MyClass(QObject *parent = 0); ~MyClass(); enum Priority { High, Low, VeryHigh, VeryLow }; void setPriority(Priority priority) { m_priority = priority; emit priorityChanged(priority); } Priority priority() const { return m_priority; } signals: void priorityChanged(Priority); private: Priority m_priority; };
|
与 Q_ENUMS() 宏类似的,还有 Q_FLAGS() 宏,标志位 flags 与普通枚举类型有区别,就是类里 Q_FLAGS() 宏声明的标志位可以做 与、或、非 等二进制运算,关于标志位声明可以查找 Qt 帮助文档,这里不贴代码了。
属性相关的内容上一节讲过,不重复了。constructor() 和 constructorCount() 不是指一般的构造函数,而是类的元构造函数,也是通过 Q_INVOKABLE 声明的构造函数。元构造函数可以通过 QMetaObject::newInstance() 函数在运行时调用,它根据元构造函数的字符串名称、参数等构造一个新的类实例对象,并返回该对象。QMetaObject::newInstance() 函数声明与 QMetaObject::invokeMethod() 函数声明差不多,只是 QMetaObject::newInstance() 用于新建对象,而 QMetaObject::invokeMethod() 函数用于调用元方法。
QMetaObject 类基本是是围绕名称字符串展开的,moc 工具将类名、元方法名称、枚举类型名称、元构造函数名称等字符串保存为类的静态数据,然后在运行时可以通过名称字符串定位到真实的函数,然后来调用元方法。上一节属性 系统里的 property()、setProperty() 和本小节的 QMetaObject :: invokeMethod()、QMetaObject::newInstance() 都是通过字符串来查找对应的函数并执行调用的。
2. 元对象系统综合示例
本节的示例 npcomplete 是 4.4.2 普通属性示例 normalpros 的完整版,外加一点小改动。
在 D:\QtProjects\ch04 目录,我们复制上一节 normalpros 示例的文件夹,并就地粘帖,新文件夹改名为 npcomplete。然后进入 npcomplete 文件夹,把 pro 文件改名为 npcomplete.pro,然后用记事本编辑 npcomplete.pro,把 TARGET 一行改成下面这样:
编辑后保存,这样就造出一个新项目 npcomplete.pro 。npcomplete 目录里的旧用户文件 normalpros.pro.user 可以删了,其他文件都保留。
我们打开 D:\QtProjects\ch04\npcomplete\npcomplete.pro 项目,然后来修改代码。首先为 ShowChanges 类添加接收窗口类对象信号的槽函数,并在 main 函数里进行了关联,下面把修改后的 ShowChanges 类和 main 函数代码贴出来,showchanges.h:
#ifndef SHOWCHANGES_H #define SHOWCHANGES_H #include <QObject> class ShowChanges : public QObject { Q_OBJECT public: explicit ShowChanges(QObject *parent = 0); ~ShowChanges(); signals: public slots: //槽函数,接收 value 变化信号 void RecvValue(double v); //槽函数,接收 nickName 变化信号 void RecvNickName(const QString& strNewName); //槽函数,接收 count 变化信号 void RecvCount(int nNewCount); }; #endif // SHOWCHANGES_H
|
showchanges.cpp:
#include "showchanges.h" #include <QDebug> ShowChanges::ShowChanges(QObject *parent) : QObject(parent) { } ShowChanges::~ShowChanges() { } //接收并打印 value 变化后的新值 void ShowChanges::RecvValue(double v) { qDebug()<<"RecvValue: "<<fixed<<v; } //接收并打印 nickName 变化后的新值 void ShowChanges::RecvNickName(const QString &strNewName) { qDebug()<<"RecvNickName: "<<strNewName; } //接收并打印 count 变化后的新值 void ShowChanges::RecvCount(int nNewCount) { qDebug()<<"RecvCount: "<<nNewCount; }
|
main.cpp:
#include "widget.h" #include <QApplication> #include <QDebug> #include "showchanges.h" int main(int argc, char *argv[]) { QApplication a(argc, argv); Widget w; //源头对象 //接收端对象 ShowChanges s; //关联 QObject::connect(&w, SIGNAL(valueChanged(double)), &s, SLOT(RecvValue(double))); QObject::connect(&w, SIGNAL(nickNameChanged(QString)), &s, SLOT(RecvNickName(QString))); QObject::connect(&w, SIGNAL(countChanged(int)), &s, SLOT(RecvCount(int))); //属性读写 //通过写函数、读函数 w.setNickName( "Wid" ); qDebug()<<w.nickName(); w.setCount(100); qDebug()<<w.count(); //通过 setProperty() 函数 和 property() 函数 w.setProperty("value", 2.3456); qDebug()<<fixed<<w.property("value").toDouble(); //显示窗体 w.show(); return a.exec(); }
|
widget.h 和 widget.cpp 代码和 4.4.2 节一样的,就不贴了。因为上一节例子没有修改 ui 文件,里面是空的,下面为窗体加点料,方便后面阅读 ui_widget.h 的代码。在 QtCreator 里打开 widget.ui 文件,进入界面设计模式,首先拖一个标签控件和单行编辑控件:
在 2.3 Hello Designer 一节里面介绍过设计师的四种编辑模式:编辑窗口部件、编辑信号/槽、编辑伙伴、编辑 Tab 顺序。默认情况下设计师工作在编辑窗口部件模式,下面我们来试试其他三种模式。
在设计模式的窗体上面,有一排快捷按钮,第一个小按钮是普通的编辑部件的模式,前面章节用的都是部件编辑模式。我们点击第二个小按钮(图标里有一个指向右下角的箭 头),就会进入信号和槽的编辑模式,如下图所示:
进入信号和槽的编辑模式之后,鼠标移动到控件,控件就会以红框高亮显示,然后就可以将源头控件的信号关联到接收端控件的槽函数,关联操作的过程就相当于在画图板上 画直线:鼠标指向源头控件,左键按下不松,鼠标滑动画线到接收控件,然后松开,就弹出配置连接(connect)的对话框:
进行画线操作之后,自动弹出从源头 lineEdit 到接收端 label 的配置连接对话框,左边列表选择 textEdited(QString),右边选择 setText(QString),然后点击下方的 OK 按钮。
在上图配置连接对话框里,左下角的复选框指是否显示从基类 QWidget 继承的信号和槽。
点击 OK 按钮之后,看到类似下图所示的信号和槽连接关系(这里连接和关联是一个意思):
这时候会显示 lineEdit 的 textEdited(QString) 信号已经连接到 label 的 setText(QString) 槽函数里。从信号和槽编辑模式实现的这个功能就是 4.2.1 节文本同步例子的功能。对于窗体里面控件之间简单的信号和槽关联,都可以按照上面方法实现关联(即连接)。这是信号和槽编辑模式的示范,窗体上面第三个按钮是伙伴编辑模 式,图标看起来有一个橙色的橡皮檫。
点击上面第三个伙伴编辑模式,这个模式通常是将一个标签设置为其他控件的伙伴,用于提示其他控件的功能,并可以为其他控件设置快捷按钮。快捷键设置我们以后再学, 这里仅仅将标签控件设置为单行编辑控件的伙伴,方法也是用鼠标画线,伙伴编辑模式通常都是从标签控件出发,画到其他控件:
伙伴关系比较简单,就是一根线,从标签控件连到其他控件,完事。下一章还会专门讲通过标签控件为编辑控件设置快捷键,这里先不管。
Tab 顺序一般用于多个输入控件切换输入焦点,上面只有一个单行编辑控件,不太够。我们点击顶上面第一个编辑部件的按钮,回到部件编辑模式,向窗体再拖两个单行编辑控件:
有三个输入控件之后,我们点击顶上面第四个编辑 Tab 顺序的按钮,进入 Tab 顺序编辑的模式:
只有能接收输入的控件才有 Tab 焦点,按钮类也有 Tab 键顺序的。Tab 键顺序,就是窗体里面有多个输入控件,可以按键盘上的 Tab 键依次切换各个控件,输入焦点默认在编号为 1 的控件里,按一次 Tab 键进入 2 号控件,再到 3 号控件,如果到了最后一个控件那就循环回到 1 号。Tab 键切换顺序,就是在 Tab 编辑模式里鼠标依次点击控件的顺序,先点击上面的控件,它就是1 号,再点击中间的控件,中间的就是 2 号,最后点击 下面的就是 3 号,以此类推。编辑好之后保存界面文件,我们对这个 ui 文件的编辑就完成了。
上面将设计师四种编辑模式都使用了一遍,是为了后面小节里面 ui_widget.h 里面代码的完整性,四种编辑模式都是在该文件里生成对应的代码。下面就来看看 ui_widget.h 里的代码。
3. ui_*.h 代码
按照之前的编辑,保存文件,然后点击 QtCreator 菜单“构建”-->“重新构建项目 npcomplete”(例子运行效果不截图了,大家自己运行看看),构建过程会产生我们本节需要学习的几个文件,例子源代码保存在 D:\QtProjects\ch04\npcomplete,构建文件夹为:
D:\QtProjects\ch04\build-npcomplete-Desktop_Qt_5_4_0_MinGW_32bit-Debug
|
我们进入构建文件夹,可以看到 ui_widget.h。我们打开这个文件,可以查看里面的内容:
** Form generated from reading UI file 'widget.ui' ** ** Created by: Qt User Interface Compiler version 5.4.0 ** ** WARNING! All changes made in this file will be lost when recompiling UI file! #ifndef UI_WIDGET_H #define UI_WIDGET_H #include <QtCore/QVariant> #include <QtWidgets/QAction> #include <QtWidgets/QApplication> #include <QtWidgets/QButtonGroup> #include <QtWidgets/QHeaderView> #include <QtWidgets/QLabel> #include <QtWidgets/QLineEdit> #include <QtWidgets/QWidget> QT_BEGIN_NAMESPACE class Ui_Widget { public: QLabel *label; QLineEdit *lineEdit; QLineEdit *lineEdit_2; QLineEdit *lineEdit_3; void setupUi(QWidget *Widget) { if (Widget->objectName().isEmpty()) Widget->setObjectName(QStringLiteral("Widget")); Widget->resize(400, 300); label = new QLabel(Widget); label->setObjectName(QStringLiteral("label")); label->setGeometry(QRect(80, 100, 201, 16)); lineEdit = new QLineEdit(Widget); lineEdit->setObjectName(QStringLiteral("lineEdit")); lineEdit->setGeometry(QRect(80, 50, 201, 20)); lineEdit_2 = new QLineEdit(Widget); lineEdit_2->setObjectName(QStringLiteral("lineEdit_2")); lineEdit_2->setGeometry(QRect(80, 150, 113, 20)); lineEdit_3 = new QLineEdit(Widget); lineEdit_3->setObjectName(QStringLiteral("lineEdit_3")); lineEdit_3->setGeometry(QRect(80, 210, 113, 20)); #ifndef QT_NO_SHORTCUT label->setBuddy(lineEdit); #endif // QT_NO_SHORTCUT QWidget::setTabOrder(lineEdit, lineEdit_2); QWidget::setTabOrder(lineEdit_2, lineEdit_3); retranslateUi(Widget); QObject::connect(lineEdit, SIGNAL(textEdited(QString)), label, SLOT(setText(QString))); QMetaObject::connectSlotsByName(Widget); } // setupUi void retranslateUi(QWidget *Widget) { Widget->setWindowTitle(QApplication::translate("Widget", "Widget", 0)); label->setText(QApplication::translate("Widget", "TextLabel", 0)); } // retranslateUi };
|
文件开头的注视是说明不要修改这个文件,因为修改了也没用,下次 uic 工具会自动生成这个文件,之前的修改就被覆盖了。
UI_WIDGET_H 宏是保证这个头文件只被包含一次。
接下来是包含 Qt 库里的几个必要的头文件 QVariant、QAction、...... QLabel、QLineEdit、QWidget等。
开头的 QT_BEGIN_NAMESPACE 和结尾的 QT_END_NAMESPACE 两个宏,其实是空宏,什么都没有,对编译器来说这两个宏没代码,放 在那就是提醒程序员看看的,实际没意义。
ui_widget.h 主要内容就是全局类 Ui_Widget 的代码,这个类用于构建窗体界面。我们向窗体里拖了一个标签和三个单行编辑控件, Ui_Widget 类里看到它们的成员指针 label、lineEdit、lineEdit_2 和 lineEdit_3。
Ui_Widget 有两个函数,setupUi 函数之前多次接触到,就是用于构建界面的函数,下面分块解读它的代码:
void setupUi(QWidget *Widget) { if (Widget->objectName().isEmpty()) Widget->setObjectName(QStringLiteral("Widget")); Widget->resize(400, 300);
|
首先判断窗体 Widget 内部的对象名是否为空,如果为空就设置对象名为 "Widget",然后重置窗体的大小为 400*300 像素。
label = new QLabel(Widget); label->setObjectName(QStringLiteral("label")); label->setGeometry(QRect(80, 100, 201, 16)); lineEdit = new QLineEdit(Widget); lineEdit->setObjectName(QStringLiteral("lineEdit")); lineEdit->setGeometry(QRect(80, 50, 201, 20)); lineEdit_2 = new QLineEdit(Widget); lineEdit_2->setObjectName(QStringLiteral("lineEdit_2")); lineEdit_2->setGeometry(QRect(80, 150, 113, 20)); lineEdit_3 = new QLineEdit(Widget); lineEdit_3->setObjectName(QStringLiteral("lineEdit_3")); lineEdit_3->setGeometry(QRect(80, 210, 113, 20));
|
这里新建了一个标签控件和三个单行编辑控件,并设置它们的对象名称 setObjectName 和显示矩形位置 setGeometry。
#ifndef QT_NO_SHORTCUT label->setBuddy(lineEdit); #endif // QT_NO_SHORTCUT
|
如果没有定义不能使用快捷键的宏 QT_NO_SHORTCUT,那么设置伙伴关系,label 控件与 lineEdit 是伙伴关系。
QWidget::setTabOrder(lineEdit, lineEdit_2); QWidget::setTabOrder(lineEdit_2, lineEdit_3);
|
setTabOrder 就是设置 Tab 键顺序的函数,该函数接收两个参数,都是控件指针。以第一个 setTabOrder 为例,这个函数的意义是如果输入焦点在 lineEdit 里面,这时按一次 Tab 键,输入焦点就会切换到 lineEdit_2 。Tab 键顺序的设置相当于是单向链表,从第一个切换到第二个,再从第二个切换到第三个,以此类推。
重新翻译界面,如果做了多国语言翻译,这个函数可以将界面翻译成其他语言显示。
QObject::connect(lineEdit, SIGNAL(textEdited(QString)), label, SLOT(setText(QString)));
|
我们在信号和槽编辑模式里关联的信号和槽,在这里由 uic 工具自动生成了 connect 函数调用,与我们 4.2.1 节手动写的 connect 函数差不多。
QMetaObject::connectSlotsByName(Widget);
} // setupUi
|
connectSlotsByName 是根据信号和槽函数名称等实现自动关联的关键函数,后面小节专门讲这个函数。
Ui_Widget 类第二个函数就是 retranslateUi 函数,主要是用来做翻译的:
void retranslateUi(QWidget *Widget) { Widget->setWindowTitle(QApplication::translate("Widget", "Widget", 0)); label->setText(QApplication::translate("Widget", "TextLabel", 0)); } // retranslateUi |
这个函数将界面上能看到的字符串,如窗口标题 "Widget"、标签文本 "TextLabel" 做一下翻译。我们这里都没用到翻译,所以先不管它们。
ui_widget.h 最后是一个名字空间的声明,名字空间最主要的用途是防止重名,便于管理较大的项目:
namespace Ui { class Widget: public Ui_Widget {}; } // namespace Ui
|
类 Ui::Widget 就是从全局类 Ui_Widget 做一下继承,其实啥都没干。在 widget.h 头文件的类声明里,会定义私有成员 Ui::Widget *ui 作为构建界面的对象。
ui_widget.h 文件里的代码是比较清晰明了的,无论是通过 Qt 设计师做 UI,然后生成 ui_widget.h 代码,还是我们手动编写构建界面的代码,比如手动 new 一堆控件,设置显示矩形 setGeometry 等,手动编写的代码与 uic 工具根据 ui 文件生成的代码是等价的,我们如果手动编写上面的代码,不用 ui 文件,效果也是一样的。自己手动编写代码主要是不直观,程序不运行就看不到界面长什么样。而 Qt 设计师大大方便了图形程序的设计过程,对窗体的编辑,就是所见即所得(What You See Is What You Get,WYSIWYG)。大家可以使用 Qt 设计师简化程序的编写过程,但是不能过度依赖 Qt 设计师,要学习 ui_widget.h 里面的代码写法,以后无论是自己编写代码还是阅读网上例子都有好处。
关于 UI 部分的代码就讲解到这,下一节介绍元对象系统的重要知识,除了 moc 工具根据头文件自动生成的 moc_*.cpp 代码,还从 Qt 核心源码找了一些相关的函数,详细了解一下信号和槽机制的原理。
|