48

Instead of stepping when the user clicks somewhere on the qslider I want to make the slider jump to that position. How can this be implemented ?

Vihaan Verma
  • 12,815
  • 19
  • 97
  • 126
  • 2
    See Micka's answer below instead of the accepted answer for a simple solution. – Tim MB May 01 '17 at 15:52
  • 2
    Note that the default behaviour of the slider is platform specific. On MacOS for example, the slider jumps directly to the position where the mouse click was. – normanius Feb 05 '18 at 21:53

12 Answers12

35

after having problems with all versions of @spyke @Massimo Callegari and @Ben (slider position wasnt correct for the whole area) I found some Qt Style functionality within QSlider sourcecode: QStyle::SH_Slider_AbsoluteSetButtons.

You have to create a new QStyle which can be a very annoying, or you use ProxyStyle as shown by user jpn in http://www.qtcentre.org/threads/9208-QSlider-step-customize?p=49035#post49035

I've added another constructor and fixed a typo, but used the rest of the original source code.

#include <QProxyStyle>

class MyStyle : public QProxyStyle
{
public:
    using QProxyStyle::QProxyStyle;

    int styleHint(QStyle::StyleHint hint, const QStyleOption* option = 0, const QWidget* widget = 0, QStyleHintReturn* returnData = 0) const
    {
        if (hint == QStyle::SH_Slider_AbsoluteSetButtons)
            return (Qt::LeftButton | Qt::MidButton | Qt::RightButton);
        return QProxyStyle::styleHint(hint, option, widget, returnData);
    }
};

now you can set the style of your slider in the sliders constructor (if your slider is derived from QSlider):

setStyle(new MyStyle(this->style()));

or it should work this way if it is a standard QSlider:

standardSlider.setStyle(new MyStyle(standardSlider->style()));

so you use the original style of that element, but if the QStyle::SH_Slider_AbsoluteSetButtons "property" is asked you return as you want ;)

maybe you'll have to destroy these proxystyles on slider deletion, not tested yet.

maxschlepzig
  • 35,645
  • 14
  • 145
  • 182
Micka
  • 19,585
  • 4
  • 56
  • 74
  • 2
    Yes, this is the corrent answer. – ens Jul 26 '16 at 11:14
  • 2
    Also chiming in to appreciate this answer. QSliders already behave this way in macOS, so I figured there must be a more elegant solution than subclassing QSlider to get the same behavior in Windows. It makes sense that such a solution involves using QStyle / QProxyStyle. – Bri Bri Apr 19 '17 at 19:08
  • 2
    My compiler doesn't let me use the `QProxyStyle` constructor as some constructors are hidden, so I needed to define it explicitly like this: `MyStyle(QStyle* style = nullptr) : QProxyStyle(style) {}` – Tim MB May 01 '17 at 15:52
  • 4
    Unfortunately, style sheets do not work with custom styles – mentalmushroom Nov 02 '17 at 10:28
  • 1
    `QWidget::setStyle()` doesn't transfer ownership, so `new MyStyle` will be leaked. – nyanpasu64 Aug 25 '21 at 03:29
  • I found that if you create `QProxyStyle` or a subclass, and call `QProxyStyle(style())` or `QProxyStyle(QApplication::style())`, then destroying the style causes the next Qt method to segfault (even if you never assigned the style to any widgets). A workaround is not passing a `QStyle *` to `QProxyStyle()` (it defaults to `nullptr`), which doesn't crash. – nyanpasu64 Aug 25 '21 at 17:28
  • The crash is because `QProxyStyle(QStyle *)` transfers ownership of the style (https://doc.qt.io/qt-5/qproxystyle.html#QProxyStyle), which is wrong since you shouldn't transfer ownership over a widget or app's style. – nyanpasu64 Aug 25 '21 at 17:55
  • 1
    @mentalmushroom Did this change ? I am successfully using stylesheets on top of a proxy style (both applied to the app) in 6.2 – SleepProgger Jan 03 '22 at 17:01
25

Well, I doubt that Qt has a direct function for this purpose.

Try to use custom widgets. This should work!

Try the following logic

class MySlider : public QSlider
{

protected:
  void mousePressEvent ( QMouseEvent * event )
  {
    if (event->button() == Qt::LeftButton)
    {
        if (orientation() == Qt::Vertical)
            setValue(minimum() + ((maximum()-minimum()) * (height()-event->y())) / height() ) ;
        else
            setValue(minimum() + ((maximum()-minimum()) * event->x()) / width() ) ;

        event->accept();
    }
    QSlider::mousePressEvent(event);
  }
};
normanius
  • 8,629
  • 7
  • 53
  • 83
ScarCode
  • 3,074
  • 3
  • 19
  • 32
  • 1
    For Horizontal slider value will not be calculated correctly. I suggest such variant: `setValue(minimum() + (maximum() - minimum()) * (static_cast(event->x()) / static_cast(width())));` – Alexander Sorokin Dec 02 '16 at 14:25
  • Thanks for the code, but in my case I needed to add a "+1" for it to work: "setValue(minimum() + ((maximum()-minimum()+1) * (height()-event->y())) / height() );" – Alex Feb 03 '17 at 15:19
  • Check if its appearance is inverted as well, if it is, minus it the calculated value from `maximum()`. – Abderrahmene Rayene Mihoub Jul 22 '23 at 16:39
20

I needed this too and tried spyke solution, but it's missing two things:

  • inverted appearance
  • handle picking (when the mouse is over the handle, direct jump is not necessary)

So, here's the reviewed code:

void MySlider::mousePressEvent ( QMouseEvent * event )
{
  QStyleOptionSlider opt;
  initStyleOption(&opt);
  QRect sr = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, this);

  if (event->button() == Qt::LeftButton &&
      sr.contains(event->pos()) == false)
  {
    int newVal;
    if (orientation() == Qt::Vertical)
       newVal = minimum() + ((maximum()-minimum()) * (height()-event->y())) / height();
    else
       newVal = minimum() + ((maximum()-minimum()) * event->x()) / width();

    if (invertedAppearance() == true)
        setValue( maximum() - newVal );
    else
        setValue(newVal);

    event->accept();
  }
  QSlider::mousePressEvent(event);
}
Massimo Callegari
  • 2,099
  • 1
  • 26
  • 39
