5

My aim is to have a custom QSlider with tickmarks and tick labels in Python 3 using PySide2 module. In order to do so I edit the default paintEvent of the QSlider class in a derived class. However, it turns out that that the printable area is limited and the top/bottom labels I placed are cropped (see screenshot). The code I use to generate these sliders are as follows:

import sys
from PySide2.QtCore import *
from PySide2.QtWidgets import *
from PySide2.QtGui import *

slider_x = 150
slider_y = 450
slider_step = [0.01, 0.1, 1, 10, 100]  # in microns


class MySlider(QSlider):
    def __init__(self, type, parent=None):
        super(MySlider, self).__init__(parent)
        self.Type = type

    def paintEvent(self, event):
        super(MySlider, self).paintEvent(event)
        qp = QPainter(self)
        pen = QPen()
        pen.setWidth(2)
        pen.setColor(Qt.red)

        qp.setPen(pen)
        font = QFont('Times', 10)
        qp.setFont(font)
        self.setContentsMargins(50, 50, 50, 50)
        # size = self.size()
        # print(size)
        # print("margins", self.getContentsMargins())
        # print(self.getContentsMargins())
        # print(self.contentsRect())
        contents = self.contentsRect()
        self.setFixedSize(QSize(slider_x, slider_y))
        max_slider = self.maximum()
        y_inc = 0
        for i in range(max_slider):
            qp.drawText(contents.x() - font.pointSize(), y_inc + font.pointSize() / 2, '{0:2}'.format(slider_step[i]))
            qp.drawLine(contents.x() + font.pointSize(), y_inc, contents.x() + contents.width(), y_inc)
            y_inc += slider_y/4


class Window(QWidget):
    """ Inherits from QWidget """
    def __init__(self):
        super().__init__()
        self.title = 'Control Stages'
        self.left = 10
        self.top = 10
        self.width = 320
        self.height = 100
        self.AxesMapping = [0, 1, 2, 3]
        self.initUI()

    def initUI(self):
        """ Initializes the GUI either using the grid layout or the absolute position layout"""
        self.setWindowTitle(self.title)
        self.setGeometry(self.left, self.top, self.width, self.height)
        Comp4 = self.createSlider("step_size")
        Comp5 = self.createSlider("speed")
        windowLayout = QGridLayout()
        windowLayout.setContentsMargins(50, 50, 50, 50)
        HGroupBox = QGroupBox()
        layout = QGridLayout()
        layout.addWidget(Comp4, 0, 0)
        layout.addWidget(Comp5, 0, 1)
        HGroupBox.setLayout(layout)
        HGroupBox.setFixedSize(QSize(740, 480))
        windowLayout.addWidget(HGroupBox, 0, 0)
        self.setLayout(windowLayout)
        self.show()

    def createSlider(self, variant):
        Slider = MySlider(Qt.Vertical)
        Slider.Type = variant
        Slider.setMaximum(5)
        Slider.setMinimum(1)
        Slider.setSingleStep(1)
        Slider.setTickInterval(1)
        Slider.valueChanged.connect(lambda: self.sliderChanged(Slider))
        return Slider

    @staticmethod
    def sliderChanged(Slider):
        print("Slider value changed to ", Slider.value(), "slider type is ", Slider.Type)
        if Slider.Type == "step_size":
            print("this is a step size slider")
        elif Slider.Type == "speed":
            print("this is a speed slider")


if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = Window()
    sys.exit(app.exec_())

Is it possible to expand the drawable area around the QSlider and if so how can I achieve this effect? You can see on the screenshot that the red labels next to the first and last tickmarks are not displayed properly and they are cropped (i.e in the first tick label the top of 1 and 0 is missing for the label 0.01).

enter image description here

EDIT: After trying the proposed solution still a portion of the top label is clipped off. Second version below is still similar on Windows 10 64-bit with PySide2 5.12.0 and Python 3.6.6.

EDIT2 I have a dual-boot system so I tried it on Ubuntu 16.04.3 LTS with Python 3.5.2 / PySide 5.12.0 and it worked right out of the box. Here is a screenshot from there, but unfortunately it has to work on Windows.

enter image description here

Rachel Gallen
  • 27,943
  • 21
  • 72
  • 81
