星号代理示例

星号代理示例展示了如何创建一个可以自行绘制且支持编辑的代理。

The Star Delegate Example

QListViewQTableViewQTreeView中显示数据时,单个项目是由代理绘制的。此外,当用户开始编辑项目(例如通过双击项目)时,代理提供编辑小部件,并在编辑期间将其放置在项目上方。

代理是QAbstractItemDelegate的子类。Qt提供了继承自QAbstractItemDelegateQStyledItemDelegate,它处理最常见的数据类型(特别是intQString)。如果我们需要支持自定义数据类型,或者想要定制现有数据类型的渲染或编辑,我们可以继承QAbstractItemDelegateQStyledItemDelegate。有关代理的更多信息,请参阅代理类,如果您需要关于Qt的模型/视图架构(包括代理)的高级介绍,请参阅模型/视图编程

在本示例中,我们将看到如何实现一个自定义代理以渲染和编辑“星号评分”数据类型,该数据类型可以存储诸如“5星中1星”之类的值。

示例包含以下类

  • StarRating是自定义数据类型。它存储以星号表示的评分,例如“5星中2星”或“6星中5星”。
  • StarDelegate继承自QStyledItemDelegate并提供了对StarRating的支持(包括QStyledItemDelegate已处理的数据类型)。
  • StarEditor继承自QWidget,并由StarDelegate用于使用鼠标允许用户编辑星号评分。

为了展示StarDelegate的功能,我们将填充一些数据到QTableWidget中并将其上的代理安装起来。

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类实现

paint()函数是重新实现自QStyledItemDelegate的,并在视图需要重绘项目时被调用。

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);
    }

函数针对每个由模型中的 QModelIndex 对象表示的项目调用一次。如果项目中的数据是 StarRating,我们将自己绘制它;否则,我们让 QStyledItemDelegate 为我们绘制。这保证了 StarDelegate 可以处理最常见的数据类型。

如果项目是 StarRating,当项目被选中时,我们绘制背景,并使用 StarRating::paint() 绘制项目,我们将在后面讨论这个函数。

由于出现在 starrating.h 中的宏 Q_DECLARE_METATYPE(),StartRating 可以存储在 QVariant 中。关于这个功能,我们将稍后讨论。

在用户开始编辑项目时调用 createEditor() 函数

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);
}

如果项目是 StarRating,我们创建一个 StarEditor,并将其 editingFinished() 信号连接到我们的 commitAndCloseEditor() 槽,这样我们就可以在编辑器关闭时更新模型。

以下是 commitAndCloseEditor() 的实现

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

用户完成编辑后,我们发出 commitData() 和 closeEditor (两者都在 QAbstractItemDelegate 中声明),以告诉模型有编辑过的数据和通知视图编辑器不再需要。

当创建了编辑器时调用 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);
    }
}

我们只是在编辑器上调用 setStarRating()

调用 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() 函数返回项目的首选大小

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);
}

我们简单地转发了对 StarRating 的调用。

StarEditor 类定义

在实现 StarDelegate 时使用了 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;
};

该类允许用户通过将鼠标移动到编辑器上来编辑 StarRating。当用户点击编辑器时,它发出 editingFinished() 信号。

QWidget 中重实现了受保护的功能来处理鼠标和绘制事件。私有函数 starAtPosition() 是一个辅助函数,它返回鼠标指针下星星的编号。

StarEditor 类实现

让我们从构造函数开始

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

我们在小部件上启用 鼠标跟踪,这样我们可以在用户不按任何鼠标按钮时跟踪光标。我们还打开 QWidget自动填充背景 功能以获得不透明的背景。(如果不调用这个函数,视图的背景将通过编辑器透过来。)

重新实现了从 QWidgetpaintEvent() 函数

void StarEditor::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    myStarRating.paint(&painter, rect(), palette(),
                       StarRating::EditMode::Editable);
}

我们简单地调用 StarRating::paint() 来绘制星星,就像我们在实现 StarDelegate 时所做的那样。

void StarEditor::mouseMoveEvent(QMouseEvent *event)
{
    const int star = starAtPosition(event->position().toPoint().x());

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

在鼠标事件处理程序中,我们调用私有数据成员 myStarRating 上的 setStarCount() 来反映当前光标位置,并调用 QWidget::update() 来强制重绘。

void StarEditor::mouseReleaseEvent(QMouseEvent *event)
{
    emit editingFinished();
    QWidget::mouseReleaseEvent(event);
}

当用户释放鼠标按钮时,我们简单地发出 editingFinished() 信号。

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;
}

函数 starAtPosition() 使用基本的线性代数来确定鼠标指针下面的哪颗星星。

星评级类定义

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 类表示评分为星星的数量。除了持有数据外,它还能在 QPaintDevice 上绘制星星,在这个例子中是视图或编辑器。成员变量 myStarCount 存储当前评分,而 myMaxStarCount 存储最高可评分(通常是 5)。

Q_DECLARE_METATYPE() 宏使得类型 StarRating 被认识为 QVariant,使得能够在 QVariant 中存储 StarRating 值。

StarRating 类实现

构造函数初始化 myStarCountmyMaxStarCount,并设置用于绘制星星和菱形的多边形。

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);
}

paint() 函数绘制此 StarRating 对象在绘图设备上的星星。

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();
}

我们首先设置用于绘制的笔和画刷。参数 mode 可以是 EditableReadOnly。如果 mode 是可编辑的,我们使用 Highlight 颜色绘制星星,而不是使用 WindowText 颜色。

然后我们绘制星星。如果我们处于 Edit 模式,并且在评分低于最高评分的情况下,我们用菱形代替星星绘制。

sizeHint() 函数返回用于绘制星星的区域的最佳大小。

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

最佳大小只足够绘制最大数量的星星。该函数由 StarDelegate::sizeHint()StarEditor::sizeHint() 同时调用。

main() 函数

以下是程序的 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();
}

main() 函数创建一个 QTableWidget 并将其设置为 StarDelegate。将 DoubleClickedSelectedClicked 设置为 edit triggers,这样在选择星星评分项目时,编辑器就会通过单击打开。

populateTableWidget() 函数向 QTableWidget 中填充数据

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);
    }
}

请注意,调用 QVariant::fromValueStarRating 转换为 QVariant

可能的扩展和建议

有许多方法可以定制 Qt 的 模型/视图框架。在这个例子中使用的方法适用于大多数自定义代理和编辑器。以下是一些未使用的可能性示例

示例项目 @ code.qt.io

© 2024 The Qt Company Ltd。本文件中包含的文档贡献均属于各自所有者的版权。提供的文档依据自由软件基金会发布的GNU自由文档许可协议版本1.3进行许可。Qt及其相关标志是芬兰及全球其他国家的The Qt Company Ltd.的商标。所有其他商标均属于各自所有者。