Содержание урока · 12 разделов
- NewGridWithColumns: равные ячейки
- Пустые ячейки и спейсеры
- NewGridWithRows и NewAdaptiveGrid: мобильный поворот
- NewGridWrap: плитки фиксированного размера
- Каталог сеток пакета container
- widget.Form: форма как один виджет
- Валидация полей: SetValidator и честный Submit
- Пять правил формы
- Что дальше: события и собственные виджеты
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Прошлый урок научил тебя складывать виджеты в колонки и строки контейнерами Box, и ты уже знаешь, что VBox и HBox растягивают элементы по одной оси, оставляя соседям их минимальный размер. Но для половины интерфейсов Box не подходит: панель калькулятора — это ровные клетки, галерея миниатюр — плитки одинакового размера, а окно входа — подписи слева, поля справа и две кнопки снизу. Для всего этого у Fyne есть второй этаж компоновки: сетки Grid и виджет Form. Если ты пришёл по запросу «fyne grid» или «fyne form» — это тот самый урок. Сначала разберём четыре сетки пакета container и поймём, как каждая делит свободное место, затем соберём форму с валидацией полей и посмотрим, где ошибаются чаще всего. Все примеры курса живут на твоей машине: собери модуль по второму уроку и запускай каждый листинг командой go run . из каталога проекта — окно откроется на твоём экране, и с ним можно будет покликать.
NewGridWithColumns: равные ячейки#
container.NewGridWithColumns(число, объекты...) — самый ходовой грид. Он делит ширину контейнера на равные колонки, высоту — на равные ряды и растягивает каждый объект на всю его ячейку. Проговорим три следствия. Первое: ячейки по-настоящему равные — ширина каждой колонки равна доступной ширине, поделённой на число колонок, поэтому кнопки калькулятора получаются идеально ровными без единого ручного измерения. Второе: лишнее место не остаётся внизу — если контейнер выше минимума, ряды растягиваются; грид не прижимает содержимое к верху, а заполняет отведённое пространство целиком. Третье: ряды считаются автоматически — объектов больше, чем колонок, лишние переносятся на следующий ряд, а число рядов равно округлённому вверх частному от деления. Минимальный размер грида считается от самого крупного ребёнка: каждый элемент просит свой MinSize (четвёртый урок о MinSize помнит), грид берёт максимум и умножает на число колонок и рядов. Отсюда практический вывод: один огромный виджет растянет все ячейки разом — крупные элементы лучше выносить в соседние зоны Border или в прокрутку, а грид оставлять элементам примерно равного калибра.
Панель калькулятора в три колонки
GOpackage main
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
func main() {
myApp := app.New()
window := myApp.NewWindow("Клавиатура")
keys := []string{"7", "8", "9", "4", "5", "6", "1", "2", "3", "0"}
buttons := make([]fyne.CanvasObject, 0, len(keys))
for _, k := range keys {
key := k
buttons = append(buttons, widget.NewButton(key, func() {
println("нажата", key)
}))
}
window.SetContent(container.NewGridWithColumns(3, buttons...))
window.ShowAndRun()
}Десять кнопок ложатся в четыре ряда по три клетки: число рядов Fyne считает сам. Здесь работают два приёма: копия key := k нужна, чтобы каждая кнопка запомнила своё значение — иначе все замыкания увидят последний элемент среза; а готовые кнопки передаются в контейнер распаковкой buttons... — NewGridWithColumns принимает любой список CanvasObject. Панель занимает всё окно: на любой ширине и высоте клетки остаются равными.
Пустые ячейки и спейсеры#
Что делать, если в последнем ряду не хватает элементов? Грид молча оставит ячейки пустыми — визуально это дырки, в которых просвечивает фон окна. Если пустоты не нужны, есть два приёма. Первый — layout.NewSpacer(): добавь его в список объектов как обычный элемент, он займёт свою долю места и исчезнет визуально, потому что спейсер — это CanvasObject без рисунка. Второй — вообще не поручать такой ряд гриду: кнопки согласия и отказа часто кладут в отдельный HBox и размещают под сеткой в зоне Border. Отдельно запомни, что layout.NewSpacer ведёт себя по-разному у разных хозяев: в VBox он раздвигает соседей, а в грид просто съедает ячейку — это один и тот же объект, поведение определяется компоновкой-владельцем. И ещё одна честная оговорка: грид Fyne не умеет объединять ячейки, аналогов colspan и rowspan из HTML-таблиц здесь нет. Сложную раскладку собирают вложенными контейнерами: грид внутри ячейки Border, Box внутри ячейки грида, Stack поверх обоих. Вкладывать контейнеры друг в друга — не костыль, а нормальная практика: дерево содержимого в Fyne так и растёт вниз.
В container.NewGridWithColumns(3) передали 7 кнопок. Как ляжет сетка?
Как лягут четыре кнопки?
Прочитайте код и выберите, что он выведет:
a := widget.NewButton("А", nil)
b := widget.NewButton("Б", nil)
c := widget.NewButton("В", nil)
d := widget.NewButton("Г", nil)
window.SetContent(container.NewGridWithColumns(2, a, b, c, d))NewGridWithRows и NewAdaptiveGrid: мобильный поворот#
container.NewGridWithRows(число, объекты...) — зеркальный вариант: задаёшь число рядов, а колонки считаются сами. Это удобно для панелей, которые делят окно по высоте: два ряда — зона ввода и зона истории переписки. container.NewAdaptiveGrid(n, объекты...) — грид-хамелеон для кроссплатформенности: если контейнер шире, чем выше, объекты встают в один ряд из n колонок; если контейнер уже — в одну колонку из n рядов. Это ровно тот приём, которым делают панель инструментов, одинаково живущую на десктопе и на телефоне: на широком экране четыре кнопки идут линией, на узком — столбиком. Быстрый способ увидеть обе ветки — мобильная симуляция из второго урока: go run -tags mobile, а затем просто сожми окно и посмотри, как панель перестроилась. Помни и про ограничение: AdaptiveGrid смотрит на пропорции контейнера, а не на ориентацию всего окна. Если панель живёт в узкой колонке Border, она останется вертикальной даже на широком мониторе. Иногда это именно то, что нужно; иногда — повод выбрать другую сетку или переставить зоны Border.
NewGridWrap: плитки фиксированного размера#
container.NewGridWrap(размер, объекты...) — вторая большая сетка. Каждому объекту выдаётся строго заданный размер, а укладываются они рядами: заполняют строку по ширине и переносятся на следующую — как плитки в файловом менеджере или миниатюры галереи. Разница с GridWithColumns принципиальная. Там размер ячейки определяет контейнер: растянул окно — клетки выросли вместе с ним. В GridWrap размер задаёшь ты, и он не меняется: растянул окно — плитки остались 120 на 90, но их в ряду стало больше. Именно поэтому GridWrap — выбор для галерей, списков превью и наборов файлов: предсказуемый размер каждого элемента важнее равного деления площади. Второе отличие — вертикальное поведение: строки прижимаются к верху, и если контейнер выше, лишнее место остаётся пустым снизу — грид-wrap, в отличие от обычного, ряды не растягивает. Хочешь прокрутку — оберни сетку в container.NewScroll: сетка посчитает свой размер под текущую ширину, а прокрутка добавит недостающую высоту. В этом же прячется ловушка новичка: задал размер плитки больше ширины окна — получишь по одной плитке в ряду. Тулкит не будет ужимать твои 200 пикселей до 150: размер в GridWrap — закон.
Галерея цветных плиток
GOpackage main
import (
"image/color"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
)
func main() {
myApp := app.New()
window := myApp.NewWindow("Палитра")
paints := []color.NRGBA{
{R: 231, G: 76, B: 60, A: 255},
{R: 46, G: 204, B: 113, A: 255},
{R: 52, G: 152, B: 219, A: 255},
{R: 241, G: 196, B: 15, A: 255},
{R: 155, G: 89, B: 182, A: 255},
{R: 26, G: 188, B: 156, A: 255},
}
tiles := make([]fyne.CanvasObject, 0, len(paints))
for _, p := range paints {
tile := canvas.NewRectangle(p)
tiles = append(tiles, tile)
}
window.SetContent(container.NewScroll(
container.NewGridWrap(fyne.NewSize(90, 90), tiles...),
))
window.ShowAndRun()
}Шесть цветных плиток строго 90 на 90: уменьши окно — в ряду станет их меньше, увеличь — больше, но размер каждой плитки неизменен, а лишняя высота уходит под прокрутку. canvas.NewRectangle — примитив из седьмого урока, здесь он просто даёт плиткам цвет. fyne.NewSize собирает размер из ширины и высоты — этой функцией ты будешь пользоваться постоянно.
Каталог сеток пакета container#
| Конструктор | Размер ячеек | Когда брать |
|---|---|---|
| NewGridWithColumns(n, …) | равные, делят всю площадь | панели кнопок, калькуляторы, шахматки |
| NewGridWithRows(n, …) | равные, n рядов | деление по высоте: ввод и история |
| NewAdaptiveGrid(n, …) | ряд на широком, колонка на узком | тулбары под десктоп и мобильные |
| NewGridWrap(size, …) | строго заданный size | галереи, превью, файловые плитки |
| layout.NewGridLayout(n) | как GridWithColumns, но через пакет layout | когда собираешь компоновку вручную |
| layout.NewSpacer() | занимает ячейку, невидим | залить пустую ячейку или раздвинуть соседей |
Окно с GridWrap(120×90) растянули вдвое. Что изменится?
widget.Form: форма как один виджет#
Форма входа — самая частая сборка в GUI, и Fyne даёт для неё готовый виджет: widget.Form. Ты передаёшь список пар «подпись — поле», а Form сам раскладывает их столбцом, рисует подписи, выравнивает поля и добавляет снизу две кнопки — Submit и Cancel. Тебе остаётся заполнить поля OnSubmit и OnCancel, и обработчики сразу живут на своих местах. Значения полей читаются обычным способом: entry.Text внутри OnSubmit. Дефолтные подписи кнопок английские — свойства SubmitText и CancelText меняют их на твои. Собирается форма литералом структуры &widget.Form{...}: так все обработчики видны одним взглядом. Элемент списка — widget.NewFormItem(label, field), он возвращает готовую FormItem. Есть и ручной способ: form.Append("Имя", entry) добавляет пару в живую форму — им удобно дорисовывать поля по ходу работы, например когда состав полей зависит от данных. Форма — полноценный CanvasObject: её кладут в SetContent, в зоны Border, в прокрутку, как и любой другой виджет. Валидация подключается к форме одной строкой на поле — этим займёмся прямо сейчас. Под капотом подписи и поля раскладывает компоновка Form из пакета layout — если когда-нибудь понадобится та же раскладка без кнопок, возьми именно её.
Форма входа целиком
GOpackage main
import (
"fmt"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/widget"
)
func main() {
myApp := app.New()
window := myApp.NewWindow("Вход")
login := widget.NewEntry()
login.SetPlaceHolder("логин")
password := widget.NewPasswordEntry()
password.SetPlaceHolder("пароль")
form := &widget.Form{
Items: []*widget.FormItem{
widget.NewFormItem("Логин", login),
widget.NewFormItem("Пароль", password),
},
SubmitText: "Войти",
CancelText: "Сбросить",
OnSubmit: func() {
fmt.Println("вход:", login.Text)
},
OnCancel: func() {
login.SetText("")
password.SetText("")
},
}
window.SetContent(form)
window.ShowAndRun()
}Form выравнивает подписи и поля, снизу — две кнопки с твоими подписями. OnSubmit читает значения прямо из entry.Text: замыкание помнит login и password. Cancel здесь делает то, что принято у формы входа: чистит поля через SetText — та же кнопка в другом приложении могла бы закрывать окно. widget.NewPasswordEntry — тот же Entry, но символы замаскированы.
Валидация полей: SetValidator и честный Submit#
Красота Form раскрывается с валидацией. Каждому полю назначается проверка: entry.SetValidator(func(s string) error). Вернул nil — поле в порядке; вернул ошибку — Fyne рисует её красным прямо под полем, с тем текстом, который ты написал в errors.New. Проверка срабатывает по мере ввода — это событие OnChanged поля, о нём подробнее в шестом уроке, — и при потере фокуса, а все поля разом — при нажатии Submit. И вот главное правило формы: если хотя бы одно поле невалидно, OnSubmit не вызывается вовсе — кнопка как будто не работает. Это сознательная защита: обработчик отправки гарантированно получает данные, прошедшие проверку. Отсюда два практических вывода. Первый: не дублируй проверки в OnSubmit — там место только для того, чего валидатор знать не может, например занят ли логин на сервере. Второй: если заполняешь поля кодом, а не руками, проверь их сам — у Entry есть метод Validate(), который запускает валидатор и рисует ошибку. Для нестандартных правил описывай обычную func(string) error: хоть с regexp для почты, хоть со strconv для чисел. Это и есть валидация формы в чистом виде: одна функция на поле и ноль проверок в обработчике отправки. Полный разбор пакетных валидаторов — тема восемнадцатого урока, здесь нам хватит функций.
Два валидатора на форме регистрации
GOpackage main
import (
"errors"
"strings"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/widget"
)
func main() {
myApp := app.New()
window := myApp.NewWindow("Регистрация")
email := widget.NewEntry()
email.SetValidator(func(s string) error {
if !strings.Contains(s, "@") {
return errors.New("нужен адрес почты со знаком @")
}
return nil
})
password := widget.NewPasswordEntry()
password.SetValidator(func(s string) error {
if len(s) < 8 {
return errors.New("пароль короче 8 символов")
}
return nil
})
form := &widget.Form{
Items: []*widget.FormItem{
widget.NewFormItem("Почта", email),
widget.NewFormItem("Пароль", password),
},
OnSubmit: func() { println("принято:", email.Text) },
}
window.SetContent(form)
window.ShowAndRun()
}Валидатор — обычная функция: вернул ошибку — поле краснеет, Submit отказывается работать. Пиши тексты ошибок по-человечески: эту строку пользователь прочитает под полем. Заметь: OnSubmit не проверяет почту ещё раз — к моменту вызова валидаторы уже отработали, и данные заведомо чистые.
Пользователь нажал Submit, а поле почты невалидно. Что произойдёт?
Сработает ли Submit на пустой форме?
Прочитайте код и выберите, что он выведет:
name := widget.NewEntry()
name.SetValidator(func(s string) error {
if s == "" {
return errors.New("заполните поле")
}
return nil
})
form := &widget.Form{
Items: []*widget.FormItem{widget.NewFormItem("Имя", name)},
OnSubmit: func() { println("Отправлено") },
}
// пользователь не трогает поле и сразу жмёт SubmitПять правил формы#
- Собирай форму литералом &widget.Form с Items и двумя обработчиками, а не стеком VBox с ручными подписями.
- Подписи кнопок по умолчанию английские — SubmitText и CancelText переводят их на свои.
- Валидатор поля — это func(string) error: nil означает «порядок», ошибка рисуется под полем.
- OnSubmit срабатывает только когда все поля прошли валидацию — не дублируй проверки.
- Значения читай из entry.Text внутри OnSubmit, а form.Append дорисовывает поля в живую форму.
Собери форму профиля
Восстанови четыре пропуска: тип формы, конструктор элемента, чтение значения и запуск цикла.
Путь нажатия Submit
Расставь по порядку, что происходит после нажатия кнопки Submit формы.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Окна из этого урока откроются только на твоей машине: браузерная песочница курса не умеет запускать графические приложения. Собери модуль по второму уроку, вставь пример в main.go и запусти go run . — сетки и форма появятся на экране, и с ними можно будет покликать.
Что дальше: события и собственные виджеты#
Окно с формой уже живое, но пока всё в нём делает только кнопка Submit. А что вообще происходит, когда ты нажимаешь любую кнопку? Как Fyne решает, кому отдать клик, и можно ли повесить действие на правую кнопку мыши или на Ctrl+N? Шестой урок — события и обработчики — разбирает всю вертикаль ввода от OnTapped до горячих клавиш. Если хочется освежить виджеты, из которых собраны поля формы, — вернись к каталогу базовых виджетов; а про то, как Box делит место и зачем нужен Spacer, напомнит четвёртый урок. Сетки сегодня встретились с примитивом canvas.NewRectangle — по-настоящему рисовать им научит седьмой урок. Полный план из двадцати пяти уроков лежит на странице курса Fyne. Домашнее задание ниже соберёт из этого урока маленький, но настоящий инструмент — форма с плитками настроек и прокруткой.
Термины урока
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Виртуальный экран
HTML-модель виджет-дерева fyneA1
B1
A2
B2
A3
B3
Экран собран: «Grid 2×3»
Коротко: что запомнить
- Grid с равными колонками — container.NewGridWithColumns: делит всю площадь поровну и растягивает объекты на ячейки; ряды считаются автоматически.
- GridWrap задаёт плиткам фиксированный размер и переносит их по ширине контейнера — это сетка галерей и превью, не растягивающая ряды по высоте.
- NewAdaptiveGrid переключается между рядом и колонкой по пропорциям контейнера — база адаптивных тулбаров под десктоп и мобильные.
- Пустые ячейки заливаются layout.NewSpacer(), объединения ячеек нет — сложные раскладки собираются вложенными контейнерами.
- widget.Form собирает пары «подпись — поле» через []*widget.FormItem и NewFormItem, рисует кнопки Submit и Cancel с настраиваемыми подписями.
- Валидация поля — entry.SetValidator(func(string) error): ошибка рисуется под полем, а Submit не вызывает OnSubmit, пока хоть одно поле невалидно.
- Значения полей читаются через entry.Text в OnSubmit; form.Append добавляет пары в живую форму.
Домашнее задание
Собери окно «Профиль игрока»: слева — форма из трёх полей (никнейм не пустой, почта содержит знак @, возраст — число от 1 до 99, проверяемое через strconv.Atoi с понятным текстом ошибки), справа — грид 2 на 2 из цветных плиток canvas.NewRectangle. Кнопка Submit печатает значения в консоль и не срабатывает, пока валидаторы недовольны; Cancel чистит поля через SetText. Затем замени правый грид на NewAdaptiveGrid и сожми окно до узкого состояния — убедись, что плитки перестроились в столбец. В конце собери третью версию панели на NewGridWrap с плитками 120 на 90, оберни её в container.NewScroll и добейся прокрутки при маленьком окне. Три версии одной панели — лучшая иллюстрация того, какую сетку когда брать.
Личные заметкиПусто
Вопросы и ответы по уроку
Грид портится, когда окно слишком маленькое?
Ячейки ужимаются до нуля без жалоб: грид делит всю площадь, какой бы крошечной она ни была. Если содержимое должно оставаться читаемым, оберни сетку в container.NewScroll или отдай ей центр Border, а соседние зоны сделай фиксированными — узкое окно тогда даст прокрутку, а не кашу.
Можно объединять ячейки, как colspan и rowspan в HTML?
Нет, объединения ячеек в гриде Fyne не предусмотрено. Сложную раскладку собирают вложенными контейнерами: грид в ячейке Border, Box внутри ячейки грида, Stack поверх. Дерево контейнеров полностью заменяет объединение ячеек и читается лучше табличных трюков.
Form работает без FormItem — просто со списком виджетов?
Нет: Form строится из []*widget.FormItem — пар «подпись — поле». Поле без подписи кладётся в FormItem с пустым Text. Для столбца полей без подписей логичнее вообще не Form, а VBox или грид: Form ценен именно подписями и кнопками.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
Cobra: CLI-приложения на Go
Интерактивный курс Cobra — фреймворка для CLI на Go: команды, флаги, конфиги Viper, автодополнение. 25 уроков с примерами кода, тестами и итоговым приложением.
- Создавать CLI-приложения с деревом команд на Cobra
- Работать с аргументами, флагами и их привязкой к конфигу
- Управлять конфигурацией через Viper: файлы, env, приоритеты
- Писать хуки PreRun/PostRun и корректно обрабатывать ошибки