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).
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.
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)
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.
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
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_())
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With