---
title: "События и обработчики"
description: "Доставка ввода в Fyne: OnTapped и правая кнопка, TypedRune и TypedKey, фокус canvas, OnChanged и OnSubmitted, горячие клавиши через CustomShortcut."
course: "Fyne: десктопные и мобильные GUI на Go"
url: "https://golangmaster.ru/courses/fyne/lessons/06-sobytiya/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

# События и обработчики

*Курс «Fyne: десктопные и мобильные GUI на Go», урок 6 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/fyne/lessons/06-sobytiya/*

Кнопка из третьего урока умела одно: звать твою функцию по клику. Сегодня — fyne события: что происходит между щелчком мыши и вызовом OnTapped, как получить правую кнопку, клавиатуру через TypedRune и TypedKey, фокус и горячие клавиши. Без этих обработчиков fyne не написать ничего интерактивного: даже форма пятого урока держится на Submit и Cancel. Запускай примеры на своей машине командой go run . — события по-настоящему ловятся только в живом окне.

До сих пор твоя программа реагировала на мир одной функцией: колбэком кнопки. Пора заглянуть под капот. Когда пользователь нажимает кнопку мыши, операционная система сообщает об этом драйверу 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 события: интерфейс решает, кто слушает, твоя функция — что делать. Кнопка из третьего урока оказалась не особым случаем, а одним из многих слушателей, — и это правило ты используешь в восьмом уроке, когда соберёшь свой первый виджет.

### Плитка с левой и правой кнопкой

```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"
)

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 | виджеты со встроенными комбинациями |

**Вопрос:** Как получить событие правой кнопки мыши на своём объекте?

- Реализовать fyne.Mouseable с методом MouseMoved
- ✅ Реализовать fyne.Tappable — правая кнопка вызывает TappedSecondary
- Подписаться на window.SetOnMouseClicked
- Ни как: правая кнопка зарезервирована системой

Правый клик — часть интерфейса fyne.Tappable: метод TappedSecondary получает тот же PointEvent, что и Tapped. Mouseable про наведение и движение, отдельного обработчика мыши у окна нет, а правая кнопка свободна для приложения.

### Клавиатура: 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 на уровне окна

```go
package 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. По каким каналам пойдут события?

- Все три нажатия — через TypedKey
- ✅ Буквы «о» и «к» — через TypedRune, Enter — через TypedKey
- Все три — через TypedRune, Enter дублируется в Tapped
- Буквы — TypedRune, Enter никому не доставляется

Печатные символы производят текст и идут в TypedRune; Enter текст не производит и управляет поведением, поэтому приходит в TypedKey с Name равным fyne.KeyReturn. Каналы дополняют друг друга, а не дублируют.

### OnChanged и OnSubmitted

```go
entry := widget.NewEntry()
entry.OnChanged = func(s string) {
        println("изменилось:", s)
}
entry.OnSubmitted = func(s string) {
        println("отправлено:", s)
}
// пользователь печатает «го» и жмёт Enter
```

**Что выведет программа?**

- изменилось: го, отправлено: го
- ✅ изменилось: г, изменилось: го, отправлено: го
- отправлено: го
- изменилось: го

OnChanged срабатывает на каждую правку содержимого: после первой буквы «г», после второй «го». OnSubmitted — один раз, на Enter с текущим значением. На этой паре держится живая валидация из пятого урока: валидатор дёргается на каждый OnChanged, а отправка ждёт OnSubmitted.

### Горячие клавиши: 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 на новую заметку

```go
package 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. Проверить себя легко: если окно перестало отзываться на клики — где-то в обработчике живёт долгий цикл.

**Вопрос:** Окно замерло и не отвечает на клики. Самая вероятная причина из этого урока?

- Забыли вызвать window.ShowAndRun()
- ✅ В каком-то обработчике события выполняется долгая работа без горутины
- Слишком много виджетов в контейнере Grid
- Обработчик SetOnTypedKey зарегистрирован дважды

Обработчики событий исполняются в цикле главного потока: долгая функция в OnTapped блокирует и отрисовку, и ввод. Лечение — вынести работу в горутину и вернуться к интерфейсу через fyne.Do; подробности в двадцатом уроке.

### Допиши слушателя кликов

Восстанови три пропуска: метод левого клика, назначение TappedSecondary и доступ к canvas окна.

```go
// левый клик по плитке
func (t *tapRect) ___(ev *fyne.PointEvent) {
        t.taps++
        t.Refresh()
}

// ___ кнопка мыши
func (t *tapRect) TappedSecondary(ev *fyne.PointEvent) {
        println("правый клик")
}

// перехват клавиш на уровне окна
window.___().SetOnTypedKey(func(ev *fyne.KeyEvent) {
        if ev.Name == fyne.KeyEscape {
                window.Close()
        }
})
```

Подсказка: Левый клик — Tapped, правый — TappedSecondary, а canvas окна достаётся через window.Canvas().

Правильные ответы в пропусках: `Tapped`, `правая`, `Canvas`

### Путь клика мышью

Расставь по порядку, что происходит между щелчком мыши и перерисовкой плитки.

Правильный порядок строк:

1. `операционная система сообщает драйверу Fyne о нажатии кнопки мыши`
2. `драйвер ищет самый верхний CanvasObject под курсором в дереве содержимого`
3. `драйвер проверяет type assertion: реализует ли объект fyne.Tappable`
4. `объекту вызывается Tapped с координатами в fyne.PointEvent`
5. `внутри Tapped меняется состояние и вызывается Refresh`
6. `рендерер перерисовывает объект в ближайшем кадре`

> **Честно о запуске кода.** Клики и клавиатура не проходят через браузерную песочницу курса: события этого урока живут только в настоящем окне на твоей машине. Запусти примеры по второму уроку и понажимай всё сам — правую кнопку, стрелки, Ctrl+N; в онлайн-редакторе этот материал не оживить.

### Что дальше: чем рисует рендерер

Событие получено, состояние изменено, Refresh позван — но что именно рисует рендерер? Внутри любого виджета лежат примитивы пакета canvas: прямоугольники, круги, линии и текст. Седьмой урок — [Canvas: примитивы рисования](/courses/fyne/lessons/07-canvas/) — научит красить экран напрямую и станет фундаментом для собственного виджета из восьмого. Если нужно освежить, [как ведут себя Label и Entry](/courses/fyne/lessons/03-vidzhety/), — вернись к третьему уроку; [раскладку контейнерами Box](/courses/fyne/lessons/04-box/) напомнит четвёртый, а [формы с валидацией](/courses/fyne/lessons/05-grid-form/) — пятый, они сегодня встречались в каждом примере. События — половина кастомного виджета, вторая половина — рисование: именно их соединение в восьмом уроке даст тебе контрол с нуля. Полный маршрут из двадцати пяти уроков — [на странице курса](/courses/fyne/). В домашнем задании ты прикрутишь клавиатуру и горячие клавиши к форме входа из прошлого урока.

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

- **fyne.Tappable** — Интерфейс кликов: Tapped — левая кнопка, TappedSecondary — правая; аргумент fyne.PointEvent несёт координаты в окне и внутри объекта. Реализовать может любой CanvasObject.
- **TypedRune и TypedKey** — Два канала клавиатуры: TypedRune приносит печатный символ с учётом раскладки, TypedKey — физическую клавишу (стрелки, Escape, Enter) в fyne.KeyEvent с полем Name.
- **Фокус** — Ссылка на объект, которому доставляется клавиатура. Focusable даёт FocusGained и FocusLost; управление — canvas.Focus, canvas.Unfocus, canvas.Focused; Tab ходит по полям формы.
- **SetOnTypedKey** — Глобальный перехватчик клавиш на canvas окна: получает нажатия, пока окно активно, независимо от фокуса. Пара SetOnTypedRune ловит печатные символы на уровне окна.
- **CustomShortcut и AddShortcut** — Своя комбинация: fyne.CustomShortcut{KeyName, Modifier} плюс window.Canvas().AddShortcut(shortcut, handler). Готовые ShortcutCopy и ShortcutPaste Entry обрабатывает сам.
- **OnChanged и OnSubmitted** — События Entry: OnChanged срабатывает на каждую правку текста, OnSubmitted — один раз на Enter. На них держится живая валидация и отправка по Enter.

## Коротко: что запомнить

- За кликами стоит интерфейс 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.
