---
title: "Десктопные особенности"
description: "Десктопные особенности Fyne: перехват закрытия окна, SetMaster, меню системного трея, размеры окна и HiDPI, буфер обмена и drag-and-drop файлов на десктопе."
course: "Fyne: десктопные и мобильные GUI на Go"
url: "https://golangmaster.ru/courses/fyne/lessons/23-desktop/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 20
---

# Десктопные особенности

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

Двадцать два урока собрали приложение, которое работает везде, но до сих пор оно вело себя на столе как гость: не перехватывало закрытие, жило без трея и не знало про мониторы. Запрос «fyne десктоп» — это про поведение, которое имеет смысл только на настольных системах: SetCloseIntercept и SetOnClosed вокруг крестика окна, SetMaster и список AllWindows, меню системного трея через desktop.App, логические и физические пиксели с коэффициентом Scale. Русский запрос «fyne desktop» ведёт сюда же. Урок разбирает именно платформенные особенности: что окно должно уметь, чтобы чувствоваться родным для операционной системы, и какие привычки десктопного пользователя стоит уважать — подтверждать выход, помнить геометрию, принимать перетащенные файлы.

### Один тулкит, разные столы

Fyne пишет один код для пяти платформ, но десктоп — старший брат: у него есть окна с рамками, системный трей, буфер обмена, перетаскивание файлов и меню в строке ОС. Всё, что существует только на настольных системах, Fyne выносит в отдельный пакет fyne.io/fyne/v2/driver/desktop, а точка входа в эти возможности — приведение типа приложения: myApp.(desktop.App) даёт расширенный интерфейс поверх обычного fyne.App. Приведение честное: на мобильных платформах из двадцать второго урока проверка ok вернёт false, и ветка просто не выполнится — это штатный способ писать код, который включается только там, где есть смысл. Запрос «fyne десктоп» почти всегда приводит к этому пакету. Вторая особенность — окна: на мобильном приложение живёт в одном экране, заданном системой, а на столе ты сам создаёшь окна через NewWindow, задаёшь размеры и решаешь, какое из них главное. Третья — рендер и масштаб: на десктопе встречаются экраны с разной плотностью пикселей, и Fyne прячет разницу за логическими координатами, о которых ниже отдельный раздел. Наконец, поведение: десктопный пользователь ожидает, что крестик спросит про несохранённые данные, что свернутое приложение найдётся в трее и что файл можно бросить прямо в окно. Всё это — обычные вызовы API, без единой платформенной ветки по операционной системе в твоём коде: различия Fyne берёт на себя. Сначала разберём самый драматичный момент жизни окна — его закрытие.

### Окно, которое спрашивает перед выходом

```go
package main

import (
	"fmt"

	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/dialog"
	"fyne.io/fyne/v2/widget"
)

func main() {
	myApp := app.NewWithID("io.gomaster.desk")
	window := myApp.NewWindow("Редактор")

	window.SetCloseIntercept(func() {
		dialog.ShowConfirm("Выйти?", "Несохранённые изменения будут потеряны.", func(ok bool) {
			if ok {
				window.Close()
			}
		}, window)
	})
	window.SetOnClosed(func() {
		fmt.Println("окно закрыто — черновик сохранён")
	})

	window.SetContent(widget.NewLabel("Текст..."))
	window.ShowAndRun()
}
```

SetCloseIntercept перехватывает запрос на закрытие: вместо немедленного выхода показывается диалог из тринадцатого урока, и окно закрывается только явным window.Close() внутри обработчика. SetOnClosed срабатывает уже после фактического закрытия — там место финальному сохранению.

**Вопрос:** Окно открыто с SetCloseIntercept. Что нужно, чтобы пользователь всё-таки смог выйти?

- Ничего: перехватчик только наблюдает, окно закроется само
- ✅ Вызвать window.Close() внутри перехватчика — например, после подтверждения
- Вызвать myApp.Quit() из перехватчика — другого пути нет
- Удалить перехватчик вызовом window.SetCloseIntercept(nil) и подождать

Перехватчик заменяет закрытие собой: пока внутри не вызван window.Close(), окно останется открытым. Это и смысл механизма — ты решаешь, закрывать ли окно. MyApp.Quit() гасит всё приложение целиком, а это более грубый инструмент, чем нужно в момент подтверждения.

### Геометрия: размер, центр, полноэкранный режим

