C++ Qt開發:QItemDelegate自定義代理元件

2023-12-20 12:02:34

Qt 是一個跨平臺C++圖形介面開發庫,利用Qt可以快速開發跨平臺表單應用程式,在Qt中我們可以通過拖拽的方式將不同元件放到指定的位置,實現圖形化開發極大的方便了開發效率,本章將重點介紹QStyledItemDelegate自定義代理元件的常用方法及靈活運用。

在Qt中,QStyledItemDelegate 類是用於建立自定義表格檢視(如QTableViewQTableWidget)的委託類,允許你自定義表格中每個單元格的外觀和互動。QStyledItemDelegateQItemDelegate 的子類,提供了更現代、更易用的介面。此處我們將實現對QTableView表格元件的自定義代理功能,例如預設情況下表格中的預設代理就是一個編輯框,我們只能夠在編輯框內輸入資料,而有時我們想選擇資料而不是輸入,此時就需要重寫編輯框實現選擇的效果,代理元件常用於個性化客製化表格中的欄位型別。

1.1 概述代理類

代理類的作用是用來實現元件重寫的,例如TableView中預設是可編輯的,之所以可編輯是因為Qt預設為我們重寫了QLineEdit編輯框實現的,也可理解為將元件嵌入到了表格中,實現了對錶格的編輯功能。

在自定義代理中QAbstractItemDelegate是所有代理類的抽象基礎類別,它用於建立自定義的項委託。提供了一個基本的框架,使得可以客製化如何在檢視中繪製和編輯資料項。

QAbstractItemDelegateQItemDelegate 的基礎類別,而 QItemDelegate 則是 QStyledItemDelegate 的基礎類別。這個繼承體系提供了不同層次的客製化能力。我們繼承任何元件時都必須要包括如下4個函數:

  • CreateEditor() 用於建立編輯模型資料的元件,例如(QSpinBox元件)
  • SetEditorData() 從資料模型獲取資料,以供Widget元件進行編輯
  • SetModelData() 將Widget元件上的資料更新到資料模型
  • UpdateEditorGeometry() 給Widget元件設定一個合適的大小

通過繼承 QAbstractItemDelegate 並實現這些函數,讀者可建立一個客製化的項委託,用於控制資料項在檢視中的外觀和互動行為。此處我們分別重寫三個代理介面,其中兩個ComBox元件用於選擇婚否,而第三個SpinBox元件則用於調節數值範圍,先來定義三個重寫部件。

1.2 自定義代理元件

這裡我們以第一個SpinBox元件為例,要實現代理該元件,首先需要在專案上新建一個SpinDelegate類,並依次實現上述的四個方法,先來開建立流程;

  • 選擇addnew選中 C++ Class 輸入自定義類名稱QWintSpinDelegate,然後基礎類別繼承QStyledItemDelegate/QMainWindow,然後下一步結束嚮導,同理其他功能的建立也如此。

接著就是對該介面的重寫了,此處重寫程式碼spindelegate.cpp如下所示,其關鍵位置的解釋可參考註釋部分。

#include "spindelegate.h"
#include <QSpinBox>

QWIntSpinDelegate::QWIntSpinDelegate(QObject *parent):QStyledItemDelegate(parent)
{
}

// 建立代理編輯元件
QWidget *QWIntSpinDelegate::createEditor(QWidget *parent,const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    Q_UNUSED(option);
    Q_UNUSED(index);

    QSpinBox *editor = new QSpinBox(parent);  // 建立一個QSpinBox
    editor->setFrame(false);                  // 設定為無邊框
    editor->setMinimum(0);
    editor->setMaximum(10000);
    return editor;                            // 返回此編輯器
}

// 從資料模型獲取資料,顯示到代理元件中
void QWIntSpinDelegate::setEditorData(QWidget *editor,const QModelIndex &index) const
{
    // 獲取資料模型的模型索引指向的單元的資料
    int value = index.model()->data(index, Qt::EditRole).toInt();

    QSpinBox *spinBox = static_cast<QSpinBox*>(editor);   // 強制型別轉換
    spinBox->setValue(value);                             // 設定編輯器的數值
}

// 將代理元件的資料,儲存到資料模型中
void QWIntSpinDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
{
    QSpinBox *spinBox = static_cast<QSpinBox*>(editor); // 強制型別轉換
    spinBox->interpretText();                           // 解釋資料,如果資料被修改後,就觸發訊號
    int value = spinBox->value();                       // 獲取spinBox的值
    model->setData(index, value, Qt::EditRole);         // 更新到資料模型
}

// 設定元件大小
void QWIntSpinDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    Q_UNUSED(index);
    editor->setGeometry(option.rect);
}

