# C++模型

集成C++和QML的最常见方法之一是通过 模型(models)模型(model)视图(view) 提供数据,例如ListViewsGridViewPathViews和其他采用模型并为模型中的每个条目创建代理实例的视图。该视图足够智能,可以仅在这些实例可见或者在缓存范围中时创建它们。这使得拥有数以万计条目的大型模型成为可能,但它仍然能有一个非常流畅的用户界面。代理就类似于一个模板,用于呈现模型条目的数据。总而言之:视图使用代理作为模板呈现模型中的条目,模型是视图中数据的提供者。

当不想使用C++时,也可以在纯QML中定义模型。有几种方法可以为视图提供模型。对于来自C++的数据或大量数据的处理,C++模型比这种纯QML的方法更适合。但通常只需要几个条目,那么这些QML模型则非常适合。

ListView {
    // 使用一个整形作为模型
    model: 5
    delegate: Text { text: 'index: ' + index }
}

ListView {
    // 使用一个JS数组作为模型
    model: ['A', 'B', 'C', 'D', 'E']
    delegate: Text { 'Char['+ index +']: ' + modelData }
}

ListView {
    // 使用一个动态QML列表模型作为模型
    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的列表的变体QAbstractTableModel,或者对于TableView元素使用API的表的变体QAbstractItemModel。 API包含一些必须实现的函数和一些扩展模型功能的可选函数。 可选部分主要处理用于更改、添加或删除数据这样的动态用例。

# 简单模型

一个典型的QML C++模型派生自QAbstractListModel并至少实现了datarowCount函数。 在下面的示例中,将使用QColor类提供的一系列SVG颜色名称,并使用模型显示它们。 数据存储在数据容器QList<QString>中。

DataEntryModel派生自QAbstractListModel,并实现了强制要求必须实现的函数。 这里可以忽略rowCount的父节点,因为父节点仅在树模型中使用。 QModelIndex类提供单元格的行和列信息,用于视图检索数据。 该视图从模型中以行/列和基于角色的方式提取信息。 QAbstractListModelQtCore中定义,但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

在实现方面,最复杂的部分是data函数。 首先需要进行范围检查以确保获得了有效的索引。 然后检查是否支持显示角色(display role)。 模型中的每个项目都可以具有多个显示角色,定义所包含数据的各个方面。 Qt::DisplayRole是视图要求的默认文本角色。 在Qt中,定义了一小部分可以使用的默认角色,但为了清晰起见,模型通常会定义自己的角色。 在该示例中,所有不包含显示角色的调用都将被忽略,并返回默认值QVariant()

#include "dataentrymodel.h"

DataEntryModel::DataEntryModel(QObject *parent)
    : QAbstractListModel(parent)
{
    // 使用颜色名称列表初始化数据(QList<QString>)
    m_data = QColor::colorNames();
}

DataEntryModel::~DataEntryModel()
{
}

int DataEntryModel::rowCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent);
    // 返回数据的数量
    return m_data.count();
}

QVariant DataEntryModel::data(const QModelIndex &index, int role) const
{
    // 索引返回请求的行和列信息
    // 忽略列信息,只使用行信息
    int row = index.row();

    // 行的边界检测
    if(row < 0 || row >= m_data.count()) {
        return QVariant();
    }

    // 一个模型可以为不同角色返回数据
    // 默认的角色是展示角色(display role).
    // 可以在QML中通过"model.display"访问
    switch(role) {
        case Qt::DisplayRole:
            // 返回指定行的颜色名
            // Qt自动将其转换成QVariant类型
            return m_data.value(row);
    }

    // 视图要求其他数据,只需返回一个空的QVariant
    return QVariant();
}

下一步是使用qmlRegisterType向QML注册模型。 这是在main.cpp文件中,在加载QML文件之前完成注册。

#include <QtGui>
#include <QtQml>

#include "dataentrymodel.h"

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

    // 注册DataEntryModel类型
    // 在URL"org.example"、版本1.0下
    // 在名称"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++模型和模型的显示属性展示字符串列表。 它仍然非常简单,但已经可以在QML中使用。 通常,数据是从模型外部提供的,模型将充当视图的接口。

提示

要展示数据是表格而不是列表,请使用QAbstractTableModel。 与实现QAbstractListModel相比,唯一区别是还必须提供columnCount方法。

# 更复杂点的数据