15

The answer of Massimo Callegari is almost right, but the calculation of newVal ignores the slider handle width. This problem comes up when you try to click near the end of the slider.

The following code fixes this for horizontal sliders

double halfHandleWidth = (0.5 * sr.width()) + 0.5; // Correct rounding
int adaptedPosX = event->x();
if ( adaptedPosX < halfHandleWidth )
        adaptedPosX = halfHandleWidth;
if ( adaptedPosX > width() - halfHandleWidth )
        adaptedPosX = width() - halfHandleWidth;
// get new dimensions accounting for slider handle width
double newWidth = (width() - halfHandleWidth) - halfHandleWidth;
double normalizedPosition = (adaptedPosX - halfHandleWidth)  / newWidth ;

newVal = minimum() + ((maximum()-minimum()) * normalizedPosition);
Chance
  • 2,653
  • 2
  • 26
  • 33
Ben
  • 1,519
  • 23
  • 39
  • missing semicolon, had to do 6 char edit so added another comment – Chance Jul 24 '14 at 17:34
  • To get the width of the handle (sr), check out this page https://forum.qt.io/topic/36346/solved-qslider-handle-size-after-stylesheet/9 – reddy Aug 09 '18 at 09:40
14

Here is a simple implementation in python using QStyle.sliderValueFromPosition():

class JumpSlider(QtGui.QSlider):

    def mousePressEvent(self, ev):
        """ Jump to click position """
        self.setValue(QtGui.QStyle.sliderValueFromPosition(self.minimum(), self.maximum(), ev.x(), self.width()))

    def mouseMoveEvent(self, ev):
        """ Jump to pointer position while moving """
        self.setValue(QtGui.QStyle.sliderValueFromPosition(self.minimum(), self.maximum(), ev.x(), self.width()))
Satara
  • 2,694
  • 1
  • 15
  • 15
6

The following code is actually a hack, but it works fine without sub-classing QSlider. The only thing you need to do is to connect QSlider valueChanged signal to your container.

Note1: You must set a pageStep > 0 in your slider

Note2: It works only for an horizontal, left-to-right slider (you should change the calculation of "sliderPosUnderMouse" to work with vertical orientation or inverted appearance)

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{

    // ...
    connect(ui->mySlider, SIGNAL(valueChanged(int)),
            this, SLOT(mySliderValueChanged(int)));
   // ...
}


void MainWindow::mySliderValueChanged(int newPos)
{
    // Make slider to follow the mouse directly and not by pageStep steps
    Qt::MouseButtons btns = QApplication::mouseButtons();
    QPoint localMousePos = ui->mySlider->mapFromGlobal(QCursor::pos());
    bool clickOnSlider = (btns & Qt::LeftButton) &&
                         (localMousePos.x() >= 0 && localMousePos.y() >= 0 &&
                          localMousePos.x() < ui->mySlider->size().width() &&
                          localMousePos.y() < ui->mySlider->size().height());
    if (clickOnSlider)
    {
        // Attention! The following works only for Horizontal, Left-to-right sliders
        float posRatio = localMousePos.x() / (float )ui->mySlider->size().width();
        int sliderRange = ui->mySlider->maximum() - ui->mySlider->minimum();
        int sliderPosUnderMouse = ui->mySlider->minimum() + sliderRange * posRatio;
        if (sliderPosUnderMouse != newPos)
        {
            ui->mySlider->setValue(sliderPosUnderMouse);
            return;
        }
    }
    // ...
}
Fivos Vilanakis
  • 1,490
  • 12
  • 13
4

My final implementation based on surrounding comments:

class ClickableSlider : public QSlider {
public:
    ClickableSlider(QWidget *parent = 0) : QSlider(parent) {}

protected:
    void ClickableSlider::mousePressEvent(QMouseEvent *event) {
      QStyleOptionSlider opt;
      initStyleOption(&opt);
      QRect sr = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, this);

