现在的位置: 首页 > 综合 > 正文

C++ GUI Programming with Qt 4 – 10.3 实现自定义模型

2013年11月02日 ⁄ 综合 ⁄ 共 13482字 ⁄ 字号 评论关闭
 

实现自定义模型

Qt的预定义模型为处理和浏览数据提供了便利。 然而一些数据源不能通过预定义的模型而被高效利用,所以对于这样的情况有必要创建针对底层数据源而优化的自定义模型。

在我们着手创建自定义模型前,让我们先回顾下Qt的 模型/视图 架构中使用的关键概念。 模型中的每个数据元素都有一个模型索引和一组称为角色(roles)的能够携带任何的值(arbitrary values)的属性。 在我们以前的章节中最经常使用的角色(roles)是Qt::DisplayRoleQt::EditRole。 其他的一些角色被用于附加数据(例如,Qt::ToolTipRoleQt::StatusTipRole,和Qt::WhatsThisRole),或用来控制基本的显示属性(如Qt::FontRoleQt:: TextAlignmentRoleQt::TextColorRole,和Qt::BackgroundColorRole)。

图 10.9. Qt模型的大纲

[View full size image]

schematic_view

对于一个列表模型(list model),唯一有意义的索引部分就是行号,通过QModelIndex::row()取得。 对于表格模型(table model),有意义的索引部分是行号和列号,通过QModelIndex::row()QModelIndex::column()取得。 对于列表(list)和表格(table)模型,每个条目(item)的父对象是根(root),这个根(root)用一个无效的QModelIndex表示。 本节开始的两个例子演示了如何实现自定义表格模型。

树模型类似与表格模型,但是有以下区别。 像表格模型一样,顶级条目的父对象是根(一个无效的QModelIndex),但是每一个其他条目的父对象会是某个其他的条目,在树的继承关系中。 可以通过QModelIndex::parent()来取得父对象。 每个条目都有自己的角色数据,和0个或多个孩子对象,each an item in its own right. 既然条目能够拥有它的子条目,那么它可能要用递归(类树)数据结构来实现,本节最后的例子将会展示它。

本节的第一个例子是一个只读的表格模型,展示的是货币价值之间的关系。

图 10.10. 货币程序

[View full size image]

currencies

这个应用程序可以用一个简单的表格组件来实现,但是我们想要用自定义的模型来利用数据的某些特性以实现存储的最小化。 如果我们要存储162种当前流通的货币在一个表格中,就需要存储162*162=26244个值。而接下来展示的自定义模型,只需要存储162个值(和U.S dollar有关系的货币值)。

CurrencyModel类将会同标准的QTableView一起被使用。 用一个QMap<QString,double>对象来填入Currency-Model对象。每个键(key)是一种货币代码,每个值(value)是兑换成U.S. dollars的价值。这块儿的代码片度展示了如何填入map和如何使用模型。

QMap<QString, double> currencyMap;
currencyMap.insert("AUD", 1.3259);
currencyMap.insert("CHF", 1.2970);
...
currencyMap.insert("SGD", 1.6901);
currencyMap.insert("USD", 1.0000);
CurrencyModel currencyModel;
currencyModel.setCurrencyMap(currencyMap);
QTableView tableView;
tableView.setModel(&currencyModel);
tableView.setAlternatingRowColors(true);

现在我们看一下这个模型的实现,从头部开始:

class CurrencyModel : public QAbstractTableModel
{
public:
    CurrencyModel(QObject *parent = 0);
    void setCurrencyMap(const QMap<QString, double> &map);
    int rowCount(const QModelIndex &parent) const;
    int columnCount(const QModelIndex &parent) const;
    QVariant data(const QModelIndex &index, int role) const;
    QVariant headerData(int section, Qt::Orientation orientation,
                        int role) const;
private:
    QString currencyAt(int offset) const;
    QMap<QString, double> currencyMap;
};

