Содержание урока · 11 разделов
- OnTapped: кнопка — это интерфейс
- Семейство интерфейсов ввода
- Интерфейсы ввода корневого пакета fyne
- Клавиатура: TypedRune и TypedKey
- Фокус: кому идёт клавиатура
- Горячие клавиши: Shortcut и AddShortcut
- Где живёт твой обработчик: главный поток
- Что дальше: чем рисует рендерер
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
До сих пор твоя программа реагировала на мир одной функцией: колбэком кнопки. Пора заглянуть под капот. Когда пользователь нажимает кнопку мыши, операционная система сообщает об этом драйверу Fyne, тот ищет, над каким объектом стоит курсор, проверяет, умеет ли объект слушать клики, и вызывает нужный метод с координатами. Вся обработка ввода в тулките построена на маленьких интерфейсах Go — Tappable, Draggable, Focusable, Shortcutable, — и это отличная новость: хочешь события — реализуй методы, никакой магии и регистрации в глобальных реестрах. Сегодня разберём клики и правую кнопку, семейство интерфейсов ввода, клавиатуру с фокусом, горячие клавиши и события текстового поля. Материал сквозной: он понадобится и в седьмом уроке про canvas, и в восьмом — про собственные виджеты, где интерфейсы событий станут твоим главным инструментом. И сразу договорённость о практике: каждый листинг урока собран так, чтобы его можно было скопировать в main.go и запустить без правок — проверяй на своей машине после каждого блока.
OnTapped: кнопка — это интерфейс#
Начнём с того, что уже знаешь. У widget.Button есть поле OnTapped — функция без аргументов, которую кнопка вызывает по клику; widget.NewButton(text, fn) — просто сахар над ним. Но кнопка не особенная: за кликами стоит интерфейс fyne.Tappable с двумя методами. Tapped(fyne.PointEvent) приходит по нажатию левой кнопки мыши, TappedSecondary(fyne.PointEvent) — правой. Реализовать интерфейс может любой CanvasObject: драйвер на каждом клике находит верхний объект под курсором и проверяет его через type assertion. Аргумент события — fyne.PointEvent с двумя координатами: AbsolutePosition указывает точку в системе окна, RelativePosition — внутри самого объекта. Для кнопки координаты не нужны, а для своей интерактивной плитки — самое то: клик в левую половину и клик в правую могут значить разное. Вот так работают все fyne события: интерфейс решает, кто слушает, твоя функция — что делать. Кнопка из третьего урока оказалась не особым случаем, а одним из многих слушателей, — и это правило ты используешь в восьмом уроке, когда соберёшь свой первый виджет.
Плитка с левой и правой кнопкой
GOpackage main
import (
"image/color"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
)
type tapRect struct {
*canvas.Rectangle
taps int
}
func (t *tapRect) MinSize() fyne.Size {
return fyne.NewSize(220, 130)
}
func (t *tapRect) Tapped(ev *fyne.PointEvent) {
t.taps++
if t.taps%2 == 1 {
t.FillColor = color.NRGBA{R: 46, G: 204, B: 113, A: 255}
} else {
t.FillColor = color.NRGBA{R: 231, G: 76, B: 60, A: 255}
}
t.Refresh()
println("клик №", t.taps, "в точке", ev.RelativePosition.String())
}
func (t *tapRect) TappedSecondary(*fyne.PointEvent) {
println("правая кнопка по плитке")
}
func main() {
myApp := app.New()
window := myApp.NewWindow("Плитка")
plate := &tapRect{Rectangle: canvas.NewRectangle(
color.NRGBA{R: 231, G: 76, B: 60, A: 255},
)}
window.SetContent(container.NewCenter(plate))
window.ShowAndRun()
}tapRect встраивает canvas.Rectangle и добавляет три метода: MinSize возвращает фиксированный размер, поэтому Center растянет плитку до 220 на 130 (переопределение MinSize нахрапом — приём этого урока; канонический путь через BaseWidget покажет восьмой). Tapped чередует цвет и зовёт Refresh, TappedSecondary срабатывает на правую кнопку. Драйвер сам проверил интерфейсы — мы нигде не регистрировали обработчики.
Семейство интерфейсов ввода#
Tappable — не единственный слушатель. У тулкита есть компактное семейство интерфейсов, и каждый виджет выбирает себе нужные. Перетаскивание — fyne.Draggable с методами Dragged(fyne.DragEvent) и DragEnd: событие тащит новые координаты, а DragEnd сообщает, что пользователь отпустил объект. Колесо мыши — fyne.Scrollable с единственным методом Scrolled(fyne.ScrollEvent), у которого есть DeltaX и DeltaY. Наведение и движение — fyne.Mouseable: MouseIn, MouseMoved и MouseOut дают сделать hover-эффекты и всплывающие подсказки. Клавиатура — fyne.Focusable, о нём отдельно ниже. Комбинации клавиш — fyne.Shortcutable с методом TypedShortcut. Все эти интерфейсы собраны в корневом пакете fyne, и драйвер проверяет их тем же type assertion, что и Tappable. Так работает вся система: никаких наследников и переопределений — только интерфейсы Go и честная утиная типизация. Проектировать такие контракты мы подробно разбирали в продвинутом курсе, а здесь достаточно уметь их читать: увидел в описании виджета интерфейс — значит, умеет такие-то события.
Интерфейсы ввода корневого пакета fyne#
| Интерфейс | Методы | Кто им пользуется |
|---|---|---|
| fyne.Tappable | Tapped, TappedSecondary | кнопки и любые кликабельные объекты |
| fyne.Draggable | Dragged, DragEnd | перетаскиваемые плитки, слайдеры |
| fyne.Scrollable | Scrolled | списки и прокручиваемые области |
| fyne.Mouseable | MouseIn, MouseMoved, MouseOut | hover-эффекты и подсказки |
| fyne.Focusable | FocusGained, FocusLost, TypedRune, TypedKey | Entry и всё, что принимает ввод |
| fyne.Shortcutable | TypedShortcut | виджеты со встроенными комбинациями |
Как получить событие правой кнопки мыши на своём объекте?
Клавиатура: TypedRune и TypedKey#
Клавиатура приходит двумя каналами, и перепутать их — классическая ошибка. Канал первый — TypedRune(r rune): печатный символ после раскладки и модификаторов. Пользователь нажал Ш с зажатым Shift на русской раскладке — в TypedRune придёт символ «Ш»; переключил раскладку — придёт другой символ. Всё, что превращается в текст, ловится здесь. Канал второй — TypedKey(*fyne.KeyEvent): физическая клавиша без привязки к раскладке. У события есть поле Name со значениями fyne.KeyUp, fyne.KeyDown, fyne.KeyEscape, fyne.KeyReturn, fyne.KeySpace и другими константами пакета. Стрелки, Escape, Enter и функциональные клавиши — территория TypedKey: текста они не производят, зато управляют навигацией. Правило запоминания простое: текст — TypedRune, навигация и системные клавиши — TypedKey. Перехватить нажатия для всего окна можно на canvas: window.Canvas().SetOnTypedRune(fn) и window.Canvas().SetOnTypedKey(fn). Так устроены обработчики fyne: интерфейс или canvas решает, кто слушает, твоя функция — что делать. Глобальный перехватчик окна получает нажатия независимо от того, стоит ли фокус в поле ввода, — поэтому аккорды вроде Escape работают из любого места.
Стрелки и Escape на уровне окна
GOpackage main
import (
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/widget"
)
func main() {
myApp := app.New()
window := myApp.NewWindow("Стрелки")
pos := [2]int{0, 0}
status := widget.NewLabel("0, 0")
window.Canvas().SetOnTypedKey(func(ev *fyne.KeyEvent) {
switch ev.Name {
case fyne.KeyUp:
pos[1]++
case fyne.KeyDown:
pos[1]--
case fyne.KeyLeft:
pos[0]--
case fyne.KeyRight:
pos[0]++
case fyne.KeyEscape:
window.Close()
return
default:
return
}
status.SetText(fmt.Sprintf("%d, %d", pos[0], pos[1]))
})
window.SetContent(status)
window.ShowAndRun()
}Обработчик на canvas окна получает нажатия, пока окно активно. ev.Name сравнивается с константами fyne.KeyName: четыре стрелки меняют счётчики, Escape закрывает окно. Не забывай default с выходом — иначе текст будет перестраиваться от каждой лишней клавиши. Поле живёт в замыкании, поэтому позиция сохраняется между нажатиями.
Фокус: кому идёт клавиатура#
Клавиатурные события адресные. Прежде чем буква попадёт в глобальный перехватчик, тулкит спрашивает: у кого фокус? Фокус — это ссылка на один объект, которому доставляются TypedRune и TypedKey; у объекта, реализующего fyne.Focusable, есть ещё FocusGained и FocusLost — моменты включения и выключения подсветки. Entry — фокусируемый виджет: клик по полю даёт ему фокус, дальше буквы идут в его TypedRune, а Tab ходит по полям формы из пятого урока — тулкит строит цепочку автоматически. Управлять фокусом из кода можно через canvas: canvas.Focus(obj), canvas.Unfocus() и canvas.Focused() — текущий владелец. Если делаешь свой объект, который должен принимать ввод, реализуй Focusable и получи фокус по клику через Tapped — так делают списки, которые слушают стрелки, и свои поля ввода. Разница между глобальным перехватчиком и фокусом принципиальна для проектирования: SetOnTypedKey на canvas — это аккорды уровня окна, Focusable — адресный ввод конкретному элементу. Восемнадцатый урок добавит фокусу ещё одну роль: ошибки валидации рисуются у поля, когда оно теряет фокус, поэтому реакция на FocusLost — половина UX любой формы. И ещё один практический совет: если поле должно пустить дальше только заполненный ввод, проверь его в OnSubmitted, а не в глобальном перехватчике — событие адресное, и код читается локально.
Пользователь печатает «ок» и жмёт Enter. По каким каналам пойдут события?
OnChanged и OnSubmitted
Прочитайте код и выберите, что он выведет:
entry := widget.NewEntry()
entry.OnChanged = func(s string) {
println("изменилось:", s)
}
entry.OnSubmitted = func(s string) {
println("отправлено:", s)
}
// пользователь печатает «го» и жмёт EnterГорячие клавиши: Shortcut и AddShortcut#
События клавиш — низкий уровень: сравнивать ev.Name с константами вручную удобно, пока комбинаций мало. Для настоящих аккордов у тулкита есть fyne.Shortcut — интерфейс, описывающий комбинацию, и готовые реализации: ShortcutCopy, ShortcutPaste, ShortcutCut, ShortcutSelectAll. Их Entry и текстовые виджеты обрабатывают сами — Ctrl+C и Ctrl+V работают из коробки, регистрировать их не нужно. Свои комбинации описываются литералом fyne.CustomShortcut: поле KeyName — клавиша, поле Modifier — модификаторы fyne.ControlModifier, fyne.AltModifier, fyne.ShiftModifier или fyne.SuperModifier. Регистрация — на canvas окна: window.Canvas().AddShortcut(shortcut, handler). Отсюда видно, как единообразно устроены fyne события: есть объект-событие, есть обработчик, есть место регистрации. Уровнем ниже живёт fyne.ShortcutHandler — маленький диспетчер, который хранит пары «шорткат — функция»; виджеты, реализующие Shortcutable, используют его внутри, а тебе обычно хватает AddShortcut на canvas. Комбинация сначала предлагается фокусированному виджету — так Entry успевает перехватить свои копирование и вставку, — а глобальные обработчики canvas срабатывают из любого места окна. Выбирая клавиши для своих аккордов, не наступай на стандартные: Ctrl+C и Ctrl+V уже заняты текстовыми полями, а Ctrl+Q многие пользователи ждут как выход. Пара модификаторов Shift и буква — хороший резерв для действий с выделением, Alt — для навигации, Ctrl — для главных действий окна.
Ctrl+N на новую заметку
GOpackage main
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/widget"
)
func main() {
myApp := app.New()
window := myApp.NewWindow("Заметки")
out := widget.NewLabel("Ctrl+N — новая заметка")
ctrlN := fyne.CustomShortcut{
KeyName: fyne.KeyN,
Modifier: fyne.ControlModifier,
}
window.Canvas().AddShortcut(ctrlN, func(fyne.Shortcut) {
out.SetText("создана заметка")
})
window.SetContent(out)
window.ShowAndRun()
}CustomShortcut собирается из клавиши и модификатора, AddShortcut на canvas окна регистрирует обработчик fyne.Shortcut — при желании из аргумента можно достать ShortcutName(). Ctrl+N теперь работает в любом месте окна, даже когда фокус стоит в поле. Стандартные пары копирования и вставки регистрировать не нужно: Entry обрабатывает их сам.
Где живёт твой обработчик: главный поток#
Все события из этого урока — Tapped, TypedKey, OnChanged — приходят в твои функции из цикла событий, то есть из главного потока GUI. Пока обработчик работает, интерфейс не рисует и не принимает ввод: поставь внутрь Tapped цикл на три секунды — и окно замрёт до конца цикла. Правило простое: обработчик меняет состояние и зовёт Refresh, а долгую работу — чтение файла, сетевой запрос, тяжёлый расчёт — выноси в горутину. Но из чужой горутины трогать виджеты нельзя: тулкит требует возвращаться в главный поток, и с версии 2.6.0 для этого есть fyne.Do — функция, ставящая твой код в очередь главного потока. Полный разбор многопоточности с примерами и граблями — двадцатый урок курса, там же узнаешь, как это устроено внутри драйвера. Сейчас достаточно запомнить пропорцию: событие → быстрый обработчик → Refresh; всё, что дольше пары миллисекунд, — в горутину и обратно через fyne.Do. Проверить себя легко: если окно перестало отзываться на клики — где-то в обработчике живёт долгий цикл.
Окно замерло и не отвечает на клики. Самая вероятная причина из этого урока?
Допиши слушателя кликов
Восстанови три пропуска: метод левого клика, назначение TappedSecondary и доступ к canvas окна.
Путь клика мышью
Расставь по порядку, что происходит между щелчком мыши и перерисовкой плитки.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Клики и клавиатура не проходят через браузерную песочницу курса: события этого урока живут только в настоящем окне на твоей машине. Запусти примеры по второму уроку и понажимай всё сам — правую кнопку, стрелки, Ctrl+N; в онлайн-редакторе этот материал не оживить.
Что дальше: чем рисует рендерер#
Событие получено, состояние изменено, Refresh позван — но что именно рисует рендерер? Внутри любого виджета лежат примитивы пакета canvas: прямоугольники, круги, линии и текст. Седьмой урок — Canvas: примитивы рисования — научит красить экран напрямую и станет фундаментом для собственного виджета из восьмого. Если нужно освежить, как ведут себя Label и Entry, — вернись к третьему уроку; раскладку контейнерами Box напомнит четвёртый, а формы с валидацией — пятый, они сегодня встречались в каждом примере. События — половина кастомного виджета, вторая половина — рисование: именно их соединение в восьмом уроке даст тебе контрол с нуля. Полный маршрут из двадцати пяти уроков — на странице курса. В домашнем задании ты прикрутишь клавиатуру и горячие клавиши к форме входа из прошлого урока.
Термины урока
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Виртуальный экран
HTML-модель виджет-дерева fyneОжидаю событие…
Экран собран: «События»
Коротко: что запомнить
- За кликами стоит интерфейс fyne.Tappable: Tapped для левой кнопки, TappedSecondary для правой, оба получают fyne.PointEvent с координатами.
- Любой CanvasObject может слушать события — достаточно реализовать методы интерфейса: Tappable, Draggable, Scrollable, Mouseable, Focusable или Shortcutable.
- Клавиатура идёт двумя каналами: TypedRune — печатные символы с учётом раскладки, TypedKey — физические клавиши с fyne.KeyName.
- Фокус определяет, кому доставляется клавиатура: canvas.Focus, canvas.Unfocus и canvas.Focused управляют им, FocusGained и FocusLost сообщают о смене.
- Горячие клавиши — fyne.CustomShortcut с модификаторами и window.Canvas().AddShortcut; стандартные копирование и вставку Entry обрабатывает сам.
- OnChanged срабатывает на каждую правку Entry, OnSubmitted — на Enter; их пара обеспечивает живую валидацию и отправку.
- Обработчики живут в главном потоке GUI: долгая работа выносится в горутину с возвратом через fyne.Do — иначе окно замрёт.
Домашнее задание
Возьми форму входа из пятого урока и научи её реагировать на клавиатуру: Enter в поле логина должен переносить фокус в поле пароля через canvas.Focus — подумай, где ловить Enter, в OnSubmitted поля или в SetOnTypedKey, и сравни оба варианта. Escape пусть чистит оба поля. Затем зарегистрируй Ctrl+S как fyne.CustomShortcut: обработчик печатает «сохранено» в метку внизу окна и должен работать, даже когда фокус стоит в поле. Наконец, сделай свой Tappable-квадрат на canvas.Rectangle: левый клик увеличивает счётчик на метке, правый — сбрасывает его. Проверь, что Tab ходит по полям формы, что горячие клавиши не конфликтуют со вставкой текста и что быстрый двойной клик не ломает счётчик.
Личные заметкиПусто
Вопросы и ответы по уроку
Почему клик по пустому месту окна ничего не вызывает?
События адресные: драйвер ищет объект под курсором и проверяет его интерфейсы. У обычного контейнера нет Tappable, поэтому клик в пустоту никуда не доставляется. Нужны клики по фону — реализуй fyne.Tappable на корневом объекте-обёртке, и он станет слушателем.
TypedRune и TypedKey вызываются оба на каждую клавишу?
Это два разных события с разными данными: печатный символ приносит TypedRune, физическую клавишу — TypedKey. На одну букву могут прийти оба, но у них разные роли: текст собирается из TypedRune, а навигация и системные клавиши — территория TypedKey. В коде обычно слушают один нужный канал.
Горячие клавиши работают одинаково на macOS?
Модификаторы задаёшь ты: fyne.ControlModifier — это Ctrl, fyne.SuperModifier — Command. Стандартные операции копирования и вставки Fyne отображает по-платформенному сам, а для своих шорткатов выбирай модификатор осознанно, чтобы не заставлять пользователей мака жать Ctrl.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
Cobra: CLI-приложения на Go
Интерактивный курс Cobra — фреймворка для CLI на Go: команды, флаги, конфиги Viper, автодополнение. 25 уроков с примерами кода, тестами и итоговым приложением.
- Создавать CLI-приложения с деревом команд на Cobra
- Работать с аргументами, флагами и их привязкой к конфигу
- Управлять конфигурацией через Viper: файлы, env, приоритеты
- Писать хуки PreRun/PostRun и корректно обрабатывать ошибки