4.5 扩展阅读:ui_*.h

本节和下一节都是讲同一个元对象系统综合示例,因为内容很多,所以作为两节来讲解。本节首先介绍 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

4.5.1 QMetaObject类

QMetaObject 是实现元对象系统的关键类,包含 Qt 对象的元信息,可以在 Qt 帮助文档检索关于它的资料。每个 QObject 派生类都有一个 QMetaObject 实例,保存该派生类的元信息,可以通过 QObject::metaObject() 获取元对象。QMetaObject 提供了这些公有函数:
另外还有多个索引函数,能根据字符串名称检索元构造函数、元方法、枚举类型、属性等,函数名为: 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() 都是通过字符串来查找对应的函数并执行调用的。

4.5.2 元对象系统综合示例

本节的示例 npcomplete 是 4.4.2 普通属性示例 normalpros 的完整版,外加一点小改动。
在 D:\QtProjects\ch04 目录,我们复制上一节 normalpros 示例的文件夹,并就地粘帖,新文件夹改名为 npcomplete。然后进入 npcomplete 文件夹,把 pro 文件改名为 npcomplete.pro,然后用记事本编辑 npcomplete.pro,把 TARGET 一行改成下面这样:
TARGET = npcomplete
编辑后保存,这样就造出一个新项目 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 文件,进入界面设计模式,首先拖一个标签控件和单行编辑控件:
ui
在 2.3 Hello Designer 一节里面介绍过设计师的四种编辑模式:编辑窗口部件、编辑信号/槽、编辑伙伴、编辑 Tab 顺序。默认情况下设计师工作在编辑窗口部件模式,下面我们来试试其他三种模式。
在设计模式的窗体上面,有一排快捷按钮,第一个小按钮是普通的编辑部件的模式,前面章节用的都是部件编辑模式。我们点击第二个小按钮(图标里有一个指向右下角的箭 头),就会进入信号和槽的编辑模式,如下图所示:
signals-slots
进入信号和槽的编辑模式之后,鼠标移动到控件,控件就会以红框高亮显示,然后就可以将源头控件的信号关联到接收端控件的槽函数,关联操作的过程就相当于在画图板上 画直线:鼠标指向源头控件,左键按下不松,鼠标滑动画线到接收控件,然后松开,就弹出配置连接(connect)的对话框:
signals-slots2
进行画线操作之后,自动弹出从源头 lineEdit 到接收端 label 的配置连接对话框,左边列表选择 textEdited(QString),右边选择 setText(QString),然后点击下方的 OK 按钮。
在上图配置连接对话框里,左下角的复选框指是否显示从基类 QWidget 继承的信号和槽。

点击 OK 按钮之后,看到类似下图所示的信号和槽连接关系(这里连接和关联是一个意思):
signals-slots3
这时候会显示 lineEdit 的 textEdited(QString) 信号已经连接到 label 的 setText(QString) 槽函数里。从信号和槽编辑模式实现的这个功能就是 4.2.1 节文本同步例子的功能。对于窗体里面控件之间简单的信号和槽关联,都可以按照上面方法实现关联(即连接)。这是信号和槽编辑模式的示范,窗体上面第三个按钮是伙伴编辑模 式,图标看起来有一个橙色的橡皮檫。

点击上面第三个伙伴编辑模式,这个模式通常是将一个标签设置为其他控件的伙伴,用于提示其他控件的功能,并可以为其他控件设置快捷按钮。快捷键设置我们以后再学, 这里仅仅将标签控件设置为单行编辑控件的伙伴,方法也是用鼠标画线,伙伴编辑模式通常都是从标签控件出发,画到其他控件:
buddy
伙伴关系比较简单,就是一根线,从标签控件连到其他控件,完事。下一章还会专门讲通过标签控件为编辑控件设置快捷键,这里先不管。

Tab 顺序一般用于多个输入控件切换输入焦点,上面只有一个单行编辑控件,不太够。我们点击顶上面第一个编辑部件的按钮,回到部件编辑模式,向窗体再拖两个单行编辑控件:
tab1
有三个输入控件之后,我们点击顶上面第四个编辑 Tab 顺序的按钮,进入 Tab 顺序编辑的模式:
tab2
只有能接收输入的控件才有 Tab 焦点,按钮类也有 Tab 键顺序的。Tab 键顺序,就是窗体里面有多个输入控件,可以按键盘上的 Tab 键依次切换各个控件,输入焦点默认在编号为 1 的控件里,按一次 Tab 键进入 2 号控件,再到 3 号控件,如果到了最后一个控件那就循环回到 1 号。Tab 键切换顺序,就是在 Tab 编辑模式里鼠标依次点击控件的顺序,先点击上面的控件,它就是1 号,再点击中间的控件,中间的就是 2 号,最后点击 下面的就是 3 号,以此类推。编辑好之后保存界面文件,我们对这个 ui 文件的编辑就完成了。

上面将设计师四种编辑模式都使用了一遍,是为了后面小节里面 ui_widget.h 里面代码的完整性,四种编辑模式都是在该文件里生成对应的代码。下面就来看看 ui_widget.h 里的代码。

4.5.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

};

namespace Ui {
    class Widget: public Ui_Widget {};
} // namespace Ui

QT_END_NAMESPACE

#endif // UI_WIDGET_H
文件开头的注视是说明不要修改这个文件,因为修改了也没用,下次 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 键顺序的设置相当于是单向链表,从第一个切换到第二个,再从第二个切换到第三个,以此类推。
        retranslateUi(Widget);
重新翻译界面,如果做了多国语言翻译,这个函数可以将界面翻译成其他语言显示。
        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 核心源码找了一些相关的函数,详细了解一下信号和槽机制的原理。



prev
contents
next