Управление макетом
Система макетов Qt предоставляет простой и мощный способ автоматического размещения дочерних виджетов внутри виджета, гарантируя, что они эффективно используют доступное пространство.
Введение
Qt включает набор классов управления макетом, которые используются для описания того, как виджеты размещаются в пользовательском интерфейсе приложения. Эти макеты автоматически позиционируют и изменяют размер виджетов, когда изменяется доступное для них пространство, гарантируя их последовательное расположение и сохранение работоспособности пользовательского интерфейса в целом.
Все подклассы QWidget могут использовать макеты для управления своими дочерними элементами. Функция QWidget::setLayout() применяет макет к виджету. Когда макет устанавливается на виджет таким образом, он берет на себя следующие задачи:
- Позиционирование дочерних виджетов.
- Разумные значения по умолчанию для размеров окон.
- Разумные минимальные размеры для окон.
- Обработка изменения размеров.
- Автоматические обновления при изменении содержимого:
- Размер шрифта, текст или другое содержимое дочерних виджетов.
- Скрытие или отображение дочернего виджета.
- Удаление дочерних виджетов.
Классы управления макетом Qt
Классы управления макетом Qt были разработаны для ручного кода C++, позволяя указывать измерения в пикселях для простоты, поэтому они просты в понимании и использовании. Сгенерированный код для форм, созданных с помощью Qt Designer, также использует классы управления макетом. Qt Designer полезно использовать при экспериментах с дизайном формы, так как он исключает цикл компиляции, компоновки и запуска, обычно используемый при разработке пользовательского интерфейса.
Представляет якорь между двумя элементами в QGraphicsAnchorLayout |
|
Макет, в котором можно привязать виджеты друг к другу в Graphics View |
|
Выравнивает дочерние виджеты по горизонтали или вертикали |
|
Выравнивает виджеты по горизонтали |
|
Выравнивает виджеты по вертикали |
|
Управляет формами входных виджетов и их связанными метками |
|
Размещает виджеты в сетке |
|
Базовый класс менеджеров геометрии |
|
Абстрактный элемент, который обрабатывает QLayout |
|
Пустое место в макете |
|
Элемент макета, представляющий виджет |
|
Атрибут макета, описывающий политику изменения размера по горизонтали и вертикали |
|
Стопка виджетов, где только один виджет виден в данный момент |
|
Контейнер для организации групп виджетов кнопок |
|
Рамка группы с заголовком |
|
Стопка виджетов, где только один виджет виден в данный момент |
Горизонтальные, вертикальные, сетчатые и формулярные макеты
Самый простой способ обеспечить хороший макет для ваших виджетов — использовать встроенные менеджеры макетов: 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() на макете; внутренний макет затем становится дочерним элементом макета, в который он вставлен.
Добавление виджетов в макет
Когда вы добавляете виджеты в макет, процесс макета работает следующим образом:
- Все виджеты первоначально выделяются количеством пространства в соответствии с QWidget::sizePolicy() и QWidget::sizeHint().
- Если у каких-либо виджетов установлены коэффициенты растяжения с значением больше нуля, то им выделяется пространство пропорционально их коэффициенту растяжения (объясняется ниже).
- Если коэффициенты растяжения каких-либо виджетов установлены в ноль, они получат дополнительное пространство только если другие виджеты не потребуют его. Из таких виджетов пространство выделяется виджетам с политикой размера Expanding в первую очередь.
- Любые виджеты, которым выделено меньше пространства, чем их минимальный размер (или минимальная размерная подсказка, если минимальный размер не задан), получают этот требуемый минимальный размер.
- Любые виджеты, которым выделено больше пространства, чем их максимальный размер, получают максимальное требуемое пространство.
Коэффициенты растяжения
Виджеты обычно создаются без установленных коэффициентов растяжения. Когда они размещаются в макете, виджетам выделяется доля пространства в соответствии с их QWidget::sizePolicy() или их минимальной размерной подсказкой, что больше. Коэффициенты растяжения используются для изменения того, сколько пространства виджетам выделяется пропорционально друг другу.
Если у нас есть три виджета, размещённые с помощью QHBoxLayout без установленных коэффициентов растяжения, мы получим макет, подобный этому:
Если мы применим коэффициенты растяжения к каждому виджету, они будут размещены пропорционально (но никогда не меньше их минимальной размерной подсказки), например:
Пользовательские виджеты в макетах
При создании собственного класса виджета вы также должны указать его свойства макета. Если виджет использует один из макетов Qt, это уже учтено. Если виджет не имеет дочерних виджетов или использует ручной макет, вы можете изменить поведение виджета, используя любой или все из следующих механизмов:
- Переопределите QWidget::sizeHint(), чтобы вернуть предпочтительный размер виджета.
- Переопределите QWidget::minimumSizeHint(), чтобы вернуть наименьший размер виджета.
- Вызовите QWidget::setSizePolicy(), чтобы указать требования к пространству виджета.
Вызовите QWidget::updateGeometry() всякий раз, когда изменяются размерная подсказка, минимальная размерная подсказка или политика размера. Это вызовет перерасчёт макета. Несколько последовательных вызовов QWidget::updateGeometry() вызовут только один перерасчёт макета.
Если предпочтительная высота вашего виджета зависит от его фактической ширины (например, метки с автоматическим переходом на новую строку), установите флаг высота по ширине в политике размера виджета и переопределите QWidget::heightForWidth().
Даже если вы реализуете QWidget::heightForWidth(), всё равно рекомендуется предоставить разумную подсказку размера.
Дополнительные рекомендации при реализации этих функций см. в статье 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 примеров уже используют макеты, однако существуют несколько примеров, демонстрирующих различные макеты.
© The Qt Company Ltd
Licensed under the GNU Free Documentation License, Version 1.3.
https://doc.qt.io/archives/qt-5.6/layout.html