我们选择通过继承QAbstractTableModel来实现我们的模型类,因为它非常适合我们的数据源。 Qt 提供集中模型基类,包括QAbstractListModelQAbstractTableModel,和QAbstractItemModelQAbstractItemModel类被用来支持各种各样的模型,包括那些基于递归数据结构的模型,而QAbstractListModelQAbstractTableModel类是用来方便地使用一维或二维数据集合。

图 10.11. 抽象模型类的继承树

inheritance_tree

对于一个只读的表格模型,我们必须重新实现三个函数: rowCount()columnCount(),和 data()。 在这种情况下,我们还重新实现了header-Data(),并且提供了一个函数来初始化数据(setCurrencyMap())。

CurrencyModel::CurrencyModel(QObject *parent)
    : QAbstractTableModel(parent)
{
}

我们不需要在构造函数中做任何事,除了为基类传递parent参数。

int CurrencyModel::rowCount(const QModelIndex & /* parent */) const
{
    return currencyMap.count();
}
int CurrencyModel::columnCount(const QModelIndex & /* parent */) const
{
    return currencyMap.count();
}

对于这个表格模型,行 和列的数目是货币map中货币的种数。 parent参数是没有意义的,对于一个表格模型来说;它的存在是因为rowCount()columnCount()从更一般的支持继承的基类QAbstractItemModel继承而来。

QVariant CurrencyModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid())
        return QVariant();
    if (role == Qt::TextAlignmentRole) {
        return int(Qt::AlignRight | Qt::AlignVCenter);
    } else if (role == Qt::DisplayRole) {
        QString rowCurrency = currencyAt(index.row());
        QString columnCurrency = currencyAt(index.column());
        if (currencyMap.value(rowCurrency) == 0.0)
            return "####";
        double amount = currencyMap.value(columnCurrency)
                        / currencyMap.value(rowCurrency);
        return QString("%1").arg(amount, 0, 'f', 4);
    }
    return QVariant();
}

data()函数返回一个条目中的任意角色的值。 条目被指定为QModelIndex类型。 对于一个表格模型,QModelIndex令人感兴趣的部分是它的行和列的数目,可以通过调用row()column()来获得。

If the role is Qt::TextAlignmentRole, we return an alignment suitable for numbers. 如果显示角色是Qt::DisplayRole,我们就在所有的货币中查找这个值然后计算兑换率。

我们可以是用double类型返回计算的值,但是这样的话我们就没法控制该显示几位小数(除非我们使用自定义的delegate)。 所以,我们返回字符串类型的值,把这个字符串值格式化成我们需要的。

QVariant CurrencyModel::headerData(int section,
                                   Qt::Orientation /* orientation */,
                                   int role) const
{
    if (role != Qt::DisplayRole)
        return QVariant();
    return currencyAt(section);
}

headerData()被视图调用来填写水平和垂直的表头(header)。 section参数为行或者列号(取决与方位参数)。 既然行和列有着相同的货币代码,所以我们不需要关心方位而是简单的返回给定的section号所对应的货币代码。

void CurrencyModel::setCurrencyMap(const QMap<QString, double> &map)
{
    currencyMap = map;
    reset();
}

调用者可以通过调用setCurrencyMap()来更换货币map。 调用QAbstractItemModel::reset()通知任何正在使用该模型的视图,它们的数据是无效的;这强迫这些视图请求新的数据为可见的条目。

QString CurrencyModel::currencyAt(int offset) const
{
    return (currencyMap.begin() + offset).key();
}

currencyAt()返回货币map中给定偏移出的键(货币的代码)。 我们使用STL风格的迭代器来查找条目,并调用key()

正如我们刚才所看到的,创建一个只读的模型并不困难,并且依靠原生的底层数据,使用一个好的设计从而拥有了潜在的内存节省和速度上的提升。 下一个例子,Cities程序,也是基于表格的,不过所有的数据是由用户输入的。

