文章目录

  • 高级主题
  • 什么是委托(Delegates)
  • 星星评分例子
  • StarDelegate定义
  • StarDelegate 类实现
  • StarEditor 类定义
  • StarEditor 类实现
  • StarRating 定义
  • StarRating 实现
  • main()函数
  • 总结


高级主题

什么是委托(Delegates)

在QListView、QTableView或QTreeView 中显示数据时,各个item由委托绘制。此外,当用户开始编辑一个item时(例如,通过双击该item),代理会提供一个编辑器控件,该控件在进行编辑时放置在该item的顶部。提供展示(presentation)和编辑(edit)服务的组件称为委托

委托是QAbstractItemDelegate 的子类,Qt提供的QStyledItemDelegate可以处理最常见的数据类型,如int和QString。

当我们想使用一个与众不同的编辑器,或者想把数据显示为图形,我们就可能需要使用委托。

星星评分例子

在此示例中,我们将看到如何实现自定义委托来呈现和编辑“星级”数据类型,该数据类型可以存储诸如“5 星中的 2 颗”之类的值。

该示例由以下类组成:

  • StarRating是自定义数据类型。它存储以星级表示的评级,例如“5 星中的 2 颗”或“6 颗星中的 5 颗”。
  • StarDelegate继承QStyledItemDelegate并提供支持StarRating类型数据
  • StarEditor继承QWidget并用于StarDelegate让用户使用鼠标编辑星级。

StarDelegate定义

class StarDelegate : public QStyledItemDelegate
{
    Q_OBJECT
public:
    using QStyledItemDelegate::QStyledItemDelegate;

    void paint(QPainter *painter, const QStyleOptionViewItem &option,
               const QModelIndex &index) const override;
    QSize sizeHint(const QStyleOptionViewItem &option,
                   const QModelIndex &index) const override;
    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;

private slots:
    void commitAndCloseEditor();
};

所有函数均重写了QStyledItemDelegate的虚函数,以提供自定义渲染和编辑

StarDelegate 类实现

void StarDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
                         const QModelIndex &index) const
{
    if (index.data().canConvert<StarRating>()) {
        StarRating starRating = qvariant_cast<StarRating>(index.data());

        if (option.state & QStyle::State_Selected)
            painter->fillRect(option.rect, option.palette.highlight());

        starRating.paint(painter, option.rect, option.palette,
                         StarRating::EditMode::ReadOnly);
    } else {
        QStyledItemDelegate::paint(painter, option, index);
    }
}

每当视图需要重新绘制item时,都会调用paint()。如果 item 中存储的数据是StarRating,我们自己绘制它;否则,我们让QStyledItemDelegate为我们绘制。这确保了StarDelegate可以处理最常见的数据类型。

option.state & QStyle::State_Selected语句表示当星级评价的item处于被选中的状态时,会绘制被选中的高亮背景(和其他被选中的item一样)。

QWidget *StarDelegate::createEditor(QWidget *parent,
                                    const QStyleOptionViewItem &option,
                                    const QModelIndex &index) const

{
    if (index.data().canConvert<StarRating>()) {
        StarEditor *editor = new StarEditor(parent);
        connect(editor, &StarEditor::editingFinished,
                this, &StarDelegate::commitAndCloseEditor);
        return editor;
    }
    return QStyledItemDelegate::createEditor(parent, option, index);
}

当用户开始编辑item时会调用 createEditor() 函数。commitAndCloseEditor实现如下:

void StarDelegate::commitAndCloseEditor()
{
    StarEditor *editor = qobject_cast<StarEditor *>(sender());
    emit commitData(editor);
    emit closeEditor(editor);
}

在创建编辑器时会调用 setEditorData() 函数以使用模型中的数据对其进行初始化:

void StarDelegate::setEditorData(QWidget *editor,
                                 const QModelIndex &index) const
{
    if (index.data().canConvert<StarRating>()) {
        StarRating starRating = qvariant_cast<StarRating>(index.data());
        StarEditor *starEditor = qobject_cast<StarEditor *>(editor);
        starEditor->setStarRating(starRating);
    } else {
        QStyledItemDelegate::setEditorData(editor, index);
    }
}

