Значок в области уведомлений и toast в Windows-приложении: ошибки NotifyIcon и выбор AppNotification
· Обновлено: · Го Комура · Область уведомлений, NotifyIcon, Toast-уведомления, AppNotification, Windows App SDK, WinForms, WPF, C#, .NET, Приложение в области уведомлений, Разработка под Windows, Техническая консультация
История изменений (2 обновлений, последнее 31 Aug 2026)
Журнал изменений этой статьи. Там, где версия до правки была заархивирована, она остаётся доступной для чтения по постоянной ссылке с DOI.
- Русский текст исправлен, чтобы соответствовать японскому оригиналу.
- Русский текст переписан как полноценный технический перевод, а не калька с японского. Утверждения статьи не менялись.
- Первая публикация
Цитирование статьи(DOI (зарегистрированный архив): 10.5281/zenodo.21620033)
Приведённые ниже DOI относятся к ранее зарегистрированным архивным версиям, которые могут отличаться от текущего текста. Для ссылки на текущий текст используйте URL этой страницы.
Го Комура (2026). Значок в области уведомлений и toast в Windows-приложении: ошибки NotifyIcon и выбор AppNotification. KomuraSoft LLC. https://comcomponent.com/ru/blog/windows-tray-icon-toast-notification-guide/
- DOI (зарегистрированный архив)
- 10.5281/zenodo.21620033
- DOI (последняя зарегистрированная версия)
- 10.5281/zenodo.21620034
«Хочу тихо сообщить пользователю, что обработка закончилась». «Хочу, чтобы наблюдение продолжалось, даже если окно закрыли». В бизнес-приложениях эти две задачи обычно идут вместе: значок в области уведомлений (system tray) и toast-уведомления. На практике сразу всплывают мелочи: приложение, которое вроде закрыли, не завершается; после перезапуска Проводника значок пропадает; toast, который шёл на машине разработчика, на целевых ПК не появляется.
К тому же toast-API с эпохи UWP несколько раз сменялись. В поиске легко наткнуться на пример, который сегодня — в лучшем случае «только поддержка». Ниже — как спроектировать значок через NotifyIcon и какой toast-API выбирать в 2026 году, вместе с тем, на чём обычно спотыкаются.
Как читать эту статью
Глав одиннадцать, текст длинный, поэтому ниже — входы по задаче. Читать подряд не обязательно.
| Задача | Куда смотреть |
|---|---|
| Нужна только общая картина | Глава 1 (выводы) и глава 11 (итог) |
| Собираете приложение со значком в трее | Главы 2–5: основы NotifyIcon, путь завершения, совместное использование с WPF, три типичные ошибки |
| Нужно выбрать toast-API | Сравнительная таблица в главе 6. Для решения часто хватает её одной |
| Реализуете AppNotification | Глава 7: минимальная реализация, настройки csproj, доставка runtime на целевые машины |
| Нужен прогресс-бар и более сложные уведомления | Глава 8. На этапе «просто запустить» её можно пропустить |
| Разбираете, почему уведомление не появляется | Глава 9: где смотреть настройки ОС и ограничения процесса с повышенными правами |
| Нужно уведомить пользователя из службы Windows | Глава 10 |
Термины в этой статье
| Термин | Смысл |
|---|---|
| Область уведомлений (system tray) | Область со значками в правой части панели задач. В терминах Windows — «область уведомлений» (notification area)1 |
| Toast-уведомление | Баннер в правом нижнем углу экрана, который затем остаётся в центре уведомлений |
| TFM | Target Framework Moniker. Идентификатор в <TargetFramework> файла csproj; например net8.0-windows10.0.19041.0 задаёт версию .NET и целевую платформу |
| MSIX | Формат пакета приложений Windows. Приложение, распространяемое в этом формате, называют упакованным (packaged) |
| Неупакованное (unpackaged) приложение | Обычный exe без пакета MSIX. WPF, WinForms и классические Win32-приложения по умолчанию относятся сюда2 |
| AUMID | AppUserModelID. Идентификатор, по которому Windows отличает приложения; им же помечают отправителя toast |
| Сеанс 0 | С Windows Vista — отдельный сеанс для служб. Интерактивного UI в нём нет (глава 10)3 |
1. Сначала выводы
- Значок в области уведомлений по-прежнему делают через
System.Windows.Forms.NotifyIcon. Он появится, только если задатьIconи выставитьVisible = true(по умолчаниюfalse). Текст подсказкиTextначиная с .NET 6 — не длиннее 127 символов (раньше — 63).45 - В WPF нет элемента, эквивалентного NotifyIcon. В официальной таблице соответствий прямо сказано «нет эквивалентного элемента». Минимальный вариант — в csproj включить и
UseWPF, иUseWindowsFormsи взять NotifyIcon из WinForms.67 - «По крестику окно закрывается, процесс остаётся в трее» делается так: в
FormClosingставятe.Cancel = trueи вызываютHide(). Отдельный явный выход обязателен (пункт «Выход» в меню значка), и поCloseReasonнельзя мешать завершению работы ОС.8 - Рекомендация по toast-API на 2026 год однозначна. И для WinForms, и для WPF, и для неупакованного приложения берите
AppNotificationManagerиз Windows App SDK (NuGet: Microsoft.WindowsAppSDK).Windows.UI.Notifications— «только поддержка», компонент уведомлений Community Toolkit архивирован.910 - AppNotification из Windows App SDK работает и из обычного exe без идентификатора пакета.
AppNotificationManager.Default.Register()сам регистрирует COM-сервер и делает работу, эквивалентную AUMID, поэтому ярлык в меню «Пуск», который требовался в прошлом поколении, больше не нужен. Но уведомления из процесса с правами администратора не поддерживаются.111213 - Toast — не канал, который «обязательно попадёт в глаза». Пока включён режим «Не беспокоить» (Focus Assist), баннер не показывается, уведомление сразу уходит в центр уведомлений; пользователь может и вовсе отключить уведомления. Не стройте бизнес-процесс на допущении, что «toast был показан».1415
- Служба Windows toast показать не может (в сеансе 0 нет UI). Обычная схема — агент со значком в сеансе пользователя и связь по IPC.3
На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 18, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle
2. Основы значка в области уведомлений через NotifyIcon
System.Windows.Forms.NotifyIcon — компонент, который ставит значок в область уведомлений в правой части панели задач. Фоновые приложения используют его как точку входа: показать состояние и открыть меню операций.4
private NotifyIcon _trayIcon = null!;
private void InitializeTrayIcon()
{
var menu = new ContextMenuStrip();
menu.Items.Add("Открыть", null, (_, _) => ShowMainWindow());
menu.Items.Add(new ToolStripSeparator());
menu.Items.Add("Выход", null, (_, _) => ExitApplication());
_trayIcon = new NotifyIcon
{
Icon = Properties.Resources.AppIcon, // Icon обязателен; без него значок не появится
Text = "Мониторинг заказов ── работает", // подсказка (в .NET 6+ не длиннее 127 символов)
ContextMenuStrip = menu,
Visible = true // по умолчанию false; true — и только тогда значок виден
};
_trayIcon.DoubleClick += (_, _) => ShowMainWindow();
}
Три вещи, которые стоит зафиксировать.
- Значок появляется, только когда заданы и
Icon, иVisible = true. УVisibleзначение по умолчанию —false.4 - У
Textесть предел длины. С .NET 6 — 127 символов, в .NET 5 и более ранних версиях и в .NET Framework — 63; превышение даётArgumentException. Если в подсказку набивать слишком много сведений о состоянии, в какой-то момент приложение просто упадёт на исключении.516 - Меню по правому щелчку привязывают через
ContextMenuStrip. Значок часто остаётся единственной видимой точкой входа, поэтому пункты «Открыть» и «Выход» нужны как минимум.17
Ещё одно: значок — не только вход для уведомлений, но и постоянно видимый индикатор состояния. Icon и Text можно менять на ходу. Обычный значок в норме, предупреждающий — при сбое, в подсказке — время последней обработки: даже если пользователь пропустил toast, по значку видно, что происходит сейчас. Toast сообщает о событии в этот момент, значок держит текущее состояние — так роли и делятся.
3. «Закрыли окно — осталось в трее» и корректный выход
Классика приложений со значком: крестик не завершает процесс, а только прячет окно. Саму отмену закрытия в FormClosing написать несложно, но если сделать это небрежно, получаются две проблемы: из приложения больше нельзя выйти и Windows не может завершить работу.
private bool _exitRequested;
protected override void OnFormClosing(FormClosingEventArgs e)
{
// Пользователь нажал только крестик — скрыть, не закрывать
if (!_exitRequested && e.CloseReason == CloseReason.UserClosing)
{
e.Cancel = true;
Hide();
return;
}
// Пункт «Выход», завершение работы ОС и т. п. — закрывать как обычно
base.OnFormClosing(e);
}
private void ExitApplication()
{
_exitRequested = true;
_trayIcon.Visible = false;
_trayIcon.Dispose();
Close();
}
FormClosing срабатывает до закрытия формы; e.Cancel = true отменяет закрытие. CloseReason при этом нужно смотреть всегда. Отменяйте только UserClosing (действие пользователя) и не отменяйте закрытие по инициативе ОС, например WindowsShutDown.818
В WPF та же идея: в официальной документации прямо описан шаблон e.Cancel = true в Window.Closing и затем Hide().19
У таких приложений рядом ещё одна проблема: два экземпляра обработают одно и то же событие дважды. Защиту от повторного запуска разбирает статья «Как не дать Windows-приложению запуститься второй раз: именованный Mutex и активация уже работающего экземпляра». Саму «начинку» — цикл наблюдения, периодическую работу — удобно строить на том, что описано в «Зачем в настольном приложении нужны .NET Generic Host и BackgroundService».
4. Значок в области уведомлений в приложении WPF
В WPF нет элемента, эквивалентного NotifyIcon. Это не пробел в документации, а зафиксированное отсутствие: в официальной таблице «элементы WinForms и их аналоги в WPF» стоит «No equivalent control».6
Реалистичных варианта два.
| Вариант | Суть | Когда выбирать |
|---|---|---|
| NotifyIcon из WinForms рядом с WPF | В csproj включить и UseWPF, и UseWindowsForms |
Без лишних зависимостей. Если нужны только значок и меню — этого достаточно |
| Стороннее решение (например H.NotifyIcon) | Реализация значка в трее для WPF/WinUI | Если хочется писать в XAML или нужен более богатый popup. Это не официальный компонент Microsoft |
Первый вариант — просто два свойства рядом в SDK-style csproj.7
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
</PropertyGroup>
После этого System.Windows.Forms.NotifyIcon создаётся обычным new из Application в WPF. Меню окажется WinForms (ContextMenuStrip), но для меню значка это на практике почти никогда не мешает.
5. Три типичные ошибки со значком в трее
5.1. По умолчанию значок попадает в «скрытые» — принудительно показать его нельзя
Просьба «пусть наш значок всегда виден на панели задач» возникает обязательно, но из программы этого сделать нельзя. В официальной документации сказано прямо: значки по умолчанию попадают в область переполнения (скрытые значки), и решить, поднимать ли значок в постоянно видимую область уведомлений, может только пользователь.1
В Windows 11 пользователь включает или выключает это для каждого приложения в «Параметры → Персонализация → Панель задач → Другие значки системного лотка». Если постоянная видимость важна в эксплуатации, правильный ответ — написать в инструкции по установке или в справке: «включите этот параметр», а не пытаться навязать его кодом.
В инструкции лучше дать полный путь по меню — тогда можно провести человека и по телефону. Пример текста:
- Щёлкните правой кнопкой мыши по пустому месту на панели задач и выберите «Параметры панели задач».
- На открывшейся странице откройте «Другие значки системного лотка».
- Найдите в списке это приложение и включите переключатель.
※ Если значка не видно, нажмите кнопку «^» (шеврон) справа на панели задач — скрытые значки появятся временно.
5.2. После перезапуска Проводника значок пропадает — TaskbarCreated
Если Проводник падает или перезапускается и пересоздаёт панель задач, уже зарегистрированные значки в области уведомлений исчезают. Оболочка при создании панели задач рассылает сообщение TaskbarCreated всем окнам верхнего уровня; на уровне Win32 правило такое: получили сообщение — зарегистрируйте значок заново.20
NotifyIcon в WinForms обрабатывает это сообщение внутри и перерегистрируется сам, поэтому обычно об этом можно не думать. Если вы вызываете Shell_NotifyIcon через P/Invoke (или сопровождаете нативное приложение со значком в трее), проверьте, что эта повторная регистрация есть. Симптом «после перезапуска Проводника пропал только значок, процесс жив» почти всегда именно этот.
5.3. Без Dispose значок остаётся висеть
На уровне Win32 ненужный значок нужно явно удалить (NIM_DELETE); в WinForms это делает NotifyIcon.Dispose().1 На пути выхода всегда проходите Visible = false → Dispose(). Если процесс упал и удаление не случилось, значок-призрак висит, пока на него не наведут курсор — любой, кто писал такие приложения, это хотя бы раз видел.
Безопасный вариант — как в коде главы 3: в обработчике «Выход» сначала убрать значок, потом вызвать Close().
6. Toast-уведомления: какой API брать
Toast — баннер в правом нижнем углу, который затем остаётся в центре уведомлений. Способ показать его из настольного приложения за десять лет сменился не один раз, и в поиске полно старых примеров. Картина на 2026 год такая.9
| API | NuGet / пространство имён | Статус (на 2026 год) | Решение |
|---|---|---|---|
| Windows App SDK AppNotification | Microsoft.WindowsAppSDK / Microsoft.Windows.AppNotifications |
Официальная рекомендация для WinForms, WPF и неупакованного Win32 | Для нового кода — это |
| Community Toolkit Notifications | Microsoft.Toolkit.Uwp.Notifications | Архивирован. В актуальный Community Toolkit не входит, последнее обновление — 2022 год | Только поддержка существующего кода. В новые проекты не брать |
| Прямой WinRT | Windows.UI.Notifications (через Windows TFM) |
Только поддержка. Рекомендуется в основном для UWP | В новые проекты не брать |
| NotifyIcon.ShowBalloonTip | System.Windows.Forms |
API balloon-уведомлений. В Windows 10 рисуется как toast, в Windows 11 — короткое появление, в центр уведомлений не попадает | Для простых случаев можно. Поведение зависит от версии ОС |
Два уточнения.
ShowBalloonTip— из разряда «старый API, но он жив». В Windows 10 balloon рисуется как toast и до закрытия остаётся в центре уведомлений. В Windows 11 это ближе к прежнему короткому balloon: исчез — в центре уведомлений его нет. Заданное время показа (timeout) сейчас тоже игнорируется, действует настройка специальных возможностей ОС. Для приложения со значком, которое лишь слегка сообщает о смене своего состояния, этого достаточно. Если нужна история в центре уведомлений — в Windows 11ShowBalloonTipдля этого бесполезен.21- У классического способа вызвать «сырой»
Windows.UI.Notificationsиз настольного приложения есть известное ограничение: toast не появится, пока в меню «Пуск» нет ярлыка с AUMID (AppUserModelID).22 Именно эту лишнюю работу убирает AppNotification в следующей главе.
Где кончается «для простых случаев можно», границу удобнее провести по функциям. Если выполнены все пять пунктов ниже, ShowBalloonTip достаточно, зависимость от Windows App SDK наращивать незачем.
- Приложение уже имеет значок в трее (balloon — уведомление при значке, без него его нет)
- На уведомлении не нужны кнопки и поле ввода. Достаточно, чтобы щелчок вывел окно приложения вперёд
- История в центре уведомлений не нужна. В Windows 11 её и не будет, «посмотреть потом» этим API не сделать21
- Не нужны прогресс-бар и обновление уже показанного уведомления
- Уведомление — подсказка «заметили — отреагируете быстрее», пропуск не ломает работу
Если выполняется хотя бы один пункт из следующей таблицы, берите AppNotification из главы 7.
| Требование | Почему |
|---|---|
| На уведомлении нужны кнопки («Открыть журнал» и т. п.) или поле ввода | У balloon только заголовок, текст и значок, элементов управления нет |
| В Windows 11 уведомление должно остаться в центре уведомлений | Balloon в Windows 11 туда не попадает21 |
| Долгий процесс нужно обновлять в одном уведомлении | Прогресс-бар и UpdateAsync есть у AppNotification (глава 8) |
| Нужно обработать возврат, даже если приложение не было запущено | Balloon существует, только пока процесс жив и значок показан. AppNotification по щелчку может запустить процесс (глава 7) |
| Старое уведомление нужно позже снять или заменить из кода | Управление через Tag / Group — у AppNotification (глава 8) |
Если сомневаетесь, ориентир один: нужно ли пользователю что-то сделать с этим уведомлением. Только прочитать — balloon, нажать — AppNotification.
7. Минимальная реализация Windows App SDK AppNotification
API уведомлений приложений в Windows App SDK работает и из обычного неупакованного exe, без идентификатора пакета.11 Но «поставили NuGet — и готово» здесь нет: есть требования и у разработки, и у доставки. По порядку.
7.1. Подготовка проекта: NuGet и csproj
Официальные предпосылки три: это проект WPF или WinForms на .NET 6 и новее, проект умеет вызывать WinRT API, и он настроен на Windows App SDK.12
Делать нужно две вещи. Сначала добавьте NuGet-пакет Microsoft.WindowsAppSDK (в Visual Studio: «Средства → Диспетчер пакетов NuGet → Управление пакетами NuGet для решения»). В этом пакете лежат подпакеты компонентов Windows App SDK; официально рекомендуют начинать именно с основного пакета.2
Затем правьте csproj. Если к примеру из главы 4 (WPF + WinForms) добавить часть Windows App SDK, получится так.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<!-- Чтобы вызывать WinRT API, в TFM нужна и версия Windows -->
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
<!-- Неупакованное (не MSIX) приложение: runtime инициализируется при старте -->
<WindowsPackageType>None</WindowsPackageType>
</PropertyGroup>
<ItemGroup>
<!-- Версию подставляет то, что выбрали в диспетчере пакетов NuGet -->
<PackageReference Include="Microsoft.WindowsAppSDK" Version="(выбранная стабильная версия)" />
</ItemGroup>
</Project>
Ключ — <WindowsPackageType>None</WindowsPackageType>. Неупакованное приложение должно инициализировать runtime Windows App SDK до любых его функций; это свойство включает автоматическую инициализацию при запуске. После сборки в проект добавляются MddBootstrapAutoInitializer.cs и WindowsAppSDK-VersionInfo.cs. Bootstrap API вызывают напрямую только если нужна более тонкая настройка, вместо автоинициализации.2
7.2. Доставка runtime на целевые машины
Это часто пропускают. Для неупакованного приложения доставить runtime Windows App SDK на ПК пользователя — обязанность того, кто распространяет приложение.23 Одного exe недостаточно.
Отдельно: неупакованному приложению нужен и распространяемый компонент Visual C++.23 Способов доставки два.
| Способ | Что делается | Когда уместен |
|---|---|---|
| Запустить установщик | Запустить официальный WindowsAppRuntimeInstall.exe. Для x86 / x64 / Arm64 — отдельные установщики, и каждый кладёт только пакет своей архитектуры23 |
Обычно этот |
| Поставить MSIX-пакет напрямую | Вложить MSIX в свой setup или MSI и развернуть его23 | Если нужно точнее контролировать установку |
У установщика нет UI, его вызывают цепочкой из своего установщика. Есть режим без участия пользователя.23
:: Установить без окон и без текстового вывода
WindowsAppRuntimeInstall.exe --quiet
:: Принудительно обновить, завершив процессы, которые используют Windows App SDK (с 1.1)
WindowsAppRuntimeInstall.exe --force
Три вещи, которые стоит знать при установке.23
- Есть ли права администратора — от этого зависит результат. Если установщик запущен с повышением и у пользователя есть права администратора, регистрация идёт на всю систему (включая будущих пользователей). Иначе ставится только для того, кто запустил. Код возврата
0x80070005как раз про «нет повышения / нет прав администратора». - Успех смотрят по коду возврата.
0x0— успех,0x80073d06— не удалось установить один или несколько пакетов. Если вызываете цепочкой из своего установщика, этот код нужно проверять всегда. - Runtime на машине разработчика должен совпадать с версией и каналом выпуска NuGet-пакета, на который ссылается проект.2 «На разработке показывается, у клиента — нет» часто начинается отсюда.
Если распространяете как MSIX, runtime можно объявить зависимостью пакета, но тогда в Package.appxmanifest нужно прописать COM-активатор (см. конец 7.3).
7.3. Минимальная реализация
using System;
using System.Windows.Forms;
using Microsoft.Windows.AppNotifications;
using Microsoft.Windows.AppNotifications.Builder;
internal static class ToastActivation
{
private static readonly object Gate = new();
private static Action? _pending;
private static bool _uiReady;
// Один раз при старте. Порядок важен: сначала обработчик, затем Register()
public static void Initialize()
{
AppNotificationManager.Default.NotificationInvoked += (_, args) =>
{
// Сюда приходят щелчок по toast и нажатие кнопки (различайте по args.Arguments)
if (!args.Arguments.TryGetValue("action", out var action) || action != "openLog")
{
return;
}
// NotificationInvoked приходит на фоновом потоке.
// Всё, что трогает UI, обязательно передавайте на UI-поток
Dispatch(ShowLogWindow);
};
AppNotificationManager.Default.Register();
}
// Вызывать из OnShown у MainForm. Здесь проигрывается то, что пришло до появления формы
public static void MarkUiReady()
{
Action? pending;
lock (Gate)
{
_uiReady = true;
pending = _pending;
_pending = null;
}
// OnShown уже на UI-потоке, выполнять можно сразу
pending?.Invoke();
}
private static void Dispatch(Action action)
{
lock (Gate)
{
if (!_uiReady)
{
// Щелчок по toast сам запустил приложение: в этот момент
// ещё нет ни одной формы. Откладываем, MarkUiReady проиграет позже
_pending = action;
return;
}
}
MainWindow()?.Invoke(action);
// Для WPF: Application.Current.Dispatcher.Invoke(action);
}
private static Form? MainWindow() =>
Application.OpenForms.Count > 0 ? Application.OpenForms[0] : null;
private static void ShowLogWindow() { /* открыть окно журнала */ }
}
// Сторона MainForm. Если приложение стартовало по щелчку по toast, действие подхватывается здесь
protected override void OnShown(EventArgs e)
{
base.OnShown(e);
ToastActivation.MarkUiReady();
}
ToastActivation.Initialize();
// Показать уведомление
var notification = new AppNotificationBuilder()
.AddText("Импорт заказов завершён")
.AddText("12 новых, 1 ошибка")
.AddButton(new AppNotificationButton("Открыть журнал")
.AddArgument("action", "openLog"))
.BuildNotification();
AppNotificationManager.Default.Show(notification);
На практике важно следующее.
- На
NotificationInvokedподписывайтесь доRegister(). Перепутаете порядок — пропустите событие, когда щелчок по toast запускает приложение.12 - Если приложение стартовало по щелчку, в момент обработчика ещё нет ни одной формы. Если смотреть
Application.OpenFormsи приnullничего не делать, действие на этом пути молча теряется. Как выше: отложите его и проиграйте, когда форма уже показана. Даже если событие вы получили в правильном порядке, выбросить его здесь — тот же результат. NotificationInvokedприходит на фоновом потоке. В официальном примере прямо сказано: событие на фоновом потоке, обновление UI нужно диспетчеризовать на UI-поток. Если из обработчика сразу открывать окна и трогать элементы, получите исключения по потокам или необъяснимые зависания. В WPF —Application.Current.Dispatcher.Invoke(...), в WinForms —Control.Invoke(...).12- Перед выходом вызывайте
AppNotificationManager.Default.Unregister(). В документации сказано: Unregister перед завершением освобождает ресурсы и позволяет следующему щелчку по уведомлению корректно запустить приложение. Встройте это в обработчик «Выход» из главы 3 рядом сNotifyIcon.Dispose()(есть ещёUnregisterAll()— стереть регистрацию целиком, если приложение снимают и уведомлениями больше пользоваться не будут).24 - В неупакованном приложении
Register()сам регистрирует COM-сервер и делает работу, эквивалентную AUMID. Ярлык и записи в реестре, которые требовал «сырой» WinRT, больше не нужны. Если упаковываете в MSIX, наоборот, COM-активатор нужно прописать в Package.appxmanifest.12 - Если выбираете self-contained доставку с встроенным runtime Windows App SDK, осторожнее.
AppNotificationManagerзависит от пакета Singleton Windows App SDK; для вызова из self-contained приложения есть официальные оговорки. Прочитайте их до выбора способа распространения.25 - Уведомления из процесса с правами администратора не поддерживаются.
Showне бросает исключение и тихо ничего не показывает — причину легко не заметить. Если приложению нужно повышение прав, имеет смысл отдать показ уведомлений отдельному процессу без повышения (этот приём разобран в «Как в Windows-приложении вынести только те операции, которым нужны права администратора»).13 - Если по щелчку по toast приложение не запущено, стартует новый процесс. В приложении со значком это нужно сочетать с защитой от повторного запуска (глава 3) и передавать работу уже существующему экземпляру.
8. Следующий шаг: toast с прогресс-баром и уборка уведомлений
8.1. Обновлять прогресс внутри toast — UpdateAsync
Для долгой операции вроде «идёт импорт 120 записей заказов» правильный ход — не слать новое уведомление снова и снова, а переписывать прогресс-бар внутри одного toast. У AppNotification для этого есть привязка данных и обновление.26
Сборка в три шага. Сначала каждое значение прогресс-бара делаете заполнителем через методы Bind, помечаете уведомление Tag (и при необходимости Group) и показываете. Дальше достаточно передать AppNotificationProgressData в UpdateAsync — меняется только содержимое уже показанного toast.
const string tag = "import-progress";
const string group = "batch";
// Первый показ: значения — привязки, начальное значение задаётся в Progress
var notification = new AppNotificationBuilder()
.AddText("Идёт импорт данных заказов")
.AddProgressBar(new AppNotificationProgressBar()
.BindStatus().BindValue().BindValueStringOverride())
.SetTag(tag)
.SetGroup(group)
.BuildNotification();
notification.Progress = new AppNotificationProgressData(1) // порядковый номер
{
Status = "Импорт...", Value = 0, ValueStringOverride = "0/120 записей"
};
AppNotificationManager.Default.Show(notification);
// При каждом шаге прогресса: увеличить номер и обновить
var data = new AppNotificationProgressData(2)
{
Status = "Импорт...", Value = 45 / 120.0, ValueStringOverride = "45/120 записей"
};
await AppNotificationManager.Default.UpdateAsync(data, tag, group);
Порядковый номер — ненулевое целое, задаёт порядок обновлений; если несколько обновлений приходят вперемешку, показывается то, у которого номер больше. Это защита от отката к старому значению, когда обновления летят параллельно из асинхронного кода, поэтому номер только увеличивайте.27
8.2. Когда «обновлять», а когда «заменять»
Внешне «обновить» можно и повторным Show нового уведомления с тем же Tag — это замена, — но поведение другое.26
| Замена (повторный Show с тем же Tag) | Обновление (UpdateAsync) | |
|---|---|---|
| Место в центре уведомлений | Переезжает наверх | Остаётся на месте |
| Что можно изменить | Всё, включая макет | Только привязанный прогресс-бар и верхний текст |
| Всплывающий баннер | Может появиться снова | Не появляется снова (переписывается тихо) |
| Если пользователь уже закрыл | Доставляется всегда | Завершается неудачей |
Официальная рекомендация тоже ясна: ход работы — обновлять, завершение — заменять. Нет смысла всплывать баннером на каждый шаг с 50% на 65%, а «импорт завершён (3 ошибки)» должно дойти, даже если пользователь уже закрыл уведомление о прогрессе. На практике при завершении убирают прогресс-бар и подставляют другой макет с кнопками вроде «Посмотреть ошибки».
8.3. Убрать старые уведомления из центра уведомлений
Toast копятся в центре уведомлений, пока пользователь их не снимет. Если приложение каждый день шлёт по нескольку штук, центр забивается вашими старыми уведомлениями, и важное тонет.
С Tag / Group свои уведомления можно удалять из кода (по тегу, по группе, все сразу). Можно задать и срок жизни. Правила уборки — «прогресс снимать по завершении», «информационным давать короткий срок» — лучше решить в тот же момент, когда решаете, какие уведомления слать.28
9. Заложить в дизайн случаи, когда уведомление «не доходит»
Toast — не надёжный канал доставки. Есть нормальные пути, на которых он не показывается или остаётся незамеченным.
| Случай | Поведение |
|---|---|
| Включён режим «Не беспокоить» (Focus Assist / Do not disturb) | Баннер не показывается, уведомление сразу уходит в центр уведомлений |
| Уведомления выключены для приложения или для всей системы | Ничего не показывается |
| AppNotification из процесса с правами администратора | Не поддерживается. Ничего не показывается |
| Полноэкранное приложение и т. п. | ОС может подавить показ |
Режим «не беспокоить» в Windows 10 — это «Фокусировка внимания» (Focus assist), в Windows 11 — переключатель «Не беспокоить» в «Параметры → Система → Уведомления»; им управляет пользователь или групповая политика. Есть даже правило: в первый час после обновления ОС режим включается сам.14 Фокусировка внимания — не «не слать уведомления», а «не показывать баннер и отправить в центр уведомлений»: позже в центре уведомлений пользователь их увидит.15
Где смотреть — держите полным путём по меню, на месте это быстрее. «Уведомление не появляется» часто разбирают по телефону или в чате.
| Что проверить | Куда смотреть (Windows 11) |
|---|---|
| Не включён ли режим «Не беспокоить» | Параметры → Система → Уведомления → переключатель «Не беспокоить» |
| Нет ли условий, которые включают его автоматически | На той же странице «Уведомления» — условия автоматического включения |
| Не выключены ли уведомления самого приложения | На той же странице «Уведомления» прокрутите вниз и найдите приложение в списке |
| Нет баннера или уведомление вообще не дошло | Щёлкните дату и время в правой части панели задач, откройте центр уведомлений и посмотрите, лежит ли оно там |
| Не спрятан ли только значок в трее | Параметры → Персонализация → Панель задач → Другие значки системного лотка (глава 5.1) |
В Windows 10 аналог «не беспокоить» — «Параметры → Система → Фокусировка внимания». Сначала проверьте, дошло ли уведомление в центр уведомлений: если дошло — ОС подавила баннер (режим «Не беспокоить» и т. п.), если нет — смотрите отправку со стороны приложения.
Для дизайна вывод простой.
- Toast — вспомогательный канал: заметили — отреагируете быстрее. Информацию, без которой работа не идёт, всегда можно найти на экране или в списке внутри приложения. Не стройте процесс на том, что «toast наверняка показали».
- Если прервать всё же нужно (например, предупреждение об аварийной остановке), есть
AppNotificationScenario.Urgent(важное уведомление): с согласия пользователя оно проходит сквозь режим «Не беспокоить»29, но если злоупотреблять, пользователь выключит уведомления приложения целиком. Оставляйте это только для действительно неотложных случаев.
Как показывать на экране состояние внешнего оборудования — более широкая тема, см. «Как проверять и показывать состояние внешнего оборудования».
10. Когда нужно уведомить пользователя из службы Windows
Требование звучит естественно: «фоновая служба нашла сбой — пусть вошедшему пользователю придёт toast». Но служба не может показать toast напрямую. С Windows Vista службы работают в отдельном сеансе 0 и не могут показывать UI, с которым взаимодействует пользователь.3
Обычная схема — вынести UI в процесс в сеансе пользователя.
[Сеанс 0] [Сеанс пользователя]
Служба Windows ←─ IPC ─→ Агент со значком в трее
(мониторинг и бизнес-логика) именованные каналы и т. п. (NotifyIcon + AppNotification)
- Служба занимается наблюдением и бизнес-логикой и по IPC передаёт агенту то, о чём нужно сообщить.
- Агент — приложение со значком в области уведомлений (как раз главы 2–8 этой статьи), которое стартует при входе пользователя и показывает полученные события toast или сменой значка.
- Выбор IPC разобран в «Как выбрать межпроцессное взаимодействие в Windows», устройство службы — в «Как устроить службу Windows и как её эксплуатировать», сами сеансы — в «Как понимать изоляцию сеансов Windows: Session 0, RDP и несколько пользователей сразу».
Если одновременно входят несколько пользователей (RDP, общий терминал), агент работает по одному на сеанс. Заранее зафиксируйте требование «кому слать» (всем или только определённому пользователю) — тогда схема не разъедется.
11. Итог
- Значок в трее — это
NotifyIcon. Показ:Icon+Visible = true; уTextлимит 127 символов (.NET 6+); на выходе обязательноDispose(). Значок и подсказку используйте и как постоянно видимый индикатор текущего состояния. - «По крестику остаться в трее» делается отменой
FormClosing, но отменяйте толькоCloseReason.UserClosingи всегда давайте явный пункт «Выход». - Постоянно показывать значок может решить только пользователь. Исчезновение после перезапуска Проводника закрывается повторной регистрацией по
TaskbarCreated(в WinForms — автоматически). - Для нового toast один выбор:
AppNotificationManagerиз Windows App SDK. Работает и из неупакованного exe без идентификатора пакета,Register()сам закрывает нудную регистрацию. Уведомления Community Toolkit архивированы, «сырой» WinRT — только поддержка. Если не нужны ни кнопки, ни история, ни прогресс, лёгкое уведомление можно оставить наShowBalloonTip(глава 6). - Для неупакованного приложения доставить runtime Windows App SDK на целевые машины — обязанность того, кто распространяет приложение. В csproj пишите
<WindowsPackageType>None</WindowsPackageType>и в свой установщик закладывайте цепочку сWindowsAppRuntimeInstall.exe(глава 7.2). - Ход долгой операции тихо переписывайте прогресс-баром в одном toast через
UpdateAsync, завершение — заменой с баннером. ВешайтеTag/Groupи закладывайте удаление старых уведомлений. - Toast может не дойти из-за режима «Не беспокоить» или выключенных уведомлений — это вспомогательный канал. Обязательную информацию держите доступной внутри приложения и помните: из процесса с повышенными правами уведомления не поддерживаются.
- Уведомление от службы делается разделением: агент со значком в сеансе пользователя плюс IPC.
Похожие статьи
- Как не дать Windows-приложению запуститься второй раз: именованный Mutex и активация уже работающего экземпляра
- Как устроить службу Windows и как её эксплуатировать: служба или планировщик заданий, и как сделать службу из BackgroundService
- Как выбрать межпроцессное взаимодействие в Windows: именованные каналы / TCP / gRPC / общая память / COM
- Как понимать изоляцию сеансов Windows: Session 0, RDP и несколько пользователей сразу
- Зачем в настольном приложении нужны .NET Generic Host и BackgroundService
- Как в Windows-приложении вынести только те операции, которым нужны права администратора
- Как проверять и показывать состояние внешнего оборудования — не ограничиваться статусом «подключено»
Смежные области консультаций
KomuraSoft LLC (合同会社小村ソフト) проектирует и реализует инструменты наблюдения со значком в области уведомлений и агенты уведомлений, разбирает схемы фоновых систем в связке со службами Windows и ищет причины сбоев в уже существующих таких приложениях.
Источники
-
Microsoft Learn, Notification Area (Windows Shell). О том, что значки по умолчанию попадают в область переполнения, что поднять значок в область уведомлений может только пользователь, и что ненужный значок удаляют через NIM_DELETE. ↩ ↩2 ↩3
-
Microsoft Learn, Use the Windows App SDK in an existing project. О том, что настольные WPF-, WinForms- и Win32-приложения по умолчанию неупакованные; что рекомендуют ставить основной NuGet-пакет
Microsoft.WindowsAppSDK; что перед функциями Windows App SDK неупакованному приложению нужна инициализация runtime и<WindowsPackageType>None</WindowsPackageType>включает автоинициализацию при старте; что при сборке добавляютсяMddBootstrapAutoInitializer.csиWindowsAppSDK-VersionInfo.cs; что runtime на машине разработчика должен совпадать с версией и каналом NuGet-пакета. ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, Service Changes for Windows Vista / Interactive Services. О том, что сеанс 0 предназначен для служб и не поддерживает взаимодействие с пользователем, и о рекомендации создать процесс в сеансе пользователя через CreateProcessAsUser и связать его по IPC, чтобы что-то показать пользователю. ↩ ↩2 ↩3
-
Microsoft Learn, NotifyIcon Class / NotifyIcon.Visible Property. О том, что это компонент, показывающий значок в области уведомлений, что нужно задать свойство Icon и что Visible по умолчанию равно false. ↩ ↩2 ↩3
-
Microsoft Learn, NotifyIcon.Text Property. О пределе длины текста подсказки и об ArgumentException при превышении. ↩ ↩2
-
Microsoft Learn, Windows Forms Controls and Equivalent WPF Controls. О том, что в WPF нет элемента, эквивалентного NotifyIcon. ↩ ↩2
-
Microsoft Learn, MSBuild properties for .NET desktop projects. О том, что UseWPF и UseWindowsForms можно включить вместе. ↩ ↩2
-
Microsoft Learn, Form.FormClosing Event. О том, что событие возникает до закрытия формы и закрытие можно отменить через Cancel. ↩ ↩2
-
Microsoft Learn, Windows notifications overview. Таблица рекомендуемых API уведомлений по типу приложения: рекомендация Windows App SDK для WinForms/WPF/неупакованного Win32 и статус «только поддержка» у Windows.UI.Notifications. ↩ ↩2
-
Microsoft Learn, Windows Community Toolkit — Notifications (archive). О том, что компонент Notifications архивирован и заменён уведомлениями приложений Windows App SDK. ↩
-
Microsoft Learn, Use the Windows App SDK in an existing project. О том, что API уведомлений приложений Windows App SDK работает и в упакованном, и в неупакованном приложении и не требует идентификатора пакета. ↩ ↩2
-
Microsoft Learn, Quickstart: App notifications in the Windows App SDK. Об использовании AppNotificationManager/AppNotificationBuilder, автоматической регистрации через Register() в неупакованном приложении, о том, что на NotificationInvoked нужно подписаться до Register(), и о записях в манифесте упакованного приложения. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, App notifications overview. О том, что отправка уведомлений из приложения с правами администратора не поддерживается. ↩ ↩2
-
Microsoft Learn, Notifications don’t appear or aren’t displayed. О подавлении уведомлений режимом «не беспокоить» (Quiet Hours / Focus Assist), автоматическом включении в первый час после обновления ОС и шагах проверки, если уведомления не показываются. ↩ ↩2
-
Microsoft Learn, Focus assist. О том, что фокусировка внимания подавляет баннер и отправляет уведомление сразу в центр уведомлений. ↩ ↩2
-
Microsoft Learn, NotifyIcon.Text maximum text length increased. О критическом изменении в .NET 6: максимальная длина Text выросла с 63 до 127 символов. ↩
-
Microsoft Learn, FormClosingEventArgs Class. О том, что причину закрытия формы можно различить по CloseReason. ↩
-
Microsoft Learn, How to close a window or dialog box (WPF). О шаблоне события Closing в WPF: e.Cancel = true и Hide() вместо закрытия. ↩
-
Microsoft Learn, The Taskbar. О рассылке сообщения TaskbarCreated при создании панели задач и о том, что получив его, значок в трее нужно зарегистрировать заново. ↩
-
Microsoft Learn, Shell_NotifyIconW function. О том, что balloon в Windows 10 показывается как toast (баннер) и остаётся в центре уведомлений, в Windows 11 — короткое появление без записи в центр уведомлений, а время показа следует настройкам специальных возможностей. ↩ ↩2 ↩3
-
Microsoft Learn, Send a local toast notification from a C# app (Win32/классический API). О том, что при «сыром» ToastNotificationManager в меню «Пуск» обязателен ярлык с AUMID. ↩
-
Microsoft Learn, Windows App SDK deployment guide for framework-dependent apps packaged with external location or unpackaged. О том, что разработчик неупакованного приложения отвечает за доставку runtime Windows App SDK пользователю; что нужен распространяемый компонент Visual C++; что есть два способа — установщик и прямая установка MSIX; что установщики разделены по x86, x64 и Arm64 и каждый ставит только пакет своей архитектуры; о параметрах
--quietи--force; о том, что при повышении и правах администратора установка идёт на всю систему, иначе — только для запустившего пользователя; о кодах возврата0x0,0x80073d06и0x80070005; о том, что свой setup может вызвать установщик через ShellExecute. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 -
Microsoft Learn, AppNotificationManager.Register Method. О том, что перед выходом нужно вызвать Unregister, чтобы освободить ресурсы и чтобы последующий запуск по уведомлению работал корректно, и об UnregisterAll, если уведомлениями больше пользоваться не будут. ↩
-
Microsoft Learn, AppNotificationManager.UpdateAsync Method / Windows App SDK deployment architecture. О том, что класс AppNotificationManager зависит от пакета Singleton Windows App SDK, и об оговорках при вызове из self-contained приложения. ↩
-
Microsoft Learn, App notification progress bar and data binding. О привязке данных через методы Bind, обновлении через AppNotificationProgressData и UpdateAsync, различиях обновления и замены (место в центре уведомлений, что можно менять, повтор баннера, поведение после закрытия пользователем) и о том, что при завершении стоит рассмотреть замену. ↩ ↩2
-
Microsoft Learn, AppNotificationProgressData(UInt32) Constructor. О том, что порядковый номер — ненулевое целое, задающее порядок обновлений, и при нескольких обновлениях показываются данные с наибольшим номером. ↩
-
Microsoft Learn, Remove app notifications. О назначении тега и группы, удалении уже показанных уведомлений и сроке жизни. ↩
-
Microsoft Learn, App notification content. О том, что важные уведомления с scenario=”urgent” с согласия пользователя имеют приоритет над режимом «не беспокоить». ↩
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Аутентификация Entra ID в WinForms и WPF: MSAL.NET и брокер WAM
Как добавить вход через Entra ID в десктопное приложение WinForms или WPF: общедоступный клиент, регистрация приложения, AcquireTokenSile...
Окончание драйверов принтера Windows ── как готовить печать форм и этикеток в бизнес-приложениях
Microsoft поэтапно прекращает сопровождение драйверов принтера v3/v4; с июля 2026 IPP class driver предпочтут. Что исчезает в Windows pro...
CI/CD для WinForms / WPF: сборка, подпись и распространение в GitHub Actions
Практическое руководство по CI/CD для WinForms / WPF в GitHub Actions. Минимальный YAML сборки и тестов на windows-latest, нумерация верс...
Если Microsoft Defender принял ваше Windows-приложение за вирус — ложные срабатывания и влияние на производительность
Разбираем штатный порядок действий, если Microsoft Defender ложно определяет ваше Windows-приложение как вредоносное. Как устроена соврем...
MAX_PATH и ловушки путей и имён файлов в Windows — лимит 260 символов, зарезервированные имена, точка в конце, регистр
Разбираем ограничения путей и имён файлов — типичную причину ошибки «файл не найден». Из чего складывается MAX_PATH=260, как включить дли...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Поток UI и таймеры
Поток UI WPF / WinForms, асинхронные операции, Dispatcher и проектирование таймеров.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Разработка приложений для Windows
Бизнес-приложения, интеграция оборудования и средства связи — от требований до разработки.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- Как в WPF сделать приложение со значком в области уведомлений?
- В WPF нет элемента, эквивалентного NotifyIcon, поэтому минимальный вариант — в csproj включить и UseWPF, и UseWindowsForms и взять NotifyIcon из WinForms. Если хочется писать ближе к XAML, есть сторонние библиотеки вроде H.NotifyIcon, но это не официальный компонент Microsoft — учитывайте это при выборе.
- Какой API брать для toast-уведомлений?
- Для новой реализации в 2026 году официальная рекомендация одна: AppNotificationManager из Windows App SDK, и для WinForms, и для WPF, и для неупакованного приложения. Microsoft.Toolkit.Uwp.Notifications архивирован и в актуальный Community Toolkit не входит, а WinRT ToastNotificationManager — только поддержка. Если существующий код на одном из двух последних уже работает, переписывать срочно не нужно, но выбирать их для нового проекта незачем.
- Что проверить, если toast не показывается?
- Сначала настройки ОС. Если включён режим «Не беспокоить» (Focus Assist), баннер не появляется, уведомление сразу уходит в центр уведомлений; то же самое, если уведомления выключены для приложения или для всей системы в «Параметрах». На стороне кода типичны два случая: AppNotification из процесса с правами администратора не поддерживается и молча не показывается, а «сырой» Windows.UI.Notifications из настольного приложения без AUMID тоже ничего не рисует.
- Может ли служба Windows показывать toast-уведомления?
- Нет. Начиная с Windows Vista службы работают в сеансе 0 и не могут показывать UI, с которым взаимодействует пользователь. Если нужно уведомить пользователя, обычная схема — процесс-агент со значком в области уведомлений в сеансе пользователя и связь со службой по IPC, например через именованные каналы; показ уведомления делает агент.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.