Управление макетом
Система макетов 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(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() вызовут только один перерасчет макета.
Если предпочтительная высота вашего виджета зависит от его фактической ширины (например, метка с автоматическим переходом на следующую строку), установите флаг высоты по ширине в политике размера виджета и переопределите 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.0/layout.html