首页 > 技术文章 > 第十七章:Qt C++

sammy621 2022-03-31 17:31 原文

第十七章:Qt C++

Qt 和 C++

Qt 是包含QML和Javascript扩展的C++ 工具集。有不少语言作为Qt的绑定开发语言,但Qt本身是用C++ 开发的。C++ 的精神在类中随处可见。本节,我们将从C++ 的视角来审视Qt,以更好地理解如何使用C++ 开发的原生插件来扩展QML。通过 C++ ,能够扩展和控制提供给QML的运行环境。

本单元仅就Qt而言,需要读者对C++ 的基础概念具有一定的了解。Qt并不依赖于C++ 的高级特征,我通常认为Qt风格的C++ 更具可读性,所以不用担心你的C++ 知识不够牢固。

Qt C++

从C++ 的角度来看Qt,会发现Qt为C++ 增加了不少现代语言的特性,如内省数据的可用性。这是通过QObject基类的应用达到的。内省数据,或称为元数据,在运行时保留着类的信息,这在普通的C++ 里无法做到。这使得动态获取对象的属性细节以及可用函数等信息成为可能。
Qt 应用元数据信息实现了非常松散的回调函数绑定理念,即信号。每个信号可以连接到任意数量的槽,也可连到其它信号。当一个信号从对象实例中发出时,其连接的槽就被激活了。发出信号的对象无须知道拥有槽的对象的细节,反之亦然,这个机制以非常少的组件间依赖来创建复用性很强的组件。

Qt for Python

内省特性也用于创建动态语言绑定,可以暴露C++ 对象实例给QML,可以从Javascript调用C++ 函数。除了标准的Javascript绑定也有其它的语言绑定到 Qt C++ ,官方有与Python的绑定,称为 PySide6

跨平台

Qt围绕这个核心理念,使得使用C++ 开发跨平台的应用程序成为可能。Qt C++ 针对不同的操作系统提供了一个平台级别的抽象层,开发者可以将精力集中于手头的业务逻辑而不是处理诸如在不同操作系统打开文件的琐碎操作。这意味着可以在不同操作系统如Windows、OS X、Linux 下,重新编译相同的代码,而由Qt来为不同操作系统处理特定事务的细节。结果会生成基于平台特征的外观和体验的原生应用。因为移动桌面是一种新的桌面系统,新的Qt版本编译后的程序可以部署在一系列的移动平台,比如iOS,Android, Jolla,BlackBerry,Ubuntu Phone, Tizen。
提到重用,不仅意味着代码重用,开发技能也是通用的。一个了解Qt能够适配多平台特性的团队会聚焦于单一平台和特定的技术,因为Qt如此灵活以致于团队可以以相同的技术创造不同系统的组件。
对于所有的平台,Qt提供一系列基础类型,比如,完全支持Unicode的字符、列表、向量、缓存。它还为目标平台的主循环提供了一个通用抽象,以及跨平台的线程和网络支持。一般的理念是,对于应用程序开发人员来说,Qt 包含了所有必需的功能。对于特定领域的业务,例如与本机程序库文件的接口,Qt附带了几个帮助类,使之更容易实现。

样例程序

理解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里有用于构建无用户界面应用的最常用的类。详见 QtCore class listQtCore overview
要用CMake和make来构建应用程序。CMake读取工程文件CMakeLists.txt,并生成一个Makefile,以用于构建应用程序。CMake也支持其它的编译系统,比如ninja。工程文件是平台无关的,并且CMake有一些规则应用于特定平台的设置,以生成makefile。项目也可以基于指定平台原则,在某些特定场景下来包含一些操作系统平台。
这有一个由Qt Creator生成的简单的工程文件。注意,Qt试图创建一个兼容Qt 5和Qt 6,以及兼容Android,OS X 的工程文件。

cmake_minimum_required(VERSION 3.14)

project(projectname VERSION 0.1 LANGUAGES CXX)

set(CMAKE_INCLUDE_CURRENT_DIR ON)

set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# QtCreator supports the following variables for Android, which are identical to qmake Android variables.
# Check https://doc.qt.io/qt/deployment-android.html for more information.
# They need to be set before the find_package(...) calls below.

#if(ANDROID)
#    set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android")
#    if (ANDROID_ABI STREQUAL "armeabi-v7a")
#        set(ANDROID_EXTRA_LIBS
#            ${CMAKE_CURRENT_SOURCE_DIR}/path/to/libcrypto.so
#            ${CMAKE_CURRENT_SOURCE_DIR}/path/to/libssl.so)
#    endif()
#endif()

find_package(QT NAMES Qt6 Qt5 COMPONENTS Core Quick REQUIRED)
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Quick REQUIRED)

set(PROJECT_SOURCES
        main.cpp
        qml.qrc
)

if(${QT_VERSION_MAJOR} GREATER_EQUAL 6)
    qt_add_executable(projectname
        MANUAL_FINALIZATION
        ${PROJECT_SOURCES}
    )
else()
    if(ANDROID)
        add_library(projectname SHARED
            ${PROJECT_SOURCES}
        )
    else()
        add_executable(projectname
          ${PROJECT_SOURCES}
        )
    endif()
endif()

target_compile_definitions(projectname
  PRIVATE $<$<OR:$<CONFIG:Debug>,$<CONFIG:RelWithDebInfo>>:QT_QML_DEBUG>)
