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


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

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

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

Важный

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

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

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

  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, а также файлы заголовков и пространства имен, связанные с использованием 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. Реализация активатора

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

Реализуйте интерфейс INotificationActivationCallback, включая связанный UUID, а также вызовите CoCreatableClass, чтобы пометить класс как COM-создаваемый. Для UUID создайте уникальный 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 для 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>

Распаковка

Если ваше приложение является распакованным (см. раздел Создание нового проекта для распакованного настольного приложения 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 кэширует идентификатор 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 можно проанализировать аргументы, указанные в уведомлении, и получить данные, которые пользователь ввёл или выбрал, а затем соответствующим образом активировать ваше приложение.

Заметка

Метод 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-уведомление или нет. При запуске из всплывающего уведомления будет передан параметр "-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. Ваше приложение в виде EXE-файла запущено, вы получите аргументы командной строки "-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., обязательно вызовите CoInitialize(nullptr) в приложении перед вызовом API.

Если вы получаете HRESULT 0x8000000e A method was called at an unexpected time. при вызове Compat API, это, вероятно, означает, что вам не удалось вызвать необходимые методы 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 N/A N/A Поддерживается
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!
}

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

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

Ресурсы