---
title: "Ресурсы: изображения и шрифты"
description: "Ресурсы в Fyne: интерфейс fyne.Resource, resource.NewStaticResource, встроенные иконки темы, canvas.Image и FillMode, embed-ресурсы, fyne bundle и шрифты."
course: "Fyne: десктопные и мобильные GUI на Go"
url: "https://golangmaster.ru/courses/fyne/lessons/16-resursy/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 16
---

# Ресурсы: изображения и шрифты

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

Кнопка без значка, окно без иконки, шрифт не тот — все эти жалобы лечатся одним механизмом. Запрос «fyne ресурсы» — это интерфейс fyne.Resource: два метода, Name() string и Content() []byte, за которыми прячется любой бинарник — значок, картинка, шрифт. Тулкит раздаёт встроенные значки через theme.Icon, свои файлы можно вшить в бинарник директивой //go:embed (те самые embed ресурсы), упаковать командой fyne bundle или прочитать в рантайме и завернуть через resource.NewStaticResource. А любое fyne изображение на экране — это canvas.Image с источником-ресурсом: разберём режимы заполнения, тематические значки, которые перекрашиваются со сменой палитры из пятнадцатого урока, и честно поговорим про шрифты.

Ресурс в fyne — не файл и не URL, а маленький интерфейс с двумя вопросами: как тебя зовут (Name() string) и что у тебя внутри (Content() []byte). Этого хватает всему тулкиту: рендерер по расширению из имени понимает формат — PNG, JPEG, SVG, TTF — а байты из Content просто рисует. Почему так, а не прямо путь к файлу? Потому что приложение fyne — один бинарник, который должен работать одинаково на Windows у бухгалтера и на Android у курьера: жёсткие пути к файлам живут своей жизнью, а ресурс отвязывает «что рисуем» от «где это лежит». Внутри ресурса может быть что угодно: байты, вшитые в бинарник при компиляции, файл, прочитанный в рантайме, ответ из сети, данные из хранилища приложения. Все места, где тулкит просит картинку, принимают fyne.Resource: иконка окна, значок кнопки, иконка пункта меню — обещание четырнадцатого урока — и шрифт из метода Font темы, о котором говорил пятнадцатый. Начнём с ресурса, который проще всего написать руками: статического, из файла на диске.

### Иконка окна из файла в рантайме

```go
package 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?

- Путь к файлу и метод Open()
- ✅ Name() string и Content() []byte
- URL и HTTP-клиент для загрузки
- image.Image и формат кодека

Два метода — имя и байты. Никаких файлов, дисков и сети на уровне интерфейса: источник данных решаешь ты, а тулкит лишь читает Name для определения формата и Content для отрисовки. Именно поэтому один и тот же ресурс может прийти из embed-пакета, файла или ответа сервера.

### Встроенные значки: каталог 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 | галочка и пустой квадрат | чекбоксы |

### Значки в кнопке, пункте меню и тулбаре

```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("Значки")

        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 рисует значок без подписи, поэтому для тулбаров монохромные значки темы — основной жанр.

**Вопрос:** Как получить встроенный значок темы для кнопки?

- theme.NewIcon(theme.IconNameHome) — фабрика значков
- ✅ theme.Icon(theme.IconNameHome) — ресурс из активной темы
- canvas.NewIcon("home") — по строковому имени
- Никак: встроенные значки не отдаются в код

theme.Icon(name fyne.ThemeIconName) — официальный способ: он спрашивает ресурс у активной темы и возвращает fyne.Resource, готовый для кнопки, меню и вкладки. Фабрика NewIcon не существует, а строковых имён тулкит не принимает — только типизированные константы IconName.

### 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 с источником-ресурсом

