【问题标题】:Is it possible to add text on top of a scrollbar?是否可以在滚动条顶部添加文本?
【发布时间】:2019-11-04 13:51:16
【问题描述】:

我想在左端、右端和滑块上添加一些文字,如下图所示

我不明白如何在小部件顶部添加文本 这里是Qscrollbar 的最小示例(无文本)

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


class Viewer(QMainWindow):
    def __init__(self, parent=None):
        super(Viewer, self).__init__()
        self.parent = parent 
        self.centralWidget = QWidget()
        self.setCentralWidget(self.centralWidget)
        self.mainVBOX_param_scene = QVBoxLayout()

        self.paramPlotV = QVBoxLayout()
        self.horizontalSliders = QScrollBar(Qt.Horizontal)
        self.horizontalSliders.setMinimum(0)
        self.horizontalSliders.setMaximum(10)
        self.horizontalSliders.setPageStep(1)

        self.paramPlotV.addWidget(self.horizontalSliders) 
        self.centralWidget.setLayout(self.paramPlotV)


def main():
    app = QApplication(sys.argv)
    app.setStyle('Windows')
    ex = Viewer(app)
    ex.showMaximized()
    sys.exit(app.exec())


if __name__ == '__main__':
    main()

【问题讨论】:

  • 是的,有可能:stackoverflow.com/q/22606122/984421.
  • 我不知道如何在 python 中使用它。我应该写一个继承自 QScrollBar 的新类吗?
  • @ymmx 是的............

标签: python text slider pyqt5 scrollbar


【解决方案1】:

有两种可能的方法,它们都使用QStyle 来获取滑块的几何形状和 subPage/addPage 矩形(滑块外部和按钮内部的“空间”,如果它们可见的话)。

子类 QScrollBar 并覆盖 paintEvent()

这里我们覆盖滚动条的paintEvent(),调用基类实现(绘制滚动条小部件)并在其上绘制文本。
为了得到我们要绘制的矩形,我们创建了一个QStyleOptionSlider,它是一个QStyleOption子类,用于任何基于滑块的小部件(包括滚动条); QStyleOption 包含 QStyle 绘制图形元素所需的所有信息,其子类允许 QStyle 找出如何绘制复杂元素(例如滚动条)或控制针对任何鼠标事件的行为。

class PaintTextScrollBar(QScrollBar):
    preText = 'pre text'
    postText = 'post text'
    sliderText = 'slider'
    def paintEvent(self, event):
        # call the base class paintEvent, which will draw the scrollbar
        super().paintEvent(event)

        # create a suitable styleoption and "init" it to this instance
        option = QStyleOptionSlider()
        self.initStyleOption(option)

        painter = QPainter(self)

        # get the slider rectangle
        sliderRect = self.style().subControlRect(QStyle.CC_ScrollBar, 
            option, QStyle.SC_ScrollBarSlider, self)
        # if the slider text is wider than the slider width, adjust its size;
        # note: it's always better to add some horizontal margin for text
        textWidth = self.fontMetrics().width(self.sliderText)
        if textWidth > sliderRect.width():
            sideWidth = (textWidth - sliderRect.width()) / 2
            sliderRect.adjust(-sideWidth, 0, sideWidth, 0)
        painter.drawText(sliderRect, Qt.AlignCenter, 
            self.sliderText)

        # get the "subPage" rectangle and draw the text
        subPageRect = self.style().subControlRect(QStyle.CC_ScrollBar, 
            option, QStyle.SC_ScrollBarSubPage, self)
        painter.drawText(subPageRect, Qt.AlignLeft|Qt.AlignVCenter, self.preText)

        # get the "addPage" rectangle and draw its text
        addPageRect = self.style().subControlRect(QStyle.CC_ScrollBar, 
            option, QStyle.SC_ScrollBarAddPage, self)
        painter.drawText(addPageRect, Qt.AlignRight|Qt.AlignVCenter, self.postText)

这种方法非常有效,可能适用于大多数简单的情况,但只要文本大于滑块句柄的大小,就会出现问题,因为 Qt 决定了滑块的范围基于其整体大小及其最小值和最大值之间的范围。
虽然您可以调整正在绘制文本的矩形的大小(正如我在示例中所做的那样),但它远非完美:每当滑块文本太宽时,它可能会绘制 在“pre”和“post”文本上方,如果滑块靠近边缘,会使整个滚动条非常难看,因为文本可能会覆盖箭头按钮:

注意:“未调整”文本矩形的结果将与上图中的第一个滚动条相同,文本“剪辑”到滑块几何形状。

使用代理样式

QProxyStyleQStyle 的后代,它通过提供一种简单的方法来仅覆盖 现有 样式的方法,从而使子类化变得更加容易。 我们最感兴趣的函数是drawComplexControl(),它是Qt用来绘制旋转框和滚动条等复杂控件的函数。通过仅实现此功能,只要将自定义样式应用于标准 QScrollBar,其行为将与上述 paintEvent() 方法完全相同。

