快速入门:Windows 应用 SDK 中的应用通知

显示任务栏上方的应用通知的屏幕截图。通知是对事件的提醒。显示了应用名称、事件名称、事件时间和事件位置。选择输入显示当前所选值“正在进行”。有两个标记为“RSVP”和“关闭”的按钮

在本快速入门中,你将使用 Windows 应用 SDK 创建一个桌面 Windows 应用程序,它能够发送和接收本地应用通知,这也称为 toast 通知。

重要

目前不支持提升的(管理员)应用的通知。

先决条件

示例应用

本快速入门涵盖 GitHub 上的通知示例应用的代码。

API 参考

有关应用通知的 API 参考文档,请参阅 Microsoft.Windows.AppNotifications 命名空间

步骤 1:添加命名空间声明

添加 Windows 应用 SDK 应用通知 Microsoft.Windows.AppNotifications 的命名空间。

using Microsoft.Windows.AppNotifications;

步骤 2:更新应用部件清单

如果你的应用未打包(即在运行时缺少程序包标识符),请跳到“步骤 3:注册以处理应用通知”。

如果你的应用已打包(包括使用外部位置打包):

  1. 打开你的 Package.appxmanifest。
  2. xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10" 命名空间添加到 <Package>
  3. windows.toastNotificationActivation 添加 <desktop:Extension> 以声明 COM 激活器 CLSID 可以通过导航到 Visual Studio 中“工具”下的“创建 GUID”来获取 CLSID。
  4. 使用同一 CLSID 为 COM 激活器添加 <com:Extension>
    1. Executable 属性中指定你的 .exe 文件。 在注册应用的通知时,.exe 文件必须是调用 Register() 的相同进程,这在“步骤 3”中进行了更详细的介绍。 在下面的示例中,我们使用 Executable="SampleApp\SampleApp.exe"
    2. 指定 Arguments="----AppNotificationActivated:" 以确保 Windows 应用 SDK 可以将通知的有效负载作为 AppNotification 类型进行处理。
    3. 指定 DisplayName

重要

警告:如果你在 appx 清单中使用 <uap:Protocol> 定义了一个 Windows.Protocol 应用扩展性类型,则单击通知时将启动同一应用的新进程,即使应用已在运行。

<!--Packaged apps only-->
<!--package.appxmanifest-->

<Package
  xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
  xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
  ...
  <Applications>
    <Application>
      ...
      <Extensions>

        <!--Specify which CLSID to activate when notification is clicked-->   
        <desktop:Extension Category="windows.toastNotificationActivation">
          <desktop:ToastNotificationActivation ToastActivatorCLSID="replaced-with-your-guid-C173E6ADF0C3" />
        </desktop:Extension>

        <!--Register COM CLSID-->    
        <com:Extension Category="windows.comServer">
          <com:ComServer>
            <com:ExeServer Executable="SampleApp\SampleApp.exe" DisplayName="SampleApp" Arguments="----AppNotificationActivated:">
              <com:Class Id="replaced-with-your-guid-C173E6ADF0C3" />
            </com:ExeServer>
          </com:ComServer>
        </com:Extension>
    
      </Extensions>
    </Application>
  </Applications>
 </Package>

步骤 3:注册以处理应用通知

注册应用以处理通知,然后在应用终止时注销。

在你的 App.xaml 文件中,为 AppNotificationManager::Default().NotificationInvoked 注册,然后调用 AppNotificationManager::Default().Register。 这些调用的顺序很重要。

重要

必须在调用 AppInstance.GetCurrent.GetActivatedEventArgs 之前调用 AppNotificationManager::Default().Register

当应用正在终止时,请调用 AppNotificationManager::Default().Unregister() 以释放 COM 服务器,并允许后续调用启动新进程。

// App.xaml.cs
namespace CsUnpackagedAppNotifications
{

    public partial class App : Application
    {
        private Window mainWindow;
        private NotificationManager notificationManager;
        
        public App()
        {
            this.InitializeComponent();
            notificationManager = new NotificationManager();
            AppDomain.CurrentDomain.ProcessExit += new EventHandler(OnProcessExit);
        }

