从 WRL C++ 桌面应用发送本地 toast 通知
打包和未打包的桌面应用可以发送交互式 toast 通知,就像通用 Windows 平台 (UWP) 应用一样。 这包括打包的应用(请参阅为打包的 WinUI 3 桌面应用创建一个新项目);具有外部位置的打包应用(请参阅通过使用外部位置进行打包来授予包标识);和未打包的应用程序(请参阅为未打包的 WinUI 3 桌面应用创建一个新项目)。
然而,对于未打包的桌面应用,需要执行一些特殊步骤。 这是由于不同的激活方案,以及运行时缺少包标识。
步骤 1:启用 Windows SDK
如果尚未为应用启用 Windows SDK,则必须先启用。 有几个关键步骤。
- 将
runtimeobject.lib
添加到其他依赖项。 - 面向 Windows SDK。
右键单击项目,并选择属性。
在顶部配置菜单中,选择所有配置,以便对“调试”和“发布”应用以下更改。
在“链接器”->“输入”下,将 runtimeobject.lib
添加到“其他依赖项”。
然后,在常规下,确保 Windows SDK 版本设置为版本 10.0 或更高版本。
步骤 2:复制兼容性库代码
从 GitHub 将 DesktopNotificationManagerCompat.h 和 DesktopNotificationManagerCompat.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 中添加:
- xmlns:com 声明
- xmlns:desktop 声明
- 在 IgnorableNamespaces 属性中,com 和 桌面
- com:Extension 用于使用来自步骤 4 的 GUID 的 COM 激活器。 一定要包括
Arguments="-ToastActivated"
,这样你就知道发布是在 toast 中完成的 - 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(¬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 中的 Activate
如果应用未运行:
- 应用是 EXE 启动的,你会得到一个命令行参数"-ToastActivated"
- 调用 NotificationActivator 中的 Activate
前台激活与后台激活
对于桌面应用,前台激活和后台激活的处理方式相同,即调用 COM 激活器。 这取决于应用代码决定是显示窗口还是只是执行一些工作,然后退出。 因此,在 toast 内容中指定 background 的 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
编译错误,则可能是你忘记在步骤 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 或更高版本中修复了此问题。