Why do QGridLayout Widgets move around when adding new ones? - python

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.

Related

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)

With PyQt, what is the preferred (efficient) method for monitoring window size and adjusting layouts?

I have a grid of items in PyQt, and when the user modifies the window size I need to increase/decrease the number of columns accordingly. The number of rows are handled by a scrollarea, so I don't need to worry about changes in the y direction (if that matters).
Inside my implementation of QMainWindow, I know it's possible to override the resizeEvent() function, which will be triggered for any and all window adjustments. However, using that to rebuild the grid everytime is horribly inefficient. Just to test the function to see how it worked, I had resizeEvent merely print a string, and that caused my window adjustments to be slightly laggy and visually imperfect (jittery rather than smooth). I'll probably run a simple division operation on the window size to see if it has gotten larger or smaller enough to change the number of columns, but even that, when run a hundred times per adjustment, might cause lag issues. Rebuilding the entire grid might even take a second to do, so it would be preferable not to need to do it as the user is manipulating the window.
Is there a more efficient way to do it, or is resizeEvent my only option? Ideally, I'd like an event that triggered only once the user finished adjusting the window and not an event that triggers for practically every pixel movement as they happen (which can be hundreds or thousands of times per adjustment in the span of 1 second).
I'm using PyQt5, but if you're more familiar with PyQt4, I can figure out your PyQt4 solution in the context of PyQt5. Same for a C++ Qt4/5 solution.
It looks like the only real problem is detecting when resizing has completed. So long as this is carefully controlled, the actual laying out can be done in any way you like.
Below is a solution that uses a timer to control when a resize-completed signal is emitted. It doesn't appear to be laggy, but I haven't tested it with any complex layouts (should be okay, though).
from PyQt4 import QtCore, QtGui
class Window(QtGui.QWidget):
resizeCompleted = QtCore.pyqtSignal()
def __init__(self):
QtGui.QWidget.__init__(self)
self._resize_timer = None
self.resizeCompleted.connect(self.handleResizeCompleted)
def updateResizeTimer(self, interval=None):
if self._resize_timer is not None:
self.killTimer(self._resize_timer)
if interval is not None:
self._resize_timer = self.startTimer(interval)
else:
self._resize_timer = None
def resizeEvent(self, event):
self.updateResizeTimer(300)
def timerEvent(self, event):
if event.timerId() == self._resize_timer:
self.updateResizeTimer()
self.resizeCompleted.emit()
def handleResizeCompleted(self):
print('resize complete')
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.setGeometry(500, 300, 300, 300)
window.show()
sys.exit(app.exec_())
I think you need a FlowLayoutfot this purpose, which automatically adjusts the number of columns on resizing the widget containing it. Here is the documentation for FlowLayout and here is the PyQt version of the same layout.

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.

pygtk.HBox not redrawing after add

I have a program written in Python, using PyGTK+Glade. I use Glade to create the Layout, and internally, I create some other elements, including a list of labels. I have a VBox of size 3, each of these 3 elements containing an EventBox which contains an HBox.
Each HBox will contain a dinamically changing set of Labels. The problem is that after Adding elemens to the HBox, it doesn't show anything or doesn't get redraw.
As I said, there are some events/functions that change the HBoxes, however, something like this even doesn't work:
def __init__(self):
self.builder = gtk.Builder()
self.builder.add_from_file("maininterface.glade")
self.window = self.builder.get_object("mainWindow")
self.fila1 = self.builder.get_object("hbox1")
self.fila2 = self.builder.get_object("hbox2")
self.fila3 = self.builder.get_object("hbox3")
self.window.show_all()
lab0 = gtk.Label("XXXXXX")
self.fila1.add(lab0) #this label is not shown
#if I uncomment the next line, it works:
#self.window.show_all()
Obviously, I am missing something, I don't know what. I could make all the adds before show_all() but that just works for initialization, the program will remove/add elements on the fly.
PD: I used pack_end() instead of add() but the result is the same.
You will need to call gtk.Widget.show() on each label that you add to the HBox. Alternatively, you can call gtk.Widget.show_all() on the HBox after adding one or more labels.

Categories

Resources