原創作品,允許轉載,轉載時請務必以超鏈接形式標明文章 原始出處 、作者信息和本聲明。否則將追究法律責任。http://devbean.blog.51cto.com/448512/268468
今天來說的是自定義model中最複雜的例子。這個例子同樣也是出自C++ GUI Programming with Qt 4, 2nd Edition這本書。
這個例子是將布爾表達式分析成一棵樹。這個分析過程在離散數學中經常遇到,特別是複雜的布爾表達式,類似的分析可以比較方便的進行表達式化簡、求值等一系列的計算。同樣,這個技術也可以很方便的分析一個表達式是不是一個正確的布爾表達式。在這個例子中,一共有四個類:
Node:組成樹的節點;
BooleaModel:布爾表達式的model,實際上是一個tree model,用於將布爾表達式表示成一棵樹;
BooleanParser:將布爾表達式生成分析樹的分析器;
BooleanWindow:輸入布爾表達式並進行分析,展現成一棵樹。
這個例子可能是目前爲止最複雜的一個了,所以先來看看最終的結果,以便讓我們心中有數:
先來看這張圖片,我們輸入的布爾表達式是!(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很像一個典型的樹的節點:一個Node指針類型的parent屬性,保存父節點;一個QString類型的str,保存數據。另外,Node裏面還有一個Type屬性,指明這個Node的類型,是一個詞素,還是操作符,或者其他什麼東西;children是一個QList<Node *>類型的列表,保存這個 node 的子節點。注意,在Node類的析構函數中,使用了qDeleteAll()這個全局函數。這個函數是將[start, end)範圍內的所有元素進行delete。因此,它的參數的元素必須是指針類型的。並且,這個函數使用delete之後並不會將指針賦值爲0,所以,如果要在析構函數之外調用這個函數,建議在調用之後顯示的調用clear()函數,將所有子元素的指針清爲0.
在構造完子節點之後,我們開始構造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();
}
現在,我們繼承了QAbstractItemModel。之所以不繼承前面說的QAbstractListModel或者QAbstractTableModel,是因爲我們要構造一個tree model,而這個model是有層次結構的。所以,我們直接繼承了那兩個類的基類。在構造函數中,我們把根節點的指針賦值爲0,因此我們提供了另外的一個函數setRootNode(),將根節點進行有效地賦值。而在析構中,我們直接使用delete操作符將這個根節點delete掉。在setRootNode()函數中,首先我們delete掉原有的根節點,再將根節點賦值,然後調用reset()函數。這個函數將通知所有的view對界面進行重繪,以表現最新的數據。
使用QAbstractItemModel,我們必須重寫它的五個純虛函數。首先是index()函數。這個函數在QAbstractTableModel或者QAbstractListModel中不需要覆蓋,因此那兩個類已經重寫過了。但是,我們繼承QAbstractItemModel時必須覆蓋。這個函數的簽名如下:
virtual QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const = 0;
這是一個純虛函數,用於返回第row行,第column列,父節點爲parent的那個元素的QModelIndex對象。對於tree model,我們關注的是parent參數。看一下我們的實現:
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()函數取得索引爲parent的節點,然後我們使用children屬性(這是我們前面定義的Node裏面的屬性)獲得子節點。如果子節點不存在,返回一個非法值。最後,當是一個有效值時,由createIndex()函數返回有效地QModelIndex對象。
對於具有層次結構的model來說,只有row和column值是不能確定這個元素的位置的,因此,QModelIndex中除了row和column之外,還有一個void*或者int的空白屬性,可以存儲一個值。在這裏我們就把父節點的指針存入,這樣,就可以由這三個屬性定位這個元素。因此,createIndex()中第三個參數就是這個內部的指針。所以我們自己定義一個nodeFromIndex()函數的時候要注意使用QModelIndex的internalPointer()函數獲得這個內部指針,從而定位我們的node。
後面的rowCount()和columnCount()這兩個函數比較簡單,就是要獲得model的行和列的值。由於我們的model定義成2列,所以在columnCount()函數中始終返回2.
parent()函數要返回子節點的父節點的索引,我們要從子節點開始尋找,直到找到父節點的父節點,這樣就能定位到父節點,從而得到子節點的位置。而data()函數要返回每個單元格的返回值,經過前面兩個例子,我想這個函數已經不會有很大的困難了的。headerData()函數返回列頭的名字,同前面一樣,這裏就不再贅述了。
前面的代碼很長,BooleanWindow部分就很簡單了。就是把整個view和model組合起來。另外的一個BooleanParser類沒有什麼GUI方面的代碼,是純粹的算法問題。如果我看得沒錯的話,這裏應該使用的是編譯原理裏面的遞歸下降詞法分析,有興趣的朋友可以到網上查一下相關的資料。我想在以後的《自己動手寫編譯器》中再詳細介紹這個算法。
好了,今天的內容很多,爲了方便大家查看和編譯代碼,我已經把這接種出現的所有代碼打包傳到附件中。
本文出自 “豆子空間” 博客,請務必保留此出處http://devbean.blog.51cto.com/448512/268468