【Qt6】列表模型——抽象基類

列表模型(Item Model),老周沒有翻譯爲“項目模型”,因爲 Project 和 Item 都可以翻譯爲“項目”,容易出現歧義。乾脆叫列表模型。這個模型也確實是爲數據列表準備的,它以 MVC 的概念爲基礎,在原始數據和用戶界面視圖之間搭建橋樑,使兩者可以傳遞數據(提取、修改)。

Qt 裏面使用列表控制比較複雜,需要先創建模型(Model)。當然,也有像 QListWidget 類這樣已經封裝好,開箱即食的,這個後面再扯,現在咱們的重點是弄清楚 Item Model 是啥玩意兒。

這裏所說的 Item Model 並不是真正的數據,應該說算是個控制器。當用戶界面要顯示數據時,模型負責從原始數據那裏提取值,再把值傳到界面上呈現;如果用戶界面要修改數據,通過輸入框(QLineEdit等)輸入/修改內容,然後傳給模型,模型負責修改原始數據。這麼看來,視圖和原始數據不是直接通信的,模型就成了“中間商”。這個“中間商”可以不賺差價(按原始數據的樣子呈現),也可能賺差價(把原始數據加工一下再讓你看)。

列表模型有一個抽象基類,叫 QAbstractItemModel;對應地,視圖組件也有一個抽象基類,叫 QAbstractItemView。另外,在模型和視圖之間還有一個“代理人”,抽象基類叫 QAbstractItemDelegate,它幹嗎的呢?這是專業經紀人,負責門面工作。比如,在視圖組件裏呈現數據時用什麼字體,什麼顏色來繪製文本,用什麼方式從模型提取數據等;在編輯數據時,有什麼控件來輸入文本。以及在編輯結束後,輸入的內容怎麼傳給模型等。日常使用時咱們用到 QAbstractItemDelegate 不多,除非你自己想爲數據項繪製 UI,或用自定義的編輯組件。如果只是改改外觀什麼的,還不如用 QSS 方便。

行了,不扯太遠了,咱們只要知道這幾個基類之間的關係就行了。咱們的重點還是放在 QAbstractItemModel 類上面。

QAbstractItemModel 有幾個純虛函數是必須在派生類中重寫的:

1、index 方法,聲明如下:

virtual QModelIndex index(
             int row, 
             int column,
             const QModelIndex &parent = QModelIndex()) const = 0;

列表模型中的索引,專門用一個叫 QModelIndex 的類表示。index 方法是根據傳入的參數,返回 QModelIndex 對象。之所以要用 QModelIndex 類來表示列表項的索引,是因爲它是由幾個值組成的:

a、行號;
b、列號;
c、父索引。

Qt 中的列表模型用的是二維表結構,即由行和列組成,就像這樣:

問:D在哪裏?

答:row = 1,column = 0。

每個項又可以包含父級節點和子級節點,但上面的二維表只有一層,沒有父級,所以它的 parent = QModelIndex()。用默認構造函數創建的 QModelIndex 表示無效索引,即行號是 -1,列號是 -1,無父無子。

綜上所言,D 的索引就是:row = 1,col = 0,parent = QModelIndex()。

這個模型真正可怕的地方在於,每個索引都有父、子級。於是你可以構想下面這麼恐怖的列表:

Root是一個無效的索引,可以認爲是頂層的”父級“。A、B、C、D、E、F 的父級都是 Root,行列號由0開始編排,A在第一行第一列,所以 row=0,col=0,parent=Root。E有子節點,即 M、N、O、P,然後MNOP的行號和列號也要從 0 重新計算,即 N 的索引是 row=0, col=1, parent=E。最後,Q 這廝又有子節點,是一個只有一行的列表:R、S、T。於是,RST的行列號也重新計算。即 R 的索引是 row=0, col=0, parent=Q。

不過,實際使用時,一般不需要構建這麼神的數據結構,而且這玩意放到用戶界面上還不知道怎麼顯示好呢。畢竟,咱們在界面上常見的視圖也就以下三種:

1)、多行,只有一列,這就相當於像數組這樣的數據了。用 QListView 組件來呈現;

2)、一級二維表,由行、列組成,由 QTableView 組件呈現;

3)、多級節點,典型的就是 QTreeView 組件了。Qt 的 TreeView 比 .NET 的控件多了一個特點——可以在顯示多級節點的同時顯示錶格。但要注意的是,只有首列才支持父子節點。所以,對於 QTreeView 視圖,構建這樣的數據也足夠了:

2、parent 方法。它的聲明如下:

virtual QModelIndex parent(const QModelIndex &child) const = 0;

返回 child 節點的父級節點,對於只有一層的列表,返回 QModelIndex() 即可。

3、rowCount 方法。聲明如下:

virtual int rowCount(const QModelIndex &parent = QModelIndex()) const = 0;

返回原始數據有總共有多少行。

4、columnCount 方法。它的聲明如下:

virtual int columnCount(const QModelIndex &parent = QModelIndex()) const = 0;

該方法返回原始數據有多少列,如果是數組之類的,返回 1。

5、data 方法。聲明如下:

virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const = 0;

這是個重要的成員,它要根據 index 參數指定的索引,返回數據項的值。這裏要說一下叫”數據角色“的概念。說通俗不易懂一點就是返回的值的用途。比如,role 參數的默認值指定了 DisplayRole,意思就是你返回的值是要顯示在用戶界面上的,就是你想讓用戶看到的文本。Qt::ItemDataRole 枚舉定義了一系列數據角色。

enum ItemDataRole {
    DisplayRole = 0,               // 顯示在界面的內容
    DecorationRole = 1,          // 和文本一起顯示的圖標,類型一般是QIcon
    EditRole = 2,                    // 當編輯數據時,返回給用戶看的值
    ToolTipRole = 3,                // 顯示在工具提示中的文本
    StatusTipRole = 4,            // 顯示在狀態欄中的文本
    WhatsThisRole = 5,           // 幫助信息,顯示在”這是啥?“提示中
    // Metadata
    FontRole = 6,                    // 呈現數據時用啥字體
    TextAlignmentRole = 7,      // 文本的對齊方式
    BackgroundRole = 8,          // 返回畫刷對象,用來繪製列表項的背景
    ForegroundRole = 9,           // 文本的顏色
    CheckStateRole = 10,         // 如果界面上顯示了 checkbox,那麼返回checkbox的狀態(選中?未選中?未知?)
    // Accessibility
    AccessibleTextRole = 11,     // 簡練的輔助信息。用於像”講述人“這些輔助工具
    AccessibleDescriptionRole = 12,    //詳細輔助信息,用於像”講述人“類似的輔助工具
    // More general purpose
    SizeHintRole = 13,               // 返回該列表項希望顯示的大小(寬多少,高多少)
    InitialSortOrderRole = 14,    // 數據第一次呈現時用的排序方式(升序?降序?)
    // 下面五個不知道是什麼鬼
    // Internal UiLib roles. Start worrying whe
high.
    DisplayPropertyRole = 27, 
    DecorationPropertyRole = 28,
    ToolTipPropertyRole = 29,
    StatusTipPropertyRole = 30,
    WhatsThisPropertyRole = 31,
    // Reserved,保留用來給開發者自定義角色。自定義角色從這個數值開始
    UserRole = 0x0100
};

重寫 data 方法實現了數據的只讀模式,若數據支持編輯,必須重寫 setData 方法,把內容寫入原始數據。

如果要實現添加、刪除數據項,還要重寫以下方法:

6、insertRows:在行號爲 row 處連續插入 count 行數據。

virtual bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex());

7、removeRows:從行號爲 row 處開始,連續刪除 count 行。

virtual bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex());

8、insertColumns:從列號爲 column 處起,連接插入 count 列。

virtual bool insertColumns(int column, int count, const QModelIndex &parent = QModelIndex());

9、removeColumns:從列號爲 column 處開始,連續刪除 count 列。

virtual bool removeColumns(int column, int count, const QModelIndex &parent = QModelIndex());

 ===============================================================

下面咱們做一個簡單的模型練練手。該模型的原始數據是一個整數列表(QList<int>)。先實現只讀功能,即需要重寫 parent、index、rowCount、columnCount 和 data 方法。頭文件的聲明如下:

#ifndef MODELS_H
#define MODELS_H

#include <QAbstractItemModel>
#include <QObject>
#include <QList>

class MyItemModel: public QAbstractItemModel
{
    Q_OBJECT

public:
    // 下面兩個是構造函數
    explicit MyItemModel(QObject* parent = nullptr);
    explicit MyItemModel(const QList<int> &list, QObject* parent = nullptr);
    
