今天來說的是自定義 model 中最復雜的例子。這個例子同樣也是出自 C++ GUI Programming with Qt 4, 2nd Edition 這本書。
這個例子是將布爾表達式分析成一棵樹。這個分析過程在離散數(shù)學中經(jīng)常遇到,特別是復雜的布爾表達式,類似的分析可以比較方便的進行表達式化簡、求值等一系列的計算。同樣,這個技術也可以很方便的分析一個表達式是不是一個正確的布爾表達式。在這個例子中,一共有四個類:
Node:組成樹的節(jié)點;BooleaModel:布爾表達式的 model,實際上是一個 tree model,用于將布爾表達式表示成一棵樹;BooleanParser:將布爾表達式生成分析樹的分析器;BooleanWindow:輸入布爾表達式并進行分析,展現(xiàn)成一棵樹。
這個例子可能是目前為止最復雜的一個了,所以先來看看最終的結果,以便讓我們心中有數(shù):
先來看這張圖片,我們輸入的布爾表達式是!(a||b)&&c||d, 在下面的 Node 欄中,用樹的形式將這個表達式分析了出來。如果你熟悉編譯原理,這個過程很像詞法分析的過程:將一個語句分析稱一個一個獨立的詞素。
我們從最底層的 Node 類開始看起,一步步構造這個程序。
Node.h
class Node
{
public:
enum Type
{
Root,
OrExpression,
AndExpression,
NotExpression,
Atom,
Identifier,
Operator,
Punctuator
};
Node(Type type, const QString &str = "");
~Node();
Type type;
QString str;
Node *parent;
QList<Node *> children;
};
Node.cpp
Node::Node(Type type, const QString &str)
{
this->type = type;
this->str = str;
parent = 0;
}
Node::~Node()
{
qDeleteAll(children);
}
Node 很像一個典型的樹的節(jié)點:一個 Node 指針類型的 parent 屬性,保存父節(jié)點;一個 QString類型的 str,保存數(shù)據(jù)。另外,Node 里面還有一個 Type 屬性,指明這個 Node 的類型,是一個詞素,還是操作符,或者其他什么東西;children 是一個 QList<Node *>類型的列表,保存這個 node 的子節(jié)點。注意,在 Node 類的析構函數(shù)中,使用了 qDeleteAll()這個全局函數(shù)。這個函數(shù)是將[start, end)范圍內的所有元素進行 delete。因此,它的參數(shù)的元素必須是指針類型的。并且,這個函數(shù)使用 delete 之后并不會將指針賦值為0,所以,如果要在析構函數(shù)之外調用這個函數(shù),建議在調用之后顯示的調用 clear()函數(shù),將所有子元素的指針清為0.
在構造完子節(jié)點之后,我們開始構造 model:
booleanmodel.h
class BooleanModel : public QAbstractItemModel
{
public:
BooleanModel(QObject *parent = 0);
~BooleanModel();
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;
};
booleanmodel.cpp
BooleanModel::BooleanModel(QObject *parent)
: QAbstractItemModel(parent)
{
rootNode = 0;
}
BooleanModel::~BooleanModel()
{
delete rootNode;
}
void BooleanModel::setRootNode(Node *node)
{
delete rootNode;
rootNode = node;
reset();
}
QModelIndex BooleanModel::index(int row, int column,
const QModelIndex &parent) const
{
if (!rootNode || row < 0 || column < 0)
return QModelIndex();
Node *parentNode = nodeFromIndex(parent);
Node *childNode = parentNode->children.value(row);
if (!childNode)
return QModelIndex();
return createIndex(row, column, childNode);
}
Node *BooleanModel::nodeFromIndex(const QModelIndex &index) const
{
if (index.isValid()) {
return static_cast<Node *>(index.internalPointer());
} else {
return rootNode;
}
}
int BooleanModel::rowCount(const QModelIndex &parent) const
{
if (parent.column() > 0)
return 0;
Node *parentNode = nodeFromIndex(parent);
if (!parentNode)
return 0;
return parentNode->children.count();
}
int BooleanModel::columnCount(const QModelIndex & /* parent */) const
{
return 2;
}
QModelIndex BooleanModel::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, 0, parentNode);
}
QVariant BooleanModel::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::Root:
return tr("Root");
case Node::OrExpression:
return tr("OR Expression");
case Node::AndExpression:
return tr("AND Expression");
case Node::NotExpression:
return tr("NOT Expression");
case Node::Atom:
return tr("Atom");
case Node::Identifier:
return tr("Identifier");
case Node::Operator:
return tr("Operator");
case Node::Punctuator:
return tr("Punctuator");
default:
return tr("Unknown");
}
} else if (index.column() == 1) {
return node->str;
}
return QVariant();
}
QVariant BooleanModel::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();
}
現(xiàn)在,我們繼承了 QAbstractItemModel。之所以不繼承前面說的 QAbstractListModel 或者QAbstractTableModel,是因為我們要構造一個 tree model,而這個 model 是有層次結構的。所以,我們直接繼承了那兩個類的基類。在構造函數(shù)中,我們把根節(jié)點的指針賦值為0,因此我們提供了另外的一個函數(shù) setRootNode(),將根節(jié)點進行有效地賦值。而在析構中,我們直接使用 delete 操作符將這個根節(jié)點 delete掉。在 setRootNode()函數(shù)中,首先我們 delete 掉原有的根節(jié)點,再將根節(jié)點賦值,然后調用 reset()函數(shù)。這個函數(shù)將通知所有的 view 對界面進行重繪,以表現(xiàn)最新的數(shù)據(jù)。
使用 QAbstractItemModel,我們必須重寫它的五個純虛函數(shù)。首先是 index()函數(shù)。這個函數(shù)在QAbstractTableModel 或者 QAbstractListModel 中不需要覆蓋,因此那兩個類已經(jīng)重寫過了。但是,我們繼承 QAbstractItemModel 時必須覆蓋。這個函數(shù)的簽名如下:
virtual QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const = 0;
這是一個純虛函數(shù),用于返回第 row 行,第 column 列,父節(jié)點為 parent 的那個元素的QModelIndex 對象。對于 tree model,我們關注的是 parent 參數(shù)。看一下我們的實現(xiàn):
QModelIndex BooleanModel::index(int row, int column,
const QModelIndex &parent) const
{
if (!rootNode || row < 0 || column < 0)
return QModelIndex();
Node *parentNode = nodeFromIndex(parent);
Node *childNode = parentNode->children.value(row);
if (!childNode)
return QModelIndex();
return createIndex(row, column, childNode);
}
如果 rootNode 或者 row 或者 column 非法,返回一個非法的 QModelIndex。然后使用nodeFromIndex()函數(shù)取得索引為 parent 的節(jié)點,然后我們使用 children 屬性(這是我們前面定義的 Node 里面的屬性)獲得子節(jié)點。如果子節(jié)點不存在,返回一個非法值。最后,當是一個有效值時,由 createIndex()函數(shù)返回有效地 QModelIndex對象。
對于具有層次結構的 model 來說,只有 row 和 column 值是不能確定這個元素的位置的,因此,QModelIndex 中除了 row 和 column 之外,還有一個 void*或者 int 的空白屬性,可以存儲一個值。在這里我們就把父節(jié)點的指針存入,這樣,就可以由這三個屬性定位這個元素。因此,createIndex()中第三個參數(shù)就是這個內部的指針。所以我們自己定義一個 nodeFromIndex()函數(shù)的時候要注意使用QModelIndex 的 internalPointer()函數(shù)獲得這個內部指針,從而定位我們的 node。
后面的 rowCount()和 columnCount()這兩個函數(shù)比較簡單,就是要獲得 model 的行和列的值。由于我們的 model 定義成2列,所以在 columnCount()函數(shù)中始終返回2.
parent()函數(shù)要返回子節(jié)點的父節(jié)點的索引,我們要從子節(jié)點開始尋找,直到找到父節(jié)點的父節(jié)點,這樣就能定位到父節(jié)點,從而得到子節(jié)點的位置。而 data()函數(shù)要返回每個單元格的返回值,經(jīng)過前面兩個例子,我想這個函數(shù)已經(jīng)不會有很大的困難了的。headerData()函數(shù)返回列頭的名字,同前面一樣,這里就不再贅述了。
前面的代碼很長,BooleanWindow部分就很簡單了。就是把整個 view 和 model 組合起來。另外的一個 BooleanParser 類沒有什么 GUI 方面的代碼,是純粹的算法問題。如果我看得沒錯的話,這里應該使用的是編譯原理里面的遞歸下降詞法分析,有興趣的朋友可以到網(wǎng)上查一下相關的資料。我想在以后的《自己動手寫編譯器》中再詳細介紹這個算法。
好了,今天的內容很多,為了方便大家查看和編譯代碼,我已經(jīng)把這接種出現(xiàn)的所有代碼打包傳到附件中。
本文出自 “豆子空間” 博客,請務必保留此出處 http://devbean.blog.51cto.com/448512/193918
更多建議: