Enviar uma notificação do sistema local de aplicativo de desktop C++ WRL

Os aplicativos de desktop empacotados e não empacotados podem enviar notificações interativas do sistema, assim como os aplicativos da plataforma Universal do Windows (UWP). Isso inclui aplicativos empacotados (consulte Criar um novo projeto para um aplicativo de desktop WinUI 3 empacotado); aplicativos empacotados com localização externa (consulte Conceder o identificador de pacote por empacotamento com localização externa); e aplicativos não empacotados (consulte Criar um novo projeto para um aplicativo de desktop WinUI 3 não empacotado).

No entanto, para um aplicativo de desktop não empacotado, há algumas etapas especiais. Isso se deve aos diferentes esquemas de ativação e à falta de identidade do pacote em tempo de execução.

Importante

Se você estiver criando um aplicativo UWP, consulte a documentação da UWP. Para outras linguagens de desktop, consulte Desktop C#.

Etapa 1: Habilitar o SDK do Windows

Se você não habilitou o SDK do Windows para seu aplicativo, faça isso primeiro. Há algumas etapas principais.

  1. Adicionar runtimeobject.lib às Dependências adicionais.
  2. Direcionar o SDK do Windows.

Clique com o botão direito do mouse no projeto e selecione Propriedades.

No menu superior Configuração, selecione Todas as Configurações para que a alteração a seguir seja aplicada à Depuração e Liberação.

Em Vinculador -> Entrada, adicione runtimeobject.lib às Dependências adicionais.

Em seguida, em Geral, verifique se a versão do SDK do Windows está definida como versão 10.0 ou posterior.

Etapa 2: Copiar o código da biblioteca de compatibilidade

Copie o arquivo DesktopNotificationManagerCompat.h e DesktopNotificationManagerCompat.cpp do GitHub para o seu projeto. A biblioteca de compatibilidade abstrai grande parte da complexidade das notificações da área de trabalho. As instruções a seguir exigem a biblioteca de compatibilidade.

Se você estiver usando cabeçalhos pré-compilados, certifique-se #include "stdafx.h" como a primeira linha do arquivo DesktopNotificationManagerCompat.cpp.

Etapa 3: Incluir os arquivos de cabeçalho e namespaces

Inclua o arquivo de cabeçalho da biblioteca de compatibilidade e os arquivos de cabeçalho e namespaces relacionados ao uso das APIs de notificação do sistema do 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;

Etapa 4: Implementar o ativador

Você deve implementar um manipulador para ativação da notificação do sistema, para que, quando o usuário clicar na notificação do sistema, seu aplicativo possa fazer algo. Isso é necessário para que a notificação do sistema persista na Central de Ações (já que a notificação do sistema pode ser clicada dias depois, quando o aplicativo é fechado). Esta classe pode ser colocada em qualquer lugar do seu projeto.

Implemente a interface INotificationActivationCallback como visto abaixo, incluindo um UUID, e também chame CoCreatableClass para sinalizar sua classe como COM criável. Para seu UUID, crie um GUID exclusivo usando um dos muitos geradores de GUID online. Esse GUID CLSID (identificador de classe) é como a Central de Ações sabe qual classe ativar 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);

Etapa 5: Registrar-se na plataforma de notificação

Em seguida, você deve se registrar na plataforma de notificação. Há etapas diferentes, dependendo se seu aplicativo está empacotado ou não empacotado. Se você oferecer suporte a ambos, deverá executar os dois conjuntos de etapas (no entanto, não há necessidade de bifurcar seu código, já que nossa biblioteca lida com isso para você).

Empacotados

