Можно ли добавить текст поверх полосы прокрутки? - PullRequest
0 голосов
/ 04 ноября 2019

Я хотел бы добавить текст к левой стороне, правой стороне и ползунку, как показано на рисунке ниже

enter image description here

Я не понимаю, как я могу добавить текст поверх виджета здесь минимальный пример 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()

1 Ответ

2 голосов
/ 05 ноября 2019

Существует два возможных подхода, и оба они используют QStyle, чтобы получить геометрию ползунка и прямоугольников subPage / addPage («пробелы» вне ползунка и внутри его кнопок, еслиони видны).

Подкласс QScrollBar и переопределение paintEvent()

Здесь мы переопределяем paintEvent() полосы прокрутки, вызываем реализацию базового класса (которая рисует виджет полосы прокрутки) инарисовать текст поверх it.
Чтобы получить прямоугольник, в котором мы будем рисовать, мы создаем 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», и сделать всю полосу прокрутки очень некрасивой, если ползунок находится вблизи краев, так кактекст может покрывать кнопки со стрелками:

That's an ugly scroll bar!

Примечание: результат «ненастроенного» прямоугольника текстаугол будет таким же, как у первой полосы прокрутки на изображении выше, с текстом, «обрезанным» по геометрии слайдера.

Используйте прокси-стиль

QProxyStyle isпотомок QStyle , который упрощает создание подклассов, предоставляя простой способ переопределить только методы существующего стиля . Нам больше всего интересна функция drawComplexControl(), которую Qt использует для рисования сложных элементов управления, таких как спин-боксы и полосы прокрутки. Реализуя только эту функцию, поведение будет точно таким же, как в методе paintEvent(), описанном выше, при условии, что вы применяете пользовательский стиль к стандартному QScrollBar.

Какой стиль (прокси) действительно может помочьС его помощью можно изменять общий вид и поведение практически любого виджета.
Чтобы использовать большинство его функций, я реализовал еще один подкласс 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, выбранный для пользовательского стиля прокси) может быть не очень дружелюбным по внешнему виду:

Almost very beautiful scroll bars!

Какие изменения между двумя (тремя) подходами и что вы в итоге решите использовать, зависит от ваших потребностей;если вам нужно добавить другие функции на полосу прокрутки (или добавить больше контроля к текстовому содержимому или его внешнему виду), и текст не очень широк, вы можете перейти к созданию подклассов;с другой стороны, подход 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_())
...