Vesnog
  • 773
  • 2
  • 16
  • 33
  • 1
    You could explain your question better, your code can not be executed so I can not compare what you get with what image you show to understand what you want. – eyllanesc Jan 17 '19 at 22:30
  • @eyllanesc You are right I provided a MWE and clarified my question more. As you can see the first and last labels in the screenshot are not displayed properly possibly due to extending into regions that are not drawable. – Vesnog Jan 18 '19 at 08:44
  • @eyllanesc Do you have a suggestion for how to achieve custom tickmarks with the QSlider? The drawable area around them seems to be clipped.`setContentsMargins` method does not change that area. – Vesnog Jan 29 '19 at 21:00
  • @Vesnog The QSlider inherits QAbstractSlider - could you integrate SliderToMaximum in order to view the top? Just a thought .. (http://pyqt.sourceforge.net/Docs/PyQt4/qabstractslider.html#SliderAction-enum) – Rachel Gallen Jan 30 '19 at 22:49
  • @RachelGallen I did not understand your statement completely. So do you mean that I should just call that method and see what happens? – Vesnog Jan 30 '19 at 23:05
  • @Vesnog It's worth investigating ... – Rachel Gallen Jan 30 '19 at 23:13
  • @RachelGallen Tried with Linux and everything worked fine, now on Windows back again and I will investigate that method. – Vesnog Jan 30 '19 at 23:29
  • @RachelGallen **TypeError: 'PySide2.QtWidgets.QAbstractSlider.SliderAction' object is not callable** It is a probably a private method in C++. – Vesnog Jan 30 '19 at 23:37
  • @Vesnog that looks like you're calling the method incorrectly rather than its uncallable. – Rachel Gallen Jan 30 '19 at 23:40
  • @RachelGallen Probably I did not call it properly. Can you help? – Vesnog Jan 30 '19 at 23:43
  • 1
    @Vesnog my last word on this tonight - https://doc.qt.io/qtforpython/PySide2/QtWidgets/QAbstractSlider.html#PySide2.QtWidgets.PySide2.QtWidgets.QAbstractSlider.maximum had to post to clear my head...zzzz – Rachel Gallen Jan 31 '19 at 00:08
  • @RachelGallen I did `print(Comp4.maximum())` and the value is 5. I was not able to do `SliderToMaximum`, yet I can do `Comp4.setSliderPosition(Comp4.maximum())` – Vesnog Jan 31 '19 at 08:55

3 Answers3

4

If I got it right, you want the ticks to be visible in the top and bottom, since they seemed to be "cut" due to the margins. I think that's the outcome of assuming the widgets margins are properly set to zero or something like that, you can play around changing numbers and see this effects. (I tried moving margins and didn't succeed)

Now, following an old post, I noticed that achieving something similar seems to be tricky, and you can base your ticks on a formula based on the minimum and maximum of the QSlider.

Since the tick at the bottom will be under the main widget, you can just add a special condition for it to make it visible.

Didn't want to paste the whole code, but you just need to declare opt and handle before your loop. Calculate the y position inside, and used it instead of your y_inc.

def paintEvent(self, event):                                                                                                                                                            
    super(MySlider, self).paintEvent(event)                                                     
    qp = QPainter(self)                                                                         
    pen = QPen()                                                                                
    pen.setWidth(2)                                                                             
    pen.setColor(Qt.red)                                                                        

    qp.setPen(pen)                                                                              
    font = QFont('Times', 10)                                                                   
    qp.setFont(font)                                                                            
    self.setContentsMargins(50, 50, 50, 50)                                                     
    # size = self.size()                                                                        
    # print(size)                                                                               
    # print("margins", self.getContentsMargins())                                               
    # print(self.getContentsMargins())                                                          
    # print(self.contentsRect())                                                                
    contents = self.contentsRect()                                                              
    self.setFixedSize(QSize(slider_x, slider_y))                                                

    # New code                                                                                  
    max_slider = self.maximum()                                                                 
    min_slider = self.minimum()                                                                 
    len_slider = max_slider - min_slider                                                        
    height = self.height()                                                                      

    opt = QStyleOptionSlider()                                                                  
    handle = self.style().subControlRect(QStyle.CC_Slider, opt, QStyle.SC_SliderHandle, self)   
    handle_height = handle.height() # Add +10 on windows (workarount)                                                            
    height_diff = height - handle_height                                                        
    point_size = font.pointSize()                                                               

    for i in range(max_slider):                                                                 
        y = round(((1 - i / len_slider) * height_diff + (handle_height / 2.0))) - 1             

        # Trick for the tick at the bottom                                                      
        if i == 0:                                                                              
            y = self.height() - handle_height/2.0 # To have the tick in the middle              
            #y = height_diff # to shift it a bit                                                

        qp.drawText(contents.x() - point_size, y + point_size / 2, '{0:2}'.format(slider_step[len_slider - i]))
        qp.drawLine(contents.x() + point_size, y, contents.x() + contents.width(), y)  

