Содержание урока · 13 разделов
- Правило сеттеров: поле меняет данные, сеттер меняет экран
- Button: OnTapped, Importance и запрет на клик
- Label: текст, стиль и выравнивание
- Entry подробно: многострочность, пароли и путь к валидации
- Check и RadioGroup: булевы вопросы
- Select: список с одним выбором
- ProgressBar подробно: доли, диапазоны и TextFormatter
- Каталог урока: шесть виджетов и их словарь
- Какой виджет когда: сценарии выбора
- Что дальше
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Во втором уроке мы собрали окно: Resize, SetFixedSize, перехват закрытия, живой цикл событий. Только внутри него пока скучно — та же метка с кнопкой из hello world. Здесь мы пройдём по каталогу из шести элементов — это базовые fyne виджеты: Label, Button, Entry, Check и Select, плюс ProgressBar. Их хватит для подавляющего большинства внутренних утилит: панели управления, формы настроек, мониторы статусов. Если гуглить тот же список по-английски — fyne widgets — ты попадёшь на страницу explore/widgets в документации, где живут все элементы тулкита; сегодня мы берём из неё ядро. У урока есть сквозная идея, одна на все шесть виджетов: у каждого есть фабрика вида NewXxx, набор полей состояния и методы-сеттеры, через которые состояние меняют. Это правило сеттеров — самая важная привычка урока, и начнём мы именно с неё: без неё кнопка молча перестанет обновлять надпись, и ты будешь искать баг там, где его нет.
Правило сеттеров: поле меняет данные, сеттер меняет экран#
Вспомним анатомию из первого урока: виджет — это CanvasObject с поведением, а внешний вид вынесен в WidgetRenderer. Когда состояние виджета меняется, кто-то должен сказать рендереру «перерисуйся» — это делает Refresh(). Так вот, методы-сеттеры — SetText, SetChecked, SetValue, Disable — делают ровно две вещи: записывают новое значение в поле и вызывают Refresh. Поля у виджетов публичные, и компилятор не запретит тебе написать button.Text = "Готово" напрямую. Значение в поле запишется — а экран нет: рендерер о новом состоянии не узнал, Refresh никто не вызвал. Классический симптом: логика отработала, переменные верные, а интерфейс показывает старьё. Отсюда правило урока: меняешь виджет — вызывай сеттер, а не трогай поле. Прямое присваивание оправдано только в связке с ручным Refresh() — так делают внутри самих сеттеров и при массовом обновлении, но в обычном коде это лишний шанс забыть перерисовку. Читаемость тоже выигрывает: по вызову SetText видно намерение, по присваиванию поля — нет.
Label и Button: надпись, действие, счётчик
GOpackage main
import (
"fmt"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
func main() {
myApp := app.New()
window := myApp.NewWindow("Счётчик")
count := 0
label := widget.NewLabel("Кликов: 0")
add := widget.NewButton("Плюс один", func() {
count++
label.SetText(fmt.Sprintf("Кликов: %d", count))
})
// главная кнопка формы: заметнее остальных
add.SetImportance(widget.HighImportance)
window.SetContent(container.NewVBox(
label,
add,
))
window.ShowAndRun()
}widget.NewLabel создаёт метку, widget.NewButton — кнопку: первый аргумент текст, второй — функция-обработчик, которая попадает в поле OnTapped. Внутри обработчика мы меняем надпись через label.SetText — сеттер, а не label.Text = ...: поэтому экран обновляется сам. SetImportance(widget.HighImportance) выделяет кнопку основным цветом темы — так помечают главное действие формы.
Button: OnTapped, Importance и запрет на клик#
Кнопка — самый востребованный из fyne виджетов, и запросы вроде fyne button ведут на неё же. Внутри у неё четыре главных поля: Text — надпись, Icon — иконка fyne.Resource, Importance — «важность» внешнего вида и OnTapped — функция, которую цикл событий вызовет по клику. Фабрика NewButton(label, tapped) заполняет Text и OnTapped одним вызовом; есть и NewButtonWithIcon для кнопки с иконкой, полезной в тулбарах. Importance появился в версии 1.4 и управляет заметностью: HighImportance красит кнопку в primary-цвет темы — главное действие; LowImportance наоборот убирает часть оформления — второстепенное; DangerImportance — для разрушительных действий вроде удаления, у него красная заливка. Менять важно только сеттером SetImportance по тому же правилу. Вторая способность кнопки — временный запрет: Disable() делает её серой и нечувствительной к кликам, Enable() возвращает; состояние читает Enabled(). Типовой сценарий: форма невалидна — кнопка «Сохранить» заблокирована, поле заполнили корректно — Enable. Нажатие заблокированной кнопки не вызывает OnTapped — обработчик просто не сработает.
Написали button.Text = "Готово", переменная изменилась, но надпись на экране осталась старой. Почему?
Label: текст, стиль и выравнивание#
Label — метка, нередактируемый текст: статусы, подписи полей, результаты расчёта. Главный метод тебе уже знаком — SetText. Стилизация идёт через fyne.TextStyle: структура с флагами Bold, Italic, Monospace, BoldItalic — для моноширинных значений вроде ID и IP-адресов Monospace незаменим. Выравнивание задаётся fyne.TextAlign: Leading, Center, Trailing. Всё это одной фабрикой: widget.NewLabelWithStyle("Готово к работе", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}) — и есть одноимённый метод SetTextStyle и SetTextAlignment, чтобы менять стиль по ходу работы. С версии 2.4 у Label появилось поле Importance — им помечают предупреждающий и приглушённый текст, например серым рисуют недоступные значения; и Truncation — что делать с длинной строкой: переносить или обрезать многоточием. Если текста в метке становится много и нужен цвет, абзацы и вставки — это территория RichText, разберём его в девятом уроке про текст и ввод. Пока правило простое: одна строка или абзац статуса — Label, всё сложнее — RichText.
Entry: ввод с обратной связью
GOpackage main
import (
"fmt"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
func main() {
myApp := app.New()
window := myApp.NewWindow("Вход")
email := widget.NewEntry()
email.SetPlaceHolder("you@example.com")
email.OnChanged = func(text string) {
fmt.Println("Печатает:", text)
}
password := widget.NewPasswordEntry()
password.SetPlaceHolder("Пароль")
login := widget.NewButton("Войти", func() {
fmt.Println("Логин:", email.Text, "пароль:", password.Text)
})
window.SetContent(container.NewVBox(email, password, login))
window.ShowAndRun()
}NewEntry создаёт однострочное поле, SetPlaceHolder кладёт серую подсказку внутри пустого поля. OnChanged вызывается на каждое изменение текста, OnSubmitted — по Enter у однострочного. NewPasswordEntry — тот же Entry с маской ввода и кнопкой показать текст. Текущее значение читает поле Text, записывает — SetText.
Сколько раз сработает OnChanged?
Прочитайте код и выберите, что он выведет:
email := widget.NewEntry()
email.OnChanged = func(text string) {
fmt.Println("Изменение:", text)
}
// пользователь вводит слово "go" посимвольноEntry подробно: многострочность, пароли и путь к валидации#
Entry — рабочая лошадь ввода, и у неё несколько лиц. NewMultiLineEntry даёт многострочное поле для комментариев и заметок; у многострочного Enter вставляет перевод строки, а отправка срабатывает по Shift+Enter, если OnSubmitted задан. NewPasswordEntry прячет символы и добавляет кнопку «показать» — специально сочинять маску не нужно. Значение читается полем Text в любой момент, пишется сеттером SetText — и да, заполнить поле программно перед показом формы — нормальный приём. Подсказка PlaceHolder экономит подпись над коротким полем. Отдельная тема — контроль ввода: у Entry есть SetValidator, функция, которая получает текст и возвращает nil или ошибку; поле само покажет ошибку под строкой и заблокирует «сломанное» состояние. Полный разбор валидации — восемнадцатый урок, а сегодня достаточно знать, что место для проверки уже предусмотрено архитектурой. Ещё полезно: Disable делает поле недоступным (не редактируется), Enable возвращает — удобно для режимов «только просмотр».
Нужно блокировать кнопку «Далее», пока пароль короче восьми символов. Куда встать?
Check и Select: выбор пользователя
GOpackage main
import (
"log"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
func main() {
myApp := app.New()
window := myApp.NewWindow("Настройки")
notify := widget.NewCheck("Присылать уведомления", func(on bool) {
log.Println("Уведомления:", on)
})
theme := widget.NewSelect(
[]string{"Системная", "Светлая", "Тёмная"},
func(value string) {
log.Println("Тема:", value)
},
)
window.SetContent(container.NewVBox(
notify,
theme,
))
window.ShowAndRun()
}NewCheck получает подпись и обработчик func(bool) — он вызывается при каждом переключении. NewSelect получает список опций и обработчик func(string). При старте чекбокс снят, а селект показывает подсказку-плейсхолдер: ни одна опция ещё не выбрана.
Check и RadioGroup: булевы вопросы#
Check — ответ на булев вопрос: включить уведомления, согласиться с условиями, добавить в избранное. Фабрика NewCheck(label, changed) принимает подпись и функцию func(bool); состояние читает поле Checked, програмно ставит сеттер SetChecked — например, чтобы восстановить сохранённые настройки при старте. Обработчик получает новое значение, поэтому внутри него не нужно читать Checked: аргумент и есть текущее состояние. Когда вариантов больше двух, а выбрать нужно один, берут RadioGroup — круги с точкой: NewRadioGroup([]string{"Email", "SMS"}, func(value string)) — обработчик получает выбранную строку. Нужен выбор нескольких из многих — есть CheckGroup, многовыборочный родственник RadioGroup с SetSelected для списка отмеченных. Вариантов становится много и хочется их редактировать на ходу — у RadioGroup и Select есть SetOptions, заменяющая список опций целиком. И помни каталог: чекбокс — про «да/нет», радиокруги — про «ровно один», и не выворачивай их местами, пользователи читают форму именно так.
Select: список с одним выбором#
Select — выпадающий список для выбора одного значения из известного набора: тема, город, приоритет задачи. Фабрика NewSelect(options, changed): первый аргумент — слайс строк, второй — обработчик func(string), который получит выбранное значение. Текущий выбор лежит в поле Selected, програмно ставится SetSelected — так восстанавливают сохранённую опцию при запуске, а ClearSelected сбрасывает выбор до плейсхолдера. Пока ничего не выбрано, Select показывает подсказку: по умолчанию это «(Select one)», а поле PlaceHolder позволяет поставить своё, например «выберите тему». Обработчик срабатывает и на выбор опции, и на сброс — получая пустую строку, учитывай это. Менять список опций на лету умеет SetOptions — удобно для зависимых списков: выбрали страну, подгрузили города. Когда список должен и подсказывать, и принимать произвольный ввод, есть гибрид SelectEntry — селект с редактируемым полем; разберём его вместе с остальным вводом в девятом уроке. Для «да/нет» Select не нужен — это работа Check.
ProgressBar: индикатор прогресса
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("Прогресс")
bar := widget.NewProgressBar() // Min=0, Max=1
bar.SetValue(0.25) // 25%
// длительность неизвестна: "что-то происходит"
spin := widget.NewProgressBarInfinite()
window.SetContent(container.NewVBox(
bar,
spin,
))
window.ShowAndRun()
}NewProgressBar создаёт полосу с Min 0 и Max 1; SetValue принимает долю и рисует процент. ProgressBarInfinite не знает значений — полоса ездит туда-сюда, показывая занятость. Оба обновляются сеттером: SetValue под капотом вызывает Refresh, заодно пересчитывая надпись.
ProgressBar подробно: доли, диапазоны и TextFormatter#
ProgressBar показывает известную долю завершённости. По умолчанию диапазон от 0 до 1: SetValue(0.25) — четверть, SetValue(1) — всё; надпись процента рисует сам виджет. Диапазон настраивается полями Min и Max: загрузили файл на 20 мегабайт — поставь Max = 20 и вызывай SetValue с мегабайтами, виджет сам переведёт в проценты. Но помни правило сеттеров: Value тоже поле, однако присваивание напрямую не перерисует полосу — только SetValue. Подпись по умолчанию — проценты; свою форматирование даёт поле TextFormatter (с версии 1.4): функция, возвращающая строку, которая пересчитывается при каждом обновлении значения, — например «3 из 12 файлов». Полезная оговорка честности: если длительность неизвестна, не выдумывай проценты — возьми ProgressBarInfinite, бесконечно движущуюся полосу занятости: SetValue для неё не имеет смысла, остановить индикатор можно Hide или удалением из контейнера. Прогресс из горутины обновляй только через fyne.Do из двадцатого урока — SetValue из чужого потока небезопасен.
Какой процент покажет полоса?
Прочитайте код и выберите, что он выведет:
bar := widget.NewProgressBar()
bar.Max = 200
bar.SetValue(100)Каталог урока: шесть виджетов и их словарь#
| Виджет | Фабрика | Сеттеры и события | Задача |
|---|---|---|---|
| Label | NewLabel, NewLabelWithStyle | SetText, SetTextStyle | Показать текст, статус, подпись |
| Button | NewButton, NewButtonWithIcon | SetText, SetImportance, Disable/Enable, OnTapped | Действие по клику |
| Entry | NewEntry, NewMultiLineEntry, NewPasswordEntry | SetText, SetPlaceHolder, OnChanged, OnSubmitted | Ввод текста |
| Check | NewCheck | SetChecked, Checked, обработчик func(bool) | Булев выбор «да/нет» |
| RadioGroup | NewRadioGroup | SetSelected, SetOptions, обработчик func(string) | Один вариант из многих |
| Select | NewSelect | SetSelected, SetOptions, ClearSelected, PlaceHolder | Выбор строки из списка |
| ProgressBar | NewProgressBar | SetValue, Min/Max, TextFormatter | Известная доля работы |
| ProgressBarInfinite | NewProgressBarInfinite | Hide/показ, без SetValue | Занятость без процента |
Какой виджет когда: сценарии выбора#
Пройдём по типовым ситуациям. Статус операции («Файл сохранён», «Ошибка сети») — Label, при необходимости с Monospace для значений. Единственное главное действие формы — Button с HighImportance; разрушительное вроде «Удалить всё» — DangerImportance; вторичные действия — обычные или LowImportance. Ввод любых данных от человека — Entry: однострочное для полей, многострочное для текстов, парольное для секретов; подсказку в пустое поле ставь PlaceHolder, а не первой строкой значения. Переключатель режима «вкл/выкл» — Check. Выбор одной опции из трёх-пяти известных — Select; если опции — режимы с видимой альтернативой, RadioGroup покажет все варианты сразу без клика. Отчёт о ходе долгой операции — ProgressBar с честным диапазоном, а если честного процента нет — ProgressBarInfinite. Проверка ввода на корректность — это не про виджеты выбора: у Entry есть валидатор, и это восемнадцатый урок. А собирают все эти элементы вместе не в окне напрямую, а в контейнерах — этим занимается следующий, четвёртый урок.
Собери форму подписки
Восстанови четыре пропуска: метка, поле с подсказкой, чекбокс и процент прогресса.
Путь клика по кнопке
Собери цепочку от нажатия до перерисованного экрана — она повторяется у каждого виджета урока.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Копирование файлов занимает от 5 секунд до получаса, точный процент неизвестен. Какой индикатор честнее?
Браузерная песочница курса окна не открывает: графическому приложению нужен настоящий экран и C-компилятор, поэтому runner-кнопок здесь нет. Каждый пример — рабочий код: собери модуль по второму уроку и запускай go run ., меняя аргументы фабрик и наблюдая за виджетами вживую.
Что дальше#
Шесть виджетов из каталога — это стройматериал, но в окне он должен стоять по местам: колонкой, строкой, с отступами. Этим займётся четвёртый урок про контейнеры и компоновку Box — NewVBox, NewHBox, спейсеры и правило минимальных размеров. Дальше — пятый урок 05-grid-form с сетками и Form, который соберёт поля и подписи в готовую форму с валидацией. Если что-то из словаря урока забыл — вернись к установке и первому окну за каркасом запуска или к дебютному уроку курса за анатомией Widget/Renderer. Полная программа из двадцати пяти уроков — на странице курса Fyne. Домашнее задание ниже собирает всё в один файл — мини-настройки приложения, которые мы ещё вернём и подключим к хранилищу preferences в девятнадцатом уроке.
Термины урока
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Виртуальный экран
HTML-модель виджет-дерева fyneКликов: 0
Экран собран: «Счётчик»
Коротко: что запомнить
- Шесть базовых виджетов покрывают большинство форм: Label для текста, Button для действий, Entry для ввода, Check и Select для выбора, ProgressBar для прогресса.
- Правило сеттеров: SetText, SetChecked, SetValue, Disable записывают значение и вызывают Refresh; прямое присваивание публичного поля перерисовки не даёт.
- Button: OnTapped — обработчик клика, Importance (с 1.4) выделяет главное действие, Disable/Enable временно запрещают нажатия.
- Entry: OnChanged срабатывает посимвольно, OnSubmitted — по Enter; многострочный NewMultiLineEntry и NewPasswordEntry — готовые варианты ввода; валидатор появится в 18-м уроке.
- Check — булев выбор (NewCheck с func(bool)), Select — выбор строки (Selected, SetSelected, SetOptions, PlaceHolder «(Select one)» по умолчанию), RadioGroup — один вариант из многих.
- ProgressBar рисует долю от Min..Max (по умолчанию 0..1) через SetValue, текст — TextFormatter; без известного процента честнее NewProgressBarInfinite.
Домашнее задание
Собери окно «настройки приложения»: Label-заголовок с Bold-стилем, Entry для имени пользователя с PlaceHolder, NewPasswordEntry для токена, Check «запускать при старте» и Select с тремя вариантами темы. Добавь ProgressBar и кнопку «Сохранить» с HighImportance: по клику она должна Disable, напечатать значения полей и вернуть Enable через пару секунд по таймеру time.AfterFunc из горутины — но трогать виджеты внутри горутины пока не смей, только печатай в консоль (fyne.Do — тема двадцатого урока). Поэкспериментируй: закомментируй SetImportance, попробуй button.Text = "..." вместо SetText и наблюдай, что изменится на экране, а что нет.
Личные заметкиПусто
Вопросы и ответы по уроку
Почему виджет не обновился, когда я поменял его поле напрямую?
Поля у виджетов публичные, поэтому присваивание компилируется, но Refresh никто не вызвал — рендерер рисует старое состояние. Используй сеттеры: SetText вместо присваивания Text, SetValue вместо Value, SetChecked вместо Checked — они обновляют экран сами.
Чем OnChanged отличается от OnSubmitted у Entry?
OnChanged вызывается на каждое промежуточное изменение текста — посимвольно при вводе. OnSubmitted срабатывает по нажатию Enter: у однострочного поля — сразу, у многострочного — Shift+Enter. Проверки «на каждый символ» живёт в OnChanged, финальная отправка — в OnSubmitted.
Что показывает Select, пока ничего не выбрано?
Плейсхолдер. По умолчанию это «(Select one)», свой текст ставится в поле PlaceHolder. Сброс выбора через ClearSelected или SetSelected с пустой строкой возвращает плейсхолдер и вызывает обработчик с пустой строкой — учитывай это в коде.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
Cobra: CLI-приложения на Go
Интерактивный курс Cobra — фреймворка для CLI на Go: команды, флаги, конфиги Viper, автодополнение. 25 уроков с примерами кода, тестами и итоговым приложением.
- Создавать CLI-приложения с деревом команд на Cobra
- Работать с аргументами, флагами и их привязкой к конфигу
- Управлять конфигурацией через Viper: файлы, env, приоритеты
- Писать хуки PreRun/PostRun и корректно обрабатывать ошибки