Содержание урока · 11 разделов
- Rectangle: квадратное начало
- Circle и Line: точки и черты
- canvas.Text: текст как примитив
- Каталог примитивов пакета canvas
- Image: три способа загрузить и FillMode
- Raster и LinearGradient: пиксели и плавность
- Покраска: цвета, прозрачность и тема
- Что дальше: примитивы внутри своего виджета
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Шесть уроков ты собирал интерфейс из готовых виджетов — и этого хватало. Сегодня спускаемся на уровень ниже, к материалу, из которого сделаны сами виджеты: пакет canvas и его примитивы. Это не декоративная тема: внешний вид любой кнопки — это WidgetRenderer, собранный из тех же прямоугольников и текста, а индикаторы, диаграммы, шкалы и миниатюры в реальных приложениях почти всегда рисуются примитивами вручную. В этом уроке разберём семь примитивов — Rectangle, Circle, Line, Text, Image, Raster и LinearGradient, — научимся красить их цветами и прозрачностью, укрощать размеры и не забывать Refresh. Всё это станет фундаментом восьмого урока, где ты соберёшь собственный виджет; а ещё это ответ на любой запрос вида «нарисовать график в fyne» — примитивы плюс математика, больше ничего не нужно.
Rectangle: квадратное начало#
canvas.Rectangle — самый частый примитив: подложки панелей, плашки статусов, цветные полосы. У него три краски: FillColor заливает фигуру, StrokeColor со StrokeWidth рисуют контур. Создаётся либо функцией canvas.NewRectangle(color), либо литералом &canvas.Rectangle{FillColor: ...} — разницы нет, выбирай что читабельнее. Первое, что надо понять: примитив не знает, какого размера ему быть. У Rectangle, как и у большинства объектов пакета, минимальный размер нулевой — размеры диктует контейнер-владелец или прямой вызов Resize. Положишь прямоугольник в VBox — получишь полосу нулевой высоты; отдашь ячейке грида — он растянется на всю ячейку. Второе железное правило: поменяй поле — позови Refresh(). Цвет, записанный в FillColor, не сам просится на экран: перерисовка происходит по команде, и без Refresh новое значение проявится только когда окно перерисуется само, например при изменении размера. Так устроен весь fyne canvas: примитив — данные, Refresh — приказ их нарисовать. Третье: цвета задаются из пакета image/color стандартной библиотеки — обычно color.NRGBA с четырьмя каналами, о палитрах чуть позже.
Лампа, которую красит кнопка
GOpackage 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"
)
func main() {
myApp := app.New()
window := myApp.NewWindow("Светофор")
lamp := canvas.NewRectangle(
color.NRGBA{R: 231, G: 76, B: 60, A: 255},
)
green := false
switcher := widget.NewButton("Переключить", func() {
green = !green
if green {
lamp.FillColor = color.NRGBA{R: 46, G: 204, B: 113, A: 255}
} else {
lamp.FillColor = color.NRGBA{R: 231, G: 76, B: 60, A: 255}
}
lamp.Refresh()
})
window.SetContent(container.NewVBox(
container.NewGridWrap(fyne.NewSize(160, 48), lamp),
switcher,
))
window.ShowAndRun()
}GridWrap из пятого урока даёт прямоугольнику точный размер 160 на 48 — сам по себе примитив нулевого минимального размера. Кнопка меняет поле FillColor и зовёт Refresh(): без него цвет обновился бы только при следующей перерисовке окна. color.NRGBA — цвет с альфа-каналом; 255 означает полную непрозрачность. Логика состояния зелёный/красный живёт в булевой переменной замыкания.
Circle и Line: точки и черты#
canvas.Circle — окружность, вписанная в свой прямоугольник: те же FillColor, StrokeColor и StrokeWidth. В ячейке с равными сторонами получится круг, в вытянутой — эллипс: примитив честно заполняет отведённое место, за пропорциями следишь ты — здесь снова пригодится GridWrap из пятого урока. canvas.Line устроен неожиданно: у него нет полей вида «от точки до точки» — линия рисуется как диагональ своего прямоугольника. Позиция объекта задаёт начало, позиция плюс размер — конец; направь размер строго горизонтально — получишь черту-разделитель, дай высоту — наклонную линию, а набор коротких отрезков складывается в график. Толщина — StrokeWidth, заливки у линии нет, только StrokeColor. Это модель «ограничивающего прямоугольника», и она сначала сбивает с толку, но потом окупается: линия подчиняется тем же компоновкам, что и всё остальное, растёт и сжимается вместе с контейнером без пересчёта координат. Хочешь разделитель на всю ширину панели — положи Line в грид или в Border, и он растянется сам. Полоску нулевой высоты в VBox, как и прямоугольник, не увидишь: размеры примитивам всегда выдаёт контейнер.
Как canvas.Line понимает, откуда и куда рисовать?
Почему прямоугольника не видно?
Прочитайте код и выберите, что он выведет:
window.SetContent(container.NewVBox(
canvas.NewRectangle(color.NRGBA{R: 255, G: 0, B: 0, A: 255}),
widget.NewButton("Нажми", nil),
))canvas.Text: текст как примитив#
canvas.Text — надпись без обёртки виджета: поле Text хранит строку, Color — цвет, Alignment задаёт выравнивание константами fyne.TextAlignLeading, fyne.TextAlignCenter и fyne.TextAlignTrailing, TextStyle включает Bold, Italic и Monospace, а TextSize — размер в пунктах. Функция canvas.NewText(s, color) делает то же в одну строку. В отличие от виджетов, у Text нет темы: какой цвет назначил — такой и есть, и смена темы его не перекрасит, пока ты не назначишь новый и не позовёшь Refresh. Чем это отличается от widget.Label из третьего урока? Label — виджет: берёт цвет и шрифт из активной темы, перерисовывается сам при её смене и участвует в стандартных механизмах вроде масштабирования шрифтов; canvas.Text — сырое рисование: полный контроль над цветом, размером и стилем в обмен на ручное обновление. Редактировать текст он тоже не умеет: для ввода нужен Entry из третьего урока, подробный разбор поля ввода — девятый. Практическая ниша Text — надписи поверх рисунка: штампы на превью, подписи осей, значения на шкалах, watermark на миниатюре.
Штамп и линия-разделитель
GOpackage main
import (
"image/color"
"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("Штамп")
stamp := canvas.NewText("ОТГРУЖЕНО",
color.NRGBA{R: 46, G: 204, B: 113, A: 255})
stamp.TextStyle = fyne.TextStyle{Bold: true}
stamp.Alignment = fyne.TextAlignTrailing
rule := canvas.NewLine(color.NRGBA{R: 120, G: 120, B: 120, A: 255})
rule.StrokeWidth = 2
window.SetContent(container.NewVBox(
container.NewGridWrap(fyne.NewSize(240, 32), stamp),
container.NewGridWrap(fyne.NewSize(240, 2), rule),
))
window.ShowAndRun()
}Две плитки GridWrap выдают примитивам точные размеры: штамп 240 на 32 выравнивается по правому краю, линия 240 на 2 растянулась почти горизонтально — она же диагональ своего прямоугольника, сделай плитку 240 на 20 и получишь наклон. Без GridWrap оба примитива в VBox имели бы нулевую высоту — это та же ловушка, что и с прямоугольником.
Каталог примитивов пакета canvas#
| Примитив | Ключевые поля | Типичная роль |
|---|---|---|
| canvas.Rectangle | FillColor, StrokeColor, StrokeWidth | подложки, плашки, индикаторы |
| canvas.Circle | FillColor, StrokeColor, StrokeWidth | точки, лампочки, маркеры |
| canvas.Line | StrokeColor, StrokeWidth | разделители, простые графики |
| canvas.Text | Text, Color, Alignment, TextStyle | надписи поверх рисунка, штампы |
| canvas.Image | File, Resource, FillMode, Translucency | фото, иконки, заставки |
| canvas.Raster | Generator func(w, h int) image.Image | пиксельные эффекты, визуализации |
| canvas.LinearGradient | StartColor, EndColor, Angle | фоны и плашки с градиентом |
Чем canvas.Text отличается от widget.Label?
Image: три способа загрузить и FillMode#
canvas.Image показывает растровые картинки, и у него три источника. Поле File — путь к файлу на диске, для него есть canvas.NewImageFromFile; поле Resource — fyne.Resource, данные, вшитые в бинарник, — canvas.NewImageFromResource, о ресурсах и embed подробно расскажет шестнадцатый урок; поле Image — готовый image.Image из памяти, например отрисованное другим кодом, — canvas.NewImageFromImage. Главный настрой — FillMode, режим вписывания. ImageFillStretch растягивает картинку на весь контейнер без сохранения пропорций: заполнит любую ячейку, но круг превратит в овал — это значение по умолчанию. ImageFillContain вписывает с сохранением пропорций: если контейнер выше, появятся пустые поля сверху и снизу. ImageFillOriginal рисует в натуральном размере без всякого масштабирования: картинка 1200 на 800 будет именно такой, даже если окно 300 на 200, — удобно с прокруткой, а в маленьком контейнере она просто уйдёт за края. Плюс Translucency — прозрачность от 0 до 1, чтобы призрачно наслаивать картинки. Частая ошибка новичка — ожидание, что Image сам уменьшится: он делает ровно то, что сказал FillMode, и не пикселем больше.
Что покажет ImageFillOriginal?
Прочитайте код и выберите, что он выведет:
img := canvas.NewImageFromFile("кот.png")
img.FillMode = canvas.ImageFillOriginal
// файл кот.png имеет размер 1200 x 800
window.SetContent(container.NewScroll(img))
// окно открыто на 400 x 300Raster и LinearGradient: пиксели и плавность#
Когда примитивов мало, включается canvas.Raster: ты отдаёшь функцию-генератор func(w, h int) image.Image, и при каждой перерисовке она получает размер области в пикселях и возвращает картинку. Полный контроль над каждым пикселем: визуализации, тепловые карты, фракталы, эффекты пламени — всё это Raster. Плата за свободу — собственные руки: у примитивов сглаживание делает painter тулкита, а в генераторе ты рисуешь в буфер image.Image сам, и ступеньки убираешь своими силами. Для простых плавных переходов есть готовый и дешёвый canvas.LinearGradient: поля StartColor, EndColor и Angle дают градиентную подложку любой плашки без единого пикселя вручную — чаще всего это именно то, что нужно, а Raster оставь попиксельным задачам. Важная деталь производительности: генератор Raster вызывается заново при каждой перерисовке области, поэтому тяжёлую математику кешируй — считай один раз и отдавай готовый image.Image, если параметры не изменились. Эта же гигиена пригодится в восьмом уроке: рендерер виджета тоже перерисовывается неоднократно.
Горизонтальный градиент через Raster
GOpackage main
import (
"image"
"image/color"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
)
func main() {
myApp := app.New()
window := myApp.NewWindow("Градиент")
fade := canvas.NewRaster(func(w, h int) image.Image {
img := image.NewNRGBA(image.Rect(0, 0, w, h))
for x := 0; x < w; x++ {
v := uint8(255 * x / max(w-1, 1))
for y := 0; y < h; y++ {
img.SetNRGBA(x, y,
color.NRGBA{R: v, G: v, B: 255, A: 255})
}
}
return img
})
window.SetContent(container.NewStack(fade))
window.ShowAndRun()
}Генератор получает ширину и высоту области в пикселях и собирает image.NewNRGBA столбец за столбцом: яркость синего растёт слева направо. container.NewStack растягивает Raster на всё окно — стек-компоновка подгоняет детей под свой размер. Для простого перехода хватило бы canvas.LinearGradient с StartColor и EndColor; Raster здесь ради демонстрации попиксельного контроля.
Поменяли lamp.FillColor, но экран не изменился. Что забыли?
Покраска: цвета, прозрачность и тема#
Все примитивы красятся одним типом — color.Color из стандартной библиотеки, и на практике это почти всегда color.NRGBA{R, G, B, A}: четыре канала от 0 до 255, где альфа задаёт прозрачность — 0 полностью прозрачно, 255 непрозрачно. Полупрозрачные плашки поверх фото делаются одним значением альфы, например A: 160. Второй источник цветов — активная тема: theme.Primary(), theme.Error(), theme.Warning() и другие возвращают color.Color текущей темы, и подложки, совпадающие с палитрой приложения, лучше брать оттуда, а не изобретать оттенки. Но есть нюанс, о котором часто спотыкаются: ты присваиваешь полю примитива значение цвета на момент присваивания. Если пользователь сменит тему, виджеты перекрасятся сами — их рендереры слушают тему, а твой Rectangle останется в старом цвете, пока ты не присвоишь новое значение из обновлённой темы и не позовёшь Refresh. Это плата за сырость примитивов: полный контроль и ручное обслуживание. Как строить свои темы и корректно перекрашивать всё приложение — пятнадцатый урок; здесь запомни правило: примитив запоминает цвет, а не подписку на него. Так начинается осмысленное рисование fyne: палитра — твоя забота.
Перекрась лампу
Восстанови четыре пропуска: конструктор примитива, альфа-канал, поле цвета и команду перерисовки.
Путь одного кадра
Расставь по порядку, что происходит от смены цвета до появления его на экране.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Рисование из этого урока не показать в браузерной песочнице курса: примитивам нужен настоящий экран и графический рендер. Собери примеры локально по второму уроку — меняй цвета, размеры плиток и FillMode прямо в коде и смотри, что происходит после каждой правки.
Что дальше: примитивы внутри своего виджета#
Примитивы у тебя в руках, и это не случайность: внешний вид любого виджета — это WidgetRenderer, собранный из тех же canvas-объектов. Восьмой урок — «Кастомные виджеты» — соединит события шестого урока и примитивы этого в один навык: свой контрол с поведением и внешним видом. До этого стоит держать в голове каталог базовых виджетов, сетки пятого урока, которые сегодня выдавали примитивам размеры, и события шестого, которые заставят твой будущий виджет реагировать на клики. Картинки, кстати, грузятся и из ресурсов, вшитых в бинарник, — им посвящён шестнадцатый урок, а живое применение шкал и индикаторов ждёт в итоговом менеджере задач. Общий маршрут из двадцати пяти уроков — на странице курса Fyne. Домашнее задание соберёт из примитивов спидометр — и это будет твоя первая нарисованная, а не собранная из виджетов вещь.
Термины урока
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Виртуальный экран
HTML-модель виджет-дерева fyneПримитивы рисования
Экран собран: «Canvas-примитивы»
Коротко: что запомнить
- Пакет canvas — материал всех виджетов: Rectangle, Circle, Line, Text, Image, Raster и LinearGradient собираются в любую нарисованную вещь.
- Примитивы имеют нулевой MinSize: размеры диктует контейнер — GridWrap, Stack — или прямой Resize; в VBox без размера примитив невидим.
- Смена поля примитива требует Refresh(): данные меняет код, экран обновляет только команда на перерисовку.
- Line — диагональ своего ограничивающего прямоугольника: наклон вытекает из Position и Size, координатных полей нет.
- canvas.Text — ручной текст с Color, Alignment и TextStyle; Label — виджет по теме; редактирование — территория Entry.
- У Image три источника — File, Resource, Image — и FillMode: Stretch без пропорций, Contain с полями, Original в натуральную величину.
- Цвета — color.NRGBA с альфой прозрачности или цвета темы; примитив запоминает значение и не перекрашивается при смене темы сам.
Домашнее задание
Нарисуй спидометр-заглушку: тёмный Rectangle-фон, поверх — дуга из десяти коротких Line, координаты концов посчитай через math.Sin и math.Cos, по центру — canvas.Text со значением. Кнопка «Плюс 10» увеличивает число, перекрашивает заполненные сегменты и зовёт Refresh у каждого изменившегося. Затем замени фон на canvas.LinearGradient с Angle и сравни, как выглядит ImageFillContain против ImageFillStretch на любой картинке — из файла или из embedded-ресурса. В конце перепиши ту же шкалу на widget.ProgressBar из третьего урока и честно сравни объём кода и гибкость: примитивы дают контроль, виджеты — скорость сборки. Добавь к спидометру клик-обработчик из шестого урока: тап по шкале ставит значение по позиции клика.
Личные заметкиПусто
Вопросы и ответы по уроку
Почему примитив растянулся на всё окно?
Так делает компоновка: грид и Stack растягивают детей на свою площадь, а сопротивляться примитиву нечем — его MinSize нулевой. Ограничь размер GridWrap, положи примитив в Box или задай Resize; у примитива нет собственного мнения о размере.
Image не показывает картинку — что проверить?
Путь в File должен указывать на существующий файл, а FillMode не должен быть ImageFillOriginal при маленьком контейнере — в натуральную величину картинка уйдёт за края. Вшитые в бинарник картинки грузятся через NewImageFromResource с fyne.NewStaticResource; это надёжнее путей к файловой системе, особенно на мобильных.
Примитивы умеют скруглённые углы и тени?
У Rectangle есть CornerRadius для скругления, а сложные фигуры собираются из примитивов или рисуются в Raster вручную. Теней из коробки нет — их имитируют полупрозрачными слоями или рисуют градиентом; за этим уровнем контроля как раз и идут в canvas.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
Cobra: CLI-приложения на Go
Интерактивный курс Cobra — фреймворка для CLI на Go: команды, флаги, конфиги Viper, автодополнение. 25 уроков с примерами кода, тестами и итоговым приложением.
- Создавать CLI-приложения с деревом команд на Cobra
- Работать с аргументами, флагами и их привязкой к конфигу
- Управлять конфигурацией через Viper: файлы, env, приоритеты
- Писать хуки PreRun/PostRun и корректно обрабатывать ошибки