tick marks disappear on styled QSlider

4k views Asked by At

I'm using Qt 5.3 and trying to style a QSlider. However, when I apply my style-sheet, the tick marks disappear. Does anyone know how to keep the styling without affecting the tick marks?

Here is the style sheet:

QSlider::groove:horizontal
{
    border: 1px inset #B0B0B0;
    background-color: #EAEAEA;
    height: 2px;
}

QSlider::Handle
{   
    border: 1px solid black;
    background: #B0B0B0;                        
    background-image: url(:/metal_background_small);    
    width: 12px;
    margin: -8px 0;
}

QSlider::Handle:Hover
{   
    border: 1px solid black;
    background: #707070;                        
    background-image: url(:/metal_background_small);    
}

QSlider::sub-page
{
/*  margin: 7px 1px 7px 0px;*/
    height: 2px;
    background: #05bcfe;
}
2

There are 2 answers

1
Nicolas Holthaus On BEST ANSWER

Qt Style Sheets and tick marks do NOT play nicely together. the simplest solution is to subclass QSlider and re-implement the paint_event.

virtual void paintEvent(QPaintEvent *ev)
{

    QStylePainter p(this);
    QStyleOptionSlider opt;
    initStyleOption(&opt);

    QRect handle = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, this);

    // draw tick marks
    // do this manually because they are very badly behaved with style sheets
    int interval = tickInterval();
    if (interval == 0)
    {
        interval = pageStep();
    }

    if (tickPosition() != NoTicks)
    {
        for (int i = minimum(); i <= maximum(); i += interval)
        {
            int x = round((double)((double)((double)(i - this->minimum()) / (double)(this->maximum() - this->minimum())) * (double)(this->width() - handle.width()) + (double)(handle.width() / 2.0))) - 1;
            int h = 4;
            p.setPen(QColor("#a5a294"));
            if (tickPosition() == TicksBothSides || tickPosition() == TicksAbove)
            {
                int y = this->rect().top();
                p.drawLine(x, y, x, y + h);
            }
            if (tickPosition() == TicksBothSides || tickPosition() == TicksBelow)
            {
                int y = this->rect().bottom();
                p.drawLine(x, y, x, y - h);
            }

        }
    }

    // draw the slider (this is basically copy/pasted from QSlider::paintEvent)
    opt.subControls = QStyle::SC_SliderGroove;
    p.drawComplexControl(QStyle::CC_Slider, opt);

    // draw the slider handle
    opt.subControls = QStyle::SC_SliderHandle;
    p.drawComplexControl(QStyle::CC_Slider, opt);
}
0
Maxime Casas On

I had to adapt the previous answer to account for Qt's CSS margin property (the answer only worked if the handle's margin was 0). I also needed to draw the ticks after drawing the slider and the handle, probably because it was being drawn on top of the ticks.

When drawing the ticks, only start drawing after translating by the handle's margin and by half of the handle's width. The last tick must also be at a similar distance from the right side of the slider. See: drawing area image.

virtual void paintEvent(QPaintEvent *event)
{
    QStylePainter p(this);
    QStyleOptionSlider styleOptions;
    initStyleOption(&styleOptions);

    // Rectangle representing the slider's handle (position and size).
    const QRect handleRectangle =
    style()->subControlRect(QStyle::CC_Slider, &styleOptions, QStyle::SC_SliderHandle, this);

    // Draw the slider (this is basically copy/pasted from QSlider::paintEvent).
    styleOptions.subControls = QStyle::SC_SliderGroove;
    p.drawComplexControl(QStyle::CC_Slider, styleOptions);

    // Draw the slider handle.
    styleOptions.subControls = QStyle::SC_SliderHandle;
    p.drawComplexControl(QStyle::CC_Slider, styleOptions);

    // Draw tick marks.
    // Do this manually because they are very badly behaved with style sheets.
    int interval = tickInterval();
    if (interval == 0)
    {
        interval = pageStep();
    }

    if (tickPosition() != NoTicks)
    {
        // This is the horizontal margin of the slider's *handle*, as defined in the stylesheet.
        constexpr float margin = 25;

        const float handleWidth = static_cast<float>(handleRectangle.width());
        const float handleHalfWidth = handleWidth / 2.0f;
        const float sliderValueRange = static_cast<float>(this->maximum() - this->minimum());
        // Drawing range = control's width, minus twice the handle half width (one on each side), minus twice the margin (one on each side).
        const float drawingRange = static_cast<float>(this->width()) - handleWidth - 2.0f * margin;
        const float factor = drawingRange / sliderValueRange;
    
        for (int i = minimum(); i <= maximum(); i += interval)
        {
            // Height of the ticks' bars to draw.
            constexpr int tickHeight = 5;

            const int offsetValueSpace = i - minimum(); // How far from the slider's minimum value we are.
            const float offsetDrawingSpace = factor * static_cast<float>(offsetValueSpace) + handleHalfWidth + margin; // Where to draw in the horizontal range.
            const int x = static_cast<int>(offsetDrawingSpace);

            p.setPen(QColor{255, 255, 255, 255});
            if (tickPosition() == TicksBothSides || tickPosition() == TicksAbove)
            {
                const int y = this->rect().top();
                p.drawLine(x, y, x, y + tickHeight);
            }
            if (tickPosition() == TicksBothSides || tickPosition() == TicksBelow)
            {
                const int y = this->rect().bottom();
                p.drawLine(x, y, x, y - tickHeight);
            }
        }
    }
}

With the margin defined in Qt's CSS with:

MySlider::handle:horizontal {
    margin: -5px 25px; /* Match the 'margin' constexpr in the paint event. */
    ...
}