Окно создаётся вызовом myApp.NewWindow(title) — первое созданное окно становится мастер-окном: его закрытие завершает приложение, если не сказано иное. Размер задаётся window.Resize(fyne.NewSize(w, h)) в логических пикселях, позиция — window.CenterOnScreen(), который ставит окно в центр того монитора, на котором оно сейчас. Дальше три переключателя поведения: SetFixedSize(true) запрещает пользователю тянуть рамку — режим для палитр и маленьких утилит с жёсткой сеткой; SetFullScreen(true) разворачивает окно без рамок на весь экран — просмотрщики и киоски; SetPadded(false) убирает внутренний отступ между содержимым и краями окна, что полезно для графики из седьмого урока, прижатой к границам. Важно понимать, что Resize — это запрос: оконный менеджер вправе скорректировать размер по своим минимумам, поэтому не строй логику на точном равенстве, а фактический размер содержимого читай из window.Canvas().Size(). Ещё два полезных жеста: window.RequestFocus() поднимает окно и передаёт ему фокус — вызывай его, когда приложение нашло что-то по поиску или получило внешнее событие, но помни, что без причины крадущий фокус раздражает. И window.SetIcon(fyne.Resource) — иконка окна и панели задач, ресурсом из шестнадцатого урока. Все эти вызовы — hints, подсказки системе, а не приказы: десктоп уважает волю пользователя сильнее желания программы.

### Главное окно и вспомогательная палитра

```go
package main

import (
	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/widget"
)

func main() {
	myApp := app.New()
	main := myApp.NewWindow("Главное")
	main.Resize(fyne.NewSize(600, 400))
	main.CenterOnScreen()

	palette := myApp.NewWindow("Палитра")
	palette.Resize(fyne.NewSize(220, 300))
	palette.SetFixedSize(true)

	main.SetContent(widget.NewLabel("Основное окно"))
	palette.SetContent(widget.NewLabel("Инструменты"))

	palette.Show()
	main.ShowAndRun()
}
```

Два окна одного приложения: главное создаётся первым и потому мастер-окно — его закрытие завершит программу, даже если палитра ещё открыта. Палитре назначен фиксированный размер: тянуть её рамку бессмысленно. ShowAndRun из каркаса второго урока остаётся последней строкой.

### Какое окно закрывает приложение

```go
main := myApp.NewWindow("Главное")
palette := myApp.NewWindow("Палитра")

main.Show()
palette.Show()

// пользователь сначала закрывает палитру,
// потом закрывает главное — что происходит?
```

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

- Закрытие любого окна сразу завершает приложение
- ✅ Палитра закроется, а приложение закроется только вместе с главным окном
- Главное закроется, а приложение закроется только вместе с палитрой
- Оба закроются, но процесс останется висеть до выхода из диспетчера задач

Мастер-окном по умолчанию считается первое открытое окно: его закрытие завершает приложение. Палитра — второстепенное, её закрытие окно лишь прячет. Роль можно перенести явным SetMaster на другое окно, если главное создаётся не первым.

### Системный трей

Трей — маленькая иконка в системной панели рядом с часами, по клику показывающая меню. Для свернутых в фон утилит это второй дом: окно можно закрыть, а приложение продолжит жить и примет команду из трея. В Fyne трей включается через расширенный интерфейс приложения: приведение myApp.(desktop.App) даёт два метода — SetSystemTrayMenu(menu *fyne.Menu) и SetSystemTrayIcon(icon fyne.Resource). Меню строится знакомым по четырнадцатому уроку конструктором: fyne.NewMenu(«имя», элементы...) и fyne.NewMenuItem(«подпись», функция). Типичный состав — «Показать окно» с window.Show() и RequestFocus, переключатель действия и «Выход» с myApp.Quit(). Есть тонкость жизненного цикла: если окно закрыто, но назначено меню трея, приложение не завершится при закрытии последнего окна — выйти из него можно через Quit из меню трея; без трея закрытие единственного окна означает конец. Иконка трея назначается ресурсом из шестнадцатого урока: если её не ставить, платформа покажет значок приложения по умолчанию. Два правила гигиены: держи в трей-меню три-пять пунктов, не превращай его в второе главное меню, и всегда оставляй путь к выходу — пользователь должен иметь возможность завершить процесс, не открывая диспетчер задач. Для случаев, когда трей нужен глубже — всплывающие уведомления из иконки, динамическая иконка-индикатор, — есть отдельный пакет github.com/fyne-io/systray, но в девяти случаях из десяти хватает встроенных двух методов.

