打包和解压缩的桌面应用可以发送交互式 Toast 通知,就像通用 Windows 平台 (UWP) 应用可以一样。 这包括打包的应用(请参阅 为打包的 WinUI 3 桌面应用创建新项目):具有外部位置的打包应用(请参阅 通过外部位置打包来授予包标识):和解压缩的应用(请参阅 为未打包的 WinUI 3 桌面应用创建新项目)。
但是,对于未打包的桌面应用,有一些特殊步骤。 这是因为不同的激活方案,以及运行时缺少包标识。
步骤 1:启用 Windows SDK
如果尚未为应用启用 Windows SDK,则必须首先执行此作。 有几个关键步骤。
- 将
runtimeobject.lib
添加到 附加依赖项。 - 针对 Windows SDK。
右键单击项目并选择“ 属性”。
在顶部 配置 菜单中,选择 “所有配置”,以便对“调试”和“发布”应用以下更改。
在 链接器 -> 输入下,将 runtimeobject.lib
添加到 附加依赖项中。
然后在 常规下,确保 Windows SDK 版本 已设置为 10.0 或更高版本。
步骤二:复制兼容性库代码
将 DesktopNotificationManagerCompat.h 和 DesktopNotificationManagerCompat.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 中添加:
- xmlns:com 声明
- xmlns:desktop 声明
- 在 IgnorableNamespaces 属性中,com 和 desktop
- 使用步骤 4 中的 GUID 为 COM 激活器 com:Extension。 请务必包含
Arguments="-ToastActivated"
,以便你知道你的启动来自提示通知。 - 桌面: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(¬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());
}
}
}
重要
桌面应用无法使用旧 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);
}
事件的激活序列
激活序列如下...
如果应用已在运行:
- 在你的 NotificationActivator 中调用 激活
如果应用未运行:
- EXE 应用程序已启动,您将获得 "-ToastActivated" 的命令行参数。
- 在你的 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 或更高版本中的此问题。
资源
- GitHub 上的完整代码示例
- 从桌面应用
Toast 通知 - Toast 内容文档