---
title: "Контейнеры и компоновка Box"
description: "Fyne контейнеры и компоновка Box: NewVBox и NewHBox по MinSize, вложенные боксы, layout.NewSpacer для выравнивания, Padded и Center, динамика Add и Remove."
course: "Fyne: десктопные и мобильные GUI на Go"
url: "https://golangmaster.ru/courses/fyne/lessons/04-box/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

# Контейнеры и компоновка Box

*Курс «Fyne: десктопные и мобильные GUI на Go», урок 4 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/fyne/lessons/04-box/*

Виджеты сами по себе не решают, где стоять: расстановкой ведают fyne контейнеры. В этом уроке — рабочая лошадка компоновки Box: container.NewVBox и NewHBox, поведение vbox hbox по минимальным размерам, вложенные боксы для скелета приложения, layout.NewSpacer для прижатия и центрирования, Padded и Center, отступы темы и живое содержимое через Add и Remove.

К третьему уроку у тебя есть шесть виджетов — и одна проблема: они не умеют раскладывать себя в окне. Позиции и размеры назначает не виджет, а контейнер с компоновкой; сам виджет лишь сообщает свой минимальный размер, MinSize, о котором договорились ещё в первом уроке. Здесь мы разберём Box — самый ходовой тип fyne контейнеров: вообще fyne контейнеры ничего не рисуют, только расставляют; вертикальный и горизонтальный бокс, из которых собирается девять из десяти экранов внутренних утилит. В hello world и примерах прошлого урока мы писали container.NewVBox не вникая — теперь вникнем: почему элементы растягиваются по ширине, почему кнопка не расплющивается в высоту, куда девается свободное место и как его забирает спейсер. По-английски эти вопросы ищут как fyne vbox и fyne layout — и все ответы живут в пакете container и пакете layout, паре модулей, которые стоит запомнить целиком. Плюс соберём типовой скелет приложения из вложенных боксов и научимся менять содержимое на лету, без пересоздания окна.

### Модель: контейнер держит детей, layout их расставляет

Разложим понятия. fyne.Container — это тоже CanvasObject, но его работа не рисоваться, а держать других: у контейнера есть поле Objects со списком детей и поле Layout с алгоритмом расстановки. Компоновка — это интерфейс fyne.Layout с двумя методами: MinSize возвращает минимальный размер контейнера по его детям, а Layout расставляет детей внутри выданного размера. Отсюда конвейер: контейнер спрашивает у каждого ребёнка MinSize, передаёт их layout, а тот назначает каждому позицию и размер по своим правилам. Виджеты по этому конвейеру ходят пассивно — приказ получили, встали. Универсальная фабрика одна: container.New(layout, объекты...) — подставляешь компоновку и получаешь контейнер. А NewVBox и NewHBox — сокращения: внутри у них тот же container.New с готовыми компоновками layout.NewVBoxLayout() и layout.NewHBoxLayout(). Когда важнее читаемость — пиши сокращения; когда компоновку нужно подменять на лету — собирай через container.New и меняй поле Layout. И помни из второго урока: SetContent ставит корневой контейнер, поэтому почти каждый экран начинается с одного верхнего бокса.

### NewVBox: колонка

```go
package main

import (
        "fyne.io/fyne/v2/app"
        "fyne.io/fyne/v2/container"
        "fyne.io/fyne/v2/widget"
)

func main() {
        myApp := app.New()
        window := myApp.NewWindow("Профиль")

        name := widget.NewLabel("Имя: Аня")
        role := widget.NewLabel("Роль: администратор")
        edit := widget.NewButton("Изменить", func() {})

        window.SetContent(container.NewVBox(
                name,
                role,
                edit,
        ))
        window.ShowAndRun()
}
```

NewVBox принимает любое число объектов и складывает их в колонку: каждый элемент получает ширину самого широкого соседа, а высоту — свою минимальную. Кнопка «Изменить» станет такой же широкой, как самая длинная метка: это не баг, а правило VBox.

### Как VBox раскладывает: ширина по максимуму, высота по минимуму

Документация формулирует поведение вертикального бокса одной фразой: каждый элемент получает высоту по минимуму, а ширины всех элементов равны — берётся наибольшая из минимальных ширин. Разберём на следствия. Первое: колонка выглядит выровненной по краям, как таблица из одной графы — узкая кнопка распухает до ширины соседа с длинным текстом. Если хочется узкую кнопку в широкой колонке — заворачивай её в горизонтальный бокс или центр, об этом ниже. Второе: вертикального растягивания нет. Каждый ребёнок занимает свою минимальную высоту, и если окно выше суммы минимумов, лишнее место остаётся снизу пустым — элементы прижаты к верху. Забрать пустоту умеет спейсер, до него дойдём. Третье: при изменении текста метки её MinSize пересчитывается, и ширина всей колонки может прыгнуть — сеттер SetText из прошлого урока триггерит перерасчёт автоматически. И четвёртое: окно не станет меньше контента — правило минимума сработало и для Resize во втором уроке, здесь тот же механизм, просто root-контейнер сообщает минимум всему окну.

### NewHBox: строка

```go
toolbar := container.NewHBox(
        widget.NewButton("Сохранить", func() {}),
        widget.NewButton("Отмена", func() {}),
        widget.NewButton("Справка", func() {}),
)
// toolbar встаёт первой строкой в колонку:
window.SetContent(container.NewVBox(
        toolbar,
        widget.NewLabel("Рабочая область"),
))
```

NewHBox раскладывает детей в строку: каждый получает свою минимальную ширину, а высоты выравниваются по самому высокому элементу. Кнопки в строке одинаковой высоты — правило HBox; ширина строки — сумма минимальных ширин детей.

### Как HBox раскладывает: высота по максимуму, ширина по минимуму

Горизонтальный бокс зеркален вертикальному: элементы стоят в один ряд, каждый — на своей минимальной ширине, а высоты всех равны наибольшей из минимальных высот. Кнопки в тулбаре получаются одинаковой высоты, даже если у одной текст длиннее — визуально аккуратно. Свободное место по горизонтали, если строка уже окна, остаётся справа пустым; элементы прижаты к левому краю. Отсюда важная честность про ожидания: HBox не растягивает кнопки на всю строку и не распределяет место поровну — если нужен «резиновый» тулбар, это задача спейсера или компоновок из следующих уроков. Сумма минимумов по горизонтали и по вертикали работает и вложенно: строка внутри колонки занимает в ней ширину всей колонки, но сами кнопки останутся узкими и прижмутся влево. Кстати, в документации встречается и виджетный вариант бокса — widget.NewHBox из ранних версий; актуальный путь — именно контейнеры пакета container, к виджетам бокс не относится: он ничего не рисует, только расставляет.

**Вопрос:** В NewVBox лежат: метка «Ок», метка «Это очень длинная строка статуса» и кнопка «Старт». Какой ширины окажется кнопка?

- Своей минимальной ширины — VBox не меняет ширину детей
- ✅ Ровно ширине самой длинной метки — ширины всех элементов равны
- Половине ширины окна — VBox делит ширину между детьми
- Нулевой — кнопки в VBox не показываются

Вертикальный бокс уравнивает ширины: каждый элемент получает наибольшую из минимальных ширин соседей. Кнопка растянется до ширины длинной метки. Высота при этом останется кнопкиной — растягивания по вертикали в VBox нет.

### Что будет внизу окна?

```go
window.Resize(fyne.NewSize(500, 600))
window.SetContent(container.NewVBox(
        widget.NewLabel("короткая"),
        widget.NewLabel("гораздо более длинная строка"),
        widget.NewButton("Дно", func() {}),
))
```

**Что выведет программа?**

- Три элемента растянутся на всю высоту окна, поделив её поровну
- ✅ Элементы прижмутся к верху, каждая на своей минимальной высоте, снизу останется пустота
- Кнопка уедет к нижнему краю окна, метки останутся сверху
- Окно сожмётся до высоты трёх элементов — Resize не сработает

VBox ставит детей друг под другом, каждый — на минимальной высоте, и прижимает их к верху контейнера. Лишняя высота окна остаётся снизу незанятой. Чтобы прижать содержимое к низу или разложить по краям, нужен layout.NewSpacer — он появится ниже.

### Вложенные боксы: скелет настоящего приложения

Сила Box-подхода — вложенность: бокс — обычный CanvasObject, поэтому встаёт ребёнком в другой бокс без церемоний. Типовой скелет выглядит так: корневой VBox держит сверху HBox-панель инструментов, под ней строку заголовка, ниже — рабочую область. Рабочая область, в свою очередь, может быть VBox'ом из полей, а поля с кнопками — HBox'ами. Читается такой код сверху вниз как схема экрана: строка — полоса интерфейса, вложение — уровень. Полезный приём: одноэлементный HBox сохраняет ребёнку его собственную ширину — узкая кнопка под широкой меткой перестанет распухать, потому что в строке ширина ребёнка всегда его собственный минимум. Не увлекайся глубиной: три-четыре уровня вложенности читаются легко, дальше начинает путаться сам автор. Для более сложных зон — сеток плиток и настоящих форм — есть GridLayout и Form, их разберёт следующий урок; Box остаётся каркасом, к которому всё это прикрепляется.

### Заметки: HBox-тулбар внутри VBox

```go
package main

import (
        "fyne.io/fyne/v2/app"
        "fyne.io/fyne/v2/container"
        "fyne.io/fyne/v2/widget"
)

func main() {
        myApp := app.New()
        window := myApp.NewWindow("Заметки")

        input := widget.NewMultiLineEntry()
        input.SetPlaceHolder("Текст заметки...")

        toolbar := container.NewHBox(
                widget.NewButton("Новая", func() { input.SetText("") }),
                widget.NewButton("Считать символы", func() {}),
        )

        window.SetContent(container.NewVBox(
                toolbar,                     // строка кнопок
                widget.NewLabel("Мои заметки"), // заголовок
                input,                       // рабочая область
        ))
        window.ShowAndRun()
}
```

Классический скелет: корневой VBox, первым ребёнком — HBox-тулбар, дальше заголовок и многострочное поле. Каждая зона правится отдельно: кнопки добавляются в toolbar, не трогая колонку, — вложенность изолирует изменения.

### Spacer: резиновый зазор

layout.NewSpacer() возвращает специальный объект-распорку, чья работа — расширяться и занимать всё свободное место бокса. Документация описывает его ровно для box-компоновок — в других раскладках спейсер смысла не имеет. Приёмы стоит выучить как таблицу умножения. Спейсер перед кнопкой в HBox прижимает кнопку вправо: всё место слева съедает распорка. Два спейсера по бокам от элемента центрируют его в строке. Спейсер первым элементом VBox прижимает всё содержимое колонки к низу — редкий, но полезный приём для панелей состояния. Спейсер между блоками VBox растягивается вертикально и раздвигает их по краям окна. Учти эффект на минимум: спейсер не имеет минимального размера, поэтому сам по себе окно не распирает — работает только когда есть излишек. И не путай с отступами: спейсер — про свободное место и выравнивание, фиксированные зазоры между элементами задаёт тема своим паддингом; для произвольных зазоров есть и другие приёмы, но о них — в разговоре о темах.

### Спейсеры: вправо и по центру

```go
save := widget.NewButton("Сохранить", func() {})

// кнопка прижата к правому краю строки
right := container.NewHBox(
        layout.NewSpacer(),
        save,
)

// кнопка в центре строки
center := container.NewHBox(
        layout.NewSpacer(),
        widget.NewButton("По центру", func() {}),
        layout.NewSpacer(),
)
```

Спейсер из пакета layout встаёт обычным ребёнком бокса и отдаёт себе всё свободное место. Один спейсер слева — прижатие вправо, два по бокам — центрирование. В VBox спейсер работает вертикально: первым элементом прижимает содержимое к низу.

**Вопрос:** Как прижать всё содержимое колонки VBox к нижнему краю окна?

- Вызвать у каждого ребёнка Move с нижней позицией
- ✅ Поставить layout.NewSpacer() первым элементом колонки
- Поставить layout.NewSpacer() последним элементом колонки
- Никак: VBox всегда центрирует содержимое

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

### Padded и Center: отступы темы и центральное размещение

Два коротких контейнера из той же семьи решают бытовые вопросы оформления. container.NewPadded(obj) оборачивает содержимое стандартным отступом темы со всех сторон — тот самый padding, равный четырём единицам, о котором говорилось в первом уроке. Он полезен, когда ты собираешь панель вручную и хочешь, чтобы она не прилипала к краям. Между прочим, окно уже делает это за тебя: SetContent оборачивает содержимое во внутренний отступ, и SetPadded(false) из второго урока как раз выключает это поведение — теперь ты знаешь, что именно выключаешь. container.NewCenter(obj) размещает элемент в центре доступного пространства на его минимальном размере — идеален для заглушек «Нет данных», карточек и стартовых экранов. Знай и соседей, чтобы не изобретать: NewMax растягивает единственного ребёнка на всё место, NewStack кладёт детей друг на друга стопкой, а NewBorder выделяет зоны сверху, снизу и по бокам, отдавая середину остатку — Border мы упоминали в первом уроке и вернёмся к нему, когда дойдём до панелей и форм.

### Заглушка в центре окна

```go
card := container.NewVBox(
        widget.NewLabelWithStyle("Пока пусто",
                fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
        widget.NewLabel("Выберите файл для начала работы"),
)

window.SetContent(container.NewCenter(card))
```

NewCenter держит карточку в середине окна на её минимальном размере. Внутри — обычный VBox из двух меток: выравнивание текста по центру делает NewLabelWithStyle из третьего урока. При ресайзе окна карточка остаётся по центру.

### Куда прижмутся метки?

```go
window.Resize(fyne.NewSize(400, 500))
window.SetContent(container.NewVBox(
        layout.NewSpacer(),
        widget.NewLabel("Верхняя строка"),
        widget.NewButton("Нижняя кнопка", func() {}),
))
```

**Что выведет программа?**

- Метка и кнопка останутся у верхнего края, спейсер ничего не меняет
- ✅ Элементы прижмутся к нижнему краю окна: спейсер забрал всю свободную высоту сверху
- Элементы растянутся на середину окна, пустоты останется поровну сверху и снизу
- Программа не скомпилируется: спейсер нельзя вкладывать в VBox

Спейсер первым элементом вертикального бокса расширяется вертикально и занимает весь излишек над содержимым — содержимое прижимается к низу. Спейсер рассчитан на box-компоновки: он работает и в HBox, и в VBox.

### Живое содержимое: Objects, Add, Remove и Refresh

Компоновка — не приговор: содержимое контейнера можно менять после запуска. У fyne.Container есть методы Add (с версии 1.4) и Remove — добавить ребёнка в конец или убрать конкретный объект; RemoveAll (с версии 2.2) чистит контейнер разом. Метод Remove даже документирован с оговоркой: для массового удаления он не предназначен — в цикле вызывай RemoveAll. Дети лежат в публичном поле Objects, и прямая правка среза тоже допустима — но помни правило сеттеров из третьего урока в применении к контейнерам: изменил содержимое — сообщи об этом. После прямой правки Objects нужен Refresh() у контейнера: он пересчитает минимумы и заставит layout расставить детей заново. Add и Remove сами перерисовку запускают, поэтому предпочтительны. Практический сценарий: список строк, который растёт по кнопке «Добавить» — каждая строка это HBox с меткой и кнопкой удаления, а корень держит их в VBox. Когда строк станет сотни, смени VBox на List из десятого урока: виртуализация для того и придумана.

### Кнопка добавляет и убирает строки

```go
list := container.NewVBox()

addLine := widget.NewButton("Добавить строку", func() {
        label := widget.NewLabel("Строка №" + strconv.Itoa(len(list.Objects)))
        row := container.NewHBox(label,
                widget.NewButton("Удалить", func() {
                        list.Remove(row)
                }))
        list.Add(row)
})

window.SetContent(container.NewVBox(addLine, list))
```

list.Add дописывает HBox-строку в конец колонки и сам вызывает перерисовку; list.Remove(row) убирает конкретную строку. Поле Objects доступно для чтения — его длина покажет, сколько строк сейчас в списке. Для честного счётчика в реальном коде заведи переменную-счётчик.

**Вопрос:** Сделали box.Objects = append(box.Objects, widget.NewLabel("новая")), но экран не изменился. Что забыли?

- ✅ Вызвать box.Refresh() — прямая правка Objects перерисовку не запускает
- Вызвать window.Show() заново — контейнер пересобирается только при показе
- Ничего: append в срез автоматически перерисовывает контейнер
- Пересоздать контейнер через container.NewVBox и передать в SetContent

Objects — обычное публичное поле, его правка сама по себе ничего не триггерит. Нужен box.Refresh(), чтобы контейнер пересчитал размеры и layout расставил детей. Методы Add и Remove запускают перерисовку сами — потому они и предпочтительнее ручной правки среза.

### Каталог урока: конструкторы и компоновки

| Конструктор | Компоновка внутри | Поведение |
| --- | --- | --- |
| container.NewVBox(...) | layout.NewVBoxLayout() | Колонка: ширины равны максимуму, высоты по минимуму |
| container.NewHBox(...) | layout.NewHBoxLayout() | Строка: ширины по минимуму, высоты равны максимуму |
| container.NewPadded(...) | padded-компоновка | Содержимое со стандартным отступом темы (4) |
| container.NewCenter(...) | layout.NewCenterLayout() | Элемент в центре, на минимальном размере |
| container.NewMax(...) | layout.NewMaxLayout() | Единственный ребёнок растянут на всё место |
| container.NewStack(...) | layout.NewStackLayout() | Дети стопкой друг на друге, верхний видимый |
| container.NewBorder(top, bottom, left, right, ...) | layout.NewBorderLayout(...) | Зоны по краям, середина — остатку |
| container.New(layout, ...) | любая fyne.Layout | Универсальная фабрика: компоновка подменяема на лету |

### Собери каркас экрана

Восстанови четыре пропуска: строка кнопок, корневая колонка, распорка и отступы.

```go
toolbar := container.___(          // кнопки в строку
        widget.NewButton("Открыть", func() {}),
        widget.NewButton("Сохранить", func() {}),
)
gap := layout.___()                // резиновый зазор
root := container.___(             // колонка
        toolbar,
        gap,
        widget.NewLabel("Статус"),
)
window.SetContent(container.___(root)) // отступы темы вокруг всего
```

Подсказка: Строка, распорка, колонка и обёртка со стандартным отступом — все из пакета container, кроме распорки: она из layout.

Правильные ответы в пропусках: `NewHBox`, `NewSpacer`, `NewVBox`, `NewPadded`

### Что происходит при ресайзе окна

Собери конвейер перекомпоновки: от движения края окна до перерисованного экрана.

Правильный порядок строк:

1. `пользователь тянет край окна`
2. `драйвер меняет размер окна и вызывает Resize у корневого контейнера`
3. `контейнер опрашивает MinSize всех детей по цепочке вложенности`
4. `layout расставляет детей по своим правилам в новом размере`
5. `каждому ребёнку назначены новые позиция и размер`
6. `канвас перерисовывается — экран обновлён`

> **Компоновку видно только на живом экране.** Раскладка — визуальная дисциплина: в браузерной песочнице курса окна нет, поэтому упражняйся локально. Собери примеры урока, потяни окно за края и понаблюдай, как ведут себя ширины и спейсеры, — пять минут с живым окном дают больше, чем перечитывание кода.

### Что дальше

Каркас из боксов держит любые зоны, но таблицы плитками и формы с подписями Box'ом делать тесновато. Пятый урок 05-grid-form добавит Grid — сетку равных ячеек и GridWrap для плиток любого числа строк — и Form, который раскладывает пары «подпись — поле» и подключает валидацию из восемнадцатого урока. Виджеты для этих сеток бери из [третьего урока про базовые виджеты](/courses/fyne/lessons/03-vidzhety/) — Label, Button и Entry сейчас лягут в форму. Напомнить каркас запуска можно во [втором уроке про первое окно](/courses/fyne/lessons/02-pervoe-okno/), а словарь CanvasObject и MinSize — в [дебютном уроке](/courses/fyne/lessons/01-znakomstvo/). Полный план курса из двадцати пяти уроков живёт на [странице курса Fyne](/courses/fyne/). Домашнее задание — собрать настоящий каркас приложения из трёх зон; он ещё пригодится: на его базе мы будем делать диалоги, вкладки и итоговый менеджер задач.

### Термины урока

- **fyne.Container** — CanvasObject, который держит детей: поле Objects со списком и поле Layout с алгоритмом. Создаётся container.New(layout, объекты...) или сокращениями NewVBox/NewHBox.
- **VBox** — container.NewVBox — колонка. Высоты детей минимальные, ширины всех равны наибольшей из минимальных ширин; излишек высоты остаётся снизу пустым.
- **HBox** — container.NewHBox — строка. Ширины детей минимальные, высоты всех равны наибольшей из минимальных высот; излишек ширины остаётся справа.
- **layout.NewSpacer()** — Распорка для box-компоновок: забирает всё свободное место. В HBox прижимает вправо/центрирует, в VBox первым элементом прижимает содержимое к низу.
- **NewPadded и NewCenter** — NewPadded оборачивает содержимое стандартным отступом темы (padding = 4); NewCenter ставит элемент в центр на его MinSize. Окно само добавляет отступ — SetPadded(false) его выключает.
- **Динамика контейнера** — Add и Remove меняют детей и сами перерисовывают (RemoveAll чистит разом, с 2.2). Прямая правка Objects требует ручного Refresh() — как сеттеры у виджетов.

## Коротко: что запомнить

- Расстановкой ведает контейнер: fyne.Container держит Objects и Layout, а виджеты пассивно получают позицию и размер по своим MinSize.
- NewVBox — колонка: ширины детей равны максимуму из минимальных, высоты по минимуму, излишек внизу пуст; NewHBox — зеркальная строка.
- Вложенные боксы дают скелет приложения: HBox-тулбар первой строкой корневого VBox, ниже заголовок и рабочая область.
- layout.NewSpacer() забирает свободное место: прижатие вправо, центрирование двумя спейсерами, прижатие колонки к низу спейсером первым элементом.
- container.NewPadded добавляет отступ темы (4), NewCenter держит элемент в центре на MinSize; у окна тот же отступ выключает SetPadded(false).
- Содержимое меняется на лету: Add/Remove перерисовывают сами, RemoveAll чистит разом, прямая правка Objects требует box.Refresh().

## Домашнее задание

Собери каркас приложения «задачник на минималках»: корневой VBox, сверху HBox-тулбар с кнопками «Новая задача» (HighImportance из третьего урока) и «Очистить», ниже заголовок, ниже — колонка-список задач, которую наполняет кнопка «Новая задача»: каждая задача — HBox из метки и кнопки удаления, работающей через list.Remove. Прижми панель статуса к низу окна спейсером и оберни весь корень в container.NewPadded. Потяни окно за края и запиши, как меняется поведение: что растягивается, что остаётся узким, куда девается пустота. Затем замени NewPadded на SetContent без обёртки и сравни зазоры у краёв.

## Вопросы и ответы по уроку

### Почему кнопка в NewVBox растянулась на всю ширину колонки?

Так устроен вертикальный бокс: ширины всех элементов равны наибольшей из минимальных ширин. Узкую кнопку можно получить, завернув её в одноэлементный container.NewHBox или container.NewCenter, — в строке ширина ребёнка остаётся его собственной.

### Спейсер не работает: элементы не сдвинулись. Что проверить?

layout.NewSpacer() рассчитан на box-компоновки — в Grid, Border и других раскладках он ведёт себя как обычный объект без минимума. Проверь также, что свободное место вообще есть: если окно ровно по сумме минимумов, спейсеру нечего забирать.

### Чем container.NewVBox отличается от container.New с layout.NewVBoxLayout()?

Ничем по поведению: NewVBox — сокращение, которое создаёт контейнер с той же компоновкой. Вариант через container.New выбирают, когда layout нужно подменять на лету — поле Layout контейнера можно переприсвоить.
