Spec-Zone.ru › Qt 5.15

Управление макетом

Система макетов Qt предоставляет простой и мощный способ автоматического размещения дочерних виджетов внутри виджета, гарантируя, что они эффективно используют доступное пространство.

Введение

Qt включает набор классов управления макетом, используемых для описания того, как виджеты расположены в пользовательском интерфейсе приложения. Эти макеты автоматически позиционируют и изменяют размер виджетов при изменении доступного для них пространства, обеспечивая согласованное расположение и сохранение удобства использования пользовательского интерфейса в целом.

Все подклассы QWidget могут использовать макеты для управления своими дочерними элементами. Функция QWidget::setLayout() применяет макет к виджету. Когда макет установлен на виджете таким образом, он берет на себя следующие задачи:

  • Позиционирование дочерних виджетов
  • Установление разумных размеров по умолчанию для окон
  • Установление разумных минимальных размеров для окон
  • Обработка изменения размеров
  • Автоматические обновления при изменении содержимого:
    • Размер шрифта, текст или другое содержимое дочерних виджетов
    • Скрытие или показ дочернего виджета
    • Удаление дочерних виджетов

Классы макетов Qt

Классы макетов Qt были разработаны для написания кода на C++, позволяя указывать измерения в пикселях для простоты, поэтому они просты в понимании и использовании. Код, сгенерированный для форм, созданных с помощью Qt Designer, также использует классы макетов. Qt Designer полезен для экспериментов с дизайном формы, поскольку он избегает цикла компиляции, компоновки и запуска, обычно связанного с разработкой пользовательского интерфейса.

class="annotated">

QBoxLayout

Выравнивает дочерние виджеты по горизонтали или вертикали

QButtonGroup

Контейнер для организации групп виджетов кнопок

QFormLayout

Управляет формами входных виджетов и их соответствующих меток

QGraphicsAnchor

Представляет якорь между двумя элементами в QGraphicsAnchorLayout

QGraphicsAnchorLayout

Макет, где можно привязывать виджеты друг к другу в представлении Graphics View

QGridLayout

Размещает виджеты в сетке

QGroupBox

Рамка группы с заголовком

QHBoxLayout

Выравнивает виджеты по горизонтали

QLayout

Базовый класс менеджеров геометрии

QLayoutItem

Абстрактный элемент, который управляет QLayout

QSizePolicy

Атрибут макета, описывающий политику изменения размера по горизонтали и вертикали

QSpacerItem

Пустое пространство в макете

QStackedLayout

Стопка виджетов, где только один виджет виден в данный момент

QStackedWidget

Стопка виджетов, где только один виджет виден в данный момент

QVBoxLayout

Выравнивает виджеты по вертикали

QWidgetItem

Элемент макета, представляющий виджет

Горизонтальные, вертикальные, сетчатые и формулярные макеты

Самый простой способ дать виджетам хороший макет — использовать встроенные менеджеры макетов: QHBoxLayout, QVBoxLayout, QGridLayout и QFormLayout. Эти классы наследуют от QLayout, который в свою очередь наследуется от QObject (а не QWidget). Они занимаются управлением геометрией набора виджетов. Для создания более сложных макетов можно вкладывать менеджеры макетов друг в друга.

  • QHBoxLayout выстраивает виджеты в горизонтальную строку слева направо (или справа налево для языков с направлением письма справа налево).

  • QVBoxLayout выстраивает виджеты в вертикальную колонку сверху вниз.

  • QGridLayout выстраивает виджеты в двумерную сетку. Виджеты могут занимать несколько ячеек.

  • QFormLayout выстраивает виджеты в макет 2 столбца, стиль меток-полей.

Размещение виджетов в коде

Следующий код создает QHBoxLayout, который управляет геометрией пяти QPushButtons, как показано на первом снимке экрана выше:

    QWidget *window = new QWidget;
    QPushButton *button1 = new QPushButton("One");
    QPushButton *button2 = new QPushButton("Two");
    QPushButton *button3 = new QPushButton("Three");
    QPushButton *button4 = new QPushButton("Four");
    QPushButton *button5 = new QPushButton("Five");

    QHBoxLayout *layout = new QHBoxLayout;
    layout->addWidget(button1);
    layout->addWidget(button2);
    layout->addWidget(button3);
    layout->addWidget(button4);
    layout->addWidget(button5);

    window->setLayout(layout);
    window->show();

