翻译 QMLBook17.Qt and C++

原文链接:http://qmlbook.github.io/ch17-qtcpp/qtcpp.html?highlight=qmetaobject#models-in-c

Qt是一个扩展了QML和Javascript的c++工具包。Qt有许多语言绑定,但由于Qt是用c++开发的,所以c++的精神可以在所有类中找到。在本节中,我们将从c++的角度来看Qt,以便更好地理解如何使用c++开发的本地插件扩展QML。通过c++,可以扩展和控制提供给QML的执行环境。

这一章将像Qt一样,要求读者有一些c++的基本知识。Qt不依赖于高级的c++特性,我通常认为Qt的c++风格是非常可读的,所以如果你觉得你的c++知识不可靠,不要担心。

从c++方向研究Qt,你会发现Qt丰富了带有许多现代语言特性的C++,这些特性是通过Introspection data可用来实现的。这是通过使用QObject基类实现的。Introspection data或metadata是在运行时维护类的信息,而普通c++做不到这一点。这使得动态探测对象的信息成为可能,比如对象的属性和可用方法。

Qt使用这个meta信息来启用一个非常宽松绑定的回调概念,通过使用信号和槽。每个信号可以连接到任意数量的槽或甚至其他信号。当从一个实例化对象发出信号时,将调用连接的槽。由于发出信号的对象不需要知道关于拥有槽的对象的任何信息,反之亦然,这种机制用于创建具有很少组件间依赖关系的可重用组件。

Introspection特性还用于创建动态语言绑定,使其能够向QML公开c++对象实例,并使c++函数可以从Javascript中调用。Qt c++还存在其他绑定,除了标准的Javascript绑定,还有一种流行的绑定是Python绑定,称为PyQt。

除了这个核心概念之外,Qt还使使用c++开发跨平台应用程序成为可能。Qt c++提供了不同操作系统上的平台抽象,这允许开发人员专注于手头的任务,而不是如何在不同操作系统上打开文件的细节。这意味着你可以为Windows、OS X和Linux重新编译相同的源代码,而Qt负责处理某些事情的不同OS方式。最终的结果是原生构建的应用程序具有目标平台的外观和感觉。由于移动设备是新的桌面,更新的Qt版本也可以针对使用相同源代码的多个移动平台,如iOS, Android, Jolla,黑莓,Ubuntu Phone, Tizen。

当涉及到重用时,不仅源代码可以重用,开发人员的技能也可以重用。一个了解Qt的团队可以接触到更多的平台,而不仅仅是专注于单一平台特定技术的团队。由于Qt非常灵活,团队可以使用相同的技术创建不同的系统组件。

对于所有的平台,Qt提供了一组基本类型,例如支持完全Unicode的字符串,列表,向量,缓冲区。它还提供了对目标平台主循环的公共抽象,以及跨平台线程和网络支持。一般的理念是,对于应用程序开发者来说,Qt包含了所有必需的功能。对于特定于领域的任务,比如与本地库接口,Qt提供了几个帮助类来简化这一过程。

17.5. Models in C++

/*
 * 三种纯QML类型的Model
 */
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 }
}

17.5.1. A simple model

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

我们的DataEntryModel是从QAbstractListModel派生出来的,并实现了强制函数。我们可以忽略rowCount中的父元素,因为这只在树模型中使用。QModelIndex类提供了单元格的行和列信息,视图希望从中检索数据。视图以行/列和基于角色的方式从模型中提取信息。QAbstractListModel在QtCore中定义,而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

在实现方面,最复杂的部分是data函数。我们首先要做一个范围检查。然后我们检查显示角色。Qt::DisplayRole是视图请求的默认文本角色。可以使用Qt中定义的一小组默认角色,但为了清晰起见,模型通常会定义自己的角色。此时将忽略所有不包含display角色的调用,并返回默认值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注册模型。这是在main.cpp中加载QML文件之前完成的。

#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语句import org访问DataEntryModel。示例1.0,并像其他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中已经可用了。通常数据是从模型外部提供的,模型将充当视图的接口。

17.5.2. More Complex Data

实际上,模型数据通常要复杂得多。因此,需要定义自定义角色,以便视图可以通过属性查询其他数据。例如,模型不仅可以提供十六进制字符串的颜色,还可以提供来自HSV颜色模型的hue(色相)、saturation(饱和度)和brightness(亮度)作为“model.hue”、“model.saturation”和“model.brightness”在QML中。

#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”)时,listview将查找“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数字类型,以便能够使用定点表示法格式化数字。代码在没有Number调用的情况下也能工作(例如plain model. saturator . tofixed(2))。选择哪种格式取决于您对传入数据的信任程度。

17.5.3. 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);

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

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());
}

追加非常简单。我们根据模型的大小重用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());
}

辅助函数计数很简单。它只返回数据计数。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中使用模型,并从模型中插入、追加和删除条目。下面是一个小示例,它允许用户输入颜色名称或颜色十六进制值,然后颜色被添加到模型中并显示在列表视图中。委托上的红色圆圈允许用户从模型中删除该条目。条目被删除后,列表视图会被模型通知并更新其内容。

这是QML代码。您还可以在本章的资产中找到完整的源代码。示例使用QtQuick。控制和QtQuick。布局模块,使代码更加紧凑。这些控件模块在QtQuick中提供了一组与桌面相关的UI元素,而layouts模块提供了一些非常有用的布局管理器。

import QtQuick 2.5
import QtQuick.Window 2.2
import QtQuick.Controls 1.5
import QtQuick.Layouts 1.2

// 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 api时也是如此。将数据从c++提供给QML不仅是一种技术边界,也是一种从命令式编程到声明式编程的编程范式变化。所以,准备好迎接一些挫折和顿悟时刻吧!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值