Implementar um provedor de widget em um aplicativo do Windows C#

Este artigo explica como criar um provedor de widget simples que implemente a interface IWidgetProvider. Os métodos dessa interface são invocados pelo host do widget para solicitar os dados que definem um widget ou para permitir que o provedor de widget responda a uma ação do usuário em um widget. Os provedores de widget podem dar suporte a um só widget ou a vários widgets. Neste exemplo, definiremos dois widgets diferentes. Um dos widgets é um widget de clima fictício que ilustra algumas das opções de formatação fornecidas pelo framework Cartões Adaptáveis. O segundo widget demonstrará as ações do usuário e o recurso de estado do widget personalizado mantendo um contador incrementado sempre que o usuário clicar em um botão exibido no widget.

Uma captura de tela de um widget de clima simples. O widget mostra alguns gráficos e dados relacionados ao clima, bem como algum texto de diagnóstico que ilustra que o modelo para o widget de tamanho médio está sendo exibido.

Uma captura de tela de um widget de contagem simples. O widget mostra uma cadeia de caracteres contendo o valor numérico a ser incrementado e o botão Incrementar, além de um texto de diagnóstico ilustrando que o modelo do widget de tamanho pequeno está exibido.

O código de exemplo presente neste artigo é adaptado do Exemplo de Widgets do SDK do Aplicativo Windows. Para implementar um provedor de widget usando C++/WinRT, confira Implementar um provedor de widget em um aplicativo win32 (C++/WinRT).

Pré-requisitos

  • Um dispositivo Windows 11 com o Widgets Board disponível para teste.
  • O dispositivo precisa ter o modo de desenvolvedor habilitado. Para obter mais informações, consulte Configurações para desenvolvedores.
  • Visual Studio 2022 ou posterior com o workload de desenvolvimento de aplicativos WinUI.

Criar um aplicativo de console C#

Em Visual Studio, crie um novo projeto. Na caixa de diálogo Criar um novo projeto, defina o filtro de idioma como "C#" e o filtro de plataforma para Windows e selecione o modelo de projeto do Aplicativo de Console. Nomeie o novo projeto como "ExampleWidgetProvider". Quando solicitado, defina a versão de destino do .NET como 8.0.

Quando o projeto for carregado, em Gerenciador de Soluções clique com o botão direito do mouse no nome do projeto e selecione Properties. Na página General, role para baixo até Target OS e selecione "Windows". Em Versão do SO de destino, selecione a versão 10.0.19041.0 ou posterior.

Para atualizar o project para dar suporte ao .NET 8.0, em Gerenciador de Soluções clique com o botão direito do mouse no nome do project e selecione Arquivo de Project Edit. Em PropertyGroup, adicione o seguinte elemento RuntimeIdentifiers.

<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>

Observe que este passo a passo usa um aplicativo de console que exibe a janela do console quando o widget é ativado para facilitar a depuração. Quando estiver pronto para publicar seu aplicativo de provedor de widget, você poderá converter o aplicativo de console em um aplicativo Windows seguindo as etapas em Converta seu aplicativo de console em um aplicativo Windows.

Adicionar referências ao SDK do Aplicativo Windows

Este passo a passo usa Microsoft. WindowsAppSDK 2.5.1. Em Gerenciador de Soluções, clique com o botão direito do mouse em Dependencies e selecione Manage NuGet... . No gerenciador de pacotes Do NuGet, selecione a guia Browse e pesquise "Microsoft. WindowsAppSDK". Selecione a versão 2.5.1 ou uma versão estável com suporte posterior dos canais de lançamento do SDK do Aplicativo Windows e clique em Instalar. Use a mesma versão do pacote nos projetos de aplicativo e de empacotamento. Esta é a versão usada pelo passo a passo, não a versão mínima que introduziu APIs de widget.

Adicionar uma classe WidgetProvider para lidar com operações de widget

Em Visual Studio, clique com o botão direito do mouse no projeto ExampleWidgetProvider em Gerenciador de Soluções e selecione Add->Class. No diálogo Adicionar classe, nomeie a classe como "WidgetProvider" e clique em Adicionar. No arquivo WidgetProvider.cs gerado, atualize a definição de classe para indicar que ela implementa a interface IWidgetProvider.

// WidgetProvider.cs
internal class WidgetProvider : IWidgetProvider

Prepare-se para monitorar widgets habilitados

Um provedor de widget pode dar suporte a um só widget ou a vários widgets. Sempre que o host do widget inicia uma operação com o provedor de widget, ele passa uma ID para identificar o widget associado à operação. Cada widget também tem um nome associado e um valor de estado que podem ser usados para armazenar dados personalizados. Neste exemplo, vamos declarar uma estrutura auxiliar simples para armazenar a ID, o nome e os dados de cada widget fixado. Os widgets também podem estar em um estado ativo, que é discutido na seção Ativar e Desativar abaixo, e vamos acompanhar esse estado para cada widget com um valor booliano. Adicione a definição a seguir ao arquivo WidgetProvider.cs, dentro do namespace ExampleWidgetProvider, mas fora da definição da classe WidgetProvider.

// WidgetProvider.cs

public class CompactWidgetInfo
{
    public string? widgetId { get; set; }
    public string? widgetName { get; set; }
    public int customState = 0;
    public bool isActive = false;

}

Dentro da declaração de classe WidgetProvider em WidgetProvider.h, adicione um membro para o mapa que manterá a lista de widgets habilitados, usando a ID do widget como a chave de cada entrada.

// WidgetProvider.cs

// Class member of WidgetProvider
public static Dictionary<string, CompactWidgetInfo> RunningWidgets = new Dictionary<string, CompactWidgetInfo>(); 

Declarar cadeias de caracteres JSON do modelo de widget

Este exemplo vai declarar algumas cadeias de caracteres estáticas para definir os modelos JSON de cada widget. Para facilitar o trabalho, esses modelos são armazenados nas variáveis de membro da classe WidgetProvider. Se você precisar de um armazenamento geral para os modelos, eles poderão ser incluídos como parte do pacote de aplicativos: Acessar arquivos de pacote. Para obter informações sobre como criar o documento JSON do modelo de widget, consulte Criar um modelo de widget com o designer Cartões Adaptáveis.

