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