    // 返回父級
    QModelIndex parent(const QModelIndex & child) const override;
    // 返回索引
    QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const;
    // 返回行數和列數
    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
    int columnCount(const QModelIndex &parent = QModelIndex()) const override;
    // 獲取數據
    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;

    // 下面這兩個方法用來獲取或設置數據源
    QList<int> intList() const;
    void setIntList(const QList<int> &list);

private:
    QList<int> m_list;  // 私有的
};

#endif

私有字段 m_list 用於引用原始數據,可以通過構造函數的 list 參數設置,也可以通過 setIntList 方法設置。

下面代碼實現構造函數,主要是初始化私有成員。

MyItemModel::MyItemModel(QObject *parent)
    : m_list(QList<int>()), QAbstractItemModel(parent)
{
}

MyItemModel::MyItemModel(const QList<int> &list, QObject *parent)
    : QAbstractItemModel(parent), m_list(list)
{
}

下面代碼實現 parent 方法。由於是整數列表,數據只有一層,所以直接返回 QModelIndex() 即可。

QModelIndex MyItemModel::parent(const QModelIndex &child) const
{
    // 簡單的列表不需要父子層次
    // 使用無參構造函數表示無效索引
    return QModelIndex();
}

 

接着是實現返回數據列表的行數和列數。

int MyItemModel::rowCount(const QModelIndex &parent) const
{
    // 一樣的道理,不能有父級數據
    if (parent.isValid() ){
        return 0;
    }
    // 返回QList中元素個數,每個元素代表一行
    return m_list.size();
}

int MyItemModel::columnCount(const QModelIndex &parent) const
{
    if(parent.isValid())
        return 0;
    // 咱們這個模型永遠只有一列
    return 1;
}

 

實現 index 方法,爲數據項創建索引。

QModelIndex MyItemModel::index(int row, int column, const QModelIndex &parent) const
{
    // 因爲此列表不存在爺爺/孫子/父/子關係
    // 所以如果索引是有效的,說明它不對
    // 咱們這個列表是沒有父級的
    if(parent.isValid())
        return QModelIndex();       // 有效索引不是咱們想要的,返回無效索引
    // 如果索引無效,說明是頂層數據,是咱們想要的
    return createIndex(row, column);
}

QModelIndex 無法直接訪問其成員,要產生索引請調用 createIndex 方法。

實現 data 方法。返回數據,這裏咱們實現了正常顯示的文本和作爲工具提示用的文本。

QVariant MyItemModel::data(const QModelIndex &index, int role) const
{
    // 注意 role 這個參數,返回前必須判斷
    if(role == Qt::DisplayRole)
    {
        // DisplayRole 說明獲取的數據是用在界面呈現上的
        // 咱們只考慮行號,列號嘛,反正只有一列
        int idx = index.row();
        return m_list.at(idx);
    }
    // 可以提供工具提示
    if(role == Qt::ToolTipRole)
    {
        int i = index.row();
        int val = m_list.at(i);
        return QString("這是整數值:%1").arg(val);
    }
    // 如果是其他role,就返回一個默認的QVariant給它
    return QVariant();
}

其他未用到的數據角色返回空的 QVariant 就可以。data 方法返回的值,是對應着二維表中某個單元格的,所以,你希望在那個單元格中顯示什麼就返回什麼。

最後實現的兩個方法是用來獲取或設置數據源的(即原始數據)。

QList<int> MyItemModel::intList() const
{
    return m_list;
}

void MyItemModel::setIntList(const QList<int> &list)
{
    m_list = list;
}

 

至此,一個簡單的模型就有了,當然,沒有實現 setData 方法,它只能讀數據,不支持編輯。現在我們可以拿來用了。

int main(int argc, char** argv)
{
    QApplication app(argc, argv);
    // 創建視圖實例
    QListView lv;
    lv.setWindowTitle("簡單模型");
    // 準備點數據
    QList<int> theList;
    theList << 100 << 300 << 4500 << 600 << 1200;
    // 實例化模型
    MyItemModel *model;
    model = new MyItemModel(theList);
    //model->setIntList(theList);
    // 爲視圖設置模型
    lv.setModel(model);
    // 顯示窗口
    lv.show();

    // 主循環
    return QApplication::exec();
}