Os aplicativos que implementam widgets Windows podem personalizar o cabeçalho exibido para seu widget em hosts que dão suporte à personalização de cabeçalho. Para obter mais informações, consulte Personalizar a área de cabeçalho do widget.

Observação

Um widget web pode exibir HTML servido de uma URL. O provedor ainda fornece uma carga JSON de cartão adaptável para especificar essa URL e ainda implementa IWidgetProvider. As APIs de recursos e mensagens foram introduzidas no SDK do Aplicativo Windows 1.6.3. Para obter requisitos de instalação e host, consulte provedores de widget da Web.

// WidgetProvider.cs

// Class members of WidgetProvider
        const string weatherWidgetTemplate = """
{
    "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
    "type": "AdaptiveCard",
    "version": "1.0",
    "speak": "<s>The forecast for Seattle January 20 is mostly clear with a High of 51 degrees and Low of 40 degrees</s>",
    "backgroundImage": "https://raw.githubusercontent.com/microsoft/AdaptiveCards/59402c8d43f7a69a68e0b4fccc08f8a76fc98ccd/source/nodejs/adaptivecards-site/themes/adaptivecards/source/content/Mostly%20Cloudy-Background.jpg",
    "body": [
        {
            "type": "TextBlock",
            "text": "Redmond, WA",
            "size": "large",
            "isSubtle": true,
            "wrap": true
        },
        {
            "type": "TextBlock",
            "text": "Mon, Nov 4, 2019 6:21 PM",
            "spacing": "none",
            "wrap": true
        },
        {
            "type": "ColumnSet",
            "columns": [
                {
                    "type": "Column",
                    "width": "auto",
                    "items": [
                        {
                            "type": "Image",
                            "url": "https://raw.githubusercontent.com/microsoft/AdaptiveCards/59402c8d43f7a69a68e0b4fccc08f8a76fc98ccd/source/nodejs/adaptivecards-site/themes/adaptivecards/source/content/Mostly%20Cloudy-Square.png",
                            "size": "small",
                            "altText": "Mostly cloudy weather"
                        }
                    ]
                },
                {
                    "type": "Column",
                    "width": "auto",
                    "items": [
                        {
                            "type": "TextBlock",
                            "text": "46",
                            "size": "extraLarge",
                            "spacing": "none",
                            "wrap": true
                        }
                    ]
                },
                {
                    "type": "Column",
                    "width": "stretch",
                    "items": [
                        {
                            "type": "TextBlock",
                            "text": "°F",
                            "weight": "bolder",
                            "spacing": "small",
                            "wrap": true
                        }
                    ]
                },
                {
                    "type": "Column",
                    "width": "stretch",
                    "items": [
                        {
                            "type": "TextBlock",
                            "text": "Hi 50",
                            "horizontalAlignment": "left",
                            "wrap": true
                        },
                        {
                            "type": "TextBlock",
                            "text": "Lo 41",
                            "horizontalAlignment": "left",
                            "spacing": "none",
                            "wrap": true
                        }
                    ]
                }
            ]
        }
    ]
}
""";

    const string countWidgetTemplate = """
{                                                                     
    "type": "AdaptiveCard",                                         
    "body": [                                                         
        {                                                               
            "type": "TextBlock",                                    
            "text": "You have clicked the button ${count} times"    
        },
        {
                "text":"Rendering Only if Small",
                "type":"TextBlock",
                "$when":"${$host.widgetSize==\"small\"}"
        },
        {
                "text":"Rendering Only if Medium",
                "type":"TextBlock",
                "$when":"${$host.widgetSize==\"medium\"}"
        },
        {
            "text":"Rendering Only if Large",
            "type":"TextBlock",
            "$when":"${$host.widgetSize==\"large\"}"
        }                                                                    
    ],                                                                  
    "actions": [                                                      
        {                                                               
            "type": "Action.Execute",                               
            "title": "Increment",                                   
            "verb": "inc"                                           
        }                                                               
    ],                                                                  
    "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
    "version": "1.5"                                                
}
""";

Implementar os métodos IWidgetProvider

Nas próximas seções, vamos implementar os métodos da interface IWidgetProvider. O método auxiliar UpdateWidget chamado em várias dessas implementações de método será mostrado mais adiante neste artigo.

Observação

Os objetos passados aos métodos de retorno de chamada da interface IWidgetProvider só têm garantia de serem válidos dentro do retorno de chamada. Você não deve armazenar referências a esses objetos porque o comportamento fora do contexto do retorno de chamada é indefinido.

CreateWidget

O host do widget chama CreateWidget quando o usuário fixa um dos widgets do aplicativo no host do widget. Primeiro, esse método obtém a ID e o nome do widget associado e adiciona uma nova instância da estrutura auxiliar, CompactWidgetInfo, à coleção de widgets habilitados. Em seguida, enviamos o modelo inicial e os dados do widget, que é encapsulado no método auxiliar UpdateWidget.

// WidgetProvider.cs

public void CreateWidget(WidgetContext widgetContext)
{
    var widgetId = widgetContext.Id; // To save RPC calls
    var widgetName = widgetContext.DefinitionId;
    CompactWidgetInfo runningWidgetInfo = new CompactWidgetInfo() { widgetId = widgetId, widgetName = widgetName };
    RunningWidgets[widgetId] = runningWidgetInfo;


    // Update the widget
    UpdateWidget(runningWidgetInfo);
}

DeleteWidget

O host do widget chama DeleteWidget quando o usuário desafixa um dos widgets do aplicativo do host do widget. Quando isso ocorre, removemos o widget associado da lista de widgets habilitados para que não sejam mais enviadas atualizações desse widget.

// WidgetProvider.cs

public void DeleteWidget(string widgetId, string customState)
{
    RunningWidgets.Remove(widgetId);

    if(RunningWidgets.Count == 0)
    {
        emptyWidgetListEvent.Set();
    }
}

Neste exemplo, além de remover o widget com o especificado da lista de widgets habilitados, também verificamos se a lista está vazia e, nesse caso, definimos um evento que será usado mais tarde para permitir que o aplicativo seja encerrado quando não houver widgets habilitados. Dentro da definição de classe, adicione a declaração de ManualResetEvent e uma função de acessador público.

// WidgetProvider.cs
static ManualResetEvent emptyWidgetListEvent = new ManualResetEvent(false);

public static ManualResetEvent GetEmptyWidgetListEvent()
{
    return emptyWidgetListEvent;
}

OnActionInvoked

O host do widget chama OnActionInvoked quando o usuário interage com uma ação que você definiu no modelo de widget. Para o widget de contador usado neste exemplo, uma ação foi declarada com um valor de verbo igual a "inc" no modelo JSON do widget. O código do provedor de widget usará esse valor de verbo para determinar qual ação tomar em resposta à interação do usuário.

...
    "actions": [                                                      
        {                                                               
            "type": "Action.Execute",                               
            "title": "Increment",                                   
            "verb": "inc"                                           
        }                                                               
    ], 
...

No método OnActionInvoked, obtenha o valor do verbo verificando a propriedade Verb do WidgetActionInvokedArgs passado para o método. Se o verbo for "inc", saberemos que vamos incrementar a contagem no estado personalizado para o widget. No WidgetActionInvokedArgs, obtenha o objeto WidgetContext e, em seguida, sua propriedade de ID para identificar a instância do widget que está sendo atualizada. Localize a entrada no mapa de widgets habilitados com a ID especificada e atualize o valor de estado personalizado usado para armazenar o número de incrementos. Por fim, atualize o conteúdo do widget com o novo valor usando a função auxiliar UpdateWidget.

// WidgetProvider.cs

public void OnActionInvoked(WidgetActionInvokedArgs actionInvokedArgs)
{
    var verb = actionInvokedArgs.Verb;
        if (verb == "inc")
        {
            var widgetId = actionInvokedArgs.WidgetContext.Id;
            // If you need to use some data that was passed in after
            // Action was invoked, you can get it from the args:
            var data = actionInvokedArgs.Data;
            if (RunningWidgets.ContainsKey(widgetId))
            {
                var localWidgetInfo = RunningWidgets[widgetId];
                // Increment the count
                localWidgetInfo.customState++;
                UpdateWidget(localWidgetInfo);
            }
        }
}

Para obter informações sobre a sintaxe Action.Execute para Cartões Adaptáveis, consulte Action.Execute. Para obter diretrizes sobre como criar interação para widgets, consulte as diretrizes de design de interação para widgets.

OnWidgetContextChanged

Na versão atual, OnWidgetContextChanged só é chamado quando o usuário altera o tamanho de um widget fixado. Você pode optar por retornar um modelo/dados JSON diferente para o host do widget, dependendo do tamanho solicitado. Você também pode criar o modelo JSON para dar suporte a todos os tamanhos disponíveis usando a renderização condicional com base no valor de host.widgetSize. Se você não precisar enviar um novo modelo ou dados para considerar a alteração de tamanho, use OnWidgetContextChanged para fins de telemetria.

// WidgetProvider.cs

public void OnWidgetContextChanged(WidgetContextChangedArgs contextChangedArgs)
{
    var widgetContext = contextChangedArgs.WidgetContext;
    var widgetId = widgetContext.Id;
    var widgetSize = widgetContext.Size;
    if (RunningWidgets.ContainsKey(widgetId))
    {
        var localWidgetInfo = RunningWidgets[widgetId];
        UpdateWidget(localWidgetInfo);
    }
}
    

Ativar e Desativar

O método Activate é chamado para notificar o provedor de widget de que o host do widget quer receber conteúdo atualizado do provedor no momento. Por exemplo, isso pode significar que o usuário está visualizando ativamente o host do widget no momento. O método Deactivate é chamado para notificar o provedor de widget de que o host do widget não está mais solicitando atualizações de conteúdo. Esses dois métodos definem uma janela na qual o host do widget prefere mostrar o conteúdo mais atualizado. Os provedores de widget podem enviar atualizações para o widget a qualquer momento, como em resposta a uma notificação por push, mas, como em qualquer tarefa em segundo plano, é importante equilibrar o fornecimento de conteúdo atualizado com questões de recursos, como a duração da bateria.

Activate e Deactivate são chamados por widget. Este exemplo acompanha o status ativo de cada widget no struct auxiliar CompactWidgetInfo. No método Activate, chamamos o método auxiliar UpdateWidget para atualizar o widget. Observe que a janela de tempo entre Activate e Deactivate pode ser pequena, portanto, é recomendável que você tente acelerar ao máximo o caminho do código de atualização do widget.

// WidgetProvider.cs

public void Activate(WidgetContext widgetContext)
{
    var widgetId = widgetContext.Id;

    if (RunningWidgets.ContainsKey(widgetId))
    {
        var localWidgetInfo = RunningWidgets[widgetId];
        localWidgetInfo.isActive = true;

        UpdateWidget(localWidgetInfo);
    }
}
public void Deactivate(string widgetId)
{
    if (RunningWidgets.ContainsKey(widgetId))
    {
        var localWidgetInfo = RunningWidgets[widgetId];
        localWidgetInfo.isActive = false;
    }
}

Atualizar um widget

Defina o método auxiliar UpdateWidget para atualizar um widget habilitado. Neste exemplo, marcamos o nome do widget no struct auxiliar CompactWidgetInfo passado ao método e, em seguida, definimos o modelo apropriado e o JSON de dados com base no widget que está sendo atualizado. Um WidgetUpdateRequestOptions é inicializado com o modelo, os dados e o estado personalizado do widget que está sendo atualizado. Chame WidgetManager::GetDefault para obter uma instância da classe WidgetManager e, em seguida, chame UpdateWidget para enviar os dados atualizados do widget ao host do widget.

// WidgetProvider.cs

