从 WRL C++ 桌面应用发送本地 toast 通知

打包和未打包的桌面应用可以发送交互式 toast 通知,就像通用 Windows 平台 (UWP) 应用一样。 这包括打包的应用(请参阅为打包的 WinUI 3 桌面应用创建一个新项目);具有外部位置的打包应用(请参阅通过使用外部位置进行打包来授予包标识);和未打包的应用程序(请参阅为未打包的 WinUI 3 桌面应用创建一个新项目)。

然而,对于未打包的桌面应用,需要执行一些特殊步骤。 这是由于不同的激活方案,以及运行时缺少包标识。

重要

如果正在编写 UWP 应用,请参阅 UWP 文档。 有关其他桌面语言,请参阅桌面 C#

步骤 1:启用 Windows SDK

如果尚未为应用启用 Windows SDK,则必须先启用。 有几个关键步骤。

  1. runtimeobject.lib 添加到其他依赖项
  2. 面向 Windows SDK。

右键单击项目,并选择属性

在顶部配置菜单中,选择所有配置,以便对“调试”和“发布”应用以下更改。

在“链接器”->“输入”下,将 runtimeobject.lib 添加到“其他依赖项”

然后,在常规下,确保 Windows SDK 版本设置为版本 10.0 或更高版本。

步骤 2:复制兼容性库代码

从 GitHub 将 DesktopNotificationManagerCompat.hDesktopNotificationManagerCompat.cpp 文件复制到项目中。 兼容性库抽象化了桌面通知的复杂性。 以下说明需要兼容性库。

如果使用的是预编译标头,请确保将 #include "stdafx.h" 作为 DesktopNotificationManagerCompat.cpp 文件的第一行。

步骤 3:包括标头文件和命名空间

包括兼容库头文件,以及与使用 Windows toast API 相关的头文件和命名空间。

#include "DesktopNotificationManagerCompat.h"
#include <NotificationActivationCallback.h>
#include <windows.ui.notifications.h>

using namespace ABI::Windows::Data::Xml::Dom;
using namespace ABI::Windows::UI::Notifications;
using namespace Microsoft::WRL;

步骤 4:实现激活器

必须实现一个用于 toast 激活的处理程序,以便在用户单击 toast 时,应用可以执行某些操作。 这是 toast 在操作中心中持续存在所必需的(因为 toast 可以在几天后应用关闭时单击)。 此类可以放置在项目中的任何位置。

实现 INotificationActivationCallback 接口,如下所示,包括 UUID,还可以调用 CoCreatableClass 将类标记为 COM 可创建类。 对于 UUID,使用许多随机 GUID 生成器之一创建一个唯一 GUID。 此 GUID CLSID(类标识符)是操作中心知道要 COM 激活的类的方式。

// The UUID CLSID must be unique to your app. Create a new GUID if copying this code.
class DECLSPEC_UUID("replaced-with-your-guid-C173E6ADF0C3") NotificationActivator WrlSealed WrlFinal
    : public RuntimeClass<RuntimeClassFlags<ClassicCom>, INotificationActivationCallback>
{
public:
    virtual HRESULT STDMETHODCALLTYPE Activate(
        _In_ LPCWSTR appUserModelId,
        _In_ LPCWSTR invokedArgs,
        _In_reads_(dataCount) const NOTIFICATION_USER_INPUT_DATA* data,
        ULONG dataCount) override
    {
        // TODO: Handle activation
    }
};

// Flag class as COM creatable
CoCreatableClass(NotificationActivator);

步骤 5:向通知平台注册

然后,必须向通知平台注册。 根据应用是打包的还是未打包的,有不同的步骤。 如果你同时支持这两个步骤,那么必须执行这两组步骤(但是,由于我们的库为你处理代码,因此不需要分叉代码)。

已打包

如果应用已打包(请参阅为打包的 WinUI 3 桌面应用创建一个新项目),或使用外部位置打包(请参阅通过使用外部位置进行打包来授予包标识),或者如果同时支持这两者,请在 Package.appxmanifest 中添加:

  1. xmlns:com 声明
  2. xmlns:desktop 声明
  3. IgnorableNamespaces 属性中,com桌面
  4. com:Extension 用于使用来自步骤 4 的 GUID 的 COM 激活器。 一定要包括 Arguments="-ToastActivated",这样你就知道发布是在 toast 中完成的
  5. desktop:Extension 用于 windows.toastNotificationActivation,以声明 toast 激活器 CLSID(来自步骤 4 的 GUID)。

