封裝和解壓縮的桌面應用程式可以傳送互動式 app 通知,就像通用 Windows 平臺 (UWP) 應用程式可以一樣。 這包括已封裝的應用程式(請參閱為封裝的 WinUI 3 桌面app建立新專案):具有外部位置的已封裝應用程式(請參閱使用外部位置封裝來授與套件身分識別]和未封裝的應用程式(請參閱為未封裝的 WinUI 3 桌面app建立新專案)。
不過,對於未經封裝的桌面app,有幾個特別的步驟。 這是因為不同的啟用方案,以及執行時缺少套件身分識別。
Note
“toast notification” 一詞正取代為 “app notification”。 這些詞彙都是指 Windows 的相同功能,但隨著時間推移,我們將逐步在文件中淘汰「toast 通知」的使用。
Important
如果您要撰寫 app,請參閱 UWP 說明文件。 如需其他桌面語言,請參閱 Desktop C#。
步驟 1:啟用 Windows SDK
如果您尚未為 app啟用 Windows SDK,則必須先執行此動作。 有幾個關鍵步驟。
- 將
runtimeobject.lib新增至 額外相依性。 - 以 Windows SDK 為目標。
以滑鼠右鍵按下您的項目,然後選取 [[屬性]。
在頂端 組態 功能表中,選取 所有組態,以便將下列變更同時套用至 [偵錯] 和 [發行]。
在 連結器 -> 輸入下,將 runtimeobject.lib 新增至 其他相依性。
然後在 [一般] 下,確定 Windows SDK 版本 設定為 10.0 版或更新版本。
步驟 2:複製相容庫程式碼
將 DesktopNotificationManagerCompat.h 和 DesktopNotificationManagerCompat.cpp 檔案從 GitHub 複製到您的專案。 相容性程式庫簡化了桌面通知的複雜度。 下列指示需要相容性程式庫。
如果您使用先行編譯標頭,請務必 #include "stdafx.h" 做為DesktopNotificationManagerCompat.cpp檔案的第一行。
步驟 3:包含標頭檔案與命名空間
包含 compat 函式庫標頭檔案,以及使用 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 就能夠執行一些功能。 當您應用程式 (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
-
com:Extension,適用於使用步驟 4 中 GUID 的 COM 啟動器。 請務必包含
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宣告應用程式使用者模型識別碼 (AUMID) 和app啟動程式 CLSID (步驟 #4 中的 GUID)。
挑選一個獨特的 AUMID 以識別您的 app。 這通常的格式是 [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支援封裝和未封裝的部署,那麼可以任意調用此方法。 如果您是在執行打包程式(也就是在執行時具有套件身份),那麼這個方法將立即返回。 不需要分叉您的程式碼。
此方法可讓您呼叫 compat API 來傳送和管理通知,而不需要持續提供您的 AUMID。 它會插入 LocalServer32 登錄機碼給 COM 伺服器。
步驟 6:註冊 COM 啟動器
針對已封裝和未封裝的應用程式,您必須註冊通知啟動器類型,才能處理 toast 啟用。
app在您的啟動程式碼中,調用下列 RegisterActivator 方法。 您必須呼叫此程序,才能接收任何 toast 啟用。
// Register activator type
hr = DesktopNotificationManagerCompat::RegisterActivator();
步驟 7:傳送通知
傳送通知與 UWP 應用程式相同,不同之處在於您將使用 DesktopNotificationManagerCompat 來建立 ToastNotifier。 Compat 連結庫會自動處理已封裝和未封裝應用程式之間的差異,因此您不需要分支您的程式代碼。 針對未封裝的 app,相容庫會快取您提供給 RegisterAumidAndComServer 的 AUMID,因此您不必擔心何時應提供或不提供 AUMID。
請確定您使用 ToastGeneric 系結,因為舊版 Windows 8.1 toast 通知範本不會啟動您在步驟 4 中建立的 COM 通知啟動器。
Important
Http 圖片僅支援在已封裝應用程式中,而這些應用程式的指令清單中必須具有因特網功能。 未封裝的應用程式不支援 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 中呼叫 Activate
如果您的app未啟動:
- 已啟動的 EXE app,您會收到命令列參數 “-ToastActivated”
- 在 NotificationActivator 中呼叫 Activate
前景與背景啟動
針對桌面應用程式,前台和後台啟用都以相同方式處理,並呼叫您的 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.,請務必先在 CoInitialize(nullptr) 中呼叫 app,再呼叫 API。
如果您在呼叫 Compat APIs 時收到 HRESULT 0x8000000e A method was called at an unexpected time.,這很可能意味著您沒有呼叫必要的 Register 方法(或者如果 app 是已封裝的,則您目前並未在封裝的上下文中執行 app)。
如果您收到許多 unresolved external symbol 編譯錯誤,您可能是忘了在步驟 #1 中將 runtimeobject.lib 新增至 附加相依性(或者您只將其新增至偵錯組態,而未新增至發行組態)。
處理舊版 Windows
如果您支援 Windows 8.1 或更低版本,您可能需要在執行時檢查是否在 Windows 環境中運行,再調用任何 DesktopNotificationManagerCompat API 或傳送任何 ToastGeneric 快顯通知。
Windows 8 引進 toast 通知,但使用了 舊版 toast 範本,例如 ToastText01。 由於快顯通知只是未被保存的短暫彈出,啟用是透過位於 ToastNotification 類別中的記憶體內 Activated 事件來處理的。 Windows 10 引進了 互動式 ToastGeneric 快顯通知,也引進了控制中心,其中通知會保存數天。 操作中心的引進需要引進一個 COM 啟動器,以便您在建立 toast 之後的幾天能夠啟動它。
| OS | ToastGeneric | COM啟動器 | 舊版 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!
}
已知問題
已修正:在點擊App後不會成為焦點:toast在組建15063及更早版本中,前景權限在啟用 COM 伺服器時並未轉移至您的應用程式。 因此,當您嘗試將它移至前景時,您的 app 只會簡單閃爍。 此問題沒有因應措施。 我們已在版本16299或更高版本中修正此問題。
Resources
- GitHub 上 完整程式碼範例
- App 來自桌面應用程式的通知
- App 通知內容檔