target_link_libraries(projectname
  PRIVATE Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Quick)

set_target_properties(projectname PROPERTIES
    MACOSX_BUNDLE_GUI_IDENTIFIER my.example.com
    MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
    MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
)

if(QT_VERSION_MAJOR EQUAL 6)
    qt_import_qml_plugins(projectname)
    qt_finalize_executable(projectname)
endif()

不必逐行去看这修文件。仅需要记住,Qt 使用CMake的CMakeLists.txt文件来生成特定平台的makefiles,之后它被用来构建工程。在编译系统章节,我们将涉及列基本的、手写的CMake文件。
上面简单的代码仅是写了一段文本并退出了应用程序。如果对于一个命令行工具,这已经够用了。对于用户界面,你需要事件循环来等待用户输入以及一些重绘工作操作。接下来是一个同样的例子,不过是用按钮来触发写文本操作。
main.cpp惊人地小。我们将代码移动到自定义类中以能够将Qt的信号和槽应用于用户输入,比如,处理鼠标点击。就象马上要见到的那样,信号和槽机制一般需要一个对象实例,但它也可以使用C++ lambdas 。

#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里,既可以用QML也可以用控件来构建应用程序。本书我们聚焦于QML,但本章我们将来看下控件。这让你仅用C++ 来构建程序。

主窗体本身是一个控件。它是最高层级的窗体,因为其没有任何的父类。这来源于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()的槽。槽可以是公有的、保护的或私有的,并且可以象其它类方法那样被调用。你也会遇到有很多信号标识的signals节部分。这些方法不应该被调用,而且不能被实现。信号和槽都是被Qt的元数据信息系统处理,并能够在运行时被探测和调用。
storeContent()的目的就是,当按钮被点击时被调用。一起来实现它。

#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方法将信号clicked()注册到槽storeConnect()。每当信号clicked释放时,槽storeConnect()也被调用。现在这两个对象(按钮和主窗体)通过信号和槽交流,而彼此并未在意对方的存在。这就是松耦合,这使得多数Qt类使用QObject为其基类。

QObject

正如介绍中所描述的,QObject是信号和槽等Qt核心功能的基础。这是通过由QObject所提供的内省特性所实现的。QObject是几乎所有Qt类的基类。诸如QColorQStringQList 等值类型例外。
Qt对象是标准的C++ 对象,但具有更多的能力。这可以从两个方面来讲:内省和内存管理。第一个意味着Qt对象知道自己的类名,与其它类的关系,以及方法和属性。内存管理的概念意味着每个Qt对象都是父对象的子对象。父对象拥有子对象,当父对象销毁时,会先将其所有子对象销毁。
理解QObject的能力如何影响一个类的最好的方法是写一个标准C++ 类并由Qt启用它。下面的类代表了这样的普通类。
person类是一个有着name和gender属性的数据类。person类使用Qt的对象系统为类添加元数据信息。它允许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)
    
    // makes the type creatable in QML
    QML_ELEMENT

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, or called
    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将初始化为null(QString::isNull()),而gender成员将显示初始化为未知性别。

Person::Person(QObject *parent)
    : QObject(parent)
    , m_gender(Person::Unknown)
{
}

getter函数以属性名命名,通常是个基本的const函数。setter函数在属性发生变化时发出changed信号。为确保值确实改变,我们插入一个验证表达式来比较新值与当前值,仅当二者不同时,我们才赋值给成员并发出值变化的信号。

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基类和mata对象,请查询QMetaObject文档。

注意
QObject以及Q_OBJECT宏有一个轻量级的兄弟:Q_GADGET。这个Q_GADGET宏可以被插入 非继承于QObject 的私有部分,以暴露属性和可激活方法。注意 Q_GADGET 对象不能有信号,所以属性的变化无法提供变动通知信号。尽管如此,这对于为从 C++ 暴露给 QML 的数据结构提供类似 QML 的接口,还是很有用的,无需调用成熟但耗费资源的 QObject

编译系统

编译跨不同平台的可靠软件是一项非常复杂的任务。你会遇到不同的环境不同的编译器,系统变量以及库文件的不同。Qt的初衷就是为开发者解决这些跨平台的问题。Qt依赖CMake将工程文件CMakeLists.txt转换成指定平台的make文件,然后被特定平台的工具编译。

注意
Qt自带三种不同的编译系统。原生的Qt编译系统被称为qmake。另一种Qt指定的编译系统是QBS,它可以使用声明式的方法来描述编译顺序。从Qt 6开始,Qt将官方指定编译器从qmake转换为CMake。

Unix下的Qt的典型编译流程是:

vim CMakeLists.txt
cmake . // generates Makefile
make

使用 Qt,我们鼓励您使用影子构建。影子构建是指在源码路径之外来构建。假定我们有一个内有CMakeLists.txt的myproject文件夹。流程应该是这样:

mkdir build
cd build
cmake ..

我们创建了一个build文件夹,并从工程文件夹路径中进入到build文件夹,从那里调用cmake。所有构建文件(工件,构建过程中的文件)都存储在buid文件夹而不是我们的源代码文件夹中,将以这种方式来来生成makefile。这允许我们同时为不同的 qt 版本和不同的build设置来完成项目构建,而且它不会弄乱我们的源代码文件夹,这总是一件好事。
当你使用Qt Creator时,它在幕后为你做了这些事情,而你在大多数的时候无须担心这些步骤。对于大型的工程以及地构建流程更深层次的理解,建议你从命令行来构建你的Qt工程,以保证你对正在发生的事情有所掌控。

CMAKE

CMake是由Kitware创建的工具。Kitware是以3D视觉软件VTK以及CMake这一跨平台的makefile生成器而闻名于世。它使用一系列CMakeLists.txt文件来生成特定平台的makefiles。KDE工程也在用CMake,也因此跟Qt社区有特别的关系,而自从Qt 6版本开始,它是更受推荐的编译方式。
CMakeLists.txt是用于存储项目配置的一个文件。对一个使用Qt Core的简单的hello world的工程文件应该如下所示:

// ensure cmake version is at least 3.16.0
cmake_minimum_required(VERSION 3.16.0)

// defines a project with a version
project(foundation_tests VERSION 1.0.0 LANGUAGES CXX)

// pick the C++ standard to use, in this case C++17
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

// tell CMake to run the Qt tools moc, rcc, and uic automatically
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)

// configure the Qt 6 modules core and test
find_package(Qt6 COMPONENTS Core REQUIRED)
find_package(Qt6 COMPONENTS Test REQUIRED)

// define an executable built from a source file
add_executable(foundation_tests
    tst_foundation.cpp
)

// tell cmake to link the executable to the Qt 6 core and test modules
target_link_libraries(foundation_tests PRIVATE Qt6::Core Qt6::Test)

这将使用tst_foundation.cpp构建foundations_tests可执行程序,并连接Qt 6的Core和Test程序库。本教程中将会出现更多使用CMake文件的例子,因为我们在所有基于C++ 的例子中使用CMake。
CMake是一个复杂、强大、具有工具性,但需要花点时间来适应它的语法。CMake非常灵活,在大型和复杂的项目中更能体现其价值。

参考

CMake Help可在线使用但仍然是Qt Help的格式
Running CMake
KDE CMake Tutorial
CMake Book
CMake and Qt

QMake

QMake是读取工程文件并生成构建(编译)文件的工具。工程文件是被简化记录下来的工程配置、外部依赖、以及源码文件。最简单的工程文件可能是这样:

// myproject.pro

SOURCES += main.cpp

根据工程文件的名字,这里会创建一个名为myproject的可执行应用程序。构建仅包含main.cpp源码。默认将在工程中使用QtCoreQtGui两个模块。如果工程类型是QML应用,则需要为将QtQuuickQtQml模块添加到列表:

QT += qml quick
SOURCE += main.cpp

现在构建文件知道了要连接QtQmlQtQuick两个Qt模块。QMake使用=+=-=来在选项列表里分别指定、添加和移除组件模块。对于一个不依赖于界面UI的纯控制台构建,可以移除QtGui模块:

// myproject.pro

QT -= gui

SOURCES += main.cpp

当想编译一个库文件而非一个工程时,需要改变构建模板:

// myproject.pro
TEMPLATE = lib

QT -= gui

HEADERS += utils.h
SOURCES += utils.cpp

现在,工程将使用头文件utils.h 和源文件utils.cpp来构建一个不依赖于UI的库文件。库文件的格式将依赖于构建工程所在的操作系统。
通常会有更多更复杂的设置和需求来构建工程。为此,qmake提供了子项(目)subdirs模板。假定有一个myapp工程和一个mylib库,那构建步骤可以是这样 :

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

以上声明了一个工程有两个子项目:mylibmyapp,而myapp依赖于mylib。当qmake运行这个工程文件时,它会为每个工程在其相应文件夹里生成一个构建文件。当为my.pro运行makefile时,所有工程都被构建。
有时需要在一个平台上使用这个配置,但在另外的平台需要另外的配置。为此,qmake引入了范围(scopes)的概念。当配置选项设置为true时,就被范围被应用了。
比如,要使用Unix特定工具实现,可以这样:

unix {
    SOURCES += utils_unix.cpp
} else {
    SOURCES += utils.cpp
}

也就是说,当CONFIG变量包含Unix选项,则应用这个范围,否则使用另外的。一个典型的方法是删除mac下的应用程序捆绑:

macx {
    CONFIG -= app_bundle
}

这将把你的应用程序创建为mac下的一个普通可执行文件,而不是一个应用程序放置在应用程序文件夹(.app文件夹)。
当你使用Qt来编写应用时,QMake编译通常是首选的方式。有其它构建选项,各自有优点与缺点。我们将讨论这些构建选项。

参考

QMake Manual - QMake 手册目录
QMake Language- 赋值、范围及其它
QMake Variables- TEMPLATE, CONFIG, QT 等变量说明

常用Qt类

大多数Qt类是从QObject类继承的。它包含了Qt的核心概念。但在框架中也有很多其它类。在进一步探究QML和如何拓展它之前,先了解一些有助于理解的基础Qt类。
本节所涉及的例子是使用Qt Test 库来编写的。这样我们就可以确保代码可运行,而不需要为此构建整个项目。测试库里的QVERIFYQCOMPARE两个函数用来断言特定条件。使用{ }来避免命名冲突。不要让这些困扰到你。

QString