Package.appxmanifest

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

        <!--Register COM CLSID LocalServer32 registry key-->
        <com:Extension Category="windows.comServer">
          <com:ComServer>
            <com:ExeServer Executable="YourProject\YourProject.exe" Arguments="-ToastActivated" DisplayName="Toast activator">
              <com:Class Id="replaced-with-your-guid-C173E6ADF0C3" DisplayName="Toast activator"/>
            </com:ExeServer>
          </com:ComServer>
        </com:Extension>

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

      </Extensions>
    </Application>
  </Applications>
 </Package>

未打包

如果应用已打包(请参阅为打包的 WinUI 3 桌面应用创建一个新项目),或者同时支持两者,则必须在“开始”的应用快捷方式上声明应用程序用户模型 ID (AUMID) 和 Toast 激活器 CLSID(来自步骤 4 的 GUID)。

选择一个唯一的 AUMID 来标识应用。 这通常采用 [CompanyName].[AppName] 的形式。 但你需要确保在所有应用中都是唯一的(因此可以在末尾添加一些数字)。

步骤 5.1:WiX 安装程序

如果使用 WiX 作为安装程序,请编辑 Product.wxs 文件,将两个快捷方式属性添加到“开始”菜单快捷方式中,如下所示。 请确保步骤 4 中的 GUID 包含在 {} 中,如下所示。

Product.wxs

<Shortcut Id="ApplicationStartMenuShortcut" Name="Wix Sample" Description="Wix Sample" Target="[INSTALLFOLDER]WixSample.exe" WorkingDirectory="INSTALLFOLDER">
                    
    <!--AUMID-->
    <ShortcutProperty Key="System.AppUserModel.ID" Value="YourCompany.YourApp"/>
    
    <!--COM CLSID-->
    <ShortcutProperty Key="System.AppUserModel.ToastActivatorCLSID" Value="{replaced-with-your-guid-C173E6ADF0C3}"/>
    
</Shortcut>

重要

为了实际使用通知,必须在正常调试之前通过安装程序安装一次应用程序,以便显示包含 AUMID 和 CLSID 的“开始”快捷方式。 出现“开始”快捷方式后,可以在 Visual Studio 中使用 F5 进行调试。

步骤 5.2:注册 AUMID 和 COM 服务器

然后,无论安装程序如何,在应用的启动代码中(在调用任何通知 API 之前),调用 RegisterAumidAndComServer 方法,指定上述步骤 4 中的通知激活器类和上面使用的 AUMID。

// Register AUMID and COM server (for a packaged app, this is a no-operation)
hr = DesktopNotificationManagerCompat::RegisterAumidAndComServer(L"YourCompany.YourApp", __uuidof(NotificationActivator));

如果应用程序同时支持打包和未打包部署,那么可以随意调用此方法。 如果正在运行打包部署(也就是说,在运行时使用包标识),那么此方法将立即返回。 没有必要分叉代码。

此方法允许调用兼容性 API 来发送和管理通知,而无需不断提供 AUMID。 并插入 COM 服务器的 LocalServer32 注册表项。

步骤 6:注册 COM 激活器

对于打包应用和未打包应用,必须注册通知激活器类型,以便可以处理 Toast 激活。

在应用的启动代码中,调用以下 RegisterActivator 方法。 必须调用此项才能接收任何 Toast 激活。

// Register activator type
hr = DesktopNotificationManagerCompat::RegisterActivator();

步骤 7:发送通知

发送通知与 UWP 应用相同,只是将使用 DesktopNotificationManagerCompat 创建 ToastNotifier。 Compat 库会自动处理打包应用和未打包应用之间的差异,因此你无需分叉代码。 对于未打包的应用,compat 库会缓存你在调用RegisterAumidAndComServer 时提供的 AUMID,这样你就不必担心何时提供或不提供 AUMID。

请确保使用如下所示的 ToastGeneric 绑定,因为旧版 Windows 8.1 Toast 通知模板不会激活在步骤 4 中创建的 COM 通知激活器。

重要

仅在清单中具有 Internet 功能的打包应用中支持 Http 映像。 未包装的应用不支持 Http 映像;必须将映像下载到本地应用数据中,并在本地引用它。

// Construct XML
ComPtr<IXmlDocument> doc;
hr = DesktopNotificationManagerCompat::CreateXmlDocumentFromString(
    L"<toast><visual><binding template='ToastGeneric'><text>Hello world</text></binding></visual></toast>",
    &doc);
