【Qt6】列表模型——幾個便捷的列表型別

2023-10-29 21:08:52

前面一些文章,老周簡單介紹了在Qt 中使用列表模型的方法。很明顯,使用 Item Model 在許多時候還是挺麻煩的——要先建模型,再放資料,最後才構建檢視。為了簡化這些騷操作,Qt 提供了幾個便捷類。今天咱們逐個看看。

一、QListWidget

 這廝對應的 List View,用來顯示簡單的列表。要新增列表項,此類有兩個方法

void addItem(const QString &label) ;
void addItem(QListWidgetItem *item);

void addItems(const QStringList &labels);

前兩個方法是呼叫一次就新增一個列表項,新加的列表項將追加到列表末尾;addItems 方法是一次性新增多個項,以字串列表的方式新增。

對於簡單的列表項,可以用第一個方法,直接傳個字串就完事了。第二個方法需要一個 QListWidgetItem 物件,它可以對列表項做一些其他設定,如放個圖示,文字對齊方式等。當然,如果你不想用追加模式新增列表項,也可以用插入方法:

void insertItem(int row, const QString &label);
void insertItem(int row, QListWidgetItem *item);
void insertItems(int row, const QStringList &labels);

和 addItem 方法一樣,但多了一個 row 引數。因為簡單的列表模型只有一個列,所以 row 就是子項的索引。索引從 0 起計算,指定 row 參數列示在此處插入列表項,而列表中原有的元素會向後移一位。比如 5、6、7,在row=1 處插入9,即列表變成 5、9、6、7。

要刪除列表項,呼叫 takeItem 方法。

QListWidgetItem* takeItem(int row)

呼叫後,指定索引處的項被移除,並返回該項的範例參照(指標型別)。不要呼叫 removeItemWidget 方法,那個只刪除用於顯示列表項的元件罷了,列表項並未真正刪除。

下面咱們動動手,做個練習。

int main(int argc, char **argv)
{
    QApplication app(argc, argv);
    // 範例化元件
    QListWidget *view = nullptr;
    view = new QListWidget;
    // 視窗標題
    view->setWindowTitle("燒烤檔常見食物");
    // 視窗大小
    view->resize(255, 200);
    // 新增點子項
    view->addItem("烤羊肺");
    view->addItem("烤年糕");
    // 選建立QListWidgetItem範例,再新增
    QListWidgetItem* item = new QListWidgetItem("烤狗腿");
    view->addItem(item);// 也可以用字串列表
    QStringList strs;
    strs << "臭豆腐" << "烤鴨肉" << "烤雞翅";
    view->addItems(strs);

    // 顯示視窗
    view->show();
    // 進入事件迴圈
    return QApplication::exec();
}

第一、二項直接用字串新增列表項;第三項是先建立 QListWidgetItem 範例,然後再新增;第四、五、六項是通過字串列表一次性新增的。QStringList 其實就是 QList<QString> 類。

效果如下圖所示:

前面我們提到過一個 removeItemWidget 方法。提到它就得提一下 setItemWidget 方法,因為這倆是青梅竹馬的。這一對方法的作用是為某個列表項新增(或刪除)一個自定義元件(QWidget 或其子類)。被新增的元件會顯示在該列表項上面——其實是覆蓋在原有內容上面的。這個方法雖然方便我們為列表項客製化 UI,但它不支援動態行為,比如,你如果加一個文字方塊用來編輯資料,編輯好後資料是不會自動儲存的,所有的邏輯都要你自己寫程式碼實現。

咱們拿上面的範例開刀,為最後三項加入個按鈕,看看會怎樣。程式碼修改如下:

// 獲取最後三項的參照
int len = view->count();  // 猜猜它返回什麼
QListWidgetItem* tmpItem1 = view->item(len - 3);
QListWidgetItem* tmpItem2 = view->item(len - 2);
QListWidgetItem* tmpItem3 = view->item(len - 1);
// 弄三個按鈕
QPushButton* b1 = new QPushButton;
b1->setText("A");
QPushButton* b2= new QPushButton;
b2->setText("B");
QPushButton* b3 = new QPushButton;
b3->setText("C");
// 調整一下列表項的高度,不然按鈕可能顯示不全
QSize size(0, 32);
tmpItem1->setSizeHint(size);
tmpItem2->setSizeHint(size);
tmpItem3->setSizeHint(size);
// 設定三個按鈕與三個列表項關聯
view->setItemWidget(tmpItem1, b1);
view->setItemWidget(tmpItem2, b2);
view->setItemWidget(tmpItem3, b3);
// 為了能發現其中的祕密,咱們讓按鈕的寬度縮小一點
b1->setFixedWidth(25);
b2->setFixedWidth(25);
b3->setFixedWidth(25);