Код для QVBoxLayout идентичен, за исключением строки создания макета. Код для QGridLayout немного отличается, потому что нам нужно указать строку и столбец расположения дочернего виджета:

    QWidget *window = new QWidget;
    QPushButton *button1 = new QPushButton("One");
    QPushButton *button2 = new QPushButton("Two");
    QPushButton *button3 = new QPushButton("Three");
    QPushButton *button4 = new QPushButton("Four");
    QPushButton *button5 = new QPushButton("Five");

    QGridLayout *layout = new QGridLayout;
    layout->addWidget(button1, 0, 0);
    layout->addWidget(button2, 0, 1);
    layout->addWidget(button3, 1, 0, 1, 2);
    layout->addWidget(button4, 2, 0);
    layout->addWidget(button5, 2, 1);

    window->setLayout(layout);
    window->show();

Третья кнопка QPushButton занимает 2 столбца. Это возможно, указав 2 в качестве пятого аргумента для QGridLayout::addWidget().

QFormLayout добавит два виджета в строку, обычно QLabel и QLineEdit для создания форм. Добавление QLabel и QLineEdit в одну строку установит QLineEdit в качестве партнера QLabel. Следующий код будет использовать QFormLayout для размещения трех QPushButtons и соответствующего QLineEdit в строке.

    QWidget *window = new QWidget;
    QPushButton *button1 = new QPushButton("One");
    QLineEdit *lineEdit1 = new QLineEdit();
    QPushButton *button2 = new QPushButton("Two");
    QLineEdit *lineEdit2 = new QLineEdit();
    QPushButton *button3 = new QPushButton("Three");
    QLineEdit *lineEdit3 = new QLineEdit();

    QFormLayout *layout = new QFormLayout;
    layout->addRow(button1, lineEdit1);
    layout->addRow(button2, lineEdit2);
    layout->addRow(button3, lineEdit3);

    window->setLayout(layout);
    window->show();

Рекомендации по использованию макетов

При использовании макета вам не нужно передавать родительский элемент при построении дочерних виджетов. Макет автоматически сменит родительский элемент виджетов (используя QWidget::setParent()), чтобы они стали дочерними элементами виджета, на котором установлен макет.

Примечание: Виджеты в макете являются дочерними элементами виджета, на котором установлен макет, а не самого макета. Виджеты могут иметь только другие виджеты в качестве родителя, а не макеты.

Можно вложить макеты, используя addLayout() на макете; внутренний макет затем становится дочерним элементом макета, в который он вставлен.

Добавление виджетов в макет

Когда вы добавляете виджеты в макет, процесс макетирования работает следующим образом:

  1. Все виджеты изначально получают пространство в соответствии с их QWidget::sizePolicy() и QWidget::sizeHint().
  2. Если у каких-либо виджетов установлены коэффициенты растяжения с значением больше нуля, то им выделяется пространство пропорционально их коэффициенту растяжения (объясняется ниже).
  3. Если у каких-либо виджетов установлены коэффициенты растяжения, равные нулю, они получат больше места только в том случае, если другие виджеты не хотят этого места. Из этих виджетов место выделяется виджетам с политикой размера Expanding в первую очередь.
  4. Любые виджеты, которым выделяется меньше места, чем их минимальный размер (или минимальная подсказка размера, если минимальный размер не задан), получают этот минимальный требуемый размер. (Виджеты не должны иметь минимальный размер или минимальную подсказку размера, в этом случае коэффициент растяжения является определяющим фактором).
  5. Любые виджеты, которым выделяется больше места, чем их максимальный размер, получают максимальное требуемое пространство. (Виджеты могут не иметь максимального размера, в этом случае коэффициент растяжения является определяющим фактором).

Коэффициенты растяжения

Виджеты обычно создаются без установленного коэффициента растяжения. При размещении в макете виджетам выделяется часть пространства в соответствии с их QWidget::sizePolicy() или их минимальной подсказкой размера, в зависимости от того, что больше. Коэффициенты растяжения используются для изменения того, сколько места виджетам выделяется по отношению друг к другу.

Если у нас есть три виджета, расположенные в QHBoxLayout без установленных коэффициентов растяжения, мы получим макет, подобный этому:

Three widgets in a row

Если мы установим коэффициенты растяжения для каждого виджета, они будут размещены пропорционально (но никогда меньше их минимальной подсказки размера), например:

Three widgets with different stretch factors in a row

Пользовательские виджеты в макетах

Когда вы создаете свой собственный класс виджета, вы также должны передать его свойства макета. Если виджет использует макеты Qt, это уже сделано. Если виджет не имеет дочерних виджетов или использует ручной макет, вы можете изменить поведение виджета с помощью одного или всех следующих механизмов:

  • Переопределите QWidget::sizeHint(), чтобы вернуть предпочтительный размер виджета.
  • Переопределите QWidget::minimumSizeHint(), чтобы вернуть наименьший размер, который может иметь виджет.
  • Вызовите QWidget::setSizePolicy(), чтобы указать требования к пространству виджета.

Вызовите QWidget::updateGeometry() всякий раз, когда меняется подсказка размера, минимальная подсказка размера или политика размера. Это вызовет перерасчет макета. Несколько последовательных вызовов QWidget::updateGeometry() вызовут только один перерасчет макета.

