I am trying to get font size of QLabel that the text fill entire QLabel's rectangle.I try to use QFontMetrics to get, but QFontMetrics can't get font size by gived rectangle?
The example:
The GUI may stuck while resize the window.
class Label(QLabel):
def __init__(self):
super().__init__()
self.resize(400, 300)
font = self.calculate_font()
self.setFont(font)
self.setText('PASS')
def calculate_font(self):
for i in range(400):
fm = QFontMetrics( QFont('Helvetica', i) )
fmSize = fm.boundingRect(self.geometry(), Qt.AlignCenter, 'PASS').size()
print(fm.boundingRect(self.geometry(), Qt.AlignCenter, 'PASS'), self.size())
#need font height equal label height
if fmSize.height() > self.size().height():
return QFont('Helvetica', i)
def resizeEvent(self, event):
font = self.calculate_font()
self.setFont(font)
app = QApplication([])
demo = Label()
demo.show()
app.exec()
Don't do it
There are dozens of reasons for which what you want to achieve is simply wrong.
The most important one is that dealing with text drawing and its size is not an easy task; also, Qt uses the label contents to tell the window layout about the size it could have, the size it wants to have and, most importantly, the minimum size it should have; all this is very important to the GUI, as it will be taken into account in order to correctly resize all other elements of your interface.
Finally, all those factors are based on the text of the label and its formatting, which might depend on the text contents and the fact that the text might be "rich text" (including multiple font size and weight).
If you really are conscious about the concepts explained above, here are 4 possible implementations. All of them partially support rich text (font weight, text color, etc, but not real text alignment).
Resizing font label
While subclassing a QLabel might seem the simplest approach, it actually isn't, as it is a widget much more complex than it seems (as I wrote before, dealing with text widgets is not an easy task)
The most important downside of this method is that it is very slow, since it has to carefully compute the font size at each resize. While some improvement might be achieved with a better implementation, I wouldn't suggest this method anyway.
class ResizeFontLabel(QLabel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.paintFont = self.font()
def updateFont(self):
doc = QTextDocument()
if self.textFormat() == Qt.RichText or self.textFormat() == Qt.AutoText and Qt.mightBeRichText(self.text()):
doc.setHtml(self.text())
else:
doc.setPlainText(self.text())
frame = doc.rootFrame().frameFormat()
frame.setMargin(0)
doc.rootFrame().setFrameFormat(frame)
doc.setDefaultFont(self.paintFont)
width = self.width()
height = self.height()
if doc.size().width() > width or doc.size().height() > height:
while doc.size().width() > width or doc.size().height() > height:
self.paintFont.setPointSizeF(self.paintFont.pointSizeF() - .1)
doc.setDefaultFont(self.paintFont)
elif doc.size().width() < width and doc.size().height() < height:
while doc.size().width() < width and doc.size().height() < height:
self.paintFont.setPointSizeF(self.paintFont.pointSizeF() + .1)
doc.setDefaultFont(self.paintFont)
def resizeEvent(self, event):
self.updateFont()
def paintEvent(self, event):
doc = QTextDocument()
if self.textFormat() == Qt.RichText or self.textFormat() == Qt.AutoText and Qt.mightBeRichText(self.text()):
doc.setHtml(self.text())
else:
doc.setPlainText(self.text())
frame = doc.rootFrame().frameFormat()
frame.setMargin(0)
doc.rootFrame().setFrameFormat(frame)
doc.setDefaultFont(self.paintFont)
qp = QPainter(self)
doc.drawContents(qp, QRectF(self.rect()))
QLabels internally use a QTextDocument for both painting and sizing. The reason of the setMargin is due to the fact that a QTextDocument has some margin by default, and that's not used in labels.
Notes:
be careful about the different or/and in the updateFont() method. Their logic is very important
this method is slow
it doesn't support text alignment (at least with this basic implementation)
Painting based font label
This method is a simplification of the one above. It doesn't compute the font size, but just paints the contents scaled to the size.
class PaintLabel(QLabel):
def paintEvent(self, event):
doc = QTextDocument()
if self.textFormat() == Qt.RichText or self.textFormat() == Qt.AutoText and Qt.mightBeRichText(self.text()):
doc.setHtml(self.text())
else:
doc.setPlainText(self.text())
frame = doc.rootFrame().frameFormat()
frame.setMargin(0)
doc.rootFrame().setFrameFormat(frame)
scale = min(self.width() / doc.size().width(), self.height() / doc.size().height())
qp = QPainter(self)
qp.scale(scale, scale)
doc.drawContents(qp, QRectF(self.rect()))
Notes:
it's faster than the first method
it doesn't support alignment
since the scaling doesn't take into account the font size, the text will not be as big as it could be (that's due to the fact that a QTextDocument can have multiple "text blocks", and computing them at each paintEvent would make this really complex and slow
Graphics view label
This is a completely different approach, as it uses the Graphics View Framework.
The trick is to use a single QGraphicsTextItem in the scene, and let the view take care about the resizing/alignment.
class GraphicsLabel(QGraphicsView):
def __init__(self, text=''):
super().__init__()
# graphics view usually have a background and a frame around them,
# let's remove both of them
self.setFrameShape(0)
self.setStyleSheet('background: transparent;')
# as most QAbstractScrollAreas, views have a default minimum size hint
# that makes them "bigger" whenever they're shown; let's ignore that
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
scene = QGraphicsScene(self)
self.setScene(scene)
self.textItem = scene.addText('')
self.setText(text)
def minimumSizeHint(self):
# this is related to the minimum size hint specified above
font = self.font()
font.setPointSize(1)
return QFontMetrics(font).boundingRect(self.textItem.toPlainText()).size()
def setText(self, text):
font = self.font()
font.setPointSize(1)
self.setMinimumSize(QFontMetrics(font).boundingRect(text).size())
if Qt.mightBeRichText(text):
self.textItem.setHtml(text)
else:
self.textItem.setPlainText(text)
doc = self.textItem.document()
frame = self.textItem.document().rootFrame().frameFormat()
if frame.margin():
frame.setMargin(0)
doc.rootFrame().setFrameFormat(frame)
self.textItem.setDocument(doc)
def text(self):
# provide a basic interface similar to a QLabel
return self.textItem.toPlainText()
def resizeEvent(self, event):
# the base class implementation is always required for QAbstractScrollArea
# descendants; then we resize its contents to fit its size.
super().resizeEvent(event)
self.fitInView(self.textItem, Qt.KeepAspectRatio)
Notes:
it does support alignment
there's no "optimal" minimum size (the readability of the text is based on the font, and we can't do anything about that)
there's no "optimal" size hint, so the widget will be resized without any "complaints" about its text contents: if the label has very long text, that text will just be very, very small.
basic QWidget styling (through QWidget.setStyleSheet) is not supported
Graphics view QLabel widget
This method is very similar to the one above, but instead of creating a simple "text item", it adds an actual QLabel to the graphics scene.
class GraphicsLabelWidget(QGraphicsView):
def __init__(self, text=''):
super().__init__()
self.setFrameShape(0)
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
self.setStyleSheet('background: transparent;')
scene = QGraphicsScene(self)
self.setScene(scene)
self.label = QLabel(text)
self.labelItem = scene.addWidget(self.label)
self.label.setStyleSheet(self.styleSheet())
self.setText(text)
def minimumSizeHint(self):
font = self.font()
font.setPointSize(1)
doc = QTextDocument()
if Qt.mightBeRichText(self.label.text()):
doc.setHtml(self.label.text())
else:
doc.setPlainText(self.label.text())
return QFontMetrics(font).boundingRect(self.label.text()).size()
def setText(self, text):
font = self.font()
font.setPointSize(1)
self.setMinimumSize(QFontMetrics(font).boundingRect(text).size())
self.label.setText(text)
def text(self):
return self.label.toPlainText()
def resizeEvent(self, event):
super().resizeEvent(event)
self.fitInView(self.labelItem, Qt.KeepAspectRatio)
Notes:
slightly better than the basic addText graphics implementation; at the same time, it's also slightly slower
better support for stylesheets (but they should be applied to the actual child label)
Test example:
Provided just for commodity (add the classes above to see it in action).
class Demo(QWidget):
def __init__(self):
super().__init__()
layout = QVBoxLayout(self)
testText = 'PASS <b>bold</b><br/><i>new italic line</i><br/>{}'
resizeLabel = ResizeFontLabel(testText.format('resize mode'))
layout.addWidget(resizeLabel)
resizeLabel.setAlignment(Qt.AlignRight|Qt.AlignBottom)
paintLabel = PaintLabel(testText.format('paint mode'))
layout.addWidget(paintLabel)
paintLabel.setAlignment(Qt.AlignRight|Qt.AlignBottom)
graphicsLabel = GraphicsLabel(testText.format('graphicsview mode'))
layout.addWidget(graphicsLabel)
graphicsLabel.setAlignment(Qt.AlignRight|Qt.AlignBottom)
graphicsLabelWidget = GraphicsLabelWidget(testText.format('graphicsview mode'))
layout.addWidget(graphicsLabelWidget)
graphicsLabelWidget.setAlignment(Qt.AlignRight|Qt.AlignBottom)
graphicsLabelWidget.label.setStyleSheet('QLabel {background: green;}')
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
demo = Demo()
demo.show()
app.exec()
Related
I'm trying to learn it by re-making an old command line C program I've got for working with pixel art.
At the moment, the main window starts as a single QLabel set to show a 300 x 300 scaled up version of a 10 x 10 white image.
I'm using the resizeEvent (I've also tried using paintEvent with the same problem) to rescale the image to fill the window as the window size is increased.
My question is, how do I rescale the image to fit in the window as the window size is decreased? As it stands, the window can't be resized smaller than the widget displaying the image. Essentially, I can make the window (and image) bigger, but never smaller.
My code for this so far is below. As it stands it's only working based on changes to window width, just to keep it simple while I'm working this out. Is there a way to allow the window to be resized to be smaller than the largest widget? Or is there a better way to approach this problem?
#Create white 10*10 image
image = QImage(10,10,QImage.Format.Format_ARGB32)
image_scaled = QImage()
image.fill(QColor(255,255,255))
class Window(QMainWindow):
#scale image to change in window width (image is window width * window width square)
def resizeEvent(self,event):
if self.imageLabel.width()>self.imageLabel.height():
self.image_scaled = image.scaled(self.imageLabel.width(),self.imageLabel.width())
self.pixmap = QPixmap.fromImage(self.image_scaled)
self.imageLabel.setPixmap(self.pixmap)
QWidget.resizeEvent(self, event)
def __init__(self, parent=None):
super().__init__(parent)
self.setGeometry(100,100,300,300)
self.imageLabel = QLabel()
self.setCentralWidget(self.imageLabel)
self.image_scaled = image.scaled(self.imageLabel.width(),self.imageLabel.width())
self.pixmap = QPixmap.fromImage(self.image_scaled)
self.imageLabel.setPixmap(self.pixmap)
app = QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec_())
While the OP proposed solution might work, it has an important drawback: it uses a QScrollArea for the wrong purpose (since it's never used for scrolling). That approach creates unnecessary overhead while resizing, as the view will need to compute lots of things about its contents before "finishing" the resize event (including scroll bar ranges and geometries) that, in the end, will never be actually used.
The main problem comes from the fact that QLabel doesn't allow resizing to a size smaller than the original pixmap set. To work around this issue, the simplest solution is to create a custom QWidget subclass that draws the pixmap on its own.
class ImageViewer(QWidget):
pixmap = None
_sizeHint = QSize()
ratio = Qt.KeepAspectRatio
transformation = Qt.SmoothTransformation
def __init__(self, pixmap=None):
super().__init__()
self.setPixmap(pixmap)
def setPixmap(self, pixmap):
if self.pixmap != pixmap:
self.pixmap = pixmap
if isinstance(pixmap, QPixmap):
self._sizeHint = pixmap.size()
else:
self._sizeHint = QSize()
self.updateGeometry()
self.updateScaled()
def setAspectRatio(self, ratio):
if self.ratio != ratio:
self.ratio = ratio
self.updateScaled()
def setTransformation(self, transformation):
if self.transformation != transformation:
self.transformation = transformation
self.updateScaled()
def updateScaled(self):
if self.pixmap:
self.scaled = self.pixmap.scaled(self.size(), self.ratio, self.transformation)
self.update()
def sizeHint(self):
return self._sizeHint
def resizeEvent(self, event):
self.updateScaled()
def paintEvent(self, event):
if not self.pixmap:
return
qp = QPainter(self)
r = self.scaled.rect()
r.moveCenter(self.rect().center())
qp.drawPixmap(r, self.scaled)
class Window(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.imageLabel = ImageViewer(QPixmap.fromImage(image))
self.setCentralWidget(self.imageLabel)
Found a solution. Turns out putting the image inside a QScrollArea widget allows the window to be made smaller than the image it contains even if the scroll bars are disabled. This then allows the image to be rescaled to fit the window as the window size is reduced.
class Window(QMainWindow):
#scale image to change in window width (image is window width * window width square)
def resizeEvent(self,event):
self.image_scaled = image.scaled(self.scroll.width(),self.scroll.height())
self.pixmap = QPixmap.fromImage(self.image_scaled)
self.imageLabel.setPixmap(self.pixmap)
QMainWindow.resizeEvent(self, event)
def __init__(self, parent=None):
super().__init__(parent)
self.setGeometry(100,100,200,200)
self.imageLabel = QLabel()
self.scroll = QScrollArea()
self.scroll.setWidget(self.imageLabel)
self.setCentralWidget(self.scroll)
self.scroll.setWidgetResizable(True)
self.scroll.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.scroll.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.image_scaled = image.scaled(self.scroll.width(),self.scroll.width())
self.pixmap = QPixmap.fromImage(self.image_scaled)
self.imageLabel.setPixmap(self.pixmap)
app = QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec_())
A custom widget (class name MyLabel, inherits QLabel) has a fixed aspect ratio 16:9.
When I resize my window, the label is top-left aligned unless the window happens to be 16:9, in which case it fills the window perfectly.
How do I get the label to be centered? I have looked at size policies, alignments, using spaceitems and stretch, but I cannot seem to get it working as desired.
Here is a minimal reproducible example:
import sys
from PyQt5.QtWidgets import QApplication, QLabel, QMainWindow
from PyQt5.QtCore import QSize, Qt
from PyQt5.Qt import QVBoxLayout, QWidget
class MyLabel(QLabel):
def __init__(self, text, parent=None):
super().__init__(text, parent)
self.setStyleSheet("background-color: lightgreen") # Just for visibility
def resizeEvent(self, event):
# Size of 16:9 and scale it to the new size maintaining aspect ratio.
new_size = QSize(16, 9)
new_size.scale(event.size(), Qt.KeepAspectRatio)
self.resize(new_size)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__(None)
# Main widget and layout, and set as centralWidget
self.main_layout = QVBoxLayout()
self.main_widget = QWidget()
self.main_widget.setLayout(self.main_layout)
self.setCentralWidget(self.main_widget)
# Add button to main_layout
label = MyLabel("Hello World")
self.main_layout.addWidget(label)
self.show()
app = QApplication(sys.argv)
ex = MainWindow()
sys.exit(app.exec_())
Examples of desired outcome:
Examples of actual outcome:
Qt unfortunately doesn't provide a straight forward solution for widgets that require a fixed aspect ratio.
There are some traces in old documentation, but the main problem is that:
all functions related to aspect ratio (hasHeightForWidth() etc) for widgets, layouts and size policies are only considered for the size hint, so no constraint is available if the widget is manually resized by the layout;
as the documentation reports changing the geometry of a widget within the moveEvent() or resizeEvent() might lead to infinite recursion;
it's not possible to (correctly) control the size growth or shrinking while keeping aspect ratio;
For the sake of completeness, here's a partial solution to this issue, but be aware that QLabel is a very peculiar widget that has some constraints related to its text representation (most importantly, with rich text and/or word wrap).
class MyLabel(QLabel):
lastRect = None
isResizing = False
def __init__(self, text, parent=None):
super().__init__(text, parent)
self.setStyleSheet("background-color: lightgreen")
self.setScaledContents(True)
def restoreRatio(self, lastRect=None):
if self.isResizing:
return
rect = QRect(QPoint(),
QSize(16, 9).scaled(self.size(), Qt.KeepAspectRatio))
if not lastRect:
lastRect = self.geometry()
rect.moveCenter(lastRect.center())
if rect != lastRect:
self.isResizing = True
self.setGeometry(rect)
self.isResizing = False
self.lastRect = None
def hasHeightForWidth(self):
return True
def heightForWidth(self, width):
if self.pixmap():
return width * self.pixmap().height() / self.pixmap().width()
return width * 9 / 16
def sizeHint(self):
if self.pixmap():
return self.pixmap().size()
return QSize(160, 90)
def moveEvent(self, event):
self.lastRect = self.geometry()
def resizeEvent(self, event):
self.restoreRatio(self.lastRect)
Since the purpose is to display an image, another possibility is to manually paint everything on your own, for which you don't need a QLabel at all, and you can just override the paintEvent of a QWidget, but for performance purposes it could be slightly better to use a container widget with a child QLabel: this would theoretically make things a bit faster, as all the computation is completely done in Qt:
class ParentedLabel(QWidget):
def __init__(self, pixmap=None):
super().__init__()
self.child = QLabel(self, scaledContents=True)
if pixmap:
self.child.setPixmap(pixmap)
def setPixmap(self, pixmap):
self.child.setPixmap(pixmap)
self.updateGeometry()
def updateChild(self):
if self.child.pixmap():
r = self.child.pixmap().rect()
size = self.child.pixmap().size().scaled(
self.size(), Qt.KeepAspectRatio)
r = QRect(QPoint(), size)
r.moveCenter(self.rect().center())
self.child.setGeometry(r)
def hasHeightForWidth(self):
return bool(self.child.pixmap())
def heightForWidth(self, width):
return width * self.child.pixmap().height() / self.child.pixmap().width()
def sizeHint(self):
if self.child.pixmap():
return self.child.pixmap().size()
return QSize(160, 90)
def moveEvent(self, event):
self.updateChild()
def resizeEvent(self, event):
self.updateChild()
Finally, another possibility is to use a QGraphicsView, which is probably the faster approach of all, with a small drawback: the image shown based on the given size hint will probably be slightly smaller (a couple of pixels) than the original, with the result that it will seem a bit "out of focus" due to the resizing.
class ViewLabel(QGraphicsView):
def __init__(self, pixmap=None):
super().__init__()
self.setStyleSheet('ViewLabel { border: 0px solid none; }')
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
scene = QGraphicsScene()
self.setScene(scene)
self.pixmapItem = QGraphicsPixmapItem(pixmap)
self.pixmapItem.setTransformationMode(Qt.SmoothTransformation)
scene.addItem(self.pixmapItem)
def setPixmap(self, pixmap):
self.pixmapItem.setPixmap(pixmap)
self.updateGeometry()
self.updateScene()
def updateScene(self):
self.fitInView(self.pixmapItem, Qt.KeepAspectRatio)
def hasHeightForWidth(self):
return not bool(self.pixmapItem.pixmap().isNull())
def heightForWidth(self, width):
return width * self.pixmapItem.pixmap().height() / self.pixmapItem.pixmap().width()
def sizeHint(self):
if not self.pixmapItem.pixmap().isNull():
return self.pixmapItem.pixmap().size()
return QSize(160, 90)
def resizeEvent(self, event):
self.updateScene()
I try to toggle a splitter container between two widgets keeping the actual size of the splitter.
For this I use QSplitter.sizes() to read the actual size and QSplitter.setSizes() after I toggle my widgets.
The problem is that I have a QToolButton which I resize with setFixedSize() in a resizeEvent(), and because of this when I set the new size, it often doesn't work.
I write a little script to reproduce this :
In the left part of the splitter, I have a button to toggle the right part of the splitter between two classes (which are QWidgets).
A little precision : I want to keep my QToolbutton in a 1:1 aspect ratio.
Here a demo :
https://webmshare.com/play/5Bmvn
So here the script :
from PyQt4 import QtGui, QtCore
minSize = 50
maxSize = 350
class mainWindow(QtGui.QWidget):
def __init__(self):
super(mainWindow, self).__init__()
self.layout = QtGui.QVBoxLayout(self)
self.splitter = QtGui.QSplitter(QtCore.Qt.Horizontal, self)
self.splitter.setHandleWidth(20)
self.layout.addWidget(self.splitter)
wgt_left = QtGui.QWidget()
lyt_left = QtGui.QVBoxLayout(wgt_left)
self.btn_toggleSplitter = QtGui.QPushButton('Toggle Button')
self.btn_toggleSplitter.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding)
self.btn_toggleSplitter.setCheckable(True)
lyt_left.addWidget(self.btn_toggleSplitter)
self.splitter.addWidget(wgt_left)
self.first = panel('1')
self.second = panel('2')
self.splitter.addWidget(self.first)
self.width = self.first.size()
self.btn_toggleSplitter.clicked.connect(self.ToggleParent)
def ToggleParent(self):
self.sizes = self.splitter.sizes()
if self.btn_toggleSplitter.isChecked() == True:
self.first.setParent(None)
self.splitter.addWidget(self.second)
else :
self.second.setParent(None)
self.splitter.addWidget(self.first)
self.splitter.setSizes(self.sizes)
class panel(QtGui.QWidget):
def __init__(self, text):
super(panel, self).__init__()
lyt_main = QtGui.QVBoxLayout(self)
lyt_icon = QtGui.QHBoxLayout()
self.tbtn_icon = QtGui.QToolButton()
self.tbtn_icon.setText(text)
self.tbtn_icon.setMinimumSize(QtCore.QSize(minSize,minSize))
self.tbtn_icon.setMaximumSize(QtCore.QSize(maxSize,maxSize))
lyt_icon.addWidget(self.tbtn_icon)
lyt_horizontal = QtGui.QHBoxLayout()
lyt_horizontal.addWidget(QtGui.QPushButton('3'))
lyt_horizontal.addWidget(QtGui.QPushButton('4'))
lyt_main.addWidget(QtGui.QLabel('Below me is the QToolButton'))
lyt_main.addLayout(lyt_icon)
lyt_main.addLayout(lyt_horizontal)
lyt_main.addWidget(QtGui.QPlainTextEdit())
def resizeEvent(self, event):
w = panel.size(self).width()
h = panel.size(self).height()
size = min(h, w)-22
if size >= maxSize:
size = maxSize
elif size <= minSize:
size = minSize
self.tbtn_icon.setFixedSize(size, size)
app = QtGui.QApplication([])
window = mainWindow()
window.resize(600,300)
window.show()
app.exec_()
Thanks
You are looking for QtGui.QStackedWidget. Adding the widgets to this on the right side of your splitter will change the code around self.first and self.second's construction to this:
self.stack_right = QtGui.QStackedWidget()
self.splitter.addWidget(self.stack_right)
self.first = panel('1')
self.second = panel('2')
self.stack_right.addWidet(self.first)
self.stack_right.addWidget(self.second)
Then your ToggleParent method:
def ToggleParent(self):
if self.btn_toggleSplitter.isChecked() == True:
self.stack_right.setCurrentWidget(self.second)
else:
self.stack_right.setCurrentWidget(self.first)
This will avoid the awkwardness of caching and manually resizing your widgets.
Addendum:
The tool button scaling is really a separate question, but here's a tip:
Have a look at the heightForWidth layout setting for lyt_left. This will help you keep a 1:1 ratio for the QToolButton. You currently have a size policy of Preferred/Expanding, which doesn't make sense if you need a 1:1 aspect ratio. I highly recommend this over manually resizing the tool button while handling an event. Generally, calling setFixedSize more than once on a widget should be considered a last resort. Let the layouts do the work.
Addendum to addendum: doing a little poking (it's been awhile), you may need to inherit from QToolButton and reimplement the hasHeightForWidth() and heightForWidth() methods. There are a plethora of questions addressing the subject here. Just search for heightForWidth.
I am trying to find a way to have one of my widgets maintain a width based on its height. I have other widgets working fine reimplementing the heightForWidth method. That was easy because that method is standard. I know there is no included widthForHeight method so I have tried many options the internet has suggested but have not gotten anything to work all the way. What I currently have almost gets me there.
I first reimplement my widget's sizeHint to get its width to be a ratio of its parent's (QHBoxLayout) height.
The sizeHint makes MyCustomLabel show with the right size at first show but did not update during times the user resized the window. I don't know if this the best way but to fix that I am reimplementing resizeEvent and calling adjustSize to force the sizeHint recalculation.
With those two reimplemented methods MyCustomLabel shows with the right size. I placed this custom widget in a QHBoxLayout with a few other standard widgets. The problem is the other widgets in the layout don't respect the new size of my MyCustomLabel when the user resizes the window. What I end up with is the other widgets in the layout either overlapping or being placed too far from MyCustomLabel. I kind of get it, I am brute forcing my widget to a size and not letting the layout do the work. However I thought updating the sizeHint would inform the layout of MyCustomLabel's new size and adjust everything accordingly. How do I fix this layout problem or am I going about this widthForHeight problem all the wrong way?
Edit:
I tried #AlexanderVX suggestion of setting the SizePolicy to Minimum and while it does prevent the other widgets from overlapping it also locked MyCustomLabel to a fixed size. I need the widget to expand and shrink with the layout. I also tried Preferred, Expanding, MinimumExpanding policies just to see if they would do anything but with no luck.
from __future__ import division
from PySide import QtCore
from PySide import QtGui
import sys
class MyCustomLabel(QtGui.QLabel):
clicked = QtCore.Signal(int)
dblClicked = QtCore.Signal(int)
def __init__(self, leadSide='height', parent=None):
super(MyCustomLabel, self).__init__()
self.leadSide = leadSide
# sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred,
# QtGui.QSizePolicy.Preferred)
# sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding,
# QtGui.QSizePolicy.Expanding)
# sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding,
# QtGui.QSizePolicy.MinimumExpanding)
# sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum,
# QtGui.QSizePolicy.Minimum)
# self.setSizePolicy(sizePolicy)
def resizeEvent(self, event):
super(MyCustomLabel, self).resizeEvent(event)
self.adjustSize()
def sizeHint(self):
super(MyCustomLabel, self).sizeHint()
parentSize = self.parent().size().toTuple()
if self.leadSide.lower() == 'height':
new_size = QtCore.QSize(parentSize[1] * (16 / 9), parentSize[1]) * .9
if self.leadSide.lower() == 'width':
new_size = QtCore.QSize(parentSize[0], parentSize[0] / (16 / 9)) * .9
return new_size
class __Test__(QtGui.QWidget):
def __init__(self):
super(__Test__, self).__init__()
self.initUI()
def initUI(self):
customLabel = MyCustomLabel(leadSide='height')
# customLabel.setScaledContents(True)
customLabel.setStyleSheet('background-color: blue;'
'border:2px solid red;')
btn01 = QtGui.QPushButton('button')
btn01.setFixedHeight(80)
textEdit = QtGui.QTextEdit()
textEdit.setFixedSize(150, 150)
layout01 = QtGui.QHBoxLayout()
layout01.setContentsMargins(0,0,0,0)
layout01.setSpacing(0)
layout01.addWidget(customLabel)
layout01.addWidget(btn01)
layout01.addWidget(textEdit)
self.setLayout(layout01)
self.setGeometry(300, 300, 600, 300)
self.setWindowTitle('Testing')
self.show()
def main():
app = QtGui.QApplication(sys.argv)
ex = __Test__()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
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_())