```go
package 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 открывает дорогу тематическим значкам, о них ниже.

### Поменяли источник, забыли про перерисовку

```go
picture := canvas.NewImageFromFile("assets/day.png")
picture.Resource = fyne.NewStaticResource("night.png", nightRaw)
picture.FillMode = canvas.ImageFillOriginal
// canvas.Refresh(picture) не вызывали
// что увидит пользователь?
```

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

- Картинка сменится на night.png сразу — поле присвоено
- ✅ До перерисовки останется day.png: примитив хранит значения, а не подписки
- Паника: нельзя задавать Resource у изображения с File
- Картинка исчезнет — File и Resource конфликтуют

Ровно правило седьмого урока: смена поля сама по себе ничего не рисует, нужен canvas.Refresh(picture). И учти приоритет источников: раз Resource задан, тулкит игнорирует File — картинка сменится на night.png при следующей перерисовке, а ImageFillOriginal возьмёт размер уже нового файла.

### embed ресурсы: вшиваем файлы в бинарник

Самый надёжный способ раздать ассеты — вшить их в исполняемый файл ещё на компиляции. С Go 1.16 это делается директивой //go:embed прямо над переменной: байты файла или целого каталога становятся частью пакета, и единственный exe уезжает к пользователю со всем содержимым. Три правила директивы. Первое: файлы должны лежать внутри каталога пакета — подняться выше через «..» нельзя, абсолютные пути нельзя. Второе: каталоги, начинающиеся с точки или подчёркивания, не вшиваются — если папка ассетов не видна, проверь имя. Третье: переменная может быть и одной строкой с []byte, и embed.FS — виртуальной файловой системой для целого дерева; из неё байты достают методом ReadFile по пути относительно пакета. Дальше всё знакомо: ReadFile отдал байты — fyne.NewStaticResource упаковал — виджет нарисовал. Эти embed ресурсы компилятор контролирует за тебя: файла нет — сборка падает, и битый путь невозможно заметить только на проде. Именно так стоит вшивать логотипы, значки и шрифты, которые живут вместе с приложением.

### Два способа embed: файл и каталог

```go
package 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?

- ../shared/logo.png — общий каталог проекта
- /home/user/assets/logo.png — абсолютный путь
- ✅ assets/logo.png — внутри каталога пакета
- https://example.com/logo.png — URL

Директива работает только с файлами и каталогами внутри пакета, относительными и без «..». Абсолютные пути, выходы наружу и сети — нет. Нужен общий ассет для нескольких пакетов — держи его в отдельном пакете-ассете и вшивай там, а остальным отдавай готовым fyne.Resource.

### 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 из пятнадцатого урока. А цветные логотипы оставай обычным ресурсом: брендовой палитре перекраска не нужна.

### Один значок — два цвета

```go
myIcon := 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 оба места показывали бы исходный цвет файла.

### Значок перекрасили под акцент

```go
icon := theme.NewThemedResource(gearSVG)
icon.ColorName = theme.ColorNamePrimary
btn := widget.NewButtonWithIcon("Настройки", icon, nil)
// пользователь переключил тему и сменил primary на красный
// какого цвета станет шестерёнка?
```

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

- Останется исходным цветом SVG — обёртка работает один раз при создании
- Станет серым: кнопка перекрашивает значки сама
- ✅ Станет primary-цветом активной темы — перекраска читает тему при каждой отрисовке
- Цвет не определён: ColorName меняется только до первого показа

ThemedResource — не одноразовая покраска, а адаптер: при каждой отрисовке он спрашивает активную тему цвет с именем ColorName. Поменялась палитра или primary — значок перечитал ответ и перерисовался вместе с кнопкой, без твоего участия и без Refresh.

### Шрифты как ресурсы

Последний жанр ресурсов — шрифты, и он уже частично знаком: метод Font(style fyne.TextStyle) твоей темы из пятнадцатого урока возвращает fyne.Resource, внутри которого лежит TTF-файл. Стандартная тема раздаёт семейство Noto Sans: обычный, жирный, курсив и монопространственный вариант — причём кириллица в них поддерживается из коробки, поэтому русский текст в fyne не превращается в квадраты. Подключить свой шрифт просто: вшиваешь TTF через embed, в своей теме возвращаешь ресурс из метода Font — весь интерфейс, от Label до Entry, перечитает шрифт сам, ведь виджеты спрашивают тему при каждой отрисовке. Стиль параметра решает, какой из четырёх файлов отдать: TextStyle.Bold, TextStyle.Italic, TextStyle.Monospace. Два практических предупреждения. Первое: проверяй глифы — модный узкий шрифт без кириллических букв даст красивую латиницу и пустые квадраты в русском тексте. Второе: не присваивай ресурс шрифта напрямую виджетам — шрифт живёт в теме, и это правильно: одна точка правды, как с цветами. Размер текста, напомню, шрифтовой ресурс не задаёт — это SizeNameText из пятнадцатого урока.

### Собери связку embed-ресурса

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

```go
//go:embed assets/logo.png
var logoRaw []byte