Если предпочтительная высота вашего виджета зависит от его фактической ширины (например, метка с автоматическим разрывом слов), установите флаг height-for-width в политике размера виджета и переопределите QWidget::heightForWidth().

Даже если вы реализовали QWidget::heightForWidth(), все равно рекомендуется предоставить разумный размер подсказки (sizeHint).

Для получения дополнительных рекомендаций по реализации этих функций, см. статью Qt Quarterly Торговля высотой на ширину.

Проблемы с макетом

Использование форматированного текста в виджете метки может создать некоторые проблемы с макетом его родительского виджета. Проблемы возникают из-за того, как менеджеры макета Qt обрабатывают форматированный текст, когда метка имеет перенос слов.

В некоторых случаях родительский макет переходит в режим QLayout::FreeResize, что означает, что он не будет адаптировать макет своего содержимого для размещения в окнах небольшого размера, или даже предотвратит уменьшение окна до непригодного для использования размера. Это можно преодолеть, создав подкласс проблемных виджетов и реализовав соответствующие функции sizeHint() и minimumSizeHint().

В некоторых случаях это актуально, когда макет добавляется в виджет. При установке виджета QDockWidget или QScrollArea (с использованием QDockWidget::setWidget() и QScrollArea::setWidget()), макет уже должен быть установлен на виджете. В противном случае виджет не будет отображаться.

Ручной макет

Если вы создаете уникальный специальный макет, вы также можете создать пользовательский виджет, как описано выше. Переопределите QWidget::resizeEvent(), чтобы рассчитать необходимое распределение размеров и вызвать setGeometry() для каждого дочернего элемента.

Виджет получит событие типа QEvent::LayoutRequest, когда макет нужно пересчитать. Переопределите QWidget::event(), чтобы обработать события QEvent::LayoutRequest.

Как написать пользовательский менеджер макета

Альтернативой ручному макету является создание собственного менеджера макета путём создания подкласса QLayout. Примеры Border Layout и Flow Layout показывают, как это сделать.

Здесь мы подробно представим пример. Класс CardLayout вдохновлен одноимённым менеджером макета Java. Он размещает элементы (виджеты или вложенные макеты) друг над другом, смещая каждый элемент на расстояние QLayout::spacing().

Чтобы написать свой собственный класс макета, вы должны определить следующее:

  • Структуру данных для хранения элементов, обрабатываемых макетом. Каждый элемент — это QLayoutItem. В этом примере мы будем использовать QVector.
  • addItem(), способ добавления элементов в макет.
  • setGeometry(), способ выполнения макета.
  • sizeHint(), предпочтительный размер макета.
  • itemAt(), способ перебора макета.
  • takeAt(), способ удаления элементов из макета.

В большинстве случаев вы также реализуете minimumSize().

Заголовочный файл (card.h)

#ifndef CARD_H
#define CARD_H

#include <QtWidgets>
#include <QVector>

class CardLayout : public QLayout
{
public:
    CardLayout(int spacing): QLayout()
    { setSpacing(spacing); }
    CardLayout(int spacing, QWidget *parent): QLayout(parent)
    { setSpacing(spacing); }
    ~CardLayout();

    void addItem(QLayoutItem *item) override;
    QSize sizeHint() const override;
    QSize minimumSize() const override;
    int count() const override;
    QLayoutItem *itemAt(int) const override;
    QLayoutItem *takeAt(int) override;
    void setGeometry(const QRect &rect) override;

private:
    QVector<QLayoutItem*> m_items;
};
#endif

Файл реализации (card.cpp)

//#include "card.h"

Сначала мы определяем count() для получения количества элементов в списке.

int CardLayout::count() const
{
    // QVector::size() returns the number of QLayoutItems in m_items
    return m_items.size();
}

Затем мы определяем две функции, которые выполняют перебор макета: itemAt() и takeAt(). Эти функции используются внутри системы макета для обработки удаления виджетов. Они также доступны для разработчиков приложений.

itemAt() возвращает элемент по заданному индексу. takeAt() удаляет элемент по заданному индексу и возвращает его. В этом случае мы используем индекс списка в качестве индекса макета. В других случаях, когда у нас более сложная структура данных, мы можем потратить больше усилий на определение линейного порядка элементов.

QLayoutItem *CardLayout::itemAt(int idx) const
{
    // QVector::value() performs index checking, and returns nullptr if we are
    // outside the valid range
    return m_items.value(idx);
}

QLayoutItem *CardLayout::takeAt(int idx)
{
    // QVector::take does not do index checking
    return idx >= 0 && idx < m_items.size() ? m_items.takeAt(idx) : 0;
}