通常,Qt中的文本是基于Unicode编码的,使用QString类。它自带了大量现代化框架中非常出色的函数特性。对于8-bit数据,一般使用QByteArray类,而对于ASCII符号,使用QLatin1String来有效使用内存。对于字符串列表可以使用QList<QString>或更简单的QStringList类(此类继承于QList<QString>)。
下面是一些如可使用QString的例子。QString可被创建于栈上,但其数据存储于堆。当将一个字符串变量赋值给另一个时,数据将不会被拷贝,仅会将对数据的引用赋值给另一个变量。所以这是非常低成本,这让开发者聚集于业务代码而非内存处理。QString使用引用计数来判断何时可以安全删除数据。这个特性被称为隐式共享Implicit Sharing,这被用于很多Qt类中。

QString data("A,B,C,D"); // create a simple string
// split it into parts
QStringList list = data.split(",");
// create a new string out of the parts
QString out = list.join(",");
// verify both are the same
QVERIFY(data == out);
// change the first character to upper case
QVERIFY(QString("A") == out[0].toUpper());

以下是将数值与字符相互转换的例子。float、double及其它类型也有相应的转换函数。可以到Qt文档中查找这里所用到的以及其它函数。

// create some variables
int v = 10;
int base = 10;
// convert an int to a string
QString a = QString::number(v, base);
// and back using and sets ok to true on success
bool ok(false);
int v2 = a.toInt(&ok, base);
// verify our results
QVERIFY(ok == true);
QVERIFY(v = v2);

在文本中经常遇到参数字符。一种方式是可以使用QString("Hello" + name)的形式,而更灵活一点的方法是arg标记法。它在翻译时保留了参数的顺序。

// create a name
QString name("Joe");
// get the day of the week as string
QString weekday = QDate::currentDate().toString("dddd");
// format a text using paramters (%1, %2)
QString hello = QString("Hello %1. Today is %2.").arg(name).arg(weekday);
// This worked on Monday. Promise!
if(Qt::Monday == QDate::currentDate().dayOfWeek()) {
    QCOMPARE(QString("Hello Joe. Today is Monday."), hello);
} else {
    QVERIFY(QString("Hello Joe. Today is Monday.") !=  hello);
}

有时想在代码中直接使用Unicode字符。这样你需要记住在QCharQString类中如何标记它们。

// Create a unicode character using the unicode for smile :-)
QChar smile(0x263A);
// you should see a :-) on you console
qDebug() << smile;
// Use a unicode in a string
QChar smile2 = QString("\u263A").at(0);
QVERIFY(smile == smile2);
// Create 12 smiles in a vector
QVector<QChar> smilies(12);
smilies.fill(smile);
// Can you see the smiles
qDebug() << smilies;

这里给出了在Qt如何处理Unicode的例子。对于非Unicode,QByteArray类有许多函数用于转换。可以查阅QString在Qt文档中的说明,那里有许多很好的例子。

有序容器

列表、队列、向量、链表都是有序容器。最常用的有序容器是QList类。它是基于模板的类,需要使用类型来初始化。它也是隐式分享,在堆上保存数据。所有的容器类都应该在栈上创建。一般不要象new QList<T>()这样用,也就是一定不要把new和容器一起用。
QList类与QString一样强大,它提供了很多很棒的API来操作数据。下面是些小例子来介绍如何使用容器,以及使用C++ 11特性来遍历列表。

// Create a simple list of ints using the new C++11 initialization
// for this you need to add "CONFIG += c++11" to your pro file.
QList<int> list{1,2};

// append another int
list << 3;

// We are using scopes to avoid variable name clashes

{ // iterate through list using Qt for each
    int sum(0);
    foreach (int v, list) {
        sum += v;
    }
    QVERIFY(sum == 6);
}
{ // iterate through list using C++ 11 range based loop
    int sum = 0;
    for(int v : list) {
        sum+= v;
    }
    QVERIFY(sum == 6);
}

{ // iterate through list using JAVA style iterators
    int sum = 0;
    QListIterator<int> i(list);

    while (i.hasNext()) {
        sum += i.next();
    }
    QVERIFY(sum == 6);
}

{ // iterate through list using STL style iterator
    int sum = 0;
    QList<int>::iterator i;
    for (i = list.begin(); i != list.end(); ++i) {
        sum += *i;
    }
    QVERIFY(sum == 6);
}


// using std::sort with mutable iterator using C++11
// list will be sorted in descending order
std::sort(list.begin(), list.end(), [](int a, int b) { return a > b; });
QVERIFY(list == QList<int>({3,2,1}));


int value = 3;
{ // using std::find with const iterator
    QList<int>::const_iterator result = std::find(list.constBegin(), list.constEnd(), value);
    QVERIFY(*result == value);
}

{ // using std::find using C++ lambda and C++ 11 auto variable
    auto result = std::find_if(list.constBegin(), list.constBegin(), [value](int v) { return v == value; });
    QVERIFY(*result == value);
}

关联容器

map、dictionary、set是关联容器的例子。它们用key键来存储值value。它们的优势是查找。我们用QHash来演示使用最多的容器,同时演示了C++ 11的一些特性。

QHash<QString, int> hash({{"b",2},{"c",3},{"a",1}});
qDebug() << hash.keys(); // a,b,c - unordered
qDebug() << hash.values(); // 1,2,3 - unordered but same as order as keys

