原文链接: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不仅是一种技术边界,也是一种从命令式编程到声明式编程的编程范式变化。所以,准备好迎接一些挫折和顿悟时刻吧!