这个程序被用来存储任意两个城市之间的距离的。 像前一个例子,我们可以简单地使用一个QTableWidget并存储一个条目为每对城市。 然而,一个自定义模型将会是更高效的,因为从任意的城市A到任意的其他城市B和从B到A是一样的,所以表格的条目是关于主对角线对称的。

为了看看一个自定义模型如何与一个简单的表格进行对比,让我们假设我有三个城市,A, B, 和 C。 如果我们为每个组合存储一个值的话,那么我们将需要存储九个值。 一个精心设计的模型只需要是三个条目(A, B), (A, C),和(B, C)。

图 10.12. 城市程序

[View full size image]

cities

这里是如何设置和使用模型:

QStringList cities;
cities << "Arvika" << "Boden" << "Eskilstuna" << "Falun"
       << "Filipstad" << "Halmstad" << "Helsingborg" << "Karlstad"
       << "Kiruna" << "Kramfors" << "Motala" << "Sandviken"
       << "Skara" << "Stockholm" << "Sundsvall" << "Trelleborg";
CityModel cityModel;
cityModel.setCities(cities);
QTableView tableView;
tableView.setModel(&cityModel);
tableView.setAlternatingRowColors(true);

我们必须重新实现同样的函数像在前一个例子中做的那样。 另外,我们还必须从新实现setData()flags()来使模型可以被编辑。 这是类的定义:

class CityModel : public QAbstractTableModel
{
    Q_OBJECT
public:
    CityModel(QObject *parent = 0);
    void setCities(const QStringList &cityNames);
    int rowCount(const QModelIndex &parent) const;
    int columnCount(const QModelIndex &parent) const;
    QVariant data(const QModelIndex &index, int role) const;
    bool setData(const QModelIndex &index, const QVariant &value,
                 int role);
    QVariant headerData(int section, Qt::Orientation orientation,
                        int role) const;
    Qt::ItemFlags flags(const QModelIndex &index) const;
private:
    int offsetOf(int row, int column) const;
    QStringList cities;
    QVector<int> distances;
};

对于这个模型,我们使用两种数据结构: citiesQStringList类型变量,用来存储城市的名称;distancesQVector<int>类型变量,用来存储独一无二的一对城市之间的距离。

CityModel::CityModel(QObject *parent)
    : QAbstractTableModel(parent)
{
}

构造函数什么也不做,除了传递parent参数给基类。

int CityModel::rowCount(const QModelIndex & /* parent */) const
{
    return cities.count();
}
int CityModel::columnCount(const QModelIndex & /* parent */) const
{
    return cities.count();
}

因为我们使用正方形网格来表示这些城市间的关系,所以行数和列数就是列表中的城市数。

QVariant CityModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid())
        return QVariant();
    if (role == Qt::TextAlignmentRole) {
        return int(Qt::AlignRight | Qt::AlignVCenter);
    } else if (role == Qt::DisplayRole) {
        if (index.row() == index.column())
            return 0;
        int offset = offsetOf(index.row(), index.column());
        return distances[offset];
    }
    return QVariant();
}

data()函数和我们在CurrencyModel中所做的相似。 如果行和列相同它返回0,因为那意味着两个城市是同一个城市;否则,它会用给定的行和列找出distances向量的入口并返回指定的城市对之间的距离。

QVariant CityModel::headerData(int section,
                               Qt::Orientation /* orientation */,
                               int role) const
{
    if (role == Qt::DisplayRole)
        return cities[section];
    return QVariant();
}

headerData()比较简单,因为我们的正方形表格的每行和列表头是同一的。 我们简单地返回在cities字符串列表中给定偏移处的城市名称。

bool CityModel::setData(const QModelIndex &index,
                        const QVariant &value, int role)
{
    if (index.isValid() && index.row() != index.column()
            && role == Qt::EditRole) {
        int offset = offsetOf(index.row(), index.column());
        distances[offset] = value.toInt();
        QModelIndex transposedIndex = createIndex(index.column(),
                                                  index.row());
        emit dataChanged(index, index);
        emit dataChanged(transposedIndex, transposedIndex);
        return true;
    }
    return false;
}