        protected override void OnLaunched(LaunchActivatedEventArgs args)
        {
            mainWindow = new MainWindow();

            notificationManager.Init();
            
            // Complete in Step 5
            
            mainWindow.Activate();
        }

        void OnProcessExit(object sender, EventArgs e)
        {
            notificationManager.Unregister();
        }
    }
}


// NotificationManager.cs
namespace CsUnpackagedAppNotifications
{
    internal class NotificationManager
    {
        private bool m_isRegistered;

        private Dictionary<int, Action<AppNotificationActivatedEventArgs>> c_map;

        public NotificationManager()
        {
            m_isRegistered = false;

            // When adding new a scenario, be sure to add its notification handler here.
            c_map = new Dictionary<int, Action<AppNotificationActivatedEventArgs>>();
            c_map.Add(ToastWithAvatar.ScenarioId, ToastWithAvatar.NotificationReceived);
            c_map.Add(ToastWithTextBox.ScenarioId, ToastWithTextBox.NotificationReceived);
        }

        ~NotificationManager()
        {
            Unregister();
        }

        public void Init()
        {
            // To ensure all Notification handling happens in this process instance, register for
            // NotificationInvoked before calling Register(). Without this a new process will
            // be launched to handle the notification.
            AppNotificationManager notificationManager = AppNotificationManager.Default;

            notificationManager.NotificationInvoked += OnNotificationInvoked;

            notificationManager.Register();
            m_isRegistered = true;
        }

        public void Unregister()
        {
            if (m_isRegistered)
            {
                AppNotificationManager.Default.Unregister();
                m_isRegistered = false;
            }
        }

        public void ProcessLaunchActivationArgs(AppNotificationActivatedEventArgs notificationActivatedEventArgs)
        {
            // Complete in Step 5
        }

    }
}       

步骤 4:显示应用通知

带有按钮的应用通知

在继续之前,必须完成“步骤 3:注册以处理应用通知”。

现在,你将显示包含一个 appLogoOverride 图像和一个按钮的简单应用通知。

使用 AppNotificationBuilder 类构造应用通知,然后调用 Show 有关如何使用 XML 构造应用通知的详细信息,请参阅 Toast 内容通知 XML 架构中的示例。

注意

如果你的应用已打包(包括使用外部位置打包),则通知左上角处的应用图标源自 package.manifest。 如果你的应用未打包,则图标的来源如下:首先查看快捷方式,然后查看应用进程中的资源文件。 如果所有尝试都失败,则会使用 Windows 默认应用图标。 支持的图标文件类型为 .jpg.png.bmp.ico

// ToastWithAvatar.cs
class ToastWithAvatar
{
    public const int ScenarioId = 1;
    public const string ScenarioName = "Local Toast with Avatar Image";

    public static bool SendToast()
    {
        var appNotification = new AppNotificationBuilder()
            .AddArgument("action", "ToastClick")
            .AddArgument(Common.scenarioTag, ScenarioId.ToString())
            .SetAppLogoOverride(new System.Uri("file://" + App.GetFullPathToAsset("Square150x150Logo.png")), AppNotificationImageCrop.Circle)
            .AddText(ScenarioName)
            .AddText("This is an example message using XML")
            .AddButton(new AppNotificationButton("Open App")
                .AddArgument("action", "OpenApp")
                .AddArgument(Common.scenarioTag, ScenarioId.ToString()))
            .BuildNotification();

        AppNotificationManager.Default.Show(appNotification);

        return appNotification.Id != 0; // return true (indicating success) if the toast was sent (if it has an Id)
    }

    public static void NotificationReceived(AppNotificationActivatedEventArgs notificationActivatedEventArgs)
    {
        // Complete in Step 5   
    }
}

// Call SendToast() to send a notification. 

步骤 5:处理用户选择通知的操作

用户可以选择通知的正文或按钮。 你的应用需要处理调用,以响应与通知交互的用户。

