Отправка локального всплывающего уведомления из классического приложения WRL C++

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

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

Внимание

Если вы пишете приложение UWP, ознакомьтесь с документацией по UWP. Для других классических языков см. статью "Настольный компьютер C#".

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

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

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

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

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

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

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

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

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

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

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

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

#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. Реализация активатора

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

Реализуйте интерфейс 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 :

  1. Объявление для xmlns:com
  2. Объявление для xmlns:desktop
  3. В атрибуте IgnorableNamespaces com и desktop
  4. com:Extension для активатора COM с помощью GUID из шага 4. Не забудьте включить так Arguments="-ToastActivated" , чтобы вы знали, что ваш запуск был из всплывающего уведомления
  5. desktop:Extension for windows.toastNotificationActivation , чтобы объявить идентификатор CLSID toast (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>

Распаковка

Если приложение распаковывается (см . статью "Создание нового проекта для распаковки классического приложения WinUI 3") или если вы поддерживаете оба приложения, необходимо объявить идентификатор пользовательской модели приложения (AUMID) и тост-активатор CLSID (GUID из шага 4) в ярлыке приложения на начальном экране.

Выберите уникальный идентификатор AUMID, который будет определять ваше приложение. Обычно это тип [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>

Внимание

Чтобы фактически использовать уведомления, необходимо установить приложение через установщик один раз перед отладкой, чтобы ярлык "Пуск" с AUMID и CLSID присутствовал. После создания ярлыка "Пуск" можно выполнить отладку с помощью F5 из Visual Studio.

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

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

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

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

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

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

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

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

Внимание

Образы 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(&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());
        }
    }
}

Внимание

Классические приложения не могут использовать устаревшие шаблоны всплывающего уведомления (например, ToastText02). Активация устаревших шаблонов завершится ошибкой при указании COM CLSID. Необходимо использовать шаблоны Windows ToastGeneric, как показано выше.

Шаг 8. Обработка активации

Когда пользователь щелкает всплывающее уведомление или кнопки в всплывающем элементе, вызывается метод Activate класса NotificationActivator.

В методе Activate можно проанализировать аrgs, указанные в всплывающем элементе, и получить введенные пользователем входные данные, введенные пользователем или выбранными, а затем активировать приложение соответствующим образом.

Примечание.

Метод 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 необходимо определить, запускается ли вы из всплывающего уведомления или нет. При запуске из всплывающего уведомления будет запущено объединение "-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);
}

Последовательность активаций событий

Последовательность активации приведена ниже...

Если приложение уже запущено:

  1. Активация в NotificationActivator вызывается

Если приложение не запущено:

  1. Приложение запущено, вы получите args командной строки "-ToastActivated"
  2. Активация в NotificationActivator вызывается

Фоновая активация и фоновая активация

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

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

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

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

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

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

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

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

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

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

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

Windows 8 представила всплывающие уведомления, но использовали устаревшие шаблоны всплывающего уведомления, такие как ToastText01. Активация была обработана событием активации в памяти в классе ToastNotification, так как всплывающие окна были только краткими всплывающими окнами, которые не сохранялись. В Windows 10 появились интерактивные всплывающие всплывающие элементы ToastGeneric, а также появился Центр уведомлений, где уведомления сохраняются в течение нескольких дней. Введение в Центр уведомлений требует введения активатора COM, чтобы всплывающая информация была активирована через несколько дней после ее создания.

ОС ToastGeneric Активатор COM Устаревшие шаблоны всплывающих элементов
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!
}

Известные проблемы

ИСПРАВЛЕНО. Приложение не становится ориентированным после нажатия всплывающего уведомления: в сборках 15063 и более ранних версий права переднего плана не передаются в приложение при активации COM-сервера. Таким образом, приложение просто мигает при попытке переместить его на передний план. Для этой проблемы не было обходного решения. Исправлено это в сборках 16299 или более поздней версии.

Ресурсы