10-3 实现自定义模型(Implementing Custom Models)

本文介绍了如何在Qt中创建自定义的数据模型以优化特定数据源的处理方式,包括只读模型、可编辑模型及解析树模型。

Qt预定义的类已经提供了方便处理和显示数据的方式。然而,一些数据源不能直接使用这些定义好的模型,这就需要创建自定义的模型,优化对这些数据的处理。

在创建自定义模型之前,让我们首先回顾一下Qtmodel/view结构的主要概念。在一个模型中每一个数据都有一个模型的索引(a model index)和一组属性,这些属性称为角色(roles),这些属性能够用任何类型的数据保存。在这一章的前几节最常用的角色为:Qt::DisplayRoleQt::EditRole。其它角色有些是用来辅助性的数据,例如Qt::ToolTipRoleQt::StatusTipRoleQt::WhatsThisRole,还有一些是对显示属性的控制,如Qt::FontRoleQt::TextAlignmentRoleQt::TextColorRoleQt::BackgroundColorRole

Figure 10.9. Schematic view of Qt's models

对一个列表模型(list model),用到的索引属性只有行号,QModelIndex::row()能够得到。对一个表格式模型(table model),用到的索引属性为行号和列号,调用函数QModelIndex::row()QModelIndex::column()得到。不论式列表模型还是表格模型,每一个项目(item)的父都是根项目(root),这个根项目用一个非法的QModelIndex得到。这一节的前两个例子说明怎么样创建一个自定义的表格模型。

一个树性模型(tree model)和一个表格模型相似,但也略有不同。和表格模型一样,顶层父项目用一个非法的QModelIndex代替,但是每一个项目还能够有子项目,父项目能够用QModelIndex::parent()得到。每一个项目都有自己的角色数据,没有或者有多个子项目。由于项目能够把其它项目作为子项目,因此能够表示递归模型,最后一个例子说明了整个模型的使用。

本节的第一个例子是一个只读的模型,显示不同的货币对比。

Figure 10.10. The Currencies application

这个程序也可以使用一个简单的表格实现,但是我们想自定义一个模型对数据的存贮进行优化。如果我们想在一个表格中保存162中货币之间的比例关系,我们就需要保存162×16226224个数据。如果使用下面的模型,我们只需要保存162个数据(每一种货币的对美元的比例)。

CurrencyModel用一个标准的QTableView表示。模型中的数据类型为QMap<QString,double>;每一个key是一个货币代码,double型的数据表示为相对美元的比例。下面是一个代码段:

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作为基类,因为QAbstractTableModel和我们的数据源最为匹配。Qt提供了一些基本的模型类,包括QAbstractListModelQAbstractTableModelQAbstractItemModelQAbstractItemModel类用来支持广泛类型的模型,而QAbstractListModelQAbstractTableModel类对一维数据和二维数据提供支持。

Figure 10.11. Inheritance tree for the abstract model classes

对一个只读的表格模型,我我们必须重写三个函数rowCount()columnCount()data()。在这个例子中,我们还实现了headerData(),提供一个函数setCurrencyMap()来初始化数据。

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

在构造函数中,把父参数传递给基类。

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

对于本例中的表格模型,行数和列数为货币映射中的货币个数。父参数对表格模型来说没有意义,传递这个参数是因为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确定。对一个表格模型,需要知道的是行号和列号,通过函数row()column()可以得到。

如果角色是Qt::TextAlignmentRole,返回一个适合显示数字的对齐方式。如果是显示角色Qt::DisplayRole,我们得到每一个货币关联的值,然后计算汇率。

我们把返回数值作为一个double处理,但是我们对数字的显示没有控制。因此,我们返回一个字符串,对它进行格式化处理。

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

函数headData()是视图(view)调用,显示水平方向和垂直方向的标题。参数section表示行号或者列号(根据方向可以确定)。由于在这个例子中行和列都显示货币代码,我们不需要关注方向,只要返回给定行号或者列号的货币代码就可以。

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

调用函数setCurrencyMap()能够改变货币种类。QAbstractItemModel::reset()通知视图,它们正在使用的模型数据是非法的,需要对可视部分数据进行更新。

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

函数currencyAt()返回给定偏移的键值(即货币代码),我们使用一个STL风格的遍历器得到对应的数据。

如上所述,我们发现创建一个只读的模型很简单,只需关注数据源的特点就可以,需要考虑数据在内存中的保存格式和读取的速度。下一个例子也是基于表格的,但是这里所有的数据都是用户输入的。

这个应用程序用来存贮任何两个城市之间的距离数据。和前面的例子一样,我们使用QTableWidget控件,每一个项目中存储每一个城市对的距离数值。然而,一个自定义的模型能够更加有效,因为一个城市A到城市B的距离和城市B到城市A的距离是一样的,因此,数据沿主对角线是镜像的。