QVERIFY(hash["a"] == 1);
QVERIFY(hash.value("a") == 1);
QVERIFY(hash.contains("c") == true);

{ // JAVA iterator
    int sum =0;
    QHashIterator<QString, int> i(hash);
    while (i.hasNext()) {
        i.next();
        sum+= i.value();
        qDebug() << i.key() << " = " << i.value();
    }
    QVERIFY(sum == 6);
}

{ // STL iterator
    int sum = 0;
    QHash<QString, int>::const_iterator i = hash.constBegin();
    while (i != hash.constEnd()) {
        sum += i.value();
        qDebug() << i.key() << " = " << i.value();
        i++;
    }
    QVERIFY(sum == 6);
}

hash.insert("d", 4);
QVERIFY(hash.contains("d") == true);
hash.remove("d");
QVERIFY(hash.contains("d") == false);

{ // hash find not successfull
    QHash<QString, int>::const_iterator i = hash.find("e");
    QVERIFY(i == hash.end());
}

{ // hash find successfull
    QHash<QString, int>::const_iterator i = hash.find("c");
    while (i != hash.end()) {
        qDebug() << i.value() << " = " << i.key();
        i++;
    }
}

// QMap
QMap<QString, int> map({{"b",2},{"c",2},{"a",1}});
qDebug() << map.keys(); // a,b,c - ordered ascending

QVERIFY(map["a"] == 1);
QVERIFY(map.value("a") == 1);
QVERIFY(map.contains("c") == true);

// JAVA and STL iterator work same as QHash

文件IO

经常有读取文件的需求。QFile实际上是从QObject继承的,多数时候,它创建在栈上。QFile包含通知用户何时可读取数据的信号。这允许以块异步方式读取数据,直到读完整个文件。简便起见,也允许以阻塞模式(非异步模式)读取数据。这种方式仅适合于小数据量而非大文件。幸好我们在本例中只用少量数据。
除了能从文件读取原始数据到QByteArray外,还可以用QDataStream来读取类型数据,用QTextStream来读取Unicode字符串。来看下如何做到。

QStringList data({"a", "b", "c"});
{ // write binary files
    QFile file("out.bin");
    if(file.open(QIODevice::WriteOnly)) {
        QDataStream stream(&file);
        stream << data;
    }
}
{ // read binary file
    QFile file("out.bin");
    if(file.open(QIODevice::ReadOnly)) {
        QDataStream stream(&file);
        QStringList data2;
        stream >> data2;
        QCOMPARE(data, data2);
    }
}
{ // write text file
    QFile file("out.txt");
    if(file.open(QIODevice::WriteOnly)) {
        QTextStream stream(&file);
        QString sdata = data.join(",");
        stream << sdata;
    }
}
{ // read text file
    QFile file("out.txt");
    if(file.open(QIODevice::ReadOnly)) {
        QTextStream stream(&file);
        QStringList data2;
        QString sdata;
        stream >> sdata;
        data2 = sdata.split(",");
        QCOMPARE(data, data2);
    }
}

更多类

Qt 是内涵丰富的应用程序框架。它有着上千个类。需要花费些时间来学会并习惯其用法。幸好Qt有着包括丰富例子的详细的文档。多数时候当你搜索类时,最常用的场景和用法都以代码片段形式为你准备好了。这意味着你只需要拷贝和调整这些代码。Qt安装附带的源码中的例子也非常有帮助。确保这些示例代码可运行,易于查找,将会使你的更具生产力。不要浪费时间。逛Qt 社区也很有帮助。当要提问时,描述出具体问题并附上能体现你需求的代码将有助于解决问题。这将快速提升别人响应你问题的速度。所以花点时间让其它人更容易帮到你。
下面是一些我认为必读的类的文档:

对于初学者来说应该够了。

C++ 中的模型

将C++ 和QML集成起来的最常用的方法之一是通过模型models 。模型model为视图提供数据,如ListViewsGridViewPathViews,以及其它视图,它们接收模型并为模型中的每个指定属性创建委托。视图足够聪明,仅创建那些可见或在缓存范围内的实例。这使得大量模型拥有成千上万个可用于显示的属性,但仍然可以对应着非常简洁的用户界面。委托的角色象一个模板,来渲染模型的属性数据。所以总的来说:视图使用委托做模板渲染来自模型的属性数据。对视图来说模型是数据提供方。
如果不想使用C++ ,你仍然可以在纯QML中定义模型。有多种方法来为视图提供模型。对于来自 C++ 的数据或大量数据的处理,C++ 模型比这种纯 QML 方法更适合。但通常只需要渲染几个条目,那么这些 QML 模型非常适合。

ListView {
    // using a integer as model
    model: 5
    delegate: Text { text: 'index: ' + index }
}

ListView {
    // using a JS array as model
    model: ['A', 'B', 'C', 'D', 'E']
    delegate: Text { 'Char['+ index +']: ' + modelData }
}

ListView {
    // using a dynamic QML ListModel as model
    model: ListModel {
        ListElement { char: 'A' }
        ListElement { char: 'B' }
        ListElement { char: 'C' }
        ListElement { char: 'D' }
        ListElement { char: 'E' }
    }
    delegate: Text { 'Char['+ index +']: ' + model.char }
}

