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 的来源,以便了解如何去做。