I'm trying to create in PyQt6 an input field (location: bottom right of the window) and next to It on the right an "Enter" button (I'm using pg.GraphicView()). I can't use the PySide library because of some interaction problems with the rest of my code. How can I achieve that?
I'm using the following code for generating a button but I can't figure out how to place It at the bottom right of the current window:
view = pg.GraphicsView()
l = pg.GraphicsLayout()
view.setCentralItem(l)
view.show()
proxy = QGraphicsProxyWidget()
button = QPushButton("ENTER")
proxy.setWidget(button)
view.addItem(proxy)
Regarding the input field I tried to implement different things without using PySide but they didn't worked.
The pg.GraphicsView class is actually a subclass of QGraphicsView, which is a standard QWidget that inherits from QAbstractScrollArea.
This means that we can potentially add any child widget to it without interfering with the contents of the scene and also ignoring possible transformations (scaling, rotation, etc.).
The solution, in fact, is quite simple: set the view as parent of the widget (either by using the view as argument in the constructor, or by calling setParent()). Then, what's left is to ensure that the widget geometry is always consistent with the view, so we need to wait for resize events and set the new geometry based on the new size. To achieve this, the simplest solution is to create a subclass.
For explanation purposes, I've implemented a system that allows to set widgets for any "corner" of the view, and defaults to the bottom right corner.
Note: since this question could also be valid for Qt5, in the following code I'm using the "old" enum style (Qt.Align...), for newer versions of Qt (and mandatory for PyQt6), you need to change to Qt.Alignment.Align....
class CustomGraphicsView(pg.GraphicsView):
toolWidgets = None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.toolWidgets = {}
def setToolWidget(self, widget, position=Qt.AlignBottom|Qt.AlignRight):
position = Qt.AlignmentFlag(position)
current = self.toolWidgets.get(position)
if current:
current.deleteLater()
self.toolWidgets[position] = widget
widget.resize(widget.sizeHint())
# ensure that the widget is reparented
widget.setParent(self)
# setParent() automatically hides widgets, we need to
# explicitly call show()
widget.show()
self._updateToolWidgets()
def _updateToolWidgets(self):
if not self.toolWidgets:
return
viewGeo = self.rect()
for pos, widget in self.toolWidgets.items():
rect = widget.rect()
if pos & Qt.AlignLeft:
rect.moveLeft(0)
elif pos & Qt.AlignRight:
rect.moveRight(viewGeo.right())
elif pos & Qt.AlignHCenter:
rect.moveLeft(viewGeo.center().x() - rect.width() / 2)
if pos & Qt.AlignTop:
rect.moveTop(0)
elif pos & Qt.AlignBottom:
rect.moveBottom(viewGeo.bottom())
elif pos & Qt.AlignVCenter:
rect.moveTop(viewGeo.center().y() - rect.height() / 2)
widget.setGeometry(rect)
def resizeEvent(self, event):
super().resizeEvent(event)
self._updateToolWidgets()
# ...
view = CustomGraphicsView()
l = pg.GraphicsLayout()
view.setCentralItem(l)
view.show()
container = QFrame(autoFillBackground=True, objectName='container')
container.setStyleSheet('''
QFrame#container {
background: palette(window);
border: 1px outset palette(mid);
border-radius: 2px;
}
''')
lineEdit = QLineEdit()
button = QPushButton("ENTER")
frameLayout = QHBoxLayout(container)
frameLayout.setContentsMargins(0, 0, 0, 0)
frameLayout.addWidget(lineEdit)
frameLayout.addWidget(button)
view.setToolWidget(container)
bottomLeftButton = QPushButton('Something')
view.setToolWidget(bottomLeftButton, Qt.AlignLeft|Qt.AlignBottom)
Related
So I have a table with 1 row and multiple columns and I use the custom delegate function for the image thumbnail. I also included the set background colour function when I click it. However, it doesn't change the colour of the item background. I have to use the custom delegate function so that my icon image is sticking with each other from different cell without any additional spaces between.
my code is as below
import random
from PyQt5 import QtCore, QtGui, QtWidgets
imagePath2 = "arrowREDHead.png"
imagePath = "arrowREDBody.png"
class IconDelegate(QtWidgets.QStyledItemDelegate):
def paint(self, painter, option, index):
icon = index.data(QtCore.Qt.DecorationRole)
mode = QtGui.QIcon.Normal
if not (option.state & QtWidgets.QStyle.State_Enabled):
mode = QtGui.QIcon.Disabled
elif option.state & QtWidgets.QStyle.State_Selected:
mode = QtGui.QIcon.Selected
state = (
QtGui.QIcon.On
if option.state & QtWidgets.QStyle.State_Open
else QtGui.QIcon.Off
)
pixmap = icon.pixmap(option.rect.size(), mode, state)
painter.drawPixmap(option.rect, pixmap)
def sizeHint(self, option, index):
return QtCore.QSize(20, 20)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
table = QtWidgets.QTableWidget(1, 10)
delegate = IconDelegate(table)
table.setItemDelegate(delegate)
self.setCentralWidget(table)
for j in range(table.columnCount()):
if(j % 2 == 0):
pixmap = QtGui.QPixmap(imagePath)
icon = QtGui.QIcon(pixmap)
it = QtWidgets.QTableWidgetItem()
it.setIcon(icon)
table.setItem(0, j, it)
else:
pixmap = QtGui.QPixmap(imagePath2)
icon = QtGui.QIcon(pixmap)
it = QtWidgets.QTableWidgetItem()
it.setIcon(icon)
table.setItem(0, j, it)
table.item(0, 1).setBackground(QtGui.QColor(100,100,150))
table.resizeRowsToContents()
table.resizeColumnsToContents()
table.setShowGrid(False)
table.itemClicked.connect(self.sequenceClicked)
def sequenceClicked(self, item):
self.itemClicked = item
print("item", item)
item.setBackground(QtGui.QColor(255, 215, 0))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
This is the image with the gap between icons and it form not a perfect looking arrow thats why I used the custom delegate function to stick the icon together and let it look like a perfect arrow
This is the image that has the perfect look of the arrow as it doesnt has any gap between the cells by using the delegate function. However, it doesnt allow me to change the background colour for the cell.
This is the image of the arrowHead
This is the image of the arrowBody
As with any overridden function, if the base implementation is not called, the default behavior is ignored.
paint() is the function responsible of drawing any aspect of the item, including its background. Since in your override you are just drawing the pixmap, everything else is missing, so the solution would be to call the base implementation and then draw the pixmap.
In this specific case, though, this wouldn't be the right choice, as the base implementation already draws the item's decoration, and in certain conditions (assuming that the pixmap has an alpha channel), it will probably result in having the images overlapping.
There are three possible solutions.
Use QStyle functions ignoring the icon
In this case, we do what the base implementation would, which is calling the style drawControl function with the CE_ItemViewItem, but we modify the option before that, removing anything related to the icon:
def paint(self, painter, option, index):
# create a new option (as the existing one should not be modified)
# and initialize it for the current index
option = QtWidgets.QStyleOptionViewItem(option)
self.initStyleOption(option, index)
# remove anything related to the icon
option.features &= ~option.HasDecoration
option.decorationSize = QtCore.QSize()
option.icon = QtGui.QIcon()
if option.widget:
style = option.widget.style()
else:
style = QtWidgets.QApplication.style()
# call the drawing function for view items
style.drawControl(style.CE_ItemViewItem, option, painter, option.widget)
# proceed with the custom drawing of the pixmap
icon = index.data(QtCore.Qt.DecorationRole)
# ...
Update the icon information in the delegate
A QStyledItemDelegate always calls initStyleOption at the beginning of most its functions, including paint and sizeHint. By overriding that function, we can change some aspects of the drawing, including the icon alignment and positioning.
In this specific case, knowing that we have only two types of icons, and they must be right or left aligned depending on the image, it's enough to update the proper decoration size (based on the optimal size of the icon), and then the alignment based on the column.
class IconDelegate(QtWidgets.QStyledItemDelegate):
def initStyleOption(self, option, index):
super().initStyleOption(option, index)
option.decorationSize = option.icon.actualSize(option.rect.size())
if index.column() & 1:
option.decorationPosition = option.Left
option.decorationAlignment = QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter
else:
option.decorationPosition = option.Right
option.decorationAlignment = QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter
Note: this approach only works as long as the images all have the same ratio, and the row height is not manually changed.
Use a custom role for the image
Qt uses roles to store and retrieve various aspects of each index (font, background, etc.), and user roles can be defined to store custom data.
setIcon() actually sets a QIcon in the item using the DecorationRole, and the delegate uses it to draw it. If the data returned for that role is empty/invalid, no icon is drawn.
If we don't use setIcon() but store the icon in the item with a custom role instead, the default drawing functions will obviously not draw any decoration, but we can still access it from the delegate.
# we usually define roles right after the imports, as they're constants
ImageRole = QtCore.Qt.UserRole + 1
class IconDelegate(QtWidgets.QStyledItemDelegate):
def paint(self, painter, option, index):
super().paint(painter, option, index)
icon = index.data(ImageRole)
# ...
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
# ...
it = QtWidgets.QTableWidgetItem()
it.setData(ImageRole, pixmap)
I have a QTreeWidget where the TopLevelIteps are replaced by a custom widget. Said widget have its maximum and minimum Heights animated with a QStateMachine.
QTreeWidget (GIF):
Custom Widget Animation (GIF):
The problem is that the rows will not adjust its height to fit the custom widget when it expands:
Row height fixed size (GIF):
Causing overlap between the widget items instead of pushing each other away like this:
The results that I'm after (GIF):
I tried using setSizeHint() on the top level item to but it creates a big empty space between items/widgets:
Using setSizeHint()(GIF):
I'm thinking that maybe I have to implement sizeHint() but I'm not really sure what to put there. Or is there a better approach to this problem?
I would really appreciate some hints.
Example code:
# Custom Widget
class ExpandableFrame(QFrame):
def __init__(self):
QFrame.__init__(self)
# Default Properties
self.setFixedSize(200,50)
self.setStyleSheet('background-color: #2e4076; border: 2px solid black; border-radius: 10px;')
# Setup expand button
self.expandToggle = QToolButton()
self.expandToggle.setText('[]')
self.expandToggle.setMaximumSize(10,20)
self.expandToggle.setCursor(Qt.PointingHandCursor)
# Setup layout
layout = QHBoxLayout()
layout.setAlignment(Qt.AlignRight)
layout.addWidget(self.expandToggle)
self.setLayout(layout)
self.expandArea()
# Animates minimumHeight and maximumHeight
def expandArea(self):
heigth = self.height()
newHeigth = 100
machine = QStateMachine(self)
state1 = QState()
state1.assignProperty(self, b'minimumHeight', heigth)
state1.assignProperty(self, b'maximumHeight', heigth)
state2 = QState()
state2.assignProperty(self, b'minimumHeight', newHeigth)
state2.assignProperty(self, b'maximumHeight', newHeigth)
# Create animations
expandAnim = QPropertyAnimation(self, 'minimumHeight')
closeAnim = QPropertyAnimation(self, 'maximumHeight')
expandAnim.setDuration(125)
closeAnim.setDuration(125)
expandAnim.setEasingCurve(QEasingCurve.Linear)
# Create event transitions
eventTransition1 = QEventTransition(self.expandToggle, QEvent.MouseButtonPress)
eventTransition1.setTargetState(state2)
eventTransition1.addAnimation(expandAnim)
state1.addTransition(eventTransition1)
eventTransition2 = QEventTransition(self.expandToggle, QEvent.MouseButtonPress)
eventTransition2.setTargetState(state1)
eventTransition2.addAnimation(closeAnim)
state2.addTransition(eventTransition2)
# Add the states to the machine
machine.addState(state1)
machine.addState(state2)
machine.setInitialState(state1)
machine.start()
# Tree Widget
class CustomTree(QTreeWidget):
customWidget = []
def __init__(self, parent=None):
QTreeWidget.__init__(self, parent)
# Create top level items
itemCount = 3
for element in range(itemCount):
topLevelItem = QTreeWidgetItem()
self.addTopLevelItem(topLevelItem)
# Replace the top level items with the expandable custom widget:
# Get the Model Index to each item
modelIndex = self.indexFromItem(topLevelItem, 0)
# Create the ExpandableFrame widgets for all the top level items
self.customWidget.append(ExpandableFrame())
# Set the widget to each top level item based on its index
self.setIndexWidget(modelIndex, self.customWidget[element])
# Create more items and add them as children of the top level items
for x in range(itemCount):
child = QTreeWidgetItem()
child.setText(0,'Child')
topLevelItem.addChild(child)
This is how the example looks:
Minimal Code Example running:
One solution could be to emit the sizeHintChanged signal of the item delegate of your view every time the size of any of the widgets is changed. This tells the view that the position of the items should be updated. To achieve this you could override the resizeEvent of ExpandableFrame and emit a custom signal, e.g.
class ExpandableFrame(QFrame):
resized = pyqtSignal(QVariant)
def resizeEvent(self, event):
self.resized.emit(self.height())
super().resizeEvent(event)
In CustomTree, you can then connect ExpandableFrame.resized to the sizeHintChanged signal of the delegate of the custom tree by overriding setIndexWidget, e.g.
class CustomTree(QTreeWidget):
...
def setIndexWidget(self, index, widget):
super().setIndexWidget(index, widget)
if isinstance(widget, ExpandableFrame):
widget.resized.connect(lambda: self.itemDelegate().sizeHintChanged.emit(index))
Screen capture:
The disadvantage of this approach is that you would need to update the connections if the the widgets are moved or replaced.
I want to put a QLineEdit and QLable to the same cell in QTableWidget. This cell widget has created the code below I modified from the internet.
class HiddenLabel(QLabel):
'''
QLable hide when mouse pressed
'''
def __init__(self, buddy, taskline, parent = None):
super(HiddenLabel, self).__init__(parent)
self.setFixedHeight(30)
self.buddy = buddy
self.taskline = taskline
# When it's clicked, hide itself and show its buddy
def mousePressEvent(self, event):
# left click to edit
if event.button() == QtCore.Qt.LeftButton:
self.hide()
self.buddy.setText(self.taskline.plain_text)
self.buddy.show()
self.buddy.setFocus() # Set focus on buddy so user doesn't have to click again
class EditableCell(QWidget):
'''
QLineEdit show when HiddenLabel is hidden
'''
def __init__(self, taskline, parent = None):
super(EditableCell, self).__init__(parent)
self.taskline = taskline
# Create ui
self.myEdit = QLineEdit()
self.myEdit.setFixedHeight(30)
self.myEdit.hide() # Hide line edit
self.myEdit.editingFinished.connect(self.textEdited)
# Create our custom label, and assign myEdit as its buddy
self.myLabel = HiddenLabel(self.myEdit, self.taskline)
self.myLabel.setText(self.taskline.enrich_text())
# Change vertical size policy so they both match and you don't get popping when switching
#self.myLabel.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
# Put them under a layout together
hLayout = QHBoxLayout()
hLayout.addWidget(self.myLabel)
hLayout.addWidget(self.myEdit)
self.setLayout(hLayout)
def textEdited(self):
# If the input is left empty, revert back to the label showing
print('edit finished')
print(self.myEdit.text())
taskline = TaskLine()
taskline.parser(self.myEdit.text())
self.taskline = taskline
self.myLabel.setText(taskline.enrich_text())
self.myEdit.hide()
self.myLabel.show()
The cell will change between QLineEdit and QLabel by left click.
As figured in the screenshot, I want to remove the blank space between the cell border and cell widget.
I think it can be adjusted by style setting, but I didn't find any useful documents about Qt style setting. I hope someone could give some use
You must set the layout margins to 0:
hLayout.setContentsMargins(0, 0, 0, 0)
since as the docs point out they are dependent on the style and platform:
void QLayout::setContentsMargins(int left, int top, int right, int
bottom)
Sets the left, top, right, and bottom margins to use around
the layout.
By default, QLayout uses the values provided by the style. On most
platforms, the margin is 11 pixels in all directions.
I have currently got a PyQt5 application which is quite simple (just one button). I'd like for it to have vibrancy, so I've ported ObjC vibrancy code to Python. My vibrancy code is as follows:
frame = NSMakeRect(0, 0, * self.get_screen_size()) # get_screen_size returns the resolution of the monitor
view = objc.objc_object(c_void_p=self.winId().__int__()) # returns NSView of current window
visualEffectView = NSVisualEffectView.new()
visualEffectView.setAutoresizingMask_(NSViewWidthSizable|NSViewHeightSizable) # equivalent to: visualEffectView.autoresizingMask = NSViewW...
visualEffectView.setFrame_(frame)
visualEffectView.setState_(NSVisualEffectStateActive)
visualEffectView.setMaterial_(NSVisualEffectMaterialDark)
visualEffectView.setBlendingMode_(NSVisualEffectBlendingModeBehindWindow)
window = view.window()
window.contentView().addSubview_positioned_relativeTo_(visualEffectView, NSWindowBelow, None)
# equal to: [window.contentView addSubview:visualEffectView positioned:NSWindowBelow relativeTo:nul]
window.setTitlebarAppearsTransparent_(True)
window.setStyleMask_(window.styleMask() | NSFullSizeContentViewWindowMask) # so the title bar is also vibrant
self.repaint()
All I'm doing to draw a button is: btn = QPushButton("test", self)
self is a class inherited from QMainWindow and everything else should be fairly unimportant.
Behaviour with the window.contentView().addSubview... line commented out (no vibrancy)
Behaviour without it commented out (with vibrancy)
Thanks!
The UI elements are actually drawn, but the NSVisualEffectView is covering them.
I fixed that issue by adding another view QMacCocoaViewContainer to the window.
You'll also need to set the Window transparent, otherwise the widgets will have a slight background border.
If you use the dark vibrant window also make sure to set appearance correctly, so buttons, labels etc. are rendered correctly.
frame = NSMakeRect(0, 0, self.width(), self.height())
view = objc.objc_object(c_void_p=self.winId().__int__())
visualEffectView = NSVisualEffectView.new()
visualEffectView.setAutoresizingMask_(NSViewWidthSizable|NSViewHeightSizable)
visualEffectView.setWantsLayer_(True)
visualEffectView.setFrame_(frame)
visualEffectView.setState_(NSVisualEffectStateActive)
visualEffectView.setMaterial_(NSVisualEffectMaterialUltraDark)
visualEffectView.setBlendingMode_(NSVisualEffectBlendingModeBehindWindow)
visualEffectView.setWantsLayer_(True)
self.setAttribute(Qt.WA_TranslucentBackground, True)
window = view.window()
content = window.contentView()
container = QMacCocoaViewContainer(0, self)
content.addSubview_positioned_relativeTo_(visualEffectView, NSWindowBelow, container)
window.setTitlebarAppearsTransparent_(True)
window.setStyleMask_(window.styleMask() | NSFullSizeContentViewWindowMask)
appearance = NSAppearance.appearanceNamed_('NSAppearanceNameVibrantDark')
window.setAppearance_(appearance)
Result:
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()
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_())