可通过 2 种常见方法来处理此过程:

  1. 选择让应用在特定 UI 上下文中启动,或
  2. 选择让应用评估特定于操作的行为(如在通知正文中按下按钮),而不呈现任何 UI。 这也称为后台操作。

下面的代码示例(并非来自示例应用)演示了处理用户生成的操作的两种方式。 将 launch 值(对应于用户单击通知正文)、input 元素(快速回复文本框)和具有 arguments 值的按钮(对应于用户单击按钮)添加到你的通知的 XML 有效负载。 在你的 ProcessLaunchActivationArgs 中,对每个参数进行分支处理。

重要

对于桌面应用,在通知 XML 有效负载中设置 activationType="background" 的操作会被忽略。 必须改为处理激活参数,并决定是否显示窗口,如此步骤中所述。

带有回复的应用通知

// Example of how to process a user either selecting the notification body or inputting a quick reply in the text box. 

// Notification XML payload
//<toast launch="action=openThread&amp;threadId=92187">
//  <visual>
//      <binding template="ToastGeneric">
//          <image placement="appLogoOverride" hint-crop="circle" src="C:\<fullpath>\Logo.png"/>
//          <text>Local Toast with Avatar and Text box</text>
//          <text>This is an example message using</text>
//      </binding>
//  </visual>
//  <actions>
//      <input id="replyBox" type="text" placeHolderContent="Reply" />
//      <action
//          content="Send"
//          hint-inputId="replyBox"
//          arguments="action=reply&amp;threadId=92187" />
//  </actions>
//</toast>

void ProcessLaunchActivationArgs(const winrt::AppNotificationActivatedEventArgs& notificationActivatedEventArgs)
{
    // If the user clicks on the notification body, your app needs to launch the chat thread window
    if (std::wstring(notificationActivatedEventArgs.Argument().c_str()).find(L"openThread") != std::wstring::npos)
    {
        GenerateChatThreadWindow();
    }
    else // If the user responds to a message by clicking a button in the notification, your app needs to reply back to the other user with no window launched
    if (std::wstring(notificationActivatedEventArgs.Argument().c_str()).find(L"reply") != std::wstring::npos)
    {
        auto input = notificationActivatedEventArgs.UserInput();
        auto replyBoxText = input.Lookup(L"replyBox");

        // Process the reply text
        SendReplyToUser(replyBoxText);
    }
}

遵循以下准则

  1. 如果用户选择了某个通知,并且你的应用未运行,则预期是你的应用会启动,并且用户可以在通知的上下文中看到前台窗口。
  2. 如果用户选择了某个通知,并且你的应用处于最小化,则预期是你的应用会被引到前台,并且通知的上下文中会呈现一个新窗口。
  3. 如果用户调用了通知后台操作(例如用户通过在通知文本框中键入并点击回复来响应通知),则应用会处理有效负载而不呈现前台窗口。

如需更详细的示例,请参阅 GitHub 上的示例应用代码。

步骤 6:移除通知

当通知对用户不再相关时,移除通知。

在此示例中,用户已看过你的应用中某个群组聊天内的所有消息,于是你清除了来自该群组聊天的所有通知。 然后,该用户禁用了某个好友的通知,于是你清除了来自该好友的所有通知。 在显示之前,你首先将“Group”和“Tag”属性添加到了通知中,以便现在识别它们。


void SendNotification(winrt::hstring const& payload, winrt::hstring const& friendId, winrt::hstring const& groupChatId)
{
    winrt::AppNotification notification(payload);

    // Setting Group Id here allows clearing notifications from a specific chat group later
    notification.Group(groupChatId);

    // Setting Tag Id here allows clearing notifications from a specific friend later
    notification.Tag(friendId);

    winrt::AppNotificationManager::Default().Show(notification);
}

winrt::Windows::Foundation::IAsyncAction RemoveAllNotificationsFromGroupChat(const std::wstring groupChatId)
{
    winrt::AppNotificationManager manager = winrt::AppNotificationManager::Default();
    co_await manager.RemoveByGroupAsync(groupChatId);    
}

