Qt预定义的类已经提供了方便处理和显示数据的方式。然而,一些数据源不能直接使用这些定义好的模型,这就需要创建自定义的模型,优化对这些数据的处理。
在创建自定义模型之前,让我们首先回顾一下Qt的model/view结构的主要概念。在一个模型中每一个数据都有一个模型的索引(a model index)和一组属性,这些属性称为角色(roles),这些属性能够用任何类型的数据保存。在这一章的前几节最常用的角色为:Qt::DisplayRole和Qt::EditRole。其它角色有些是用来辅助性的数据,例如Qt::ToolTipRole,Qt::StatusTipRole和Qt::WhatsThisRole,还有一些是对显示属性的控制,如Qt::FontRole,Qt::TextAlignmentRole,Qt::TextColorRole,Qt::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×162=26224个数据。如果使用下面的模型,我们只需要保存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(¤cyModel);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提供了一些基本的模型类,包括QAbstractListModel,QAbstractTableModel,QAbstractItemModel。QAbstractItemModel类用来支持广泛类型的模型,而QAbstractListModel和QAbstractTableModel类对一维数据和二维数据提供支持。
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的距离是一样的,因此,数据沿主对角线是镜像的。
为了说明自定义数据模型和普通表格的不同,我们假设有三个城市A,B,C,如果我们为每一个组合保存一个值,我们需要存储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_OBJECTpublic: 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::ItemIsSelectable和Qt::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;}
在构造函数中初始化成员变量type和str。Node中所有的成员变量都是公有的,可以对type,string,parent,children直接操作。
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)。对于表格式和列表式的模型,QAbstractTableModel和QAbstractListModel的实现就是正确的,我们就不需要重新四实现这个函数。
在我们重新实现的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类在显示有层次结构的模型时没有竖直方向的标题,在函数中没有进行处理竖直方向的标题。
现在我们已经完成了Node和RegExpModel,当用户改变文本时,根节点就需要重新创建。
void RegExpWindow::regExpChanged(const QString ®Exp){ 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;}
在构造函数中初始化成员变量type和str。Node中所有的成员变量都是公有的,可以对type,string,parent,children直接操作。
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)。对于表格式和列表式的模型,QAbstractTableModel和QAbstractListModel的实现就是正确的,我们就不需要重新四实现这个函数。
在我们重新实现的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类在显示有层次结构的模型时没有竖直方向的标题,在函数中没有进行处理竖直方向的标题。
现在我们已经完成了Node和RegExpModel,当用户改变文本时,根节点就需要重新创建。
void RegExpWindow::regExpChanged(const QString ®Exp){ RegExpParser parser; Node *rootNode = parser.parse(regExp); regExpModel->setRootNode(rootNode);}
当用户改变输入的表达式时,主窗口的regExpChanged()就被调用。在这个函数中,解析器解析用户输入的表达式,解析器返回的指针作为解析树的根节点。
在这里没有介绍RegExpParser类,这个类不设计GUI和模型视图编程。完整的代码在书的CD上。
在这一节中,我们介绍了三个不同的自定义模型的创建。很多模型的项目和模型索引之间是一对一的关系,要比这里提到的要简单。更多的有关模型视图编程的例子可以参考Qt提供的文档。
本文介绍了如何在Qt中创建自定义的数据模型以优化特定数据源的处理方式,包括只读模型、可编辑模型及解析树模型。
9386

被折叠的 条评论
为什么被折叠?



