前言
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