zoukankan      html  css  js  c++  java
  • C++ GUI Programming with Qt 4 10.3 实现自定义模型

    实现自定义模型

    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[1]

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

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

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

    图 10.10. 货币程序

    [View full size image]

    currencies[1]

    这个应用程序可以用一个简单的表格组件来实现,但是我们想利用数据的某些特性, 通过自定义模型以实现最小化存储。 如果我们要存储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类被用来方便地使用一维或二维数据集合。

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

    inheritance_tree[1]

    对于一个只读的表格模型,我们必须重新实现三个函数: rowCount(), columnCount(), and 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()来获得。

    如果角色是Qt::TextAlignmentRole,我们就返回一个适合数字的对齐方式。 如果显示角色是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参数为行或者列号(取决与方位参数orientation)。 既然行和列有着相同的货币代码,所以我们不需要关心方位而是简单的返回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)。

    Figure 10.12. 城市程序

    [View full size image]

    cities[1]

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

    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[1]

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

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

    图 10.14. Regexp解析器程序

    regexpparser[1]

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

    在我们的index()实现中,如果没有设置解析树,我们就返回一个无效的QModelIndex。 否则,我们就用给定的行和列还有被请求的节点的Node *来创建一个QModelIndex。 对于层次模型,知道一个条目相对于它的父节点的行和列还不足以唯一地识别它;我们还必须知道父节点是谁: 为了解决这个问题,我们可以存储一个指向内置节点的指针在QModelIndex中。 QModelIndex给予我们存储一个void *或者一个int的选择,除了行和列号。

    子节点的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自身附带的大量文档提供更多模型/视图的例子。

  • 相关阅读:
    如何分析redis中的慢查询
    redis订阅关闭异常解决
    异常解决:Caused by: com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure
    linux下postgres的安装
    springboot tomcat配置参数列表
    如何把web.xml中的context-param、Servlet、Listener和Filter定义添加到SpringBoot中
    electron-builder 由于网络原因无法下载问题解决
    Handshake failed due to invalid Upgrade header: null 解决方案
    Linux-006-执行Shell脚本报错 $' ':command not found
    VUE-013-为elementUI 设置 tootip 宽度
  • 原文地址:https://www.cnblogs.com/chenxuelian/p/1629581.html
Copyright © 2011-2022 走看看