setData()被调用,当用户编辑一个条目的时候。 假如模型索引有效,两个城市不同,并且要修改的数据元素是Qt::EditRole,函数将用户输入的值保存在distances向量中。

createIndex()函数用来生成一个模型索引。 我们需要用它来获取主对角线另外一边的与当前正在被设置的条目相对应的条目的模型索引,因为这两个条目必须显示同样的数据。 createIndex()函数先接收行再接收列;这里我们反转了参数,为了获取与给定的index相对应的对角线相反一侧的模型索引。

我们发射dataChanged()信号携带着被修改的条目的模型索引。 这个信号携带两个模型索引的原因是一个改变有可能影响的是包含多行或多列的矩形区域,所以被传递的索引是那些被改变的条目中的左上条目的索引和右下条目的索引。 我们还为transposed索引发射dataChanged()信号,以确保视图会刷新该条目。 最后,我们返回true或者false来指出编辑是否成功。

Qt::ItemFlags CityModel::flags(const QModelIndex &index) const
{
    Qt::ItemFlags flags = QAbstractItemModel::flags(index);
    if (index.row() != index.column())
        flags |= Qt::ItemIsEditable;
    return flags;
}

flags()函数被模型使用来告知一个条目可以做什么(例如,是否可编辑)。 QAbstractTableModel的缺省实现是返回Qt::ItemIsSelectable | Qt::ItemIsEnabled。 我们为所有的条目增加了Qt::ItemIsEditable标志,除了对角线处的条目(它们总是0)。

void CityModel::setCities(const QStringList &cityNames)
{
    cities = cityNames;
    distances.resize(cities.count() * (cities.count() - 1) / 2);
    distances.fill(0);
    reset();
}

如果给出一个新的列表,我们设置私有的QStringList为这个新的列表,重新分配大小并清空距离向量,然后调用QAbstractItemModel::reset()通知那些可见条目必须重新获取的视图。

int CityModel::offsetOf(int row, int column) const
{
    if (row < column)
        qSwap(row, column);
    return (row * (row - 1) / 2) + column;
}

offsetOf()计算一个给定的城市对在distances向量中的索引。 例如,如果我们有城市A, B, C, 和 D,并且用户更新了row 3, column 1,B 到 D,偏移将会是3 x (3 - 1)/2 + 1 = 4。如果用户反而更新的是row 1, column 3,D到B,多亏qSwap(),同样的计算将会被正确地执行并返回同一个偏移值。

图 10.13. citiesdistances的数据结构和表模型。

citiesanddistance

本节最后的一个例子是一个展示针对正则表达式的解析树的模型。 一个正则表达式有一个或多个约束组成,通过‘|’字符来分割。 这样,正则表达式"alpha|bravo|charlie"包含三个约束。 每个约束是一个或多个因子的序列;例如,约束"bravo"由五个因子组成(每个字母是一个因子)。 因子能被进一步分解为一个原子和一个可选的量词,如'*', '+', 和 '?'。 既然正则表达式有括号子表达式,那么它们可以有递归解析树。

图 10.14所显示的正则表达式, "ab|(cd)?e",匹配一个'a'后接一个'b',或者是一个'c'后接一个‘b'再接一个'e',或单独一个'e'。 所以它将会匹配"ab"和"cde",不会是"bc"或"cd"。

图 10.14. Regexp解析器程序

regexpparser

Regexp Parser程序由四个类组成:

  • RegExpWindow这个窗口让用户输入一个正则表达式并显示出其对应的解析树。

  • RegExpParser从一个正则表达式生成一棵解析树。

  • RegExpModel是一个封装一棵解析树的树模型。

  • Node表示解析树的一个节点。

让我们从Node类开始:

class Node
{
public:
    enum Type { RegExp, Expression, Term, Factor, Atom, Terminal };
    Node(Type type, const QString &str = "");
    ~Node();
    Type type;
    QString str;
    Node *parent;
    QList<Node *> children;
};

