For my application I created a Window class, I minimized my code as much as possible so that there would be nothing unrelated.
The code is below:
import sys
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel, QLineEdit, QLayout,
QPushButton, QVBoxLayout, QHBoxLayout, QMessageBox)
from PyQt6.QtCore import Qt
class Window(QWidget):
def __init__(self, name, width=640, height=480, horizontal_layout=False):
super().__init__()
self.setWindowTitle(name)
self.setGeometry(100, 100, width, height)
self.main_layout = QHBoxLayout() if horizontal_layout else QVBoxLayout()
self.setLayout(self.main_layout)
self.widget_list = list()
def add_layout(self):
layout = QVBoxLayout()
self.main_layout.addLayout(layout)
return layout
def add_label(self, text, layout=None):
label = QLabel(text)
self.add_widget(label, layout)
return label
def add_button(self, text, callback=None, layout=None):
layout = layout if layout else self.main_layout
button = QPushButton(text)
if callback:
button.clicked.connect(callback)
self.add_widget(button, layout)
return button
def add_widget(self, widget, layout=None):
layout = layout if layout else self.main_layout
if not hasattr(layout, 'widgets_list'):
layout.widgets_list = list()
layout.widgets_list.append(widget)
layout.addWidget(widget)
app = QApplication([])
main = Window("interface", horizontal_layout=True)
col1, col2 = main.add_layout(), main.add_layout()
main.add_label("Label 1", layout=col1)
main.add_button("Button 1", layout=col1)
main.add_label("Label 2", layout=col2)
main.add_button("Button 2", layout=col2)
main.show()
sys.exit(app.exec())
to cut to the chase, I wanted something like on this pic:
What I wanted drawn in paint:
however, this is the result - the columns do not stack things vertically like I wanted them to, so things just get into a single row in the window:
in attempts to fix it, I had some version of the code using the addStretch method to the row layout and wrapping all qvboxlayouts inside qwidgets, it kinda fixed the problem, but all the columns's contents sank to the bottom and absolutely nothing worked to fix it.
I would really like some feedback, if there are alternative decisions to this that do not involve qhboxlayout (for example, if you can wrap it and add some css, tho I dont think display flex would work here) or something else - it would be great. I don't know about using the grid layout though because I really want to specifically find the problem in my code.
When doing comparisons to singletons like None
, you should always use is
or is not
, not the simple if
or if not
. Your code doesn't work as expected because if layout
does not tell whether layout
is None
or not.
Those lines should be changed so that the layout
reference is set to self.main_layout
only if layout
is None
:
def someFunction(self, ..., layout=None):
if layout is None:
layout = self.main_layout
...
In PyQt, the if <PyQt-object>:
syntax does not always result in checking whether <PyQt-object>
is None
or not. If, for instance, <PyQt-object>
is a Qt layout, it checks if the layout contains at least one QLayoutItem, which is an abstract object used to manage the geometry of widgets, spacers or nested layouts within that layout.
Remember that if
is a conditional statement that evaluates the "truthfulness" of an expression: if <something>:
evaluates if <something>
is true or false. If the <something>
is an object, then the expression is evaluated through Truth Value Testing:
By default, an object is considered true unless its class defines either a
__bool__()
method that returns False or a__len__()
method that returns zero, when called with the object.
If an object (typically, a class) does not implement those methods, if <object>:
will enter the block of that statement because the object "exists" (it is "something" that is not "none").
If it does implement one of those functions, it will enter that block only if one those functions return True
or not zero, respectively.
Those are convenience implementations (commonly called "syntactic sugar") allowing things such as if <someObject>
:
if someList:
... # the list has at least one item
if someString:
... # the string is not empty
# as opposed to
if len(someList):
...
if someString != '':
...
Historically, PyQt implements some built-in dunder methods (instance methods that start and end with a double underscore, such as __init__
) used in the data model when certain conditions are met for the class.
Typically, if a Qt class has a count()
function, it normally means that it can contain some items in a mono-dimensional data model (a "list"). This is the case of classes such as, for instance: QListWidget, QComboBox, QPolygon[F], and, interestingly enough, layout managers (including layouts that do not "show" items in a mono-dimensional concept, which is the case of QGridLayout).
In most cases, when a class implements count()
, PyQt also implements the __len__()
method, internally returning the result of count()
.
Remember the "Truth Value Testing" above: if <object>:
means that an object is considered true, unless the class defines either __bool__()
that returns False
, or a __len__()
returning zero.
If a class does contain a count()
function, PyQt implements __len__()
and returns the result of count()
. Consider the following:
combo = None
if not combo:
# this will be printed
print('no combo')
combo = QComboBox()
if not combo:
# this will be printed as well!
print('the combo exists, but it is empty!')
Another example of these convenience implementations is for classes that have a contains()
function, such as QRect, which implements __contains__()
in PyQt; in that case, using the in
operator returns the result of the various QRect::contains()
function overrides:
point = QPoint(1, 2)
rect = QRect(0, 0, 10, 10)
if point in rect:
... # the rectangle contains the point
# as opposed to
if rect.contains(point):
...
Note that these convenience implementations are only available in PyQt, not PySide.
If you plan to release your program allowing the possibility of using either of the bindings (such as using shims like QtPy
or Qt.py
) you cannot use that syntax, and you must follow the official Qt API.
Qt layout managers (subclasses of QLayout) always have a list of their items, it doesn't matter how they're eventually placed or shown in the UI.
Even though QGridLayout shows its items in rows and columns, it still has an internal list of items, and its count()
function returns the length of that list.
This means that, in PyQt, if layout
may evaluate as False
if the layout is empty (therefore the block within if layout:
won't be executed), even though a QLayout object does exist.
None
and better syntaxAs written at the beginning, a reference that may be None
should never be tested with a simple if <object>:
unless you really know what you're doing.
Consider the post if A vs if A is not None:, and the following important note in the Programming Recommendations in the Style Guide for Python Code:
Comparisons to singletons like None should always be done with
is
oris not
, never the equality operators.Also, beware of writing
if x
when you really meanif x is not None
– e.g. when testing whether a variable or argument that defaults to None was set to some other value. The other value might have a type (such as a container) that could be false in a boolean context!
In a case like yours, not only it would be mandatory to use the is None
(or is not None
) testing due to the PyQt implementation explained above, but it would be more appropriate in principle: the usage of None
for keyworded (default) arguments means that the argument can be explicitly used with None
(to follow the default behavior) and with anything that may not compare to None
, as the "Truth Value Testing" explains: if <object>:
may give the same result even if that object is 0, an empty string, False
, an empty list and any other object that may implement __bool__
or __len__
even though the object is not None
.
Your code could be changed in the following way:
layout = layout if layout is not None else self.main_layout
Yet, while sometimes necessary, there is usually no point in overwriting the reference with itself if the object is considered valid. The above would be fundamentally identical to:
if layout is not None:
layout = layout
else:
layout = self.main_layout
You can probably spot the redundancy in the second line: it's completely pointless.
You probably choose the x if x else y
"one-liner" approach for code simplicity, but in reality it's usually inefficient and irrelevant.
Now, consider this approach, using a slightly different syntax while giving an absolutely identical result:
layout = self.main_layout if layout is None else layout
It's slightly simpler, and sometimes preferable. Yet, let's see the more verbose version without the one-liner:
if layout is None:
layout = self.main_layout
else:
layout = layout
It's clear that the whole else:
block is completely pointless.
This means that you can (and should!) change those lines with the following:
def someFunction(self, ..., layout=None):
if layout is None:
layout = self.main_layout
...
Yes, those are two lines, not one. But they are more appropriate.
Note that Python does allow one-liners. The following would be completely legal:
def someFunction(self, ..., layout=None):
if layout is None: layout = self.main_layout
...
Yet, such a syntax is normally discouraged: the block indentation requirement of Python is not just about code fanciness, but readability, which is an extremely important aspect of programming.
The last snippet above will only give extremely small benefits when the code is being parsed from the Python interpreter, but you'd need to have thousands of similar one-liners to see any relevant difference, while still having thousands of less readable lines that will make your code more difficult to read and understand, both while debugging and reviewing (especially if returning to code written a lot of time before).
Remember: don't code "short", code smart.