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