Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 4 из 25 ~18 мин +50 XP

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

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

Содержание урока · 12 разделов

К третьему уроку у тебя есть шесть виджетов — и одна проблема: они не умеют раскладывать себя в окне. Позиции и размеры назначает не виджет, а контейнер с компоновкой; сам виджет лишь сообщает свой минимальный размер, 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 лежат: метка «Ок», метка «Это очень длинная строка статуса» и кнопка «Старт». Какой ширины окажется кнопка?

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

Прочитайте код и выберите, что он выведет:

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

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 к нижнему краю окна?

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 из третьего урока. При ресайзе окна карточка остаётся по центру.

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

Прочитайте код и выберите, что он выведет:

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 из десятого урока: виртуализация для того и придумана.

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

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("новая")), но экран не изменился. Что забыли?

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

КонструкторКомпоновка внутриПоведение
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Универсальная фабрика: компоновка подменяема на лету

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

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

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

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

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

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Что дальше#

Каркас из боксов держит любые зоны, но таблицы плитками и формы с подписями Box'ом делать тесновато. Пятый урок 05-grid-form добавит Grid — сетку равных ячеек и GridWrap для плиток любого числа строк — и Form, который раскладывает пары «подпись — поле» и подключает валидацию из восемнадцатого урока. Виджеты для этих сеток бери из третьего урока про базовые виджеты — Label, Button и Entry сейчас лягут в форму. Напомнить каркас запуска можно во втором уроке про первое окно, а словарь CanvasObject и MinSize — в дебютном уроке. Полный план курса из двадцати пяти уроков живёт на странице курса Fyne. Домашнее задание — собрать настоящий каркас приложения из трёх зон; он ещё пригодится: на его базе мы будем делать диалоги, вкладки и итоговый менеджер задач.

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

Термин

fyne.Container

нажмите, чтобы перевернуть
Определение

CanvasObject, который держит детей: поле Objects со списком и поле Layout с алгоритмом. Создаётся container.New(layout, объекты...) или сокращениями NewVBox/NewHBox.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Виртуальный экран

HTML-модель виджет-дерева fyne
Box-компоновкаfyne · мок

VBox: сверху вниз

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 без обёртки и сравни зазоры у краёв.

Личные заметкиПусто
Хранятся только в вашем браузере (Fyne · Контейнеры и компоновка Box)

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

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

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

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

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

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

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

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

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

Следующая ступень учебного пути

Cobra25 уроков~10 чпрактика25 уроков уже доступны

Cobra: CLI-приложения на Go

Интерактивный курс Cobra — фреймворка для CLI на Go: команды, флаги, конфиги Viper, автодополнение. 25 уроков с примерами кода, тестами и итоговым приложением.

  • Создавать CLI-приложения с деревом команд на Cobra
  • Работать с аргументами, флагами и их привязкой к конфигу
  • Управлять конфигурацией через Viper: файлы, env, приоритеты
  • Писать хуки PreRun/PostRun и корректно обрабатывать ошибки