pythonpyqt5qlabelqsliderqvboxlayout

Align QSlider with an above QLabel that contains a QPixmap


I am trying to align a QSlider and a QLabel that I have in a QGridBoxLayout

For reference, this is for viewing frames in a video, which are shown elsewhere. The QLabel here is for showing labelled sections of the video, with green sections being "labelled" and black sections being "not labelled"

My QSLider is initialized as:

self.slider = QSlider(Qt.Horizontal, self)
self.slider.setSingleStep(1)
self.slider.setFocusPolicy(Qt.NoFocus)
self.slider.valueChanged.connect(self.sliderChanged)
self.layout.addWidget(self.slider)

The QLabel contains a QPixmap, which has a QImage created from a numpy array of shape (100,1000,3). Here is how it is initialized:

image = np.zeros((100,1000,3), dtype=np.uint8) #Initialize a black picture
for combo in indices_list:
    start, end = combo
    start = int(start / self.length * 1000)
    end = int(end / self.length * 1000)
    image[:,start:end] = [0,255,0] #Label the columns as green
height, width, bytevalue = image.shape
qimage = QImage(image, width, height, bytevalue * width, QImage.Format_RGB888)
pixmap = QPixmap(qimage)
self.label.setPixmap(pixmap)

What the above code does is take a list of start and end indices and then change the columns in that range to green.

The problem I am having is that even though the QSlider and the QLabel are stacked together in a QVBoxLayout, the slider position does not always match up with the corresponding pixel. I believe this is because the leftmost side of the slider "handle" (I am not sure of the correct name) stops at the leftmost side of the slider. They match up directly in the center, and then match up less and less as you move away from the center.

I would like the center of the handle to be directly underneath the corresponding column in the QLabel for the current slider value.

I have tried setting the margins of the slider to 0, but that did not work.

Thank you for your help.

Edit:

Pictures will probably be helpful:

Here is a picture of the entire bar:

Entire bar.

Here is a picture of it not being aligned on the left:

Not aligned left.

Here is a picture of it not aligned on the right:

Not aligned right.

And finally, Here is it aligned in the center:

Aligned in the center.


Solution

  • To obtain the horizontal position you must use QStyle::subControlRect():

    from PyQt5 import QtCore, QtGui, QtWidgets
    
    class Label(QtWidgets.QWidget):
        def __init__(self, parent=None):
            super(Label, self).__init__(parent)
            self._slider = None
            self._values = []
    
        def setSlider(self, slider):
            self._slider = slider
            self.update()
    
        def set_values(self, values):
            self._values = values
            self.update()
    
        def paintEvent(self, event):
            if self._slider is None: return
            painter = QtGui.QPainter(self)
            X1 = self.get_pos_by_value(self._slider.minimum())
            X2 = self.get_pos_by_value(self._slider.maximum())
            R = QtCore.QRect(QtCore.QPoint(X1, 0), QtCore.QPoint(X2, self.height()))
            painter.fillRect(R, QtGui.QColor("#000000"))
            for start, end in self._values:
                x1 = self.get_pos_by_value(start)
                x2 = self.get_pos_by_value(end)
                r = QtCore.QRect(QtCore.QPoint(x1, 0), QtCore.QPoint(x2, self.height()))
                painter.fillRect(r, QtGui.QColor("#00ff00"))
    
        def get_pos_by_value(self, value):
            opt = QtWidgets.QStyleOptionSlider()
            self._slider.initStyleOption(opt)
            opt.sliderPosition = value
            r = self._slider.style().subControlRect(
                QtWidgets.QStyle.CC_Slider, 
                opt, 
                QtWidgets.QStyle.SC_SliderHandle, 
                self._slider
            )
            return r.center().x()
    
    class Widget(QtWidgets.QWidget):
        def __init__(self, parent=None):
            super(Widget, self).__init__(parent)
            length = 1000
            indices_list = [(100, 200), (400, 500), (700, 900)]
            self.label = Label()
            self.slider = QtWidgets.QSlider(
                orientation=QtCore.Qt.Horizontal,
                minimum = 0,
                maximum=length,
                singleStep=1,
                pageStep=1
            )
            self.label.setSlider(self.slider)
            self.label.set_values(indices_list)
            label_value = QtWidgets.QLabel(alignment=QtCore.Qt.AlignCenter)
            self.slider.valueChanged.connect(label_value.setNum)
            label_value.setNum(self.slider.value())
    
            lay = QtWidgets.QVBoxLayout(self)
            lay.addWidget(self.label, 1)
            lay.addWidget(self.slider)
            lay.addWidget(label_value)
            self.resize(600, 300)
    
    if __name__ == '__main__':
        import sys
        app = QtWidgets.QApplication(sys.argv)
        app.setStyle('fusion')
        w = Widget()
        w.show()
        sys.exit(app.exec_())