Содержание урока · 10 разделов
- Анатомия пункта: fyne.MenuItem по полям
- Меню — данные: обновление на живом приложении
- Тулбар: быстрые команды под рукой
- Контекстное меню по правому клику
- Каталог API меню и тулбара
- Хорошие манеры: меню, тулбар и горячие клавиши
- Что дальше: краска и значки
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
У любого зрелого приложения одни и те же действия доступны тремя путями: кнопкой на экране, командой в меню и горячей клавишей. Меню — путь для команд, которые должны быть всегда находимы, но не обязаны всегда занимать экран: сохранить, экспортировать, открыть настройки, выйти. В fyne меню — это данные, а не разметка: fyne.Menu хранит заголовок и список пунктов, каждый пункт — fyne.MenuItem с подписью и функцией-действием. Собирается всё фабриками fyne.NewMenu и fyne.NewMenuItem, а место в интерфейсе меню получает через window.SetMainMenu — главное меню окна. Дальше тулкит сам решает, как это показать: на Windows и Linux отрисуется строка меню в верхней части окна, на macOS пункты уедут в нативную системную панель вверху экрана, на мобильных появится кнопка меню, открывающая список. Твой код один и тот же — различия платформ fyne берёт на себя. Это тот же принцип, что и у остального тулкита с самого первого урока: ты описываешь структуру и поведение, платформу не трогаешь.
Главное меню минимального блокнота
GOpackage 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?
Анатомия пункта: fyne.MenuItem по полям#
Пункт меню — обычная структура с говорящими полями, и полезно знать их все. Label — подпись; Action func() — действие по клику, которое вызывается в момент выбора пункта. Icon fyne.Resource добавляет иконку слева от подписи — откуда брать ресурсы, шестнадцатый урок. Disabled приглушает пункт и отключает реакцию на клик: команда неприменима, пока не открыт документ. Checked рисует галочку — так делают переключатели вроде «Автосохранение»; поле ты меняешь сам и сам зовёшь menu.Refresh(), чтобы галочка перерисовалась. IsSeparator превращает пункт в разделитель, и для него есть фабрика fyne.NewMenuItemSeparator. IsQuit помечает пункт, завершающий приложение. Shortcut (с версии 2.2) хранит fyne.Shortcut для горячей клавиши — подпись о ней покажет, но надёжный триггер ты вешаешь сам через AddShortcut на канвасе, как в шестом уроке; не рассчитывай на автоматическое срабатывание. Наконец, ChildMenu — вложенное меню, подменю: пункт становится папкой, по клику раскрывается список его детей. Все поля мутируемые — меню это данные, и их можно менять на ходу, о чём ниже.
Подменю, галочки и недоступные пункты
GOpackage 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 не даст по нему промахнуться.
Что произойдёт по клику на «Плагины»?
Прочитайте код и выберите, что он выведет:
plugins := fyne.NewMenuItem("Плагины", nil)
plugins.Disabled = true
fileMenu := fyne.NewMenu("Файл", plugins)
// пользователь кликает по пункту «Плагины» в открытом менюМеню — данные: обновление на живом приложении#
Раз меню — структура, его можно менять в любой момент работы: добавить пункт после успешной загрузки плагина, включить «Сохранить», когда появился документ, поставить галочку у активного режима. Правило то же, что и с виджетами: после изменения поля пункта или списка Items вызови menu.Refresh() — и все места, где меню уже отрисовано, перечитают данные. У fyne.Menu и fyne.MainMenu есть свои методы Refresh с версии 2.2, а радикальный путь — снова позвать window.SetMainMenu с новой структурой. Практический приём: держи пункты как поля структуры приложения — openItem, saveItem, prefsItem — и управляй их флагами Disabled и Checked из бизнес-логики. Меню перестаёт быть статичной картинкой и становится проекцией состояния: недоступный документ — серый «Сохранить», включённый тёмный режим — галочка у соответствующего пункта «Вида». Данные первичны, экран догоняет — тот же договор, что у списка с updateItem из десятого урока.
Пункты меню собрались в приложении давно, но бизнес-логика только что изменила поля нескольких MenuItem. Как показать изменения?
Тулбар: быстрые команды под рукой#
Второй жанр команд — 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-компоновке
GOpackage 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?
Контекстное меню по правому клику#
Третий жанр — контекстное меню, всплывающее там, где кликнули. Событие правого клика в fyne — метод TappedSecondary из интерфейса fyne.SecondaryTappable: на десктопе это правая кнопка мыши, на мобильных — долгий тап; обычный левый клик живёт в парном Tapped из шестого урока. Клик ты ловишь на своём объекте — у примитивов интерфейса нет, поэтому берётся либо виджет, либо маленький кастомный контрол на BaseWidget, как учил восьмой урок. Показывает меню функция widget.ShowPopUpMenuAtPosition(menu, canvas, position): она создаёт всплывающее окно-оверлей поверх интерфейса, а закрывается оно само — по выбору пункта или клику мимо. Позицию бери из события: e.AbsolutePosition даёт координаты относительно всего канваса, и меню откроется точно под курсором. Есть и синоним widget.ShowPopUpMenuAtRelativePosition(menu, canvas, rel, to) — для позиций, посчитанных относительно конкретного объекта. Контекстные меню и диалоги-подтверждения из тринадцатого урока хорошо работают в паре: пункт «Удалить» в попапе открывает dialog.ShowConfirm.
Правый клик открывает своё меню
GOpackage 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?
Прочитайте код и выберите, что он выведет:
func (p *pane) TappedSecondary(e *fyne.PointEvent) {
widget.ShowPopUpMenuAtPosition(menu, p.window.Canvas(), e.Position)
}Собери меню и контекстный вызов
Восстанови четыре пропуска: фабрика меню, фабрика пункта, назначение главного меню и показ контекстного меню.
Путь клика по пункту главного меню
Расставь по порядку, что происходит от клика по строке меню до выполнения команды.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Каталог 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 — территория шестнадцатого урока. Подтверждения для опасных команд берутся из тринадцатого урока про диалоги, события правого клика и горячие клавиши — из шестого, Border-компоновка под тулбар — из четвёртого, а виджеты-кнопки с иконками — из третьего. Каркас приложения с app.New и ShowAndRun — во втором; полный маршрут курса — на странице Fyne. Дальше по программе — привязка данных в семнадцатом уроке: пункты меню с Checked — отличные кандидаты на живую связь с настройками, но пока ты управляешь ими руками.
Термины урока
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Виртуальный экран
HTML-модель виджет-дерева fynemenu.NewMenu рисуется в шапке окна ОС
Экран собран: «Меню и тулбары»
Коротко: что запомнить
- Меню в 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 для аккуратного отступа.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
Cobra: CLI-приложения на Go
Интерактивный курс Cobra — фреймворка для CLI на Go: команды, флаги, конфиги Viper, автодополнение. 25 уроков с примерами кода, тестами и итоговым приложением.
- Создавать CLI-приложения с деревом команд на Cobra
- Работать с аргументами, флагами и их привязкой к конфигу
- Управлять конфигурацией через Viper: файлы, env, приоритеты
- Писать хуки PreRun/PostRun и корректно обрабатывать ошибки