setSizeHint 方法為專案的「期望」大小設定一個固定的高度,寬度為0表示由佈局行為決定;而 32 是高度,告訴容器元件:「我需要32的高度」,畢竟預設的高度可能顯示不全按鈕。最後,我用 setFixedWidth 方法把三個按鈕的寬度給固死了,它的寬度只能是25畫素。這麼做是為了讓大夥看清楚,我們自定義的元件其實就是顯示在原有列表項上面的。如下圖,你看看,原列表項的文字還在呢。

不過,上述程式碼只是方便理解,沒什麼實際用處。下面咱們做有用的,重新做一下這個範例。

view->connect(view, &QListWidget::currentItemChanged, [=]
(QListWidgetItem* current, QListWidgetItem* previous) -> void{
    // 刪除前一個列表項所關聯的QWidget
    if(previous)
    {
        view->removeItemWidget(previous);
        // 還原列表項高度
        previous->setSizeHint(QSize(-1, -1));
    }
    // 如當前項為NULL,那後面的程式碼就沒必要執行了
    if(!current){
        return;
    }
    // 為當前項建立QWidget
    QWidget* wg = new QWidget;
    // 設定背景色
    wg->setAutoFillBackground(true);
    wg->setStyleSheet("background: lightgray");
    // 佈局
    QHBoxLayout* layout=new QHBoxLayout;
    wg->setLayout(layout);
    // 標籤
    QLabel* lb=new QLabel;
    // 標籤的文字就是列表項的文字
    lb->setText(current->text());
    layout->addWidget(lb);
    // 加個空白,填補剩餘空間
    layout->addStretch(1);
    // 按鈕
    QPushButton* btn= new QPushButton("刪除");
    layout->addWidget(btn);
    // 套娃,又一個訊號連線
    QObject::connect(btn, &QPushButton::clicked, [=](){
        // 當前索引
        int row = view->row(current);
        // 把當前列表項分離出來
        QListWidgetItem* _oldItem = view->takeItem(row);
        // 清除它
        delete _oldItem;
    });
    // 要改一下列表項的高度,不然按鈕可能顯示不全
    current->setSizeHint(QSize(0, 40));
    // 關聯列表項與元件
    view->setItemWidget(current, wg);
});

這裡實現的邏輯是:當列表項被選中才顯示按鈕。現在咱們也知道,setItemWidget 是建立一個自定義元件覆蓋在列表項上的,所以,在列表項選中後,顯示的自定義元件的背景不能透明,不然就穿幫了。元件裡面放一個QLabel 元件顯示列表項的文字,然後再放一個「刪除」按鈕,這樣就差不多了。

需要用到 QListWidget 的一個訊號:

void currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous);

這個訊號正符合咱們的需求,current 表示當前項(99.9% 的情況下就是被選中的項),previous 是前一個項——即被取消選擇的項。有了這兩個引數,咱們就可以用 removeItemWidget 方法刪除 previous 關聯的 Widget,併為 current 關聯新的 Widget。

currentItemChanged 訊號連線的 lambda 表示式內部又巢狀了一個 lambda 表示式—— 連線按鈕的 clicked 訊號。

QObject::connect(btn, &QPushButton::clicked, [=](){
    // 當前索引
    int row = view->row(current);
    // 把當前列表項分離出來
    QListWidgetItem* _oldItem = view->takeItem(row);
    // 清除它
    delete _oldItem;
});

刪除列表項時,takeItem 方法返回指定索引處的列表項指標。正因為這貨需要的引數是索引,所以不得不呼叫 row 方法選獲取 current 的索引,再傳給 takeItem 方法。列表項被移除後會返回其指標,因為這時候我們不需要它了,所以得 delete 掉其指向的物件。

執行程式後,選中「臭豆腐」。

然後單擊右邊的「刪除」按鈕,臭豆腐就沒了。

 

二、QTableWidget

QTableWidget 類派生自 QTableView,也是一個便捷類,可以不建立模型物件而直接新增資料。對應的列表項型別是 QTableWidgetItem。注意,一個 QTableWidgetItem 僅表示一個單元格的資料,所以,如果資料表格有兩行兩列,那麼,你得向裡面四個 item。

在新增列表項之前,要先呼叫:

setRowCount—— 設定表格共有幾行;

setColumnCount—— 設定表格共有多少列。

然後設定標題。包括列標題、行標題。一般設定列標題就可以了,行標題通常不用設定(預設顯示行號)。

