Управление макетом
Система макетов Qt предоставляет простой и мощный способ автоматического расположения дочерних виджетов внутри виджета, гарантируя, что они эффективно используют доступное пространство.
Введение
Qt включает набор классов управления макетом, которые используются для описания того, как виджеты размещаются в пользовательском интерфейсе приложения. Эти макеты автоматически позиционируют и изменяют размер виджетов при изменении доступного пространства, гарантируя их последовательное расположение и сохранение работоспособности пользовательского интерфейса в целом.
Все подклассы QWidget могут использовать макеты для управления своими дочерними элементами. Функция QWidget::setLayout() применяет макет к виджету. Когда макет таким образом назначается виджету, он берет на себя следующие задачи:
- Позиционирование дочерних виджетов
- Разумные значения по умолчанию для размеров окон
- Разумные минимальные размеры для окон
- Обработка изменения размеров
- Автоматические обновления при изменении содержимого:
- Размер шрифта, текст или другое содержимое дочерних виджетов
- Скрытие или отображение дочернего виджета
- Удаление дочерних виджетов
Классы управления макетом Qt
Классы управления макетом Qt были разработаны для написания кода на C++, позволяя указывать размеры в пикселях для простоты, поэтому их легко понять и использовать. Код, сгенерированный для форм, созданных с помощью Qt Designer, также использует классы макета. Qt Designer полезен при работе с дизайном формы, поскольку он позволяет избежать цикла компиляция-линковка-запуск, обычно используемого при разработке пользовательского интерфейса.
Выравнивает дочерние виджеты по горизонтали или вертикали |
|
Контейнер для организации групп кнопок |
|
Управляет формами входных виджетов и соответствующих меток |
|
Представляет якорь между двумя элементами в QGraphicsAnchorLayout |
|
Макет, где можно привязать виджеты друг к другу в представлении графики |
|
Размещает виджеты в сетке |
|
Рамка группового поля с заголовком |
|
Выравнивает виджеты по горизонтали |
|
Базовый класс менеджеров геометрии |
|
Абстрактный элемент, которым манипулирует QLayout |
|
Атрибут макета, описывающий политику изменения размера по горизонтали и вертикали |
|
Пустое место в макете |
|
Стопка виджетов, где только один виджет виден в данный момент |
|
Стопка виджетов, где только один виджет виден в данный момент |
|
Выравнивает виджеты по вертикали |
|
Элемент макета, представляющий виджет |
Горизонтальные, вертикальные, сетчатые и формулярные макеты
Самый простой способ обеспечить хороший макет виджетов — использовать встроенные менеджеры макетов: 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(window);
layout->addWidget(button1);
layout->addWidget(button2);
layout->addWidget(button3);
layout->addWidget(button4);
layout->addWidget(button5);
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(window);
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->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(window);
layout->addRow(button1, lineEdit1);
layout->addRow(button2, lineEdit2);
layout->addRow(button3, lineEdit3);
window->show(); Рекомендации по использованию макетов
При использовании макета вам не нужно передавать родительский элемент при создании дочерних виджетов. Макет автоматически переродитель виджеты (с помощью QWidget::setParent()), чтобы они являлись дочерними элементами виджета, на котором установлен макет.
Примечание: Виджеты в макете являются дочерними элементами виджета, на котором установлен макет, а не самого макета. Виджеты могут иметь только другие виджеты как родителей, а не макеты.
Вы можете вкладывать макеты, используя addLayout() в макете; внутренний макет затем становится дочерним элементом макета, в который он вставлен.
Добавление виджетов в макет
При добавлении виджетов в макет процесс работы следующий:
- Все виджеты изначально будут выделены количеством пространства в соответствии с их QWidget::sizePolicy() и QWidget::sizeHint().
- Если у любого из виджетов установлены коэффициенты растяжения со значением больше нуля, им выделяется пространство пропорционально коэффициенту растяжения (объясняется ниже).
- Если у любого из виджетов установлены коэффициенты растяжения, равные нулю, они получат больше пространства только если другие виджеты не потребуют его. Из них пространство сначала выделяется виджетам с политикой размера Expanding.
- Любые виджеты, которым выделено меньше пространства, чем их минимальный размер (или минимальная подсказка размера, если минимальный размер не указан), получают этот минимальный размер, который они требуют. (Виджеты не должны иметь минимального размера или подсказки минимального размера, в этом случае коэффициент растяжения является определяющим фактором.)
- Любые виджеты, которым выделено больше места, чем их максимальный размер, получают максимальное пространство, которое они требуют. (Виджеты не должны иметь максимального размера, в этом случае коэффициент растяжения является определяющим фактором.)
Коэффициенты растяжения
Виджеты обычно создаются без установленного коэффициента растяжения. При их размещении в макете виджетам выделяется доля пространства в соответствии с их QWidget::sizePolicy() или их минимальной подсказкой размера, что больше. Коэффициенты растяжения используются для изменения того, сколько пространства виджеты получают пропорционально друг другу.
Если у нас есть три виджета, расположенных с помощью QHBoxLayout без установленных коэффициентов растяжения, мы получим макет, похожий на этот:
Если мы применим коэффициенты растяжения к каждому виджету, они будут расположены пропорционально (но никогда не меньше их минимальной подсказки размера), например:
Пользовательские виджеты в макетах
При создании собственного класса виджета вы также должны сообщить о его свойствах макета. Если виджет использует один из макетов 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 обрабатывают форматированный текст, когда метка обтекает текст.
В некоторых случаях родительский макет переходит в режим QLayout::FreeResize, что означает, что он не будет адаптировать макет своего содержимого для размещения в окнах малого размера или даже предотвратит уменьшение окна до непригодного для использования размера. Это можно преодолеть, создав подкласс проблемных виджетов и реализовав подходящие функции sizeHint() и minimumSizeHint().
В некоторых случаях это имеет отношение к тому, когда макет добавляется в виджет. При установке виджета QDockWidget или QScrollArea (с помощью QDockWidget::setWidget() и QScrollArea::setWidget()) макет должен быть уже установлен на виджете. В противном случае виджет не будет отображаться.
Ручной макет
Если вы создаете уникальный специальный макет, вы также можете создать пользовательский виджет, как описано выше. Переопределите QWidget::resizeEvent(), чтобы рассчитать необходимое распределение размеров и вызвать setGeometry() для каждого дочернего элемента.
Виджет получит событие типа QEvent::LayoutRequest, когда макет должен быть пересчитан. Переопределите QWidget::event(), чтобы обработать события QEvent::LayoutRequest.
Как создать пользовательский менеджер макета
Альтернативой ручному макету является создание собственного менеджера макета путём создания подкласса QLayout. Примеры Макет по границам и Макет потока демонстрируют, как это сделать.
Здесь мы подробно представим пример. Класс 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(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:
QList<QLayoutItem *> m_items;
};
#endif Файл реализации (card.cpp)
//#include "card.h"
Сначала мы определяем count() для получения количества элементов в списке.
int CardLayout::count() const
{
// QList::size() returns the number of QLayoutItems in m_items
return m_items.size();
} Затем мы определяем две функции, которые перебирают макет: itemAt() и takeAt(). Эти функции используются внутренне системой макета для обработки удаления виджетов. Они также доступны программистам приложений.
itemAt() возвращает элемент по заданному индексу. takeAt() удаляет элемент по заданному индексу и возвращает его. В данном случае мы используем индекс списка как индекс макета. В других случаях, где у нас более сложная структура данных, нам может потребоваться больше усилий для определения линейного порядка элементов.
QLayoutItem *CardLayout::itemAt(int idx) const
{
// QList::value() performs index checking, and returns nullptr if we are
// outside the valid range
return m_items.value(idx);
}
QLayoutItem *CardLayout::takeAt(int idx)
{
// QList::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 примеры уже используют макеты, однако существуют несколько примеров, демонстрирующих различные макеты.
Введение в программирование графического интерфейса, показывающее, как собрать простое, но полностью функциональное приложение. |
|
Показ того, как разместить дочерние виджеты вдоль границы. |
|
Пример демонстрирует, как использовать сигналы и слоты для реализации функциональности виджета калькулятора и как использовать QGridLayout для размещения дочерних виджетов в сетке. |
|
Пример виджета календаря демонстрирует использование QCalendarWidget. |
|
Этот пример показывает, как создать плагин Qt. |
|
Показ того, как разместить виджеты для различных размеров окна. |
|
Демонстрация работы режимов композиции в QPainter. |
|
Пример меню демонстрирует, как меню могут использоваться в приложении главного окна. |
|
Пример простого дерева модели показывает, как использовать иерархическую модель с стандартными классами представлений Qt. |
© The Qt Company Ltd
Licensed under the GNU Free Documentation License, Version 1.3.
https://doc.qt.io/qt-6.2/layout.html