QML知道如何处理这些不同类型的模型。对于C++ 来的模型,模型期待其遵循特定的协议。这个协议是由定义在QAbstractItemModel的API,以及描述其动态行为的文档来共同定义的。API被用于驱动桌面控件视图,其足够灵活以作为树、多列表格、以有列表的基础。在QML,一般既使用API的列表变量QAbstractListModel,或对于TableView元素,使用API的表格变量QAbstractTableModel。API包括一些强制要被实现的函数,也有一些可选择实现的用于扩展模型能力的函数。可选的部分多是用于处理动态改变、添加、删除数据等场景。

一个简单模型

一个典型的QML的C++ 模型继承自QAbstractListModel,至少要实现datarowCount两个函数。在下面的例子中,要用到QColor类所提供的一系列SVG颜色名,并使用我们的模型呈现它们。数据被存放在QList<QString>数据容器。
DataEntryModel类继承自QAbstractListModel并实现了必须要实现的函数。我们可以忽略父类中的rowCount,因为它仅用于树形模型。QModelIndex类为单元格提供了行和列信息,而视图要从单元格中检索数据。视图是从基于行/列及角色的模型中拉取数据。QAbstractListModel定义在QtCore模块,而QColorQtGui模块中。这是要添加QtGui依赖的原因。对于QML应用来说可以依赖于QtGui,但一般不依赖于QtWidgets

#ifndef DATAENTRYMODEL_H
#define DATAENTRYMODEL_H

#include <QtCore>
#include <QtGui>

class DataEntryModel : public QAbstractListModel
{
    Q_OBJECT
public:
    explicit DataEntryModel(QObject *parent = 0);
    ~DataEntryModel();

public: // QAbstractItemModel interface
    virtual int rowCount(const QModelIndex &parent) const;
    virtual QVariant data(const QModelIndex &index, int role) const;
private:
    QList<QString> m_data;
};

#endif // DATAENTRYMODEL_H

在实现的部分,最复杂的应该是数据函数。先要做一个范围有效性检查,以确保拿到一个有效的索引index。然后检查显示角色是受支持的。模型中的每个项目可以有多个显示角色,来定义各个方面所包含的数据。DisplayRole是视图要求的默认文本角色。Qt定义了几个默认角色可用,但模型通常自定义角色。本例中,所有不包含显示角色的调用此时都被忽略,而返回默认的QVariant()值。

#include "dataentrymodel.h"

DataEntryModel::DataEntryModel(QObject *parent)
    : QAbstractListModel(parent)
{
    // initialize our data (QList<QString>) with a list of color names
    m_data = QColor::colorNames();
}

DataEntryModel::~DataEntryModel()
{
}

int DataEntryModel::rowCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent);
    // return our data count
    return m_data.count();
}

QVariant DataEntryModel::data(const QModelIndex &index, int role) const
{
    // the index returns the requested row and column information.
    // we ignore the column and only use the row information
    int row = index.row();

    // boundary check for the row
    if(row < 0 || row >= m_data.count()) {
        return QVariant();
    }

    // A model can return data for different roles.
    // The default role is the display role.
    // it can be accesses in QML with "model.display"
    switch(role) {
        case Qt::DisplayRole:
            // Return the color name for the particular row
            // Qt automatically converts it to the QVariant type
            return m_data.value(row);
    }

    // The view asked for other data, just return an empty QVariant
    return QVariant();
}

下一步应该是调用qmlRegisterType来把模型注册给QML。这步应该在QML文件被加载前在main.cpp里完成。

#include <QtGui>
#include <QtQml>

#include "dataentrymodel.h"

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    // register the type DataEntryModel
    // under the url "org.example" in version 1.0
    // under the name "DataEntryModel"
    qmlRegisterType<DataEntryModel>("org.example", 1, 0, "DataEntryModel");

    QQmlApplicationEngine engine;
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

    return app.exec();
}

现在可以通过QML引入语句import org.example 1.0,来访问DataEntryModel,就象使用其它QML元素那样:DataEntryModel {}
在下面的例子中使用这个来展示颜色条目的简单列表。

import org.example 1.0

ListView {
    id: view
    anchors.fill: parent
    model: DataEntryModel {}
    delegate: ListDelegate {
        // use the defined model role "display"
        text: model.display
    }
    highlight: ListHighlight { }
}

ListDelegate是展示文本的自定义类型。ListHighlight只是一个矩形。为便于清晰展示例子的逻辑,代码进行了精简。
这个视图现在可以使用C++ 模型以及模型的display属性来展示字符串列表。虽然非常简单,但已经在QML中可用了。通常数据在模型之外提供,而模型主要为视图扮演接口的角色。

注意
可以使用QAbstractTableModel来展示表格数据,以替代列表。与实现一个QAbstractListModel的唯一不同是,你必须提供columnCount方法。

复杂一些的数据

实际上,模型数据通常更复杂。所以有必要定义角色,以便视图可以通过属性查询其它数据。比如,模型不仅能提供16进制格式的字符串颜色值,在QML中可能也可以由HSV颜色模型提供色调hue,饱和度saturation, 以及亮度 brightness,如 “model.hue”, “model.saturation” 以及 “model.brightness” 的形式。