每个节点拥有一个类型(type),一个字符串(可能是空),一个父节点(可能是0),和一列子节点(可能是空)。

Node::Node(Type type, const QString &str)
{
    this->type = type;
    this->str = str;
    parent = 0;
}

构造函数简单地初始化节点的类型和字符串。 因为所有的数据都是public,所以用到Node的代码就能够直接操作它的类型,字符串,父节点,和子节点。

Node::~Node()
{
    qDeleteAll(children);
}

qDeleteAll()函数遍历指针容器对象并在每个指针上调用delete。 它不会将指针设置为0,所以如果它被用在析构函数之外,那么通常接着调用clear()为这个指针容器。

现在我们已经定义了我们的数据条目(每个由Node表示),我们准备创建一个模型:

class RegExpModel : public QAbstractItemModel
{
public:
    RegExpModel(QObject *parent = 0);
    ~RegExpModel();
    void setRootNode(Node *node);
    QModelIndex index(int row, int column,
                      const QModelIndex &parent) const;
    QModelIndex parent(const QModelIndex &child) const;
    int rowCount(const QModelIndex &parent) const;
    int columnCount(const QModelIndex &parent) const;
    QVariant data(const QModelIndex &index, int role) const;
    QVariant headerData(int section, Qt::Orientation orientation,
                        int role) const;
private:
    Node *nodeFromIndex(const QModelIndex &index) const;
    Node *rootNode;
};

这次我们从QAbstractItemModel派生,而不是从它的便捷子类QAbstractTableModel,因为我们希望创建一个层次模型。 我们必须实现的重要的函数还是一样的,除了还必须实现index()parent()。 为了设置模型的数据,我们有一个setRootNode()函数,它必须用解析树的根节点来调用。

RegExpModel::RegExpModel(QObject *parent)
    : QAbstractItemModel(parent)
{
    rootNode = 0;
}

在模型的构造函数中,我们只需要设置根节点为一个安全的空值并传递parent给基类。

RegExpModel::~RegExpModel()
{
    delete rootNode;
}

我们在析构函数中删除根节点。 如果根节点有子节点,那么每个子节点会被删除,并这样递归下去,这是有Node的析构函数完成的。

void RegExpModel::setRootNode(Node *node)
{
    delete rootNode;
    rootNode = node;
    reset();
}

当要设置一个新的根节点,我们开始删除前一个根节点(包括所有的子节点)。 然后我们设置新的根节点并调用reset()来通知视图为可见的条目重新请求数据。

QModelIndex RegExpModel::index(int row, int column,
                               const QModelIndex &parent) const
{
    if (!rootNode)
        return QModelIndex();
    Node *parentNode = nodeFromIndex(parent);
    return createIndex(row, column, parentNode->children[row]);
}

index()函数是来自QAbstractItemModel的重新实现。 它会被调用每当模型或视图需要创建一个QModelIndex为某个子条目(或者一个顶级条目如果parent是一个无效的QModelIndex)。 对于表格和列表模型,我们不需要重新实现这个函数,因为QAbstractList-ModelQAbstractTableModel的缺省实现通常是足够的。

在我们的index()实现中,如果没有设置解析树,我们就返回一个无效的QModelIndex。 否则,我们就用给定的行和列还有被请求的节点的Node *来创建一个QModelIndex。 对于层次模型,知道一个条目相对于它的父节点的行和列还不足以唯一地识别它;我们还必须知道父节点是谁: 为了解决这个问题,我们可以存储一个指向内置节点的指针在QModelIndex中。 QModelIndex gives us the option of storing a void * or an int in addition to the row and column numbers.

子节点的Node *被获取是通过父节点的children列表。 父节点的获取是通过对parent模型索引使用私有函数nodeFromIndex()