enter image description here

Edit: There seems to be a overlooked top margin of around 10 units on windows, and that's IMHHO a bug in Qt. A workaround is to replace:

handle_height = handle.height()

with

handle_height = handle.height() + 10

inside the paintEvent method.

cmaureir
  • 285
  • 2
  • 8
  • Thanks but still a portion of the top label is clipped and the ordering is changed with 0.01 being at the bottom. – Vesnog Jan 30 '19 at 20:24
  • oops, didn't noticed. Just change the iteration direction on the for loop, and just change -5 by a -8 for example to avoid the bottom tick. – cmaureir Jan 30 '19 at 21:20
  • Still the label keeps getting clipped at the top. – Vesnog Jan 30 '19 at 22:17
  • I updated the paintEvent method, and attached a new screenshot, hopefully it helps. – cmaureir Jan 30 '19 at 22:27
  • I copied the paintEvent method directly from your latest post but the end values are still cropped. Maybe it has something to do with Python version or PySide2. – Vesnog Jan 30 '19 at 22:33
  • Could be, I did a `pip install PySide2` on a new Python `3.7.2` virtual environment (linux). Which PySide2, Python, and OS are you using? – cmaureir Jan 30 '19 at 22:36
  • Windows 10 64-bit, Python 3.6.6, PySide2 5.11.2 – Vesnog Jan 30 '19 at 22:42
  • can you try to upgrade `PySide2` ? the official release is `5.12.0`, and this could be a leftover bug from the Technical Preview version. If `5.12.0` also has the issue, then it might be a Windows bug. – cmaureir Jan 30 '19 at 22:52
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/187614/discussion-between-vesnog-and-cmaureir). – Vesnog Jan 30 '19 at 22:53
  • Everything works fine on Linux, yet I should have it working on Windows. – Vesnog Jan 30 '19 at 23:30
  • Nice. Exactly a 10 pixel margin on both ends of the QSlider. Is there any way to retrieve this from Qt? – automorphic Feb 21 '20 at 04:11
4

My recommendation would be that you investigate QAbstractSlider which is inherited by QSlider. It contains a method called triggerAction (SliderToMaximum value is 6) which can be used to set the Slider Position when the action is triggered.

Syntax for triggering an action is

 QAbstractSlider.triggerAction (self, SliderAction action)

The values for SliderAction are as follows:

QAbstractSlider.SliderNoAction 0 QAbstractSlider.SliderSingleStepAdd 1

QAbstractSlider.SliderSingleStepSub 2

QAbstractSlider.SliderPageStepAdd 3

QAbstractSlider.SliderPageStepSub 4

QAbstractSlider.SliderToMinimum 5

QAbstractSlider.SliderToMaximum 6

QAbstractSlider.SliderMove 7

(source)

Using PySide2, you can call these methods using

PySide2.QtWidgets.QAbstractSlider.maximum()  //returns the value of the maximum

or

PySide2.QtWidgets.QAbstractSlider.setMaximum(arg) // pass your own value
Rachel Gallen
  • 27,943
  • 21
  • 72
  • 81
  • Thanks for the answer as far as I understood the `PySide2.QtWidgets.QAbstractSlider.setMaximum(arg)` sets the maximum value possible as a integer, whereas the method `PySide2.QtWidgets.QAbstractSlider.maximum() ` returns this value. So how might the positioning of labels affected by this? Sorry I am a little confused and did not understand your statement `The slide rmax value is 6, the minimum is 5, that's why it's cut off probably. I put my suggestions in an answer below. ` in the comment. – Vesnog Jan 31 '19 at 10:13
  • @Vesnog look at the QAbstractSlider slider action values that I put in the answer. Slider ToMinimum is 5, Slider ToMaximum is 6, you said the value you printed was 5... That's what I thought you meant, that it had been at the minimum . – Rachel Gallen Jan 31 '19 at 10:19
  • You can set the maximum value using the setMaximum or else it wouldn't be called that, and there would be no need for an argument. – Rachel Gallen Jan 31 '19 at 10:26
  • That is not the case sorry for the misunderstanding. The result of `print("The max is ", Comp4.maximum(), "The min is :", Comp4.minimum())` is `The max is 5 The min is : 1` – Vesnog Jan 31 '19 at 10:46
2

After fiddling around more I finally found a solution. It includes styling sheets and I had tried it before. However, back then I was not able to implement it correctly. II kept the widget size constant, but decreased the groove length to be able to fit the labels inside the printable area. The complete code is below:

import sys
from PySide2.QtCore import *
from PySide2.QtWidgets import *
from PySide2.QtGui import *

slider_x = 150
slider_y = 450
slider_step = [0.01, 0.1, 1, 10, 100]  # in microns
groove_y = 400
handle_height = 10


class MySlider(QSlider):
    def __init__(self, type, parent=None):
        # super(MySlider, self).__init__(parent)
        super().__init__()
        self.parent = parent
        self.Type = type
        # self.setFixedHeight(115)
        self.setStyleSheet("""QSlider::groove:vertical {
    border: 1px solid black;
    height: """ + str(groove_y) + """ px;
    width: 10px;
    border-radius: 2px;
    }

    QSlider::handle:vertical {
        background: red;
        border: 1px solid red;
        height: """ + str(handle_height) + """ px;
        margin: 2px 0;
        border-radius: 1px;
    }

    QSlider::add-page:vertical {
        background: blue;
    }
    QSlider::sub-page:vertical {
        background: red;
""")

    def paintEvent(self, event):
        super(MySlider, self).paintEvent(event)
        qp = QPainter(self)
        pen = QPen()
        pen.setWidth(2)
        pen.setColor(Qt.black)

        qp.setPen(pen)
        font = QFont('Times', 10)
        qp.setFont(font)
        self.setContentsMargins(50, 50, 50, 50)
        self.setFixedSize(QSize(slider_x, slider_y))
        contents = self.contentsRect()
        max = self.maximum()
        min = self.minimum()

        y_inc = slider_y - (slider_y - groove_y) / 2
        for i in range(len(slider_step)):
            qp.drawText(contents.x() - 2 * font.pointSize(), y_inc + font.pointSize() / 2, '{0:3}'.format(slider_step[i]))
            qp.drawLine(contents.x() + 2 * font.pointSize(), y_inc, contents.x() + contents.width(), y_inc)
            y_inc -= groove_y / (max - min)



class Window(QWidget):
    """ Inherits from QWidget """
    def __init__(self):
        super().__init__()
        self.title = 'Control Stages'
        self.left = 10
        self.top = 10
        self.width = 320
        self.height = 100
        self.AxesMapping = [0, 1, 2, 3]
        self.initUI()

    def initUI(self):
        """ Initializes the GUI either using the grid layout or the absolute position layout"""
        self.setWindowTitle(self.title)
        self.setGeometry(self.left, self.top, self.width, self.height)
        Comp4 = self.createSlider("step_size")
        Comp5 = self.createSlider("speed")
        windowLayout = QGridLayout()
        windowLayout.setContentsMargins(50, 50, 50, 50)
        HGroupBox = QGroupBox()
        layout = QGridLayout()
        layout.addWidget(Comp4, 0, 0)
        layout.addWidget(Comp5, 0, 1)
        HGroupBox.setLayout(layout)
        HGroupBox.setFixedSize(QSize(740, 480))
        windowLayout.addWidget(HGroupBox, 0, 0)
        self.setLayout(windowLayout)
        self.show()

    def createSlider(self, variant):
        Slider = MySlider(Qt.Vertical)
        Slider.Type = variant
        Slider.setMaximum(len(slider_step))
        Slider.setMinimum(1)
        Slider.setSingleStep(1)
        Slider.setTickInterval(1)
        Slider.valueChanged.connect(lambda: self.sliderChanged(Slider))
        return Slider

    @staticmethod
    def sliderChanged(Slider):
        print("Slider value changed to ", Slider.value(), "slider type is ", Slider.Type)
        if Slider.Type == "step_size":
            print("this is a step size slider")
        elif Slider.Type == "speed":
            print("this is a speed slider")


if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = Window()
    sys.exit(app.exec_())
Vesnog
  • 773
  • 2
  • 16
  • 33