#ifndef ROLEENTRYMODEL_H
#define ROLEENTRYMODEL_H

#include <QtCore>
#include <QtGui>

class RoleEntryModel : public QAbstractListModel
{
    Q_OBJECT
public:
    // Define the role names to be used
    enum RoleNames {
        NameRole = Qt::UserRole,
        HueRole = Qt::UserRole+2,
        SaturationRole = Qt::UserRole+3,
        BrightnessRole = Qt::UserRole+4
    };

    explicit RoleEntryModel(QObject *parent = 0);
    ~RoleEntryModel();

    // QAbstractItemModel interface
public:
    virtual int rowCount(const QModelIndex &parent) const override;
    virtual QVariant data(const QModelIndex &index, int role) const override;
protected:
    // return the roles mapping to be used by QML
    virtual QHash<int, QByteArray> roleNames() const override;
private:
    QList<QColor> m_data;
    QHash<int, QByteArray> m_roleNames;
};

#endif // ROLEENTRYMODEL_H

在头文件,添加角色映射表以供QML使用。当QML从模型中访问一个属性(如“model.name”),列表视图将会查找“name”的映射,并使用NameRole请求模型数据。用户定义角色应该以Qt::UserRole开头,且必须在模型中唯一 。

#include "roleentrymodel.h"

RoleEntryModel::RoleEntryModel(QObject *parent)
    : QAbstractListModel(parent)
{
    // Set names to the role name hash container (QHash<int, QByteArray>)
    // model.name, model.hue, model.saturation, model.brightness
    m_roleNames[NameRole] = "name";
    m_roleNames[HueRole] = "hue";
    m_roleNames[SaturationRole] = "saturation";
    m_roleNames[BrightnessRole] = "brightness";

    // Append the color names as QColor to the data list (QList<QColor>)
    for(const QString& name : QColor::colorNames()) {
        m_data.append(QColor(name));
    }

}

RoleEntryModel::~RoleEntryModel()
{
}

int RoleEntryModel::rowCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent);
    return m_data.count();
}

QVariant RoleEntryModel::data(const QModelIndex &index, int role) const
{
    int row = index.row();
    if(row < 0 || row >= m_data.count()) {
        return QVariant();
    }
    const QColor& color = m_data.at(row);
    qDebug() << row << role << color;
    switch(role) {
    case NameRole:
        // return the color name as hex string (model.name)
        return color.name();
    case HueRole:
        // return the hue of the color (model.hue)
        return color.hueF();
    case SaturationRole:
        // return the saturation of the color (model.saturation)
        return color.saturationF();
    case BrightnessRole:
        // return the brightness of the color (model.brightness)
        return color.lightnessF();
    }
    return QVariant();
}

QHash<int, QByteArray> RoleEntryModel::roleNames() const
{
    return m_roleNames;
}

实现代码仅改动了两个地方。第一是初始化。现在以QColor数据类型来初始化数据列表(m_data)。并且,定义了角色名称映射以备QML访问。这个映射稍后由::roleNames函数返回。
第二个变化是::data函数。增加了switch语句以涵盖其它角色(如,色调hue, 饱和度saturation, 亮度brightness)。无法从color获取SVG name,因为颜色color可以是任意颜色,而SVG names是有限的(不足以描述所有颜色)。所以我们跳过这个。需要一个结构struct { QColor, QString }来存储名字,以便识别有名字的颜色。
注册类型后,就可以在用户界面里使用模型及其条目了。

ListView {
    id: view
    anchors.fill: parent
    model: RoleEntryModel {}
    focus: true
    delegate: ListDelegate {
        text: 'hsv(' +
              Number(model.hue).toFixed(2) + ',' +
              Number(model.saturation).toFixed() + ',' +
              Number(model.brightness).toFixed() + ')'
        color: model.name
    }
    highlight: ListHighlight { }
}

将返回的类型转换为 JS 数字类型,以便能够使用定点表示法格式化数字。不调用Number函数代码也能正常工作(比如:model.saturation.toFixed(2))。选择哪种格式,取决于对返回的数据的信赖程度。

动态数据

动态数据涵盖插入、移除、从模型清理数据等方面。当条目被插入或移除,QAbstractListModel期待特定的行为。行为是以信号的形式表达,而信号是在操作之前或之后调用的。比如为一个模型插入一行数据,需要先发出信号beginInsertRows,然后操作数据,最后发出endInsertRows信号。
我们将在头文件中增加如下函数。这些函数是以Q_INVOKABLE来声明的,以便能从QML来调用。另一种方式是将其声明为公共pulic的槽。

// inserts a color at the index (0 at begining, count-1 at end)
Q_INVOKABLE void insert(int index, const QString& colorValue);
// uses insert to insert a color at the end
Q_INVOKABLE void append(const QString& colorValue);
// removes a color from the index
Q_INVOKABLE void remove(int index);
// clear the whole model (e.g. reset)
Q_INVOKABLE void clear();

此外,我们定义了count属性来获取模型的大小,以及get方法来获得给定索引位置的color。这在QML是遍历一个模型时非常有用。

// gives the size of the model
Q_PROPERTY(int count READ count NOTIFY countChanged)
// gets a color at the index
Q_INVOKABLE QColor get(int index);

插入实现首先要检查给定值是否在边界内,是否为有效值。只有满足条件后才开始插入数据。