void UpdateWidget(CompactWidgetInfo localWidgetInfo)
{
    WidgetUpdateRequestOptions updateOptions = new WidgetUpdateRequestOptions(localWidgetInfo.widgetId);

    string? templateJson = null;
    if (localWidgetInfo.widgetName == "Weather_Widget")
    {
        templateJson = weatherWidgetTemplate.ToString();
    }
    else if (localWidgetInfo.widgetName == "Counting_Widget")
    {
        templateJson = countWidgetTemplate.ToString();
    }

    string? dataJson = null;
    if (localWidgetInfo.widgetName == "Weather_Widget")
    {
        dataJson = "{}";
    }
    else if (localWidgetInfo.widgetName == "Counting_Widget")
    {
        dataJson = "{ \"count\": " + localWidgetInfo.customState.ToString() + " }";
    }

    updateOptions.Template = templateJson;
    updateOptions.Data = dataJson;
    updateOptions.IsPlaceholderContent = localWidgetInfo.widgetName == "Weather_Widget";
    // You can store some custom state in the widget service that you will be able to query at any time.
    updateOptions.CustomState= localWidgetInfo.customState.ToString();
    WidgetManager.GetDefault().UpdateWidget(updateOptions);
}

Defina WidgetUpdateRequestOptions.IsPlaceholderContent para true para o conteúdo que ainda não seja personalizado ou acionável. O widget de clima fictício exibe um local fixo, portanto, este exemplo o marca como conteúdo de espaço reservado. O host pode priorizar widgets com conteúdo personalizado em vez deste widget de clima de exemplo. Defina a propriedade para false quando você fornecer conteúdo personalizado. Essa propriedade foi adicionada no SDK do Aplicativo Windows 1.6.6.

Inicializar a lista de widgets habilitados na inicialização

Quando o provedor de widget é inicializado pela primeira vez, é bom perguntar ao WidgetManager se há widgets em execução que o provedor está fornecendo no momento. Isso ajudará a recuperar o aplicativo para o estado anterior em caso de reinicialização do computador ou falha do provedor. Chame WidgetManager::GetDefault para obter a instância do gerenciador de widget padrão do aplicativo. Em seguida, chame GetWidgetInfos, que retorna uma matriz de objetos WidgetInfo. Copie as IDs, os nomes e o estado personalizado do widget no struct auxiliar CompactWidgetInfo e salve-o na variável de membro RunningWidgets. Cole o código a seguir na definição da classe WidgetProvider.

// WidgetProvider.cs

public WidgetProvider()
{
    var runningWidgets = WidgetManager.GetDefault().GetWidgetInfos();

    foreach (var widgetInfo in runningWidgets)
    {
        var widgetContext = widgetInfo.WidgetContext;
        var widgetId = widgetContext.Id;
        var widgetName = widgetContext.DefinitionId;
        var customState = widgetInfo.CustomState;
        if (!RunningWidgets.ContainsKey(widgetId))
        {
            CompactWidgetInfo runningWidgetInfo = new CompactWidgetInfo() { widgetId = widgetId, widgetName = widgetName };
            if (!string.IsNullOrEmpty(customState))
            {
                if (int.TryParse(customState, out int count))
                {
                    runningWidgetInfo.customState = count;
                }
                else
                {
                    Console.Error.WriteLine($"Invalid saved count for widget {widgetId}; resetting to 0.");
                }
            }
            RunningWidgets[widgetId] = runningWidgetInfo;
        }
    }
}

Implementar uma fábrica de classes que criará uma instância do WidgetProvider mediante solicitação

Para que o host do widget se comunique com o provedor de widget, precisamos chamar CoRegisterClassObject. Essa função exige a criação de uma implementação de IClassFactory que criará um objeto de classe para a classe WidgetProvider. Implementaremos a fábrica de classes em uma classe auxiliar autossuficiente.

Em Visual Studio, clique com o botão direito do mouse no projeto ExampleWidgetProvider em Gerenciador de Soluções e selecione Add->Class. No diálogo Adicionar classe, nomeie a classe como "FactoryHelper" e clique em Adicionar.

Substitua o conteúdo do arquivo FactoryHelper.cs pelo código a seguir. Esse código define a interface IClassFactory e implementa seus dois métodos, CreateInstance e LockServer. Esse código é o clichê típico para implementar uma fábrica de classes e não é específico da funcionalidade de um provedor de widget, exceto que indicamos que o objeto de classe que está sendo criado implementa a interface IWidgetProvider.

// FactoryHelper.cs

using Microsoft.Windows.Widgets.Providers;
using System.Runtime.InteropServices;
using WinRT;

namespace COM
{
    static class Guids
    {
        public const string IClassFactory = "00000001-0000-0000-C000-000000000046";
        public const string IUnknown = "00000000-0000-0000-C000-000000000046";
    }

    /// 
    /// IClassFactory declaration
    /// 
    [ComImport, ComVisible(false), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid(COM.Guids.IClassFactory)]
    internal interface IClassFactory
    {
        [PreserveSig]
        int CreateInstance(IntPtr pUnkOuter, ref Guid riid, out IntPtr ppvObject);
        [PreserveSig]
        int LockServer(bool fLock);
    }

    [ComVisible(true)]
    class WidgetProviderFactory<T> : IClassFactory
    where T : IWidgetProvider, new()
    {
        public int CreateInstance(IntPtr pUnkOuter, ref Guid riid, out IntPtr ppvObject)
        {
            ppvObject = IntPtr.Zero;

            if (pUnkOuter != IntPtr.Zero)
            {
                return CLASS_E_NOAGGREGATION;
            }

            IntPtr provider = IntPtr.Zero;
            try
            {
                provider = MarshalInspectable<IWidgetProvider>.FromManaged(new T());
                return Marshal.QueryInterface(provider, in riid, out ppvObject);
            }
            catch (Exception exception)
            {
                Console.Error.WriteLine(exception);
                return Marshal.GetHRForException(exception);
            }
            finally
            {
                if (provider != IntPtr.Zero)
                {
                    Marshal.Release(provider);
                }
            }
        }

        int IClassFactory.LockServer(bool fLock)
        {
            return 0;
        }

        private const int CLASS_E_NOAGGREGATION = -2147221232;

    }
}

Criar um GUID que represente o CLSID do provedor de widget

Em seguida, você precisará criar um GUID representando o CLSID que será usado para identificar o provedor de widget para a ativação do COM. O mesmo valor também será usado ao empacotar o aplicativo. Gere um GUID no Visual Studio acessando Tools->Create GUID. Selecione a opção de formato do registro, clique em Copiar e cole-a em um arquivo de texto para usar mais tarde.

Registrar o objeto de classe do provedor de widget no OLE

