Introduce accent colored Buttons, Radio-/Checkbuttons in QWindows11Style

Introduce Windows 11 styled Buttons, Radiobuttons and Checkbuttons into
QWindows11Style. The default button, selected radio option and accepted
checkbox are drawn with accent color. The checkmark when accepting a
checkbox is now animated to replicate a hand written checking. Flat buttons are subtle
highlighted when hovered and pressed.

Change-Id: I37c0c21a25ffad427cff361f1b28d3138ff52b09
Reviewed-by: Oliver Wolff <oliver.wolff@qt.io>
bb10
Wladimir Leuschner 2023-05-30 14:56:11 +03:00
parent c27baa6aac
commit 045726818c
2 changed files with 376 additions and 0 deletions

View File

@ -12,6 +12,7 @@
#include "qdrawutil.h"
QT_BEGIN_NAMESPACE
const static int topLevelRoundingRadius = 8; //Radius for toplevel items like popups for round corners
@ -32,6 +33,33 @@ const static QColor textOnAccentSecondary = QColor(0xFF,0xFF,0xFF,0x7F);
const static QColor controlTextSecondary = QColor(0x00,0x00,0x00,0x7F); //Color of text of sunken controls
const static QColor controlStrokeOnAccentSecondary = QColor(0x00,0x00,0x00,0x66); //Color of frame around Buttons in accent color
#if QT_CONFIG(toolbutton)
static void drawArrow(const QStyle *style, const QStyleOptionToolButton *toolbutton,
const QRect &rect, QPainter *painter, const QWidget *widget = nullptr)
{
QStyle::PrimitiveElement pe;
switch (toolbutton->arrowType) {
case Qt::LeftArrow:
pe = QStyle::PE_IndicatorArrowLeft;
break;
case Qt::RightArrow:
pe = QStyle::PE_IndicatorArrowRight;
break;
case Qt::UpArrow:
pe = QStyle::PE_IndicatorArrowUp;
break;
case Qt::DownArrow:
pe = QStyle::PE_IndicatorArrowDown;
break;
default:
return;
}
QStyleOption arrowOpt = *toolbutton;
arrowOpt.rect = rect;
style->drawPrimitive(pe, &arrowOpt, painter, widget);
}
#endif // QT_CONFIG(toolbutton)
/*!
\class QWindows11Style
\brief The QWindows11Style class provides a look and feel suitable for applications on Microsoft Windows 11.
@ -179,10 +207,147 @@ void QWindows11Style::drawComplexControl(ComplexControl control, const QStyleOpt
void QWindows11Style::drawPrimitive(PrimitiveElement element, const QStyleOption *option,
QPainter *painter,
const QWidget *widget) const {
QWindows11StylePrivate *d = const_cast<QWindows11StylePrivate*>(d_func());
int state = option->state;
painter->save();
painter->setRenderHint(QPainter::Antialiasing);
if (d->transitionsEnabled() && (element == PE_IndicatorCheckBox || element == PE_IndicatorRadioButton)) {
QObject *styleObject = option->styleObject; // Can be widget or qquickitem
if (styleObject) {
int oldState = styleObject->property("_q_stylestate").toInt();
styleObject->setProperty("_q_stylestate", int(option->state));
styleObject->setProperty("_q_stylerect", option->rect);
bool doTransition = ((state & State_Sunken) != (oldState & State_Sunken)
|| ((state & State_MouseOver) != (oldState & State_MouseOver))
|| (state & State_On) != (oldState & State_On));
if (doTransition) {
if (element == PE_IndicatorRadioButton) {
QNumberStyleAnimation *t = new QNumberStyleAnimation(styleObject);
t->setStartValue(styleObject->property("_q_inner_radius").toFloat());
t->setEndValue(7.0f);
if (option->state & State_Sunken)
t->setEndValue(2.0f);
else if (option->state & State_MouseOver && !(option->state & State_On))
t->setEndValue(7.0f);
else if (option->state & State_MouseOver && (option->state & State_On))
t->setEndValue(5.0f);
else if (option->state & State_On)
t->setEndValue(4.0f);
styleObject->setProperty("_q_end_radius", t->endValue());
t->setStartTime(d->animationTime());
t->setDuration(150);
d->startAnimation(t);
}
else if (element == PE_IndicatorCheckBox) {
if ((oldState & State_Off && state & State_On) || (oldState & State_NoChange && state & State_On)) {
QNumberStyleAnimation *t = new QNumberStyleAnimation(styleObject);
t->setStartValue(0.0f);
t->setEndValue(1.0f);
t->setStartTime(d->animationTime());
t->setDuration(150);
d->startAnimation(t);
}
}
}
}
}
switch (element) {
case PE_IndicatorCheckBox:
{
QNumberStyleAnimation* animation = qobject_cast<QNumberStyleAnimation*>(d->animation(option->styleObject));
QFontMetrics fm(assetFont);
QRectF rect = option->rect;
auto center = QPoint(rect.x() + rect.width() / 2, rect.y() + rect.height() / 2);
rect.setWidth(15);
rect.setHeight(15);
rect.moveCenter(center);
float clipWidth = animation != nullptr ? animation->currentValue() : 1.0f;
QRectF clipRect = fm.boundingRect("\uE001");
clipRect.moveCenter(center);
clipRect.setLeft(rect.x() + (rect.width() - clipRect.width()) / 2.0);
clipRect.setWidth(clipWidth * clipRect.width());
QBrush fillBrush = (option->state & State_On || option->state & State_NoChange) ? option->palette.accent() : option->palette.window();
if (state & State_MouseOver && (option->state & State_On || option->state & State_NoChange))
fillBrush.setColor(fillBrush.color().lighter(107));
else if (state & State_MouseOver && !(option->state & State_On || option->state & State_NoChange))
fillBrush.setColor(fillBrush.color().darker(107));
painter->setPen(QPen(frameColorStrong));
painter->setBrush(fillBrush);
painter->drawRoundedRect(rect, secondLevelRoundingRadius, secondLevelRoundingRadius, Qt::AbsoluteSize);
painter->setFont(assetFont);
painter->setPen(option->palette.highlightedText().color());
painter->setBrush(option->palette.highlightedText().color());
if (option->state & State_On)
painter->drawText(clipRect, Qt::AlignVCenter | Qt::AlignLeft,"\uE001");
else if (option->state & State_NoChange)
painter->drawText(rect, Qt::AlignVCenter | Qt::AlignHCenter,"\uE108");
}
break;
case PE_IndicatorRadioButton:
{
if (option->styleObject->property("_q_end_radius").isNull())
option->styleObject->setProperty("_q_end_radius", option->state & State_On ? 4.0f :7.0f);
QNumberStyleAnimation* animation = qobject_cast<QNumberStyleAnimation*>(d->animation(option->styleObject));
if (animation != nullptr)
option->styleObject->setProperty("_q_inner_radius", animation->currentValue());
else
option->styleObject->setProperty("_q_inner_radius", option->styleObject->property("_q_end_radius"));
int innerRadius = option->styleObject->property("_q_inner_radius").toFloat();
QRect rect = option->rect;
auto center = QPoint(rect.x() + rect.width() / 2, rect.y() + rect.height() / 2);
rect.setWidth(15);
rect.setHeight(15);
rect.moveCenter(center);
QRect innerRect = rect;
innerRect.setWidth(8);
innerRect.setHeight(8);
innerRect.moveCenter(center);
painter->setPen(QPen(frameColorStrong));
painter->setBrush(option->palette.accent());
if (option->state & State_MouseOver)
painter->setBrush(QBrush(option->palette.accent().color().lighter(107)));
painter->drawEllipse(center, 7, 7);
painter->setBrush(QBrush(option->palette.window()));
painter->drawEllipse(center,innerRadius, innerRadius);
painter->setPen(Qt::NoPen);
painter->setBrush(QBrush(option->palette.window()));
if (option->state & State_MouseOver)
painter->setBrush(QBrush(option->palette.window().color().darker(107)));
painter->drawEllipse(center,innerRadius, innerRadius);
}
break;
case PE_PanelButtonBevel:{
QRect rect = option->rect.marginsRemoved(QMargins(2,2,2,2));
painter->setPen(QPen(controlStrokePrimary));
if (!(state & (State_Raised)))
painter->setBrush(controlFillTertiary);
else if (state & State_MouseOver)
painter->setBrush(controlFillSecondary);
else
painter->setBrush(option->palette.button());
painter->drawRoundedRect(rect, secondLevelRoundingRadius, secondLevelRoundingRadius);
painter->setPen(QPen(controlStrokeSecondary));
if (state & State_Raised)
painter->drawLine(rect.bottomLeft() + QPoint(2,1), rect.bottomRight() + QPoint(-2,1));
}
break;
case PE_FrameDefaultButton:
painter->setPen(option->palette.accent().color());
painter->setBrush(Qt::NoBrush);
painter->drawRoundedRect(option->rect, secondLevelRoundingRadius, secondLevelRoundingRadius);
break;
case QStyle::PE_FrameMenu:
break;
case QStyle::PE_PanelMenu: {
@ -274,9 +439,196 @@ void QWindows11Style::drawControl(ControlElement element, const QStyleOption *op
QPainter *painter, const QWidget *widget) const
{
Q_D(const QWindows11Style);
QRect rect(option->rect);
State flags = option->state;
painter->save();
painter->setRenderHint(QPainter::Antialiasing);
switch (element) {
case CE_ToolButtonLabel:
if (const QStyleOptionToolButton *toolbutton
= qstyleoption_cast<const QStyleOptionToolButton *>(option)) {
QRect rect = toolbutton->rect;
int shiftX = 0;
int shiftY = 0;
if (toolbutton->state & (State_Sunken | State_On)) {
shiftX = proxy()->pixelMetric(PM_ButtonShiftHorizontal, toolbutton, widget);
shiftY = proxy()->pixelMetric(PM_ButtonShiftVertical, toolbutton, widget);
}
// Arrow type always overrules and is always shown
bool hasArrow = toolbutton->features & QStyleOptionToolButton::Arrow;
if (((!hasArrow && toolbutton->icon.isNull()) && !toolbutton->text.isEmpty())
|| toolbutton->toolButtonStyle == Qt::ToolButtonTextOnly) {
int alignment = Qt::AlignCenter | Qt::TextShowMnemonic;
if (!proxy()->styleHint(SH_UnderlineShortcut, toolbutton, widget))
alignment |= Qt::TextHideMnemonic;
rect.translate(shiftX, shiftY);
painter->setFont(toolbutton->font);
const QString text = d->toolButtonElideText(toolbutton, rect, alignment);
if (toolbutton->state & State_Raised)
painter->setPen(QPen(toolbutton->palette.buttonText().color()));
else
painter->setPen(QPen(controlTextSecondary));
proxy()->drawItemText(painter, rect, alignment, toolbutton->palette,
toolbutton->state & State_Enabled, text);
} else {
QPixmap pm;
QSize pmSize = toolbutton->iconSize;
if (!toolbutton->icon.isNull()) {
QIcon::State state = toolbutton->state & State_On ? QIcon::On : QIcon::Off;
QIcon::Mode mode;
if (!(toolbutton->state & State_Enabled))
mode = QIcon::Disabled;
else if ((toolbutton->state & State_MouseOver) && (toolbutton->state & State_AutoRaise))
mode = QIcon::Active;
else
mode = QIcon::Normal;
pm = toolbutton->icon.pixmap(toolbutton->rect.size().boundedTo(toolbutton->iconSize), painter->device()->devicePixelRatio(),
mode, state);
pmSize = pm.size() / pm.devicePixelRatio();
}
if (toolbutton->toolButtonStyle != Qt::ToolButtonIconOnly) {
painter->setFont(toolbutton->font);
QRect pr = rect,
tr = rect;
int alignment = Qt::TextShowMnemonic;
if (!proxy()->styleHint(SH_UnderlineShortcut, toolbutton, widget))
alignment |= Qt::TextHideMnemonic;
if (toolbutton->toolButtonStyle == Qt::ToolButtonTextUnderIcon) {
pr.setHeight(pmSize.height() + 4); //### 4 is currently hardcoded in QToolButton::sizeHint()
tr.adjust(0, pr.height() - 1, 0, -1);
pr.translate(shiftX, shiftY);
if (!hasArrow) {
proxy()->drawItemPixmap(painter, pr, Qt::AlignCenter, pm);
} else {
drawArrow(proxy(), toolbutton, pr, painter, widget);
}
alignment |= Qt::AlignCenter;
} else {
pr.setWidth(pmSize.width() + 4); //### 4 is currently hardcoded in QToolButton::sizeHint()
tr.adjust(pr.width(), 0, 0, 0);
pr.translate(shiftX, shiftY);
if (!hasArrow) {
proxy()->drawItemPixmap(painter, QStyle::visualRect(toolbutton->direction, rect, pr), Qt::AlignCenter, pm);
} else {
drawArrow(proxy(), toolbutton, pr, painter, widget);
}
alignment |= Qt::AlignLeft | Qt::AlignVCenter;
}
tr.translate(shiftX, shiftY);
const QString text = d->toolButtonElideText(toolbutton, tr, alignment);
if (toolbutton->state & State_Raised)
painter->setPen(QPen(toolbutton->palette.buttonText().color()));
else
painter->setPen(QPen(controlTextSecondary));
proxy()->drawItemText(painter, QStyle::visualRect(toolbutton->direction, rect, tr), alignment, toolbutton->palette,
toolbutton->state & State_Enabled, text);
} else {
rect.translate(shiftX, shiftY);
if (hasArrow) {
drawArrow(proxy(), toolbutton, rect, painter, widget);
} else {
proxy()->drawItemPixmap(painter, rect, Qt::AlignCenter, pm);
}
}
}
}
break;
case CE_PushButtonLabel:
if (const QStyleOptionButton *btn = qstyleoption_cast<const QStyleOptionButton *>(option)) {
QRect textRect = btn->rect;
int tf = Qt::AlignVCenter|Qt::TextShowMnemonic;
if (!proxy()->styleHint(SH_UnderlineShortcut, btn, widget))
tf |= Qt::TextHideMnemonic;
if (btn->features & QStyleOptionButton::HasMenu) {
int indicatorSize = proxy()->pixelMetric(PM_MenuButtonIndicator, btn, widget);
if (btn->direction == Qt::LeftToRight)
textRect = textRect.adjusted(0, 0, -indicatorSize, 0);
else
textRect = textRect.adjusted(indicatorSize, 0, 0, 0);
}
if (!btn->icon.isNull()) {
//Center both icon and text
QIcon::Mode mode = btn->state & State_Enabled ? QIcon::Normal : QIcon::Disabled;
if (mode == QIcon::Normal && btn->state & State_HasFocus)
mode = QIcon::Active;
QIcon::State state = QIcon::Off;
if (btn->state & State_On)
state = QIcon::On;
QPixmap pixmap = btn->icon.pixmap(btn->iconSize, painter->device()->devicePixelRatio(), mode, state);
int pixmapWidth = pixmap.width() / pixmap.devicePixelRatio();
int pixmapHeight = pixmap.height() / pixmap.devicePixelRatio();
int labelWidth = pixmapWidth;
int labelHeight = pixmapHeight;
int iconSpacing = 4;//### 4 is currently hardcoded in QPushButton::sizeHint()
if (!btn->text.isEmpty()) {
int textWidth = btn->fontMetrics.boundingRect(option->rect, tf, btn->text).width();
labelWidth += (textWidth + iconSpacing);
}
QRect iconRect = QRect(textRect.x() + (textRect.width() - labelWidth) / 2,
textRect.y() + (textRect.height() - labelHeight) / 2,
pixmapWidth, pixmapHeight);
iconRect = visualRect(btn->direction, textRect, iconRect);
if (btn->direction == Qt::RightToLeft) {
tf |= Qt::AlignRight;
textRect.setRight(iconRect.left() - iconSpacing / 2);
} else {
tf |= Qt::AlignLeft; //left align, we adjust the text-rect instead
textRect.setLeft(iconRect.left() + iconRect.width() + iconSpacing / 2);
}
if (btn->state & (State_On | State_Sunken))
iconRect.translate(proxy()->pixelMetric(PM_ButtonShiftHorizontal, option, widget),
proxy()->pixelMetric(PM_ButtonShiftVertical, option, widget));
painter->drawPixmap(iconRect, pixmap);
} else {
tf |= Qt::AlignHCenter;
}
if (btn->state & State_Sunken)
painter->setPen(flags & State_On ? QPen(textOnAccentSecondary) : QPen(controlTextSecondary));
else
painter->setPen(flags & State_On ? QPen(textOnAccentPrimary) : QPen(btn->palette.buttonText().color()));
proxy()->drawItemText(painter, textRect, tf, option->palette,btn->state & State_Enabled, btn->text);
}
break;
case CE_PushButtonBevel:
if (const QStyleOptionButton *btn = qstyleoption_cast<const QStyleOptionButton *>(option)) {
if (btn->features.testFlag(QStyleOptionButton::Flat)) {
painter->setPen(Qt::NoPen);
if (flags & (State_Sunken | State_On)) {
painter->setBrush(subtlePressedColor);
painter->drawRoundedRect(rect, secondLevelRoundingRadius, secondLevelRoundingRadius);
}
else if (flags & State_MouseOver) {
painter->setBrush(subtleHighlightColor);
painter->drawRoundedRect(rect, secondLevelRoundingRadius, secondLevelRoundingRadius);
}
} else {
QRectF rect = btn->rect.marginsRemoved(QMargins(2,2,2,2));
painter->setPen(btn->features.testFlag(QStyleOptionButton::DefaultButton) ? QPen(option->palette.accent().color()) : QPen(controlStrokePrimary));
if (flags & (State_Sunken))
painter->setBrush(flags & State_On ? option->palette.accent().color().lighter(120) : controlFillTertiary);
else if (flags & State_MouseOver)
painter->setBrush(flags & State_On ? option->palette.accent().color().lighter(110) : controlFillSecondary);
else
painter->setBrush(flags & State_On ? option->palette.accent() : option->palette.button());
painter->drawRoundedRect(rect, secondLevelRoundingRadius, secondLevelRoundingRadius);
painter->setPen(btn->features.testFlag(QStyleOptionButton::DefaultButton) ? QPen(controlStrokeOnAccentSecondary) : QPen(controlStrokeSecondary));
if (flags & State_Raised)
painter->drawLine(rect.bottomLeft() + QPointF(4.0,0.5), rect.bottomRight() + QPointF(-4,0.5));
}
}
break;
case CE_MenuBarItem:
if (const auto *mbi = qstyleoption_cast<const QStyleOptionMenuItem *>(option)) {
bool active = mbi->state & State_Selected;
@ -608,6 +960,28 @@ QSize QWindows11Style::sizeFromContents(ContentsType type, const QStyleOption *o
return contentSize;
}
/*!
\internal
*/
int QWindows11Style::pixelMetric(PixelMetric metric, const QStyleOption *option, const QWidget *widget) const
{
int res = 0;
switch (metric) {
case QStyle::PM_IndicatorWidth:
case QStyle::PM_IndicatorHeight:
case QStyle::PM_ExclusiveIndicatorWidth:
case QStyle::PM_ExclusiveIndicatorHeight:
return 16;
default:
res = QWindowsVistaStyle::pixelMetric(metric, option, widget);
}
return res;
}
void QWindows11Style::polish(QWidget* widget)
{
QWindowsVistaStyle::polish(widget);

View File

@ -42,6 +42,8 @@ public:
QSize sizeFromContents(ContentsType type, const QStyleOption *option,
const QSize &size, const QWidget *widget) const override;
int pixelMetric(PixelMetric metric, const QStyleOption *option = nullptr,
const QWidget *widget = nullptr) const override;
protected:
QWindows11Style(QWindows11StylePrivate &dd);
private: