Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to expand the drawable area around the QSlider

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

like image 770
Vesnog Avatar asked Jan 17 '19 22:01

Vesnog


3 Answers

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.

like image 92
cmaureir Avatar answered Oct 21 '22 05:10

cmaureir


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
like image 4
Rachel Gallen Avatar answered Oct 21 '22 04:10

Rachel Gallen


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_())
like image 2
Vesnog Avatar answered Oct 21 '22 05:10

Vesnog