接著重寫介面floatspindelegate.cpp實現程式碼如上述部分一致,唯一的變化是元件變了,程式碼如下;

#include "floatspindelegate.h"
#include <QDoubleSpinBox>

QWFloatSpinDelegate::QWFloatSpinDelegate(QObject *parent):QStyledItemDelegate(parent)
{
}

QWidget *QWFloatSpinDelegate::createEditor(QWidget *parent,
   const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    Q_UNUSED(option);
    Q_UNUSED(index);

    QDoubleSpinBox *editor = new QDoubleSpinBox(parent);
    editor->setFrame(false);
    editor->setMinimum(0);
    editor->setDecimals(2);
    editor->setMaximum(10000);

    return editor;
}

void QWFloatSpinDelegate::setEditorData(QWidget *editor,
                      const QModelIndex &index) const
{
    float value = index.model()->data(index, Qt::EditRole).toFloat();
    QDoubleSpinBox *spinBox = static_cast<QDoubleSpinBox*>(editor);
    spinBox->setValue(value);
}

void QWFloatSpinDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
{
    QDoubleSpinBox *spinBox = static_cast<QDoubleSpinBox*>(editor);
    spinBox->interpretText();
    float value = spinBox->value();
    QString str=QString::asprintf("%.2f",value);

    model->setData(index, str, Qt::EditRole);
}

void QWFloatSpinDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    editor->setGeometry(option.rect);
}

最後重寫介面comboxdelegate.cpp其程式碼如下所示;

#include "comboxdelegate.h"
#include <QComboBox>

QWComboBoxDelegate::QWComboBoxDelegate(QObject *parent):QItemDelegate(parent)
{

}

QWidget *QWComboBoxDelegate::createEditor(QWidget *parent,const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    QComboBox *editor = new QComboBox(parent);

    editor->addItem("已婚");
    editor->addItem("未婚");
    editor->addItem("單身");

    return editor;
}

void QWComboBoxDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
{
    QString str = index.model()->data(index, Qt::EditRole).toString();

    QComboBox *comboBox = static_cast<QComboBox*>(editor);
    comboBox->setCurrentText(str);
}

void QWComboBoxDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
{
    QComboBox *comboBox = static_cast<QComboBox*>(editor);
    QString str = comboBox->currentText();
    model->setData(index, str, Qt::EditRole);
}

void QWComboBoxDelegate::updateEditorGeometry(QWidget *editor,const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    editor->setGeometry(option.rect);
}

將部件匯入到mainwindow.cpp主程式中,並將其通過ui->tableView->setItemDelegateForColumn(0,&intSpinDelegate);關聯部件到指定的table下標索引上面。

#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // 初始化模型資料
    model = new QStandardItemModel(4,6,this);      // 初始化4行,每行六列
    selection = new QItemSelectionModel(model);    // 關聯模型

    ui->tableView->setModel(model);
    ui->tableView->setSelectionModel(selection);

    // 新增表頭
    QStringList HeaderList;
    HeaderList << "序號" << "姓名" << "年齡" << "性別" << "婚否" << "薪資";
    model->setHorizontalHeaderLabels(HeaderList);

    // 批次新增資料
    QStringList DataList[3];
    QStandardItem *Item;

    DataList[0] << "1001" << "admin" << "24" << "男" << "已婚" << "4235.43";
    DataList[1] << "1002" << "guest" << "23" << "男" << "未婚" << "20000.21";
    DataList[2] << "1003" << "lucy" << "37" << "女" << "單身" << "8900.23";

    int Array_Length = DataList->length();                          // 獲取每個陣列中元素數
    int Array_Count = sizeof(DataList) / sizeof(DataList[0]);       // 獲取陣列個數

    for(int x=0; x<Array_Count; x++)
    {
        for(int y=0; y<Array_Length; y++)
        {
            // std::cout << DataList[x][y].toStdString().data() << std::endl;
            Item = new QStandardItem(DataList[x][y]);
            model->setItem(x,y,Item);
        }
    }

    // 為各列設定自定義代理元件
    // 0,4,5 代表第幾列 後面的函數則是使用哪個代理類的意思
    ui->tableView->setItemDelegateForColumn(0,&intSpinDelegate);
    ui->tableView->setItemDelegateForColumn(4,&comboBoxDelegate);
    ui->tableView->setItemDelegateForColumn(5,&floatSpinDelegate);
}

MainWindow::~MainWindow()
{
    delete ui;
}

執行後,序號部分與薪資部分將變成一個SpinBox元件,讀者可自行調節大小,如下圖;

而婚否欄位將被重寫成一個ComBoBox元件,這有助於讓使用者直接選擇一個狀態,如下圖;

完整案例下載