(代理)样式真正有帮助的是能够改变几乎任何小部件的整体外观和行为。
为了能够充分利用它的大部分功能,我实现了另一个 QScrollBar 子类,允许更多的自定义,同时覆盖其他重要的 QProxyStyle 函数。

class TextScrollBarStyle(QProxyStyle):
    def drawComplexControl(self, control, option, painter, widget):
        # call the base implementation which will draw anything Qt will ask
        super().drawComplexControl(control, option, painter, widget)
        # check if control type and orientation match
        if control == QStyle.CC_ScrollBar and option.orientation == Qt.Horizontal:
            # the option is already provided by the widget's internal paintEvent;
            # from this point on, it's almost the same as explained above, but 
            # setting the pen might be required for some styles
            painter.setPen(widget.palette().color(QPalette.WindowText))
            margin = self.frameMargin(widget) + 1

            sliderRect = self.subControlRect(control, option, 
                QStyle.SC_ScrollBarSlider, widget)
            painter.drawText(sliderRect, Qt.AlignCenter, widget.sliderText)

            subPageRect = self.subControlRect(control, option, 
                QStyle.SC_ScrollBarSubPage, widget)
            subPageRect.setRight(sliderRect.left() - 1)
            painter.save()
            painter.setClipRect(subPageRect)
            painter.drawText(subPageRect.adjusted(margin, 0, 0, 0), 
                Qt.AlignLeft|Qt.AlignVCenter, widget.preText)
            painter.restore()

            addPageRect = self.subControlRect(control, option, 
                QStyle.SC_ScrollBarAddPage, widget)
            addPageRect.setLeft(sliderRect.right() + 1)
            painter.save()
            painter.setClipRect(addPageRect)
            painter.drawText(addPageRect.adjusted(0, 0, -margin, 0), 
                Qt.AlignRight|Qt.AlignVCenter, widget.postText)
            painter.restore()

    def frameMargin(self, widget):
        # a helper function to get the default frame margin which is usually added
        # to widgets and sub widgets that might look like a frame, which usually
        # includes the slider of a scrollbar
        option = QStyleOptionFrame()
        option.initFrom(widget)
        return self.pixelMetric(QStyle.PM_DefaultFrameWidth, option, widget)

    def subControlRect(self, control, option, subControl, widget):
        rect = super().subControlRect(control, option, subControl, widget)
        if (control == QStyle.CC_ScrollBar 
            and isinstance(widget, StyledTextScrollBar)
            and option.orientation == Qt.Horizontal):
                if subControl == QStyle.SC_ScrollBarSlider:
                    # get the *default* groove rectangle (the space in which the
                    # slider can move)
                    grooveRect = super().subControlRect(control, option, 
                        QStyle.SC_ScrollBarGroove, widget)
                    # ensure that the slider is wide enough for its text
                    width = max(rect.width(), 
                        widget.sliderWidth + self.frameMargin(widget))
                    # compute the position of the slider according to the
                    # scrollbar value and available space (the "groove")
                    pos = self.sliderPositionFromValue(widget.minimum(), 
                        widget.maximum(), widget.sliderPosition(), 
                        grooveRect.width() - width)
                    # return the new rectangle
                    return QRect(grooveRect.x() + pos, 
                        (grooveRect.height() - rect.height()) / 2, 
                        width, rect.height())
                elif subControl == QStyle.SC_ScrollBarSubPage:
                    # adjust the rectangle based on the slider
                    sliderRect = self.subControlRect(
                        control, option, QStyle.SC_ScrollBarSlider, widget)
                    rect.setRight(sliderRect.left())
                elif subControl == QStyle.SC_ScrollBarAddPage:
                    # same as above
                    sliderRect = self.subControlRect(
                        control, option, QStyle.SC_ScrollBarSlider, widget)
                    rect.setLeft(sliderRect.right())
        return rect

    def hitTestComplexControl(self, control, option, pos, widget):
        if control == QStyle.CC_ScrollBar:
            # check click events against the resized slider
            sliderRect = self.subControlRect(control, option, 
                QStyle.SC_ScrollBarSlider, widget)
            if pos in sliderRect:
                return QStyle.SC_ScrollBarSlider
        return super().hitTestComplexControl(control, option, pos, widget)


class StyledTextScrollBar(QScrollBar):
    def __init__(self, sliderText='', preText='', postText=''):
        super().__init__(Qt.Horizontal)
        self.setStyle(TextScrollBarStyle())
        self.preText = preText
        self.postText = postText
        self.sliderText = sliderText
        self.sliderTextMargin = 2
        self.sliderWidth = self.fontMetrics().width(sliderText) + self.sliderTextMargin + 2

    def setPreText(self, text):
        self.preText = text
        self.update()

    def setPostText(self, text):
        self.postText = text
        self.update()

    def setSliderText(self, text):
        self.sliderText = text
        self.sliderWidth = self.fontMetrics().width(text) + self.sliderTextMargin + 2

    def setSliderTextMargin(self, margin):
        self.sliderTextMargin = margin
        self.sliderWidth = self.fontMetrics().width(self.sliderText) + margin + 2

    def sizeHint(self):
        # give the scrollbar enough height for the font
        hint = super().sizeHint()
        if hint.height() < self.fontMetrics().height() + 4:
            hint.setHeight(self.fontMetrics().height() + 4)
        return hint