res := fyne.___("logo.png", logoRaw)
picture := canvas.___(res)
picture.___ = canvas.ImageFillContain
btn := widget.NewButtonWithIcon("Лого", res, func() {})
myApp.___(res)
```

Подсказка: Байты в ресурс — fyne.NewStaticResource, ресурс в картинку — canvas.NewImageFromResource, поведение в контейнере — поле FillMode, значок приложения — SetIcon.

Правильные ответы в пропусках: `NewStaticResource`, `NewImageFromResource`, `FillMode`, `SetIcon`

### Путь своего значка от файла до экрана

Расставь по порядку шаги, через которые проходит значок, вшитый в бинарник и перекрашиваемый темой.

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

1. `SVG-файл лежит в каталоге assets внутри пакета`
2. `директива //go:embed вшивает файл в бинарник при сборке`
3. `метод ReadFile у embed.FS отдаёт байты значка`
4. `fyne.NewStaticResource упаковывает байты в fyne.Resource с именем и расширением`
5. `theme.NewThemedResource оборачивает ресурс и назначает цвет темы`
6. `кнопка рисует значок и перекрашивает его при каждой смене темы`

> **Честно о запуске кода.** Ресурсам нужен настоящий экран — в браузерной песочнице курса GUI не исполняется. Собери пример по второму уроку и подложи свой PNG и свой SVG: смена иконки окна, три режима FillMode у canvas.Image и перекраска ThemedResource при переключении темы — три эффекта, которые стоит увидеть своими глазами.

### Что дальше

Ресурс — последняя «кирпичная» абстракция тулкита: теперь у тебя есть всё, из чего собирается интерфейс. Каркас приложения и первый запуск — во [втором уроке](/courses/fyne/lessons/02-pervoe-okno/); примитивы и правило «значение вместо подписки», на котором держится Refresh у canvas.Image, — в [седьмом](/courses/fyne/lessons/07-canvas/); пункты меню с полем Icon — в [четырнадцатом](/courses/fyne/lessons/14-menju/), а тема, чьи Color, Font и Icon раздают ресурсы, — в [пятнадцатом](/courses/fyne/lessons/15-temy/). Дальше по программе данные: семнадцатый урок посвящён привязке binding — когда данные меняются, виджеты обновляются сами, и правило ручного Refresh наконец отступает. Практические выбросы сегодняшнего материала: иконки приложения для сторов собираются при упаковке под мобильные, а полный маршрут курса — [на странице Fyne](/courses/fyne/). Держи в голове одну рекомендацию из этого урока как дефолт: всё, что должно жить с программой, — embed; всё, что меняется в рантайме, — статический ресурс из свежих байтов.

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

- **fyne.Resource** — Интерфейс из двух методов: Name() string и Content() []byte. Имя помогает определить формат, байты — это данные. Источник может быть любым: embed, файл, сеть.
- **fyne.NewStaticResource / resource.NewStaticResource** — Фабрика статического ресурса: пара имя-плюс-байты в структуре StaticResource{StaticName, StaticContent}. Способ завернуть в ресурс файл, прочитанный в рантайме.
- **theme.Icon(theme.IconNameX)** — Ресурс встроенного значка из активной темы. Монохромные тематические иконки для кнопок, пунктов меню, тулбаров и вкладок.
- **canvas.Image и FillMode** — Примитив изображения с приоритетом источников Resource, Image, File. Режимы: ImageFillStretch (растянуть), ImageFillContain (вписать), ImageFillOriginal (нативный размер); после смены полей — canvas.Refresh.
- **//go:embed** — Директива вшивания файлов в бинарник: []byte для одного файла, embed.FS для каталога. Только пути внутри пакета, без «..». Нет файла — сборка падает.
- **theme.NewThemedResource** — Обёртка, перекрашивающая SVG-значок цветом темы при каждой отрисовке. Поле ColorName задаёт имя цвета (по умолчанию Foreground). PNG не перекрашивает.

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

- Ресурс — интерфейс 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 проще в повседневной работе: компилятор сам следит за содержимым.
