Is it possible to add text on top of a scrollbar?

北城以北 提交于 2020-12-13 04:44:21

问题


I would like to add some text to the left end side, the right end side and on the slider as in the figure below

I don't understand how I can add text on top of a widget here the minimal example of the Qscrollbar (without texts)

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()

回答1:


There are two possible approaches, and both of them use QStyle to get the geometry of the slider and the subPage/addPage rectangles (the "spaces" outside the slider and within its buttons, if they are visible).

Subclass QScrollBar and override paintEvent()

Here we override the paintEvent() of the scroll bar, call the base class implementation (which paints the scroll bar widget) and draw the text over it.
To get the rectangle where we're going to draw, we create a QStyleOptionSlider, which is a QStyleOption sub class used for any slider based widget (including scroll bars); a QStyleOption contains all the information QStyle needs to draw graphical elements, and its subclasses allow QStyle to find out how to draw complex elements such as scroll bars or control the behavior against any mouse event.

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)

This approach is very effective and may be fine for most simple cases, but there will be problems whenever the text is wider than the size of the slider handle, since Qt decides the extent of the slider based on its overall size and the range between its minimum and maximum values.
While you can adjust the size of the rectangle you're drawing text (as I've done in the example), it will be far from perfect: whenever the slider text is too wide it might draw over the "pre" and "post" text, and make the whole scrollbar very ugly if the slider is near the edges, since the text might cover the arrow buttons:

Note: the result of a "non adjusted" text rectangle would be the same as the first scroll bar in the image above, with the text "clipped" to the slider geometry.

Use a proxy style

QProxyStyle is a QStyle descendant that makes subclassing easier by providing an easy way to override only methods of an existing style. The function we're most interested in is drawComplexControl(), which is what Qt uses to draw complex controls like spin boxes and scroll bars. By implementing this function only, the behavior will be exactly the same as the paintEvent() method explained above, as long as you apply the custom style to a standard QScrollBar.

What a (proxy) style could really help with is being able to change the overall appearance and behavior of almost any widget.
To be able to take the most of its features, I've implemented another QScrollBar subclass, allowing much more customization, while overriding other important QProxyStyle functions.

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

There's a lot of difference between using the basic paintEvent override, applying the style to a standard QScrollBar and using a full "style-enabled" scroll bar with a fully implemented subclass; as you can see it's always possible that the current style (or the baseStyle chosen for the custom proxy style) might not be very friendly in its appearance:

What changes between the two (three) approaches and what you will finally decide to use depends on your needs; if you need to add other features to the scroll bar (or add more control to text contents or their apparance) and the text is not very wide, you might want to go with subclassing; on the other hand, the QProxyStyle approach might be useful to control other aspects or elements too.
Remember that if the QStyle is not set before the QApplication constructor, it's possible that the applied style won't be perfect to work with: as opposed with QFont and QPalette, QStyle is not propagated to the children of the QWidget it's applied to (meaning that the new proxy style has to be notified about the parent style change and behave accordingly).

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_())


来源:https://stackoverflow.com/questions/58694821/is-it-possible-to-add-text-on-top-of-a-scrollbar

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!