setHorizontalHeaderLabels—— 設定列標題;

setVerticalHeaderLabels—— 設定行標題。

不管是行還是列標題,都可以使用字串列表(QStringList)來傳遞,有幾行/列就設定幾個值。

設定完上述基本引數後,就可以用 setItem 方法來設定每個單元格的資料了。方法原型如下:

void setItem(int row, int column, QTableWidgetItem *item);

row 表示行索引,column 表示列索引,索引從 0 算起。

 

接下來咱們做個演示:

int main(int argc, char* argv[])
{
    QApplication app(argc, argv);
    // 建立元件範例
    QTableWidget* viewWindow = new QTableWidget;
    // 設定標題和大小
    viewWindow->setWindowTitle("經典語錄");
    viewWindow->resize(350, 270);
    // 設定行數和列數
    viewWindow->setColumnCount(3);  // 三列
    viewWindow->setRowCount(4);     // 四行
    // 先弄好列標題
    viewWindow->setHorizontalHeaderLabels({"編號", "句子", "傷害指數"});
    // 建立列表項
    // 注意,每個QTableWidgetItem代表一個單元格
    // 第一行
    viewWindow->setItem(0, 0, new QTableWidgetItem("001"));
    viewWindow->setItem(0, 1, new QTableWidgetItem("你就長這個樣子啊?"));
    viewWindow->setItem(0, 2, new QTableWidgetItem("2"));
    // 第二行
    viewWindow->setItem(1, 0, new QTableWidgetItem("002"));
    viewWindow->setItem(1, 1, new QTableWidgetItem("你進化到靈長目動物了嗎?"));
    viewWindow->setItem(1, 2, new QTableWidgetItem("3"));
    // 第三行
    viewWindow->setItem(2, 0, new QTableWidgetItem("003"));
    viewWindow->setItem(2, 1, new QTableWidgetItem("你腦細胞夠不夠用?"));
    viewWindow->setItem(2, 2, new QTableWidgetItem("5"));
    // 第四行
    viewWindow->setItem(3, 0, new QTableWidgetItem("004"));
    viewWindow->setItem(3, 1, new QTableWidgetItem("學姐,你有頭嗎?"));
    viewWindow->setItem(3, 2, new QTableWidgetItem("10"));
    // 顯示檢視視窗
    viewWindow->show();
    return QApplication::exec();
}

執行結果如下:

 

三、QTreeWidget

 QTreeWidget 類針對的就是樹形結構的列表,是 QTreeView 的子類。它對應的列表項是 QTreeWidgetItem 類。

不過這裡要注意的是,QTreeWidgetItem 雖然表示樹形資料中的一個節點,但它在形式上就像一行資料。因為它可以包含多個列。Qt 的 Tree 檢視是可以展示多列的。

下面咱們先做個單列的 Tree 檢視。

int main(int argc, char** argv)
{
    QApplication app(argc, argv);

    // 建立檢視視窗
    QTreeWidget* view = nullptr;
    view = new QTreeWidget;
    // 先來幾個頂層節點
    QTreeWidgetItem* item1 = new QTreeWidgetItem({""});
    QTreeWidgetItem* item2 = new QTreeWidgetItem({""});
    QTreeWidgetItem* item3 = new QTreeWidgetItem({""});
    QTreeWidgetItem* item4 = new QTreeWidgetItem({""});
    QTreeWidgetItem* item5 = new QTreeWidgetItem({""});
    QTreeWidgetItem* item6 = new QTreeWidgetItem({""});
    // 給頂層節點新增子節點
    // 秦朝
    item1->addChild(new QTreeWidgetItem({"胡亥"}));
    item1->addChild(new QTreeWidgetItem({"扶蘇"}));
    item1->addChild(new QTreeWidgetItem({"辛追"}));
    item1->addChild(new QTreeWidgetItem({"項籍"}));
    // 漢朝
    item2->addChildren({
        new QTreeWidgetItem({"霍光"}),
        new QTreeWidgetItem({"劉向"}),
        new QTreeWidgetItem({"司馬遷"})
    });
    // 晉朝
    item3->addChildren({
        new QTreeWidgetItem({"衛鑠"}),
        new QTreeWidgetItem({"司馬承"}),
        new QTreeWidgetItem({"謝安"}),
        new QTreeWidgetItem({"王導"})
    });
    // 隋朝
    item4->addChild(new QTreeWidgetItem({"楊堅"}));
    item4->addChild(new QTreeWidgetItem({"史萬歲"}));
    item4->addChild(new QTreeWidgetItem({"王通"}));
    // 唐朝
    item5->addChildren({
        new QTreeWidgetItem({"上官婉兒"}),
        new QTreeWidgetItem({"李龜年"}),
        new QTreeWidgetItem({"張旭"}),
        new QTreeWidgetItem({"杜牧"}),
        new QTreeWidgetItem({"武三思"}),
        new QTreeWidgetItem({"李靖"})
    });
    // 宋朝
    item6->addChildren({
        new QTreeWidgetItem({"王堅"}),
        new QTreeWidgetItem({"賈似道"}),
        new QTreeWidgetItem({"司馬光"}),
        new QTreeWidgetItem({"宋慈"}),
        new QTreeWidgetItem({"張士遜"})
    });
    // 將頂層節點新增到QTreeWidget中
    view->addTopLevelItems({
        item1,
        item2,
        item3,
        item4,
        item5,
        item6
    });
    // 隱藏標題列
    view->setHeaderHidden(true);
    // 設定視窗標題
    view->setWindowTitle("中華名人表");
    // 顯示視窗
    view->show();

    return QApplication::exec();
}

