Envío de una notificación del sistema local desde una aplicación de escritorio WRL de C++

Las aplicaciones de escritorio empaquetadas y sin empaquetar pueden enviar notificaciones del sistema interactivas, como las aplicaciones de la Plataforma universal de Windows (UWP). Esto incluye aplicaciones empaquetadas (consulte Creación de un nuevo proyecto para una aplicación de escritorio WinUI 3 empaquetada), aplicaciones empaquetadas con ubicación externa (consulte Concesión de identidad de paquete mediante el empaquetado con ubicación externa) y aplicaciones sin empaquetar (consulte Creación de un nuevo proyecto para una aplicación de escritorio WinUI 3 sin empaquetar).

Sin embargo, para una aplicación de escritorio sin empaquetar, hay algunos pasos especiales. Esto se debe a los distintos esquemas de activación y a la falta de identidad de paquete en tiempo de ejecución.

Importante

Si estás escribiendo una aplicación para UWP, consulte la Documentación de UWP. Para otros lenguajes de escritorio, consulte C# de escritorio.

Paso 1: Habilitar el SDK de Windows

Si no ha habilitado el SDK de Windows para la aplicación, hágalo primero. Hay varios pasos clave.

  1. Añada runtimeobject.lib a Dependencias adicionales.
  2. Acceda al SDK de Windows.

Haga clic con el botón derecho en el proyecto y seleccione Propiedades.

En el menú Configuración superior, seleccione Todas las configuraciones para que el siguiente cambio se aplique tanto a Depuración como a Versión.

En Enlazador -> entrada, agregue runtimeobject.lib a Dependencias adicionales.

A continuación, en General, asegúrese de que la Versión del SDK de Windows esté establecida en la versión 10.0 o posterior.

Paso 2: Copiar el código de la biblioteca de compatibilidad

Copie el archivo DesktopNotificationManagerCompat.h y DesktopNotificationManagerCompat.cpp de GitHub en el proyecto. La biblioteca de compatibilidad abstrae gran parte de la complejidad de las notificaciones de escritorio. Las instrucciones siguientes requieren la biblioteca de compatibilidad.

Si usa encabezados precompilados, asegúrese de #include "stdafx.h" como la primera línea del archivo DesktopNotificationManagerCompat.cpp.

Paso 3: Incluir los archivos de encabezado y los espacios de nombres

Incluya el archivo de encabezado de la biblioteca de compatibilidad y los archivos de encabezado y los espacios de nombres relacionados con el uso de las API de notificaciones del sistema de 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;

Paso 4: Implementar el activador

Debes implementar un controlador para la activación de las notificaciones del sistema, de modo que cuando el usuario haga clic en la notificación del sistema, la aplicación pueda hacer algo. Esto es necesario para que la notificación del sistema persista en el Centro de actividades (ya que se podría hacer clic en la notificación del sistema días más tarde, cuando la aplicación está cerrada). Esta clase se puede colocar en cualquier parte del proyecto.

Implemente la interfaz INotificationActivationCallback como se muestra a continuación, incluido un UUID, y llame también a CoCreatableClass para marcar la clase como clase que se puede crear mediante COM. Para el UUID, cree un GUID único mediante uno de los muchos generadores de GUID en línea. Este CLSID GUID (identificador de clase) es cómo el Centro de actividades sabe qué clase se va a activar mediante 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);

Paso 5: Registrarse en la plataforma de notificaciones

A continuación, debe registrarse en la plataforma de notificaciones. Hay diferentes pasos en función de si la aplicación está empaquetada o sin empaquetar. Si admite ambas, debe realizar ambos conjuntos de pasos (sin embargo, no es necesario bifurcar el código, ya que nuestra biblioteca lo controla automáticamente).

Empaquetado

Si la aplicación está empaquetada (consulte Creación de un nuevo proyecto para una aplicación de escritorio WinUI 3 empaquetada) o empaquetada con ubicación externa (consulte Concesión de la identidad del paquete mediante el empaquetado con ubicación externa) o, si admite ambas, añada lo siguiente a Package.appxmanifest:

  1. Declaración para xmlns:com
  2. Declaración para xmlns:desktop
  3. En el atributo IgnoreableNamespaces, com y desktop
  4. com:Extensión para el activador de COM mediante el GUID del paso 4. Asegúrese de incluir Arguments="-ToastActivated" para que sepa que el inicio procedía de una notificación del sistema
  5. desktop:Extension para windows.toastNotificationActivation para declarar el CLSID del activador de la notificación del sistema (el GUID del paso 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>

Sin empaquetar

Si la aplicación está sin empaquetar (consulte Creación de un nuevo proyecto para una aplicación de escritorio WinUI 3 sin empaquetar) o, si admite ambas, tiene que declarar el identificador del modelo de usuario de aplicación (AUMID) y el CLSID del activador de notificaciones del sistema (el GUID del paso 4) en el acceso directo de la aplicación en Inicio.

Elija un AUMID único que identifique su aplicación. Suele tener el formato [NombreEmpresa]. [NombreAplicación]. Pero debe asegurarse de que sea único para todas las aplicaciones (así que no dude en añadir algunos dígitos al final).

Paso 5.1: Instalador de WiX

Si usa WiX para el instalador, edite el archivo Product.wxs para agregar las dos propiedades de acceso directo al acceso directo del menú Inicio como se muestra a continuación. Asegúrese de que el GUID del paso 4 esté entre corchetes {} como se muestra a continuación.

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>

Importante

Para poder utilizar realmente las notificaciones, debe instalar la aplicación a través del instalador una vez antes de depurar normalmente, para que el acceso directo de Inicio con su AUMID y CLSID esté presente. Una vez presente el acceso directo de Inicio, puede depurar con F5 desde Visual Studio.

Paso 5.2: Registrar AUMID y el servidor COM

A continuación, independientemente del instalador, en el código de inicio de la aplicación (antes de llamar a cualquier API de notificación), llame al método RegisterAumidAndComServer , especificando la clase de activador de notificación del paso 4 y el AUMID usado anteriormente.

// Register AUMID and COM server (for a packaged app, this is a no-operation)
hr = DesktopNotificationManagerCompat::RegisterAumidAndComServer(L"YourCompany.YourApp", __uuidof(NotificationActivator));

Si la aplicación admite la implementación empaquetada y sin empaquetar, no dude en llamar a este método independientemente. Si se está ejecutando aplicaciones empaquetadas (es decir, con identidad de paquete en tiempo de ejecución),este método simplemente devolverá inmediatamente. No es necesario bifurcar el código.

Este método le permite llamar a las API de compatibilidad para enviar y administrar las notificaciones sin tener que proporcionar constantemente su AUMID. E inserta la clave del Registro LocalServer32 para el servidor COM.

Paso 6: Registrar el activador de COM

En el caso de las aplicaciones empaquetadas y sin empaquetar, debe registrar el tipo de activador de las notificaciones para que pueda controlar las activaciones de las notificaciones del sistema.

En el código de inicio de la aplicación, llame al siguiente método RegisterActivator. Se debe llamar para que reciba las activaciones de las notificaciones del sistema.

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

Paso 7: Enviar una notificación

El envío de una notificación es idéntico a las aplicaciones para UWP, salvo que usará DesktopNotificationManagerCompat para crear un ToastNotifier. La biblioteca de compatibilidad controla automáticamente la diferencia entre las aplicaciones empaquetadas y sin empaquetar, por lo que no es necesario bifurcar el código. Para una aplicación sin empaquetar, la biblioteca de compatibilidad almacena en caché el AUMID que proporcionó cuando llamó a RegisterAumidAndComServer, para que no tenga que preocuparse por cuándo proporcionar o no proporcionar el AUMID.

Asegúrese de usar el enlace ToastGeneric como se muestra a continuación, ya que las plantillas de notificación del sistema heredadas de Windows 8.1 no activarán el activador de notificación COM que creó en el paso 4.

Importante

Las imágenes HTTP solo se admiten en aplicaciones empaquetadas que tienen la funcionalidad de Internet en su manifiesto. Las aplicaciones sin empaquetar no admiten imágenes HTTP; debe descargar la imagen en los datos de la aplicación local y hacer referencia a ella localmente.

// 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());
        }
    }
}

Importante

Las aplicaciones de escritorio no pueden usar plantillas de notificación del sistema heredadas (como ToastText02). Se producirá un error en la activación de las plantillas heredadas si cuando se especifique el CLSID COM. Debes usar las plantillas ToastGeneric de Windows, como se ha visto anteriormente.

Paso 8: Controlar la activación

Cuando el usuario hace clic en la notificación del sistema o en los botones de la notificación del sistema, se invoca el método Activate de la clase NotificationActivator.

Dentro del método Activate, puede analizar los argumentos que especificó en la notificación del sistema y obtener la entrada del usuario que el usuario ha escrito o seleccionado y, a continuación, activar la aplicación en consecuencia.

Nota:

Se llama al método Activate en un subproceso independiente del subproceso principal.

// 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);

Para admitir correctamente el inicio mientras la aplicación está cerrada, en la función WinMain, deberá determinar si se debe iniciar desde una notificación del sistema o no. Si se inicia desde una notificación del sistema, habrá un argumento de inicio de "-ToastActivated". Cuando vea esto, debe dejar de realizar cualquier código de activación de inicio normal y permitir que NotificationActivator controle las ventanas de inicio, si es necesario.

// 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);
}

Secuencia de activación de eventos

La secuencia de activación es la siguiente...

Si la aplicación ya se está ejecutando:

  1. Se llama a Activate en NotificationActivator.

Si la aplicación no se está ejecutando:

  1. La aplicación se inicia en EXE y obtiene un argumento de línea de comandos de "-ToastActivated"
  2. Se llama a Activate en NotificationActivator.