No arquivo Program.cs do executável, chamaremos CoRegisterClassObject para registrar o provedor de widget no OLE, para que o host do widget possa interagir com ele. Substitua os conteúdos do Program.cs pelo código a seguir. Esse código importa a função CoRegisterClassObject e a chama, passando a interface WidgetProviderFactory que definimos em uma etapa anterior. Atualize a declaração de variável CLSID_Factory para usar o GUID gerado na etapa anterior.

// Program.cs

using System.Runtime.InteropServices;
using ComTypes = System.Runtime.InteropServices.ComTypes;
using Microsoft.Windows.Widgets;
using ExampleWidgetProvider;
using COM;
using System;

[DllImport("kernel32.dll")]
static extern IntPtr GetConsoleWindow();

[DllImport("ole32.dll")]

static extern int CoRegisterClassObject(
            [MarshalAs(UnmanagedType.LPStruct)] Guid rclsid,
            [MarshalAs(UnmanagedType.IUnknown)] object pUnk,
            uint dwClsContext,
            uint flags,
            out uint lpdwRegister);

[DllImport("ole32.dll")] static extern int CoRevokeClassObject(uint dwRegister);

WinRT.ComWrappersSupport.InitializeComWrappers();
Guid CLSID_Factory = Guid.Parse("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX");
var factory = new WidgetProviderFactory<WidgetProvider>();
Marshal.ThrowExceptionForHR(
    CoRegisterClassObject(CLSID_Factory, factory, 0x4, 0x1, out uint cookie));

try
{
    if (GetConsoleWindow() != IntPtr.Zero)
    {
        Console.WriteLine("Registered successfully. Press ENTER to exit.");
        Console.ReadLine();
    }
    else
    {
        WidgetProvider.GetEmptyWidgetListEvent().WaitOne();
    }
}
finally
{
    int revokeResult = CoRevokeClassObject(cookie);
    if (revokeResult < 0)
    {
        System.Diagnostics.Trace.TraceError($"CoRevokeClassObject failed: 0x{revokeResult:X8}");
    }
    GC.KeepAlive(factory);
}

Observe que este exemplo de código importa a função GetConsoleWindow para determinar se o aplicativo está em execução como um aplicativo de console, o comportamento padrão deste passo a passo. Se a função retornar um ponteiro válido, gravaremos informações de depuração no console. Caso contrário, o aplicativo está em execução como um aplicativo Windows. Nesse caso, aguardamos o evento que definimos no método DeleteWidget quando a lista de widgets habilitados estiver vazia e, em seguida, sairemos do aplicativo. Para obter informações sobre como converter o aplicativo de console de exemplo em um aplicativo Windows, consulte Converta seu aplicativo de console em um aplicativo Windows.

Observação

Aguardar a exclusão do último widget é uma política de tempo de vida simplificada para este exemplo, não um requisito de IWidgetProvider. O host pode ativar e liberar um provedor enquanto os widgets permanecem fixados. Em um provedor de produção, gerencie o tempo de vida do servidor COM de forma independente, restaure o estado do widget salvo na inicialização e sincronize o acesso ao estado compartilhado se os retornos de chamada forem executados simultaneamente. Consulte as observações do IWidgetProvider.

Empacote seu aplicativo de provedor de widget

Este passo a passo registra o provedor de widget da área de trabalho por meio de um manifesto de pacote MSIX e usa a implantação dependente de framework do runtime do SDK do Aplicativo Windows. As etapas a seguir empacotam seu aplicativo e registram seu provedor. Se você escolher a implantação autocontida, siga Implantar aplicativos autocontidos, incluindo as configurações para ambos os projetos ao usar um Projeto de Empacotamento de Aplicativo do Windows.

Criar um projeto de empacotamento MSIX

Em Gerenciador de Soluções, clique com o botão direito do mouse na solução e selecione Adicionar->Novo Projeto... . Na caixa de diálogo Adicionar um novo projeto, selecione o modelo "Projeto de Pacote de Aplicativos do Windows" e clique em Próximo. Defina o nome do projeto como "ExampleWidgetProviderPackage" e clique em Criar. Quando solicitado, defina a versão de destino como 1809 ou posterior e clique em OK. Em seguida, clique com o botão direito do mouse no projeto ExampleWidgetProviderPackage e selecione Add->Project reference. Selecione o projeto ExampleWidgetProvider e clique em OK.

Adicionar a referência do pacote do SDK do Aplicativo Windows ao projeto de empacotamento

Você precisa adicionar uma referência ao pacote NuGet do SDK do Aplicativo Windows ao projeto de empacotamento MSIX. Em Gerenciador de Soluções, clique duas vezes no projeto ExampleWidgetProviderPackage para abrir o arquivo ExampleWidgetProviderPackage.wapproj. Adicione o xml a seguir dentro do elemento Project.

<!--ExampleWidgetProviderPackage.wapproj-->
<ItemGroup>
    <PackageReference Include="Microsoft.WindowsAppSDK" Version="2.5.1">
        <IncludeAssets>build</IncludeAssets>
    </PackageReference>  
</ItemGroup>

Observação

O snippet usa a versão 2.5.1, não a versão mínima que introduziu APIs de widget. Se você selecionou uma versão estável compatível mais recente na etapa anterior, atualize o valor Version para corresponder a ela.

Atualizar o manifesto do pacote

Em Gerenciador de Soluções clique com o botão direito do mouse no arquivo Package.appxmanifest e selecione View Code para abrir o arquivo xml de manifesto. Em seguida, você precisa adicionar algumas declarações de namespace para as extensões do pacote do aplicativo que usaremos. Adicione as seguintes definições de namespace ao elemento Package de nível superior.

<!-- Package.appmanifest -->
<Package
  ...
  xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
  xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"

Dentro do elemento Application, crie um elemento vazio chamado Extensions. Certifique-se de que isso venha após a marca de fechamento de uap:VisualElements.

<!-- Package.appxmanifest -->
<Application>
...
    <Extensions>

    </Extensions>
</Application>

A primeira extensão que precisamos adicionar é a ComServer. Isso registra o ponto de entrada do executável no sistema operacional. Essa extensão é equivalente a um aplicativo empacotado que registra um servidor COM por meio da configuração de uma chave de Registro, e não é específica para provedores de widgets. Adicione o seguinte elemento com:Extension como um filho do elemento Extensions. Altere o GUID no atributo Id do elemento com:Class para o GUID que você gerou em uma etapa anterior.

