2026, Jan 08 09:03

Пустой QLayout в PyQt6 оценивается как False: как исправить схлопывание колонок

Разбираем, почему пустой QLayout в PyQt6 считается False и ломает колонную верстку: виджеты уходят в корневой layout. Покажем проверку is None и фикс.

Создание интерфейсов на PyQt6 с колонками на первый взгляд кажется простым: вложите пару QVBoxLayout в основной QHBoxLayout и добавляйте виджеты в каждую колонку. А затем всё упрямо выстраивается в один ряд. Ловушка обычно не в менеджере геометрии, а в том, как код выбирает целевой layout через проверку на истинность, которая незаметно проваливается для пустых компоновок.

Исходная схема

Предполагаемая структура — горизонтальный контейнер с двумя вертикальными колонками. Внутри каждой колонки виджеты должны располагаться сверху вниз, а сами колонки — слева направо. Интерфейс «сплющивается», потому что код откатывается к главному горизонтальному layout, когда layout колонки ещё пуст. Это происходит из‑за невинного на вид условия, которое проверяет сам объект компоновки вместо явной проверки на None.

Проблемный пример кода

Ниже показан фрагмент, демонстрирующий проблему. Имена условны, но логика совпадает с некорректной версией: компоновки колонок существуют, однако ложная проверка воспринимает пустые layout’ы так, будто их вовсе не передали.

import sys
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel, QLineEdit, QLayout,
                             QPushButton, QVBoxLayout, QHBoxLayout, QMessageBox)
from PyQt6.QtCore import Qt
class UiShell(QWidget):
    def __init__(self, title, w=640, h=480, row_mode=False):
        super().__init__()
        self.setWindowTitle(title)
        self.setGeometry(100, 100, w, h)
        self.root_box = QHBoxLayout() if row_mode else QVBoxLayout()
        self.setLayout(self.root_box)
        self.components_log = list()
    def push_section(self):
        column = QVBoxLayout()
        self.root_box.addLayout(column)
        return column
    def put_text(self, text, container=None):
        lab = QLabel(text)
        self.attach_item(lab, container)
        return lab
    def put_button(self, text, on_click=None, container=None):
        container = container if container else self.root_box
        btn = QPushButton(text)
        if on_click:
            btn.clicked.connect(on_click)
        self.attach_item(btn, container)
        return btn
    def attach_item(self, obj, container=None):
        container = container if container else self.root_box
        if not hasattr(container, 'items_bucket'):
            container.items_bucket = list()
        container.items_bucket.append(obj)
        container.addWidget(obj)
app = QApplication([])
win = UiShell("interface", row_mode=True)
left, right = win.push_section(), win.push_section()
win.put_text("Label 1", container=left)
win.put_button("Button 1", container=left)
win.put_text("Label 2", container=right)
win.put_button("Button 2", container=right)
win.show()
sys.exit(app.exec())

Почему так происходит

Суть проблемы — в условии вида if container, применяемом, когда аргумент по умолчанию может быть None. В Python такие условия оценивают булево значение выражения. PyQt добавляет удобное поведение для многих Qt‑типов, включая менеджеры компоновки. В PyQt компоновка получает «истинность» на основе содержимого через модель данных: у компоновок есть метод count(), PyQt сопоставляет его с __len__(), и пустая компоновка оценивается как False. В результате if container трактует пустой QLayout как ложный, хотя объект компоновки полностью валиден и был явно передан. Код отката затем направляет виджеты в главный layout, из‑за чего задуманная колонная структура схлопывается в один ряд.

Правильный подход при сравнении с одиночками вроде None — использовать is или is not. Это не смешивает «пусто, но валидно» с «совсем не передано». Такой стиль соответствует рекомендациям руководства по стилю Python и предотвращает тонкие ошибки, вызванные значениями, ложными, но не равными None. Отметим, что эти удобные реализации «истинности» специфичны для PyQt и отсутствуют в PySide — ещё одна причина писать явные, прозрачно выражающие намерение условия.

Исправление

Замените все проверки на истинность, которые выбирают между «использовать переданный layout» и «вернуться к главному layout», на явные сравнения с None. Тогда переданная пустая компоновка останется целевым контейнером — ровно как и задумано.

import sys
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel, QLineEdit, QLayout,
                             QPushButton, QVBoxLayout, QHBoxLayout, QMessageBox)
from PyQt6.QtCore import Qt
class UiShell(QWidget):
    def __init__(self, title, w=640, h=480, row_mode=False):
        super().__init__()
        self.setWindowTitle(title)
        self.setGeometry(100, 100, w, h)
        self.root_box = QHBoxLayout() if row_mode else QVBoxLayout()
        self.setLayout(self.root_box)
        self.components_log = list()
    def push_section(self):
        column = QVBoxLayout()
        self.root_box.addLayout(column)
        return column
    def put_text(self, text, container=None):
        lab = QLabel(text)
        self.attach_item(lab, container)
        return lab
    def put_button(self, text, on_click=None, container=None):
        if container is None:
            container = self.root_box
        btn = QPushButton(text)
        if on_click:
            btn.clicked.connect(on_click)
        self.attach_item(btn, container)
        return btn
    def attach_item(self, obj, container=None):
        if container is None:
            container = self.root_box
        if not hasattr(container, 'items_bucket'):
            container.items_bucket = list()
        container.items_bucket.append(obj)
        container.addWidget(obj)
app = QApplication([])
win = UiShell("interface", row_mode=True)
left, right = win.push_section(), win.push_section()
win.put_text("Label 1", container=left)
win.put_button("Button 1", container=left)
win.put_text("Label 2", container=right)
win.put_button("Button 2", container=right)
win.show()
sys.exit(app.exec())

Почему это важно

Компоновки в PyQt могут быть валидными объектами и при этом оцениваться как False, пока в них нет ни одного QLayoutItem. Опора на истинность при выборе опций тихо перенаправит виджеты и исказит интерфейс. Использование is None явно выражает намерение, исключает неверную трактовку пустых значений и сохраняет предсказуемость поведения в разных ветках кода. Это также помогает при работе с другими привязками или обёртками Qt, где подобных удобств может не быть или они работают иначе.

Вывод

Если параметр может быть None, сравнивайте через is или is not. Не применяйте if x как замену проверке «был ли аргумент передан». Для layout’ов PyQt это критично: пустой — не значит отсутствующий. Предпочитайте понятный шаблон вроде if target is None: target = default — и колонный интерфейс будет вести себя как задумано.