      if (event->button() == Qt::LeftButton &&
          !sr.contains(event->pos())) {
        int newVal;
        if (orientation() == Qt::Vertical) {
           double halfHandleHeight = (0.5 * sr.height()) + 0.5;
           int adaptedPosY = height() - event->y();
           if ( adaptedPosY < halfHandleHeight )
                 adaptedPosY = halfHandleHeight;
           if ( adaptedPosY > height() - halfHandleHeight )
                 adaptedPosY = height() - halfHandleHeight;
           double newHeight = (height() - halfHandleHeight) - halfHandleHeight;
           double normalizedPosition = (adaptedPosY - halfHandleHeight)  / newHeight ;

           newVal = minimum() + (maximum()-minimum()) * normalizedPosition;
        } else {
            double halfHandleWidth = (0.5 * sr.width()) + 0.5;
            int adaptedPosX = event->x();
            if ( adaptedPosX < halfHandleWidth )
                  adaptedPosX = halfHandleWidth;
            if ( adaptedPosX > width() - halfHandleWidth )
                  adaptedPosX = width() - halfHandleWidth;
            double newWidth = (width() - halfHandleWidth) - halfHandleWidth;
            double normalizedPosition = (adaptedPosX - halfHandleWidth)  / newWidth ;

            newVal = minimum() + ((maximum()-minimum()) * normalizedPosition);
        }

        if (invertedAppearance())
            setValue( maximum() - newVal );
        else
            setValue(newVal);

        event->accept();
      } else {
        QSlider::mousePressEvent(event);
      }
    }
};
BiTOk
  • 716
  • 1
  • 6
  • 15
3

This modification to the JumpSlider above works in PyQt5:

class JumpSlider(QSlider):
    def _FixPositionToInterval(self,ev):
        """ Function to force the slider position to be on tick locations """

        # Get the value from the slider
        Value=QStyle.sliderValueFromPosition(self.minimum(), self.maximum(), ev.x(), self.width())

        # Get the desired tick interval from the slider
        TickInterval=self.tickInterval()

        # Convert the value to be only at the tick interval locations
        Value=round(Value/TickInterval)*TickInterval

        # Set the position of the slider based on the interval position
        self.setValue(Value)

    def mousePressEvent(self, ev):
        self._FixPositionToInterval(ev)

    def mouseMoveEvent(self, ev):
        self._FixPositionToInterval(ev)
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
Jim Graham
  • 31
  • 1
2

I think,

the QStyle::sliderValueFromPosition() function can be used.

http://qt-project.org/doc/qt-5/qstyle.html#sliderValueFromPosition

Tim S
  • 136
  • 1
  • 4
1

i have been trying and searching this on net and was expecting Qt for a smarter way doing this, unfortunately there was not big help (may be i was not searching properly )

well i have done this in Qt creator:

  1. Add an eventFilter in header ( takes QObject and QEvent as argument ) (bool return type)
  2. Initialize in constructor ..for eg .. if ur slider is HSlider then ui->HSlider->installEventFilter(this);
  3. In the defination :

    a. check if the object is your slider type something like : ui->HSlider == Object

    b. Check for mouse click event something like : QEvent::MouseButtonPress == event->type

    c. if the above all passes means u have got mouse event on the slider do something like : in definition : ui->HSlider->setValue( Qcursor::pos().x() - firstval ); return QMainWindow::eventFilter(object, event);

Note: fistVal : can be taken out by printing the cursur position at 0 = initial position of the slider ( with help of QCursor::pos().x() )

hope this helps

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
Biraj Borah
  • 101
  • 8
0

A simple method would be to derive from QSlider and reimplement mousePressEvent(....) to set the marker position using setSliderPosition(int).

cmannett85
  • 21,725
  • 8
  • 76
  • 119
0

I also met this problem. My solution is shown below.

slider->installEventFilter(this);
---    
bool MyDialog::eventFilter(QObject *object, QEvent *event)
    {
        if (object == slider && slider->isEnabled())
        {
            if (event->type() == QEvent::MouseButtonPress)
            {
                auto mevent = static_cast<QMouseEvent *>(event);
                qreal value = slider->minimum() + (slider->maximum() - slider->minimum()) * mevent->localPos().x() / slider->width();
                if (mevent->button() == Qt::LeftButton)
                {
                    slider->setValue(qRound(value));
                }
                event->accept();
                return true;
            }
            if (event->type() == QEvent::MouseMove)
            {
                auto mevent = static_cast<QMouseEvent *>(event);
                qreal value = slider->minimum() + (slider->maximum() - slider->minimum()) * mevent->localPos().x() / slider->width();
                if (mevent->buttons() & Qt::LeftButton)
                {
                    slider->setValue(qRound(value));
                }
                event->accept();
                return true;
            }
            if (event->type() == QEvent::MouseButtonDblClick)
            {
                event->accept();
                return true;
            }
        }
        return QDialog::eventFilter(object, event);
    }

You can also override these event handlers of QSlider.

  • QSlider::mousePressedEvent
  • QSlider::mouseMoveEvent
  • QSlider::mouseDoubleClickEvent
Nathan Tuggy
  • 2,237
  • 27
  • 30
  • 38
Destiny
  • 466
  • 5
  • 8