Содержание урока · 12 разделов
- Модель: контейнер держит детей, layout их расставляет
- Как VBox раскладывает: ширина по максимуму, высота по минимуму
- Как HBox раскладывает: высота по максимуму, ширина по минимуму
- Вложенные боксы: скелет настоящего приложения
- Spacer: резиновый зазор
- Padded и Center: отступы темы и центральное размещение
- Живое содержимое: Objects, Add, Remove и Refresh
- Каталог урока: конструкторы и компоновки
- Что дальше
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
К третьему уроку у тебя есть шесть виджетов — и одна проблема: они не умеют раскладывать себя в окне. Позиции и размеры назначает не виджет, а контейнер с компоновкой; сам виджет лишь сообщает свой минимальный размер, 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: колонка
GOpackage 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: строка
GOtoolbar := 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 лежат: метка «Ок», метка «Это очень длинная строка статуса» и кнопка «Старт». Какой ширины окажется кнопка?
Что будет внизу окна?
Прочитайте код и выберите, что он выведет:
window.Resize(fyne.NewSize(500, 600))
window.SetContent(container.NewVBox(
widget.NewLabel("короткая"),
widget.NewLabel("гораздо более длинная строка"),
widget.NewButton("Дно", func() {}),
))Вложенные боксы: скелет настоящего приложения#
Сила Box-подхода — вложенность: бокс — обычный CanvasObject, поэтому встаёт ребёнком в другой бокс без церемоний. Типовой скелет выглядит так: корневой VBox держит сверху HBox-панель инструментов, под ней строку заголовка, ниже — рабочую область. Рабочая область, в свою очередь, может быть VBox'ом из полей, а поля с кнопками — HBox'ами. Читается такой код сверху вниз как схема экрана: строка — полоса интерфейса, вложение — уровень. Полезный приём: одноэлементный HBox сохраняет ребёнку его собственную ширину — узкая кнопка под широкой меткой перестанет распухать, потому что в строке ширина ребёнка всегда его собственный минимум. Не увлекайся глубиной: три-четыре уровня вложенности читаются легко, дальше начинает путаться сам автор. Для более сложных зон — сеток плиток и настоящих форм — есть GridLayout и Form, их разберёт следующий урок; Box остаётся каркасом, к которому всё это прикрепляется.
Заметки: HBox-тулбар внутри VBox
GOpackage 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 растягивается вертикально и раздвигает их по краям окна. Учти эффект на минимум: спейсер не имеет минимального размера, поэтому сам по себе окно не распирает — работает только когда есть излишек. И не путай с отступами: спейсер — про свободное место и выравнивание, фиксированные зазоры между элементами задаёт тема своим паддингом; для произвольных зазоров есть и другие приёмы, но о них — в разговоре о темах.
Спейсеры: вправо и по центру
GOsave := widget.NewButton("Сохранить", func() {})
// кнопка прижата к правому краю строки
right := container.NewHBox(
layout.NewSpacer(),
save,
)
// кнопка в центре строки
center := container.NewHBox(
layout.NewSpacer(),
widget.NewButton("По центру", func() {}),
layout.NewSpacer(),
)Спейсер из пакета layout встаёт обычным ребёнком бокса и отдаёт себе всё свободное место. Один спейсер слева — прижатие вправо, два по бокам — центрирование. В VBox спейсер работает вертикально: первым элементом прижимает содержимое к низу.
Как прижать всё содержимое колонки VBox к нижнему краю окна?
Padded и Center: отступы темы и центральное размещение#
Два коротких контейнера из той же семьи решают бытовые вопросы оформления. container.NewPadded(obj) оборачивает содержимое стандартным отступом темы со всех сторон — тот самый padding, равный четырём единицам, о котором говорилось в первом уроке. Он полезен, когда ты собираешь панель вручную и хочешь, чтобы она не прилипала к краям. Между прочим, окно уже делает это за тебя: SetContent оборачивает содержимое во внутренний отступ, и SetPadded(false) из второго урока как раз выключает это поведение — теперь ты знаешь, что именно выключаешь. container.NewCenter(obj) размещает элемент в центре доступного пространства на его минимальном размере — идеален для заглушек «Нет данных», карточек и стартовых экранов. Знай и соседей, чтобы не изобретать: NewMax растягивает единственного ребёнка на всё место, NewStack кладёт детей друг на друга стопкой, а NewBorder выделяет зоны сверху, снизу и по бокам, отдавая середину остатку — Border мы упоминали в первом уроке и вернёмся к нему, когда дойдём до панелей и форм.
Заглушка в центре окна
GOcard := container.NewVBox(
widget.NewLabelWithStyle("Пока пусто",
fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
widget.NewLabel("Выберите файл для начала работы"),
)
window.SetContent(container.NewCenter(card))NewCenter держит карточку в середине окна на её минимальном размере. Внутри — обычный VBox из двух меток: выравнивание текста по центру делает NewLabelWithStyle из третьего урока. При ресайзе окна карточка остаётся по центру.
Куда прижмутся метки?
Прочитайте код и выберите, что он выведет:
window.Resize(fyne.NewSize(400, 500))
window.SetContent(container.NewVBox(
layout.NewSpacer(),
widget.NewLabel("Верхняя строка"),
widget.NewButton("Нижняя кнопка", func() {}),
))Живое содержимое: 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 из десятого урока: виртуализация для того и придумана.
Кнопка добавляет и убирает строки
GOlist := 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("новая")), но экран не изменился. Что забыли?
Каталог урока: конструкторы и компоновки#
| Конструктор | Компоновка внутри | Поведение |
|---|---|---|
| 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 | Универсальная фабрика: компоновка подменяема на лету |
Собери каркас экрана
Восстанови четыре пропуска: строка кнопок, корневая колонка, распорка и отступы.
Что происходит при ресайзе окна
Собери конвейер перекомпоновки: от движения края окна до перерисованного экрана.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Раскладка — визуальная дисциплина: в браузерной песочнице курса окна нет, поэтому упражняйся локально. Собери примеры урока, потяни окно за края и понаблюдай, как ведут себя ширины и спейсеры, — пять минут с живым окном дают больше, чем перечитывание кода.
Что дальше#
Каркас из боксов держит любые зоны, но таблицы плитками и формы с подписями Box'ом делать тесновато. Пятый урок 05-grid-form добавит Grid — сетку равных ячеек и GridWrap для плиток любого числа строк — и Form, который раскладывает пары «подпись — поле» и подключает валидацию из восемнадцатого урока. Виджеты для этих сеток бери из третьего урока про базовые виджеты — Label, Button и Entry сейчас лягут в форму. Напомнить каркас запуска можно во втором уроке про первое окно, а словарь CanvasObject и MinSize — в дебютном уроке. Полный план курса из двадцати пяти уроков живёт на странице курса Fyne. Домашнее задание — собрать настоящий каркас приложения из трёх зон; он ещё пригодится: на его базе мы будем делать диалоги, вкладки и итоговый менеджер задач.
Термины урока
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Виртуальный экран
HTML-модель виджет-дерева fyneVBox: сверху вниз
Spacer прижал кнопки к краям
Экран собран: «Box-компоновка»
Коротко: что запомнить
- Расстановкой ведает контейнер: 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 контейнера можно переприсвоить.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
Cobra: CLI-приложения на Go
Интерактивный курс Cobra — фреймворка для CLI на Go: команды, флаги, конфиги Viper, автодополнение. 25 уроков с примерами кода, тестами и итоговым приложением.
- Создавать CLI-приложения с деревом команд на Cobra
- Работать с аргументами, флагами и их привязкой к конфигу
- Управлять конфигурацией через Viper: файлы, env, приоритеты
- Писать хуки PreRun/PostRun и корректно обрабатывать ошибки