从 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 或更高版本。

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

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

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

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

包括 compat 库头文件,以及与使用 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 属性中,comdesktop
  4. 使用步骤 4 中的 GUID 为 COM 激活器 com:Extension。 请务必包含 Arguments="-ToastActivated",以便你知道你的启动来自提示通知。
  5. 桌面:Extension for 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。 兼容性库会自动处理打包和非打包应用之间的差异,因此无需分支代码。 对于未打包的应用,兼容性库会缓存在调用 RegisterAumidAndComServer 时提供的 AUMID,这样就无需担心何时提供或未提供 AUMID。

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

重要

Http 图片仅在清单中具有 Internet 功能的打包应用程序中才受支持。 未打包的应用不支持 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 中调用 激活

如果应用未运行:

  1. EXE 应用程序已启动,您将获得 "-ToastActivated" 的命令行参数。
  2. 在你的 NotificationActivator 中调用 激活

前台激活与后台激活对比

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

步骤 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 编译错误,则可能忘记在 runtimeobject.lib 步骤 1 中添加 其他依赖项 (或者只将其添加到调试配置而不是发布配置)。

处理较旧版本的 Windows 系统

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

Windows 8 引入了 Toast 通知,但使用了 旧 Toast 模板,例如 ToastText01。 激活是由内存中的 ToastNotification 类中处理的 激活事件处理的,因为通知只是未保留的简短弹出窗口。 Windows 10 引入了 交互式 ToastGeneric 通知,还引入了操作中心,以便通知能保留数天。 操作中心的引入需要引入 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 或更高版本中的此问题。

资源