void DynamicEntryModel::insert(int index, const QString &colorValue)
{
    if(index < 0 || index > m_data.count()) {
        return;
    }
    QColor color(colorValue);
    if(!color.isValid()) {
        return;
    }
    // view protocol (begin => manipulate => end]
    emit beginInsertRows(QModelIndex(), index, index);
    m_data.insert(index, color);
    emit endInsertRows();
    // update our count property
    emit countChanged(m_data.count());
}

追加函数Append非常简单,利用模型的大小做参数来重用添加insert函数即可实现。

void DynamicEntryModel::append(const QString &colorValue)
{
    insert(count(), colorValue);
}

移除函数与插入函数相似,但要根据移除操作逻辑。

void DynamicEntryModel::remove(int index)
{
    if(index < 0 || index >= m_data.count()) {
        return;
    }
    emit beginRemoveRows(QModelIndex(), index, index);
    m_data.removeAt(index);
    emit endRemoveRows();
    // do not forget to update our count property
    emit countChanged(m_data.count());
}

辅助函数count很简单,它只返回数据量。函数get也很简单。

QColor DynamicEntryModel::get(int index)
{
    if(index < 0 || index >= m_data.count()) {
        return QColor();
    }
    return m_data.at(index);
}

要注意返回那些QML能认识的值。如果不是QML基本类型之一或不是QML能认识的类型,就需要先用qmlRegisterTypeqmlRegisterUncreatableType先注册一下。如果用户不在QML中实例化其对象,则使用qmlRegisterUncreatableType来注册类型。
现在就可以在QML中使用模型,并从模型中插入、添加、和移除条目。这有个小例子,它允许用户输入颜色名或颜色色调值然后颜色就被添加到模型,并显示在列表视图里。在委托上的红圆圈允许用户移除模型中的条目。当条目被移除后,列表视图被模型通知关修改其内容。

这是QML代码。也可以在本章附件中找到全部的代码(可以到Github来找相关资源)。本例使用QtQuick.ControlsQtQuick.Layout模块使得代码更紧凑。控件模块提供了Qt Quick里一系列桌面相关的UI元素。布局模块提供了一些非常有用的布局管理器。

import QtQuick
import QtQuick.Window
import QtQuick.Controls
import QtQuick.Layouts

// our module
import org.example 1.0

Window {
    visible: true
    width: 480
    height: 480

    Background { // a dark background
        id: background
    }

    // our dyanmic model
    DynamicEntryModel {
        id: dynamic
        onCountChanged: {
            // we print out count and the last entry when count is changing
            print('new count: ' + dynamic.count)
            print('last entry: ' + dynamic.get(dynamic.count - 1))
        }
    }

    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 8
        ScrollView {
            Layout.fillHeight: true
            Layout.fillWidth: true
            ListView {
                id: view
                // set our dynamic model to the views model property
                model: dynamic
                delegate: ListDelegate {
                    required property var model
                    width: ListView.view.width
                    // construct a string based on the models proeprties
                    text: 'hsv(' +
                          Number(model.hue).toFixed(2) + ',' +
                          Number(model.saturation).toFixed() + ',' +
                          Number(model.brightness).toFixed() + ')'
                    // sets the font color of our custom delegates
                    color: model.name

                    onClicked: {
                        // make this delegate the current item
                        view.currentIndex = model.index
                        view.focus = true
                    }
                    onRemove: {
                        // remove the current entry from the model
                        dynamic.remove(model.index)
                    }
                }
                highlight: ListHighlight { }
                // some fun with transitions :-)
                add: Transition {
                    // applied when entry is added
                    NumberAnimation {
                        properties: "x"; from: -view.width;
                        duration: 250; easing.type: Easing.InCirc
                    }
                    NumberAnimation { properties: "y"; from: view.height;
                        duration: 250; easing.type: Easing.InCirc
                    }
                }
                remove: Transition {
                    // applied when entry is removed
                    NumberAnimation {
                        properties: "x"; to: view.width;
                        duration: 250; easing.type: Easing.InBounce
                    }
                }
                displaced: Transition {
                    // applied when entry is moved
                    // (e.g because another element was removed)
                    SequentialAnimation {
                        // wait until remove has finished
                        PauseAnimation { duration: 250 }
                        NumberAnimation { properties: "y"; duration: 75
                        }
                    }
                }
            }
        }
        TextEntry {
            id: textEntry
            onAppend: function (color) {
                // called when the user presses return on the text field
                // or clicks the add button
                dynamic.append(color)
            }

            onUp: {
                // called when the user presses up while the text field is focused
                view.decrementCurrentIndex()
            }
            onDown: {
                // same for down
                view.incrementCurrentIndex()
            }
        }
    }
}

模型视图编程在Qt开发中是较复杂的一种模式。模型不象其它应用开发那样,它是不需要实现接口的少量类之一。所有其它类可正常使用。应该在QML端来对模型进行刻画。你应该关注你的用户如何在QML中使用你的模型。为此,应该首先使用ListModel创建一个原型来看下在QML如它如何最好地运作。当定义QML API的时候,这条也适用。让C++ 端数据到QML可用,这不仅是技术范畴,也是编程范式的变化:从实现式到声明式编程的变化。所以要为挫折与心惊喜做好准备:-)

推荐阅读