Содержание урока · 10 разделов
- Анатомия Dialog: show, hide и модальность
- Обещание второго урока: выход с подтверждением
- Форма и выбор файла внутри диалога
- Тосты и лёгкие уведомления
- Каталог API диалогов и уведомлений
- Диалоги в архитектуре приложения
- Что дальше: меню и инструментальные панели
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Необратимое действие без вопроса — это баг, а не UX: удаление заметки, выход без сохранения, очистка архива. Fyne собирает все такие вопросы в одном пакете fyne.io/fyne/v2/dialog, и у него есть три кита. Информация: dialog.ShowInformation — окно с сообщением и одной кнопкой «понятно». Вопрос: dialog.ShowConfirm — сообщение и две кнопки, ответ приходит колбэком func(bool): true — подтверждение, false — отказ. Ошибка: dialog.ShowError — текст err с иконкой сбоя, чтобы пользователь увидел проблему, а не гадал по молчанию. Все три привязаны к родительскому окну — последний аргумент fyne.Window, без него fyne диалог не построить: оверлей поднимается именно над этим окном. Запросы «fyne диалоги» и «fyne dialog» ведут сюда — к этим трём вызовам. Сразу фиксируем главную механику, незнакомую пришедшим из блокирующих GUI: вызов show не ждёт ответа — он показывает оверлей и возвращает управление мгновенно, а твоя логика продолжается в колбэке после решения пользователя. Разберём это на коде.
Информация и подтверждение
GOpackage main
import (
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/widget"
)
func main() {
myApp := app.New()
window := myApp.NewWindow("Заметки")
notes := []string{"молоко", "хлеб", "сыр"}
list := widget.NewLabel("молоко, хлеб, сыр")
info := widget.NewButton("О приложении", func() {
dialog.ShowInformation("Заметки 2.0",
"Учебное приложение курса Fyne.", window)
})
wipe := widget.NewButton("Очистить всё", func() {
dialog.ShowConfirm("Очистить всё",
"Удалить все заметки без возврата?",
func(ok bool) {
if ok {
notes = nil
list.SetText("")
}
}, window)
})
window.SetContent(container.NewVBox(list, info, wipe))
window.ShowAndRun()
}ShowInformation поднимает оверлей с текстом и одной кнопкой — колбэка у него нет, информация не требует ответа. ShowConfirm получает колбэк func(bool): нажатие подтверждения вызовет его с true и очистит срез, отказ — с false и ничего не сделает. Оба вызова короткие сокращения: под ними те же диалоги, которые создаются вручную через NewInformation и NewConfirm с последующим Show.
Что попадает в колбэк dialog.ShowConfirm?
Анатомия Dialog: show, hide и модальность#
Сокращения Show* скрывают двухшаговую механику. dialog.NewInformation, dialog.NewConfirm и dialog.NewError создают объект диалога и ничего не показывают; Show() выводит его на экран. Зачем разделять? Чтобы настроить: SetDismissText меняет текст нижней кнопки — «Понятно», «Закрыть», «Не сейчас»; SetOnClosed вешает уборку на момент закрытия диалога, когда она нужна независимо от ответа. Hide() убирает диалог программно — так делают диалоги с собственной логикой закрытия. И главное свойство: модальность. Диалог поднимается оверлеем над родительским окном и перехватывает весь ввод, пока открыт; кликнуть по кнопке под оверлеем нельзя. Но модальность блокирует только окно — не программу: run loop из второго урока крутится, таймеры тикают, горутины работают, и код после вызова show уже выполняется. Отсюда железное правило: логика продолжения пишется в колбэке, а не строкой после show. Кнопки и подписи локализуются под язык системы в свежих версиях — но рассчитывать на конкретные подписи не стоит: если текст кнопки важен, задавай его SetDismissText явно.
В каком порядке напечатается
Прочитайте код и выберите, что он выведет:
fmt.Println("до")
dialog.ShowConfirm("Удалить", "Точно?",
func(ok bool) {
fmt.Println("ответ:", ok)
}, window)
fmt.Println("после")
// пользователь нажимает «подтвердить» через три секунды
// что и в каком порядке напечатается?Создали, но не показали
Прочитайте код и выберите, что он выведет:
d := dialog.NewConfirm("Выход",
"Выйти без сохранения?",
func(ok bool) { if ok { window.Close() } },
window)
fmt.Println("диалог создан")
// Show() никто не вызвал
// что увидит пользователь?Обещание второго урока: выход с подтверждением#
Второй урок показал SetCloseIntercept и пообещал развёрнутый сценарий с настоящим диалогом подтверждения. Время платить. Перехватчик закрытия окна полностью берёт закрытие на себя: без вызова window.Close() окно не закроется. Собираем конвейер: пользователь жмёт крестик → SetCloseIntercept вызывает dialog.ShowConfirm → пользователь отвечает → в колбэке true закрываем окно явным window.Close(), в false просто отпускаем диалог — и окно остаётся жить. Обрати внимание на порядок: window.Close() внутри колбэка — не рекурсия и не конфликт с перехватчиком; перехватчик срабатывает на запрос закрытия, а Close() выполняет его по решению пользователя. Тот же конвейер встречается повсюду: удаление записи через dialog.ShowConfirm из двенадцатого урока, очистка корзины, выход из аккаунта. Общее правило оформления: короткий вопрос, честная кнопка отказа и никакой логики удаления вне колбэка — она будет выполнена в любом случае, включая отказ.
Выход без потери заметки
GOpackage main
import (
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/widget"
)
func main() {
myApp := app.New()
window := myApp.NewWindow("Редактор")
editor := widget.NewMultiLineEntry()
editor.SetPlaceHolder("Текст заметки...")
window.SetCloseIntercept(func() {
dialog.ShowConfirm("Выход",
"Заметка не сохранена. Всё равно выйти?",
func(ok bool) {
if ok {
window.Close()
}
}, window)
})
window.SetContent(editor)
window.ShowAndRun()
}Исполнение обещания второго урока: крестик больше не закрывает окно напрямую — перехватчик задаёт confirm dialog, и только подтверждение приводит к window.Close(). Отказ снимает оверлей, редактор остаётся открыт. Хочешь не спрашивать каждый раз — проверяй editor.Text на пустоту в перехватчике и закрывай сразу, когда терять нечего.
SetCloseIntercept задан и вызывает ShowConfirm. Что нужно, чтобы окно всё-таки закрылось?
Форма и выбор файла внутри диалога#
Диалог — не обязательно вопрос с двумя кнопками. dialog.NewForm собирает полноценную форму из []*widget.FormItem, знакомых по пятому уроку: заголовок, тексты кнопок подтверждения и отмены, элементы формы и колбэк onSubmit, который вызовется по подтверждению после автоматической проверки валидации полей — то самое правило «невалидное поле блокирует submit». Это готовый экран «настройки импорта» или «новая категория» без отдельного окна. Вторая рабочая лошадка — файловые диалоги: dialog.NewFileOpen создаёт системный выбор файла, колбэк получает пару (fyne.URIReadCloser, error): дескриптор чтения с методом URI() или nil при отмене. Фильтр ставится picker.SetFilter(storage.NewExtensionFileFilter([]string{".txt"})) — в списке останутся только текстовые файлы. У URI читаются Scheme(), Name() и Extension() — схема, имя и расширение файла; полный путь даёт String(). Пара важностей: отмена выбора — это reader nil и err nil, не ошибка; а ошибка реального сбоя приходит во втором аргументе и заслуживает ShowError. Папки выбирает dialog.NewFolderOpen с колбэком (fyne.ListableURI, error) — приём тот же. Полное погружение в файловое хранилище — сохранение, папки, ListableURI — двадцать первый урок, здесь только открытие.
Импорт заметки из файла
GOpackage main
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/storage"
"fyne.io/fyne/v2/widget"
)
func main() {
myApp := app.New()
window := myApp.NewWindow("Импорт")
status := widget.NewLabel("Файл не выбран")
editor := widget.NewMultiLineEntry()
choose := widget.NewButton("Импорт из .txt...", func() {
picker := dialog.NewFileOpen(func(reader fyne.URIReadCloser, err error) {
if err != nil {
dialog.ShowError(err, window)
return
}
if reader == nil {
status.SetText("Отмена")
return
}
defer reader.Close()
status.SetText("Выбрано: " + reader.URI().Name())
}, window)
picker.SetFilter(storage.NewExtensionFileFilter([]string{".txt"}))
picker.Show()
})
window.SetContent(container.NewVBox(choose, status, editor))
window.ShowAndRun()
}Двухшаговый путь в деле: NewFileOpen собирает диалог, SetFilter оставляет только .txt, Show открывает. Колбэк различает три исхода: ошибка уходит в ShowError, отмена — reader nil без ошибки, успех — читаем URI и не забываем Close через defer. Показ системного окна выбора — работа драйвера: на каждой платформе диалог выглядит по-своему, контракт колбэка общий.
Пользователь закрыл окно выбора файла кнопкой отмены. Что придёт в колбэк NewFileOpen?
Тосты и лёгкие уведомления#
Диалоги громкие: оверлей, модальность, кнопка. А «сохранено», «скопировано», «задача перенесена» — сообщения, на которые не нужен ответ и тем более блокировка. Для них с версии 2.6 в комплекте есть тост: widget.NewToast собирает всплывающую плашку, Show показывает её на пару секунд поверх интерфейса, без кнопок и без модальности — пользователь продолжает работать. До 2.6 тосты собирали руками: стек поверх содержимого, Label внутри, time.AfterFunc на три секунды вызывает Hide — рабочий приём, если целевая версия старше. Когда что выбирать: тост — для фактов, не требующих решения; ShowInformation — для сводок, которые пользователь обязан заметить и закрыть; ShowConfirm — для развилок с последствиями; ShowError — для сбоев с текстом ошибки. Ошибка дублирования каналов: если каждый чих программы всплывает диалогом, пользователь перестаёт их читать — держи диалоги для дорогих решений, тосты для дешёвых подтверждений. В финальном проекте так и разделено: перенос задачи — тост, удаление задач — confirm dialog, сбой сохранения — ShowError. И помни про фокус: тост не забирает фокус и не останавливает ввод, а диалог забирает — не открывай его в ответ на каждое действие пользователя, иначе через неделю он будет бить кнопки по памяти, не читая текст.
Собери подтверждение и импорт
Восстанови пять пропусков: сокращение подтверждения, точечное обновление списка, конструктор выбора файла, чтение URI и показ диалога.
Путь подтверждённого удаления
Расставь по порядку, что происходит от клика по кнопке «Удалить» до обновления списка.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Каталог API диалогов и уведомлений#
| API | Что делает | Когда звать |
|---|---|---|
| dialog.ShowInformation(title, msg, window) | уведомление с одной кнопкой | факты, которые нужно заметить |
| dialog.ShowConfirm(title, msg, func(bool), window) | вопрос с подтверждением и отказом | необратимые действия |
| dialog.ShowError(err, window) | ошибка с текстом err и иконкой сбоя | любой пойманный err, важный пользователю |
| New* + Show() + SetDismissText / SetOnClosed | двухшаговый путь с настройкой | нужен свой текст кнопки или уборка |
| dialog.NewForm / ShowForm | форма из FormItem внутри диалога | мини-настройки без отдельного окна |
| dialog.NewFileOpen(callback, window) | системный выбор файла | импорт данных |
| picker.SetFilter(storage.NewExtensionFileFilter) | фильтр по расширениям | перед Show файлового диалога |
| fyne.URIReadCloser и URI().Name() | дескриптор чтения и имя файла | в колбэке после выбора |
| widget.Toast (2.6+) | тост без кнопок и модальности | дешёвые подтверждения действий |
Диалогам из этого урока нужен настоящий экран — в браузерной песочнице курса GUI не исполняется. Собери примеры по второму уроку и обязательно проверь три исхода подтверждения: обе кнопки и закрытие диалога без выбора — отказ не всегда кнопка, и логика удаления обязана оставаться внутри колбэка.
Диалоги в архитектуре приложения#
Последний обход по архитектуре. Диалог — не место для бизнес-логики: в колбэке живут одна-две строки, которые зовут методы твоих структур, а не разворачивают сценарии на сто строк; иначе confirm dialog превращается в нечитаемую лапшу замыканий. Повторные вызовы Show на одном диалоге допустимы: собери NewConfirm один раз при старте окна и переиспользуй — SetText и Show по мере надобности. Следи за наследованием фокуса: модальность перехватывает ввод всего окна, поэтому диалог поверх диалога не соберётся в очередь — второй вызов Show заменит картинку, и колбэки могут разминуться с ожиданиями. Проверяй сценарий «отказ» так же придирчиво, как «подтверждение»: предсказание выше показывало, что ответ приходит позже, — значит, и отказ приходит позже, а код между show и колбэком уже отработал. Наконец, чисти за собой SetOnClosed: если диалог открывал соединение или подписку, закрывай их там, а не в колбэке подтверждения — закрытие без кнопки тоже должно освобождать ресурсы. Дальше по курсу — меню и тулбары: у них своя механика команд, но правило «дорогие решения через диалог, дешёвые через тост» остаётся. Полный маршрут — на странице курса Fyne.
Что дальше: меню и инструментальные панели#
Диалоги закрыли пробел навигации из двенадцатого урока: теперь перехват закрытия вкладки и окна умеет спрашивать, импорт поднимает файловый выбор, а финальный менеджер задач получил все три канала сообщений. Опора урока — второй урок с SetCloseIntercept и run loop, десятый с RefreshItem, пятый с формами и валидацией, двенадцатый с DocTabs. Следующая тема — меню и тулбары: MainMenu с иерархией команд, Toolbar с кнопками-иконками и контекстные меню по правой кнопке из шестого урока; там появится и место для вызова наших диалогов — команды «Файл → Импорт» и «Правка → Очистить». Домашнее задание соберёт безопасную корзину заметок с подтверждением удаления и импортом из файла.
Термины урока
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Виртуальный экран
HTML-модель виджет-дерева fyneУдалить файл?
Подтверждение
dialog.ShowConfirm показал бы модальное окно
Экран собран: «Диалоги»
Коротко: что запомнить
- Пакет dialog даёт три кита: ShowInformation — уведомление, ShowConfirm — вопрос с func(bool), ShowError — сбой с текстом err; все привязаны к родительскому окну.
- Show не блокирует программу: оверлей поднимается, код после вызова выполняется, ответ приходит в колбэк — логика продолжения живёт только в колбэке.
- Модальность блокирует окно, но не run loop; диалог поверх диалога не складывается в очередь — не строй сценарии из вложенных Show.
- Двухшаговый путь New* + Show оставляет окно для SetDismissText и SetOnClosed; Hide закрывает программно.
- SetCloseIntercept + ShowConfirm + window.Close() в колбэке — честный выход с подтверждением, обещанный вторым уроком.
- Файловый выбор: NewFileOpen с колбэком (fyne.URIReadCloser, error), фильтр через storage.NewExtensionFileFilter, отмена — nil без ошибки; глубина — двадцать первый урок.
- Тосты widget.Toast с 2.6 — для дешёвых подтверждений; диалоги — для дорогих решений и ошибок.
Домашнее задание
Собери корзину заметок: список из десятого урока, кнопка «Удалить» под выделенной строкой вызывает dialog.ShowConfirm, и только колбэк с true удаляет запись из среза и перерисовывает список RefreshItem-ом — код удаления вне колбэка считается багом. Добавь SetCloseIntercept на окно: при непустом тексте редактора — ShowConfirm с вопросом о выходе, при подтверждении window.Close. Кнопка «Импорт .txt» открывает NewFileOpen с фильтром storage.NewExtensionFileFilter: ошибка уходит в ShowError, отмена молча игнорируется, содержимое файла попадает в editor. Сведи каналы сообщений по правилу урока: успешный импорт — ShowInformation, сбой чтения — ShowError, удаление — confirm dialog. Ловушка для самопроверки: поставь печать между show и колбэком и убедись, что она выполняется раньше ответа пользователя; если ждёшь обратного — перечитай предсказание про порядок вывода.
Личные заметкиПусто
Вопросы и ответы по уроку
ShowConfirm останавливает выполнение программы?
Нет: блокируется только ввод родительского окна — оверлей перехватывает клики. Программа живёт: run loop крутится, код после show выполняется сразу, а ответ пользователя приходит в колбэк, когда он примет решение.
Можно показать диалог без родительского окна?
Нет: последний аргумент каждого диалога — fyne.Window: fyne окно диалога всегда привязано к родителю, оверлей поднимается над ним. Всплывающие подсказки и легковесные плашки без окна собираются тостами или оверлеями из Stack, но настоящий диалог всегда привязан к окну.
Что вернёт колбэк, если пользователь закрыл confirm-диалог кнопкой отмены?
false — отказ выражается не только кнопкой: любое расставание с диалогом без подтверждения означает false. Поэтому код удаления пишется строго внутри ветки if ok, а не строкой после show.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
Cobra: CLI-приложения на Go
Интерактивный курс Cobra — фреймворка для CLI на Go: команды, флаги, конфиги Viper, автодополнение. 25 уроков с примерами кода, тестами и итоговым приложением.
- Создавать CLI-приложения с деревом команд на Cobra
- Работать с аргументами, флагами и их привязкой к конфигу
- Управлять конфигурацией через Viper: файлы, env, приоритеты
- Писать хуки PreRun/PostRun и корректно обрабатывать ошибки