Slow initial show performance of QComboBox with large item count - python

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)

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 draw rectangle on Qlabel of image with mouse in the PyQt5 [duplicate]

I display images with Qlabel.I need image coordinates/pixel coordinates but, I use mouseclickevent its show me only Qlabel coordinates.
for examples my image is 800*753 and my Qlabel geometry is (701,451).I reads coordinates in (701,451) but I need image coordinates in (800*753)
def resimac(self):
filename= QtWidgets.QFileDialog.getOpenFileName(None, 'Resim Yükle', '.', 'Image Files (*.png *.jpg *.jpeg *.bmp *.tif)')
self.image=QtGui.QImage(filename[0])
self.pixmap=QtGui.QPixmap.fromImage(self.image)
self.resim1.setPixmap(self.pixmap)
self.resim1.mousePressEvent=self.getPixel
def getPixel(self, event):
x = event.pos().x()
y = event.pos().y()
print("X=",x," y= ",y)
Since you didn't provide a minimal, reproducible example, I'm going to assume that you're probably setting the scaledContents property, but that could also be not true (in case you set a maximum or fixed size for the label).
There are some other serious issues about your answer, I'll address them at the end of this answer.
The point has to be mapped to the pixmap coordinates
When setting a pixmap to a QLabel, Qt automatically resizes the label to its contents.
Well, it does it unless the label has some size constrains: a maximum/fixed size that is smaller than the pixmap, and/or the QLabel has the scaledContents property set to True as written above. Note that this also happens if any of its ancestors has some size constraints (for example, the main window has a maximum size, or it's maximized to a screen smaller than the space the window needs).
In any of those cases, the mousePressEvent will obviously give you the coordinates based on the widget, not on the pixmap.
First of all, even if it doesn't seem to be that important, you'll have to consider that every widget can have some contents margins: the widget will still receive events that happen inside the area of those margins, even if they are outside its actual contents, so you'll have to consider that aspect, and ensure that the event happens within the real geometry of the widget contents (in this case, the pixmap). If that's true, you'll have to translate the event position to that rectangle to get its position according to the pixmap.
Then, if the scaledContents property is true, the image will be scaled to the current available size of the label (which also means that its aspect ratio will not be maintained), so you'll need to scale the position.
This is just a matter of math: compute the proportion between the image size and the (contents of the) label, then multiply the value using that proportion.
# click on the horizontal center of the widget
mouseX = 100
pixmapWidth = 400
widgetWidth = 200
xRatio = pixmapWidth / widgetWidth
# xRatio = 2.0
pixmapX = mouseX * xRatio
# the resulting "x" is the horizontal center of the pixmap
# pixmapX = 200
On the other hand, if the contents are not scaled you'll have to consider the QLabel alignment property; it is usually aligned on the left and vertically centered, but that depends on the OS, the style currently in use and the localization (consider right-to-left writing languages). This means that if the image is smaller than the available size, there will be some empty space within its margins, and you'll have to be aware of that.
In the following example I'm trying to take care about all of that (I'd have to be honest, I'm not 100% sure, as there might be some 1-pixel tolerance due to various reasons, most regarding integer-based coordinates and DPI awareness).
Note that instead of overwriting mousePressEvent as you did, I'm using an event filter, I'll explain the reason for it afterwards.
from PyQt5 import QtCore, QtGui, QtWidgets
class Window(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
layout = QtWidgets.QGridLayout(self)
self.getImageButton = QtWidgets.QPushButton('Select')
layout.addWidget(self.getImageButton)
self.getImageButton.clicked.connect(self.resimac)
self.resim1 = QtWidgets.QLabel()
layout.addWidget(self.resim1)
self.resim1.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter)
# I'm assuming the following...
self.resim1.setScaledContents(True)
self.resim1.setFixedSize(701,451)
# install an event filter to "capture" mouse events (amongst others)
self.resim1.installEventFilter(self)
def resimac(self):
filename, filter = QtWidgets.QFileDialog.getOpenFileName(None, 'Resim Yükle', '.', 'Image Files (*.png *.jpg *.jpeg *.bmp *.tif)')
if not filename:
return
self.resim1.setPixmap(QtGui.QPixmap(filename))
def eventFilter(self, source, event):
# if the source is our QLabel, it has a valid pixmap, and the event is
# a left click, proceed in trying to get the event position
if (source == self.resim1 and source.pixmap() and not source.pixmap().isNull() and
event.type() == QtCore.QEvent.MouseButtonPress and
event.button() == QtCore.Qt.LeftButton):
self.getClickedPosition(event.pos())
return super().eventFilter(source, event)
def getClickedPosition(self, pos):
# consider the widget contents margins
contentsRect = QtCore.QRectF(self.resim1.contentsRect())
if pos not in contentsRect:
# outside widget margins, ignore!
return
# adjust the position to the contents margins
pos -= contentsRect.topLeft()
pixmapRect = self.resim1.pixmap().rect()
if self.resim1.hasScaledContents():
x = pos.x() * pixmapRect.width() / contentsRect.width()
y = pos.y() * pixmapRect.height() / contentsRect.height()
pos = QtCore.QPoint(x, y)
else:
align = self.resim1.alignment()
# for historical reasons, QRect (which is based on integer values),
# returns right() as (left+width-1) and bottom as (top+height-1),
# and so their opposite functions set/moveRight and set/moveBottom
# take that into consideration; using a QRectF can prevent that; see:
# https://doc.qt.io/qt-5/qrect.html#right
# https://doc.qt.io/qt-5/qrect.html#bottom
pixmapRect = QtCore.QRectF(pixmapRect)
# the pixmap is not left aligned, align it correctly
if align & QtCore.Qt.AlignRight:
pixmapRect.moveRight(contentsRect.x() + contentsRect.width())
elif align & QtCore.Qt.AlignHCenter:
pixmapRect.moveLeft(contentsRect.center().x() - pixmapRect.width() / 2)
# the pixmap is not top aligned (note that the default for QLabel is
# Qt.AlignVCenter, the vertical center)
if align & QtCore.Qt.AlignBottom:
pixmapRect.moveBottom(contentsRect.y() + contentsRect.height())
elif align & QtCore.Qt.AlignVCenter:
pixmapRect.moveTop(contentsRect.center().y() - pixmapRect.height() / 2)
if not pos in pixmapRect:
# outside image margins, ignore!
return
# translate coordinates to the image position and convert it back to
# a QPoint, which is integer based
pos = (pos - pixmapRect.topLeft()).toPoint()
print('X={}, Y={}'.format(pos.x(), pos.y()))
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = Window()
w.show()
sys.exit(app.exec_())
Now. A couple of suggestions.
Don't overwrite existing child object methods with [other] object's instance attributes
There are various reasons for which this is not a good idea, and, while dealing with Qt, the most important of them is that Qt uses function caching for virtual functions; this means that as soon as a virtual is called the first time, that function will always be called in the future. While your approach could work in simple cases (especially if the overwriting happens within the parent's __init__), it's usually prone to unexpected behavior that's difficult to debug if you're not very careful.
And that's exactly your case: I suppose that resimac is not called upon parent instantiation and until after some other event (possibly a clicked button) happens. But if the user, for some reason, clicks on the label before a new pixmap is loaded, your supposedly overwritten method will never get called: at that time, you've not overwritten it yet, so the user clicks the label, Qt calls the QLabel's base class mousePressEvent implementation, and then that method will always be called from that point on, no matter if you try to overwrite it.
To work around that, you have at least 3 options:
use an event filter (as the example above); an event filter is something that "captures" events of a widgets and allows you to observe (and interact) with it; you can also decide to propagate that event to the widget's parent or not (that's mostly the case of key/mouse events: if a widget isn't "interested" about one of those events, it "tells" its parent to care about it); this is the simplest method, but it can become hard to implement and debug for complex cases;
subclass the widget and manually add it to your GUI within your code;
subclass it and "promote" the widget if you're using Qt's Designer;
You don't need to use a QImage for a QLabel.
This is not that an issue, it's just a suggestion: QPixmap already uses (sort of) fromImage within its C++ code when constructing it with a path as an argument, so there's no need for that.
Always, always provide usable, Minimal Reproducible Example code.
See:
https://stackoverflow.com/help/how-to-ask
https://stackoverflow.com/help/minimal-reproducible-example
It could take time, even hours to get an "MRE", but it's worth it: there'll always somebody that could answer you, but doesn't want to or couldn't dig into your code for various reasons (mostly because it's incomplete, vague, inusable, lacking context, or even too expanded). If, for any reason, there'll be just that one user, you'll be losing your occasion to solve your problem. Be patient, carefully prepare your questions, and you'll probably get plenty of interactions and useful insight from it.

When QListWidget loads a large number of image, it will become very slow. Is there a way to load by QScrollBar? Or is there a better way?

I want to get a list of image thumbnails, When QListWidget loads a large number of image, it will become very slow.More than 200 pictures will take 5s to load.
One-time loading seems to be a stupid way:
from PyQt5 import QtWidgets
from PyQt5 import QtGui
from PyQt5 import QtCore
from PyQt5 import Qt
import os
class MyQListWidgetItem(QtWidgets.QListWidgetItem):
'''icon item'''
def __init__(self, path, parent=None):
self.icon = QtGui.QIcon(path)
super(MyQListWidgetItem, self).__init__(self.icon, '', parent)
class MyQListWidget(QtWidgets.QListWidget):
def __init__(self):
super(MyQListWidget, self).__init__()
path = './imgpath'
self.setFlow(QtWidgets.QListView.LeftToRight)
self.setIconSize(QtCore.QSize(180, 160))
self.setResizeMode(Qt.QListWidget.Adjust)
#add icon
for fp in os.listdir(path):
self.addItem(MyQListWidgetItem(os.path.join(path, fp), self))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = MyQListWidget()
w.show()
sys.exit(app.exec_())
There are various possible approaches for this.
The important aspect is to delay the loading of images until it's actually required.
In the following example I used two custom roles to simplify the process: PathRole contains the full path to the image, ImageRequestedRole is a "flag" that tells if the image has been already loaded (or queued for loading).
The priority obviously goes to the images that are currently visible in the viewport, and we need to ensure that whenever the visible area changes the images are loaded as soon as possible.
To achieve that, I connected the scroll bar valueChanged and rangeChanged signals (the latter is mostly required on startup) to a function that checks the range of visible indexes and verifies whether they contain a path and if they have not been loaded nor queued yet. This will also queue loading of images whenever the window is enlarged to a bigger size which would show items previously hidden.
Once the function above finds that some images require loading, they are queued, and a timer is started (if not already active): using a timer ensures that loading is progressive and doesn't block the whole UI until all requested images are processed.
Some important aspects:
images are not stored as their source (otherwise you'll easily end up all resources), but scaled down.
a "lazy loader" ensures that images that are not currently shown are lazily loaded as soon as the current queue is completed; note that if you plan to browse through huge amount of images that are also very big, this is not suggested.
since the images are not loaded instantly, the items don't have a correct size by default: setting the icon size is not sufficient, as that size not considered until the item actually has a "decoration"; to work around that, a delegate is used, which implements the sizeHint method and sets a decoration size even if the image is not yet loaded: this ensures that the view already reserves enough space for each item without continuously computing positions relative to every other item.
setting the "loaded" flag requires writing data on the model, which by default causes the view to compute again sizes; to avoid that, a temporary signal blocker is used, so that the model is updated without letting it know to the view.
for performance reasons, you cannot have different widths for each image depending on the image aspect ratio.
PathRole = QtCore.Qt.UserRole + 1
ImageRequestedRole = PathRole + 1
class ImageDelegate(QtWidgets.QStyledItemDelegate):
def initStyleOption(self, opt, index):
super().initStyleOption(opt, index)
if index.data(PathRole):
opt.features |= opt.HasDecoration
opt.decorationSize = QtCore.QSize(180, 160)
class MyQListWidget(QtWidgets.QListWidget):
def __init__(self):
super(MyQListWidget, self).__init__()
path = './imgpath'
self.setFlow(QtWidgets.QListView.LeftToRight)
self.setIconSize(QtCore.QSize(180, 160))
self.setResizeMode(Qt.QListWidget.Adjust)
for fp in os.listdir(path):
imagePath = os.path.join(path, fp)
item = QtWidgets.QListWidgetItem()
if os.path.isfile(imagePath):
item.setData(PathRole, imagePath)
self.addItem(item)
self.imageDelegate = ImageDelegate(self)
self.setItemDelegate(self.imageDelegate)
self.imageQueue = []
self.loadTimer = QtCore.QTimer(
interval=25, timeout=self.loadImage, singleShot=True)
self.lazyTimer = QtCore.QTimer(
interval=100, timeout=self.lazyLoadImage, singleShot=True)
self.lazyIndex = 0
self.horizontalScrollBar().valueChanged.connect(self.checkVisible)
self.horizontalScrollBar().rangeChanged.connect(self.checkVisible)
def checkVisible(self):
start = self.indexAt(QtCore.QPoint()).row()
end = self.indexAt(self.viewport().rect().bottomRight()).row()
if end < 0:
end = start
model = self.model()
for row in range(start, end + 1):
index = model.index(row, 0)
if not index.data(ImageRequestedRole) and index.data(PathRole):
with QtCore.QSignalBlocker(model):
model.setData(index, True, ImageRequestedRole)
self.imageQueue.append(index)
if self.imageQueue and not self.loadTimer.isActive():
self.loadTimer.start()
def requestImage(self, index):
with QtCore.QSignalBlocker(self.model()):
self.model().setData(index, True, ImageRequestedRole)
self.imageQueue.append(index)
if not self.loadTimer.isActive():
self.loadTimer.start()
def loadImage(self):
if not self.imageQueue:
return
index = self.imageQueue.pop()
image = QtGui.QPixmap(index.data(PathRole))
if not image.isNull():
self.model().setData(
index,
image.scaled(self.iconSize(), QtCore.Qt.KeepAspectRatio),
QtCore.Qt.DecorationRole
)
if self.imageQueue:
self.loadTimer.start()
else:
self.lazyTimer.start()
def lazyLoadImage(self):
self.lazyIndex += 1
if self.lazyIndex >= self.count():
return
index = self.model().index(self.lazyIndex, 0)
if not index.data(ImageRequestedRole) and index.data(PathRole):
with QtCore.QSignalBlocker(self.model()):
self.model().setData(index, True, ImageRequestedRole)
image = QtGui.QPixmap(index.data(PathRole))
if not image.isNull():
self.model().setData(
index,
image.scaled(self.iconSize(), QtCore.Qt.KeepAspectRatio),
QtCore.Qt.DecorationRole
)
else:
self.lazyLoadImage()
return
if not self.imageQueue:
self.lazyTimer.start()
Finally, consider that this is a very basic and simple implementation for learning purposes:
an image viewer should not store all images in memory (not even as thumbnails as in my example): consider that images are stored as raster ("bitmap"), so even a thumbnail could occupy much more memory than the original compressed image;
a cache could be used in a temporary path in case a maximum amount of thumbnail is reached;
image loading should happen in a separate thread, possibly displaying a placeholder until the process is complete;
appropriate checks should be done to ensure that a file is actually an image, and/or if the image is corrupted;
unless you plan to show something else beside the thumbnail (file name, stats, etc), you should probably consider implementing the paint function of the delegate, otherwise some margin will always be shown on the right of the image;

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.

How to set QStackedWidget size to child widgets minimum size?

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)

Categories

Resources