---
title: "Меню и тулбары"
description: "Меню в Fyne: fyne.NewMenu и главное меню окна, подменю через ChildMenu, виджет Toolbar, контекстные меню по правому клику и живое обновление пунктов."
course: "Fyne: десктопные и мобильные GUI на Go"
url: "https://golangmaster.ru/courses/fyne/lessons/14-menju/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 16
---

# Меню и тулбары

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

Кнопки и списки отвечают на прямые действия, но взрослому приложению нужны команды, спрятанные до востребования: файл, правка, справка. Запрос «fyne меню» — это семейство fyne.Menu и fyne.MenuItem: fyne.NewMenu собирает меню, window.SetMainMenu поднимает его в главное меню окна, а на десктопе fyne menu превращается в настоящую строку меню, на macOS — в системную панель. Рядом живут toolbar fyne-тулбары из widget.NewToolbar — ряд иконок-действий под заголовком окна — и контекстные меню по правому клику. Сегодня соберём все три жанра команд и научимся обновлять их на живом приложении.

У любого зрелого приложения одни и те же действия доступны тремя путями: кнопкой на экране, командой в меню и горячей клавишей. Меню — путь для команд, которые должны быть всегда находимы, но не обязаны всегда занимать экран: сохранить, экспортировать, открыть настройки, выйти. В fyne меню — это данные, а не разметка: fyne.Menu хранит заголовок и список пунктов, каждый пункт — fyne.MenuItem с подписью и функцией-действием. Собирается всё фабриками fyne.NewMenu и fyne.NewMenuItem, а место в интерфейсе меню получает через window.SetMainMenu — главное меню окна. Дальше тулкит сам решает, как это показать: на Windows и Linux отрисуется строка меню в верхней части окна, на macOS пункты уедут в нативную системную панель вверху экрана, на мобильных появится кнопка меню, открывающая список. Твой код один и тот же — различия платформ fyne берёт на себя. Это тот же принцип, что и у остального тулкита с самого первого урока: ты описываешь структуру и поведение, платформу не трогаешь.

### Главное меню минимального блокнота

```go
package main

import (
        "fyne.io/fyne/v2"
        "fyne.io/fyne/v2/app"
)

func main() {
        myApp := app.New()
        window := myApp.NewWindow("Блокнот")

        open := fyne.NewMenuItem("Открыть...", func() {})
        save := fyne.NewMenuItem("Сохранить", func() {})
        quit := fyne.NewMenuItem("Выход", func() { myApp.Quit() })
        quit.IsQuit = true

        window.SetMainMenu(fyne.NewMainMenu(
                fyne.NewMenu("Файл", open, save, fyne.NewMenuItemSeparator(), quit),
                fyne.NewMenu("Справка", fyne.NewMenuItem("О программе", func() {})),
        ))
        window.ShowAndRun()
}
```

fyne.NewMenu принимает заголовок и произвольное число пунктов; fyne.NewMenuItem — подпись и функцию, которая вызовется по клику. fyne.NewMenuItemSeparator вставляет разделитель, а пометка IsQuit у пункта «Выход» говорит тулкиту, что этот пункт завершает приложение — на macOS он встраивается в системное меню приложения. fyne.NewMainMenu собирает список меню, и window.SetMainMenu назначает его окну. На Windows и Linux запусти — над содержимым окна появится строка «Файл Справка»; на macOS то же самое откроется в системной панели.

**Вопрос:** Где пользователь увидит главное меню, назначенное через window.SetMainMenu, на macOS?

- В верхней части каждого окна приложения — как на Windows
- ✅ В системной строке меню вверху экрана, как у нативных приложений macOS
- Нигде: на macOS главное меню не поддерживается
- Только по правому клику в любом месте окна

На macOS fyne передаёт главное меню нативной системной панели вверху экрана, поэтому пункты живут там, где у обычных маковских программ. На Windows и Linux строка меню рисуется в верхней части окна. Твой код при этом одинаков — различие полностью на стороне драйвера.

### Анатомия пункта: fyne.MenuItem по полям

Пункт меню — обычная структура с говорящими полями, и полезно знать их все. Label — подпись; Action func() — действие по клику, которое вызывается в момент выбора пункта. Icon fyne.Resource добавляет иконку слева от подписи — откуда брать ресурсы, шестнадцатый урок. Disabled приглушает пункт и отключает реакцию на клик: команда неприменима, пока не открыт документ. Checked рисует галочку — так делают переключатели вроде «Автосохранение»; поле ты меняешь сам и сам зовёшь menu.Refresh(), чтобы галочка перерисовалась. IsSeparator превращает пункт в разделитель, и для него есть фабрика fyne.NewMenuItemSeparator. IsQuit помечает пункт, завершающий приложение. Shortcut (с версии 2.2) хранит fyne.Shortcut для горячей клавиши — подпись о ней покажет, но надёжный триггер ты вешаешь сам через AddShortcut на канвасе, как в шестом уроке; не рассчитывай на автоматическое срабатывание. Наконец, ChildMenu — вложенное меню, подменю: пункт становится папкой, по клику раскрывается список его детей. Все поля мутируемые — меню это данные, и их можно менять на ходу, о чём ниже.

### Подменю, галочки и недоступные пункты

```go
package main

import (
        "fmt"

        "fyne.io/fyne/v2"
        "fyne.io/fyne/v2/app"
        "fyne.io/fyne/v2/widget"
)

func exportReport(format string) {
        fmt.Println("экспортирую:", format)
}

func main() {
        myApp := app.New()
        window := myApp.NewWindow("Отчёты")
        status := widget.NewLabel("Готово")

        export := fyne.NewMenuItem("Экспортировать как...", nil)
        export.ChildMenu = fyne.NewMenu("Экспортировать как...",
                fyne.NewMenuItem("PDF", func() { exportReport("pdf") }),
                fyne.NewMenuItem("HTML", func() { exportReport("html") }),
        )

        autosave := fyne.NewMenuItem("Автосохранение", func() {
                autosave.Checked = !autosave.Checked
                fileMenu.Refresh()
        })
        autosave.Checked = true

        plugins := fyne.NewMenuItem("Плагины", nil)
        plugins.Disabled = true

        fileMenu := fyne.NewMenu("Файл", export, autosave, fyne.NewMenuItemSeparator(), plugins)

        window.SetMainMenu(fyne.NewMainMenu(fileMenu,
                fyne.NewMenu("Вид", fyne.NewMenuItem("Статусная строка", func() {
                        status.Hidden = !status.Hidden
                })),
        ))
        window.SetContent(status)
        window.ShowAndRun()
}
```

Подменю — это пункт с заполненным ChildMenu: клик по «Экспортировать как...» раскрывает второй уровень, где уже лежат пункты с Action. У autosave действие переворачивает Checked и зовёт fileMenu.Refresh() — без перерисовки галочка обновилась бы только при следующем открытии меню. plugins.Disabled = true делает пункт серым и нечувствительным к клику: Action у него nil, и fyne не даст по нему промахнуться.

### Что произойдёт по клику на «Плагины»?

```go
plugins := fyne.NewMenuItem("Плагины", nil)
plugins.Disabled = true
fileMenu := fyne.NewMenu("Файл", plugins)
// пользователь кликает по пункту «Плагины» в открытом меню
```

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

- Приложение упадёт: Action равен nil и вызывать нечего
- ✅ Ничего не произойдёт: пункт приглушён и не реагирует на клик
- Откроется пустое подменю
- Пункт исчезнет из меню после Refresh

Disabled-пункт рисуется приглушённым и не пропускает клик дальше: Action не вызывается, и nil-функция никуда не дёргается. Нюанс полезен и без Disabled: Action обязан быть не nil у обычных пунктов — фабрика NewMenuItem ждёт функцию, а вот пункту-подменю с ChildMenu действие не нужно вовсе.

### Меню — данные: обновление на живом приложении

Раз меню — структура, его можно менять в любой момент работы: добавить пункт после успешной загрузки плагина, включить «Сохранить», когда появился документ, поставить галочку у активного режима. Правило то же, что и с виджетами: после изменения поля пункта или списка Items вызови menu.Refresh() — и все места, где меню уже отрисовано, перечитают данные. У fyne.Menu и fyne.MainMenu есть свои методы Refresh с версии 2.2, а радикальный путь — снова позвать window.SetMainMenu с новой структурой. Практический приём: держи пункты как поля структуры приложения — openItem, saveItem, prefsItem — и управляй их флагами Disabled и Checked из бизнес-логики. Меню перестаёт быть статичной картинкой и становится проекцией состояния: недоступный документ — серый «Сохранить», включённый тёмный режим — галочка у соответствующего пункта «Вида». Данные первичны, экран догоняет — тот же договор, что у списка с updateItem из десятого урока.

**Вопрос:** Пункты меню собрались в приложении давно, но бизнес-логика только что изменила поля нескольких MenuItem. Как показать изменения?

- Перезапустить приложение — меню читается один раз при старте
- ✅ Вызвать menu.Refresh() у изменённого меню или заново window.SetMainMenu
- Ничего: поля пунктов меняют отрисовку мгновенно и сами
- Создать новый fyne.Menu и заменить поле внутри fyne.MainMenu — этого достаточно

Меню — данные, но отрисовка не следит за ними автоматически: после изменений зовут menu.Refresh() (методы Refresh есть у fyne.Menu и fyne.MainMenu с 2.2) или повторно SetMainMenu. Заменить Items внутри структуры и промолчать — типичная ошибка: данные изменились, экран нет.

### Тулбар: быстрые команды под рукой

Второй жанр команд — toolbar fyne-тулбар: горизонтальная полоса иконок-кнопок, которую обычно кладут в верхнюю ячейку Border-компоновки, сразу под заголовком окна. Собирается widget.NewToolbar из элементов-ToolbarItem; каждый реализует один метод ToolbarObject() fyne.CanvasObject — то, что показать в полосе. Готовых элементов три: widget.NewToolbarAction(иконка, функция) — кнопка, widget.NewToolbarSeparator() — тонкая линия-разделитель, widget.NewToolbarSeparator-близнец widget.NewToolbarSpacer() — пружина, прижимающая последующие элементы вправо, как layout.NewSpacer в четвёртом уроке. Кнопка внутри тулбара — обычный widget.Button с низкой важностью, поэтому у ToolbarAction с версии 2.2 есть SetIcon для смены иконки, а с 2.5 — Enable, Disable и Disabled для управления доступностью. Пополняется полоса на ходу: t.Append(item) добавляет в конец, t.Prepend(item) — в начало. И раз каждый элемент — просто объект с одним методом, в тулбар можно вписать что угодно, например подпись: объяви тип с встроенным *widget.Label и методом ToolbarObject — интерфейс будет доволен. Иконки для тулбара берутся из темы или из своих ресурсов — тема шестнадцатого урока перекрасит их сама.

### Тулбар редактора на Border-компоновке

```go
package main

import (
        "fyne.io/fyne/v2"
        "fyne.io/fyne/v2/app"
        "fyne.io/fyne/v2/container"
        "fyne.io/fyne/v2/theme"
        "fyne.io/fyne/v2/widget"
)

func main() {
        myApp := app.New()
        window := myApp.NewWindow("Редактор заметок")
        status := widget.NewLabel("Пустой документ")

        toolbar := widget.NewToolbar(
                widget.NewToolbarAction(theme.Icon(theme.IconNameDocumentCreate), func() {
                        status.SetText("Создан новый документ")
                }),
                widget.NewToolbarAction(theme.Icon(theme.IconNameFolderOpen), func() {
                        status.SetText("Открываем файл...")
                }),
                widget.NewToolbarAction(theme.Icon(theme.IconNameDocumentSave), func() {
                        status.SetText("Сохранено")
                }),
                widget.NewToolbarSeparator(),
                widget.NewToolbarAction(theme.Icon(theme.IconNameDelete), func() {
                        status.SetText("Документ удалён")
                }),
                widget.NewToolbarSpacer(),
                widget.NewToolbarAction(theme.Icon(theme.IconNameHelp), func() {
                        status.SetText("Справка")
                }),
        )

        window.SetContent(container.NewBorder(toolbar, status, nil, nil))
        window.ShowAndRun()
}
```

widget.NewToolbar собирает полосу из действий: NewToolbarAction получает иконку ресурсом из темы — theme.Icon(theme.IconName...) возвращает встроенный значок — и функцию. Разделитель рисует линию между группами, а Spacer перед «Справкой» прижимает её к правому краю. Тулбар отдан верхней ячейке NewBorder — та всегда занимает всю ширину и высоту по содержимому, поэтому полоса тянется ровно на ширину окна.

**Вопрос:** Чем widget.NewToolbarSpacer отличается от widget.NewToolbarSeparator?

- Ничем: оба рисуют вертикальную линию, различие только в ширине
- ✅ Spacer раздвигает элементы, прижимая последующие к правому краю, а Separator рисует линию-разделитель между группами
- Separator растягивается на всё свободное место, а Spacer рисует линию
- Spacer включает режим прокрутки тулбара на узких окнах

