Use delegate to draw ComboBox Label

Allows customization and dynamic rendering of items
in the labels.

Task-number: QTBUG-126696
Change-Id: I6261131808aa303660f991e2f19248e547b14566
Reviewed-by: Richard Moe Gustavsen <richard.gustavsen@qt.io>
Reviewed-by: Matthias Rauter <matthias.rauter@qt.io>
This commit is contained in:
Magdalena Stojek 2024-07-02 16:02:54 +02:00 committed by Volker Hilsheimer
parent 4ce7235fdd
commit 0ac8ab0d20
9 changed files with 189 additions and 53 deletions

View File

@ -6,36 +6,31 @@
#include <QMouseEvent>
#include <QPainter>
#include <QSpinBox>
#include <QTableView>
BookDelegate::BookDelegate(int ratingColumn, QObject *parent)
: QSqlRelationalDelegate(parent), ratingColumn(ratingColumn)
{}
void BookDelegate::paint(QPainter *painter,
const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
if (index.column() != 5) {
if (index.column() != ratingColumn) {
QSqlRelationalDelegate::paint(painter, option, index);
} else {
const QAbstractItemModel *model = index.model();
QPalette::ColorGroup cg = (option.state & QStyle::State_Enabled) ?
(option.state & QStyle::State_Active) ?
QPalette::Normal :
QPalette::Inactive :
QPalette::Disabled;
if (option.state & QStyle::State_Selected) {
painter->fillRect(option.rect,
option.palette.color(QPalette::Highlight));
}
if (option.state & QStyle::State_Selected)
painter->fillRect(
option.rect,
option.palette.color(cg, QPalette::Highlight));
const int rating = model->data(index, Qt::DisplayRole).toInt();
const int width = iconDimension;
const int height = width;
const int rating = index.data(Qt::DisplayRole).toInt();
const int height = qMin(option.rect.height(), iconDimension);
const int width = height;
// add cellPadding / 2 to center the stars in the cell
int x = option.rect.x() + cellPadding / 2;
int y = option.rect.y() + (option.rect.height() / 2) - (height / 2);
QIcon starIcon(QStringLiteral(":images/star.svg"));
QIcon starFilledIcon(QStringLiteral(":images/star-filled.svg"));
for (int i = 0; i < 5; ++i) {
if (i < rating)
starFilledIcon.paint(painter, QRect(x, y, width, height));
@ -49,7 +44,7 @@ void BookDelegate::paint(QPainter *painter,
QSize BookDelegate::sizeHint(const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
if (index.column() == 5)
if (index.column() == ratingColumn)
return QSize(5 * iconDimension, iconDimension) + QSize(cellPadding, cellPadding);
// Since we draw the grid ourselves:
return QSqlRelationalDelegate::sizeHint(option, index) + QSize(cellPadding, cellPadding);
@ -59,7 +54,7 @@ bool BookDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
const QStyleOptionViewItem &option,
const QModelIndex &index)
{
if (index.column() != 5)
if (!ratingColumn || index.column() != ratingColumn)
return QSqlRelationalDelegate::editorEvent(event, model, option, index);
if (event->type() == QEvent::MouseButtonPress) {
@ -78,7 +73,7 @@ QWidget *BookDelegate::createEditor(QWidget *parent,
const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
if (index.column() != 4)
if (!ratingColumn || index.column() != 4)
return QSqlRelationalDelegate::createEditor(parent, option, index);
// For editing the year, return a spinbox with a range from -1000 to 2100.

View File

@ -13,7 +13,7 @@ QT_FORWARD_DECLARE_CLASS(QPainter)
class BookDelegate : public QSqlRelationalDelegate
{
public:
using QSqlRelationalDelegate::QSqlRelationalDelegate;
explicit BookDelegate(int ratingColumn, QObject *parent = nullptr);
void paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
@ -29,8 +29,12 @@ public:
const QModelIndex &index) const override;
private:
const QIcon starIcon{QStringLiteral(":images/star.svg")};
const QIcon starFilledIcon{QStringLiteral(":images/star-filled.svg")};
const int cellPadding = 6;
const int iconDimension = 24;
const int ratingColumn; // 0 in the combobox, otherwise 5
};
#endif

View File

@ -127,7 +127,7 @@ void BookWindow::createModel()
void BookWindow::configureWidgets()
{
tableView->setModel(model);
tableView->setItemDelegate(new BookDelegate(tableView));
tableView->setItemDelegate(new BookDelegate(model->fieldIndex("rating"), tableView));
tableView->setColumnHidden(model->fieldIndex("id"), true);
tableView->verticalHeader()->setVisible(false);
tableView->setSelectionMode(QAbstractItemView::ExtendedSelection);
@ -152,41 +152,18 @@ void BookWindow::configureWidgets()
yearSpinBox->setMaximum(9999);
const int width = 16;
const int height = width;
const int y = 2;
const int padding = 2;
ratingComboBox->setItemDelegate(new BookDelegate(0, this));
ratingComboBox->setLabelDrawingMode(QComboBox::LabelDrawingMode::UseDelegate);
ratingComboBox->addItems({"0", "1", "2", "3", "4", "5"});
QSize iconSize = QSize(width * 5 + padding * 2, width + padding * 2);
QIcon starIcon(QStringLiteral(":images/star.svg"));
QIcon starFilledIcon(QStringLiteral(":images/star-filled.svg"));
for (int row = 0; row < 6; ++row) {
QPixmap icon(iconSize);
icon.fill(Qt::transparent);
QPainter painter(&icon);
int x = 2;
for (int col = 0; col < 5; ++col) {
if (col < row) {
starFilledIcon.paint(&painter, QRect(x, y, width, height));
} else {
starIcon.paint(&painter, QRect(x, y, width, height));
}
x += width;
}
ratingComboBox->addItem(icon, "");
ratingComboBox->setItemData(row, QString::number(row + 1));
}
ratingComboBox->setIconSize(iconSize);
ratingComboBox->setIconSize(iconSize());
}
void BookWindow::createMappings()
{
QDataWidgetMapper *mapper = new QDataWidgetMapper(this);
mapper->setModel(model);
mapper->setItemDelegate(new BookDelegate(this));
mapper->setItemDelegate(new BookDelegate(model->fieldIndex("rating"), this));
mapper->addMapping(titleLineEdit, model->fieldIndex("title"));
mapper->addMapping(yearSpinBox, model->fieldIndex("year"));
mapper->addMapping(authorComboBox, authorIdx);

View File

@ -5,6 +5,7 @@
#define BOOKWINDOW_H
#include <QMainWindow>
QT_FORWARD_DECLARE_CLASS(QComboBox)
QT_FORWARD_DECLARE_CLASS(QGridLayout)
QT_FORWARD_DECLARE_CLASS(QLabel)

View File

@ -338,6 +338,7 @@ private:
friend class QListModeViewBase;
friend class QListViewPrivate;
friend class QAbstractSlider;
friend class QComboBoxPrivate; // needed to call initViewItemOption
};
Q_DECLARE_OPERATORS_FOR_FLAGS(QAbstractItemView::EditTriggers)

View File

@ -348,6 +348,12 @@ QSize QComboBoxPrivate::recomputeSizeHint(QSize &sh) const
{
Q_Q(const QComboBox);
if (!sh.isValid()) {
if (q->itemDelegate() && q->labelDrawingMode() == QComboBox::LabelDrawingMode::UseDelegate) {
QStyleOptionViewItem option;
initViewItemOption(&option);
sh = q->itemDelegate()->sizeHint(option, currentIndex);
}
bool hasIcon = sizeAdjustPolicy == QComboBox::AdjustToMinimumContentsLengthWithIcon;
int count = q->count();
QSize iconSize = q->iconSize();
@ -1259,6 +1265,16 @@ void QComboBox::initStyleOption(QStyleOptionComboBox *option) const
option->state |= QStyle::State_On;
}
void QComboBoxPrivate::initViewItemOption(QStyleOptionViewItem *option) const
{
Q_Q(const QComboBox);
q->view()->initViewItemOption(option);
option->widget = q;
option->index = currentIndex;
option->text = q->currentText();
option->icon = itemIcon(currentIndex);
}
void QComboBoxPrivate::updateLineEditGeometry()
{
if (!lineEdit)
@ -3067,6 +3083,7 @@ void QComboBox::resizeEvent(QResizeEvent *)
*/
void QComboBox::paintEvent(QPaintEvent *)
{
Q_D(QComboBox);
QStylePainter painter(this);
painter.setPen(palette().color(QPalette::Text));
@ -3080,8 +3097,17 @@ void QComboBox::paintEvent(QPaintEvent *)
opt.currentText = placeholderText();
}
// draw the icon and text
painter.drawControl(QStyle::CE_ComboBoxLabel, opt);
// draw contents
if (itemDelegate() && labelDrawingMode() == QComboBox::LabelDrawingMode::UseDelegate) {
QStyleOptionViewItem itemOption;
d->initViewItemOption(&itemOption);
itemOption.rect = style()->subControlRect(QStyle::CC_ComboBox, &opt,
QStyle::SC_ComboBoxEditField, this);
itemDelegate()->paint(&painter, itemOption, d->currentIndex);
} else {
// draw the icon and text
painter.drawControl(QStyle::CE_ComboBoxLabel, opt);
}
}
/*!
@ -3574,6 +3600,47 @@ void QComboBox::setModelColumn(int visibleColumn)
setCurrentIndex(currentIndex()); //update the text to the text of the new column;
}
/*!
\enum QComboBox::LabelDrawingMode
\since 6.9
This enum specifies how the combobox draws its label.
\value UseStyle The combobox uses the \l{QStyle}{style} to draw its label.
\value UseDelegate The combobox uses the \l{itemDelegate()}{item delegate} to
draw the label. Set a suitable item delegate when using this mode.
\sa labelDrawingMode, {Books}{Books example}
*/
/*!
\property QComboBox::labelDrawingMode
\since 6.9
\brief the mode used by the combobox to draw its label.
The default value is \l{QComboBox::}{UseStyle}. When changing this property
to UseDelegate, make sure to also set a suitable \l{itemDelegate()}{item delegate}.
The default delegate depends on the style and might not be suitable for
drawing the label.
\sa {Books}{Books example}
*/
QComboBox::LabelDrawingMode QComboBox::labelDrawingMode() const
{
Q_D(const QComboBox);
return d->labelDrawingMode;
}
void QComboBox::setLabelDrawingMode(LabelDrawingMode drawingLabel)
{
Q_D(QComboBox);
if (d->labelDrawingMode != drawingLabel) {
d->labelDrawingMode = drawingLabel;
update();
}
}
QT_END_NAMESPACE
#include "moc_qcombobox.cpp"

View File

@ -40,6 +40,7 @@ class Q_WIDGETS_EXPORT QComboBox : public QWidget
Q_PROPERTY(bool duplicatesEnabled READ duplicatesEnabled WRITE setDuplicatesEnabled)
Q_PROPERTY(bool frame READ hasFrame WRITE setFrame)
Q_PROPERTY(int modelColumn READ modelColumn WRITE setModelColumn)
Q_PROPERTY(LabelDrawingMode labelDrawingMode READ labelDrawingMode WRITE setLabelDrawingMode)
public:
explicit QComboBox(QWidget *parent = nullptr);
@ -85,6 +86,12 @@ public:
};
Q_ENUM(SizeAdjustPolicy)
enum class LabelDrawingMode {
UseStyle,
UseDelegate,
};
Q_ENUM(LabelDrawingMode)
SizeAdjustPolicy sizeAdjustPolicy() const;
void setSizeAdjustPolicy(SizeAdjustPolicy policy);
int minimumContentsLength() const;
@ -121,6 +128,9 @@ public:
int modelColumn() const;
void setModelColumn(int visibleColumn);
LabelDrawingMode labelDrawingMode() const;
void setLabelDrawingMode(LabelDrawingMode labelDrawing);
int currentIndex() const;
QString currentText() const;
QVariant currentData(int role = Qt::UserRole) const;

View File

@ -352,6 +352,7 @@ public:
void updateLayoutDirection();
void setCurrentIndex(const QModelIndex &index);
void updateDelegate(bool force = false);
void initViewItemOption(QStyleOptionViewItem *option) const;
void keyboardSearchString(const QString &text);
void modelChanged();
void updateViewContainerPaletteAndOpacity();
@ -396,6 +397,7 @@ public:
QComboBox::SizeAdjustPolicy sizeAdjustPolicy = QComboBox::AdjustToContentsOnFirstShow;
QStyle::StateFlag arrowState = QStyle::State_None;
QStyle::SubControl hoverControl = QStyle::SC_None;
QComboBox::LabelDrawingMode labelDrawingMode = QComboBox::LabelDrawingMode::UseStyle;
int minimumContentsLength = 0;
int indexBeforeChange = -1;
int maxVisibleItems = 10;

View File

@ -76,6 +76,9 @@ private slots:
void tst_QCombobox_data();
void tst_QCombobox();
void tst_QComboboxDelegate_data();
void tst_QComboboxDelegate();
void tst_QCommandLinkButton_data();
void tst_QCommandLinkButton();
@ -1223,6 +1226,82 @@ void tst_Widgets::tst_QCombobox()
QBASELINE_CHECK_DEFERRED(takeScreenSnapshot(testWindow()->geometry()), "combobox");
}
void tst_Widgets::tst_QComboboxDelegate_data()
{
QTest::addColumn<int>("paddingTest");
QTest::addColumn<int>("widthTest");
QTest::addRow("padding0") << 2 << 0;
QTest::addRow("padding20") << 20 << 0;
QTest::addRow("padding50") << 50 << 0;
QTest::addRow("width0") << 2 << 0;
QTest::addRow("width20") << 2 << 20;
QTest::addRow("width150") << 2 << 450;
}
void tst_Widgets::tst_QComboboxDelegate()
{
QFETCH(int, paddingTest);
QFETCH(int, widthTest);
testWindow()->resize(300, 300);
QScopedPointer<QComboBox> combobox(new QComboBox(testWindow()));
class RectDelegate : public QAbstractItemDelegate
{
public:
int sizeHintPadding = 2;
int sizeHintWidth = 22;
RectDelegate(QObject *parent = nullptr) : QAbstractItemDelegate(parent) {}
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
{
Q_UNUSED(index);
QRect rect = option.rect;
int padding = sizeHintPadding;
const int height = 22;
const int width = height + sizeHintWidth;
int yOffset = (option.rect.height() - height) / 2;
int x = rect.x() + padding;
int y = rect.y() + yOffset;
painter->setClipRect(rect);
painter->setBrush(QBrush(Qt::blue));
painter->drawRect(QRect(x, y, width, height));
}
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override
{
Q_UNUSED(option);
Q_UNUSED(index);
const int height = 22;
const int width = height;
return QSize(width + 2 * sizeHintPadding, height + 2 * sizeHintPadding);
}
};
auto rect1 = new RectDelegate(this);
rect1->sizeHintPadding = paddingTest;
rect1->sizeHintWidth = widthTest;
combobox->setLabelDrawingMode(QComboBox::LabelDrawingMode::UseDelegate);
combobox->setItemDelegate(rect1);
combobox->addItem("item1");
auto rect2 = new RectDelegate(this);
rect2->sizeHintPadding = paddingTest;
rect2->sizeHintWidth = widthTest;
combobox->setLabelDrawingMode(QComboBox::LabelDrawingMode::UseDelegate);
combobox->setItemDelegate(rect2);
combobox->addItem("item2");
QHBoxLayout layout;
layout.addWidget(combobox.get());
testWindow()->setLayout(&layout);
takeStandardSnapshots();
QTest::keyClick(combobox.get(), Qt::Key_Down, Qt::AltModifier);
QBASELINE_CHECK_DEFERRED(takeScreenSnapshot(testWindow()->geometry()), "combobox");
}
void tst_Widgets::tst_QCommandLinkButton_data()
{
QTest::addColumn<bool>("flat");