打包和解压缩的桌面应用可以发送交互式 app 通知,就像通用 Windows 平台 (UWP) 应用可以一样。 这包括打包的应用(请参阅为打包的 WinUI 3 桌面app创建新项目)、具有外部位置的打包应用(请参阅通过打包外部位置授予包标识);以及解压缩的应用(请参阅为未打包的 WinUI 3 桌面app创建新项目)。
但是,对于未打包的桌面 app,有一些特殊步骤。 这是因为不同的激活方案,以及运行时缺少包标识。
Note
当前正在将术语“toast notification”替换为“app notification”。 这些术语都指的是 Windows 的相同功能,但随着时间的推移,我们将逐步取消使用文档中的“toast 通知”。
步骤 1:启用 Windows SDK
如果尚未为 app 启用 Windows SDK,则必须首先启用它。 有几个关键步骤。
- 将
runtimeobject.lib添加到 附加依赖项。 - 针对 Windows SDK。
右键单击项目并选择“ 属性”。
在顶部 配置 菜单中,选择 “所有配置”,以便对“调试”和“发布”应用以下更改。
在 链接器 -> 输入下,将 runtimeobject.lib 添加到 附加依赖项中。
然后在 常规下,确保 Windows SDK 版本 已设置为 10.0 或更高版本。
步骤二:复制兼容性库代码
将 DesktopNotificationManagerCompat.h 和 DesktopNotificationManagerCompat.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 中添加:
- xmlns:com 声明
- xmlns:desktop 声明
- 在 IgnorableNamespaces 属性中,com 和 desktop
- 使用步骤 4 中的 GUID 为 COM 激活器 com:Extension。 请务必包含
Arguments="-ToastActivated",以便确保您的启动是由app通知触发的 - 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(¬ifier);
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已经在运行:
- 在你的 NotificationActivator 中调用 激活
如果您的app未运行:
- 启动 app EXE 后,你将获得“-ToastActivated”的命令行参数
- 在你的 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 或更高版本中的此问题。