Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 14 из 25 ~16 мин +50 XP

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

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

Содержание урока · 10 разделов

У любого зрелого приложения одни и те же действия доступны тремя путями: кнопкой на экране, командой в меню и горячей клавишей. Меню — путь для команд, которые должны быть всегда находимы, но не обязаны всегда занимать экран: сохранить, экспортировать, открыть настройки, выйти. В 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?

Анатомия пункта: 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 не даст по нему промахнуться.

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

Прочитайте код и выберите, что он выведет:

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-компоновке

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?

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

Третий жанр — контекстное меню, всплывающее там, где кликнули. Событие правого клика в 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?

Прочитайте код и выберите, что он выведет:

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

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

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

fileMenu := fyne.("Файл", fyne.("Открыть...", func() {}), ) window.(fyne.NewMainMenu(fileMenu)) widget.ShowPopUpMenuAtPosition(menu, window.Canvas(), e.)

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

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

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Каталог 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 из шестого урока различает модификаторы, а системное меню добавит свои стандартные пункты само.

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

Меню и тулбар выкрашиваются активной темой — как её настроить и сделать свою, расскажет следующий, пятнадцатый урок; там же поймёшь, почему пункты меню перекрашиваются сами, а твои примитивы — нет. Иконки для тулбара и пунктов — это ресурсы: встроенный набор темы, свои PNG через embed и утилита fyne bundle — территория шестнадцатого урока. Подтверждения для опасных команд берутся из тринадцатого урока про диалоги, события правого клика и горячие клавиши — из шестого, Border-компоновка под тулбар — из четвёртого, а виджеты-кнопки с иконками — из третьего. Каркас приложения с app.New и ShowAndRun — во втором; полный маршрут курса — на странице Fyne. Дальше по программе — привязка данных в семнадцатом уроке: пункты меню с Checked — отличные кандидаты на живую связь с настройками, но пока ты управляешь ими руками.

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

Термин

fyne.NewMenu / fyne.NewMenuItem

нажмите, чтобы перевернуть
Определение

NewMenu(label, items...) собирает меню из заголовка и пунктов; NewMenuItem(label, action) — пункт с действием. Меню — данные: поля можно менять на ходу и звать Refresh.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Виртуальный экран

HTML-модель виджет-дерева fyne
Меню и тулбарыfyne · мок

menu.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. Проверь, что пункт «Открыть» реально включается и выключается на живом приложении, а галочка автосохранения перерисовывается сразу после клика, без перезапуска.

Личные заметкиПусто
Хранятся только в вашем браузере (Fyne · Меню и тулбары)

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

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

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

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

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

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

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

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

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

Следующая ступень учебного пути

Cobra25 уроков~10 чпрактика25 уроков уже доступны

Cobra: CLI-приложения на Go

Интерактивный курс Cobra — фреймворка для CLI на Go: команды, флаги, конфиги Viper, автодополнение. 25 уроков с примерами кода, тестами и итоговым приложением.

  • Создавать CLI-приложения с деревом команд на Cobra
  • Работать с аргументами, флагами и их привязкой к конфигу
  • Управлять конфигурацией через Viper: файлы, env, приоритеты
  • Писать хуки PreRun/PostRun и корректно обрабатывать ошибки