How to set QStackedWidget size to child widgets minimum size? - python

Unable to get the QLabel in this example to be of the minimum size to contain its text. I need the layout and stacked widget to then size themselves to the minimum required to fit the label.
I have used code from https://www.tutorialspoint.com/pyqt/pyqt_qstackedwidget.htm to demonstrate my issue.
Setting the size policies seems to work when the application starts, but increasing the application width eventually causes the label to expand after the list reaches a certain width.
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
class stackedExample(QWidget):
def __init__(self):
super(stackedExample, self).__init__()
self.rightlist = QListWidget()
self.rightlist.setSizePolicy(QSizePolicy.Expanding,QSizePolicy.Expanding)
self.rightlist.insertItem(0, 'Contact')
self.stack1 = QWidget()
self.stack1.setSizePolicy(QSizePolicy.Minimum,QSizePolicy.Minimum)
self.stack1UI()
self.Stack = QStackedWidget(self)
self.Stack.setSizePolicy(QSizePolicy.Minimum,QSizePolicy.Minimum)
self.Stack.addWidget(self.stack1)
hbox = QHBoxLayout(self)
hbox.addWidget(self.Stack)
hbox.addWidget(self.rightlist)
self.setLayout(hbox)
self.rightlist.currentRowChanged.connect(self.display)
self.setGeometry(300, 50, 10, 10)
self.setWindowTitle('StackedWidget demo')
self.show()
def stack1UI(self):
layout = QVBoxLayout()
label = QLabel("Hello World")
label.setSizePolicy(QSizePolicy.Minimum,QSizePolicy.Minimum)
label.setStyleSheet("QLabel { background-color : red; color : blue; }")
layout.addWidget(label)
self.stack1.setLayout(layout)
def display(self, i):
self.Stack.setCurrentIndex(i)
def main():
app = QApplication(sys.argv)
ex = stackedExample()
sys.exit(app.exec_())
if __name__ == '__main__':
main()