<!-- Package.appxmanifest -->
<Extensions>
    <com:Extension Category="windows.comServer">
        <com:ComServer>
            <com:ExeServer Executable="ExampleWidgetProvider\ExampleWidgetProvider.exe" DisplayName="ExampleWidgetProvider">
                <com:Class Id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" DisplayName="ExampleWidgetProvider" />
            </com:ExeServer>
        </com:ComServer>
    </com:Extension>
</Extensions>

Em seguida, adicione a extensão que registra o aplicativo como um provedor de widget. Cole o elemento uap3:Extension no snippet de código a seguir, como um filho do elemento Extensions. Substitua o atributo ClassId do elemento COM pelo GUID usado nas etapas anteriores.

<!-- Package.appxmanifest -->
<Extensions>
    ...
    <uap3:Extension Category="windows.appExtension">
        <uap3:AppExtension Name="com.microsoft.windows.widgets" DisplayName="WidgetTestApp" Id="ContosoWidgetApp" PublicFolder="Public">
            <uap3:Properties>
                <WidgetProvider>
                    <ProviderIcons>
                        <Icon Path="Images\StoreLogo.png" />
                    </ProviderIcons>
                    <Activation>
                        <!-- Apps exports COM interface which implements IWidgetProvider -->
                        <CreateInstance ClassId="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" />
                    </Activation>

                    <TrustedPackageFamilyNames>
                        <TrustedPackageFamilyName>Microsoft.MicrosoftEdge.Stable_8wekyb3d8bbwe</TrustedPackageFamilyName>
                    </TrustedPackageFamilyNames>

                    <Definitions>
                        <Definition Id="Weather_Widget"
                            DisplayName="Weather Widget"
                            Description="Weather Widget Description"
                            AllowMultiple="true">
                            <Capabilities>
                                <Capability>
                                    <Size Name="small" />
                                </Capability>
                                <Capability>
                                    <Size Name="medium" />
                                </Capability>
                                <Capability>
                                    <Size Name="large" />
                                </Capability>
                            </Capabilities>
                            <ThemeResources>
                                <Icons>
                                    <Icon Path="ProviderAssets\Weather_Icon.png" />
                                </Icons>
                                <Screenshots>
                                    <Screenshot Path="ProviderAssets\Weather_Screenshot.png" DisplayAltText="For accessibility" />
                                </Screenshots>
                                <!-- DarkMode and LightMode are optional -->
                                <DarkMode />
                                <LightMode />
                            </ThemeResources>
                        </Definition>
                        <Definition Id="Counting_Widget"
                                DisplayName="Microsoft Counting Widget"
                                Description="Counting Widget Description">
                            <Capabilities>
                                <Capability>
                                    <Size Name="small" />
                                </Capability>
                            </Capabilities>
                            <ThemeResources>
                                <Icons>
                                    <Icon Path="ProviderAssets\Counting_Icon.png" />
                                </Icons>
                                <Screenshots>
                                    <Screenshot Path="ProviderAssets\Counting_Screenshot.png" DisplayAltText="For accessibility" />
                                </Screenshots>
                                <!-- DarkMode and LightMode are optional -->
                                <DarkMode>

                                </DarkMode>
                                <LightMode />
                            </ThemeResources>
                        </Definition>
                    </Definitions>
                </WidgetProvider>
            </uap3:Properties>
        </uap3:AppExtension>
    </uap3:Extension>
</Extensions>

Para obter descrições detalhadas e informações de formato de todos esses elementos, confira Formato XML do manifesto do pacote do provedor de widget.

Adicionar ícones e outras imagens ao projeto de empacotamento

Em Gerenciador de Soluções, clique com o botão direito do mouse no ExampleWidgetProviderPackage e selecione Add->New Folder. Nomeie essa pasta como ProviderAssets, pois esse nome foi usado no Package.appxmanifest da etapa anterior. É aqui que armazenaremos os ícones e as capturas de tela dos widgets. Depois de adicionar os ícones e as capturas de tela desejados, verifique se os nomes de imagem correspondem ao que vem após Path=ProviderAssets\ no Package.appxmanifest ou os widgets não aparecerão no host do widget.

Para obter informações sobre os requisitos de design de imagens de captura de tela e as convenções de nomenclatura de capturas de tela localizadas, confira Integrar com o seletor de widget.

Testar o provedor de widget

Selecione a arquitetura que corresponde ao computador de desenvolvimento na lista suspensa Solution Platforms, por exemplo, "x64". Em Gerenciador de Soluções, clique com o botão direito do mouse na solução e selecione Compilar Solução, clique com o botão direito do mouse em ExampleWidgetProviderPackage e selecione Implantar. No Windows 11, abra o painel de widgets com Windows+W, acesse o seletor de widgets e encontre o Widget de Clima e o Widget de Contagem da Microsoft. Adicione-os ao painel e teste a funcionalidade deles. Os controles e o posicionamento do seletor podem variar com as atualizações do Windows e do Widgets Board.

O Cartões Adaptáveis Designer não fornece valores $host, não invoca os callbacks do seu provedor, nem impõe os limites de renderização do Painel de Widgets. Conclua estas verificações no Painel de Widgets:

  1. Confirme se o widget aparece no seletor com o nome, o ícone, a captura de tela e a descrição esperados.
  2. Fixe o widget e verifique seu modelo inicial e dados iniciais.
  3. Redimensione-o para todos os tamanhos declarados no manifesto do pacote. Verifique se há conteúdo recortado, oculto ou excedente em cada tamanho.
  4. Teste cada operação Action.Execute quando $host.isUserContextAuthenticated for verdadeira. Quando for falso, oculte ações não compatíveis e forneça um fallback apropriado; somente Action.OpenUrl tem suporte nesse contexto.
  5. Pare e reinicie o processo do provedor e confirme se cada widget fixado foi restaurado de seu estado personalizado.
  6. Desafixe o widget e confirme se DeleteWidget é chamado.

