QSlider:在任意索引处添加 "special" 个刻度标记
QSlider: add "special" tick markers at arbitrary indices
我想知道是否有可能以及在 QSlider 的任意索引处添加特殊刻度的最简单方法是什么。非常感谢这方面的任何信息或文档。
为了阐明我想要实现的目标,这里有一个应用案例:我有一个带有给定刻度数的 QSlider,我可以使用图中粘贴的函数来控制它(截图来自documentation):
如何在给定的刻度索引处添加黑色小三角形或任何其他“特殊”刻度?另外,我想在其他任意位置重新绘制它们,这意味着它们不会停留在静态位置。
(我从 this SO answer 开始,但从那里我无法朝着我的目标前进)。
这是一个基本实现(使用QPixmap
):
class NewSlider(QtWidgets.QSlider):
indicator_up = None
def __init__(self, *args):
# for now, this class is prepared for horizontal sliders only
super().__init__(*args)
self._secondary_slider_pos = []
if self.__class__.indicator_up is None:
indicator_up = QPixmap(r'path_to_image.png')
center = self.height() / 2
if indicator_up.height() > center:
indicator_up = indicator_up.scaledToHeight(center)
self.__class__.indicator_up = indicator_up
def set_secondary_slider_pos(self, other_pos: List[int]):
self._secondary_slider_pos = other_pos
def get_px_of_secondary_slider_pos(self):
return [
QtWidgets.QStyle.sliderPositionFromValue(self.minimum(), self.maximum(), idx, self.width())
for idx in self._secondary_slider_pos
]
def paintEvent(self, ev: QtGui.QPaintEvent) -> None:
super().paintEvent(ev)
pix_secondary_slider_pos = self.get_px_of_secondary_slider_pos()
if len(pix_secondary_slider_pos) > 0:
painter = QtGui.QPainter(self)
center = self.height() / 2
for x_pos in pix_secondary_slider_pos:
painter.drawPixmap(QtCore.QPoint(x_pos, center), self.__class__.indicator_up)
用法示例:
不知怎么的,我无法让它与 painter.drawImage
一起工作。
使用的图像是:
sliderPositionFromValue
不能只用滑块本身的宽度(或高度),因为每种样式绘制滑块的方式不同,space要考虑句柄移动通常小于小部件的实际大小。
手柄移动实际使用的 space 被考虑到整个范围(像素度量 PM_SliderSpaceAvailable
),其中包括手柄本身的大小。
因此,您需要考虑 space 在计算指标位置时,减去手柄大小的一半,同时减去指标大小的一半(否则三角形的顶部不会重合)位置正确)。
这是您的答案的更正版本:
class NewSlider(QtWidgets.QSlider):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._secondary_slider_pos = []
@property
def indicator(self):
try:
return self._indicator
except AttributeError:
image = QtGui.QPixmap('triangle.png')
if self.orientation() == QtCore.Qt.Horizontal:
height = self.height() / 2
if image.height() > height:
image = image.scaledToHeight(
height, QtCore.Qt.SmoothTransformation)
else:
width = self.width() / 2
if image.height() > width:
image = image.scaledToHeight(
width, QtCore.Qt.SmoothTransformation)
rotated = QtGui.QPixmap(image.height(), image.width())
rotated.fill(QtCore.Qt.transparent)
qp = QtGui.QPainter(rotated)
qp.rotate(-90)
qp.drawPixmap(-image.width(), 0, image)
qp.end()
image = rotated
self._indicator = image
return self._indicator
def set_secondary_slider_pos(self, other_pos):
self._secondary_slider_pos = other_pos
self.update()
def paintEvent(self, event):
super().paintEvent(event)
if not self._secondary_slider_pos:
return
style = self.style()
opt = QtWidgets.QStyleOptionSlider()
self.initStyleOption(opt)
# the available space for the handle
available = style.pixelMetric(style.PM_SliderSpaceAvailable, opt, self)
# the extent of the slider handle
sLen = style.pixelMetric(style.PM_SliderLength, opt, self) / 2
x = self.width() / 2
y = self.height() / 2
horizontal = self.orientation() == QtCore.Qt.Horizontal
if horizontal:
delta = self.indicator.width() / 2
else:
delta = self.indicator.height() / 2
minimum = self.minimum()
maximum = self.maximum()
qp = QtGui.QPainter(self)
# just in case
qp.translate(opt.rect.x(), opt.rect.y())
for value in self._secondary_slider_pos:
# get the actual position based on the available space and add half
# the slider handle size for the correct position
pos = style.sliderPositionFromValue(
minimum, maximum, value, available, opt.upsideDown) + sLen
# draw the image by removing half of its size in order to center it
if horizontal:
qp.drawPixmap(pos - delta, y, self.indicator)
else:
qp.drawPixmap(x, pos - delta, self.indicator)
def resizeEvent(self, event):
# delete the "cached" image so that it gets generated when necessary
if (self.orientation() == QtCore.Qt.Horizontal and
event.size().height() != event.oldSize().height() or
self.orientation() == QtCore.Qt.Vertical and
event.size().width() != event.oldSize().width()):
try:
del self._indicator
except AttributeError:
pass
请注意,在任何情况下,这种方法都有其局限性:三角形将始终显示在上方手柄,即从用户体验的角度来看,这不是一件好事。 正确的 解决方案需要通过多次调用 drawComplexControl
来部分重写 paintEvent()
,以便以正确的顺序绘制所有元素:凹槽和刻度线,然后是指示器,最后是手柄;可以做到,但你需要添加更多方面(包括考虑当前活动的控件与当前样式的视觉一致性)。
我建议你研究 QSlider 的来源,以便了解如何去做。
我想知道是否有可能以及在 QSlider 的任意索引处添加特殊刻度的最简单方法是什么。非常感谢这方面的任何信息或文档。
为了阐明我想要实现的目标,这里有一个应用案例:我有一个带有给定刻度数的 QSlider,我可以使用图中粘贴的函数来控制它(截图来自documentation):
如何在给定的刻度索引处添加黑色小三角形或任何其他“特殊”刻度?另外,我想在其他任意位置重新绘制它们,这意味着它们不会停留在静态位置。
(我从 this SO answer 开始,但从那里我无法朝着我的目标前进)。
这是一个基本实现(使用QPixmap
):
class NewSlider(QtWidgets.QSlider):
indicator_up = None
def __init__(self, *args):
# for now, this class is prepared for horizontal sliders only
super().__init__(*args)
self._secondary_slider_pos = []
if self.__class__.indicator_up is None:
indicator_up = QPixmap(r'path_to_image.png')
center = self.height() / 2
if indicator_up.height() > center:
indicator_up = indicator_up.scaledToHeight(center)
self.__class__.indicator_up = indicator_up
def set_secondary_slider_pos(self, other_pos: List[int]):
self._secondary_slider_pos = other_pos
def get_px_of_secondary_slider_pos(self):
return [
QtWidgets.QStyle.sliderPositionFromValue(self.minimum(), self.maximum(), idx, self.width())
for idx in self._secondary_slider_pos
]
def paintEvent(self, ev: QtGui.QPaintEvent) -> None:
super().paintEvent(ev)
pix_secondary_slider_pos = self.get_px_of_secondary_slider_pos()
if len(pix_secondary_slider_pos) > 0:
painter = QtGui.QPainter(self)
center = self.height() / 2
for x_pos in pix_secondary_slider_pos:
painter.drawPixmap(QtCore.QPoint(x_pos, center), self.__class__.indicator_up)
用法示例:
不知怎么的,我无法让它与 painter.drawImage
一起工作。
使用的图像是:
sliderPositionFromValue
不能只用滑块本身的宽度(或高度),因为每种样式绘制滑块的方式不同,space要考虑句柄移动通常小于小部件的实际大小。
手柄移动实际使用的 space 被考虑到整个范围(像素度量 PM_SliderSpaceAvailable
),其中包括手柄本身的大小。
因此,您需要考虑 space 在计算指标位置时,减去手柄大小的一半,同时减去指标大小的一半(否则三角形的顶部不会重合)位置正确)。
这是您的答案的更正版本:
class NewSlider(QtWidgets.QSlider):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._secondary_slider_pos = []
@property
def indicator(self):
try:
return self._indicator
except AttributeError:
image = QtGui.QPixmap('triangle.png')
if self.orientation() == QtCore.Qt.Horizontal:
height = self.height() / 2
if image.height() > height:
image = image.scaledToHeight(
height, QtCore.Qt.SmoothTransformation)
else:
width = self.width() / 2
if image.height() > width:
image = image.scaledToHeight(
width, QtCore.Qt.SmoothTransformation)
rotated = QtGui.QPixmap(image.height(), image.width())
rotated.fill(QtCore.Qt.transparent)
qp = QtGui.QPainter(rotated)
qp.rotate(-90)
qp.drawPixmap(-image.width(), 0, image)
qp.end()
image = rotated
self._indicator = image
return self._indicator
def set_secondary_slider_pos(self, other_pos):
self._secondary_slider_pos = other_pos
self.update()
def paintEvent(self, event):
super().paintEvent(event)
if not self._secondary_slider_pos:
return
style = self.style()
opt = QtWidgets.QStyleOptionSlider()
self.initStyleOption(opt)
# the available space for the handle
available = style.pixelMetric(style.PM_SliderSpaceAvailable, opt, self)
# the extent of the slider handle
sLen = style.pixelMetric(style.PM_SliderLength, opt, self) / 2
x = self.width() / 2
y = self.height() / 2
horizontal = self.orientation() == QtCore.Qt.Horizontal
if horizontal:
delta = self.indicator.width() / 2
else:
delta = self.indicator.height() / 2
minimum = self.minimum()
maximum = self.maximum()
qp = QtGui.QPainter(self)
# just in case
qp.translate(opt.rect.x(), opt.rect.y())
for value in self._secondary_slider_pos:
# get the actual position based on the available space and add half
# the slider handle size for the correct position
pos = style.sliderPositionFromValue(
minimum, maximum, value, available, opt.upsideDown) + sLen
# draw the image by removing half of its size in order to center it
if horizontal:
qp.drawPixmap(pos - delta, y, self.indicator)
else:
qp.drawPixmap(x, pos - delta, self.indicator)
def resizeEvent(self, event):
# delete the "cached" image so that it gets generated when necessary
if (self.orientation() == QtCore.Qt.Horizontal and
event.size().height() != event.oldSize().height() or
self.orientation() == QtCore.Qt.Vertical and
event.size().width() != event.oldSize().width()):
try:
del self._indicator
except AttributeError:
pass
请注意,在任何情况下,这种方法都有其局限性:三角形将始终显示在上方手柄,即从用户体验的角度来看,这不是一件好事。 正确的 解决方案需要通过多次调用 drawComplexControl
来部分重写 paintEvent()
,以便以正确的顺序绘制所有元素:凹槽和刻度线,然后是指示器,最后是手柄;可以做到,但你需要添加更多方面(包括考虑当前活动的控件与当前样式的视觉一致性)。
我建议你研究 QSlider 的来源,以便了解如何去做。