9

I have a QSlider that I want to move to the position of the mouse cursor when the user presses the left mouse button. I've been hunting around and couldn't find anything that was recent and solved my problem.

Progress Bar

This is the slider I have. I want to be able to click to have the slider jump to the position where the mouse clicks. I can drag the slider, but I want to be able to click. I tested out clicking on the slider in the Dolphin file manager. It incremented rather than jumping to the exact position of the mouse.

Looking at the Qt5 documentation

QSlider has very few of its own functions [...]

This would indicate that there is no built-in way to do this. Is there no way to get where the mouse clicked and move the slider to that point?

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
Eamonn
  • 658
  • 1
  • 7
  • 22

3 Answers3

13

The solution is to make a calculation of the position and set it in the mousePressEvent, the calculation is not easy as an arithmetic calculation since it depends on the style of each OS and the stylesheet so we must use QStyle as shown below:

from PyQt5 import QtCore, QtWidgets


class Slider(QtWidgets.QSlider):
    def mousePressEvent(self, event):
        super(Slider, self).mousePressEvent(event)
        if event.button() == QtCore.Qt.LeftButton:
            val = self.pixelPosToRangeValue(event.pos())
            self.setValue(val)

    def pixelPosToRangeValue(self, pos):
        opt = QtWidgets.QStyleOptionSlider()
        self.initStyleOption(opt)
        gr = self.style().subControlRect(QtWidgets.QStyle.CC_Slider, opt, QtWidgets.QStyle.SC_SliderGroove, self)
        sr = self.style().subControlRect(QtWidgets.QStyle.CC_Slider, opt, QtWidgets.QStyle.SC_SliderHandle, self)

        if self.orientation() == QtCore.Qt.Horizontal:
            sliderLength = sr.width()
            sliderMin = gr.x()
            sliderMax = gr.right() - sliderLength + 1
        else:
            sliderLength = sr.height()
            sliderMin = gr.y()
            sliderMax = gr.bottom() - sliderLength + 1;
        pr = pos - sr.center() + sr.topLeft()
        p = pr.x() if self.orientation() == QtCore.Qt.Horizontal else pr.y()
        return QtWidgets.QStyle.sliderValueFromPosition(self.minimum(), self.maximum(), p - sliderMin,
                                               sliderMax - sliderMin, opt.upsideDown)


if __name__ == '__main__':
    import sys

    app = QtWidgets.QApplication(sys.argv)
    w = QtWidgets.QWidget()
    flay = QtWidgets.QFormLayout(w)
    w1 = QtWidgets.QSlider(QtCore.Qt.Horizontal)
    w2 = Slider(QtCore.Qt.Horizontal)
    flay.addRow("default: ", w1)
    flay.addRow("modified: ", w2)
    w.show()
    sys.exit(app.exec_())
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Works perfectly, thank you! The only problem I had was when I called `__init__` the slider ignored any arguments that I passed, so I had to manually pass orientation with `setOrientation` – Eamonn Oct 08 '18 at 19:49
  • 1
    Thanks! It works. It is good to also emit sliderPressed signal in mousePressEvent (I already edited the post). Also, ideally, it should become "pressed" (i.e. become draggable) until mouse release. Otherwise, you need to drag it only after releasing left button after first click. Please update if you know how to do it. – Ashark Jun 11 '23 at 08:27
  • You can get that behavior simply by moving the `super(Slider, self).mousePressEvent(event)` to after the if statement (outside the indented block). – wesmlr Aug 22 '23 at 20:45
3

QSlider doesn't have such feature so the only way to implepent this is write custom widget and override mouse click in it:

class Slider(QSlider):

    def mousePressEvent(self, e):
        if e.button() == Qt.LeftButton:
            e.accept()
            x = e.pos().x()
            value = (self.maximum() - self.minimum()) * x / self.width() + self.minimum()
            self.setValue(value)
        else:
            return super().mousePressEvent(self, e)

Note that this code will work for horizontal slider only.

ingvar
  • 4,169
  • 4
  • 16
  • 29
1

I believe I have a much less involved solution:

from PyQt5.QtWidgets import QSlider


class ClickSlider(QSlider):
    """A slider with a signal that emits its position when it is pressed. Created to get around the slider only updating when the handle is dragged, but not when a new position is clicked"""

    sliderPressedWithValue = QSignal(int)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.sliderPressed.connect(self.on_slider_pressed)

    def on_slider_pressed(self):
        """emits a more descriptive signal when pressed (with slider value during the press event)"""
        self.sliderPressedWithValue.emit(self.value())

And then just make sure to connect to whatever you're updating like this:

# example if you're updating a QMediaPlayer object
from PyQt5.QtMultimedia import QMediaPlayer
player = QMediaPlayer()

slider = ClickSlider()
slider.sliderPressedWithValue.connect(player.setPosition)  # updates on click
slider.sliderMoved.connect(player.setPosition)  # updates on drag
Tomerikoo
  • 18,379
  • 16
  • 47
  • 61
Nevermore
  • 318
  • 2
  • 11