共用方式為


使用 WRL C++ 桌面程式傳送 app 本機通知 app

封裝和解壓縮的桌面應用程式可以傳送互動式 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,則必須先執行此動作。 有幾個關鍵步驟。

  1. runtimeobject.lib 新增至 額外相依性
  2. 以 Windows SDK 為目標。

以滑鼠右鍵按下您的項目,然後選取 [[屬性]

在頂端 組態 功能表中,選取 所有組態,以便將下列變更同時套用至 [偵錯] 和 [發行]。

連結器 -> 輸入下,將 runtimeobject.lib 新增至 其他相依性

然後在 [一般] 下,確定 Windows SDK 版本 設定為 10.0 版或更新版本。

步驟 2:複製相容庫程式碼

DesktopNotificationManagerCompat.hDesktopNotificationManagerCompat.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 中新增:

  1. xmlns:com 的宣告
  2. xmlns:desktop 的宣告
  3. IgnorableNamespaces 屬性中,comdesktop
  4. com:Extension,適用於使用步驟 4 中 GUID 的 COM 啟動器。 請務必包含Arguments="-ToastActivated",以便您知道您的啟動來自app通知。
  5. 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(&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());
        }
    }
}

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已在執行中:

  1. NotificationActivator 中呼叫 Activate

如果您的app未啟動:

  1. 已啟動的 EXE app,您會收到命令列參數 “-ToastActivated”
  2. 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