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

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

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

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

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

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

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

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

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. Что нужно, чтобы пользователь всё-таки смог выйти?

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

Окно создаётся вызовом 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 из каркаса второго урока остаётся последней строкой.

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

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

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

main.Show()
palette.Show()

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

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

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

Размеры экрана и 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.

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

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

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

Буфер обмена, 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 правильно читать и писать системный буфер обмена?

Каталог десктопных 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копирование и вставка по своей логике

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

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

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)) () })

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

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

Строки

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

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

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

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

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

Теперь приложение ведёт себя как нативное для стола: спрашивает перед выходом, помнит геометрию, живёт в трее, принимает файлы перетаскиванием и разговаривает с буфером обмена. Опора урока — второй урок с каркасом запуска, тринадцатый с диалогом подтверждения, четырнадцатый с меню и тулбаром, девятнадцатый с хранением геометрии и шестнадцатый с иконками. Следующая тема — тестирование: как проверить, что кнопка действительно меняет состояние и что виджет рендерится так, как задуман, не открывая ни одного окна, и почему после этого урока регрессии в интерфейсе перестанут быть сюрпризом. Полный маршрут — на странице курса Fyne.

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

Термин

SetCloseIntercept

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

Перехватчик закрытия окна: заменяет выход собой, и окно закроется только явным window.Close() внутри. Место для подтверждений и сохранения черновиков.

1 / 6

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

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

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

HTML-модель виджет-дерева fyne
Десктопfyne · мок

Особенности десктопа

Меню ОС, трей, размеры

fyne.App().SetMainMenu и SetIcon работают только тут

Журнал событий

Экран собран: «Десктоп»

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

  • Десктопные возможности живут в пакете 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 и подумай, почему это ошибка.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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