addItem() реализует стратегию размещения по умолчанию для элементов макета. Эта функция должна быть реализована. Она используется QLayout::add(), а также конструктором QLayout, принимающим макет в качестве родителя. Если ваш макет имеет расширенные параметры размещения, требующие параметров, вы должны предоставить дополнительные функции доступа, такие как перегрузки с указанием строки и столбца для QGridLayout::addItem(), QGridLayout::addWidget() и QGridLayout::addLayout().

void CardLayout::addItem(QLayoutItem *item)
{
    m_items.append(item);
}

Макет берёт на себя ответственность за добавленные элементы. Так как QLayoutItem не наследует QObject, мы должны удалить элементы вручную. В деструкторе мы удаляем каждый элемент из списка с помощью takeAt(), а затем удаляем его.

CardLayout::~CardLayout()
{
     QLayoutItem *item;
     while ((item = takeAt(0)))
         delete item;
}

Функция setGeometry() фактически выполняет макет. Прямоугольник, переданный в качестве аргумента, не включает margin(). Если это актуально, используйте spacing() в качестве расстояния между элементами.

void CardLayout::setGeometry(const QRect &r)
{
    QLayout::setGeometry(r);

    if (m_items.size() == 0)
        return;

    int w = r.width() - (m_items.count() - 1) * spacing();
    int h = r.height() - (m_items.count() - 1) * spacing();
    int i = 0;
    while (i < m_items.size()) {
        QLayoutItem *o = m_items.at(i);
        QRect geom(r.x() + i * spacing(), r.y() + i * spacing(), w, h);
        o->setGeometry(geom);
        ++i;
    }
}

sizeHint() и minimumSize() обычно очень похожи по реализации. Возвращаемые этими функциями размеры должны включать spacing(), но не margin().

QSize CardLayout::sizeHint() const
{
    QSize s(0, 0);
    int n = m_items.count();
    if (n > 0)
        s = QSize(100, 70); //start with a nice default size
    int i = 0;
    while (i < n) {
        QLayoutItem *o = m_items.at(i);
        s = s.expandedTo(o->sizeHint());
        ++i;
    }
    return s + n * QSize(spacing(), spacing());
}

QSize CardLayout::minimumSize() const
{
    QSize s(0, 0);
    int n = m_items.count();
    int i = 0;
    while (i < n) {
        QLayoutItem *o = m_items.at(i);
        s = s.expandedTo(o->minimumSize());
        ++i;
    }
    return s + n * QSize(spacing(), spacing());
}

Дополнительные замечания

  • Этот пользовательский макет не обрабатывает высоту относительно ширины.
  • Мы игнорируем QLayoutItem::isEmpty(); это означает, что макет будет рассматривать скрытые виджеты как видимые.
  • Для сложных макетов скорость можно значительно увеличить, кешируя вычисленные значения. В этом случае реализуйте QLayoutItem::invalidate(), чтобы пометить кешированные данные как устаревшие.
  • Вызов QLayoutItem::sizeHint() и т. д. может быть дорогостоящим. Поэтому, если вам нужно значение повторно в той же функции, сохраните его в локальной переменной.
  • Не следует вызывать QLayoutItem::setGeometry() дважды для одного и того же элемента в одной и той же функции. Этот вызов может быть очень дорогостоящим, если элемент имеет несколько дочерних виджетов, потому что менеджер макета должен выполнить полный макет каждый раз. Вместо этого вычислите геометрию и затем установите её. (Это относится не только к макетам, вы должны сделать то же самое, если реализуете свой собственный resizeEvent(), например.)

Примеры макетов

Многие примеры Qt Widgets примеров уже используют макеты, однако существуют несколько примеров для демонстрации различных макетов.

Пример адресной книги

Введение в программирование графического интерфейса, показывающее, как собрать простое, но полностью функциональное приложение.

Пример макета границы

Показывает, как разместить дочерние виджеты вдоль границы.

Пример калькулятора

Пример демонстрирует, как использовать сигналы и слоты для реализации функциональности калькулятора и как использовать QGridLayout для размещения дочерних виджетов в сетке.

Пример виджета календаря

Пример виджета календаря демонстрирует использование QCalendarWidget.

Пример плагина эха

Этот пример показывает, как создать плагин Qt.

Пример макета потока

Показывает, как разместить виджеты для различных размеров окон.

Пример композиции изображений

Показывает, как работают режимы композиции в QPainter.

Пример меню

Пример меню демонстрирует, как можно использовать меню в приложении главного окна.

Пример простого модели дерева

Пример простого модели дерева показывает, как использовать иерархическую модель с стандартными классами представлений Qt.

Под-Аттак

Этот пример демонстрирует способность Qt объединять систему анимации и систему состояний для создания игры.

© The Qt Company Ltd
Licensed under the GNU Free Documentation License, Version 1.3.
https://doc.qt.io/qt-5.15/layout.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API