I have this class to create a Statusbar:
class Statusbar(Canvas):
'''Creates a statusbar widget'''
def __init__(self, master = None, **options):
if not master: master = Tk()
self.master, self.options = master, options
self.barFill, self.addText, self.value = self.options.get('barFill', 'red'), self.options.get('addText', True), 0
for option in ('barFill', 'addText'):
if option in self.options: del self.options[option]
Canvas.__init__(self, master, **self.options)
self.offset = self.winfo_reqwidth() / 100
self.height = self.winfo_reqwidth()
if self.addText: self.text = self.create_text(self.winfo_reqwidth()/2, self.winfo_reqheight()/2, text = '0%')
self.bar = self.create_rectangle(0, 0, self.value, self.height, fill = self.barFill)
def setValue(self, value):
'''Sets the value of the status bar as a percent'''
self.value = value * self.offset
self.coords(self.bar, 0, 0, self.value, self.height)
if self.addText: self.itemconfigure(self.text, text = str(self.value/self.offset) + '%')
def change(self, value):
'''Changes the value as a percent'''
self.value += (value * self.offset)
self.coords(self.bar, 0, 0, self.value, self.height)
if self.addText: self.itemconfigure(self.text, text = str(self.value/self.offset) + '%')
My issue is that the text is always drawn under rectangle. So when the rectangle reaches the text, you can't see the text anymore. How can I fix this? Thanks in advance.
The fact that one object sits atop another is called the stacking order. By default, objects created later have a higher stacking order than those that were created earlier. So, one solution is to draw the rectangle and then draw the text.
You can also move things up or down the stacking order using the lift and lower commands of a canvas. You must give it an id or tag of what you want to lift or lower, and optionally an id or tag of the object you want the first object(s) to be above or below.
So, for example, you could raise the text above the rectangle like this:
self.lift(self.text, self.bar)
If you want to get really fancy, you can create the notion of layers. I gave an example in another answer, here: https://stackoverflow.com/a/9576938/7432
In my programming class, we said put whatever text you don't want to be blocked drawn last. So put the text at the bottom of what function you are using to draw with
Related
This question already has an answer here:
How do I move multiple objects at once on a Tkinter canvas?
(1 answer)
Closed last year.
Im trying to move some rectangles with text in them around a canvas with mouse dragNdrop. Im using find_overlapping to select rectangles to be moved. This means the text originally created as part of class object Rect is not moved. Is there a way to modify my code to move all objects in a class object or perhaps find the class object ID using find_overlapping?
Text on rectangles can be identical, as shown in example. Tagging all elements in the class object with a random tag to group them together was my first idea, but retrieving such tag info using find_ovelapping has not been succesful.
import tkinter as tk
root=tk.Tk()
PAB=tk.Canvas(width=400, height=400)
#checks if a certain canvas object has a certain tag
def hastag(tag, id):
if any(tag in i for i in PAB.gettags(id)):return True
else:return False
class Rect:
def __init__(self, x1, y1, name):
rec = PAB.create_rectangle(x1,y1,x1+40,y1+40, fill='#c0c0c0', tag=('movable', name))
text = PAB.create_text(x1+20,y1+20, text=name)
#mouse click find object to move
def get_it(event):
delta=5
global cur_rec
for i in PAB.find_overlapping(event.x-delta, event.y-delta, event.x+delta, event.y-delta):
if hastag('movable', i):
cur_rec = i
PAB.bind('<Button-1>', get_it)
#mouse movement moves object
def move_it(event):
xPos, yPos = event.x, event.y
xObject, yObject = PAB.coords(cur_rec)[0],PAB.coords(cur_rec)[1]
PAB.move(cur_rec, xPos-xObject, yPos-yObject)
PAB.bind('<B1-Motion>', move_it)
#test rects
bob = Rect(20,20,'Bob')
rob = Rect(80,80,'Rob')
different_bob = Rect(160,160,'Bob')
PAB.pack()
root.mainloop()
Thanks. If any clarifications are neccesary Id be happy to help.
A better way would be to use the same tag for all the items that you want to move together so in your case both rectangle and text must have the same tag.
import tkinter as tk
root=tk.Tk()
PAB=tk.Canvas(width=400, height=400, bg="gray")
class Rect:
def __init__(self, x1, y1, name):
tag = f"movable{id(self)}"
rec = PAB.create_rectangle(x1,y1,x1+40,y1+40, fill='#c0c0c0', tag=(tag, ))
text = PAB.create_text(x1+20,y1+20, text=name, tag=(tag,))
def in_bbox(event, item): # checks if the mouse click is inside the item
bbox = PAB.bbox(item)
return bbox[0] < event.x < bbox[2] and bbox[1] < event.y < bbox[3]
#mouse click find object to move
def get_it(event):
delta=5
global cur_rec
cur_rec = PAB.find_closest(event.x, event.y) # returns the closest object
if not in_bbox(event, cur_rec): # if its not in bbox then sets current_rec as None
cur_rec = None
#mouse movement moves object
def move_it(event):
if cur_rec:
xPos, yPos = event.x, event.y
xObject, yObject = PAB.coords(cur_rec)[0],PAB.coords(cur_rec)[1]
PAB.move(PAB.gettags(cur_rec)[0], xPos-xObject, yPos-yObject)
PAB.bind('<Button-1>', get_it)
PAB.bind('<B1-Motion>', move_it)
#test rects
bob = Rect(20,20,'Bob')
rob = Rect(80,80,'Rob')
different_bob = Rect(160,160,'Bob')
PAB.pack()
root.mainloop()
This will work but I'm not sure it's the best way to do it.
Basically it uses the fact that the text is added to the canvas after the rectangle so it can be identified using cur_rec+1.
def move_it(event):
xPos, yPos = event.x, event.y
xObject, yObject = PAB.coords(cur_rec)[0],PAB.coords(cur_rec)[1]
# move rectangle
PAB.move(cur_rec, xPos-xObject, yPos-yObject)
# move text associated with rectangle
PAB.move(cur_rec+1, xPos-xObject, yPos-yObject)
Apply the same tag to both the text and the rectangle. Then, use the tag when calling move. Here's one way to do it:
class Rect:
def __init__(self, x1, y1, name):
identifier = f"id:{id(self)}"
rec = PAB.create_rectangle(x1,y1,x1+40,y1+40, fill='#c0c0c0', tags=('movable', name, identifier))
text = PAB.create_text(x1+20,y1+20, text=name, tags=('movable', identifier))
You can then return the identifier rather than then index of the selected item:
def get_it(event):
delta=5
global cur_rec
for i in PAB.find_overlapping(event.x-delta, event.y-delta, event.x+delta, event.y-delta):
if hastag('movable', i):
identifier = [tag for tag in PAB.gettags(i) if tag.startswith("id:")][0]
cur_rec = identifier
I am trying to create a simple gui that displays the (memory) layout of some components of a device, but I am having a really hard time enforcing the policy I want to the displayed area.
Let me first show what I have so far (my code became quite large, but I changed/narrowed the code down to the minimal required for anyone to be able to run it):
#!/usr/local/bin/python3
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import sys
class Register(QGraphicsRectItem):
RegisterSize = 125
NameColor = QColor(Qt.blue)
ValueColor = QColor(0, 154, 205)
def __init__(self, name, value, pos, parent = None):
super(Register, self).__init__(parent)
self.setPos(pos)
self.width = Register.RegisterSize
self.height = 0
self.set_register_name(name)
self.set_register_value(value)
self.setRect(0, 0, self.width, self.height)
def set_register_name(self, name):
self.text_item = QGraphicsTextItem(name, self)
self.text_item.setDefaultTextColor(Register.NameColor)
self.height += self.text_item.boundingRect().height()
def set_register_value(self, value):
self.value_item = QGraphicsTextItem(str(value), self)
self.value_item.setDefaultTextColor(Register.ValueColor)
self.value_item.setPos(self.text_item.boundingRect().bottomLeft())
self.height += self.value_item.boundingRect().height()
class Title(QGraphicsTextItem):
TitleFont = 'Times New Roman'
TitleFontSize = 18
TitleColor = QColor(Qt.red)
def __init__(self, title, parent = None):
super(Title, self).__init__(title, parent)
self.setFont(QFont(Title.TitleFont, Title.TitleFontSize))
self.setDefaultTextColor(Title.TitleColor)
class Component(QGraphicsItem):
def __init__(self, parent = None):
super(Component, self).__init__(parent)
self.width = Register.RegisterSize * 4
self.height = 0
self.add_title()
self.add_registers()
self.rect = QRectF(0, 0, self.width, self.height)
def add_title(self):
self.title = Title('Component Layout', self)
self.title.setPos((self.width - self.title.boundingRect().width()) / 2, 0)
self.height += self.title.boundingRect().height()
def add_registers(self):
y_coor = self.height
x_coor = 0
for i in range(64):
register = Register('register {0:d}'.format(i), i, QPointF(x_coor, y_coor), self)
x_coor = ((i + 1) % 4) * Register.RegisterSize
if (i + 1) % 4 == 0:
y_coor += register.rect().height()
self.height = y_coor
def boundingRect(self):
return self.rect.adjusted(-1, -1, 1, 1)
def paint(self, painter, option, widget):
pen = QPen(Qt.blue)
painter.setPen(pen)
painter.drawRect(self.rect)
class Device(QGraphicsItem):
LeftMargin = 50
RightMargin = 50
TopMargin = 20
BottomMargin = 20
def __init__(self, parent = None):
super(Device, self).__init__(parent)
self.width = Device.LeftMargin + Device.RightMargin
self.height = Device.TopMargin + Device.BottomMargin
component = Component(self)
component.setPos(QPointF(Device.LeftMargin, Device.TopMargin))
self.width += component.boundingRect().width()
self.height += component.boundingRect().height()
self.rect = QRectF(0, 0, self.width, self.height)
def paint(self, painter, option, widget):
pass
def boundingRect(self):
return self.rect.adjusted(-1, -1, 1, 1)
class MainForm(QDialog):
def __init__(self, parent = None):
super(MainForm, self).__init__(parent)
self.scene = QGraphicsScene(parent)
self.view = QGraphicsView(self)
self.view.setScene(self.scene)
self.scene.addItem(Device())
self.resize(700, 900)
def run_app():
app = QApplication(sys.argv)
form = MainForm()
form.show()
app.exec_()
if __name__ == '__main__':
run_app()
This code, when launched, displays the following:
I don't mind the vertical scrollbar, since I intend to add more Components to the Device, and they won't all fit, what bothers me is the horizontal scrollbar.
Why does it appear without me explicitly asking?
It's not like there's no room in the window for the QGraphicsView to display the content.
Moreover, I noticed that the horizontal (and vertical) scrollbars do not appear, when only the Component is added to the QGraphicsView:
self.scene.addItem(Component()) # << previously was self.scene.addItem(Device())
Now the scrollbars do not appear:
Also; when I instead change the following lines:
LeftMargin = 0 # previously was 50
RightMargin = 0 # previously was 50
TopMargin = 0 # previously was 20
BottomMargin = 0 # previously was 20
Scrollbars do not appear. (I probably crossed some boundary with these margins added?)
I know I can control the scrollbars policy with the QGraphicsView.setHorizontalScrollBarPolicy() to make the horizontal scrollbar always off, but that raises another problem: When there's no way to scroll right, the vertical scrollbar "steals" some of the pixels from the display, making Device.RightMargin != Device.LeftMargin. Also, I am curious about what's the size boundary above which the horizontal/vertical scrollbars appear.
So, this is the policy I want to enforce:
I want the displayed area to always have a minimum height of X pixels (regardless of Device()'s height), and for vertical scrollbar to appear only if the Device() height passes these X pixels boundary (I'll determine Device()'s height by summing all Component()s heights)
I want QGraphicsView to never show horizontal scrollbar (the width Device()'s width is fixed and independent of the number of Component()s).
Whenever vertical scrollbar is needed, I don't want it to take up pixels from my display area.
I want to know what is the boundary (in pixels) above which scrollbars will appear (when I don't specify scrollbar policy).
EDIT:
After playing with it a bit, I figured something:
The unwanted horizontal scroll bar appears only because the vertical one appears and steals some of the display space.
According to the doc, the default policy for the horizontal scroll bar is Qt::ScrollBarAsNeeded, which means: "shows a scroll bar when the content is too large to fit and not otherwise.", but it doesn't state what is considered "too large".
When I played around with the margins (Device.TopMargin/Device.BottomMargin), I discovered that the vertical scroll bar appears (and consequently the horizontal one) when Device.boundingRect().height() crosses the 786 pixels boundary.
I couldn't figure out where did this number came from or how to control it.
I believe you are looking for setFixedWidth() and setFixedHeight()
class MainForm(QDialog):
def __init__(self, parent = None):
super(MainForm, self).__init__(parent)
self.scene = QGraphicsScene(parent)
self.view = QGraphicsView(self)
self.view.setScene(self.scene)
self.scene.addItem(Device())
self.resize(700, 900)
self.view.setFixedWidth(650) # <-
self.view.setFixedHeight(500) # <- these two lines will set Device dimensions
self.setFixedWidth(700) # <- this will fix window width
When you set fixed width to view it must be greater than its content (left margin + Device + right margin), otherwise horizontal scroll bar will be displayed. This is why you did not get the horizontal scroll bar when margins were zero.
Generally, the scrollbar will appear when your current view can't display the content.
The vertical scroll bar will take some space from inside the window, and I believe that you do not have control over that, so you should reserve some place for that, too. The behavior of vertical scroll bar depends on your windows system, e.g. on Mac it hovers over and disappear when unneeded, so it does not takes space at all.
I recommend to do the layout in QT Designer. I find it much easier to do it visually, testing it immediately and only introduce small changes in the generated code.
I want to get the value of a scale and create rectangles as many as the value is. For example, if I adjust the scale to number 7, 7 rectangles would be created next to each other, and after that if I adjust the scale value to 3, the rectangles shown in the canvas decreases to three at that moment. I had used the code below:
from tkinter import *
from tkinter import ttk
class rect:
def __init__(self, root):
self.root = root
self.size = IntVar()
self.canvas = Canvas(self.root, width=800, height=300)
self.scale = Scale(self.root, orient=HORIZONTAL, from_=3, to=20, tickinterval=1, variable=self.size)
self.show()
def show(self):
x = 50
y = 50
for i in range(self.scale.get()):
self.canvas.create_rectangle(x, y, x + 50, y + 50, fill='red')
x += 50
self.canvas.pack()
self.scale.pack()
root = Tk()
a = rect(root)
root.mainloop()
I guess I have to use trace method, But I don't know how to.
Can anyone fix the code I used in the way which I explained.
Thank you.
One solution is to bind to <ButtonRelease>, and call your show method there. Since event bindings pass an object representing the event, you'll need to make that an optional argument if you also want to call show without any arguments.
For example:
class rect:
def __init__(self, root):
...
self.scale = Scale(...)
self.scale.bind("<ButtonRelease>", self.show)
I'm guessing you'll want to remove any previously drawn rectangles, so you'll need to call delete before creating the rectangles:
def show(...):
self.canvas.delete("all")
...
I'm trying to make a custom text widget that is double buffered (In order to avoid flicker).
However, I'd like to be able to do a few things. Yet, I'm unsure of the exact methods I should use.
The first two are easy I simply want to change the background and foreground color.
So more or less I want to be able to change the text color for self.Text in self.Draw().
Snippet:
self.Text = mdc.DrawText(self.TextString, 10, 0)
As sell as the Background (fill) color for self.MemoryDC.
Next, does anyone know how I could center self.Text? Finally, how do I configure self.Text after it has been created?
The widget thus far:
class DynamicText (wx.Panel):
def __init__(self, par):
self.Par = par
wx.Panel.__init__(self, self.Par)
self.Time = Time(self, func=self.SetTime)
self.Dim = self.Par.GetClientSize()
self.SetSize(self.Dim)
self.Bind(wx.EVT_SIZE, self.Resize)
self.Bind(wx.EVT_ERASE_BACKGROUND, self.Erase)
self.Bind(wx.EVT_PAINT, self.Paint)
def Set (self, text) :
self.TextString = text
def SetTime (self, time) :
self.Set(str(time))
self.Resize(None)
def Resize(self, event):
self.Width, self.Height = self.GetSize()
bitmap = wx.EmptyBitmap(self.Width, self.Height)
self.MemoryDC = wx.MemoryDC(bitmap)
''' Redraws **self.MemoryDC** '''
mdc = self.MemoryDC
''' Deletes everything from widget. '''
mdc.Clear()
fs = 11
font = wx.Font( fs, wx.DEFAULT, wx.NORMAL, wx.NORMAL)
mdc.SetFont(font)
self.Draw()
self.Refresh()
def Draw (self) :
mdc = self.MemoryDC
self.Text = mdc.DrawText(self.TextString, 10, 0)
def Erase(self, event):
''' Does nothing, as to avoid flicker. '''
pass
def Paint(self, event):
pdc = wx.PaintDC(self)
w, h = self.MemoryDC.GetSize()
pdc.Blit(0, 0, w, h, self.MemoryDC, 0, 0)
I don't understand what you mean by configuring self.Text after it was created. If you want to change the text after you've drawn it - you can't. Once you've drawn it to the DC it's there, and the only way to change it would be to clear the DC and repaint it. In your case, it seems all you need to do when the text is updated is to call Resize() again, forcing a redraw. Note that DrawText() retruns nothing, so the value of your self.Text would be None. You definitely can't use that to refer to the drawn text. :D
As for the rest, here's an example of a Draw() method that centers the text and paints it blue:
def Draw(self) :
mdc = self.MemoryDC
dc_width, dc_height = mdc.GetSizeTuple()
text_width, text_height, descent, externalLeading = mdc.GetFullTextExtent(self.TextString)
x = (dc_width - text_width) / 2
y = (dc_height - text_height) / 2
mdc.SetTextForeground('Blue')
mdc.DrawText(self.TextString, x, y)
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_())