Спейсер — пружина из раскладок: занимает всё свободное место и уводит следующие элементы вправо, как layout.NewSpacer в боксе из четвёртого урока. Сепаратор — видимый штрих, отделяющий одну группу команд от другой. В типичном тулбаре слева стоят основные действия, затем Spacer, затем служебные.

### Контекстное меню по правому клику

Третий жанр — контекстное меню, всплывающее там, где кликнули. Событие правого клика в fyne — метод TappedSecondary из интерфейса fyne.SecondaryTappable: на десктопе это правая кнопка мыши, на мобильных — долгий тап; обычный левый клик живёт в парном Tapped из шестого урока. Клик ты ловишь на своём объекте — у примитивов интерфейса нет, поэтому берётся либо виджет, либо маленький кастомный контрол на BaseWidget, как учил восьмой урок. Показывает меню функция widget.ShowPopUpMenuAtPosition(menu, canvas, position): она создаёт всплывающее окно-оверлей поверх интерфейса, а закрывается оно само — по выбору пункта или клику мимо. Позицию бери из события: e.AbsolutePosition даёт координаты относительно всего канваса, и меню откроется точно под курсором. Есть и синоним widget.ShowPopUpMenuAtRelativePosition(menu, canvas, rel, to) — для позиций, посчитанных относительно конкретного объекта. Контекстные меню и диалоги-подтверждения из тринадцатого урока хорошо работают в паре: пункт «Удалить» в попапе открывает dialog.ShowConfirm.

### Правый клик открывает своё меню

```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"
        "fyne.io/fyne/v2/widget"
)

type pane struct {
        widget.BaseWidget
        rect   *canvas.Rectangle
        status *widget.Label
        window fyne.Window
}

func newPane(window fyne.Window, status *widget.Label) *pane {
        p := &pane{
                rect:   canvas.NewRectangle(color.NRGBA{R: 0x2c, G: 0x3e, B: 0x50, A: 0xff}),
                status: status,
                window: window,
        }
        p.ExtendBaseWidget(p)
        return p
}

func (p *pane) CreateRenderer() fyne.WidgetRenderer {
        return widget.NewSimpleRenderer(p.rect)
}

func (p *pane) Tapped(*fyne.PointEvent) {
        p.status.SetText("Обычный клик")
}

func (p *pane) TappedSecondary(e *fyne.PointEvent) {
        menu := fyne.NewMenu("",
                fyne.NewMenuItem("Скопировать цвет", func() { p.status.SetText("Цвет скопирован") }),
                fyne.NewMenuItem("Сбросить выделение", func() { p.status.SetText("Сброшено") }),
        )
        widget.ShowPopUpMenuAtPosition(menu, p.window.Canvas(), e.AbsolutePosition)
}

func main() {
        myApp := app.New()
        window := myApp.NewWindow("Контекстное меню")
        status := widget.NewLabel("Кликни правой кнопкой по синей области")

        window.SetContent(container.NewBorder(nil, status, nil, nil, newPane(window, status)))
        window.ShowAndRun()
}
```

pane — минимальный кастомный виджет: BaseWidget плюс рендерер из одного Rectangle, как в восьмом уроке. TappedSecondary реализует fyne.SecondaryTappable, поэтому правый клик по области доходит до метода. Меню у попапа заголовка не имеет — fyne.NewMenu с пустой строкой. e.AbsolutePosition — точка клика относительно канваса, и меню открывается ровно там; закрывается само по клику мимо или по выбору пункта.

### А если передать e.Position вместо AbsolutePosition?

```go
func (p *pane) TappedSecondary(e *fyne.PointEvent) {
        widget.ShowPopUpMenuAtPosition(menu, p.window.Canvas(), e.Position)
}
```

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

- Меню откроется в той же точке — позиции совпадают
- ✅ Меню откроется со смещением: e.Position отсчитывается от левого верхнего угла виджета, а не канваса
- Меню откроется в левом верхнем углу окна
- Функция вернёт ошибку о недопустимой позиции

У fyne.PointEvent две координаты: Position — относительно самого объекта, AbsolutePosition — относительно канваса. ShowPopUpMenuAtPosition ждёт канвасные координаты, поэтому относительная позиция даст смещение на то, где виджет стоит в окне. Если pane начинается не в нуле — а в Border-центре он обычно и не в нуле — меню уедет.

### Собери меню и контекстный вызов

Восстанови четыре пропуска: фабрика меню, фабрика пункта, назначение главного меню и показ контекстного меню.

```go
fileMenu := fyne.___("Файл",
        fyne.___("Открыть...", func() {}),
)
window.___(fyne.NewMainMenu(fileMenu))

widget.ShowPopUpMenuAtPosition(menu, window.Canvas(), e.___)
```

Подсказка: Меню собирает fyne.NewMenu, пункт — fyne.NewMenuItem, главное меню назначает SetMainMenu, а канвасная позиция клика — AbsolutePosition.

Правильные ответы в пропусках: `NewMenu`, `NewMenuItem`, `SetMainMenu`, `AbsolutePosition`

### Путь клика по пункту главного меню

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

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

1. `пользователь кликает по заголовку меню в строке меню окна`
2. `драйвер раскрывает список Items соответствующего fyne.Menu`
3. `пользователь наводит курсор на пункт — подсветка, серые Disabled не реагируют`
4. `клик по пункту: если у него заполнен ChildMenu, раскрывается подменю`
5. `иначе вызывается Action пункта — твоя функция выполняется`
6. `меню закрывается, фокус возвращается в окно`

### Каталог API меню и тулбара

| API | Что делает | Когда звать |
| --- | --- | --- |
| fyne.NewMenu(label, items...) | создаёт меню с заголовком и пунктами | при сборке структуры меню |
| fyne.NewMenuItem(label, action) | создаёт пункт с подписью и действием | там же; Action можно nil у подменю |
| fyne.NewMenuItemSeparator() | пункт-разделитель | между группами команд |
| window.SetMainMenu(fyne.NewMainMenu(...)) | назначает окну главное меню | один раз после создания окна |
| menu.Refresh() | перерисовывает меню после изменения данных | после правки полей пунктов или Items |
| widget.NewToolbar(items...) | полоса команд из ToolbarItem | верхняя ячейка Border-компоновки |
| widget.NewToolbarAction(icon, fn) | кнопка-иконка в тулбаре | основной элемент полосы |
| widget.NewToolbarSeparator() / NewToolbarSpacer() | линия между группами / пружина вправо | группировка и прижатие служебных команд |
| widget.ShowPopUpMenuAtPosition(menu, canvas, pos) | контекстное меню в точке канваса | из TappedSecondary по e.AbsolutePosition |

### Хорошие манеры: меню, тулбар и горячие клавиши

Правило трёх путей гласит: важное действие дублируется кнопкой, пунктом меню и горячей клавишей — все три зовут одну и ту же функцию, а не три копии кода. Отсюда практика: вынеси команды в методы структуры приложения — app.Save(), app.Open() — и навешивай их откуда угодно. В меню не сваливай всё подряд: оно для команд уровня документа и приложения, а для частых операций существует тулбар и панель инструментов справа. Заголовки держи короткими и глагольными: «Открыть...» с многоточием обещает диалог, «О программе» — окно информации. Пункт с тяжёлым или необратимым действием — «Удалить всё» — сначала подтверждается диалогом из тринадцатого урока. Доступность тоже на меню: Disabled-пункты сообщают пользователю, что команда существует, но сейчас неприменима, а Checked-переключатели не дублируют настройку в трёх местах. И помни про тон нативности: на macOS не перехватывай Ctrl в горячих клавишах — fyne.Shortcut из шестого урока различает модификаторы, а системное меню добавит свои стандартные пункты само.

> **Честно о запуске кода.** Меню и тулбарам нужен настоящий экран — в браузерной песочнице курса GUI не исполняется. Собери примеры по второму уроку и проверь поведение на своей платформе: строка меню на Windows и Linux, системная панель на macOS, правый клик для контекстного меню — всё это живёт только в реальном окне.

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

Меню и тулбар выкрашиваются активной темой — как её настроить и сделать свою, расскажет следующий, пятнадцатый урок; там же поймёшь, почему пункты меню перекрашиваются сами, а твои примитивы — нет. Иконки для тулбара и пунктов — это ресурсы: встроенный набор темы, свои PNG через embed и утилита fyne bundle — территория шестнадцатого урока. Подтверждения для опасных команд берутся из [тринадцатого урока про диалоги](/courses/fyne/lessons/13-dialogi/), события правого клика и горячие клавиши — из [шестого](/courses/fyne/lessons/06-sobytiya/), Border-компоновка под тулбар — из [четвёртого](/courses/fyne/lessons/04-box/), а виджеты-кнопки с иконками — из [третьего](/courses/fyne/lessons/03-vidzhety/). Каркас приложения с app.New и ShowAndRun — во [втором](/courses/fyne/lessons/02-pervoe-okno/); полный маршрут курса — [на странице Fyne](/courses/fyne/). Дальше по программе — привязка данных в семнадцатом уроке: пункты меню с Checked — отличные кандидаты на живую связь с настройками, но пока ты управляешь ими руками.

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

