2

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?
Matteo Italia
  • 123,740
  • 17
  • 206
  • 299
  • Shouldn't `QRect upRect = subControlRect(CC_SpinBox, spinbox, SC_SpinBoxDown, w);` be `QRect upRect = subControlRect(CC_SpinBox, spinbox, SC_SpinBoxUp, w);`? Otherwise, I have much the same question... – eclarkso Jan 17 '23 at 19:45
  • @eclarkso oh dear, maybe it was just that typo? I'll check it ASAP, thank you! – Matteo Italia Jan 18 '23 at 15:52
  • @eclarkso: retried fixing the typo, unfortunately it still doesn't work... which matches the investigation I did at the time (if you use a stylesheet, `QProxyStyle` is powerless). – Matteo Italia Jan 18 '23 at 15:58
  • Yes unfortunately I wasn't too hopeful that would actually fix anything. But I did find your question (and code) helpful for my own workaround in the proper way to extract the button subcontrol size info, so thank you! My own hack fix is to use `setMinimumSize` and a variation on your `ProxyStyle` so that it doesn't get too narrow... – eclarkso Jan 18 '23 at 20:46

0 Answers0