Поделиться через


Отправка локального app уведомления с рабочего стола WRL C++ app

Упакованные и распакованные настольные приложения могут отправлять интерактивные app уведомления так же, как приложения универсальной платформы Windows (UWP). Это включает упакованные приложения (см. статью "Создание нового проекта для упакованного рабочего стола WinUIapp"); упакованные приложения с внешним расположением (см. раздел "Предоставление удостоверения пакета путем упаковки с внешним расположением"); и неупакованные приложения (см. раздел "Создание проекта для неупакованного рабочего стола WinUIapp").

Однако для распаковки рабочего стола appесть несколько специальных шагов. Это связано с различными схемами активации и отсутствием идентификации пакета во время выполнения.

Note

Термин "toast уведомление" заменяется на "app уведомление". Эти термины относятся к одной и той же функции Windows, но со временем мы постепенно перестанем использовать термин "toast уведомление" в документации.

Important

Если вы пишете UWP, ознакомьтесь с app. Для других языков для настольных систем см. C#.

Шаг 1. Включение пакета SDK для Windows

Если вы не включили пакет SDK для Windows для вашей версии app, сначала это необходимо сделать. Существует несколько ключевых шагов.

  1. Добавьте runtimeobject.lib к дополнительные зависимости.
  2. Используйте Windows SDK.

Щелкните проект правой кнопкой мыши и выберите Свойства.

В верхнем меню "Конфигурация" выберите "Все Конфигурации", чтобы следующее изменение применялось как к отладке, так и к выпуску.

В разделе Компоновщик —> входыдобавьте runtimeobject.lib в Дополнительные зависимости.

Затем в разделе Общиеубедитесь, что версия SDK для Windows установлена на версию 10.0 или более позднюю.

Шаг 2. Копирование кода библиотеки compat

Скопируйте DesktopNotificationManagerCompat.h и DesktopNotificationManagerCompat.cpp файл из GitHub в проект. Библиотека Compat упрощает большую часть процессов, связанных с уведомлениями на рабочем столе. Для выполнения приведенных ниже инструкций требуется библиотека compat.

Убедитесь, что вы используете #include "stdafx.h" в качестве первой строки файла DesktopNotificationManagerCompat.cpp, если используете предварительно скомпилированные заголовки.

Шаг 3: Включите файлы заголовков и пространства имен

Включите файл заголовка библиотеки compat, а также файлы заголовков и пространства имен, связанные с использованием API уведомлений Windows.

#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 проект упакован (см. статью "Создание проекта для упакованного рабочего стола appWinUI") или упакован с использованием внешнего расположения (см. раздел "Предоставление идентичности пакета через упаковку с внешним местоположением") или если вы поддерживаете оба варианта, добавьте в Package.appxmanifest:

  1. Объявление для xmlns:com
  2. Декларация для xmlns:desktop
  3. В атрибуте IgnorableNamespaces, com и desktop
  4. com:Extension для активатора COM с помощью GUID из шага 4. Не забудьте вставить Arguments="-ToastActivated", чтобы убедиться, что ваш запуск был произведен из уведомления app
  5. desktop:Extension для windows.toastNotificationActivation, чтобы указать CLSID вашего активатора уведомлений (GUID из шага 4).

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 не упакован (см. раздел "Создать новый проект для неупакованного рабочего стола appWinUI"), или если вы поддерживаете оба варианта, необходимо объявить идентификатор модели пользователя приложения (AUMID) и toast активатор CLSID (GUID из шага 4) для ярлыка вашего app в меню Пуск.

Выберите уникальный идентификатор AUMID, который будет определять ваш app. Обычно это в формате [CompanyName].[AppName]. Но вы хотите убедиться, что она уникальна во всех приложениях (поэтому вы можете добавить некоторые цифры в конце).

Шаг 5.1. Установщик WiX

Если вы используете WiX для установщика, измените файл Product.wxs, чтобы добавить два свойства ярлыка в ярлык меню "Пуск", как показано ниже. Убедитесь, что ваш GUID из шага №4 заключен в {}, как показано ниже.

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 присутствовал. После наличия ярлыка "Пуск" можно выполнить отладку с помощью F5 из Visual Studio.

Шаг 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-интерфейсы compat для отправки уведомлений и управления ими без постоянного предоставления AUMID. Он вставляет ключ реестра LocalServer32 для COM-сервера.

Шаг 6. Регистрация активатора COM

Для упакованных и распакованных приложений необходимо зарегистрировать тип активатора уведомлений, чтобы можно было обрабатывать активации toast.

appВ коде запуска вызовите следующий метод RegisterActivator. Это необходимо выполнить для получения любых toast активаций.

// Register activator type
hr = DesktopNotificationManagerCompat::RegisterActivator();

Шаг 7. Отправка уведомления

Отправка уведомления идентична приложениям UWP, за исключением того, что вы будете использовать DesktopNotificationManagerCompat для создания ToastNotifier. Библиотека compat автоматически обрабатывает разницу между упакованными и распакованными приложениями, поэтому вам не нужно разветвлять код. Для распаковки appбиблиотека compat кэширует AUMID, предоставленный при вызове RegisterAumidAndComServer , чтобы вам не нужно беспокоиться о том, когда предоставить или не предоставить AUMID.

Убедитесь, что вы используете привязку ToastGeneric , как показано ниже, так как устаревшие шаблоны уведомлений Windows 8.1 toast не активируют активацию COM-уведомлений, созданную на шаге 4.

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 или кнопки в уведомлении, вызывается метод Activate класса NotificationActivator.

В методе 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 вызывается

Если ваш app не работает:

  1. При запуске app EXE вы получаете аргументы командной строки "-ToastActivated".
  2. Активировать в NotificationActivator вызывается

Активация переднего плана и фонового плана

Для настольных приложений активация переднего плана и фоновая активация обрабатываются одинаково—вызывается активатор COM. Это зависит от кода вашего app, чтобы решить, следует ли отображать окно или просто выполнить некоторые задачи и затем завершить работу. Поэтому указание типа активациифона в app содержимом уведомления не изменяет поведение.

Шаг 9. Удаление уведомлений и управление ими

Удаление уведомлений и управление ими идентично приложениям UWP. Однако мы рекомендуем использовать нашу библиотеку compat для получения DesktopNotificationHistoryCompat , поэтому вам не нужно беспокоиться о предоставлении AUMID для рабочего стола app.

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 присутствовал. После наличия ярлыка "Пуск" можно выполнить отладку с помощью F5 из Visual Studio.

Если уведомления просто не отображаются на вашем рабочем столеapp (и никакие исключения не выбрасываются), вероятно, это означает, что ярлык Пуск отсутствует (установите ваш app через установщик) или AUMID, используемый в коде, не совпадает с AUMID в ярлыке Пуск.

Если уведомления отображаются, но не сохраняются в Центре уведомлений (исчезает после закрытия всплывающего окна), это означает, что вы не реализовали активатор COM правильно.

Если вы установили как упакованную, так и неупакованную версию рабочего стола app, обратите внимание, что упакованная версия app будет приоритизироваться над неупакованной app при обработке активаций toast. Это означает, что app уведомления из неупакованных app будут запускать упакованные app при нажатии. Удаление упакованного app восстановит активации обратно к неупакованному app.

Если вы получаете HRESULT 0x800401f0 CoInitialize has not been called., убедитесь, что вызвали CoInitialize(nullptr) в вашем app перед вызовом API.

Если вы получаете HRESULT 0x8000000e A method was called at an unexpected time. при вызове API Compat, это, скорее всего, означает, что вам не удалось вызвать необходимые методы Register (или если app упакован, вы не запускаете app в упакованном контексте).

Если вы получаете многочисленные ошибки компиляции unresolved external symbol, вероятно, вы забыли добавить runtimeobject.lib в Дополнительные зависимости на шаге #1 (или добавили его только в конфигурацию Debug, а не в конфигурацию Release).

Обработка старых версий Windows

Если вы поддерживаете Windows 8.1 или более ранней версии, необходимо проверить во время выполнения, работаете ли вы в Windows, перед вызовом любого API DesktopNotificationManagerCompat или отправкой любых уведомлений ToastGeneric.

Windows 8 представила toast уведомления, но использовала устаревшие toast шаблоны, такие как ToastText01. Активация осуществлялась событием Активировано в классе ToastNotification, так как тосты были лишь кратковременными всплывающими окнами и не сохранялись. 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