### Приложение, живущее в трее

```go
package main

import (
	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/driver/desktop"
	"fyne.io/fyne/v2/widget"
)

func main() {
	myApp := app.NewWithID("io.gomaster.tray")
	window := myApp.NewWindow("Агент")

	if d, ok := myApp.(desktop.App); ok {
		menu := fyne.NewMenu("Агент",
			fyne.NewMenuItem("Показать окно", func() {
				window.Show()
				window.RequestFocus()
			}),
			fyne.NewMenuItem("Выход", func() {
				myApp.Quit()
			}),
		)
		d.SetSystemTrayMenu(menu)
	}

	window.SetContent(widget.NewLabel("Закрой окно — агент останется в трее"))
	window.ShowAndRun()
}
```

Приведение к desktop.App с проверкой ok безопасно на любой платформе: где трея нет, ветка пропустится. Пункт «Показать окно» поднимает закрытое окно и забирает фокус, «Выход» завершает процесс. После назначения меню закрытие окна приложение уже не убьёт.

**Вопрос:** Как правильно получить интерфейс с SetSystemTrayMenu?

- Вызвать desktop.NewTrayApp() вместо app.New()
- ✅ Приведением типа: d, ok := myApp.(desktop.App), затем d.SetSystemTrayMenu(menu)
- Импортировать системный пакет ОС и вызвать SetSystemTrayMenu напрямую
- Создать widget.SystemTray и добавить его в окно

Трей — свойство платформы, а не отдельного вида приложения: тот же myApp проверяется на реализацию desktop.App через type assertion. Где возможности нет, ok окажется false и ветка не выполнится — код остаётся переносимым без условной компиляции.

### Размеры экрана и HiDPI

На десктопе экраны бывают 96 точек на дюйм, а бывают и 200 с лишним, и Fyne прячет эту разницу за двумя системами координат. Логические пиксели — то, чем ты оперируешь везде: в Resize, в NewSize, в координатах canvas из седьмого урока. Физические пиксели — то, что реально светится на матрице. Связывает их коэффициент window.Canvas().Scale(): при Scale 2.0 логическое окно 300 на 200 занимает 600 на 400 физических точек. Шрифты и виджеты, построенные на темах из пятнадцатого урока, масштабируются сами — тебе же коэффициент нужен, когда дело доходит до растровых картинок: изображение, нарисованное в один логический пиксель, на плотном экране замылится, так что бери исходники в двойном размере и храни в ресурсах. Практические правила: не хардкодь предположений о размере монитора — окно 1200 на 800 не влезет на маленький ноутбук, поэтому стартуй со скромного Resize и дай пользователю растянуть; фактический размер контента читай из Canvas().Size(), а не из того, что просил; геометрию при выходе запоминай в preferences из девятнадцатого урока и восстанавливай при старте — это один из самых ценимых мелких сервисов. Позицию и размер каждого монитора сам тулкит не раскрывает: если нужны перечисления дисплеев, это территория внешних пакетов, но для 95 процентов приложений достаточно центрирования и сохранённой геометрии. Помни и про минимальные размеры: слишком узкое окно ломает раскладки из четвёртого урока — задай нижнюю границу в SetFixedSize либо проверяй при Resize.

### Сколько физических пикселей

```go
window.Resize(fyne.NewSize(300, 200))
fmt.Println(window.Canvas().Size())     // 300 x 200 (логические)
fmt.Println(window.Canvas().Scale())    // 2.0
// сколько физических пикселей занимает окно?
```

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

- 300 на 200 — масштаб не влияет на размер окна
- 150 на 100 — Scale делит размер пополам
- ✅ 600 на 400 — каждая логическая единица занимает два физических пикселя
- Зависит от темы из пятнадцатого урока

Scale — множитель от логических координат к физическим: при 2.0 логическое 300 на 200 занимает 600 на 400 реальных точек. Весь остальной код продолжает думать в логических пикселях — поэтому интерфейс выглядит одинаково на экранах разной плотности.

### Буфер обмена, drag-and-drop и нативные меню

