QStyledItemDelegate基本使用:单元格数据渲染与编辑

前言

QStyledItemDelegate 继承自 QAbstractItemDelegate,主要用于为 Model-View 中的数据项提供显示和编辑功能。QAbstractItemDelegate 有两个字类,QStyledItemDelegate 和 QItemDelegate,根据文档描述 QStyledItemDelegate 使用当前样式来绘制。根据我的测试,两者无论是在 QStyle 主题还是样式表的支持上,最终显示效果是差不多的。之所以用 QStyledItemDelegate ,是根据文档的建议,在实现自定义 delegate 或使用 Qt 样式表时使用该类作为基类。

本文以 QTableView 来配合讲 QStyledItemDelegate 单元格数据渲染与编辑相关接口的相关操作。

完整代码(DemoStyledDelegate 部分):https://github.com/gongjianbo/QtTableViewDelegate 

QStyledItemDelegate 的默认实现

我们先了解下 QStyledItemDelegate 的默认实现是如何做的。

对于渲染,有两个相关接口:

void paint(QPainter *painter,const QStyleOptionViewItem &option, const QModelIndex &index) const override;
QSize sizeHint(const QStyleOptionViewItem &option,const QModelIndex &index) const override;

从接口名字就知道大概的用处,paint 就是绘制单元格,sizeHint  返回该单元格所需的尺寸大小,一般我们重新实现 paint 就足够了。

paint 中主要是调用了一个成员函数 initStyleOption 来读取 model 中的数据,并使用 QStyle 的规则来绘制:

void QStyledItemDelegate::paint(QPainter *painter,
        const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    Q_ASSERT(index.isValid());

    QStyleOptionViewItem opt = option;
    initStyleOption(&opt, index);

    const QWidget *widget = QStyledItemDelegatePrivate::widget(option);
    QStyle *style = widget ? widget->style() : QApplication::style();
    style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, widget);
}

void QStyledItemDelegate::initStyleOption(QStyleOptionViewItem *option,
                                         const QModelIndex &index) const
{
    //... ...
    //出于篇幅原因,前面的颜色字体等删了,只保留了文本的读取
    value = index.data(Qt::DisplayRole);
    if (value.isValid() && !value.isNull()) {
        option->features |= QStyleOptionViewItem::HasDisplay;
        option->text = displayText(value, option->locale);
    }
    //... ...
}

view 中的编辑功能默认实现下是在该单元格处于编辑状态时才会实例化编辑组件,相关接口如下:

QWidget *createEditor(QWidget *parent,const QStyleOptionViewItem &option,const QModelIndex &index) const override;
void setEditorData(QWidget *editor, const QModelIndex &index) const override;
void setModelData(QWidget *editor,QAbstractItemModel *model,const QModelIndex &index) const override;
void updateEditorGeometry(QWidget *editor,const QStyleOptionViewItem &option,const QModelIndex &index) const override;

当双击进入编辑状态时,调用 createEditor 在目标位置创建一个编辑组件,通过 setEditorData 更新编辑组件的数据,编辑完成后调用 setModelData 将结果设置回 model 中。 由于源码太长,只看下创建是如何实现的:

QWidget *QStyledItemDelegate::createEditor(QWidget *parent,
                                     const QStyleOptionViewItem &,
                                     const QModelIndex &index) const
{
    Q_D(const QStyledItemDelegate);
    if (!index.isValid())
        return nullptr;
    return d->editorFactory()->createEditor(index.data(Qt::EditRole).userType(), parent);
}

QWidget *QDefaultItemEditorFactory::createEditor(int userType, QWidget *parent) const
{
    switch (userType) {
#if QT_CONFIG(combobox)
    case QMetaType::Bool: {
        QBooleanComboBox *cb = new QBooleanComboBox(parent);
        cb->setFrame(false);
        cb->setSizePolicy(QSizePolicy::Ignored, cb->sizePolicy().verticalPolicy());
        return cb; }
#endif
#if QT_CONFIG(spinbox)
    case QMetaType::UInt: {
        QSpinBox *sb = new QUIntSpinBox(parent);
        sb->setFrame(false);
        sb->setMinimum(0);
        sb->setMaximum(INT_MAX);
        sb->setSizePolicy(QSizePolicy::Ignored, sb->sizePolicy().verticalPolicy());
        return sb; }
    case QMetaType::Int: {
        QSpinBox *sb = new QSpinBox(parent);
        sb->setFrame(false);
        sb->setMinimum(INT_MIN);
        sb->setMaximum(INT_MAX);
        sb->setSizePolicy(QSizePolicy::Ignored, sb->sizePolicy().verticalPolicy());
        return sb; }
#endif
#if QT_CONFIG(datetimeedit)
    case QMetaType::QDate: {
        QDateTimeEdit *ed = new QDateEdit(parent);
        ed->setFrame(false);
        return ed; }
    case QMetaType::QTime: {
        QDateTimeEdit *ed = new QTimeEdit(parent);
        ed->setFrame(false);
        return ed; }
    case QMetaType::QDateTime: {
        QDateTimeEdit *ed = new QDateTimeEdit(parent);
        ed->setFrame(false);
        return ed; }
#endif
#if QT_CONFIG(label)
    case QMetaType::QPixmap:
        return new QLabel(parent);
#endif
#if QT_CONFIG(spinbox)
    case QMetaType::Double: {
        QDoubleSpinBox *sb = new QDoubleSpinBox(parent);
        sb->setFrame(false);
        sb->setMinimum(-DBL_MAX);
        sb->setMaximum(DBL_MAX);
        sb->setSizePolicy(QSizePolicy::Ignored, sb->sizePolicy().verticalPolicy());
        return sb; }
#endif
#if QT_CONFIG(lineedit)
    case QMetaType::QString:
    default: {
        // the default editor is a lineedit
        QExpandingLineEdit *le = new QExpandingLineEdit(parent);
        le->setFrame(le->style()->styleHint(QStyle::SH_ItemView_DrawDelegateFrame, nullptr, le));
        if (!le->style()->styleHint(QStyle::SH_ItemView_ShowDecorationSelected, nullptr, le))
            le->setWidgetOwnsGeometry(true);
        return le; }
#else
    default:
        break;
#endif
    }
    return nullptr;
}

delegate 这些接口主要是在 view 中回调的。

自定义 paint 接口

如果 delegate 默认实现不支持我们需要的数据类型的绘制,或者想要自定义某项的绘制,我们可以重新实现 paint 接口。

我写了个简易的 demo 来展示基本操作。首先, model 提供的数据我使用了 bool、int、double、stringlist 和 string 五种,我自定义了一些规则来渲染,比如 double 我希望指定小数位数,stringlist 配合index 显示指定的字符串等。(为了降低 demo 的复杂度,model 我使用的 QStandardItemModel,后期做完整的 demo 时再自定义 model;此外,delegate 可以指定给行列或者所有 item ,我这里采用的对所有 item 进行设置的方式,在内部判断数据类型或者行列来区别处理;此外,如果只想对文本的格式化自定义,可以重写 QStyledItemDelegate 的 displayText 接口) 

实现效果:

先看 model 部分:

void DemoStyledDelegate::initModel()
{
    //QStandardItemModel类提供用于存储自定义数据的通用模型
    model = new QStandardItemModel(this);

    //模拟一份固定的数据表
    //设置列
    const int col_count=5;
    model->setColumnCount(col_count);
    model->setHeaderData(0,Qt::Horizontal, "Bool");
    model->setHeaderData(1,Qt::Horizontal, "Int");
    model->setHeaderData(2,Qt::Horizontal, "Double");
    model->setHeaderData(3,Qt::Horizontal, "List");
    model->setHeaderData(4,Qt::Horizontal, "String");

    //设置行
    const int row_count=10;
    model->setRowCount(row_count);

    //设置数据
    for(int row=0;row<row_count;row++)
    {
        for(int col=0;col<col_count;col++)
        {
            QStandardItem *new_item=new QStandardItem;

            switch(col)
            {
            default: break;
                //checkbox bool
            case 0:
                new_item->setData(row%2?true:false,Qt::DisplayRole);
                break;
                //spinbox int
            case 1:
                new_item->setData(row,Qt::DisplayRole);
                break;
                //doublespinbox double
            case 2:
                new_item->setData(row*3.1415926,Qt::DisplayRole);
                break;
                //combobox list
            case 3:
                new_item->setData(QStringList{"A","B","C"},Qt::DisplayRole);
                //这里使用userrole来保存列表的下标
                new_item->setData(0,Qt::UserRole);
                break;
                //linedit string
            case 4:
                new_item->setData(QString("String %1").arg(row),Qt::DisplayRole);
                break;
            }
            model->setItem(row, col, new_item);
        }
    }

    //view会根据model提供的数据来渲染
    ui->tableView->setModel(model);
}

然后是 delegate paint 接口的实现,数据是通过 model 的 data 接口获取的:

void MyStyledDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    //注意,此时index是logicIndex(model坐标),我们可以通过拖拽表头项交换列来测试
    Q_ASSERT(index.isValid());
    //QStyle会根据option的属性进行绘制,我们也可以不使用QStyle的规则,完全自定义
    QStyleOptionViewItem opt = option;
    //去掉焦点 setFocusPolicy(Qt::NoFocus);
    opt.state &= ~QStyle::State_HasFocus;
    //参照源码实现了自己的initStyleOption
    initMyStyleOption(&opt, index);

    const QWidget *widget = opt.widget;
    QStyle *style = widget ? widget->style() : QApplication::style();
    style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, widget);
}