为了说明自定义数据模型和普通表格的不同,我们假设有三个城市ABC,如果我们为每一个组合保存一个值,我们需要存储9个值。然而,一个自定义模型只需要三个值(A,B,(A,C),(B,C)

Figure 10.12. The Cities application

现在我们开始实现这个例子:

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

在这个模型中,我们使用两个数据结构:QStringList代表城市的名称,QVector<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,否则,它根据行数列数得到对应的城市,计算出距离。

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

函数headerData()返回行,列标题,在这个例子中表为正方形表,行数列数相等,这里我们返回对应的城市名称。

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属性,函数就保存用户输入的距离值。

函数createIndex()生成指定行,列的模型索引。用这个函数我们得到与对角线对称的另一个项目的模型索引,这两个距离是相等地。我们用当前列数表示行数,用行数表示列数,就得到对应的项目。

如果我们改变了一个项目,就发送dataChanged()信号。由于一个项目改变后可能会引起不止一行一列改变,因此信号函数中有两个索引,由这两个索引表示一个由行列组成的矩形区域,一个代表变化的左上角项目索引,另一个代表变化右下角项目索引。同时对角位置的项目也改变了,也发出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::ItemIsSelectableQt::ItemIsEnabled。在CityModel中增加了Qt::ItemIsEditable。对角线上的值总是0,不能编辑。

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

如果重新设置的城市的列表,我们用新的列表代替原城市列表,重新计算距离数组distances,调用QAbstactItemModel::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,如果用户改变了第3行第一列,在distances数组中的索引为3*(3-1)/2+1=4。如果用户改变了第一行第3列,调用qSwap()后,可以得到同样的偏移值。

Figure 10.13. The cities and distances data structures and the table model

本节的最后一个例子是一个正则表达式的解析树模型。一个正则表达式包涵一个或者多个项(terms),项与项之间用“|”分开。因此表达式“alpha|bravo|Charlie”包含3个项。每一个项有包含多个因数(factors),例如bravo包含5个因数(每一个字母代表一个因数)。因数还可以进一步分解为原子(atom)和一个符号,如‘*’,‘+’,‘?’。一个正则表达式还可能包含表达式,解析可以是递归的

Figure 10.14. The Regexp Parser application

RegExpParsery应用程序包含四个类:

·         RegExpWindow is 一个窗口,用户可以输入表达式,显示表达式的解析结构.

·         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;
};

每一个Node都有一个类型,一个字符串(可以为空),一个父节点(可以为空)和一个子节点列表(可以为空)。

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

在构造函数中初始化成员变量typestrNode中所有的成员变量都是公有的,可以对typestringparentchildren直接操作。

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

函数qDeleteAll()遍历容器里所有的指针,然后进行删除。但是删除后它并不把指针设置为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;
};

这次我们需要一个有继承关系的模型,模型类没有从QAbstractTableModel继承,而是从QAbstractItemModel继承。我们必需要重新实现的函数基本一样,在这个类中我们必须重新实现的函数还有index()parent()。为了设置模型的数据,我们调用setRootNode()函数,作为解析树的根节点。

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

在构造函数中,我们只是把根节点设置为0,把参数parent传递给基类。

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

在析构函数中我们删除根节点。如果根节点还有子节点,每一个子节点也会被Node的析构函数删除。

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

从新设置根节点前,我们删除原来的根节点(所有的子节点也一同删除)。然后设置新的根节点并调用call()函数,通知视图类刷新数据显示。

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)。对于表格式和列表式的模型,QAbstractTableModelQAbstractListModel的实现就是正确的,我们就不需要重新四实现这个函数。

在我们重新实现的index()函数中,如果没有根节点,我们返回一个无效的QModelIndex。如果有根节点,我们用行数,列数和子节点Node参数创建一个QModelIndex。对于有继承层次的模型,只知道行数,列数还不足以确定一个子节点。我们必须知道它的父节点。为了得到父节点,我们在QModelIndex中保存一个指向Node的指针。QModelIndex中除了可以保存一个行数,列数外,还可以保存一个void*指针或者一个int型数值。

子节点的Node*通过父节点的子节点列表得到。父节点由其QModelIndex索引和私有函数nodeFromIndex()得到:

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

函数nodeFromIndex()将给定索引的void*转换为一个Node*,如果给定的索引是无效的,就返回根节点rootNode,在模型类中,一个无效的模型索引指向的就是根节点。

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,或者不能得到一个有效的指针,我们返回一个无效的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提供的文档。

RegExpParsery应用程序包含四个类:

·         RegExpWindow is 一个窗口,用户可以输入表达式,显示表达式的解析结构.

·         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;
};

每一个Node都有一个类型,一个字符串(可以为空),一个父节点(可以为空)和一个子节点列表(可以为空)。

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

在构造函数中初始化成员变量typestrNode中所有的成员变量都是公有的,可以对typestringparentchildren直接操作。

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

函数qDeleteAll()遍历容器里所有的指针,然后进行删除。但是删除后它并不把指针设置为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;
};

这次我们需要一个有继承关系的模型,模型类没有从QAbstractTableModel继承,而是从QAbstractItemModel继承。我们必需要重新实现的函数基本一样,在这个类中我们必须重新实现的函数还有index()parent()。为了设置模型的数据,我们调用setRootNode()函数,作为解析树的根节点。

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

在构造函数中,我们只是把根节点设置为0,把参数parent传递给基类。

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

在析构函数中我们删除根节点。如果根节点还有子节点,每一个子节点也会被Node的析构函数删除。

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

从新设置根节点前,我们删除原来的根节点(所有的子节点也一同删除)。然后设置新的根节点并调用call()函数,通知视图类刷新数据显示。

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)。对于表格式和列表式的模型,QAbstractTableModelQAbstractListModel的实现就是正确的,我们就不需要重新四实现这个函数。

在我们重新实现的index()函数中,如果没有根节点,我们返回一个无效的QModelIndex。如果有根节点,我们用行数,列数和子节点Node参数创建一个QModelIndex。对于有继承层次的模型,只知道行数,列数还不足以确定一个子节点。我们必须知道它的父节点。为了得到父节点,我们在QModelIndex中保存一个指向Node的指针。QModelIndex中除了可以保存一个行数,列数外,还可以保存一个void*指针或者一个int型数值。

子节点的Node*通过父节点的子节点列表得到。父节点由其QModelIndex索引和私有函数nodeFromIndex()得到:

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

函数nodeFromIndex()将给定索引的void*转换为一个Node*,如果给定的索引是无效的,就返回根节点rootNode,在模型类中,一个无效的模型索引指向的就是根节点。

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,或者不能得到一个有效的指针,我们返回一个无效的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提供的文档。

### Custom BGE Model Overview Custom BGE (Bidirectional Generative Encoder) models represent a specialized category of deep learning architectures designed to handle complex data generation tasks. These models extend the capabilities of traditional generative models by incorporating bidirectional encoding mechanisms that allow for more nuanced understanding and manipulation of input sequences[^1]. ### Usage in IT Field In the information technology sector, custom BGE models find applications across various domains including natural language processing, image synthesis, audio signal reconstruction, and anomaly detection systems. The ability to generate realistic yet novel instances makes these models invaluable tools for enhancing user experience through personalized content creation or improving system robustness against adversarial attacks. For instance, within cybersecurity frameworks, such models can simulate potential threats based on historical attack patterns; thereby aiding proactive defense strategies development[^2]. ### Implementation Details Implementing a custom BGE involves several key steps: #### Data Preparation Collecting appropriate datasets is crucial as it directly impacts model performance. For text-based projects, this may involve gathering large corpora from diverse sources ensuring broad coverage over different topics and styles. ```python from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained(&#39;bert-base-uncased&#39;) tokenized_texts = tokenizer(texts, padding=True, truncation=True, max_length=512) ``` #### Architecture Design Design choices should reflect specific requirements like sequence length handling, attention mechanism inclusion etc., while leveraging pre-existing libraries where possible to expedite prototyping phases. ```python import torch.nn as nn class CustomBGE(nn.Module): def __init__(self): super(CustomBGE, self).__init__() # Define layers here def forward(self, x): pass # Implement forward propagation logic ``` #### Training Process Training typically requires substantial computational resources due to high dimensionality involved with modern neural networks. Utilizing GPU acceleration significantly reduces training times allowing faster experimentation cycles during research & development stages. ```bash CUDA_VISIBLE_DEVICES=0 python train.py --batch_size 32 --epochs 10 ``` ### Related Resources Exploring further into custom BGE implementations benefits greatly from accessing comprehensive documentation alongside active community forums dedicated specifically towards machine learning enthusiasts who share similar interests. Websites like GitHub host numerous open-source repositories offering ready-to-use code snippets along with detailed tutorials guiding beginners step-by-step throughout their journey mastering advanced concepts associated with building efficient generative encoders. --related questions-- 1. What are some best practices when preparing dataset for training custom BGE? 2. How does one choose between unidirectional vs bidirectional encoder designs? 3. Can you recommend any particular library optimized for implementing custom BGE efficiently? 4. Are there notable differences in applying custom BGE across varying industries beyond IT?
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值