Spec-Zone.ru › Qt 5.13

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

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

Введение

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

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

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

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

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

QGraphicsAnchor

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

QGraphicsAnchorLayout

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

QBoxLayout

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

QHBoxLayout

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

QVBoxLayout

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

QFormLayout

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

QGridLayout

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

QLayout

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

QLayoutItem

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

QSpacerItem

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

QWidgetItem

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

QSizePolicy

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

QStackedLayout

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

QButtonGroup

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

QGroupBox

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

QStackedWidget

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

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

Самый простой способ задать хороший макет для ваших виджетов — использовать встроенные менеджеры макетов: 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).

END_OF_DOCUMENT_MARKER

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

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

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

В некоторых случаях родительский макет переводится в режим 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. В этом примере мы будем использовать QList.
  • addItem(), способ добавления элементов в макет.
  • setGeometry(), способ выполнения макета.
  • sizeHint(), предпочтительный размер макета.
  • itemAt(), способ итерации по макету.
  • takeAt(), способ удаления элементов из макета.

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

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

#ifndef CARD_H
#define CARD_H

#include <QtWidgets>
#include <QList>

class CardLayout : public QLayout
{
public:
    CardLayout(QWidget *parent, int dist): QLayout(parent, 0, dist) {}
    CardLayout(QLayout *parent, int dist): QLayout(parent, dist) {}
    CardLayout(int dist): QLayout(dist) {}
    ~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:
    QList<QLayoutItem*> list;
};
#endif

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

//#include "card.h"

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

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

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

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

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

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

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

void CardLayout::addItem(QLayoutItem *item)
{
    list.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 (list.size() == 0)
        return;

    int w = r.width() - (list.count() - 1) * spacing();
    int h = r.height() - (list.count() - 1) * spacing();
    int i = 0;
    while (i < list.size()) {
        QLayoutItem *o = list.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 = list.count();
    if (n > 0)
        s = QSize(100,70); //start with a nice default size
    int i = 0;
    while (i < n) {
        QLayoutItem *o = list.at(i);
        s = s.expandedTo(o->sizeHint());
        ++i;
    }
    return s + n*QSize(spacing(), spacing());
}

QSize CardLayout::minimumSize() const
{
    QSize s(0,0);
    int n = list.count();
    int i = 0;
    while (i < n) {
        QLayoutItem *o = list.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 примеры уже используют макеты, однако существуют несколько примеров, демонстрирующих различные макеты.

Учебник по адресной книге

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

Пример макета Border Layout

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

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

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

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

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

Пример плагина Echo

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

Пример макета Flow Layout

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

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

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

Пример меню

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

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

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

Под-Attaq

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

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

Spec-Zone.ru

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