Spec-Zone.ru › Qt 5.9

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

Система макетов 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 выстраивает виджеты в двух столбцах в стиле полей с соответствующими метками.

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

Следующий код создаёт 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() вызовут только один перерасчёт макета.

Если предпочтительная высота вашего виджета зависит от его фактической ширины (например, метка с автоматическим переносом слов), установите флаг «высота-по-ширине» в политике размера виджета и переопределите 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. В этом примере используется 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);
    QSize sizeHint() const;
    QSize minimumSize() const;
    int count() const;
    QLayoutItem *itemAt(int) const;
    QLayoutItem *takeAt(int);
    void setGeometry(const QRect &rect);

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 examples уже используют макеты, однако существуют несколько примеров, демонстрирующих различные макеты.

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

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

Пример Border Layout

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

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

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

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

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

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

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

Пример Flow Layout

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

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

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

Пример меню

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

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

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

Sub-Attaq

В этом примере показано, как Qt может сочетать механизм анимации и механизм состояния для создания игры.

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

Spec-Zone.ru

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