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

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

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

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

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

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

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

Ресурс в 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?

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

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

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

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

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

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: файл и каталог

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?

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

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

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

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-ресурса

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

//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)

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

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

Строки

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

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

Что дальше#

Ресурс — последняя «кирпичная» абстракция тулкита: теперь у тебя есть всё, из чего собирается интерфейс. Каркас приложения и первый запуск — во втором уроке; примитивы и правило «значение вместо подписки», на котором держится Refresh у canvas.Image, — в седьмом; пункты меню с полем Icon — в четырнадцатом, а тема, чьи Color, Font и Icon раздают ресурсы, — в пятнадцатом. Дальше по программе данные: семнадцатый урок посвящён привязке binding — когда данные меняются, виджеты обновляются сами, и правило ручного Refresh наконец отступает. Практические выбросы сегодняшнего материала: иконки приложения для сторов собираются при упаковке под мобильные, а полный маршрут курса — на странице Fyne. Держи в голове одну рекомендацию из этого урока как дефолт: всё, что должно жить с программой, — embed; всё, что меняется в рантайме, — статический ресурс из свежих байтов.

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

Термин

fyne.Resource

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

Интерфейс из двух методов: Name() string и Content() []byte. Имя помогает определить формат, байты — это данные. Источник может быть любым: embed, файл, сеть.

1 / 6

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

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

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

HTML-модель виджет-дерева fyne
Ресурсыfyne · мок

fyne.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.

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

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

Можно ли перекрасить PNG-логотип через theme.NewThemedResource?

Нет: перекраска гарантированно работает с SVG — тулкит перерисовывает вектор цветом темы. PNG обёртка отдаёт с исходными цветами. Для PNG делай заранее подготовленные варианты под светлую и тёмную палитры или переводи значок в SVG.

Где fyne ищет файл при canvas.NewImageFromFile?

По относительному пути от рабочей папки процесса, а не от папки с exe, поэтому поведение зависит от того, как программу запустили. Для надёжности используй абсолютные пути или вшивай файлы через //go:embed, а файлы пользователя открывай через URI из диалога выбора.

Чем fyne bundle лучше //go:embed и когда его выбирать?

Результат одинаков — fyne.Resource в бинарнике. bundle превращает ассеты в Go-переменные с фиксированными именами, не требует директив и каталога пакета, но генерируется заново при каждом изменении файлов. Embed проще в повседневной работе: компилятор сам следит за содержимым.

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

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

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

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

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

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

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

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

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