Qt中的ItemDelegate总结

本文是对这几年Model/View框架中使用Delegate的一个总结.

在Qt的Model/View框架中, 当我们需要在ItemView中对数据做编辑, 或者我们需要定制特殊的显示模式的时候, 标准的做法是使用ItemDelegate, 当然, 还可以使用QTableWidgetItem::setItemWidget()QTreeWidgetItem::setitemWidget(), 但是这两个函数的限制也很大, 基本上也只能用来显示静态内容.

Qt中的ItemView中可以通过定制Delegate来定制特定Item的编辑和显示功能. 做这件事情呢, 有几种方法:

使用工厂类QItemEditorFactor

  • 利用QItemEditorFactory, 注册指定数据类型使用的editor. 这种做法可能是最简单的, 但是同时也是使用限制最多的一种. 因为从接口上看, 我们完全看不到任何精细的控制途径. 但是对于简单场景, 这个已经足够了.

QItemEditorFactory是一个工厂类, 它负责管理QStyledItemDelegate, 而这种管理是通过工厂类内部维护的一个QItemEditorCreatorBase的集合来进行的, 而这些Creator类负责调用Delegate, 创建编辑器. 工厂类通过方法registerEditor()简化了这个过程, 当然, 简化了就增加了限制, 就只能通过Item里面的数据的实际类型来识别和分发了.

在Qt的例子coloreditorfactory里面演示了这种用法:

首先定义了一个Editor, ColorListEditor.

1
2
3
4
5
6
7
8
9
10
11
12
class ColorListEditor : public QComboBox
{
Q_OBJECT
Q_PROPERTY(QColor color READ color WRITE setColor USER true)
public:
ColorListEditor(QWidget *widget = nullptr);
public:
QColor color() const;
void setColor(const QColor &color);
private:
void populateList();
};

这里面最重要的是Q_PROPERTY这一行, 在其中, 最重要的是USER. 在Qt的QItemEditorCreatorBase的帮助信息里面是这样写的:
An editor should provide a user property for the data it edits. QItemDelagates can then access the property using Qt’s meta-object system to set and retrieve the editing data. A property is set as the user property with the USER keyword:

Q_PROPERTY(QColor color READ color WRITE setColor USER true)

同时, 它也指出:
If the editor does not provide a user property, it must return the name of the property from valuePropertyName(); delegates will then use the name to access the property. If a user property exists, item delegates will not call valuePropertyName().

然后, 如果要使用, 只需要注册它:

1
2
3
4
5
6
7
QItemEditorFactory *factory = new QItemEditorFactory;

QItemEditorCreatorBase *colorListCreator =
new QStandardItemEditorCreator<ColorListEditor>();

factory->registerEditor(QMetaType::QColor, colorListCreator);
QItemEditorFactory::setDefaultFactory(factory);

使用时, 我们需要给要它编辑的Item指定Data, 例如:

1
2
3
QTableWidgetItem *colorItem = new QTableWidgetItem;
colorItem->setData(Qt::DisplayRole, QColor("springgreen"));
...

这种做法最大的限制, 其实是, 这个defaultFactory()是全局的. 也就是说, 我一旦修改了, 那么应用程序的所有的ListView都会受到影响. 这个几乎可以肯定不是我们所期望的. 另外一个根本无解的问题是, 我们完全无法对取值做控制. 比如, 如果我们想限制上面的Color的取值该怎么办? 只能派生定制新的Color Editor. 这就完全不可行了.

所以, 其实这个就是个玩具而已.

指定Item使用哪个Delegate

另一种做法是实现Delegate, 并指定ItemView使用Delegate. 这是标准用法.

QAbstractItemView类提供了三个这种函数, 可以用来控制给所有Item/指定列/指定行使用Delegate.

  • setItemDelegate()
  • setItemDelegateForColumn()
  • setItemDelegateForRow()

比如, 下面的代码:

1
2
3
4
tableView.setModel(&model);

SpinBoxDelegate delegate;
tableView.setItemDelegate(&delegate);

至于实现自己的Delegate, 有两种选择. 按照手册说法, 当我们只需要呈现数据时, 使用QAbstractitemDelegate作为派生类的基类. 如果需要读写数据时, 使用QStyledItemDelegate更合适一些.

  • QAbstractItemDelegate. 需要实现paint()sizeHint()两个函数. 一般, 如果仅仅是为了呈现数据, 并不需要修改数据, 可以使用这个类作为派生的基类. 这两个函数中,

    • paint()实现的是绘制功能. 它的参数包括, QPainter的指针, 一个QStyleOptionViewItem的引用option, 以及当前数据的索引的引用. QModelIndex &. 绘图时则是标准的业务流程. 以painter->save()开始, 以painter->restore()结束.
    • sizeHint()则是返回要绘制的大小.
  • QStyledItemDelegate. 适用于简单的基于Widget控件的Delegate, 此时不需要实现这两个函数. 一般情况下, 当我们需要实现Editor的时候, 基本上都是从QStyledItemDelegate派生出自己的类, 并实现createEditor(), setEditorData(), setModelData()三个函数.

    • createEditor(): 当View中的item被修改时, 框架会调用createEditor()函数来创建一个编辑器, 它返回创建的Widget的指针, 并由框架管理其生命期.
    • setEditorData(): 用于从Model中获取要编辑的数据并在Widget中显示
    • setModelData(): 用于在完成编辑后从Delegate Widget中获取用户修改后的数据更新到Model中.
    • updateEditorGeometry(): 设置编辑器的尺寸. 当editor被创建, 其大小和尺寸被修改时. View提供的QStyleOptionViewItem参数中提供了这个值. 我们一般只需要将其设置给这个widget旧可以了.

如果我们在Delegate使用的仅仅是使用单个的标准控件, 那么这么就够了. 如果我们需要做定制的行为效果, 则需要同样派生paint()sizeHint()两个方法.

我们可以看到, 它们的基础都是Model, 即使是在Widget中使用, 背后也仍然是Model.

利用Model的最大的好处, 个人觉得, 是我们可以很方便地同时定义其他的一些属性. 例如, 比如, 我们使用QSpinBox作为Delegate, 最简单的实现是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
QWidget *SpinBoxDelegate::createEditor(QWidget *parent,
const QStyleOptionViewItem &/* option */,
const QModelIndex &/* index */) const
{
QSpinBox *editor = new QSpinBox(parent);
editor->setFrame(false);
editor->setMinimum(0);
editor->setMaximum(100);

return editor;
}
void SpinBoxDelegate::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);
}

可是, 如果我们需要针对不同的Item定义不同的取值范围该怎么办?
使用Model模式, 我们最简单的做法就是定义两个其他Role的属性过来, 在这里获取并设置给这个SpinBox里面就可以了.

更细粒度的控制

前面说过, 一个ItemView类有三个设置Delegate的函数:

  • setItemDelegate()
  • setItemDelegateForColumn()
  • setItemDelegateForRow()

分别用于指定所有Item, 某一行, 某一列Item所使用的Delegate.

那么, 如果是更特殊的情况, 比如我们需要让某一列的不同Item使用不同的Delegate, 该如何实现? 当然, 有一些线程的第三方控件可以做这个, 但是, 如果我们并不需要这么重度的东西, 或者说我们只是需要一个小编辑, 而不是完整的Property Sheet, 该如何?

我倒是真的没有找到类似的接口. 不过, 想到了一个变通的法子.

我写一个Delegate的Wraper, 它根据特定的位置来决定.

例如, PropEditDelegate, 我们可以这样实现它的createEditor函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
QWidget *PropEditDelegate::createEditor(QWidget *parent,
const QStyleOptionViewItem &option,
const QModelIndex &index) const

{
auto row = index.row;
auto col = index.col;
if( row == xxx){
auto editor = new AEditor(parent);
...
return editor;
}
else if (col== xxx){
auto editor = new BEditor(...);
...
}
else if(index.data().canConvert<ZZZ>())
{
...
}
else{
return QStyledItemDelegate::createEditor(parent, option, index);
}
}

在这里, canCovert()的使用, 在本质上我们是使用QVariant的类型系统来识别QVariant里面存储的数据的类型. 除了canConvert<>()之外, 我们还有其他的OVariant的类型系统方法来使用:

  • typeId()
  • userType()
  • typeName()
  • metaType()(Qt6)

在这种模式下, 我们根据特定的位置, 根据特定的数据类型做dispatch. 这或许会是一种可行的方法. 当然, 这种耦合是很紧密的了. 要从别的地方考虑进行解耦处理.