QListView 作爲視圖組件,適合顯示簡單的列表。調用視圖的 setModel 方法就可以關聯指定的模型對象了。如上述代碼中,咱們自定義的 MyItemModel 在設置了原始數據後,就可以傳遞給 setModel 方法以提供數據。

結果如下圖所示:

把鼠標移到某個項上,還能看到工具提示呢。

 

咱們給 MyItemModel 加上 setData 方法的重寫,使它支持編輯功能。

// 頭文件
bool setData(const QModelIndex &index, const QVariant &value, int 
                   role = Qt::EditRole) override;

Qt::ItemFlags flags(const QModelIndex &index) const override;
// 實現代碼
bool MyItemModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    // 設置數據時數據角色通常是編輯
    if(role == Qt::EditRole)
    {
        // 因爲只有一列,我們不用關心列號,只取行號
        int row = index.row();
        if(value.canConvert<int>() == false)
        {
            // 不是int值,玩不下去了
            return false;
        }
        // 更新數據
        m_list.replace(row, value.toInt());
        // 發出信號
        QList<int> roles = { Qt::DisplayRole, Qt::EditRole, Qt::ToolTipRole };
        emit dataChanged(index, index, roles);
        // 輸出一下,主要是檢查list有沒有順利修改
        qDebug() << m_list;
        return true;
    }
    return false;
}

Qt::ItemFlags MyItemModel::flags(const QModelIndex &index) const
{
    Qt::ItemFlags oldFlags = QAbstractItemModel::flags(index);
    return oldFlags | Qt::ItemIsEditable;
}

先說說爲什麼要同時重寫 flags 方法,此方法返回 ItemFlag 枚舉的值(值可以合併)。如果想讓視圖組件知道此模型允許編輯,那麼返回的 ItemFlags 必須包含 ItemEditable 值。

現在說 setData 方法。首先,role 參數得是 EditRole 才表明用戶界面已進入編輯狀態,並且這個值是在編輯狀態下傳送過來的。canConvert 方法是檢查一下傳過來的是不是 int 值,這裏如果是 QListView 組件默認處理的話,一般不會搞錯類型。

咱們的原始數據就是存放在 QList<int> 對象中的,所以只調用 replace 方法把某個索引處的值替換下就可以了;如果數據來自文件,就得寫入文件以保存。

在數據更新後,記得發送一個 dataChanged 信號,通知所有連接到此信號的對象,數據已變更,趕緊刷新提取最更的值。dataChanged 信號需要三個參數:

void dataChanged(
         const QModelIndex &topLeft, 
         const QModelIndex &bottomRight,
         const QList<int> &roles = QList<int>());

topLeft 參數和 bottomRight 參數是兩個索引,它們描述了被修動數據的區域,用左上角和右下角的索引來表示。本示例中,每次只修改一個行,所以,左上角和右下角的索引都是被修改項的索引。roles 參數告訴程序:哪些角色的數要更新一下。一般 EditRole 和 DisplayRole 的要更新,這樣可讓應用程序知道去刷新數據。模型只用在 QListView 視圖中,所以就算不發出 dataChanged 信號,組件也能自動刷新。但如果模型同時應用在多個視圖中,並且有其他代碼連接了 dataChanged 信號,那就得發出這個信號了。

setData 方法返回 bool 值,true 表示成功,false 表示失敗。

修改後,只要雙擊列表項,就會出現文本框,然後你可以輸入新的值,輸完後按“回車”鍵,或者移開焦點(如點擊其他空白地方),就會觸發更新。

 但是,你會發現一個問題:進入編輯狀態時,文本框裏都是空的。如下圖:

這不合理,應該顯示原有的值讓用戶修改。造成編輯狀態下初始值空白的原因是咱們前面的 data 方法。因爲咱們在返回值的時候,只判斷了在 DisplayRole 角色下才返回,當視圖進入編輯狀態後,調用 data 方法獲取數據時,role 參數的值是 EditRole,這就導致獲取到空值。

回去修改一下 data 方法的代碼。

QVariant MyItemModel::data(const QModelIndex &index, int role) const
{
    // 注意 role 這個參數,返回前必須判斷
    if(role == Qt::DisplayRole || role == Qt::EditRole)
    {
        ……
    }
    ……
}

現在,雙擊列表項或按【F2】鍵進入編輯狀態,文本框中的初始值就不會空白了。

 

好了,關於怎麼繼承列表模型的公共基類的話題,咱們就扯到這兒了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章