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

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

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

Урок 5 из 25 ~20 мин +50 XP

Grid и формы

Box из четвёртого урока складывает виджеты в ряд, но панель калькулятора, галерея плиток и окно входа требуют другого: равных ячеек и готовой формы. Урок про fyne grid и fyne form: NewGridWithColumns и GridWrap, адаптивная сетка для мобильных, затем widget.Form с подписями, полями и валидацией Entry. Примеры, как всегда, запускаются на твоей машине командой go run . — каркас app.New → окно → SetContent тебе уже знаком.

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

Прошлый урок научил тебя складывать виджеты в колонки и строки контейнерами Box, и ты уже знаешь, что VBox и HBox растягивают элементы по одной оси, оставляя соседям их минимальный размер. Но для половины интерфейсов Box не подходит: панель калькулятора — это ровные клетки, галерея миниатюр — плитки одинакового размера, а окно входа — подписи слева, поля справа и две кнопки снизу. Для всего этого у Fyne есть второй этаж компоновки: сетки Grid и виджет Form. Если ты пришёл по запросу «fyne grid» или «fyne form» — это тот самый урок. Сначала разберём четыре сетки пакета container и поймём, как каждая делит свободное место, затем соберём форму с валидацией полей и посмотрим, где ошибаются чаще всего. Все примеры курса живут на твоей машине: собери модуль по второму уроку и запускай каждый листинг командой go run . из каталога проекта — окно откроется на твоём экране, и с ним можно будет покликать.

NewGridWithColumns: равные ячейки#

container.NewGridWithColumns(число, объекты...) — самый ходовой грид. Он делит ширину контейнера на равные колонки, высоту — на равные ряды и растягивает каждый объект на всю его ячейку. Проговорим три следствия. Первое: ячейки по-настоящему равные — ширина каждой колонки равна доступной ширине, поделённой на число колонок, поэтому кнопки калькулятора получаются идеально ровными без единого ручного измерения. Второе: лишнее место не остаётся внизу — если контейнер выше минимума, ряды растягиваются; грид не прижимает содержимое к верху, а заполняет отведённое пространство целиком. Третье: ряды считаются автоматически — объектов больше, чем колонок, лишние переносятся на следующий ряд, а число рядов равно округлённому вверх частному от деления. Минимальный размер грида считается от самого крупного ребёнка: каждый элемент просит свой MinSize (четвёртый урок о MinSize помнит), грид берёт максимум и умножает на число колонок и рядов. Отсюда практический вывод: один огромный виджет растянет все ячейки разом — крупные элементы лучше выносить в соседние зоны Border или в прокрутку, а грид оставлять элементам примерно равного калибра.

Панель калькулятора в три колонки

GO
package 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 — закон.

Галерея цветных плиток

GO
package 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 — если когда-нибудь понадобится та же раскладка без кнопок, возьми именно её.

Форма входа целиком

GO
package 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 для чисел. Это и есть валидация формы в чистом виде: одна функция на поле и ноль проверок в обработчике отправки. Полный разбор пакетных валидаторов — тема восемнадцатого урока, здесь нам хватит функций.

Два валидатора на форме регистрации

GO
package 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

Пять правил формы#

  1. Собирай форму литералом &widget.Form с Items и двумя обработчиками, а не стеком VBox с ручными подписями.
  2. Подписи кнопок по умолчанию английские — SubmitText и CancelText переводят их на свои.
  3. Валидатор поля — это func(string) error: nil означает «порядок», ошибка рисуется под полем.
  4. OnSubmit срабатывает только когда все поля прошли валидацию — не дублируй проверки.
  5. Значения читай из entry.Text внутри OnSubmit, а form.Append дорисовывает поля в живую форму.

Собери форму профиля

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

package main import ( "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/widget" ) func main() { myApp := app.New() window := myApp.NewWindow("Профиль") name := widget.NewEntry() form := &widget.{ Items: []*widget.FormItem{ widget.("Имя", name), }, OnSubmit: func() { println("имя:", name.) }, } window.SetContent(form) window.() }

Путь нажатия Submit

Расставь по порядку, что происходит после нажатия кнопки Submit формы.

Строки

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

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

Что дальше: события и собственные виджеты#

Окно с формой уже живое, но пока всё в нём делает только кнопка Submit. А что вообще происходит, когда ты нажимаешь любую кнопку? Как Fyne решает, кому отдать клик, и можно ли повесить действие на правую кнопку мыши или на Ctrl+N? Шестой урок — события и обработчики — разбирает всю вертикаль ввода от OnTapped до горячих клавиш. Если хочется освежить виджеты, из которых собраны поля формы, — вернись к каталогу базовых виджетов; а про то, как Box делит место и зачем нужен Spacer, напомнит четвёртый урок. Сетки сегодня встретились с примитивом canvas.NewRectangle — по-настоящему рисовать им научит седьмой урок. Полный план из двадцати пяти уроков лежит на странице курса Fyne. Домашнее задание ниже соберёт из этого урока маленький, но настоящий инструмент — форма с плитками настроек и прокруткой.

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

Термин

NewGridWithColumns

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

Сетка с равными колонками: ширина и высота делятся поровну, объекты растягиваются на ячейку, лишние элементы переносятся в следующий ряд автоматически.

1 / 6

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

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

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

HTML-модель виджет-дерева fyne
Grid 2×3fyne · мок

A1

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 и добейся прокрутки при маленьком окне. Три версии одной панели — лучшая иллюстрация того, какую сетку когда брать.

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

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

Грид портится, когда окно слишком маленькое?

Ячейки ужимаются до нуля без жалоб: грид делит всю площадь, какой бы крошечной она ни была. Если содержимое должно оставаться читаемым, оберни сетку в container.NewScroll или отдай ей центр Border, а соседние зоны сделай фиксированными — узкое окно тогда даст прокрутку, а не кашу.

Можно объединять ячейки, как colspan и rowspan в HTML?

Нет, объединения ячеек в гриде Fyne не предусмотрено. Сложную раскладку собирают вложенными контейнерами: грид в ячейке Border, Box внутри ячейки грида, Stack поверх. Дерево контейнеров полностью заменяет объединение ячеек и читается лучше табличных трюков.

Form работает без FormItem — просто со списком виджетов?

Нет: Form строится из []*widget.FormItem — пар «подпись — поле». Поле без подписи кладётся в FormItem с пустым Text. Для столбца полей без подписей логичнее вообще не Form, а VBox или грид: Form ценен именно подписями и кнопками.

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

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

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

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

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

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

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

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

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