QTreeWidgetItem 類別建構函式可以使用字串列表來初始化顯示的文字。原型如下:

explicit QTreeWidgetItem(const QStringList &strings, int type = Type);

在賦值的時候,可以直接用 { },例如

QTreeWidgetItem({"天時", "地利", "人和"});

在上面例子中,因為咱們這次用的只是一列,所以傳一個字串元素就可以了。

QTreeWidgetItem 要新增子節點,可以用這些方法:

// 一次只加一個節點,新節點追加到末尾
void addChild(QTreeWidgetItem *child);

// 一次只新增一個節點,但可以指定新節點插入到哪個位置
void insertChild(int index, QTreeWidgetItem *child);

// 一次可以新增多個節點,引數是個列表物件
void addChildren(const QList<QTreeWidgetItem*> &children);

// 一次可以新增多個節點,能指定插入位置
void insertChildren(int index, const QList<QTreeWidgetItem*> &children);

QTreeWidget 元件用以下方法新增頂層節點:

// 新增一個節點,追加到末尾
void addTopLevelItem(QTreeWidgetItem *item);

// 新增一個節點,可以指定插入位置
void insertTopLevelItem(int index, QTreeWidgetItem *item);

// 新增多個節點,追加到末尾
void addTopLevelItems(const QList<QTreeWidgetItem*> &items);

// 新增多個節點,可指定插入位置
void insertTopLevelItems(int index, const QList<QTreeWidgetItem*> &items);

執行效果如下:

 

QTreeWidget 類也有 setItemWidget、removeItemWidget 方法。這個就不必多介紹了,和上面 QListWidget 類的一個意思,就是用一個自定義 Widget 顯示在資料項上面。

下面咱們做個多列的 Tree 檢視。

#include <qapplication.h>
#include <qtreewidget.h>

int main(int argc, char* argv[])
{
    QApplication app(argc, argv);

    QTreeWidget * view = new QTreeWidget;
    // 設定列數
    view->setColumnCount(4);
    // 頂層節點
    auto itemA = new QTreeWidgetItem({"潛水部"});
    auto itemB = new QTreeWidgetItem({"洗腦部"});
    auto itemC = new QTreeWidgetItem({"韭菜部"});
    // 子節點
    itemA->addChildren({
        new QTreeWidgetItem({"01", "樑酷襠", "經理", "6"}),
        new QTreeWidgetItem({"02", "王曉意", "祕書", "3"})
    });
    itemB->addChildren({
        new QTreeWidgetItem({"03", "於三明", "經理", "4"}),
        new QTreeWidgetItem({"04", "週日清", "經理助理", "3"}),
        new QTreeWidgetItem({"05", "費潔合", "跟辦", "5"})
    });
    itemC->addChildren({
        new QTreeWidgetItem({"06", "安德詩", "經理", "3"}),
        new QTreeWidgetItem({"07", "李殿馳", "助理", "2"}),
        new QTreeWidgetItem({"08", "易更菁", "文員", "3"})
    });
    // 新增頂層節點到檢視
    view->addTopLevelItems({itemA, itemB, itemC});
    // 設定列標題
    view->setHeaderLabels({"編號", "姓名", "職務", "工齡"});
    // 設定視窗標題
    view->setWindowTitle("啃瓜裝飾服務有限公司員工表");
    // 顯示視窗
    view->show();

    return QApplication::exec();
}

效果如下:

程式碼就不用多解釋了吧,單列和多列的節點用法是一樣的,只是傳遞給 QTreeWidgetItem 類建構函式的列表元素個數不同罷了。