1 year ago

#386055

test-img

Matteo Italia

How to fix a broken sizeHint in core controls modified by a stylesheet?

I have an application that styles QAbstractSpinBox using an application-wide stylesheet, that goes like this:

QAbstractSpinBox {
    border: 2px inset grey;
    text-align: right;
    padding-left: 1px;
    padding-right: 1px;
    qproperty-buttonSymbols: PlusMinus;
}

QAbstractSpinBox::up-button
{
    subcontrol-origin: margin;
    subcontrol-position: right;
    width: 25px;
    height: 21px;
    right: 1px;
}

QAbstractSpinBox::down-button
{
    subcontrol-origin: margin;
    subcontrol-position: left;
    width: 25px;
    height: 21px;
    left: 2px;
}

The objective here is to obtain spinboxes with +/- buttons on the sides, like this:

example of a QSpinBox with +/- buttons on the sides

While the look is ok, the control does not behave correctly in layouts; in particular, the sizeHint() is wrong, as can be seen here for the QDoubleSpinBox:

two QSpinBox and two QDoubleSpinBox, where QDoubleSpinBoxes have clearly wrong sizeHint

Experimenting a bit, it turned out that it's not taking into account the size needed for one of the buttons; I don't want to replace each and every QAbstractSpinBox derived classes with my own subclassed version that fixes the sizeHint(), so I went at the bottom of the issue and checked how QAbstractSpinBox implements its sizeHint() so look for hints:

QSize QAbstractSpinBox::sizeHint() const
{
    Q_D(const QAbstractSpinBox);
    if (d->cachedSizeHint.isEmpty()) {
        ensurePolished();

        const QFontMetrics fm(fontMetrics());
        int h = d->edit->sizeHint().height();
        int w = 0;
        QString s;
        QString fixedContent =  d->prefix + d->suffix + QLatin1Char(' ');
        s = d->textFromValue(d->minimum);
        s.truncate(18);
        s += fixedContent;
        w = qMax(w, fm.horizontalAdvance(s));
        s = d->textFromValue(d->maximum);
        s.truncate(18);
        s += fixedContent;
        w = qMax(w, fm.horizontalAdvance(s));

        if (d->specialValueText.size()) {
            s = d->specialValueText;
            w = qMax(w, fm.horizontalAdvance(s));
        }
        w += 2; // cursor blinking space

        QStyleOptionSpinBox opt;
        initStyleOption(&opt);
        QSize hint(w, h);
        d->cachedSizeHint = style()->sizeFromContents(QStyle::CT_SpinBox, &opt, hint, this)
                            .expandedTo(QApplication::globalStrut());
    }
    return d->cachedSizeHint;
}

Which essentially calculates a size for the internal textbox part considering the maximum width that the content can assume (using the max/min/special text), and then delegates calculating the full size to QStyle::sizeFromContents from the current style. In turn, the relevant portion in QStyleSheetStyle::sizeFromContents is this:

    case CT_SpinBox:
        if (const QStyleOptionSpinBox *spinbox = qstyleoption_cast<const QStyleOptionSpinBox *>(opt)) {
            if (spinbox->buttonSymbols != QAbstractSpinBox::NoButtons) {
                // Add some space for the up/down buttons
                QRenderRule subRule = renderRule(w, opt, PseudoElement_SpinBoxUpButton);
                if (subRule.hasDrawable()) {
                    QRect r = positionRect(w, rule, subRule, PseudoElement_SpinBoxUpButton,
                                           opt->rect, opt->direction);
                    sz.rwidth() += r.width();
                } else {
                    QSize defaultUpSize = defaultSize(w, subRule.size(), spinbox->rect, PseudoElement_SpinBoxUpButton);
                    sz.rwidth() += defaultUpSize.width();
                }
            }
            if (rule.hasBox() || rule.hasBorder() || !rule.hasNativeBorder())
                sz = rule.boxSize(sz);
            return sz;
        }
        break;

Without going into the details, it's clear that only the Up button is considered, and only for its width, with the underlying assumption that the two buttons are stacked vertically; the down button is ignored completely.

I tried to work around the problem by means of a custom QStyle:

struct GLProxyStyle : QProxyStyle {
    virtual QSize sizeFromContents(ContentsType ct, const QStyleOption *opt, const QSize &contentsSize, const QWidget *w) const override {
        QSize ret = QProxyStyle::sizeFromContents(ct, opt, contentsSize, w);
        if (ct == CT_SpinBox) {
            if (const QStyleOptionSpinBox *spinbox = qstyleoption_cast<const QStyleOptionSpinBox *>(opt)) {
                if (spinbox->buttonSymbols != QAbstractSpinBox::NoButtons) {
                    QRect downRect = subControlRect(CC_SpinBox, spinbox, SC_SpinBoxDown, w);
                    QRect upRect = subControlRect(CC_SpinBox, spinbox, SC_SpinBoxDown, w);
                    if (downRect.left() != upRect.left()) {
                        ret.rwidth() += downRect.width();
                    }
                }
            }
        }
        return ret;
    }
};

but unfortunately setting a stylesheet makes it override QStyle.

For now I'm forcing a minimum size depending on an adjusted size hint after building all widgets, but that's a pain and, most importantly, it introduces a relayout, so I'd like to find a way to fix the sizeHint() globally.

So, coming to my question:

  • is there a way to override the QStyleSheetStyle implementation of sizeFromContents?
  • or, more in general: is there some other way to fix this that I missed?

c++

qt

qtstylesheets

qtwidgets

0 Answers

Your Answer

Accepted video resources