编辑完成后,调用 setModelData() 函数将数据从编辑器提交到模型

void StarDelegate::setModelData(QWidget *editor, QAbstractItemModel *model,
                                const QModelIndex &index) const
{
    if (index.data().canConvert<StarRating>()) {
        StarEditor *starEditor = qobject_cast<StarEditor *>(editor);
        model->setData(index, QVariant::fromValue(starEditor->starRating()));
    } else {
        QStyledItemDelegate::setModelData(editor, model, index);
    }
}

sizeHint() 函数返回item的首选大小.

QSize StarDelegate::sizeHint(const QStyleOptionViewItem &option,
                             const QModelIndex &index) const
{
    if (index.data().canConvert<StarRating>()) {
        StarRating starRating = qvariant_cast<StarRating>(index.data());
        return starRating.sizeHint();
    }
    return QStyledItemDelegate::sizeHint(option, index);
}

StarEditor 类定义

class StarEditor : public QWidget
{
    Q_OBJECT
public:
    StarEditor(QWidget *parent = nullptr);

    QSize sizeHint() const override;
    void setStarRating(const StarRating &starRating) {
        myStarRating = starRating;
    }
    StarRating starRating() { return myStarRating; }

signals:
    void editingFinished();

protected:
    void paintEvent(QPaintEvent *event) override;
    void mouseMoveEvent(QMouseEvent *event) override;
    void mouseReleaseEvent(QMouseEvent *event) override;

private:
    int starAtPosition(int x) const;

    StarRating myStarRating;
};

StarEditor 类实现

StarEditor::StarEditor(QWidget *parent)
    : QWidget(parent)
{
    setMouseTracking(true);
    setAutoFillBackground(true);
}

我们打开QWidget的自动填充背景功能以获得不透明背景。(如果没有调用,视图的背景会穿透到编辑器中。)

void StarEditor::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    myStarRating.paint(&painter, rect(), palette(),
                       StarRating::EditMode::Editable);
}
void StarEditor::mouseMoveEvent(QMouseEvent *event)
{
    const int star = starAtPosition(event->x());

    if (star != myStarRating.starCount() && star != -1) {
        myStarRating.setStarCount(star);
        update();
    }
    QWidget::mouseMoveEvent(event);
}

我们调用 QWidget::update() 来强制重绘。

void StarEditor::mouseReleaseEvent(QMouseEvent *event)
{
    emit editingFinished();
    QWidget::mouseReleaseEvent(event);
}
int StarEditor::starAtPosition(int x) const
{
    const int star = (x / (myStarRating.sizeHint().width()
                           / myStarRating.maxStarCount())) + 1;
    if (star <= 0 || star > myStarRating.maxStarCount())
        return -1;

    return star;
}

根据鼠标x轴坐标计算星级。后面的判断语句是当x坐标为负数,或者计算的星级大于最大星级时返回-1,此时StarEditor::mouseMoveEvent内只会调用QWidget::mouseMoveEvent。

StarRating 定义

class StarRating
{
public:
    enum class EditMode { Editable, ReadOnly };

    explicit StarRating(int starCount = 1, int maxStarCount = 5);

    void paint(QPainter *painter, const QRect &rect,
               const QPalette &palette, EditMode mode) const;
    QSize sizeHint() const;
    int starCount() const { return myStarCount; }
    int maxStarCount() const { return myMaxStarCount; }
    void setStarCount(int starCount) { myStarCount = starCount; }
    void setMaxStarCount(int maxStarCount) { myMaxStarCount = maxStarCount; }

private:
    QPolygonF starPolygon;
    QPolygonF diamondPolygon;
    int myStarCount;
    int myMaxStarCount;
};

Q_DECLARE_METATYPE(StarRating)

StarRating类表示评分星级,包含当前星级和最大星级,此外它还能绘制星星。

StarRating 实现