if (SUCCEEDED(hr))
{
    // See full code sample to learn how to inject dynamic text, buttons, and more

    // Create the notifier
    // Desktop apps must use the compat method to create the notifier.
    ComPtr<IToastNotifier> notifier;
    hr = DesktopNotificationManagerCompat::CreateToastNotifier(&notifier);
    if (SUCCEEDED(hr))
    {
        // Create the notification itself (using helper method from compat library)
        ComPtr<IToastNotification> toast;
        hr = DesktopNotificationManagerCompat::CreateToastNotification(doc.Get(), &toast);
        if (SUCCEEDED(hr))
        {
            // And show it!
            hr = notifier->Show(toast.Get());
        }
    }
}

重要

桌面应用不能使用旧 Toast 模板(如 ToastText02)。 当指定 COM CLSID 时,旧版模板的激活将失败。 必须使用 Windows ToastGeneric 模板,如上所示。

步骤 8:处理激活

当用户单击 Toast 或 Toast 中的按钮时,会调用 NotificationActivator 类的 Activate 方法。

在 Activate 方法中,可以分析在 Toast 中指定的参数,并获取用户键入或选择的用户输入,然后相应地激活应用。

注意

Activate 方法在主线程之外的另一个线程上调用。

// The GUID must be unique to your app. Create a new GUID if copying this code.
class DECLSPEC_UUID("replaced-with-your-guid-C173E6ADF0C3") NotificationActivator WrlSealed WrlFinal
    : public RuntimeClass<RuntimeClassFlags<ClassicCom>, INotificationActivationCallback>
{
public: 
    virtual HRESULT STDMETHODCALLTYPE Activate(
        _In_ LPCWSTR appUserModelId,
        _In_ LPCWSTR invokedArgs,
        _In_reads_(dataCount) const NOTIFICATION_USER_INPUT_DATA* data,
        ULONG dataCount) override
    {
        std::wstring arguments(invokedArgs);
        HRESULT hr = S_OK;

        // Background: Quick reply to the conversation
        if (arguments.find(L"action=reply") == 0)
        {
            // Get the response user typed.
            // We know this is first and only user input since our toasts only have one input
            LPCWSTR response = data[0].Value;

            hr = DesktopToastsApp::SendResponse(response);
        }

        else
        {
            // The remaining scenarios are foreground activations,
            // so we first make sure we have a window open and in foreground
            hr = DesktopToastsApp::GetInstance()->OpenWindowIfNeeded();
            if (SUCCEEDED(hr))
            {
                // Open the image
                if (arguments.find(L"action=viewImage") == 0)
                {
                    hr = DesktopToastsApp::GetInstance()->OpenImage();
                }

                // Open the app itself
                // User might have clicked on app title in Action Center which launches with empty args
                else
                {
                    // Nothing to do, already launched
                }
            }
        }

        if (FAILED(hr))
        {
            // Log failed HRESULT
        }

        return S_OK;
    }

    ~NotificationActivator()
    {
        // If we don't have window open
        if (!DesktopToastsApp::GetInstance()->HasWindow())
        {
            // Exit (this is for background activation scenarios)
            exit(0);
        }
    }
};

// Flag class as COM creatable
CoCreatableClass(NotificationActivator);

为了在应用关闭时正确支持启动,在 WinMain 函数中,需要确定是否是从 Toast 启动。 如果从 Toast 启动,将会有一个启动参数"-ToastActivated"。 当看到此情况时,应该停止执行任何正常的启动激活代码,并允许您 NotificationActivator 在需要时处理启动窗口。

// Main function
int WINAPI wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE, _In_ LPWSTR cmdLineArgs, _In_ int)
{
    RoInitializeWrapper winRtInitializer(RO_INIT_MULTITHREADED);

    HRESULT hr = winRtInitializer;
    if (SUCCEEDED(hr))
    {
        // Register AUMID and COM server (for a packaged app, this is a no-operation)
        hr = DesktopNotificationManagerCompat::RegisterAumidAndComServer(L"WindowsNotifications.DesktopToastsCpp", __uuidof(NotificationActivator));
        if (SUCCEEDED(hr))
        {
            // Register activator type
            hr = DesktopNotificationManagerCompat::RegisterActivator();
            if (SUCCEEDED(hr))
            {
                DesktopToastsApp app;
                app.SetHInstance(hInstance);

                std::wstring cmdLineArgsStr(cmdLineArgs);

                // If launched from toast
                if (cmdLineArgsStr.find(TOAST_ACTIVATED_LAUNCH_ARG) != std::string::npos)
                {
                    // Let our NotificationActivator handle activation
                }

                else
                {
                    // Otherwise launch like normal
                    app.Initialize(hInstance);
                }

                app.RunMessageLoop();
            }
        }
    }

    return SUCCEEDED(hr);
}