tl;dr
Remove the size policy settings for everything but the QStackedWidget only (for which you'll have to set the horizontal policy to Maximum), and everything should be fine.
Explanation
I have to admit: I always felt that QSizePolicy enum names are confusing (I know that I'm not the only one), so I sympathize with your doubts.
Setting the stretch resolves the issue only partially, because sometime you might need or want to manually set the stretches, and that will possibly mess around with some size policies.
The problem is that you're setting the size policy to "Minimum", which, as the QSizePolicy documentation explains, says that:
The sizeHint() is minimal, and sufficient. The widget can be expanded [...]
And that's because Minimum uses the GrowFlag.
This means that, if the layout "thinks" that there's some available space for a widget, it will let it expand: Minimum does not mean that the widget will use it's minimal size (or, better, its minimumSizeHint()), but that it will use the sizeHint() as a minimum size for the layout, while it keeping its capacity to expand; if there's available space, it will use it.
What you actually need is to set the horizontal policy to Maximum instead, and, specifically, to the Stack object only (the QStackWidget, not the QWidget container, nor the QLabel).
That's because Maximum actually uses the ShrinkFlag (again, from the QSizePolicy docs):
The sizeHint() is a maximum. The widget can be shrunk any amount
without detriment if other widgets need the space (e.g. a separator
line). It cannot be larger than the size provided by sizeHint().
That said, be aware that there are known issues with QLabels in certain cases, specifically if the label has word wrapping.

Not sure if it is the correct approach, but adding in a stretch factor seems to have achieved what I was looking for.
hbox.addWidget(self.rightlist, stretch=1)

Related

Why do QGridLayout Widgets move around when adding new ones?

I can't seem to wrap my head around how they work. The the best for placing multiple widgets seems to be QGridLayout but when I add something into a specific row/column and later decide to add somthing into another row/column everything shifts and it's just really frustrating.
For example I would not even be able to do such a simple layout as the google mainpage. When I add a searchbar to the place I want it to be and then add an image/text above it everything moves into weird spots etc and I can't find proper explanations online on how to handle it. Thus I would be delighted if anyone could explain it to an absolute beginner, like me, in an understandable way.
So when I have the following code:
from PyQt6.QtWidgets import *
import sys
import numpy as np
import os
from PyQt6 import QtCore, QtGui
from PyQt6.QtCore import QEvent, Qt
from PyQt6.QtGui import QPalette, QColor
from pathlib import Path
app = QApplication(sys.argv)
class MainWindow(QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.resize(1024, 768)
self.setWindowTitle("Tracker")
layout = QGridLayout()
self.setLayout(layout)
#layout.setRowMinimumHeight(0, 50)
#layout.setColumnMinimumWidth(0,50)
self.input = QLineEdit(self)
self.input.setPlaceholderText('Enter Username')
layout.addWidget(self.input,1,1)
self.input.setFixedSize(300,30)
self.darkmode_check = QCheckBox('Darkmode',self)
self.darkmode_check.toggled.connect(self.darkmode)
self.darkmode_check.setChecked(True)
self.darkmode_check.move(0,0)
def darkmode(self):
if self.darkmode_check.isChecked() == True:
app.setStyleSheet(Path('D:\CODE\League Code\darkorange.qss').read_text())
else:
app.setStyleSheet(Path('D:\CODE\League Code\classic_edit.qss').read_text())
window = MainWindow()
window.show()
sys.exit(app.exec())
I get this screen which is what I want.
When I only want to add a text above this search bar:
by adding
self.text = QLabel(self)
self.text.setText('Tracker')
layout.addWidget(self.text,0,1)
I get this:
which is all over the place.
Does anyone have good explanations on GridLayout or can recommend good websites for it? I found a lot about what the grid looks like etc but nothing helped (and also some posts giving me 3x3 grids, some 4x4 etc, I'm just confused at this point)
I basically just want to place a searchbar in the middle, then a text above that and keep on adding little things here and there.
Thank you
Qt basic layouts always try to evenly divide the available space in its "cells", and each widget will have that space reserved (even if it doesn't use all of it).
Note that different widget types have also different size policies that tell the layout how it should allocate the available space and eventually set the geometry of those widgets.
For instance, QLineEdit has a Fixed vertical policy, meaning that its size hint will always be considered as the only valid height (which is similar to calling setFixedHeight() or setFixedSize() as you did).
QLabel, instead, has a Preferred size policy, meaning that if there's space left in the layout, it can take advantage of it.
When you only have the line edit, the layout only has that widget, so it will place it in the center (because you didn't specify an alignment). But when you add the label, the layout finds that the line edit needs a very small space, so it will leave the remaining to the label, hence your result.
For a simple case like this, you can just specify a proper alignment when adding the widgets: when the alignment is provided, the item will not try to occupy the whole cell and the layout will align it to the available space of that layout cell.
layout.addWidget(self.text, 0, 0, alignment=Qt.AlignBottom)
layout.addWidget(self.input, 1, 0, alignment=Qt.AlignTop)
Note that I changed the column to 0, as there is no point in placing widgets in the second column if there's nothing in the first, unless you want to get advantage of setColumnStretch() or setColumnMinimumWidth().
Also consider that for this kind of "wide" layouts with lots of empty spaces it's usually better to use nested layouts, or use container widgets.
For instance:
layout = QGridLayout(self)
centerLayout = QVBoxLayout()
layout.addLayout(centerLayout, 0, 0, alignment=Qt.AlignCenter)
# ...
centerLayout.addWidget(self.text)
centerLayout.addWidget(self.input)
Or, alternatively:
layout = QGridLayout(self)
centerWidget = QWidget()
layout.addWidget(centerWidget, 0, 0, alignment=Qt.AlignCenter)
centerLayout = QVBoxLayout(centerWidget)
# ... as above
Try to remove the alignment argument in the two examples above and you'll see the difference.
I suggest you to do some experiments using layouts in Qt Designer, which makes it easier to immediately understand how layout work and behave with different widget types.

How to prevent QComboBox from displaying unnecessary scrollbar

The code below, which is based on an example from zetcode.com, creates a single combo box. There are several issues with the resulting dialog, but the following are especially annoying:
PyQt displays a vertical scrollbar for the combo box, although there is plenty of space to display the entire list of options without a scrollbar.
I've tried to move the combo box to a position near the upper-left corner of the window, but this isn't working.
#!/usr/bin/python
import sys
from PyQt5.QtWidgets import QWidget, QLabel, QHBoxLayout, QComboBox, QApplication
class Example(QWidget):
def __init__(self):
super().__init__()
self.setFixedWidth(400)
self.setFixedHeight(500)
self.initUI()
def initUI(self):
hbox = QHBoxLayout()
self.lbl = QLabel('Animals', self)
self.lbl.setStyleSheet('font-size:11pt')
combo = QComboBox(self)
combo.addItem('bear')
combo.addItem('cat')
combo.addItem('dog')
combo.addItem('dolphin')
combo.addItem('elephant')
combo.addItem('fish')
combo.addItem('frog')
combo.addItem('horse')
combo.addItem('rabbit')
combo.addItem('rat')
combo.addItem('shark')
combo.addItem('snake')
combo.addItem('tiger')
combo.addItem('whale')
combo.activated[str].connect(self.onActivated)
hbox.addWidget(combo)
hbox.setSpacing(20)
hbox.addWidget(self.lbl)
self.setContentsMargins(20, 20, 20, 20)
self.setLayout(hbox)
combo.move(20, 60)
self.setWindowTitle('QComboBox')
self.show()
def onActivated(self, text):
self.lbl.setText(text)
self.lbl.adjustSize()
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
There are two wrong assumptions in the question.
the list of a QComboBox is a popup widget, it doesn't care (nor it should) about the available space the combobox might have: consider it as much as a context menu, which just pops out over the window, possibly going outside its boundaries if it requires more space (and that's just because those boundaries are meaningless to the menu);
the combo has been added to a layout manager, which takes care about resizing and positioning its (managed) child widgets, and that's why you cannot manually "move" them: the layout already sets the geometries automatically on its own everytime the widget is resized (which also happen when it's shown the first time), so any attempt to use move(), resize() or setGeometry() is completely useless;
When adding a widget to a layout, the default behavior is to make it occupy as much space as possible; since a QComboBox is one of those widgets that have a fixed size, the result is that it's actually centered (vertically and horizontally) in the space the layout is "assigning" to it, and this is clearly visible in your case because you set a fixed size for the container widget that is much bigger than what its contents would need.
There are two ways to align those widgets on top:
add the alignment arguments to addWidget:
hbox.addWidget(combo, alignment=QtCore.Qt.AlignTop)
hbox.addWidget(self.lbl, alignment=QtCore.Qt.AlignTop)
note that this won't give you good results in your case, because the label and the combo box have different heights, so the label might look "higher" than the combo;
use a QVBoxLayout layout as main layout for the widget, add the horizontal layout to it and then add a stretch after that (a stretch on a box layout is a "spacer" that tries to occupy as much space as possible)
# ...
mainLayout = QVBoxLayout()
mainLayout.addLayout(hbox)
mainLayout.addStretch()
self.setLayout(mainLayout)
PS: if you need to add lots of (string only) elements to a QComboBox, use addItems() instead of individually adding each of them.

QWidget becomes invisible on layout assignment

I'm making a custom window using QGridLayout to place window elements: like title bar, size grip, etc.
When I set any layout to my widget it doesn't show up on a start.
If I set .setVisible(True) it works nice.
So the question is: why does it happen, why widget becomes invisible on layout assignment?
Is that some kind of a bug or so it is conceived?
Widget file is:
from PySide2 import QtWidgets, QtGui, QtCore
class QCustomWindow(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.background_color = QtGui.QColor(23, 23, 34)
self._initUI()
def _initUI(self):
self.setWindowFlag(QtCore.Qt.FramelessWindowHint)
print(self.isVisible())
#self.setVisible(True)
# Is visible without layout
self.setLayout(QtWidgets.QGridLayout())
self.layout().setMargin(0)
self.layout().setSpacing(0)
# ---------------------------------------
def showEvent(self, event: QtGui.QShowEvent):
self.centerOnScreen()
def paintEvent(self, event: QtGui.QPaintEvent):
painter = QtGui.QPainter(self)
painter.setBrush(self.background_color)
painter.setPen(QtCore.Qt.NoPen)
painter.drawRect(0, 0, self.width(), self.height())
def centerOnScreen(self):
screen = QtWidgets.QDesktopWidget()
screen_geometry = screen.screenGeometry(self)
screen_center_x = screen_geometry.center().x()
screen_center_y = screen_geometry.center().y()
self.move(screen_center_x - self.width() // 2,
screen_center_y - self.height() // 2)
App file is:
from PySide2 import QtWidgets
from QCustomWindow import QCustomWindow
import sys
app = QtWidgets.QApplication(sys.argv)
window = QCustomWindow()
window.show()
sys.exit(app.exec_())
Qt does not update the geometry unless necessary or force him to do it, and it does this for efficiency reasons. So if the setVisible(True) or equivalent method is not called then the geometry will not change.
By not calling setVisible(True) then the size is recalculated when the parent widget is visible, and at that moment the widget analyzes the information of the QSizePolicy, QLayouts, etc. In your particular case the one that has the preference has the layout, and the layout calculates the size based on the widgets that are added to it but in your case it has no added widgets so the geometry calculated by the layout is 0x0 making it not visible for our sight. So if you add widgets to the QGridLayout you won't see that problem.
But if you call setVisible(True) the geometry is calculated at that moment, since there is no layout at that moment, the sizeHint is used, which by default is 640x480 and is therefore visible. And when the layout is established the container size is equal to the maximum of the previous size and the size provided by the layout.
Summary:
why widget becomes invisible on layout assignment?
If there is a layout set in a widget, then the size of the container widget will be the size determined by the widgets assigned to the layout, which in your case, since there are none, the size is 0x0.
Is that some kind of a bug or so it is conceived?
No, it is not a bug but an expected behavior. Add widgets or set a size to show the window.

Slow initial show performance of QComboBox with large item count

It looks like calling show on a widget/window that contains a QComboBox with large item count is very slow.
Take the following snippet that compares the performance of using QComboBox vs QTreeWidget
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from time import perf_counter
import sys
class MainWindowComboBox(QMainWindow):
def __init__(self, items, *args, **kwargs):
super().__init__(*args, **kwargs)
widget = QComboBox()
widget.addItems(items)
self.setCentralWidget(widget)
class MainWindowTreeWidget(QMainWindow):
def __init__(self, items, *args, **kwargs):
super().__init__(*args, **kwargs)
widget = QTreeWidget()
items = [QTreeWidgetItem([item]) for item in items]
widget.addTopLevelItems(items)
self.setCentralWidget(widget)
items = [f'item {i}' for i in range(100_000)]
app = QApplication(sys.argv)
window = MainWindowTreeWidget(items)
s = perf_counter()
window.show()
print('took', perf_counter()-s)
app.exec_()
I get the following timings:
using QComboBOx -> 8.09s
using QTreeWidget -> 0.06s
Using QComboBox is orders of magnitude slower.
QTreeWidget, like other item views that are descendant of QAbstractItemView, are not aware of their contents, so they normally use a minimum size hint for the widget size by default, and can eventually be able to resize themselves (normally by expanding if there's available space).
QComboBox, on the other hand, has the sizeAdjustPolicy property which provides a different size hint according to the contents of its internal model.
The default value for that property is AdjustToContentsOnFirstShow, which causes the widget to navigate through the whole content of the model to find the largest item size and use that size for the size hint as soon as the combo is shown the first time.
To obtain each item size, Qt uses a QItemDelegate that is initialized for each item, computes the text size, adds an icon (if it exists) and the spacing required between the icon and the text, and adjusts it to the delegate's margins. As you can imagine, doing that process for a large amount of items requires a lot of time.
As the documentation for the AdjustToMinimumContentsLengthWithIcon value reports:
The combobox will adjust to minimumContentsLength plus space for an
icon. For performance reasons use this policy on large models.
As soon as you set the policy property to this value, the size computation explained above will not happen, and the widget will be shown instantly.
Of course, the drawback of this is that if the combo is in a window that's very small or in a layout that contains other widget that require more space, it will be very small, possibly not even showing any text content at all.
To prevent that, you could set an arbitrary minimum width for the combo based on the item text; it will not be perfect (as Qt adds some margins around the text, and you should also account for the down arrow), but it will be much faster.
Note that, depending on the font, this could give you unexpected result, since I'm using max against the string length, not the actual font width: a string with 8 "i" will be considered larger than one with 7 "w", but the latter will be probably larger whenever you don't use monospaced fonts.
combo = QComboBox()
combo.setSizeAdjustPolicy(combo.AdjustToMinimumContentsLengthWithIcon)
combo.addItems(items)
# use font metrics to obtain the pixel width of the (possibly) longest
# text in the list;
textWidth = self.fontMetrics().width(max(items))
# add some arbitrary margin
combo.setMinimumWidth(textWidth + 20)

How to overload sizehint() method of QMainWindow's layout

I want to make a main window application in PyQt that its size is fixed.
I tried to find a way how to overload sizehint() method of the QMainWindow's layout in order to set the size returned by sizeHint() but didn't know how to do it.
After some researching i have learned that i can set the size of a QMainWindow with QWidget's function setFixedSize(QSize).
But still i'm curious how it can be done the other way.
It could be a lack of knowledge in Python programming.
Just for your curiosity you could do this:
from PyQt4 import QtGui as gui, QtCore as core
class MainWindow(gui.QMainWindow):
def __init__(self):
super(MainWindow,self).__init__()
self.setCentralWidget(gui.QPushButton("Push me!"))
def sizeHint(self):
return core.QSize(300,500)
app = gui.QApplication([])
mw = MainWindow()
mw.show()
app.exec_()
You should keep in mind that sizeHint is only used to get a default size for the widgets when it is applicable. I mean when there is some extra space that could be filled or when there is more than one widget that share space in a layout, just experiment with it an layouts and you will see, could be very helpful! ;)
Finally, it is not a valid approach for setting a fixed size, for that just use setFixedSize as you posted.

Categories

Resources