Se seu aplicativo estiver empacotado (consulte Criar um novo projeto para um aplicativo de desktop WinUI 3 empacotado) ou empacotado com localização externa (consulte Conceder o identificador de pacote por empacotamento com localização externa), ou se você oferece suporte aos dois, então em seu Package.appxmanifest adicione:

  1. Declaração para xmlns:com
  2. Declaração para xmlns:desktop
  3. No atributo IgnorableNamespaces, com e desktop
  4. com:Extension para o ativador COM usando o GUID da etapa 4. Certifique-se de incluir o Arguments="-ToastActivated" para que você saiba que sua inicialização foi de uma notificação do sistema
  5. desktop:Extension para windows.toastNotificationActivation para declarar o CLSID do ativador de notificação do sistema (o GUID da etapa 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>

Desempacotado

Se o seu aplicativo não estiver empacotado (consulte Criar um novo projeto para um aplicativo de desktop WinUI 3 empacotado), ou se você oferecer suporte a ambos, será necessário declarar o ID do Modelo de Usuário do Aplicativo (AUMID) e o CLSID do ativador de notificação do sistema (o GUID da etapa 4) no atalho do aplicativo na tela inicial.

Escolha um AUMID exclusivo que identificará seu aplicativo. Isso geralmente é na forma de [CompanyName].[AppName]. Mas convém garantir que ele seja exclusivo em todos os aplicativos (sinta-se à vontade para adicionar alguns dígitos no final).

Passo 5.1: WiX Installer

Se você estiver usando o WiX para o instalador, edite o arquivo Product.wxs para adicionar as duas propriedades de atalho ao atalho do menu Iniciar, conforme mostrado abaixo. Certifique-se de que seu GUID da etapa 4 esteja incluído como {} conforme visto abaixo.

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 realmente usar notificações, você deve instalar seu aplicativo através do instalador uma vez antes de depurar normalmente, para que o atalho Iniciar com seu AUMID e CLSID esteja presente. Depois que o atalho Iniciar estiver presente, você poderá depurar usando F5 do Visual Studio.

Etapa 5.2: Registrar o servidor AUMID e COM

Então, independentemente do seu instalador, no código de inicialização do seu aplicativo (antes de chamar qualquer API de notificação), chame o método RegisterAumidAndComServer , especificando sua classe ativadora de notificação da etapa 4 e seu AUMID usado acima.

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

Se seu aplicativo oferecer suporte à implantação empacotada e não empacotada, sinta-se à vontade para chamar esse método independentemente disso. Se você estiver executando empacotado (ou seja, com identidade de pacote em tempo de execução), esse método simplesmente retornará imediatamente. Não há necessidade de bifurcar seu código.

Esse método permite que você chame as APIs de compatibilidade para enviar e gerenciar notificações sem ter que fornecer constantemente seu AUMID. E insere a chave do registro LocalServer32 para o servidor COM.

Etapa 6: Registrar o ativador COM

Para aplicativos empacotados e não empacotados, você deve registrar seu tipo de ativador de notificação, para que possa lidar com ativações de notificação do sistema.

No código de inicialização do seu aplicativo, chame o seguinte método RegisterActivator. Isso deve ser chamado para que você receba todas as ativações de notificação do sistema.

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

Etapa 7: Enviar uma notificação

O envio de uma notificação é idêntico aos aplicativos UWP, exceto que você usará DesktopNotificationManagerCompat para criar um ToastNotifier. A biblioteca de compatibilidade lida automaticamente com a diferença entre aplicativos empacotados e não empacotados, para que você não precise bifurcar seu código. Para um aplicativo não empacotado, a biblioteca de compatibilidade armazena em cache o AUMID que você forneceu quando chamou RegisterAumidAndComServer para que você não precise se preocupar sobre quando fornecer ou não o AUMID.

Certifique-se de usar a associação ToastGeneric como visto abaixo, pois os modelos de notificação do sistema herdados do Windows 8.1 não ativarão o ativador de notificação COM que você criou na etapa 4.

Importante

As imagens http são suportadas apenas em aplicativos empacotados que têm o recurso de Internet em seu manifesto. Aplicativos não empacotados não oferecem suporte a imagens http. Você deve baixar a imagem para os dados do aplicativo local e fazer referência a ela 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

Os aplicativos de desktop não podem usar modelos de notificação do sistema herdados (como ToastText02). A ativação dos modelos herdados falhará quando o COM CLSID for especificado. Você deve usar os modelos ToastGeneric do Windows, como visto acima.

Etapa 8: Manipular a ativação

Quando o usuário clica na notificação do sistema ou nos botões da notificação do sistema, o método Activate da classe NotificationActivator é invocado.

Dentro do método Activate, você pode analisar os argumentos especificados na notificação do sistema e obter a entrada do usuário que o usuário digitou ou selecionou e, em seguida, ativar seu aplicativo adequadamente.

Observação

O método Activate é chamado em um thread separado do thread 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 suportar corretamente a inicialização enquanto seu aplicativo está fechado, em sua função WinMain, recomenda-se determinar se está sendo iniciado a partir de uma notificação do sistema ou não. Se iniciado a partir de uma notificação do sistema, haverá um argumento de inicialização de "-ToastActivated". Quando você vir isso, deverá parar de executar qualquer código de ativação de inicialização normal e permitir que seu NotificationActivator lide com janelas de inicialização, se necessário.

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

Sequência de ativação de eventos

A sequência de ativação é a seguinte...

Se seu aplicativo já estiver em execução:

  1. Activate em seu NotificationActivator será chamado

Se seu aplicativo não estiver em execução:

  1. Seu aplicativo será EXE iniciado, você receberá argumentos de linha de comando de "-ToastActivated"
  2. Activate em seu NotificationActivator será chamado

Ativação em primeiro plano vs em segundo plano

Para aplicativos de desktop, a ativação em primeiro plano e em segundo plano é tratada de forma idêntica, seu ativador COM é chamado. Cabe ao código do seu aplicativo decidir se deseja mostrar uma janela ou apenas executar algum trabalho e, em seguida, sair. Portanto, especificar um activationType de segundo plano no conteúdo de notificação do seu sistema não altera o comportamento.

Etapa 9: Remover e gerenciar notificações

A remoção e o gerenciamento de notificações são idênticos aos aplicativos UWP. No entanto, recomendamos que você use nossa biblioteca de compatibilidade para obter um DesktopNotificationHistoryCompat para que você não precise se preocupar em fornecer o AUMID para um aplicativo de desktop.

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

Etapa 10: Implantar e depurar

Para implantar e depurar seu aplicativo empacotado, consulte Executar, depurar e testar um aplicativo de desktop empacotado.

Para implantar e depurar seu aplicativo de desktop, você deve instalá-lo por meio do instalador uma vez antes de depurar normalmente, para que o atalho Iniciar com seu AUMID e CLSID esteja presente. Depois que o atalho Iniciar estiver presente, você poderá depurar usando F5 do Visual Studio.

Se suas notificações simplesmente não aparecerem em seu aplicativo de desktop (e nenhuma exceção for lançada), isso provavelmente significará que o atalho Iniciar não está presente (instale seu aplicativo por meio do instalador) ou o AUMID que você usou no código não corresponde ao AUMID no atalho Iniciar.

Se suas notificações aparecerem, mas não persistirem na Central de Ações (desaparecendo depois que o pop-up for descartado), isso significará que você não implementou o ativador COM corretamente.

Se você instalou seu aplicativo de desktop empacotado e não empacotado, observe que o aplicativo empacotado substituirá o aplicativo não empacotado ao lidar com as ativações de notificação do sistema. Isso significa que as notificações do sistema do aplicativo não empacotado iniciarão o aplicativo empacotado quando clicadas. A desinstalação do aplicativo empacotado reverterá as ativações de volta para o aplicativo não empacotado.

Se você receber HRESULT 0x800401f0 CoInitialize has not been called. , certifique-se de chamar CoInitialize(nullptr) em seu aplicativo antes de chamar as APIs.

Se você receber HRESULT 0x8000000e A method was called at an unexpected time. ao chamar as APIs de compatibilidade, isso provavelmente significará que você não conseguiu chamar os métodos Register necessários (ou, se um aplicativo empacotado, você não está executando seu aplicativo no contexto empacotado).

Se você receber vários erros de compilação unresolved external symbol, provavelmente esqueceu de adicionar runtimeobject.lib às Dependências adicionais na etapa 1 (ou você só o adicionou à configuração de depuração e não à configuração de versão).

Lidar com versões mais antigas do Windows

Se você oferecer suporte ao Windows 8.1 ou inferior, recomenda-se verificar em tempo de execução se está executando no Windows antes de chamar qualquer API DesktopNotificationManagerCompat ou enviar qualquer notificação do sistema ToastGeneric.

O Windows 8 introduziu notificações do sistema, mas usou os modelos de notificação do sistema herdados, como ToastText01. A ativação foi manipulada pelo evento Activated na memória na classe ToastNotification já que as notificações do sistema eram apenas pop-ups breves que não eram persistentes. O Windows 10 introduziu notificações do interativas do sistema ToastGeneric, e também introduziu a Central de Ações, onde as notificações são persistentes por vários dias. A introdução da Central de Ações exigiu a introdução de um ativador COM, para que sua notificação do sistema possa ser ativada dias depois de criá-la.

SO ToastGeneric Ativador COM Modelos de notificação do sistema herdados
Windows 10 e posterior Com suporte Com suporte Suportado (mas não ativará o servidor COM)
Windows 8.1 / 8 N/D N/D Com suporte
Windows 7 e inferior N/D N/D N/D

Para verificar se você está executando no Windows 10 ou posterior, inclua o cabeçalho <VersionHelpers.h> e verifique o método IsWindows10OrGreater. Se isso retornar true, continue chamando todos os métodos descritos nesta documentação.

#include <VersionHelpers.h>

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

Problemas conhecidos

CORRIGIDO: o aplicativo não se concentra depois de clicar na notificação do sistema: nas compilações 15063 e anteriores, os direitos de primeiro plano não estavam sendo transferidos para seu aplicativo quando ativamos o servidor COM. Portanto, seu aplicativo simplesmente piscaria quando você tentasse movê-lo para o primeiro plano. Não houve nenhuma solução alternativa para esse problema. Corrigimos isso nas compilações 16299 ou posteriores.

Recursos