void MyStyledDelegate::initMyStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const
{
    //字体
    QVariant value = index.data(Qt::FontRole);
    if (value.isValid() && !value.isNull()) {
        option->font = qvariant_cast<QFont>(value).resolve(option->font);
        option->fontMetrics = QFontMetrics(option->font);
    }
    //对齐方式
    //value = index.data(Qt::TextAlignmentRole);
    //if (value.isValid() && !value.isNull())
    //    option->displayAlignment = Qt::Alignment(value.toInt());
    //也可以直接全部指定为居中对齐
    option->displayAlignment = Qt::AlignCenter;
    //前景色
    value = index.data(Qt::ForegroundRole);
    if (value.canConvert<QBrush>())
        option->palette.setBrush(QPalette::Text, qvariant_cast<QBrush>(value));
    option->index = index;
    //value = index.data(Qt::CheckStateRole); 未使用,暂略
    //value = index.data(Qt::DecorationRole); 未使用,暂略
    //文本
    //value = index.data(Qt::DisplayRole);
    //if (value.isValid() && !value.isNull()) {
    option->features |= QStyleOptionViewItem::HasDisplay;
    option->text = getDisplayText(index); //displayText(value, option->locale);
    //}
    //背景色
    option->backgroundBrush = qvariant_cast<QBrush>(index.data(Qt::BackgroundRole));
    // disable style animations for checkboxes etc. within itemviews (QTBUG-30146)
    option->styleObject = nullptr;
}

QString MyStyledDelegate::getDisplayText(const QModelIndex &index) const
{
    //注意,此时index是logicIndex(model坐标),我们可以通过拖拽表头项交换列来测试
    const QVariant value = index.data(Qt::DisplayRole);
    //我们可以根据variant的type或者index的行列来特殊处理
    switch(index.column())
    {
    default: break;
    case 0://bool
        return value.toBool()?"True":"False";
    case 1://int
        return QString::number(value.toInt());
    case 2://double
        return QString::number(value.toDouble(),'f',3);
    case 3://list
    {
        const QStringList str_list=value.toStringList();
        //这里使用userrole来保存列表的下标
        const int str_index=index.data(Qt::UserRole).toInt();
        if(str_index>=0&&str_index<str_list.count())
            //给字符串加个括号
            return QString("[ %1 ]").arg(str_list.at(str_index));
    }
        break;
    case 4://string
        //给字符串加个括号
        return QString("[ %1 ]").arg(value.toString());
    }
    return QString();
}

这里只是简单的对单元格文本格式化做了处理,一些颜色样式之类的会在后期的博文进行演示。

自定义编辑接口

前面我们了解了  QStyledItemDelegate 的默认实现,像 QStringList 这种数据他是没处理的,我们可以实例化一个 QComboBox 来进行编辑,浮点数编辑框我们也可以指定小数位数为三位等。

实现我参照了实例 spinboxdelegate :

QWidget *MyStyledDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    Q_UNUSED(option)
    //参照了实例spinboxdelegate
    if (!index.isValid())
        return nullptr;

    switch(index.column())
    {
    default: break;
    case 0://bool
    {
        QComboBox *editor=new QComboBox(parent);
        editor->setFrame(false);
        editor->addItems({"True","False"});
        //editor->setCurrentIndex(index.data(Qt::DisplayRole).toBool()?0:1);
        return editor;
    }
    case 1://int
    {
        QSpinBox *editor=new QSpinBox(parent);
        editor->setFrame(false);
        editor->setMinimum(-10000);
        editor->setMaximum(10000);
        //editor->setValue(index.data(Qt::DisplayRole).toInt());
        return editor;
    }
    case 2://double
    {
        QDoubleSpinBox *editor=new QDoubleSpinBox(parent);
        editor->setFrame(false);
        editor->setMinimum(-10000);
        editor->setMaximum(10000);
        editor->setDecimals(3);
        //editor->setValue(index.data(Qt::DisplayRole).toDouble());
        return editor;
    }
    case 3://list
    {
        QComboBox *editor=new QComboBox(parent);
        editor->setFrame(false);
        const QStringList str_list=index.data(Qt::DisplayRole).toStringList();
        editor->addItems(str_list);
        //这里使用userrole来保存列表的下标
        //const int str_index=index.data(Qt::UserRole).toInt();
        //if(str_index>=0&&str_index<str_list.count())
        //    editor->setCurrentIndex(str_index);
        return editor;
    }
    case 4://string
    {
        QLineEdit *editor=new QLineEdit(parent);
        editor->setFrame(false);
        //editor->setText(index.data(Qt::DisplayRole).toString());
        return editor;
    }
    }
    return nullptr;
}

void MyStyledDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
{
    switch(index.column())
    {
    default: break;
    case 0://bool
    {
        QComboBox *box = static_cast<QComboBox*>(editor);
        box->setCurrentIndex(index.data(Qt::DisplayRole).toBool()?0:1);
    }break;
    case 1://int
    {
        QSpinBox *spinBox = static_cast<QSpinBox*>(editor);
        spinBox->setValue(index.data(Qt::DisplayRole).toInt());
    }break;
    case 2://double
    {
        QDoubleSpinBox *spinBox = static_cast<QDoubleSpinBox*>(editor);
        spinBox->setValue(index.data(Qt::DisplayRole).toDouble());
    }break;
    case 3://list
    {
        QComboBox *box = static_cast<QComboBox*>(editor);
        //这里使用userrole来保存列表的下标
        const int str_index=index.data(Qt::UserRole).toInt();
        if(str_index>=0&&str_index<box->count())
            box->setCurrentIndex(str_index);
    }break;
    case 4://string
    {
        QLineEdit *edit = static_cast<QLineEdit*>(editor);
        edit->setText(index.data(Qt::DisplayRole).toString());
    }break;
    }
}

void MyStyledDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
{
    switch(index.column())
    {
    default: break;
    case 0://bool
    {
        QComboBox *box = static_cast<QComboBox*>(editor);
        model->setData(index,box->currentIndex()==0?true:false,Qt::EditRole);
    }break;
    case 1://int
    {
        QSpinBox *spinBox = static_cast<QSpinBox*>(editor);
        model->setData(index,spinBox->value(),Qt::DisplayRole);
    }break;
    case 2://double
    {
        QDoubleSpinBox *spinBox = static_cast<QDoubleSpinBox*>(editor);
        model->setData(index,spinBox->value(),Qt::DisplayRole);
    }break;
    case 3://list
    {
        QComboBox *box = static_cast<QComboBox*>(editor);
        //这里使用userrole来保存列表的下标
        model->setData(index,box->currentIndex(),Qt::UserRole);
    }break;
    case 4://string
    {
        QLineEdit *edit = static_cast<QLineEdit*>(editor);
        model->setData(index,edit->text(),Qt::DisplayRole);
    }break;
    }
}

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

可以看到,这里用到了 model 的 data 和 setData 接口,当然,我们也可以自定义接口,然后转换下类型进行调用。

上面这种生成编辑组件的方式只会在触发编辑状态时(如双击)才会实例化组件,但有时需求是编辑组件一直显示,这种我在网上百度看了并没有很好的解决方法,只能 paint 里用 QStyle 的 drawControl,但是支持的组件有限(毕竟 Qt 的 view 只在单元格处于可见区域时才进行渲染,所以没有提供这项功能,view 只有一个 setIndexWidget 的接口,用于展示静态数据)。

(2020-10-15 补充)如果 model view delegate 我们是封装在一起的,在设置数据后,可以用 view 的 openPersistentEditor 接口使某个 index 的 editor 一直处于编辑状态,直到调用 closePersistentEditor 关闭。

参考

官方文档:https://doc.qt.io/qt-5/model-view-programming.html

官方文档:https://doc.qt.io/qt-5/qabstractitemdelegate.html

官方文档:https://doc.qt.io/qt-5/qstyleditemdelegate.html

QStyledItemDelegate可以通过重载paint()方法来绘制合并的单元格。在paint()方法中,可以通过QStyleOptionViewItem类的rect属性来获取单元格的位置和大小信息,通过QTableView或QTableWidget的span()方法来获取单元格的合并信息,从而确定应该在哪些单元格上绘制数据。具体实现可以参考下面的示例代码: ```python def paint(self, painter, option, index): # 获取单元格的位置和大小信息 rect = option.rect # 获取单元格的行列信息 row = index.row() column = index.column() # 获取单元格的合并信息 table = self.parent() if table: span = table.span(row, column) if span: # 如果单元格被合并,则根据合并信息调整绘制的位置和大小 row, column, rowSpan, columnSpan = span rect = table.visualRect(table.model().index(row, column)) for i in range(1, rowSpan): rect = rect.united(table.visualRect(table.model().index(row + i, column))) for j in range(1, columnSpan): rect = rect.united(table.visualRect(table.model().index(row, column + j))) # 在单元格上绘制数据 self.drawBackground(painter, option, index) self.drawDisplay(painter, option, rect, index.data(Qt.DisplayRole)) self.drawFocus(painter, option, rect) ``` 在上述代码中,首先获取单元格的位置和大小信息,然后获取单元格的行列信息和合并信息,通过QRect类的united()方法来调整绘制的位置和大小,最后调用drawBackground()、drawDisplay()和drawFocus()方法来绘制背景、数据和焦点。需要注意的是,由于合并单元格可能会导致单元格大小不一致,因此在绘制时需要对绘制位置和大小进行适当的调整。
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龚建波

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值