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

来源:https://www.cnblogs.com/tcjiaan/archive/2023/09/09/17688963.html
-Advertisement-
Play Games

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


列表模型(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】鍵進入編輯狀態,文本框中的初始值就不會空白了。

 

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 前言 一. keep-alive 的作用 二. keep-alive 的原理 三. keep-alive 的應用 四. keep-alive 的刷新 五. keep-alive 頁面緩存思路 一. keep-alive 的作用 首先引用官 ...
  • 前段時間和朋友做了一個區域網考試系統,總共有3個端:考生端、監考端、管理端。 框架與相關的庫 先簡單說明一下我使用的框架和相關的庫: 構建工具:Vite 框架:Vue3 UI組件庫:element-plus 網路請求庫:axios 路由跳轉:vue-router 狀態管理:pinia CSS擴展語言 ...
  • >我們是[袋鼠雲數棧 UED 團隊](http://ued.dtstack.cn/),致力於打造優秀的一站式數據中台產品。我們始終保持工匠精神,探索前端道路,為社區積累並傳播經驗價值。 >本文作者:琉易 [liuxianyu.cn](https://link.juejin.cn/?target=ht ...
  • 以下是一個Python實現的簡單二分查找演算法的代碼示例: def binary_search(arr, target): left, right = 0, len(arr) - 1 while left <= right: mid = (left + right) // 2 # 找到中間元素的索引 ...
  • 一條爬蟲抓取一個小網站所有數據 ​ 今天閑來無事,寫一個爬蟲來玩玩。在網上衝浪的時候發現了一個搞笑的段子網,發現裡面的內容還是比較有意思的,於是心血來潮,就想著能不能寫一個Python程式,抓取幾條數據下來看看,一不小心就把這個網站的所有數據都拿到了。 ​ 這個網站主要的數據都是詳情在HTML裡面的 ...
  • 上篇文章12分鐘從Executor自頂向下徹底搞懂線程池中我們聊到線程池,而線程池中包含阻塞隊列 這篇文章我們主要聊聊併發包下的阻塞隊列 阻塞隊列 什麼是隊列? 隊列的實現可以是數組、也可以是鏈表,可以實現先進先出的順序隊列,也可以實現先進後出的棧隊列 那什麼是阻塞隊列? 在經典的生產者/消費者模型 ...
  • 本篇文章深入探討了 Go 語言中類型確定值、類型不確定值以及對應類型轉換的知識點,後續充分解析了常量與變數及其高級用法,並舉出豐富的案例。 關註公眾號【TechLeadCloud】,分享互聯網架構、雲服務技術的全維度知識。作者擁有10+年互聯網服務架構、AI產品研發經驗、團隊管理經驗,同濟本復旦碩, ...
  • 0 前言 註冊中心不應僅提供服務註冊和發現功能,還應保證對服務可用性監測,對不健康的服務和過期的進行標識或剔除,維護實例的生命周期,以保證客戶端儘可能的查詢到可用的服務列表。 因此本文介紹Nacos註冊中心的健康檢查機制。 1 註冊中心的健康檢查機制 知道⼀個服務是否還健康的方式: 客戶端主動上報, ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...