鍍金池/ 教程/ C/ 自定義 Model 之三
Qt 容器和算法拾遺
自定義 model 之一
反走樣
Hello, world!
Qt 容器類之關(guān)聯(lián)存儲(chǔ)容器
QStringListModel
拖放技術(shù)之一
狀態(tài)欄
QTreeWidget
拖放技術(shù)之二
通用算法
event()
Qt 學(xué)習(xí)之路(18): Qt 標(biāo)準(zhǔn)對(duì)話框之 QInputDialog
Qt 容器類之遍歷器和隱式數(shù)據(jù)共享
QListWidget
Meta-Object 系統(tǒng)
事件接收與忽略
Qt 學(xué)習(xí)之路(tip): parent 參數(shù)
Qt 標(biāo)準(zhǔn)對(duì)話框之 QColorDialog
QPainter(續(xù))
國(guó)際化(下)
漸變填充
自定義委托
創(chuàng)建 shared library
model-view 架構(gòu)
Graphics View Framework
自定義拖放數(shù)據(jù)對(duì)象
QSortFilterProxyModel
國(guó)際化(上)
組件布局
自定義 Model 之三
事件過(guò)濾器
QDirModel
Hello, world!(續(xù))
Qt 標(biāo)準(zhǔn)對(duì)話框之 QFileDialog
自定義 model 之二
深入了解信號(hào)槽
坐標(biāo)變換
剪貼板操作
QTableWidget
QByteArray 和 QVariant
創(chuàng)建一個(gè)對(duì)話框(下)
Qt 學(xué)習(xí)之路(32): 一個(gè)簡(jiǎn)易畫板的實(shí)現(xiàn)(Graphics View)
文本文件讀寫
自定義事件
編寫跨平臺(tái)的程序
MainWindow
初探信號(hào)槽
Qt 學(xué)習(xí)之路(17): Qt 標(biāo)準(zhǔn)對(duì)話框之 QMessageBox
繪圖設(shè)備
菜單和工具條(續(xù))
二進(jìn)制文件讀寫
QString
事件(event)
菜單和工具條
QPainter
Qt 容器類之順序存儲(chǔ)容器
進(jìn)程間交互
API 文檔的使用
創(chuàng)建一個(gè)對(duì)話框(上)
一個(gè)簡(jiǎn)易畫板的實(shí)現(xiàn)(QWidget)

自定義 Model 之三

今天來(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