实际上,模型数据通常要比上面展示的复杂得多。 因此需要自定义角色,以便视图可以通过属性查询其他数据。 例如,该模型不仅可以提供十六进制字符串形式的颜色,还可以提供来自HSV颜色模型的色调、饱和度和亮度,如QML中的model.huemodel.saturationmodel.brightness.

#ifndef ROLEENTRYMODEL_H
#define ROLEENTRYMODEL_H

#include <QtCore>
#include <QtGui>

class RoleEntryModel : public QAbstractListModel
{
    Q_OBJECT
public:
    // 定义要使用的角色名称
    enum RoleNames {
        NameRole = Qt::UserRole,
        HueRole = Qt::UserRole+2,
        SaturationRole = Qt::UserRole+3,
        BrightnessRole = Qt::UserRole+4
    };

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

    // QAbstractItemModel接口
public:
    virtual int rowCount(const QModelIndex &parent) const override;
    virtual QVariant data(const QModelIndex &index, int role) const override;
protected:
    // 返回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)
{
    // 设置名称到角色名的哈希容器 (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";

    // 将颜色名称作为QColor附加到数据列表 (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:
        // 以十六进制字符串形式返回颜色名称 (model.name) 
        return color.name();
    case HueRole:
        // 返回颜色的HUE (model.hue)
        return color.hueF();
    case SaturationRole:
        // 返回颜色的饱和度 (model.saturation)
        return color.saturationF();
    case BrightnessRole:
        // 返回颜色的亮度(model.brightness)
        return color.lightnessF();
    }
    return QVariant();
}

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

现在,在类的实现中,仅有两个地方发生变化。 首先,在初始化时。使用QColor数据类型初始化数据列表。 此外,定义角色名称映射,使其可供QML访问。 该映射可以在稍后的::roleNames函数中返回。

第二个变化在::data函数中。 扩展switch语句以使能覆盖其他角色(例如色调、饱和度、亮度)。 无法从颜色返回SVG名称,这是因为颜色可以是任何颜色但是SVG名称却是有限的,所以跳过这个角色。 存储名称需要创建一个结构体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中调用它们。 另一种方法是将它们声明为public的槽。

// 在index位置插入一个颜色 (以0为开始, count-1为结尾)
Q_INVOKABLE void insert(int index, const QString& colorValue);
// 使用追加在最后插入一个颜色
Q_INVOKABLE void append(const QString& colorValue);
// 通过index移除一个颜色
Q_INVOKABLE void remove(int index);
// 清理整改模型 (e.g. reset)
Q_INVOKABLE void clear();

此外,定义了一个count属性来获取模型的尺寸和一个get方法来获取给定索引处的颜色。 当想从QML中迭代模型内容时,这很有用。

// 给定模型的尺寸
Q_PROPERTY(int count READ count NOTIFY countChanged)
// 给定index的颜色
Q_INVOKABLE QColor get(int index);

insert的实现首先检查边界以及给定值是否有效,只有这样才开始插入数据。

void DynamicEntryModel::insert(int index, const QString &colorValue)
{
    if(index < 0 || index > m_data.count()) {
        return;
    }
    QColor color(colorValue);
    if(!color.isValid()) {
        return;
    }
    // 视图协议(begin => 操作 => end)
    emit beginInsertRows(QModelIndex(), index, index);
    m_data.insert(index, color);
    emit endInsertRows();
    // 更新数量属性
    emit countChanged(m_data.count());
}

追加颜色非常简单。可以根据模型的尺寸来重用insert函数。

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

remove与insert类似,但它根据remove的操作协议调用。

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基本类型或者已知类型之一,则需要先使用qmlRegisterTypeqmlRegisterUncreatableType注册该类型。 如果用户不能在QML中实例化自己的对象,则使用qmlRegisterUncreatableType注册。

现在可以在QML中使用模型并在模型中插入、附加、删除条目。 这是一个小示例,它允许用户输入颜色名称或颜色十六进制值,然后将颜色附加到模型并显示在列表视图中。 代理上的红色圆圈表示允许用户从模型中删除此条目。 在条目被删除后,模型会通知列表视图并更新其内容。

image

上面使用的是QML代码,可以在本章的资源中找到完整的源代码。 该示例使用了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不仅是跨越了一个技术边界,它也是从命令式编程到声明式编程的编程范式的变化。 所以要同时为受到挫折和收获惊喜时刻做好准备 😃 。

最后更新: 2/5/2022, 10:06:39 PM