Три механизма делают приложение своим для операционной системы. Буфер обмена: с версии 2.6 он живёт на приложении — myApp.Clipboard() возвращает объект с Content() для чтения и SetContent(string) для записи; одноимённый метод окна объявлен устаревшим, так что новый код пиши через приложение. Вставка картинки в редактор, копирование ссылки, обмен задачами между окнами — всё это одна строка, без внешних пакетов. Drag-and-drop: window.SetOnDropped(func(pos fyne.Position, uris []fyne.URI)) — пользователь бросает файлы из проводника прямо в окно, колбэк получает позицию падения и срез URI; имя файла достаётся uris[i].Name(), а читается содержимое хранилищем из двадцать первого урока. Это самый короткий путь «открыть документ»: поддержка перетаскивания экономит пользователю обход диалога выбора. Нативные меню: SetMainMenu из четырнадцатого урока на разных столах рендерится по-разному — на Windows и Linux это строка внутри окна, на macOS пункты уходят в системную строку сверху экрана, как у родных приложений; твой код при этом не меняется ни на символ. Отдельная десктопная вежливость — горячие клавиши: пользователь стола ждёт Ctrl+C и Ctrl+V везде, где есть текст, и Ctrl+S в редакторе; базовые привязки Fyne делает сам, а свои добавляются через AddShortcut из шестого урока с desktop.CustomShortcut. Собирая всё вместе, ты получаешь приложение, которое ОС считает своим: оно в трее, в буфере обмена, в строке меню и в системе перетаскивания.

### Приём файлов перетаскиванием

```go
package main

import (
	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/widget"
)

func main() {
	myApp := app.New()
	window := myApp.NewWindow("Приём файлов")

	label := widget.NewLabel("Перетащи файл сюда")
	window.SetOnDropped(func(pos fyne.Position, uris []fyne.URI) {
		if len(uris) > 0 {
			label.SetText("Получен: " + uris[0].Name())
		}
	})

	window.SetContent(label)
	window.Resize(fyne.NewSize(400, 160))
	window.ShowAndRun()
}
```

Один колбэк — и окно принимает файлы из проводника: приходят позиция падения и срез URI. Имя достаётся методом Name() URI, а содержимое потом читается через хранилище файлов из двадцать первого урока. Никаких платформенных веток — механику перетаскивания ведёт тулкит.

**Вопрос:** Где с версии 2.6 правильно читать и писать системный буфер обмена?

- window.Clipboard() — как и раньше, других вариантов нет
- ✅ myApp.Clipboard() с методами Content() и SetContent(); метод окна устарел
- Через widget.NewEntry: копировать и вставлять можно только в полях ввода
- Буфер обмена в Fyne не поддерживается — нужен внешний пакет

Буфер обмена переехал на уровень приложения: myApp.Clipboard().Content() читает, SetContent пишет. Метод окна остался как устаревший мост для старого кода. Это удобно ещё и тем, что копировать можно из любого места приложения, а не только из виджета.

### Каталог десктопных API

| API | Что делает | Когда звать |
| --- | --- | --- |
| myApp.NewWindow(title) | создать окно приложения | каждое окно; первое становится мастер-окном |
| window.SetMaster() | закрытие окна завершает приложение | когда главное окно создано не первым |
| window.SetCloseIntercept(fn) | перехватить запрос на закрытие | подтверждение выхода, сохранение черновика |
| window.SetOnClosed(fn) | действие после фактического закрытия | финальное сохранение, логирование |
| window.Resize(fyne.NewSize(w, h)) | запросить размер в логических пикселях | стартовая и восстановленная геометрия |
| window.CenterOnScreen() | центр текущего монитора | старт приложения, окна-инструменты |
| window.SetFixedSize(true) | запрет изменения размера | палитры, утилиты с фиксированной сеткой |
| window.SetFullScreen(true) | развернуть без рамок | просмотрщики, киоски, слайды |
| window.SetPadded(false) | убрать внутренний отступ окна | графика и полноэкранный режим |
| window.RequestFocus() | поднять окно и отдать фокус | результат поиска, внешнее событие |
| myApp.(desktop.App).SetSystemTrayMenu(menu) | меню системного трея | приложение, живущее свернутым |
| myApp.(desktop.App).SetSystemTrayIcon(res) | иконка трея | узнаваемость в системной панели |
| myApp.Driver().AllWindows() | список всех окон приложения | циклы «закрыть все», проверки состояния |
| window.SetOnDropped(fn) | приём перетащенных файлов | открытие документов броском |
| myApp.Clipboard() | буфер обмена: Content и SetContent | копирование и вставка по своей логике |