- **fyne.NewMenu / fyne.NewMenuItem** — NewMenu(label, items...) собирает меню из заголовка и пунктов; NewMenuItem(label, action) — пункт с действием. Меню — данные: поля можно менять на ходу и звать Refresh.
- **Главное меню окна** — window.SetMainMenu(fyne.NewMainMenu(...)). Десктоп Win/Linux — строка меню в окне, macOS — нативная системная панель, мобильные — кнопка меню со списком.
- **ChildMenu** — Поле MenuItem, превращающее пункт в подменю: клик раскрывает вложенное меню. Action у такого пункта обычно nil.
- **IsQuit, Disabled, Checked** — IsQuit помечает завершающий приложение пункт; Disabled делает его серым и нечувствительным; Checked рисует галочку — после смены звони menu.Refresh().
- **widget.NewToolbar** — Полоса ToolbarItem: NewToolbarAction(иконка, fn) — кнопка, Separator — линия, Spacer — пружина. Append/Prepend добавляют элементы, у Action есть SetIcon/Enable/Disable.
- **Контекстное меню** — TappedSecondary ловит правый клик (или долгий тап), widget.ShowPopUpMenuAtPosition(menu, canvas, e.AbsolutePosition) показывает попап, закрывающийся сам.

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

- Меню в fyne — данные: fyne.NewMenu собирает fyne.Menu, fyne.NewMenuItem — пункты с Label и Action.
- window.SetMainMenu поднимает главное меню: на Windows и Linux строка в окне, на macOS системная панель, на мобильных кнопка со списком.
- MenuItem умеет подменю через ChildMenu, галочку Checked, серый Disabled, разделитель и пометку IsQuit.
- После изменения пунктов зовут menu.Refresh() — у Menu и MainMenu методы Refresh появились в 2.2.
- widget.NewToolbar строит полосу команд из Action, Separator и Spacer; пополняется Append и Prepend.
- Контекстное меню: TappedSecondary ловит правый клик, ShowPopUpMenuAtPosition показывает попап по e.AbsolutePosition.
- Правило трёх путей: кнопка, пункт меню и горячая клавиша зовут одну функцию, а не три копии.

## Домашнее задание

Собери мини-редактор текста: центр — Entry из девятого урока, сверху тулбар из Border-компоновки, плюс главное меню. Тулбар: «Создать» очищает поле, «Сохранить» пишет статус, разделитель и справа через ToolbarSpacer кнопка «О программе». Меню «Файл»: Открыть (Disabled при пустом поле, включается после ввода текста через menu.Refresh), Автосохранение с галочкой Checked, которая переключается кликом, и Выход с IsQuit. В меню «Правка» добавь подменю «Регистр» с пунктами «В ВЕРХНИЙ» и «в нижний», преобразующими текст поля. Затем повесь контекстное меню на свой маленький виджет-панель с BaseWidget из восьмого урока: правый клик открывает попап «Статистика» со счётом символов, позиция — e.AbsolutePosition. Проверь, что пункт «Открыть» реально включается и выключается на живом приложении, а галочка автосохранения перерисовывается сразу после клика, без перезапуска.

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

### Можно ли показывать главное меню в нескольких окнах?

Да: SetMainMenu назначается конкретному окну, и каждое может иметь свою структуру. На macOS все меню уезжают в одну системную панель, и активной становится версия окна в фокусе — поэтому ключевые команды лучше держать одинаковыми во всех окнах.

### Почему не срабатывает горячая клавиша, записанная в MenuItem.Shortcut?

Поле Shortcut хранит привязку для отображения в меню, но надёжный триггер вешается программно: window.Canvas().AddShortcut с fyne.CustomShortcut, как в шестом уроке. Дублируй хоткей именно там, а в меню оставляй подпись.

### Как в тулбар добавить текст, а не только иконки?

ToolbarItem — интерфейс с единственным методом ToolbarObject() fyne.CanvasObject: объяви свой тип с встроенным *widget.Label и этим методом, и NewToolbar примет его наравне с кнопками. Между текстом и соседями стоит поставить Separator для аккуратного отступа.
