从 WRL C++ 桌面发送本地 app 通知 app

打包和解压缩的桌面应用可以发送交互式 app 通知,就像通用 Windows 平台 (UWP) 应用可以一样。 这包括打包的应用(请参阅为打包的 WinUI 3 桌面app创建新项目)、具有外部位置的打包应用(请参阅通过打包外部位置授予包标识);以及解压缩的应用(请参阅为未打包的 WinUI 3 桌面app创建新项目)。

但是,对于未打包的桌面 app,有一些特殊步骤。 这是因为不同的激活方案,以及运行时缺少包标识。

Note

当前正在将术语“toast notification”替换为“app notification”。 这些术语都指的是 Windows 的相同功能,但随着时间的推移,我们将逐步取消使用文档中的“toast 通知”。

Important

如果要编写 UWP app,请参阅 UWP 文档。 有关其他桌面语言,请参阅 桌面 C#

步骤 1:启用 Windows SDK

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

  1. runtimeobject.lib 添加到 附加依赖项
  2. 针对 Windows SDK。

右键单击项目并选择“ 属性”。

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

链接器 -> 输入下,将 runtimeobject.lib 添加到 附加依赖项中。

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

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

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

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

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

包括兼容性库头文件,以及与使用 Windows 通知 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:实现激活器

必须实现通知激活的处理程序,使得当用户单击通知时,app 可以执行某些动作。 通知必须保留在操作中心,以便在几天后关闭 app 时,仍然可以单击该通知。 此类可以放置在项目中的任意位置。

实现如下所示的 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:向通知平台注册

然后,必须向通知平台注册。 有不同的步骤,具体取决于 app 是已打包还是未打包。 如果同时支持这两个步骤,则必须执行这两组步骤(但是,由于我们的库为你处理了代码,因此无需分叉代码)。

Packaged

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

  1. xmlns:com 声明
  2. xmlns:desktop 声明
  3. IgnorableNamespaces 属性中,comdesktop
  4. 使用步骤 4 中的 GUID 为 COM 激活器 com:Extension。 请务必包含Arguments="-ToastActivated",以便确保您的启动是由app通知触发的
  5. desktop:Extension 用于 windows.toastNotificationActivation ,以声明通知激活器 app 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>

Unpackaged

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

为app选择一个唯一的 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>

Important

为了能够实际使用通知,您必须事先通过安装程序安装一次 app,这样才能在正常调试之前确保含有 AUMID 和 CLSID 的“开始”快捷方式存在。 出现“开始”快捷方式后,可以使用 Visual Studio 中的 F5 进行调试。

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

然后,无论使用何种安装程序,在 app 启动代码中,在调用任何通知 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));

app如果同时支持打包和未打包的部署,则无论怎样,都可以随意调用此方法。 如果您运行的是打包的应用程序(即在运行时使用包标识),那么此方法将立即返回。 无需复制代码。

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

步骤 6:注册 COM 激活器

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

app在启动代码中,调用以下 RegisterActivator 方法。 要接收任何 toast 激活,必须调用此项。

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

步骤 7:发送通知

发送通知与 UWP 应用相同,只不过你将使用 DesktopNotificationManagerCompat 创建 ToastNotifier。 兼容性库会自动处理打包和非打包应用之间的差异,因此无需分支代码。 对于未打包的 app,兼容库会缓存您在调用 RegisterAumidAndComServer 时所提供的 AUMID,这样您无需考虑何时需要提供 AUMID或者不需要提供。

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

Important

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

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

Important

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

步骤 8:处理激活

当用户单击通知app或通知中的按钮时,将调用 NotificationActivator 类的 Activate 方法。

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

Note

在与主线程分开的线程上调用 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);

若要正确支持在 app 关闭时启动,请在 WinMain 函数中确定启动是否来源于 app 通知。 如果从通知启动,则会出现“-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);
}

事件的激活序列

激活序列如下...

如果app已经在运行:

  1. 在你的 NotificationActivator 中调用 激活

如果您的app未运行:

  1. 启动 app EXE 后,你将获得“-ToastActivated”的命令行参数
  2. 在你的 NotificationActivator 中调用 激活

前台激活与后台激活对比

对于桌面应用,前台和后台激活的处理方式相同 - 调用 COM 激活器。 由你的代码 app决定是显示窗口还是进行一些任务后退出。 因此,在您的通知内容中指定activationType后台不会更改行为。

步骤 9:删除和管理通知

删除和管理通知与 UWP 应用相同。 但是,我们建议使用我们的兼容库来获取 DesktopNotificationHistoryCompat,这样您就不必担心为桌面 app提供 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:部署和调试

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

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

如果通知无法在桌面 app 中显示(且未引发异常),这可能意味着“开始”快捷方式不存在(通过安装程序来安装 app),也可能是因为代码中使用的 AUMID 与“开始”快捷方式中的 AUMID 不匹配。

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

如果同时安装了打包桌面和未封装桌面app,请注意,打包的app会在处理app激活时取代未封装的toast。 这意味着来自未打包 app 的通知将在单击时启动已打包的 app 应用程序。 卸载打包的 app 后,激活将恢复为未打包的 app。

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

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

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

处理较旧版本的 Windows

如果支持 Windows 8.1 或更低版本,则需要在运行时检查是否正在 Windows 上运行,然后再调用任何 DesktopNotificationManagerCompat API 或发送任何 ToastGeneric 通知。

Windows 8 引入了 toast 通知,但使用了 旧 toast 模板,如 ToastText01。 激活是由内存中的 ToastNotification 类中处理的 激活事件处理的,因为通知只是未保留的简短弹出窗口。 Windows 10 引入了 交互式 ToastGeneric 通知,还引入了操作中心,以便通知能保留数天。 引入操作中心需要引入一个 COM 激活器,以便在您创建 toast 后的几天内可以对其进行激活。

OS ToastGeneric COM activator 旧 toast 模板
Windows 10 及更高版本 Supported Supported 支持(但不会激活 COM 服务器)
Windows 8.1 / 8 N/A N/A Supported
Windows 7 和更低版本 N/A N/A N/A

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

#include <VersionHelpers.h>

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

Known issues

已修复:在单击App后,toast不会获得焦点。在内部版本15063及更早版本中,激活COM服务器时,前台权限未传递到您的应用程序。 因此,当你试图将app移动到前台时,它只是闪烁。 此问题没有解决方法。 修复了 16299 或更高版本中的此问题。

Resources