### Собери окно с памятью и вежливым выходом

Восстанови пять пропусков: чтение геометрии с запасными значениями, применение размера, центрирование и честное закрытие из перехватчика.

```go
prefs := myApp.Preferences()
w := prefs.___("win.w", 600.0)
h := prefs.___("win.h", 400.0)

window.___(fyne.NewSize(w, h))
window.___()

window.SetCloseIntercept(func() {
	prefs.SetFloat("win.w", float64(window.Content().Size().Width))
	___()
})
```

Подсказка: Дробные настройки читаются семейством WithFallback из девятнадцатого урока, размер применяется Resize, а перехватчик обязан сам вызвать window.Close(), иначе окно не закроется.

Правильные ответы в пропусках: `FloatWithFallback`, `FloatWithFallback`, `Resize`, `CenterOnScreen`, `window.Close()`

### Путь нажатия на крестик

Расставь по порядку события от клика по крестику до завершения приложения.

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

1. `пользователь нажимает крестик в заголовке окна`
2. `оконный менеджер ОС передаёт Fyne запрос на закрытие`
3. `срабатывает SetCloseIntercept — закрытие перехвачено`
4. `показывается dialog.ShowConfirm с вопросом о выходе`
5. `пользователь подтверждает — обработчик вызывает window.Close()`
6. `выполняется SetOnClosed, затем закрытие мастер-окна завершает Run`

> **Честно о запуске кода.** Десктопным примерам нужен настоящий оконный менеджер — в браузерной песочнице курса GUI не исполняется. Собери примеры по каркасу второго урока на своей машине и обязательно закрой окно крестиком: перехватчик, диалог и сохранение геометрии проверяются только живой рукой пользователя.

> **Трей — маленькая икона большой ответственности.** Иконка трея рисуется в 16-24 логических пикселях и часто монохромна: детализированный логотип утонет, бери упрощённую версию из ресурсов. И помни про жизненный цикл: с назначенным трей-меню закрытие окна не завершает процесс — забыл пункт «Выход», и приложение останется висеть в панели вечно.

### Практика: окно, которое ведёт себя по-взрослому

Соберём чек-лист десктопного приличия из уже пройденного. Геометрия: при закрытии — в перехватчике или SetOnClosed — пишешь ширину, высоту и позицию в preferences девятнадцатого урока; при старте читаешь с WithFallback и применяешь до ShowAndRun, чтобы первый кадр уже был правильным, без прыжка «сначала центр, потом куда-то влево». Выход: если в данных есть несохранённое — флаг dirty, который ставят обработчики из шестого урока, — перехватчик показывает диалог из тринадцатого урока с тремя исходами: сохранить и выйти, выйти без сохранения, остаться. Трей: сворачиваемая в панель утилита получает меню из двух-трёх пунктов и иконку-упрощение; окно при этом можно закрыть совсем — агент живёт в трее. Файлы: SetOnDropped открывает документ броском, а буфер обмена через myApp.Clipboard() даёт обмен текстом с другими программами. Горячие клавиши: Ctrl+S на сохранение и Ctrl+Q на выход через AddShortcut — десктопный пользователь проверит их в первую минуту. Каждая строка этого чек-листа — два-пятнадцать строк кода, но именно они отличают инструмент, которым пользуются год, от поделки, которую закрывают в первый день. Прогони свой проект из двадцать пятого урока по списку — и он будет выглядеть родным на любом столе.

### Что дальше: тесты без монитора

Теперь приложение ведёт себя как нативное для стола: спрашивает перед выходом, помнит геометрию, живёт в трее, принимает файлы перетаскиванием и разговаривает с буфером обмена. Опора урока — [второй урок](/courses/fyne/lessons/02-pervoe-okno/) с каркасом запуска, [тринадцатый](/courses/fyne/lessons/13-dialogi/) с диалогом подтверждения, [четырнадцатый](/courses/fyne/lessons/14-menju/) с меню и тулбаром, [девятнадцатый](/courses/fyne/lessons/19-preferences/) с хранением геометрии и [шестнадцатый](/courses/fyne/lessons/16-resursy/) с иконками. Следующая тема — тестирование: как проверить, что кнопка действительно меняет состояние и что виджет рендерится так, как задуман, не открывая ни одного окна, и почему после этого урока регрессии в интерфейсе перестанут быть сюрпризом. Полный маршрут — [на странице курса Fyne](/courses/fyne/).

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