Activación en primer plano frente a segundo plano

En el caso de las aplicaciones de escritorio, la activación en primer plano y en segundo plano se controla de forma idéntica: se llama al activador de COM. Es necesario que el código de la aplicación decida si desea mostrar una ventana o simplemente realizar algún trabajo y, a continuación, salir. Por lo tanto, especificar un activationType de fondo en el contenido de la notificación del sistema no cambia el comportamiento.

Paso 9: Eliminar y administrar notificaciones

Eliminar y administrar notificaciones es idéntico a las aplicaciones para UWP. Sin embargo, se recomienda usar nuestra biblioteca de compatibilidad para obtener un DesktopNotificationHistoryCompat para que no tenga que preocuparse por proporcionar el AUMID para una aplicación de escritorio.

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();
}

Paso 10: Implementar y depurar

Para implementar y depurar la aplicación empaquetada, consulte Ejecución, depuración y prueba de una aplicación de escritorio empaquetada.

Para implementar y depurar la aplicación de escritorio, debe instalar la aplicación a través del instalador una vez antes de depurar normalmente, de modo que el acceso directo Inicio con AUMID y CLSID esté presente. Una vez presente el acceso directo de Inicio, puede depurar con F5 desde Visual Studio.

Si las notificaciones simplemente no aparecen en la aplicación de escritorio (y no se produce ninguna excepción), es probable que el acceso directo Inicio no esté presente (instale la aplicación a través del instalador) o que el AUMID que usó en el código no coincida con el AUMID del acceso directo de Inicio.

Si las notificaciones aparecen pero no se conservan en el Centro de actividades (desaparecen después de descartar el elemento emergente), significa que no ha implementado correctamente el activador de COM.

Si ha instalado la aplicación de escritorio empaquetada y sin empaquetar, tenga en cuenta que la aplicación empaquetada reemplazará a la aplicación sin empaquetar al controlar las activaciones de las notificaciones del sistema. Esto significa que las notificaciones del sistema de la aplicación sin empaquetar iniciarán la aplicación empaquetada cuando se haga clic en ellas. La desinstalación de la aplicación empaquetada revertirá las activaciones a la aplicación sin empaquetar.

Si recibe HRESULT 0x800401f0 CoInitialize has not been called., asegúrese de llamar a CoInitialize(nullptr) en la aplicación antes de llamar a las API.

Si recibe HRESULT 0x8000000e A method was called at an unexpected time. al llamar a las API de compatibilidad, es probable que no haya llamado a los métodos Register necesarios (o si se trata de una aplicación empaquetada, que no está ejecutando la aplicación en el contexto empaquetado).

Si recibe numerosos errores de compilación unresolved external symbol, es probable que haya olvidado agregar runtimeobject.lib a Dependencias adicionales en el paso 1 (o que solo lo agregó a la configuración de depuración y no a la configuración de versión).

Control de versiones anteriores de Windows

Si admite Windows 8.1 o versiones anteriores, deberá comprobar en tiempo de ejecución si se ejecuta en Windows antes de llamar a las API DesktopNotificationManagerCompat o enviar cualquier notificación del sistema ToastGeneric.

Windows 8 introdujo notificaciones del sistema, pero usó las plantillas de notificación del sistema heredadas, como ToastText01. La activación se controló mediante el evento Activado en memoria en la clase ToastNotification, ya que las notificaciones del sistema solo eran ventanas emergentes breves que no se conservaban. Windows 10 introdujo notificaciones del sistema ToastGeneric interactivas y también introdujo el Centro de actividades, en el que las notificaciones del sistema se conservan durante varios días. La introducción del Centro de actividades requería la introducción de un activador de COM, para que la notificación del sistema se pudiera activar días después de crearla.

SISTEMA OPERATIVO ToastGeneric Activador de COM Plantillas de notificación del sistema heredadas
Windows 10 y versiones posteriores Compatible Compatible Compatible (pero no activará el servidor COM)
Windows 8.1 / 8 N/D N/D Compatible
Windows 7 y versiones anteriores N/D N/D N/D

Para comprobar si se ejecuta en Windows 10 o posterior, incluya el encabezado <VersionHelpers.h> y compruebe el método IsWindows10OrGreater. Si devuelve true, continúe llamando a todos los métodos descritos en esta documentación.

#include <VersionHelpers.h>

if (IsWindows10OrGreater())
{
    // Running on Windows 10 or later, continue with sending toasts!
}

Problemas conocidos

CORREGIDO: la aplicación no se centra después de hacer clic en la notificación del sistema: en las compilaciones 15063 y anteriores, los derechos en primer plano no se transferían a la aplicación cuando se activaba el servidor COM. Por lo tanto, la aplicación simplemente parpadeaba cuando intentaba moverla al primer plano. No había ninguna solución alternativa para este problema. Se ha corregido este problema en las compilaciones 16299 o posteriores.

Recursos