winrt::Windows::Foundation::IAsyncAction RemoveAllNotificationsFromFriend(const std::wstring friendId)
{
    winrt::AppNotificationManager manager = winrt::AppNotificationManager::Default();
    co_await manager.RemoveByTagAsync(friendId);    
}

其他功能

发送源自云的应用通知

要从云发送应用通知,请参照快速入门:Windows 应用 SDK 中的推送通知中的“发送源自云的应用通知”。

设置过期时间

如果通知中的消息仅在特定时段内相关,请使用 Expiration 属性设置应用通知的过期时间。 例如,如果要发送日历事件提醒,请将过期时间设置为日历事件的结束时间。

注意

默认和最长过期时间为 3 天。

class ToastWithAvatar
{
    public static bool SendToast()
    {

        var appNotification = new AppNotificationBuilder()
            .SetAppLogoOverride(new System.Uri("ms-appx:///images/logo.png"), AppNotificationImageCrop.Circle)
            .AddText("Example expiring notification")
            .AddText("This is an example message")
            .BuildNotification();


        appNotification.Expiration = DateTime.Now.AddDays(1);
        AppNotificationManager.Default.Show(appNotification);

        return appNotification.Id != 0; // return true (indicating success) if the toast was sent (if it has an Id)
    }
}

确保通知在重新启动时过期

如果想要通知在重新启动时删除,请将 ExpiresOnReboot 属性设置为 True。

class ToastWithAvatar
{
    public static bool SendToast()
    {

        var appNotification = new AppNotificationBuilder()
            .SetAppLogoOverride(new System.Uri("ms-appx:///images/logo.png"), AppNotificationImageCrop.Circle)
            .AddText("Example ExpiresOnReboot notification")
            .AddText("This is an example message")
            .BuildNotification();


            appNotification.ExpiresOnReboot = true;
            AppNotificationManager.Default.Show(appNotification);

            return appNotification.Id != 0; // return true (indicating success) if the toast was sent (if it has an Id)
    }
}

发送和更新进度栏通知

你可以在通知中显示进度栏相关的更新:

包含进度栏的通知

使用 AppNotificationProgressData 构造更新进度栏通知。

const winrt::hstring c_tag = L"weekly-playlist";
const winrt::hstring c_group = L"downloads";

// Send first Notification Progress Update
void SendUpdatableNotificationWithProgress()
{
    auto notification{ winrt::AppNotificationBuilder()
            .AddText(L"Downloading this week's new music...")
            .AddProgressBar(winrt::AppNotificationProgressBar()
                .BindTitle()
                .BindValue()
                .BindValueStringOverride()
                .BindStatus())
            .BuildNotification() }

    notification.Tag(c_tag);
    notification.Group(c_group);

    // Assign initial values for first notification progress UI
    winrt::AppNotificationProgressData data(1); // Sequence number
    data.Title(L"Weekly playlist"); // Binds to {progressTitle} in xml payload
    data.Value(0.6); // Binds to {progressValue} in xml payload
    data.ValueStringOverride(L"15/26 songs"); // Binds to {progressValueString} in xml payload
    data.Status(L"Downloading..."); // Binds to {progressStatus} in xml payload

    notification.Progress(data);
    winrt::AppNotificationManager::Default().Show(notification);
}

// Send subsequent progress updates
winrt::Windows::Foundation::IAsyncAction UpdateProgressAsync()
{
    // Assign new values
    winrt::AppNotificationProgressData data(2 /* Sequence number */ );
    data.Title(L"Weekly playlist"); // Binds to {progressTitle} in xml payload
    data.Value(0.7); // Binds to {progressValue} in xml payload
    data.ValueStringOverride(L"18/26 songs"); // Binds to {progressValueString} in xml payload
    data.Status(L"Downloading..."); // Binds to {progressStatus} in xml payload

    auto result = co_await winrt::AppNotificationManager::Default().UpdateAsync(data, c_tag, c_group);
    if (result == winrt::AppNotificationProgressResult::AppNotificationNotFound)
    {
        // Progress Update failed since the previous notification update was dismissed by the user! So account for this in your logic by stopping updates or starting a new Progress Update flow.
    }
}

资源