Para um projeto C# da CLI que usa o MSIX de projeto único em vez do projeto de empacotamento separado neste passo a passo, coloque Package.appxmanifest no projeto do provedor, defina EnableMsixTooling para true e use aplicativo do Windows Development CLI para criar e registrar um pacote de desenvolvimento:

Importante

A CLI de Desenvolvimento de Aplicativos do Windows está atualmente em prévia pública. Recursos e comandos podem ser alterados antes da versão final. Verifique a saída winapp run --help da versão instalada antes de usar esse comando.

winapp run ExampleWidgetProvider.csproj --configuration Release --arch x64 --no-launch

O comando registra o pacote sem iniciar o aplicativo diretamente. Abra o Painel de Widgets para ativar o servidor COM por meio do host do widget.

Depurar o provedor de widget

Depois de fixar os widgets, a Plataforma de Widget iniciará o aplicativo do provedor de widget para receber e enviar informações relevantes sobre o widget. Para depurar o widget em execução, você pode anexar um depurador ao aplicativo do provedor de widget em execução ou configurar Visual Studio para iniciar automaticamente a depuração do processo do provedor de widget quando ele for iniciado.

Para anexá-lo ao processo em execução:

  1. Em Visual Studio clique em Debug -> Anexar ao processo.
  2. Filtre os processos e localize o aplicativo do provedor de widget desejado.
  3. Anexe o depurador.

Para anexar automaticamente o depurador ao processo quando ele for iniciado pela primeira vez:

  1. Em Visual Studio clique em Debug -> Outros Alvos de Depuração -> Depurar Pacote de Aplicativo Instalado.
  2. Filtre os pacotes e localize o pacote do provedor de widget desejado.
  3. Selecione-o e marque a caixa "Não iniciar, mas depurar o código quando ele for iniciado".
  4. Clique em Anexar.

Converter seu aplicativo de console em um aplicativo Windows

Para converter o aplicativo de console criado neste passo a passo em um aplicativo Windows, clique com o botão direito do mouse no projeto ExampleWidgetProvider em Gerenciador de Soluções e selecione Properties. Em Application->General, altere o tipo Output de "Aplicativo de Console" para "aplicativo Windows".

Uma captura de tela mostrando as propriedades do projeto do provedor de widget do C# com o tipo de saída definido como Aplicativo Windows

Para uma edição de arquivo de projeto, defina OutputType como WinExe:

<OutputType>WinExe</OutputType>

Faça isso antes de validar a experiência final do usuário ou publicar. Um provedor de console pode abrir uma janela de terminal sempre que o host do widget ativa seu servidor COM.

Publicar o widget

Depois de desenvolver e testar seu widget, você poderá publicar seu aplicativo no Microsoft Store para que os usuários instalem seus widgets em seus dispositivos. Para obter diretrizes passo a passo para publicar um aplicativo, consulte Publish seu aplicativo no Microsoft Store.

A coleção de repositórios de widgets

Depois que seu aplicativo tiver sido publicado na Microsoft Store, você poderá solicitar que ele seja incluído na Coleção da Loja de widgets que ajuda os usuários a descobrir aplicativos com widgets do Windows. Para enviar sua solicitação, confira Enviar informações do widget para adição à Coleção da Store.

Screenshot do Microsoft Store mostrando a coleção de widgets que permite aos usuários descobrir aplicativos que apresentam Windows Widgets.

Implementando a personalização do widget

A partir do SDK do Aplicativo Windows 1.4, os widgets podem dar suporte à personalização do usuário. Quando esse recurso é implementado, uma opção Personalizar widget é adicionada ao menu de reticências acima da opção Desafixar widget.

Uma captura de tela mostrando um widget com a caixa de diálogo de personalização exibida.

As etapas a seguir resumem o processo de personalização do widget.

  1. Em operação normal, o provedor de widget responde às solicitações do host do widget com o modelo e os conteúdos de dados para a experiência regular do widget.
  2. O usuário clica no botão Personalizar widget no menu de elipses.
  3. O widget gera o evento OnCustomizationRequested no provedor do widget para indicar que o usuário solicitou a experiência de personalização.
  4. O provedor define um sinalizador interno para indicar que o widget está no modo de personalização. Enquanto está no modo de personalização, o provedor do widget envia os modelos JSON para a interface do usuário de personalização do widget em vez da regular.
  5. Enquanto está no modo de personalização, o provedor do widget recebe eventos OnActionInvoked à medida que o usuário interage com a interface do usuário de personalização e ajusta sua configuração e comportamento internos com base nas ações do usuário.
  6. Quando a ação associada ao evento OnActionInvoked é a ação de "personalização de saída" definida pelo aplicativo, o provedor de widget redefine seu sinalizador interno para indicar que ele não está mais no modo de personalização e retoma o envio dos modelos JSON visual e de dados para a experiência regular do widget, refletindo as alterações solicitadas durante a personalização. É possível que o usuário feche a experiência de personalização sem clicar na ação de personalização de saída definida pelo aplicativo. Nesse caso, o evento IWidgetProviderAnalytics.OnAnalyticsInfoReported será acionado e o evento WidgetAnalyticsInfoReportedArgs.AnalyticsJson terá um interactionKind de "exitCustomization".
  7. O provedor do widget persiste as opções de personalização para o disco ou para a nuvem a fim de que as alterações sejam preservadas entre suas invocações.

Observação

Há um bug conhecido com o Windows Widget Board para widgets criados usando o SDK do Aplicativo do Windows, que faz com que o menu de reticências pare de responder depois que o cartão de personalização é mostrado.

Em cenários típicos de personalização do Widget, o usuário escolherá quais dados são exibidos no widget ou ajustará a apresentação visual do widget. Para simplificar, o exemplo nesta seção adicionará o comportamento de personalização que permite ao usuário redefinir o contador do widget de contagem implementado nas etapas anteriores.

Observação

A personalização do widget requer SDK do Aplicativo Windows 1.4 ou posterior. Este passo a passo usa Microsoft. WindowsAppSDK versão 2.5.1.

Atualizar o manifesto do pacote para declarar suporte à personalização

Para permitir que o host do widget saiba que o widget dá suporte à personalização, adicione o atributo IsCustomizable ao elemento Definição para o widget e defina-o como true.

...
<Definition Id="Counting_Widget"
    DisplayName="Microsoft Counting Widget"
    Description="CONFIG counting widget description"
    IsCustomizable="true">