- **SetCloseIntercept** — Перехватчик закрытия окна: заменяет выход собой, и окно закроется только явным window.Close() внутри. Место для подтверждений и сохранения черновиков.
- **SetMaster** — Помечает окно главным: его закрытие завершает приложение. Без явного вызова мастер-окном считается первое созданное окно.
- **desktop.App** — Расширение fyne.App с возможностями стола: SetSystemTrayMenu и SetSystemTrayIcon. Получается приведением myApp.(desktop.App) с проверкой ok.
- **Системный трей** — Иконка с меню в системной панели: fyne.NewMenu из двух-пяти пунктов, иконка ресурсом. С назначенным меню закрытие окна больше не завершает процесс.
- **Canvas().Scale()** — Коэффициент плотности экрана: множитель от логических пикселей, в которых живёт весь код, к физическим точкам матрицы. При 2.0 окно 300 на 200 занимает 600 на 400.
- **SetOnDropped** — Колбэк окна для перетащенных файлов: позиция падения и срез fyne.URI. Имя файла — uris[i].Name(), содержимое читается хранилищем файлов.

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

- Десктопные возможности живут в пакете driver/desktop и включаются приведением myApp.(desktop.App) с проверкой ok — на мобильных ветка просто пропускается.
- SetCloseIntercept заменяет закрытие собой: подтверждай и вызывай window.Close() явно; SetOnClosed срабатывает уже после закрытия.
- Первое окно — мастер-окно: его закрытие завершает приложение; SetMaster переносит эту роль, а AllWindows() перечисляет окна.
- Resize, CenterOnScreen, SetFixedSize, SetFullScreen и SetPadded — подсказки оконному менеджеру; фактический размер читай из Canvas().Size().
- Трей собирается из fyne.NewMenu: Show с RequestFocus и Quit обязательны; с назначенным меню закрытие окна не гасит процесс.
- Canvas().Scale() связывает логические пиксели кода с физическими точками экрана — растровые ресурсы бери в двойном размере.
- Буфер обмена с 2.6 живёт на приложении, файлы принимаются SetOnDropped, а SetMainMenu на macOS уходит в системную строку меню.

## Домашнее задание

Собери «вежливое» десктопное окно текстового редактора. Во-первых, геометрия: при закрытии сохрани ширину и высоту окна в preferences через SetFloat, при старте прочитай FloatWithFallback и примени Resize с CenterOnScreen до ShowAndRun — первый кадр должен открываться в сохранённом размере. Во-вторых, выход: поставь SetCloseIntercept с dialog.ShowConfirm из тринадцатого урока; держи флаг dirty, который ставит OnChanged поля ввода, и показывай вопрос о выходе только при несохранённых изменениях, а в SetOnClosed напиши в лог факт закрытия. В-третьих, трей: приведи приложение к desktop.App, назначь меню из пунктов «Показать окно» с Show и RequestFocus и «Выход» с myApp.Quit, и убедись, что после назначения меню закрытие окна оставляет процесс живым, а выход работает из трея. В-четвёртых, приём файлов: SetOnDropped выводит имя первого брошенного файла в метку. Проверь на своей машине три сценария: выход с несохранёнными данными спрашивает подтверждение, перезапуск восстанавливает размер окна, закрытие окна при активном трее не убивает приложение. Ловушка для самопроверки: попробуй вызвать window.Close() из SetOnClosed и подумай, почему это ошибка.

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

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

Если назначено меню системного трея через SetSystemTrayMenu, закрытие окна не завершает процесс — приложение живёт в трее по дизайну. Добавь в меню пункт «Выход» с myApp.Quit() или убери назначение меню, если трей не нужен.

### Как узнать размер монитора пользователя?

Прямого API для перечисления мониторов у Fyne нет. Центрируй окно через CenterOnScreen, задавай скромный стартовый размер и полагайся на фактический размер содержимого из window.Canvas().Size() — оконный менеджер сам не даст окну выйти за пределы экрана.

### Работает ли системный трей на мобильных платформах?

Нет: трей — возможность десктопа. Приведение myApp.(desktop.App) на мобильной сборке вернёт ok равным false, и ветка с SetSystemTrayMenu просто не выполнится — код остаётся единым без платформенных if-ов.
