Qt and C++

Qt是QML与JavaScript的C++扩展工具包。有许多语言与Qt绑定,但是由于Qt是由C++开发的,C++的精神贯穿了整个Qt。在这一节中,我们着重从C++的角度介绍Qt,使用C++开发的本地插件来了解如何更好的扩展QML。通过C++,可以扩展和控制提供给QML的执行环境。

这章将讲解Qt,正如Qt所要求的一样,需要读者有一定的C++基础知识。Qt不依赖于先进的C++特性,我认为Qt风格的C++代码可读性非常高,所以不要担心你的C++方面比较差。

从C++的角度分析Qt,你会发现Qt通过内省数据的机制实现了许多现代语言的特性。这是通过使用基础类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的开发者提供所有必须的功能。对于特定领域的任务,例如本地库接口,Qt也提供了一些帮助类来使得这些操作更加简单。

演示程序(A Boilerplate Application)

理解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读取项目文件(project file)并生成一个Makefile供make使用。项目文件(project file)独立于平台,qmake会根据特定平台的设置应用一些规则来生成Makefile。在有特殊需求的项目中,项目文件也可以包含特定平台规则的平台作用域。下面是一个简单的项目文件(project file)例子。

# 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项目会使用特定的项目文件(project file),qmake会根据这些项目文件和指定平台生成Makefile。

上面简单的例子只是在应用程序中写入文本。一个命令行工具这是不够的。对于一个用户界面我们需要一个事件循环来等待用户的输入并安排刷新绘制操作。下面这个相同的例子使用一个桌面按钮来触发写入。

令人惊奇的是我们的main.cpp依然很小。我们将代码移入到我们的类中,并使用信号槽(signal/slots)来连接用用户的输入,例如按钮点击。信号槽(signal/slot)机制通常需要一个对象,你很快就会看到。

#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++的桌面窗口用户界面库。我们需要创建一个主窗口来放置一个触发功能的按钮,同事由主窗口来实现我们的核心功能,正如我们在上面例子上看到的。

主窗口本身也是一个窗口,它是一个没有父对象的窗口。这与Qt如何定义用户界面为一个界面元素树类似。在这个例子中,主窗口是我们的根元素,按钮是主窗口上的一个子元素。

#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;
}

在主窗口中我们首先创建了一个按钮,并将clicked()信号与storeContent()槽连接起来。每点击信号发送时都会触发调用槽函数storeContent()。就是这么简单,通过信号与槽的机制实现了松耦合的对象通信。

QObject对象(The QObject)

正如介绍中描述的,QObject是Qt的内省机制。在Qt中它几乎是所有类的基类。值类型除外,例如QColorQStringQList

Qt对象是一个标准的C++对象,但是它具有更多的功能。可以从两个方向来深入探讨:内省和内存管理。内省意味着Qt对象知道它的类名,它与其它类的关系,以及它的方法和属性。内存管理意味着每个Qt对象都可以成为是其它子对象的父对象。父对象拥有子对象,当父对象销毁时,它也会负责销毁它的子对象。

理解QObject的能力如何影响一个类最好的方法是使用Qt的类来替换一个典型的C++类。如下所示的代表一个普通的类。

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)
{
}

获取函数在命名在属性后并且是一个简单的const函数。使用设置属性函数当属性被改变时会发送改变信号。为此我们插入一个保护用来比较当前值与新值。只有在值不同时我们指定给成员变量的值才会生效,并且发送改变信号。

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文档获取更多信息。

编译系统(Build Systems)

在不同的平台上稳定的编译软件是一个复杂的任务。你将会遇到不同环境下的不同编译器,路径和库变量的问题。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文件夹,里面有一个myproject.pro文件。如下输入命令:

$ mkdir build
$ cd build
$ qmake ../myproject/myproject.pro

我们创建一个编译文件夹并且在这个编译文件中使用qmake指向我们项目文件夹中的项目文件。这将配置makefile使用编译文件夹替代我们的源代码文件夹来存放所有的编译中间件和结果。这允许我们同时为不同的qt版本和编译配置创建不同的编译文件夹并且不会弄乱我们的源代码文件夹。

当你使用Qt Creator时,它会在后代为你做这些事情,通常你不在需要担心这些步骤。对于比较大的项目,建议使用命令行方式来编译你的Qt项目可以更加深入的了解编译流。

QMake

QMake是用来读取项目文件并生成编译文件的工具。项目文件记录了你的项目配置,扩展依赖库和源代码文件。最简单包含一个源代码文件的项目可能像这样:

// myproject.pro

SOURCES += main.cpp

我们编译了一个基于项目文件名称myproject的可执行程序。这个编译将只包含main.cpp源文件。默认情况下我们会为项目添加QtCore和QtGui模块。如果我们的项目是一个QML应用程序,我们需要添加QtQuick和QtQml到这个链表中:

// myproject.pro

QT += qml quick

SOURCES += main.cpp

现在编译文件知道与Qt的QtQml和QtQuick模块链接。QMake使用=+=` and ``-=来指定,添加和移除选项链表中的元素。例如一个只有控制台的编译将不会依赖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模板。假设我们有一个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

在项目文件中声明包含两个子项目mylibmyappmyapp依赖于mylib。当你使用qmake为这个项目文件生成编译文件时,将会在每个项目文件对应的文件夹下生成一个编译文件。当你使用my.pro文件的makefile编译时,所有的子项目也会编译。

有时你需要基于你的配置在不同的平台上做不同的事情。qmake推荐使用域的概念来处理它。当一个配置选项设置为true时使一个域生效。

例如使用unix指定utils的实现可以像这样:

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

这表示如果CONFIG变量包含了unix配置将使用对应域下的文件路径,否则使用其它的文件路径。一个典型的例子,移除mac下的应用绑定:

macx {
    CONFIG -= app_bundle
}

这将在mac下创建的应用程序不再是一个.app文件夹而是创建一个应用程序替换它。

基于QMake的项目通常在你开始编写Qt应用程序最好的选择。但是也有一些其它的选择。它们各有优势。我们将在下一小节中简短的讨论其它的选择。

引用

QMake Manual - qmake手册目录。

QMake Language - 赋值,域和相关语法

QMake Variables - TEMPLATE,CONFIG,QT等变量解释

CMake

CMake是由Kitware创造的工具。由于它们的3D可视化软件VTK使得Kitware家喻户晓,当然这也有CMake这个跨平台makefile生成器的功劳。它使用一系列的CMakeLists.txt文件来生成平台指定的makefile。CMake被KDE项目所使用,它与Qt社区有一种特殊的关系。


```C++
// 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 - CMake在线帮助文档

Running CMake

KDE CMake Tutorial

CMake Book

CMake and Qt

Qt通用类(Common Qt Classes)

QObject组成了Qt的基础,但是在这个框架里还有很多的类。在我们继续探寻如何扩展QML之前,我们需要先了解一些有用的Qt基础类。

在这一节中的示例代码需要使用Qt Test库。它提供一种非常好的方法来测试Qt的API并将其存储供以后参考使用。测试库提供的QVERIFYQCOMPARE函数断言一个正确条件。我们也将使用域来避免名称校验冲突。所以不要对后面的代码有困惑。

QString

通常在Qt中文本操作是基于unicode完成的。你需要使用QString类来完成这个事情。它包含了很多好用的功能函数,这与其它流行的框架类似。对于8位的数据你通常需要使用QByteArray类,对于ASCII校验最好使用QLatin1String来暂存。对于一个字符串链你可以使用QList<QString>或者QStringList类(派生自QList<QString>)。

这里有一些例子介绍了如何使用QString类。QString可以在栈上创建,但是它的数据存储在堆上。分配一个字符串数据到另一个上,不会产生拷贝操作,只是创建了数据的引用。这个操作非常廉价让开发者更专注于代码而不是内存操作。QString使用引用计数的方式来确定何时可以安全的删除数据。这个功能叫做隐式共享,在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类同样有很多方便的函数可以使用。阅读Qt帮助文档中QString部分,它有一些很好的示例。

顺序容器(Sequential Containers)

链表,队列,数组都是顺序容器。最常用的顺序容器是QList类。它是一个模板类,需要一个类型才能被初始化。它也是隐式共享的,数据存放在堆中。所有的容器类应该被创建在栈上。正常情况下你不需要使用new QList<T>()这样的语句,千万不要使用new来初始化一个容器。

QList与类QString一样强大,提供了方便的接口来查询数据。下面一个简单的示例展示了如何使用和遍历链表,这里面也使用到了一些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);
}

组合容器(Associative Containers)

映射,字典或者集合是组合容器的例子。它们使用一个键来保存一个值。它们可以快速的查询它们的元素。我们将展示使用最多的组合容器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(File 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);
    }
}

C++数据模型(Models in C++)

在QML中的数据模型为链表视图,路径视图和其它需要为模型中的每个子项创建一个代理引用的视图提供数据。视图只创建可是区域内或者缓冲范围内的引用。这使得即使包含成千上万的子项模型仍然可以保持流畅的用户界面。代理扮演了用来渲染模型子项数据的模板。总之:视图使用代理作为模板来渲染模型中的子项。模型为视图提供数据。

当你不想使用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++的模型需要遵循一个特定的协议。这个协议与动态行为一起被定义在一个API(QAbstractItemModel)中。这个API时为桌面窗口开发的,它可以灵活的作为一个树的基础或者多列表格或者链表。在QML中我们通常值使用API的链表版本(QAbstractListModel)。API包含了一些需要强制实现的函数,另外一些函数时可选的。可选部分通常是用来动态添加或者删除数据。

一个简单的模型(A simple model)

一个典型的QML C++模型继承自QAbstractListModel
,并且最少需要实现datarowCount函数。在这个例子中我们将使用由QColor类提供的一系列SVG颜色名称并且使用我们的模型展示它们。数据被存储在QList<QString>数据容器中。

我们的DataEntryModel基础自QAbstractListModel并且实现了需要强制实现的函数。我们可以在rowCount中忽略父对象索引,这只在树模型中使用。QModelIndex类提供了视图检索数据需要的单元格行和列的信息,视图基于行列和数据角色从模型中拉取数据。QAbstractListModelQtCore中定义,但是QColor被定义在QtGui中。我们需要附加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

现在你可以使用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++模型来显示字符串列表,并且显示模型的属性。它仍然非常简单,但是已经可以在QML中使用。通常数据由外部的模型提供,这里的模型只是扮演了视图的一个接口。

更复杂的数据(More Complex Data)

实际工作中使用的模型数据通常比较复杂。所以需要自定义一些角色枚举方便视图通过属性查找数据。例如模型提供颜色数据不仅只是16进制字符串,在QML中也可以是来自HSV颜色模型的色调,饱和度和亮度,以“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数据类型初始化数据链表。此外我们还定义了我们自己的角色名称映射实现QML的访问。这个映射将在后面的::roleNames函数中返回。

第二个变化是在::data函数中。我们确保能够覆盖到其它的角色枚举(例如色调,饱和度,亮度)。没有可以从颜色中获取SVG名称的方法,由于一个颜色可以替代任何颜色,但SVG名称是受限的。所以我们忽略掉这点。我们需要创建一个结构体{ 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数字类型,这样可以使用定点标记来格式化数字。代码中应当避免直接调用数字(例如model.saturation.toFixed(2))。选择哪种格式取决于你的输入数据。

动态数据(Dynamic Data)

动态数据包含了从模型中插入,移除,清除数据等。QAbstractListModel期望当条目被移除或者插入时有一个明确的行为。这个行为使用一个信号来表示,在操作调用前和调用后调用这个行为。例如向一个模型插入一行数据,你首先需要发送beginInsertRows信号,然后操作数据,最后发送endInsertRows信号。

我们将在头文件中加入后续的函数。这些使用Q_INVOKABLE函数定义使得可以在QML中调用它们。另一种方法是将它们定义为公共槽函数。

// 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方法来获取颜色。这些东西在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());
}

添加数据非常简单。我们使用模型大小并调用插入函数来实现。

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所知类型,你需要使用qmlRegisterType或者qmlRegisterUncreatableType注册类型。如果是用户不能在QML中实例化对象的类型应该使用qmlRegisterUncreatableType注册。

现在你可以在QML中使用模型并且可以从模型中插入,添加,移除条目。这里有一个小例子,它允许用户输入一个颜色名称或者颜色16进制值,并将这个颜色加入到模型中在链表视图中显示。代理上的红色圆圈允许用户从模型中移除这个条目。在条目被移除后,模型通知链表视图更新它的内容。

这里是QML代码。你可以在这章的资源里找到完整的源代码。这个例子使用了QtQuick.Controls和QtQuick.Layout模块使得代码更加紧凑。控制模块提供了QtQuick中一组与桌面相关的用户界面元素,布局模块提供了非常有用的布局管理器。

import QtQuick 2.2
import QtQuick.Window 2.0
import QtQuick.Controls 1.2
import QtQuick.Layouts 1.1

// 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: ' + count);
            print('last entry: ' + get(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 {
                    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 = index
                        view.focus = true
                    }
                    onRemove: {
                        // remove the current entry from the model
                        dynamic.remove(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: {
                // 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编程接口同样有效。使数据从C++到QML中可用不仅仅是技术边界,也是从命令式编程到声明式编程的编程方法转变。所以准备好经历一些挫折并从中获取快乐吧。

进阶技巧(Advanced Techniques)


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 2291184112@qq.com

×

喜欢就点赞,疼爱就打赏