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.
Related
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)
i have a comboBox inside of a tableWidget and the verticalHeader DefaultSectionSize is 60.
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
QtWidgets.QMainWindow.__init__(self,parent)
self.table = QTableWidget()
self.setCentralWidget(self.table)
self.table.verticalHeader().setDefaultSectionSize(60)
self.table.setColumnCount(2)
self.table.setRowCount(2)
data = ["1","2"]
for i in range(2):
item = QTableWidgetItem(data[i])
self.table.setItem(i,0,item)
self.combo_sell = QComboBox()
self.combo_sell.setMaximumHeight(30)
self.table.setCellWidget(i,1,self.combo_sell)
But since i set the maximum size of the comboBox to 30, it stays in the top of the item.
I want to know if there's a way to align it to the center.
When setting an index widget, the view tries to set the widget geometry based on the visualRect() of the index. Setting a fixed dimension forces the widget to align itself to the default origin, which is usually the top left corner.
The only way to vertically center a widget with a fixed height is to use a container with a vertical box layout and add the combo to it:
for i in range(2):
item = QTableWidgetItem(data[i])
item.setTextAlignment(Qt.AlignCenter)
self.table.setItem(i,0,item)
container = QWidget()
layout = QVBoxLayout(container)
combo_sell = QComboBox()
layout.addWidget(combo_sell)
combo_sell.setMaximumHeight(30)
self.table.setCellWidget(i, 1, container)
Note: setting instance attributes in a for loop is pointless, as the reference is lost every time the cycle loops.
If you need a simple reference to the combo, you can set it as an attribute of the widget:
container.combo_sell = QComboBox()
In this way you can easily access it when required:
widget = self.table.cellWidget(row, column)
if widget and hasattr(widget, 'combo'):
combo = widget.combo
print(combo.currentIndex())
Note that the reference is created for the python wrapper of the widget, and that behavior might change in future versions of Qt. A better and safer way to achieve this would be to use a subclass, which would also allow easier access to the combo:
class TableCombo(QWidget):
def __init__(self):
super().__init__()
layout = QVBoxLayout(self)
self.combo = QComboBox()
layout.addWidget(self.combo)
self.combo.setMaximumHeight(30)
self.currentIndex = self.combo.currentIndex
self.setCurrentIndex = self.combo.setCurrentIndex
self.addItems = self.combo.addItems
# ...
combo_sell = TableCombo()
self.table.setCellWidget(i, 1, combo_sell)
# ...
combo = self.table.cellWidget(row, column)
print(combo.currentIndex())
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 have a CheckLabelTool in a wx.ToolBar and I want a menu to popup directly beneath it on mouse click. I'm trying to get the location of the tool so I can set the position of the menu, but everything I've tried (GetEventObject, GetPosition, etc) gives me the position of the toolbar, so consequently the menu pops under the toolbar, but very far from the associated tool. Any suggestions? I need the tool to have toggle and bitmap capability, but I'm not fixed on CheckLabelTool if there's something else that would work better.
Thanks!
Read the section on the PopupMenu method on wxpython.org:
"Pops up the given menu at the
specified coordinates, relative to
this window, and returns control when
the user has dismissed the menu. If a
menu item is selected, the
corresponding menu event is generated
and will be processed as usual. If the
default position is given then the
current position of the mouse cursor
will be used."
You need to bind to the EVT_MENU event of your check tool. Once the tool button is checked, you can pop the menu up. If you don't specify the location of the popup, it will use the current position of the mouse, which is what you want.
If you want the menu to pop up at a pre-determined location that is independent of the mouse, you can get the screen location of the toolbar and add an offset
Let's look at code:
[Edit: To show how to compute the position of any point on a tool, I have modified the code to compute and display various points on the tool bar once you click a tool. The menu appears on the lower right corner of the clicked button. It works for me on Windows. I'm curious to know if it doesn't behave on other platforms.]
import wx
class ViewApp(wx.App):
def OnInit(self):
self.frame = ToolFrame(None, -1, "Test App")
self.frame.Show(True)
return True
class MyPopupMenu(wx.Menu):
def __init__(self, parent):
wx.Menu.__init__(self)
self.parent = parent
minimize = wx.MenuItem(self, wx.NewId(), 'Minimize')
self.AppendItem(minimize)
self.Bind(wx.EVT_MENU, self.OnMinimize, id=minimize.GetId())
def OnMinimize(self, event):
self.parent.Iconize()
class ToolFrame(wx.Frame):
def __init__(self, parent, id, title):
wx.Frame.__init__(self, parent, id, title, size=(350, 250))
self.toolbar = self.CreateToolBar()
self.tool_id = wx.NewId()
for i in range(3):
tool_id = wx.NewId()
self.toolbar.AddCheckLabelTool(tool_id, 'Tool', wx.EmptyBitmap(10,10))
self.toolbar.Bind(wx.EVT_MENU, self.OnTool, id=tool_id)
self.toolbar.Realize()
self.Centre()
self.Show()
def OnTool(self, event):
if event.IsChecked():
# Get the position of the toolbar relative to
# the frame. This will be the upper left corner of the first tool
bar_pos = self.toolbar.GetScreenPosition()-self.GetScreenPosition()
# This is the position of the tool along the tool bar (1st, 2nd, 3rd, etc...)
tool_index = self.toolbar.GetToolPos(event.GetId())
# Get the size of the tool
tool_size = self.toolbar.GetToolSize()
# This is the upper left corner of the clicked tool
upper_left_pos = (bar_pos[0]+tool_size[0]*tool_index, bar_pos[1])
# Menu position will be in the lower right corner
lower_right_pos = (bar_pos[0]+tool_size[0]*(tool_index+1), bar_pos[1]+tool_size[1])
# Show upper left corner of first tool in black
dc = wx.WindowDC(self)
dc.SetPen(wx.Pen("BLACK", 4))
dc.DrawCircle(bar_pos[0], bar_pos[1], 4)
# Show upper left corner of this tool in blue
dc.SetPen(wx.Pen("BLUE", 4))
dc.DrawCircle(upper_left_pos[0], upper_left_pos[1], 4)
# Show lower right corner of this tool in green
dc.SetPen(wx.Pen("GREEN", 4))
dc.DrawCircle(lower_right_pos[0], lower_right_pos[1], 4)
# Correct for the position of the tool bar
menu_pos = (lower_right_pos[0]-bar_pos[0],lower_right_pos[1]-bar_pos[1])
# Pop up the menu
self.PopupMenu(MyPopupMenu(self), menu_pos)
if __name__ == "__main__":
app = ViewApp(0)
app.MainLoop()
Parts of this code come from here.
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_())