Содержание урока · 12 разделов
- Встроенные значки: каталог theme.IconName
- Каталог встроенных значков темы
- resource.NewStaticResource и StaticResource
- canvas.Image: три источника и режимы заполнения
- embed ресурсы: вшиваем файлы в бинарник
- fyne bundle: ассеты, превращённые в Go-код
- Тематические значки: theme.NewThemedResource
- Шрифты как ресурсы
- Что дальше
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Ресурс в fyne — не файл и не URL, а маленький интерфейс с двумя вопросами: как тебя зовут (Name() string) и что у тебя внутри (Content() []byte). Этого хватает всему тулкиту: рендерер по расширению из имени понимает формат — PNG, JPEG, SVG, TTF — а байты из Content просто рисует. Почему так, а не прямо путь к файлу? Потому что приложение fyne — один бинарник, который должен работать одинаково на Windows у бухгалтера и на Android у курьера: жёсткие пути к файлам живут своей жизнью, а ресурс отвязывает «что рисуем» от «где это лежит». Внутри ресурса может быть что угодно: байты, вшитые в бинарник при компиляции, файл, прочитанный в рантайме, ответ из сети, данные из хранилища приложения. Все места, где тулкит просит картинку, принимают fyne.Resource: иконка окна, значок кнопки, иконка пункта меню — обещание четырнадцатого урока — и шрифт из метода Font темы, о котором говорил пятнадцатый. Начнём с ресурса, который проще всего написать руками: статического, из файла на диске.
Иконка окна из файла в рантайме
GOpackage main
import (
"os"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
)
func main() {
myApp := app.New()
window := myApp.NewWindow("Паспорт")
data, err := os.ReadFile("assets/logo.png")
if err != nil {
fyne.LogError("логотип не найден", err)
}
icon := fyne.NewStaticResource("logo.png", data)
myApp.SetIcon(icon)
window.SetIcon(icon)
window.ShowAndRun()
}os.ReadFile возвращает байты, fyne.NewStaticResource заворачивает их в fyne.Resource с именем «logo.png». SetIcon у приложения задаёт значок в панели задач и доке, SetIcon у окна — в заголовке. Ошибка чтения не роняет программу: ресурс с пустым Content просто не нарисуется, но логировать её через fyne.LogError — правильная привычка.
Что входит в интерфейс fyne.Resource?
Встроенные значки: каталог theme.IconName#
Прежде чем рисовать свои значки, посмотри на шкаф с готовыми: тема приложения содержит десятки встроенных иконок, каждая под именем fyne.ThemeIconName — IconNameHome, IconNameContentAdd, IconNameDocumentSave и так далее. Получить ресурс можно хелпером theme.Icon(theme.IconNameHome) — он спросит активную тему за тебя, как theme.Padding() из пятнадцатого урока. Значки монохромные и тематические: тема раздаёт их уже перекрашенными под свою палитру, поэтому в тёмном режиме значок светлый, в светлом — тёмный, и никакой работы с твоей стороны не нужно. Применяются они везде, где тулкит ждёт fyne.Resource: в кнопке через widget.NewButtonWithIcon, в пункте меню через поле Icon у fyne.MenuItem, в тулбаре через widget.NewToolbarAction, во вкладках через NewTabItemWithIcon из двенадцатого урока. Диалоги тринадцатого урока рисуют свои заголовки этими же значками. Ниже — рабочий каталог самых ходовых имён: держи его под рукой, прежде чем рисовать свою шестерёнку.
Каталог встроенных значков темы#
| Константа | Значок | Типичное место |
|---|---|---|
| IconNameHome | домик | кнопка «на главную», навигация |
| IconNameContentAdd / ContentRemove | плюс и минус | добавить и удалить запись |
| IconNameContentCopy / ContentCut / ContentPaste | буфер обмена | правка в меню и тулбаре |
| IconNameContentUndo / ContentRedo | стрелки отмены | история правок |
| IconNameDocumentCreate / DocumentSave | документ и дискета | файловые операции |
| IconNameFolder / FolderOpen | закрытая и открытая папка | выбор каталога, файловые панели |
| IconNameSearch | лупа | поиск по спискам из десятого урока |
| IconNameDelete / Cancel / Confirm | корзина, крестик, галочка | деструктивные действия и подтверждения |
| IconNameMediaPlay / MediaPause / MediaStop | треугольник и палочки | плееры, таймеры, фоновые задачи |
| IconNameArrowBack / ArrowForward / ArrowUp / ArrowDown | стрелки | назад-вперёд, прокрутка |
| IconNameInfo / Error / Question | знаки состояний | диалоги и уведомления |
| IconNameSettings | шестерёнка | настройки приложения |
| IconNameMoreVertical / MoreHorizontal | три точки | кнопка «ещё», контекстные действия |
| IconNameCheckButtonChecked / Unchecked | галочка и пустой квадрат | чекбоксы |
Значки в кнопке, пункте меню и тулбаре
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("Значки")
home := widget.NewButtonWithIcon("Домой", theme.Icon(theme.IconNameHome), func() {})
save := fyne.NewMenuItem("Сохранить", func() {})
save.Icon = theme.Icon(theme.IconNameDocumentSave)
bar := widget.NewToolbar(
widget.NewToolbarAction(theme.Icon(theme.IconNameContentAdd), func() {}),
widget.NewToolbarAction(theme.Icon(theme.IconNameDelete), func() {}),
)
window.SetContent(container.NewVBox(home, bar))
window.ShowAndRun()
}theme.Icon возвращает ресурс из активной темы — кнопку он красит сам. Поле Icon у fyne.MenuItem присваивается после создания пункта, ровно как в четырнадцатом уроке: меню — это данные. ToolbarAction рисует значок без подписи, поэтому для тулбаров монохромные значки темы — основной жанр.
Как получить встроенный значок темы для кнопки?
resource.NewStaticResource и StaticResource#
Статический ресурс — байты, зафиксированные на момент создания. В пакете fyne.io/fyne/v2/resource живёт структура StaticResource с двумя полями: StaticName string и StaticContent []byte, плюс фабрика resource.NewStaticResource(name, content). Сокращение fyne.NewStaticResource делает то же самое — это удобная обёртка в корневом пакете, и в этом курсе мы пользуемся именно ей; длинная форма нужна в сгенерированном коде, о нём ниже. Когда статический ресурс из файла оправдан? Когда данные появляются или меняются в рантайме: пользователь выбрал аватарку, приложение скачало обновление каталога, логотип положили рядом с исполняемым файлом. Подводный камень один, но классический: относительный путь «assets/logo.png» отсчитывается от рабочей папки процесса, а не от папки с exe — запустишь ярлыком из другого каталога, и файл «исчезнет». Для данных, которые живут вместе с программой, надёжнее вшить их в бинарник — этим займёмся через пару блоков. А для фоновых проверок существования файла лучше storage-API из двадцать первого урока. Пока запомни главное: статический ресурс — это просто пара имя-плюс-байты, и создать его можно где угодно, хоть в горутине.
canvas.Image с источником-ресурсом
GOpackage main
import (
"os"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
)
func main() {
myApp := app.New()
window := myApp.NewWindow("Логотип")
data, _ := os.ReadFile("assets/logo.png")
logo := fyne.NewStaticResource("logo.png", data)
picture := canvas.NewImageFromResource(logo)
picture.FillMode = canvas.ImageFillContain
picture.SetMinSize(fyne.NewSize(240, 160))
window.SetContent(container.NewVBox(picture))
window.ShowAndRun()
}canvas.NewImageFromResource принимает fyne.Resource и рисует его содержимое. FillMode задаёт поведение в контейнере, SetMinSize — минимальную площадь: без неё изображение сжимается до размеров файла, и раскладке не за что уцепиться.
canvas.Image: три источника и режимы заполнения#
Любое fyne изображение в окне — это canvas.Image, примитив из седьмого урока. Источников у него три, в порядке приоритета: Resource fyne.Resource, Image image.Image — уже декодированный в памяти Go-образ — и File string с путём к файлу. Фабрики удобства просто прячут выбор: canvas.NewImageFromFile для диска, canvas.NewImageFromImage для готового image.Image, canvas.NewImageFromResource для ресурса, canvas.NewImageFromURI для URI из диалога выбора файла — она пригодится в двадцать первом уроке. За поведение в раскладке отвечает поле FillMode: canvas.ImageFillStretch растягивает под отведённый прямоугольник, возможно с искажением пропорций; canvas.ImageFillContain вписывает целиком, сохраняя пропорции и оставляя поля; canvas.ImageFillOriginal рисует в исходном пиксельном размере. Поле Translucency от 0 до 1 делает картинку полупрозрачной — водяные знаки и фоны под текстом. И не забывай правило седьмого урока: поля изображения хранят значения, а не подписки. Поменял Resource или FillMode — позови canvas.Refresh(img), иначе рендерер не узнает. Форматы: PNG, JPEG и GIF декодируются стандартной библиотекой, SVG fyne рисует собственным рендерером — и именно SVG открывает дорогу тематическим значкам, о них ниже.
Поменяли источник, забыли про перерисовку
Прочитайте код и выберите, что он выведет:
picture := canvas.NewImageFromFile("assets/day.png")
picture.Resource = fyne.NewStaticResource("night.png", nightRaw)
picture.FillMode = canvas.ImageFillOriginal
// canvas.Refresh(picture) не вызывали
// что увидит пользователь?embed ресурсы: вшиваем файлы в бинарник#
Самый надёжный способ раздать ассеты — вшить их в исполняемый файл ещё на компиляции. С Go 1.16 это делается директивой //go:embed прямо над переменной: байты файла или целого каталога становятся частью пакета, и единственный exe уезжает к пользователю со всем содержимым. Три правила директивы. Первое: файлы должны лежать внутри каталога пакета — подняться выше через «..» нельзя, абсолютные пути нельзя. Второе: каталоги, начинающиеся с точки или подчёркивания, не вшиваются — если папка ассетов не видна, проверь имя. Третье: переменная может быть и одной строкой с []byte, и embed.FS — виртуальной файловой системой для целого дерева; из неё байты достают методом ReadFile по пути относительно пакета. Дальше всё знакомо: ReadFile отдал байты — fyne.NewStaticResource упаковал — виджет нарисовал. Эти embed ресурсы компилятор контролирует за тебя: файла нет — сборка падает, и битый путь невозможно заметить только на проде. Именно так стоит вшивать логотипы, значки и шрифты, которые живут вместе с приложением.
Два способа embed: файл и каталог
GOpackage main
import (
"embed"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
//go:embed assets/star.svg
var starRaw []byte
//go:embed assets/icons
var iconsFS embed.FS
func main() {
myApp := app.New()
window := myApp.NewWindow("Embed")
star := fyne.NewStaticResource("star.svg", starRaw)
gearRaw, _ := iconsFS.ReadFile("assets/icons/gear.svg")
gear := fyne.NewStaticResource("gear.svg", gearRaw)
window.SetContent(container.NewVBox(
widget.NewButtonWithIcon("Звезда", star, func() {}),
widget.NewButtonWithIcon("Шестерёнка", gear, func() {}),
))
window.ShowAndRun()
}Одиночный файл вшит как []byte, каталог assets/icons — как embed.FS, из которого ReadFile достаёт каждый файл по пути. Оба значка стали fyne.Resource и живут в бинарнике: пользователю не нужно ничего, кроме самого exe. Имена ресурсов оставь с расширением — по нему определяется формат.
Какой путь допустим в директиве //go:embed?
fyne bundle: ассеты, превращённые в Go-код#
Когда файлов много, а держать рядом с кодом каталог ассетов не хочется, в дело вступает утилита fyne bundle: она принимает каталог или файл и генерирует Go-исходник с готовыми константами fyne.StaticResource. Запуск выглядит так: go run fyne.io/fyne/v2/cmd/fyne@latest bundle -o assets.go -pkg main assets/icons. Результат — файл assets.go, в котором каждый ассет стал переменной вида resourceAssetsIconsGearSvg со вшитыми байтами: импортируешь пакет — ресурсы уже там, никакой embed.FS и ReadFile не нужно. Это старший приём из тех же fyne ресурсы, и у него своя ниша: имя ресурса фиксируется при генерации, типизация — чистая переменная вместо строки, а на этапе сборки не требуются ни директивы, ни правило о каталоге пакета. Цена — перегенерация: изменил значок, запусти bundle заново, закоммить сгенерированный файл; продвинутые команды вешают это на make-цель рядом со сборкой. Выбор между embed и bundle — вопрос вкуса и размера ассет-парка: принципиально оба дают одно и то же — fyne.Resource с байтами внутри бинарника.
Тематические значки: theme.NewThemedResource#
Вот и обещание пятнадцатого урока: значки, перекрашивающиеся со сменой палитры. Встроенные значки темы так себя ведут потому, что внутри них — обёртка theme.NewThemedResource(res): ресурс-адаптер, который при отрисовке красит содержимое цветом активной темы. По умолчанию это ColorNameForeground, но поле ColorName можно переназначить: theme.ColorNamePrimary даст фирменный акцент, ColorNameDisabled — приглушённый вид, ColorNameError — тревожный. Есть и поле ColorAdjustment для тонких подстроек прозрачности. Честная оговорка: перекраска гарантированно рассчитана на SVG — тулкит перерисовывает вектор сам; PNG-файл обёртка отдаст как есть, с исходными цветами. Отсюда простая производственная привычка: свои монохромные значки рисуй в SVG одним цветом и оборачивай в NewThemedResource — они встанут в один ряд со встроенными и будут честно реагировать и на смену светлого-тёмного режима, и на локальный ThemeOverride из пятнадцатого урока. А цветные логотипы оставай обычным ресурсом: брендовой палитре перекраска не нужна.
Один значок — два цвета
GOmyIcon := theme.NewThemedResource(star)
light := container.NewThemeOverride(
widget.NewButtonWithIcon("Светлая", myIcon, func() {}),
theme.LightTheme(),
)
dark := container.NewThemeOverride(
widget.NewButtonWithIcon("Тёмная", myIcon, func() {}),
theme.DarkTheme(),
)Один и тот же myIcon показан в двух зонах с разными темами: внутри override из пятнадцатого урока обёртка перекрашивает SVG цветом foreground соответствующей палитры. Без NewThemedResource оба места показывали бы исходный цвет файла.
Значок перекрасили под акцент
Прочитайте код и выберите, что он выведет:
icon := theme.NewThemedResource(gearSVG)
icon.ColorName = theme.ColorNamePrimary
btn := widget.NewButtonWithIcon("Настройки", icon, nil)
// пользователь переключил тему и сменил primary на красный
// какого цвета станет шестерёнка?Шрифты как ресурсы#
Последний жанр ресурсов — шрифты, и он уже частично знаком: метод Font(style fyne.TextStyle) твоей темы из пятнадцатого урока возвращает fyne.Resource, внутри которого лежит TTF-файл. Стандартная тема раздаёт семейство Noto Sans: обычный, жирный, курсив и монопространственный вариант — причём кириллица в них поддерживается из коробки, поэтому русский текст в fyne не превращается в квадраты. Подключить свой шрифт просто: вшиваешь TTF через embed, в своей теме возвращаешь ресурс из метода Font — весь интерфейс, от Label до Entry, перечитает шрифт сам, ведь виджеты спрашивают тему при каждой отрисовке. Стиль параметра решает, какой из четырёх файлов отдать: TextStyle.Bold, TextStyle.Italic, TextStyle.Monospace. Два практических предупреждения. Первое: проверяй глифы — модный узкий шрифт без кириллических букв даст красивую латиницу и пустые квадраты в русском тексте. Второе: не присваивай ресурс шрифта напрямую виджетам — шрифт живёт в теме, и это правильно: одна точка правды, как с цветами. Размер текста, напомню, шрифтовой ресурс не задаёт — это SizeNameText из пятнадцатого урока.
Собери связку embed-ресурса
Восстанови четыре пропуска: упаковка байтов в ресурс, фабрика изображения, поле режима заполнения и назначение иконки приложению.
Путь своего значка от файла до экрана
Расставь по порядку шаги, через которые проходит значок, вшитый в бинарник и перекрашиваемый темой.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Ресурсам нужен настоящий экран — в браузерной песочнице курса GUI не исполняется. Собери пример по второму уроку и подложи свой PNG и свой SVG: смена иконки окна, три режима FillMode у canvas.Image и перекраска ThemedResource при переключении темы — три эффекта, которые стоит увидеть своими глазами.
Что дальше#
Ресурс — последняя «кирпичная» абстракция тулкита: теперь у тебя есть всё, из чего собирается интерфейс. Каркас приложения и первый запуск — во втором уроке; примитивы и правило «значение вместо подписки», на котором держится Refresh у canvas.Image, — в седьмом; пункты меню с полем Icon — в четырнадцатом, а тема, чьи Color, Font и Icon раздают ресурсы, — в пятнадцатом. Дальше по программе данные: семнадцатый урок посвящён привязке binding — когда данные меняются, виджеты обновляются сами, и правило ручного Refresh наконец отступает. Практические выбросы сегодняшнего материала: иконки приложения для сторов собираются при упаковке под мобильные, а полный маршрут курса — на странице Fyne. Держи в голове одну рекомендацию из этого урока как дефолт: всё, что должно жить с программой, — embed; всё, что меняется в рантайме, — статический ресурс из свежих байтов.
Термины урока
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Виртуальный экран
HTML-модель виджет-дерева fynefyne.Resource
StaticResource + embed
Картинки и шрифты — это []byte + имя ресурса
Экран собран: «Ресурсы»
Коротко: что запомнить
- Ресурс — интерфейс fyne.Resource из Name() и Content(): тулкиту всё равно, откуда байты, важны имя с расширением и содержимое.
- fyne.NewStaticResource заворачивает байты в StaticResource; так в ресурс превращается файл, прочитанный в рантайме.
- Встроенные значки получаются через theme.Icon(theme.IconNameX) и перекрашиваются темой сами: кнопки, меню, тулбары, вкладки.
- canvas.Image имеет источники Resource, Image и File и режимы FillMode: Stretch, Contain, Original; после смены полей нужен Refresh.
- //go:embed вшивает файлы в бинарник: один файл как []byte, каталог как embed.FS; пути только внутри пакета.
- fyne bundle генерирует Go-файл с константами fyne.StaticResource — альтернатива embed для больших ассет-парков.
- theme.NewThemedResource красит SVG-значок цветом темы (по умолчанию Foreground, можно ColorName) при каждой отрисовке.
- Шрифты — тоже ресурсы: метод Font темы возвращает TTF; свой шрифт подключается вшиванием и проверкой кириллических глифов.
Домашнее задание
Собери «паспорт приложения». Вшей через //go:embed логотип PNG и свой SVG-значок, нарисованный одним цветом. Первое окно: canvas.Image с логотипом и три кнопки, переключающие FillMode между Stretch, Contain и Original, — убедись, что после каждой смены нужен canvas.Refresh, и найди режим, при котором картинка искажается. Второе окно: кнопки с встроенными значками theme.Icon — IconNameHome, IconNameContentAdd, IconNameDocumentSave — и тулбар из ToolbarAction; добавь главное меню, где у пункта «Сохранить» заполнено поле Icon, как в четырнадцатом уроке. Свой SVG оберни в theme.NewThemedResource и повесь на кнопку в двух container.NewThemeOverride — светлом и тёмном: значок обязан менять цвет. Затем задай ColorName = theme.ColorNamePrimary и проверь реакцию на смену primary. Назначь myApp.SetIcon и window.SetIcon со своим PNG. Наконец, вшей свой TTF и верни его из метода Font темы с фирменным акцентом — сначала проверь, что шрифт умеет кириллицу, иначе русский текст рассыпется на квадраты. Относительные пути в os.ReadFile не используй нигде: всё, что должно жить с программой, — embed.
Личные заметкиПусто
Вопросы и ответы по уроку
Можно ли перекрасить PNG-логотип через theme.NewThemedResource?
Нет: перекраска гарантированно работает с SVG — тулкит перерисовывает вектор цветом темы. PNG обёртка отдаёт с исходными цветами. Для PNG делай заранее подготовленные варианты под светлую и тёмную палитры или переводи значок в SVG.
Где fyne ищет файл при canvas.NewImageFromFile?
По относительному пути от рабочей папки процесса, а не от папки с exe, поэтому поведение зависит от того, как программу запустили. Для надёжности используй абсолютные пути или вшивай файлы через //go:embed, а файлы пользователя открывай через URI из диалога выбора.
Чем fyne bundle лучше //go:embed и когда его выбирать?
Результат одинаков — fyne.Resource в бинарнике. bundle превращает ассеты в Go-переменные с фиксированными именами, не требует директив и каталога пакета, но генерируется заново при каждом изменении файлов. Embed проще в повседневной работе: компилятор сам следит за содержимым.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
Cobra: CLI-приложения на Go
Интерактивный курс Cobra — фреймворка для CLI на Go: команды, флаги, конфиги Viper, автодополнение. 25 уроков с примерами кода, тестами и итоговым приложением.
- Создавать CLI-приложения с деревом команд на Cobra
- Работать с аргументами, флагами и их привязкой к конфигу
- Управлять конфигурацией через Viper: файлы, env, приоритеты
- Писать хуки PreRun/PostRun и корректно обрабатывать ошибки