Node *RegExpModel::nodeFromIndex(const QModelIndex &index) const
{
    if (index.isValid()) {
        return static_cast<Node *>(index.internalPointer());
    } else {
        return rootNode;
    }
}

nodeFromIndex()将给定索引的void *转换为Node *,或者返回根节点如果索引无效,即使一个无效的模型索引被用来表示根在模型中。

int RegExpModel::rowCount(const QModelIndex &parent) const
{
    Node *parentNode = nodeFromIndex(parent);
    if (!parentNode)
        return 0;
    return parentNode->children.count();
}

一个条目的行数就是它所拥有的子节点数。

int RegExpModel::columnCount(const QModelIndex & /* parent */) const
{
    return 2;
}

列数固定为2。第一列为节点的类型;第二列是节点的值。

QModelIndex RegExpModel::parent(const QModelIndex &child) const
{
    Node *node = nodeFromIndex(child);
    if (!node)
        return QModelIndex();
    Node *parentNode = node->parent;
    if (!parentNode)
        return QModelIndex();
    Node *grandparentNode = parentNode->parent;
    if (!grandparentNode)
        return QModelIndex();
    int row = grandparentNode->children.indexOf(parentNode);
    return createIndex(row, child.column(), parentNode);
}

从子节点来获取父节点的QModelIndex要比查找一个父节点的子节点多一些工作。 我们能够轻易地取得父节点通过nodeFromIndex()并使用Node的父指针向上走,但是为了取得行号(父节点在它的兄弟节点中的位置),我们需要回到祖先节点并查找父节点索引位置在其父节点(就是,子节点的祖先)的子节点列表。

QVariant RegExpModel::data(const QModelIndex &index, int role) const
{
    if (role != Qt::DisplayRole)
        return QVariant();
    Node *node = nodeFromIndex(index);
    if (!node)
        return QVariant();
    if (index.column() == 0) {
        switch (node->type) {
        case Node::RegExp:
            return tr("RegExp");
        case Node::Expression:
            return tr("Expression");
        case Node::Term:
            return tr("Term");
        case Node::Factor:
            return tr("Factor");
        case Node::Atom:
            return tr("Atom");
        case Node::Terminal:
            return tr("Terminal");
        default:
            return tr("Unknown");
        }
    } else if (index.column() == 1) {
        return node->str;
    }
    return QVariant();
}

data()函数中,我们为请求的条目取得Node *并用它来存取底层数据。 如果调用者希望那个得到任何一个角色的值除了Qt:: DisplayRole或者我们无法为给定的模型索引取得Node,我们返回一个无效的QVariant。 如果列为0,我们返回节点的类型名称;如果列为1,我们返回节点的值(它的字符串)。

QVariant RegExpModel::headerData(int section,
                                 Qt::Orientation orientation,
                                 int role) const
{
    if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
        if (section == 0) {
            return tr("Node");
        } else if (section == 1) {
            return tr("Value");
        }
    }
    return QVariant();
}

在我们重新实现的headerData()中,我们返回适当的水平表头标签。 被用来呈现层次模型的QTReeView类,没有垂直表头,所以我们忽略这个可能性。

现在我们已经接触了NodeRegExpModel类,让我们看看如何创建根节点当用户修改行编辑器中的文本时。

void RegExpWindow::regExpChanged(const QString &regExp)
{
    RegExpParser parser;
    Node *rootNode = parser.parse(regExp);
    regExpModel->setRootNode(rootNode);
}

当用户修改了行编辑器中的文本时,主窗口的regExpChanged()槽被调用。 在这个槽中,用户文本会被解析并且解析器会返回一个指向解析树根节点的指针。

我们没有给出RegExpParser类,因为它和GUI或者模型/视图编程无关。 在CD上有这个例子的全部代码。

在这一节,我们看到了如何创建三种不同的自定义模型。 许多模型,条目和模型索引是一对一的,要比这里所展示的模型简单的多。 Qt自身附带的大量文档提供更多模型/视图的例子。

抱歉!评论已关闭.