使用基本的paintEvent覆盖、将样式应用到标准QScrollBar和使用完全“启用样式”的滚动条和完全实现的子类之间有很多区别;如您所见,当前样式(或为自定义代理样式选择的baseStyle)在外观上可能不太友好:

两(三)种方法之间的变化以及您最终决定使用的方法取决于您的需求;如果您需要向滚动条添加其他功能(或向文本内容或其外观添加更多控制)并且文本不是很宽,您可能想要使用子类化;另一方面,QProxyStyle 方法也可能有助于控制其他方面或元素。
请记住,如果 QStyle 没有在 QApplication 构造函数之前设置,则应用的样式可能不会完美地使用:与 QFont 和 QPalette 相比,QStyle 不会传播到它所应用的 QWidget 的子代(意思是必须通知新的代理样式有关父样式的更改并做出相应的行为)。

class HLine(QFrame):
    def __init__(self):
        super().__init__()
        self.setFrameShape(self.HLine|self.Sunken)


class Example(QWidget):
    def __init__(self):
        QWidget.__init__(self)
        layout = QVBoxLayout(self)

        layout.addWidget(QLabel('Base subclass with paintEvent override, small text:'))
        shortPaintTextScrollBar = PaintTextScrollBar(Qt.Horizontal)
        layout.addWidget(shortPaintTextScrollBar)

        layout.addWidget(QLabel('Same as above, long text (text rect adjusted to text width):'))
        longPaintTextScrollBar = PaintTextScrollBar(Qt.Horizontal)
        longPaintTextScrollBar.sliderText = 'I am a very long slider'
        layout.addWidget(longPaintTextScrollBar)

        layout.addWidget(HLine())

        layout.addWidget(QLabel('Base QScrollBar with drawComplexControl override of proxystyle:'))
        shortBasicScrollBar = QScrollBar(Qt.Horizontal)
        layout.addWidget(shortBasicScrollBar)
        shortBasicScrollBar.sliderText = 'slider'
        shortBasicScrollBar.preText = 'pre text'
        shortBasicScrollBar.postText = 'post text'
        shortBasicScrollBar.setStyle(TextScrollBarStyle())

        layout.addWidget(QLabel('Same as above, long text (text rectangle based on slider geometry):'))
        longBasicScrollBar = QScrollBar(Qt.Horizontal)
        layout.addWidget(longBasicScrollBar)
        longBasicScrollBar.sliderText = 'I am a very long slider'
        longBasicScrollBar.preText = 'pre text'
        longBasicScrollBar.postText = 'post text'
        longBasicScrollBar.setStyle(TextScrollBarStyle())

        layout.addWidget(HLine())

        layout.addWidget(QLabel('Subclasses with full proxystyle implementation, all available styles:'))
        for styleName in QStyleFactory.keys():
            scrollBar = StyledTextScrollBar()
            layout.addWidget(scrollBar)
            scrollBar.setSliderText('Long slider with {} style'.format(styleName))
            scrollBar.setStyle(TextScrollBarStyle(QStyleFactory.create(styleName)))
            scrollBar.valueChanged.connect(self.setScrollBarPreText)
            scrollBar.setPostText('Post text')

        for scrollBar in self.findChildren(QScrollBar):
            scrollBar.setValue(7)

    def setScrollBarPreText(self, value):
        self.sender().setPreText(str(value))


if __name__ == '__main__':
    app = QApplication(sys.argv)
    example = Example()
    example.show()
    sys.exit(app.exec_())

【讨论】:

  • 哇!正是我想要的。我不太可能找到这么短的解决方案。我在您的类中添加了三个变量以在滑块移动时修改文本。非常感谢。
  • 为了让文本超过滑块句柄限制,我只是用sliderRect.adjust(-200, 0, 200, 0) 调整了sliderRect 的大小。这为文本提供了足够的空间
  • 我强烈反对这样做:请记住,这样做只会扩大文本的可用空间,而不是滑块。您最终可能会得到覆盖滑块几何边界的文本,可能会在 其他两个“页面”文本矩形甚至滚动条按钮上绘制,从而使控件使用起来非常不舒服(而不是很好看)。即使您应用了一些剪辑,只要滑块靠近边缘,这显然会隐藏部分滑块文本。
  • @ymmx 我已经用更好的 QProxyStyle 实现更新了我的答案,允许在文本更宽的情况下更好地调整滑块的大小。
  • 非常感谢。我不得不说,我可能需要几周的时间才能找到一种方法来正确地做到这一点。在这个问题之前我不知道 QProxyStyle ,但它似乎非常有用。我会尝试找到一些关于它的教程。非常感谢。
猜你喜欢
  • 1970-01-01
  • 2021-04-25
  • 1970-01-01
  • 2020-12-17
  • 2019-04-02
  • 1970-01-01
  • 1970-01-01
  • 2017-06-15
  • 1970-01-01
相关资源
最近更新 更多