StarRating::StarRating(int starCount, int maxStarCount)
    : myStarCount(starCount),
      myMaxStarCount(maxStarCount)
{
    starPolygon << QPointF(1.0, 0.5);
    for (int i = 1; i < 5; ++i)
        starPolygon << QPointF(0.5 + 0.5 * std::cos(0.8 * i * 3.14),
                               0.5 + 0.5 * std::sin(0.8 * i * 3.14));

    diamondPolygon << QPointF(0.4, 0.5) << QPointF(0.5, 0.4)
                   << QPointF(0.6, 0.5) << QPointF(0.5, 0.6)
                   << QPointF(0.4, 0.5);
}

设置星级,初始化用于绘制星星(starPolygon)和菱形(diamondPolygon)的多边形。

void StarRating::paint(QPainter *painter, const QRect &rect,
                       const QPalette &palette, EditMode mode) const
{
    painter->save();

    painter->setRenderHint(QPainter::Antialiasing, true);
    painter->setPen(Qt::NoPen);
    painter->setBrush(mode == EditMode::Editable ?
                          palette.highlight() :
                          palette.windowText());

    const int yOffset = (rect.height() - PaintingScaleFactor) / 2;
    painter->translate(rect.x(), rect.y() + yOffset);
    painter->scale(PaintingScaleFactor, PaintingScaleFactor);

    for (int i = 0; i < myMaxStarCount; ++i) {
        if (i < myStarCount)
            painter->drawPolygon(starPolygon, Qt::WindingFill);
        else if (mode == EditMode::Editable)
            painter->drawPolygon(diamondPolygon, Qt::WindingFill);
        painter->translate(1.0, 0.0);
    }

    painter->restore();
}

其中PaintingScaleFactor是个常数,源码中为20,用于控制星星显示的大小。

QSize StarRating::sizeHint() const
{
    return PaintingScaleFactor * QSize(myMaxStarCount, 1);
}

返回星星绘制区域的大小

main()函数

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

    QTableWidget tableWidget(4, 4);
    tableWidget.setItemDelegate(new StarDelegate);
    tableWidget.setEditTriggers(QAbstractItemView::DoubleClicked
                                | QAbstractItemView::SelectedClicked);
    tableWidget.setSelectionBehavior(QAbstractItemView::SelectRows);
    tableWidget.setHorizontalHeaderLabels({"Title", "Genre", "Artist", "Rating"});

    populateTableWidget(&tableWidget);

    tableWidget.resizeColumnsToContents();
    tableWidget.resize(500, 300);
    tableWidget.show();

    return app.exec();
}

主函数创建了一个QTableWidget,设置委托为StarDelegate,并设置了编辑触发的方式
populateTableWidget填充表格,用于demo效果展示。

void populateTableWidget(QTableWidget *tableWidget)
{
    static constexpr struct {
        const char *title;
        const char *genre;
        const char *artist;
        int rating;
    } staticData[] = {
        { "Mass in B-Minor", "Baroque", "J.S. Bach", 5 },
    ...
        { nullptr, nullptr, nullptr, 0 }
    };

    for (int row = 0; staticData[row].title != nullptr; ++row) {
        QTableWidgetItem *item0 = new QTableWidgetItem(staticData[row].title);
        QTableWidgetItem *item1 = new QTableWidgetItem(staticData[row].genre);
        QTableWidgetItem *item2 = new QTableWidgetItem(staticData[row].artist);
        QTableWidgetItem *item3 = new QTableWidgetItem;
        item3->setData(0,
                       QVariant::fromValue(StarRating(staticData[row].rating)));

        tableWidget->setItem(row, 0, item0);
        tableWidget->setItem(row, 1, item1);
        tableWidget->setItem(row, 2, item2);
        tableWidget->setItem(row, 3, item3);
    }
}

总结

如果想要自定义item的样式,可以通过视图的setItemDelegate()方法,使用自定义委托替换默认委托。通过创建一个继承自QStyledItemDelegate的类来编写自定义委托,对没有编辑功能的item,我们只需重写两个方法:

  1. void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index)。绘制item
  2. QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index)。告知item的大小

如果要提供item编辑能力,还需要重写以下方法:

  1. QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index)。提供编辑器
  2. void setEditorData(QWidget *editor, const QModelIndex &index)。初始化编辑器内数据
  3. void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index)。将编辑器内数据保存到model中

参考链接:

  1. https://doc.qt.io/qt-5/qtwidgets-itemviews-stardelegate-example.html