...

Acompanhar quando um widget está no modo de personalização

O exemplo neste artigo usa o struct auxiliar CompactWidgetInfo para acompanhar o estado atual de nossos widgets ativos. Adicione o campo inCustomization, que será usado para acompanhar quando o host do widget espera que enviemos nosso modelo json de personalização em vez do modelo de widget regular.

// WidgetProvider.cs
public class CompactWidgetInfo
{
    public string widgetId { get; set; }
    public string widgetName { get; set; }
    public int customState = 0;
    public bool isActive = false;
    public bool inCustomization = false;
}

Implementar IWidgetProvider2

O recurso de personalização do widget é exposto por meio da interface IWidgetProvider2. Atualize a definição da classe WidgetProvider para implementar essa interface.

// WidgetProvider.cs
internal class WidgetProvider : IWidgetProvider, IWidgetProvider2

Adicione uma declaração para o retorno de chamada OnCustomizationRequested da interface IWidgetProvider2. Esse método usa o mesmo padrão que os outros retornos de chamada que utilizamos. Obtemos a ID do widget a ser personalizada do WidgetContext e localizamos o struct auxiliar CompactWidgetInfo associado a esse widget e definimos o campo inCustomization como true.

// WidgetProvider.cs
public void OnCustomizationRequested(WidgetCustomizationRequestedArgs customizationInvokedArgs)
{
    var widgetId = customizationInvokedArgs.WidgetContext.Id;
    if (RunningWidgets.ContainsKey(widgetId))
    {
        var localWidgetInfo = RunningWidgets[widgetId];
        localWidgetInfo.inCustomization = true;
        UpdateWidget(localWidgetInfo);
    }
}

Agora, declare uma variável de cadeia de caracteres que define o modelo JSON para a interface do usuário de personalização do widget. Para este exemplo, temos um botão "Redefinir contador" e um botão "Sair da personalização" que sinalizarão nosso provedor para retornar ao comportamento regular do widget. Coloque essa definição ao lado das outras definições de modelo.

// WidgetProvider.cs
const string countWidgetCustomizationTemplate = @"
{
    ""type"": ""AdaptiveCard"",
    ""actions"" : [
        {
            ""type"": ""Action.Execute"",
            ""title"" : ""Reset counter"",
            ""verb"": ""reset""
            },
            {
            ""type"": ""Action.Execute"",
            ""title"": ""Exit customization"",
            ""verb"": ""exitCustomization""
            }
    ],
    ""$schema"": ""http://adaptivecards.io/schemas/adaptive-card.json"",
    ""version"": ""1.5""
}";

Enviar modelo de personalização no UpdateWidget

Em seguida, atualizaremos nosso método auxiliar UpdateWidget que envia nossos modelos JSON de dados e visuais para o host do widget. Quando estamos atualizando o widget de contagem, enviamos o modelo de widget regular ou o modelo de personalização, dependendo do valor do campo inCustomization. Para fins de brevidade, o código não relevante para a personalização é omitido neste snippet de código.

// WidgetProvider.cs
void UpdateWidget(CompactWidgetInfo localWidgetInfo)
{
    ...
    else if (localWidgetInfo.widgetName == "Counting_Widget")
    {
        if (!localWidgetInfo.inCustomization)
        {
            templateJson = countWidgetTemplate.ToString();
        }
        else
        {
            templateJson = countWidgetCustomizationTemplate.ToString();
        }
    
    }
    ...
    updateOptions.Template = templateJson;
    updateOptions.Data = dataJson;
    // You can store some custom state in the widget service that you will be able to query at any time.
    updateOptions.CustomState = localWidgetInfo.customState.ToString();
    WidgetManager.GetDefault().UpdateWidget(updateOptions);
}

Responder a ações de personalização

Quando os usuários interagem com entradas em nosso modelo de personalização, ele chama o mesmo manipulador OnActionInvoked de quando o usuário interage com a experiência regular do widget. Para dar suporte à personalização, identificamos os verbos "reset" e "exitCustomization" a partir do nosso modelo JSON de personalização. A ação "Redefinir contador" redefine customState para 0 e sai da personalização. A ação "Sair da personalização" encerra sem redefinir o contador. Ambos definem inCustomization como false para que UpdateWidget envie o modelo de widget regular.

// WidgetProvider.cs
public void OnActionInvoked(WidgetActionInvokedArgs actionInvokedArgs)
{
    var verb = actionInvokedArgs.Verb;
    if (verb == "inc")
    {
        var widgetId = actionInvokedArgs.WidgetContext.Id;
        // If you need to use some data that was passed in after
        // Action was invoked, you can get it from the args:
        var data = actionInvokedArgs.Data;
        if (RunningWidgets.ContainsKey(widgetId))
        {
            var localWidgetInfo = RunningWidgets[widgetId];
            // Increment the count
            localWidgetInfo.customState++;
            UpdateWidget(localWidgetInfo);
        }
    } 
    else if (verb == "reset") 
    {
        var widgetId = actionInvokedArgs.WidgetContext.Id;
        var data = actionInvokedArgs.Data;
        if (RunningWidgets.ContainsKey(widgetId))
        {
            var localWidgetInfo = RunningWidgets[widgetId];
            // Reset the count
            localWidgetInfo.customState = 0;
            localWidgetInfo.inCustomization = false;
            UpdateWidget(localWidgetInfo);
        }
    }
    else if (verb == "exitCustomization")
    {
        var widgetId = actionInvokedArgs.WidgetContext.Id;
        var data = actionInvokedArgs.Data;
        if (RunningWidgets.ContainsKey(widgetId))
        {
            var localWidgetInfo = RunningWidgets[widgetId];
            // Stop sending the customization template
            localWidgetInfo.inCustomization = false;
            UpdateWidget(localWidgetInfo);
        }
    }
}

Agora, ao implantar o widget, você deverá ver o botão Personalizar widget no menu de reticências. Clicar no botão Personalizar exibirá seu modelo de personalização.

Uma captura de tela mostrando a IU de personalização de widgets.

Clique no botão Redefinir contador para redefinir o contador para 0. Clique no botão Sair da personalização para retornar ao comportamento regular do widget.