15.Qt 和 C++(Qt and C++)
本章的作者:jryannel
** 注意: **
最新的构建时间:2016/03/21
这章的源代码能够在assetts folder找到。
Qt 是一个带有 QML 和 Javascript 扩展的 C++ 工具包。存在许多针对 Qt 的语言绑定,但是随着 Qt 在 C++ 中开发,C++ 的精神可以在整个类中找到。在本节中,我们将从 C++ 角度来看 Qt,以便更好地了解如何使用 C++ 开发的本机插件扩展 QML。通过 C++,可以扩展和控制提供给 QML 的执行环境。
本章将与 Qt 一样,要求读者掌握 C++ 的一些基础知识。Qt 不依赖于高级 C++ 特性,我通常认为C++ 的 Qt 风格是非常易读的,所以如果你觉得 C++ 的知识是不稳定的话,别担心。
从 C++ 方向接近 Qt,我们会发现 Qt 通过使内省数据可用,丰富了 C++,使其具备了与许多现代语言类似的功能。这可以通过使用 QObject 基类来实现。内省数据或元数据在运行时维护类的信息,这是普通 C++ 不做的事情。这使得可以动态地探测对象以获得关于这些细节的信息,例如它们的属性和可用的方法。
Qt 使用这个元信息来启用使信号和槽的非常松散绑定的回调概念。每个信号可以连接到任意数量的槽或甚至任意其他的信号。当从对象实例发出信号时,调用与之连接的槽。由于信号发射对象不需要知道与之连接的槽的对象,反之亦然,因此该机制用于创建具有非常少的组件间依赖性的可重用的组件非常有用。
内省特性也用于创建动态语言的绑定,使得 QML 可以调用暴露的 C++ 对象实例,并且可以从 JavaScript 中调用 C++ 函数。除了绑定 Qt C++, 绑定标准的 JavaScript 也是一种非常流行的方式,还有 Python 的绑定,叫做 PyQt。
除了这个中心概念,Qt 还可以使用 C++ 开发跨平台应用程序。Qt C++ 在不同的操作系统上提供了一个平台抽象,它允许开发人员专注于手头的任务,而不是在不同操作系统上打开文件的细节。这意味着您可以为 Windows,OS X 和 Linux 重新编译相同的源代码,并且 Qt 负责处理某些事情的不同操作系统。最终的结果是具有目标平台外观和感觉的本地构建的应用程序。由于移动设备是新的桌面,更新的 Qt 版本也可以使用相同的源代码来移植到多个移动平台:iOS,Android,Jolla,BlackBerry,Ubuntu Phone,Tizen。
当涉及到重用时,不仅可以重新使用源代码,还可以重用开发人员技能。知道 Qt 的团队可以接触到更多的平台,然后一个团队只关注一个单一的平台特定技术,而 Qt 如此灵活,团队可以使用相同的技术创建不同的系统组件。
对于所有平台,Qt 提供了一组基本类型,例如:具有完整 unicode 支持,列表,向量,缓冲区的字符串。它还为目标平台的主循环,跨平台线程和网络支持提供了一个共同的抽象。一般的理念是,对于应用程序开发人员,Qt 附带了所有必需的功能。对于特定于领域的任务,如与本地库的接口,Qt 带有几个帮助类,使之更容易。
15.1 示例程序
理解 Qt 的最好方法是从一个小的演示应用程序开始。此应用程序创建一个简单的 “Hello World!” 字符串,并使用 unicode 字符将其写入文件。
#include <QCoreApplication>
#include <QString>
#include <QFile>
#include <QDir>
#include <QTextStream>
#include <QDebug>
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
// prepare the message
QString message("Hello World!");
// prepare a file in the users home directory named out.txt
QFile file(QDir::home().absoluteFilePath("out.txt"));
// try to open the file in write mode
if(!file.open(QIODevice::WriteOnly)) {
qWarning() << "Can not open file with write access";
return -1;
}
// as we handle text we need to use proper text codecs
QTextStream stream(&file);
// write message to file via the text stream
stream << message;
// do not start the eventloop as this would wait for external IO
// app.exec();
// no need to close file, closes automatically when scope ends
return 0;
}
简单的例子演示文件访问的使用以及使用文本编辑器通过文本流将文本写入文件的正确方法。对于二进制数据,有一个称为 QDataStream 的跨平台二进制流。我们使用的不同类需要使用它们的类名包含。另一种是使用模块名和类名例如 #include <QtCore/QFile>。对于比较懒的人,有一个更加简单的方法是包含整个模块,使用 #include <QtCore>。例如在 QtCore 中你可以在应用程序中使用很多通用的类,这没有 UI 依赖。查看 QtCore class list 或者 QtCore overview 获取更多的信息。
我们使用 qmake 和 make 构建应用程序。QMake 读取一个项目文件并生成一个 Makefile,然后可以使用 make 进行编译。项目文件与平台无关,qmake 有一些规则将平台特定设置应用于生成的 make 文件。该项目还可以包含平台特定规则的平台范围,这在特定情况下是必需的。这是一个简单的项目文件的例子。
# build an application
TEMPLATE = app
# use the core module and do not use the gui module
QT += core
QT -= gui
# name of the executable
TARGET = CoreApp
# allow console output
CONFIG += console
# for mac remove the application bundling
macx {
CONFIG -= app_bundle
}
# sources to be build
SOURCES += main.cpp
我们不会深入这个话题。只要记住 Qt 使用项目文件进行项目管理,并且 qmake 会从这些项目文件中生成特定于平台的 make 文件。
上面的简单代码示例只是写入文本并退出应用程序。对于命令行工具,这是足够好的。对于用户界面,我们需要一个等待用户输入的事件循环,并且以某种方式调度重新绘制操作。所以这里使用同样的例子,但是现在使用桌面按钮来触发写作。
我们的 main.cpp 令人惊讶地变小了。我们将代码移动到自定义的类中,以便能够使用用户输入的信号/槽,例如。按钮点击。信号/插槽机制通常需要一个对象实例,我们将很快看到。
#include <QtCore>
#include <QtGui>
#include <QtWidgets>
#include "mainwindow.h"
int main(int argc, char** argv)
{
QApplication app(argc, argv);
MainWindow win;
win.resize(320, 240);
win.setVisible(true);
return app.exec();
}
在 main 函数中,我们简单地创建应用程序对象,并使用 exec() 启动事件循环。现在应用程序位于事件循环中,并等待用户输入。
int main(int argc, char** argv)
{
QApplication app(argc, argv); // init application
// create the ui
return app.exec(); // execute event loop
}
Qt 提供多种 UI 技术。对于这个例子,我们使用纯粹的 Qt C ++ 的 Desktop Widgets 用户界面库。我们创建一个主窗口,该窗口将包含一个按钮用来触发功能,主窗口还将呈现我们从上一个例子中知道的核心功能。
主窗口本身是一个小部件。它成为顶级窗口,因为它没有任何父级。这来自于 Qt 将用户界面看作是一个 ui 元素的树。在这种情况下,主窗口是根元素,因此成为窗口,而按钮是主窗口的子窗口,并在窗口中成为窗口小部件。
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QtWidgets>
class MainWindow : public QMainWindow
{
public:
MainWindow(QWidget* parent=0);
~MainWindow();
public slots:
void storeContent();
private:
QPushButton *m_button;
};
#endif // MAINWINDOW_H
另外我们定义一个名为 storeContent() 的公共槽,当这个按钮被点击时,它将被调用。一个槽是一个 C++ 方法,它被注册到 Qt 元对象系统中,可以被动态调用。
#include "mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
m_button = new QPushButton("Store Content", this);
setCentralWidget(m_button);
connect(m_button, &QPushButton::clicked, this, &MainWindow::storeContent);
}
MainWindow::~MainWindow()
{
}
void MainWindow::storeContent()
{
qDebug() << "... store content";
QString message("Hello World!");
QFile file(QDir::home().absoluteFilePath("out.txt"));
if(!file.open(QIODevice::WriteOnly)) {
qWarning() << "Can not open file with write access";
return;
}
QTextStream stream(&file);
stream << message;
}
在主窗口中,我们首先创建按钮,然后使用 connect 方法将槽 storeContent() 和 clicked() 进行连接。每次当发出点击信号时,都会调用槽 storeContent()。这样很简单,对象通过信号和槽实现了松耦合的通信。
15.2 QObject
如引言所述,QObject 是 Qt 内省的内容。它是 Qt 几乎所有类的基类。没有继承自 QObject 的是一些值类型,如 QColor,QString 和 QList 等。
Qt 对象是一个标准的 C++ 对象,但具有更多的能力。这些可以分为两组:内省和内存管理。第一个意思是 Qt 对象知道它的类名,它与其他类的关系,以及它的方法和属性。内存管理概念意味着每个 Qt 对象都可以是子对象的父对象。父对象拥有子对象,当父对象被销毁时,会负责销毁子对象。
了解 QObject 能力如何影响类的最佳方法是使用标准的 C++ 类,并启用 Qt。下面所示的类代表普通的类。
person 类是具有名称和性别属性的数据类。person 类使用 Qt 的对象系统向 c++ 类添加元信息。它允许 person 对象的用户连接到插槽,并在属性更改时收到通知。
class Person : public QObject
{
Q_OBJECT // enabled meta object abilities
// property declarations required for QML
Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)
Q_PROPERTY(Gender gender READ gender WRITE setGender NOTIFY genderChanged)
// enables enum introspections
Q_ENUMS(Gender)
public:
// standard Qt constructor with parent for memory management
Person(QObject *parent = 0);
enum Gender { Unknown, Male, Female, Other };
QString name() const;
Gender gender() const;
public slots: // slots can be connected to signals
void setName(const QString &);
void setGender(Gender);
signals: // signals can be emitted
void nameChanged(const QString &name);
void genderChanged(Gender gender);
private:
// data members
QString m_name;
Gender m_gender;
};
构造函数将父类传递给超类并初始化成员。Qt 的值类被自动初始化。在这种情况下,QString 将初始化为一个空字符串(QString::isNull()),性别成员将显式地初始化为未知性别。
Person::Person(QObject *parent)
: QObject(parent)
, m_gender(Person::Unknown)
{
}
getter 函数以该属性命名,通常是一个简单的 const 函数。当属性真的改变时,setter 发出改变的信号。为此,我们插入一个警卫来将当前值与新值进行比较。只有当值不同的时候,我们把它分配给成员变量并发出改变的信号。
QString Person::name() const
{
return m_name;
}
void Person::setName(const QString &name)
{
if (m_name != name) // guard
{
m_name = name;
emit nameChanged(m_name);
}
}
通过定义一个派生自 QObject 的类,我们获得了更多的元对象能力,我们可以使用 metaObject() 方法来探索。例如从对象检索类名。
Person* person = new Person();
person->metaObject()->className(); // "Person"
Person::staticMetaObject.className(); // "Person"
还有更多的功能可以被 QObject 基类和元对象访问。请查看 QMetaObject 文档。
15.3 构建系统
在不同的平台上稳定的编译软件是一个复杂的任务。你将会遇到不同环境下的不同编译器,路径和库变量的问题。Qt 的目的是防止应用开发者遭遇这些跨平台问题。为了完成这个任务,Qt 引进了 qmake 编译文件生成器。qmake 操作以 .pro 结尾的项目文件。这个项目文件包含了关于应用程序的说明和需要读取的资源文件。用 qmake 执行这个项目文件会为你生成一个在 unix 和 mac 的 Makefile,如果在 windows 下使用 mingw 编译工具链也会生成。否则可能会创建一个 visual studio 项目或者一个 xcode 项目。
在 unix 下,Qt 中的典型构建流程将是:
$ edit myproject.pro
$ qmake // generates Makefile
$ make
Qt 还允许您使用影子构建。影子构建是源代码位置之外的构建。假设我们有一个带有 myproject.pro 文件的 myproject 文件夹。流程将如下所示:
$ mkdir build
$ cd build
$ qmake ../myproject/myproject.pro
我们创建一个构建文件夹,然后使用我们的项目文件夹的位置从构建文件夹内部调用 qmake。这将以所有构建工件存储在构建文件夹而不是我们的源代码文件夹内的方式设置 make 文件。这允许我们为不同的 qt 版本创建构建,同时构建配置,并且它不会使我们的代码文件夹变得混乱,这是一件好事。
当我们使用 Qt Creator 时,它会为我们在幕后执行这些操作,而我们通常不用关心这些步骤。对于较大的项目和想要了解构建流程时,建议我们从命令行学习构建我们的 qt 项目。
15.3.1 QMake
QMake 读取我们的项目文件并生成构建文件的工具。项目文件是对项目的配置,外部依赖关系和源文件的简化记录。最简单的项目文件可能是这样的:
// myproject.pro
SOURCES += main.cpp
在这里,我们构建一个可执行的应用程序,它将基于项目文件名称命名为 myproject。该构建将仅包含 main.cpp 源文件。默认情况下,我们将使用该项目的 QtCore 和 QtGui 模块。如果我们的项目是 QML 应用程序,我们需要将 QtQuick 和 QtQml 模块添加到列表中:
// myproject.pro
QT += qml quick
SOURCES += main.cpp
现在,构建文件知道与 QtQml 和 QtQuick Qt 模块链接。QMake 使用 =,+= 和 -= 的概念分别从选项列表中分配,添加,删除元素。对于没有 UI 依赖关系的纯控制台构建,您将删除 QtGui 模块:
// myproject.pro
QT -= gui
SOURCES += main.cpp
当我们要构建库而不是应用程序时,我们需要更改构建模板:
// myproject.pro
TEMPLATE = lib
QT -= gui
HEADERS += utils.h
SOURCES += utils.cpp
现在,该项目将构建为没有 UI 依赖性的库,并使用 utils.h 头文件和 utils.cpp 源文件。库的格式将取决于我们正在构建项目的操作系统。
通常我们会有更复杂的设置,需要建立一套项目。为此,qmake 提供了 subdirs 模板。假设我们将有一个 mylib 和一个 myapp 项目。那么我们的设置可能是这样的:
my.pro
mylib/mylib.pro
mylib/utils.h
mylib/utils.cpp
myapp/myapp.pro
myapp/main.cpp
我们已经知道 mylib.pro 和 myapp.pro 的外观如何。my.pro 作为总体项目文件将如下所示:
// my.pro
TEMPLATE = subdirs
subdirs = mylib \
myapp
myapp.depends = mylib
这个项目有两个子项目:mylib 和 myapp,其中 myapp 依赖于 mylib。当我们在此项目文件上运行 qmake 时,它会为相应文件夹中的每个项目生成文件一个构建文件。当我们运行 my.pro 的 make 文件时,还会构建所有子项目。
有时我们需要根据我们的配置在一个平台上做一件事,另外还要做其他平台上的一件事情。因为这个 qmake 引入了范围的概念。当配置选项设置为 true 时,应用范围。
例如,使用可以使用的 unix 下特定的 utils 实现:
unix {
SOURCES += utils_unix.cpp
} else {
SOURCES += utils.cpp
}
它说的是如果 CONFIG 变量包含一个 unix 选项,然后应用此范围,否则使用 else 路径。一个典型的应用是删除 mac 下的应用捆绑:
macx {
CONFIG -= app_bundle
}
这将创建我们的应用程序作为一个普通的可执行文件,而不是 mac 应用程序安装的 .app 文件夹。
当我们开始编写 Qt 应用程序时,基于 QMake 的项目通常是第一选择。还有其他的选择。都有自己的好处和缺点。接下来我们将再讨论这些其他选项。
** 参考 **
- QMake Manual —— qmake 手册的目录
- QMake Language —— 价分配,范围等等
- QMake Variables —— 对 TEMPLATE、CONFIG、QT 等变量进行说明
15.3.2 CMake
CMake 是 Kitware 创建的工具。Kitware 以其 3D 视觉软件 VTK 以及跨平台 makefile 生成器 CMake 而闻名。它使用一系列 CMakeLists.txt 文件来生成平台特定的 make 文件。CMake 由 KDE 项目使用,因此与 Qt 社区有特殊关系。
CMakeLists.txt 是用于存储项目配置的文件。对于使用 QtCore 的简单的示例,项目文件将如下所示:
// ensure cmake version is at least 3.0
cmake_minimum_required(VERSION 3.0)
// adds the source and build location to the include path
set(CMAKE_INCLUDE_CURRENT_DIR ON)
// Qt's MOC tool shall be automatically invoked
set(CMAKE_AUTOMOC ON)
// using the Qt5Core module
find_package(Qt5Core)
// create excutable helloworld using main.cpp
add_executable(helloworld main.cpp)
// helloworld links against Qt5Core
target_link_libraries(helloworld Qt5::Core)
这将使用 main.cpp 构建一个 helloworld 可执行文件,并与外部 Qt5Core 库链接。构建文件可以修改为更通用:
// sets the PROJECT_NAME variable
project(helloworld)
cmake_minimum_required(VERSION 3.0)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON)
find_package(Qt5Core)
// creates a SRC_LIST variable with main.cpp as single entry
set(SRC_LIST main.cpp)
// add an executable based on the project name and source list
add_executable(${PROJECT_NAME} ${SRC_LIST})
// links Qt5Core to the project executable
target_link_libraries(${PROJECT_NAME} Qt5::Core)
你可以看到,CMake 相当强大。需要一些时间来习惯语法。一般来说,据说 CMake 更适合大型和复杂的项目。
** 参考 **
- CMake Help —— 在线可用,也可作为 QtHelp 格式使用
- Running CMake
- KDE CMake Tutorial
- CMake Book
- CMake and Qt