事件的激活序列

激活序列如下……

如果应用已在运行:

  1. 调用 NotificationActivator 中的 Activate

如果应用未运行:

  1. 应用是 EXE 启动的,你会得到一个命令行参数"-ToastActivated"
  2. 调用 NotificationActivator 中的 Activate

前台激活与后台激活

对于桌面应用,前台激活和后台激活的处理方式相同,即调用 COM 激活器。 这取决于应用代码决定是显示窗口还是只是执行一些工作,然后退出。 因此,在 toast 内容中指定 backgroundactivationType 不会改变行为。

第 9 步:移除和管理通知

删除和管理通知与 UWP 应用相同。 但是,建议使用兼容性库来获取 DesktopNotificationHistoryCompat,这样就不必担心为桌面应用提供 AUMID 了。

std::unique_ptr<DesktopNotificationHistoryCompat> history;
auto hr = DesktopNotificationManagerCompat::get_History(&history);
if (SUCCEEDED(hr))
{
    // Remove a specific toast
    hr = history->Remove(L"Message2");

    // Clear all toasts
    hr = history->Clear();
}

步骤 10:部署和调试

若要部署和调试打包的应用,请参阅运行、调试和测试打包的桌面应用

若要部署和调试桌面应用,必须在正常调试之前通过安装程序安装一次应用程序,以便显示包含 AUMID 和 CLSID 的“开始”快捷方式。 出现“开始”快捷方式后,可以在 Visual Studio 中使用 F5 进行调试。

如果通知只是无法在桌面应用中显示(并且没有引发任何异常),则这可能意味着“开始”快捷方式不存在(通过安装程序安装应用),或者在代码中使用的 AUMID 与“开始”快捷方式中的 AUMID 不匹配。

如果通知出现但未在操作中心中持久化(在弹出窗口被关闭后消失),则这意味着尚未正确实现 COM 激活器。

如果同时安装了打包的桌面应用和未打包的桌面应用,请注意:在处理 Toast 激活时,打包的应用将取代未打包的应用。 这意味着,未打包应用的 toast 将在单击时启动打包应用。 卸载打包的应用会将激活恢复到未打包的应用。

如果收到 HRESULT 0x800401f0 CoInitialize has not been called.,请确保在调用 API 之前先在应用中调用 CoInitialize(nullptr)

如果在调用 Compat API 时收到 HRESULT 0x8000000e A method was called at an unexpected time.,这可能意味着你未能调用所需的 Register 方法(或者如果是打包的应用,则表示当前未在打包的上下文下运行应用)。

如果遇到许多 unresolved external symbol 编译错误,则可能是你忘记在步骤 1 中将 runtimeobject.lib 添加到其他依赖项中(或者你只将其添加到调试配置中,而没有添加到发布配置中)。

处理旧版 Windows

如果你支持 Windows 8.1 或更低版本,则在调用任何 DesktopNotificationManagerCompat API 或发送任何 ToastGeneric toast 之前,需要在运行时检查是否在 Windows 上运行。

Windows 8 引入了 Toast 通知,但使用了旧 Toast 模板,如 ToastText01。 激活由 ToastNotification 类上的内存中 Activated 事件处理,因为 Toast 只是未持久保存的短暂弹出窗口。 Windows 10 引入了交互式 ToastGeneric Toast,还引入了操作中心,在该中心,通知将持续多天。 操作中心的引入需要引入 COM 激活器,这样 toast 就可以在创建后几天被激活。

操作系统 ToastGeneric COM 激活器 旧 Toast 模板
Windows 10 及更高版本 支持 支持 支持(但不会激活 COM 服务器)
Windows 8.1/8 空值 空值 支持
Windows 7 及更低版本 空值 不可用 空值

若要检查是否在 Windows 10 或更高版本上运行,请包括 <VersionHelpers.h> 标头,并检查 IsWindows10OrGreater 方法。 如果返回 true,则继续调用本文档中所述的所有方法。

#include <VersionHelpers.h>

if (IsWindows10OrGreater())
{
    // Running on Windows 10 or later, continue with sending toasts!
}

已知问题

已修复:单击 Toast 后,应用不会成为焦点:在内部版本 15063 及更早版本中,激活 COM 服务器时,前台权限不会传输到应用程序。 因此,当试图将应用移动到前台时,应用只会闪烁。 没有针对此问题的解决方法。 我们在内部版本 16299 或更高版本中修复了此问题。

资源