Implementación de un proveedor de widgets en una aplicación de Windows de C#
En este artículo se explica cómo crear un proveedor de widgets sencillo que implementa la interfaz IWidgetProvider. El host del widget invoca los métodos de esta interfaz para solicitar los datos que definen un widget o para permitir que el proveedor del widget responda a una acción de usuario en un widget. Los proveedores de widgets pueden admitir un único widget o varios widgets. En este ejemplo, definiremos dos widgets diferentes. Uno de los widgets es un widget meteorológico ficticio que ilustra algunas de las opciones de formato que ofrece el marco de tarjetas adaptables. El segundo widget mostrará las acciones del usuario y la característica de estado del widget personalizado manteniendo un contador que se incrementa cada vez que el usuario hace clic en un botón que se muestra en el widget.
Este código de ejemplo de este artículo se adapta del ejemplo de widgets de SDK de Aplicaciones para Windows. Para implementar un proveedor de widgets con C++/WinRT, consulte Implementación de un proveedor de widgets en una aplicación win32 (C++/WinRT).
Requisitos previos
- El dispositivo debe tener habilitado el modo de desarrollador. Para obtener más información, vea Habilitar el dispositivo para el desarrollo.
- Visual Studio 2022 o posterior con la carga de trabajo Desarrollo de la Plataforma universal de Windows. Asegúrese de agregar el componente para C++ (v143) desde la lista desplegable opcional.
Creación de una aplicación de consola de C#
En Visual Studio, cree un nuevo proyecto. En el cuadro de diálogo Crear un nuevo proyecto, establezca el filtro de lenguaje en "C#" y el filtro de plataforma en Windows y, a continuación, seleccione la plantilla de proyecto Aplicación de consola. Ponga al nuevo proyecto el nombre "ExampleWidgetProvider". Cuando se le solicite, establezca la versión de .NET de destino en 8.0.
Cuando se cargue el proyecto, en el Explorador de soluciones, haga clic con el botón derecho en el nombre del proyecto y seleccione Propiedades. En la página General, desplácese hacia abajo hasta Sistema operativo de destino y seleccione "Windows". En Versión del sistema operativo de destino, seleccione la versión 10.0.19041.0 o posterior.
Para actualizar el proyecto para que admita .NET 8.0, en Explorador de soluciones haga clic con el botón derecho en el nombre del proyecto y seleccione Editar archivo de proyecto. Dentro de PropertyGroup, agregue el siguiente elemento RuntimeIdentifiers.
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
Tenga en cuenta que en este tutorial se usa una aplicación de consola que muestra la ventana de consola cuando se activa el widget para habilitar la depuración sencilla. Cuando esté listo para publicar la aplicación del proveedor de widgets, puede convertir la aplicación de consola en una aplicación de Windows siguiendo los pasos descritos en Conversión de la aplicación de consola en una aplicación de Windows.
Adición de referencias al SDK de Aplicaciones para Windows
En este ejemplo se usa el paquete NuGet SDK de Aplicaciones para Windows estable más reciente. En Explorador de soluciones, haga clic con el botón derecho en Dependencias y seleccione Administrar paquetes NuGet.... En el administrador de paquetes NuGet, seleccione la pestaña Examinar y busque "Microsoft.WindowsAppSDK". Seleccione la versión estable más reciente en la lista desplegable Versión y, a continuación, haga clic en Instalar.
Adición de una clase WidgetProvider para controlar las operaciones del widget
En Visual Studio, haga clic con el botón derecho en el proyecto ExampleWidgetProvider
en Explorador de soluciones y seleccione Agregar->clase. En el cuadro de diálogo Agregar clase, asigne a la clase el nombre WidgetProvider y, después, haga clic en Agregar. En el archivo WidgetProvider.cs generado, actualice la definición de clase para indicar que implementa la interfaz IWidgetProvider.
// WidgetProvider.cs
internal class WidgetProvider : IWidgetProvider
Preparación para realizar un seguimiento de los widgets habilitados
Los proveedores de widgets pueden admitir un único widget o varios widgets. Cada vez que el host del widget inicia una operación con el proveedor de widgets, pasa un identificador para identificar el widget asociado a la operación. Cada widget también tiene un nombre asociado y un valor de estado que se puede usar para almacenar datos personalizados. En este ejemplo, declararemos una estructura auxiliar sencilla para almacenar el identificador, el nombre y los datos de cada widget anclado. Los widgets también pueden estar en un estado activo, que se describe en la sección Activar y desactivar a continuación, y realizaremos un seguimiento de este estado para cada widget con un valor booleano. Agregue la siguiente definición al archivo WidgetProvider.cs, dentro del espacio de nombres ExampleWidgetProvider, pero fuera de la definición de clase 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 de la definición de clase WidgetProvider en WidgetProvider.cs, agregue un miembro para el mapa que mantendrá la lista de widgets habilitados, con el identificador de widget como clave para cada entrada.
// WidgetProvider.cs
// Class member of WidgetProvider
public static Dictionary<string, CompactWidgetInfo> RunningWidgets = new Dictionary<string, CompactWidgetInfo>();
Declaración de cadenas JSON de plantilla de widget
En este ejemplo se declararán algunas cadenas estáticas para definir las plantillas JSON para cada widget. Por comodidad, estas plantillas se almacenan en las variables miembro de la clase WidgetProvider. Si necesita un almacenamiento general para las plantillas, se pueden incluir como parte del paquete de aplicación: Acceso a los archivos de paquete. Para obtener información sobre cómo crear el documento JSON de plantilla de widget, consulte Creación de una plantilla de widget con el Diseñador de tarjetas adaptables.
// 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://messagecardplayground.azurewebsites.net/assets/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://messagecardplayground.azurewebsites.net/assets/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"
}
""";
Implementación de los métodos IWidgetProvider
En las secciones siguientes, implementaremos los métodos de la interfaz IWidgetProvider. El método auxiliar UpdateWidget al que se llama en varias de estas implementaciones de método se mostrará más adelante en este artículo.
Nota
Solo se garantiza que los objetos pasados a los métodos de devolución de llamada de la interfaz IWidgetProvider sean válidos dentro de la devolución de llamada. No debe almacenar referencias a estos objetos porque su comportamiento fuera del contexto de la devolución de llamada no está definido.
CreateWidget
El host del widget llama a CreateWidget cuando el usuario ha anclado uno de los widgets de la aplicación en el host del widget. En primer lugar, este método obtiene el identificador y el nombre del widget asociado y agrega una nueva instancia de nuestra estructura auxiliar, CompactWidgetInfo, a la colección de widgets habilitados. A continuación, enviamos la plantilla inicial y los datos del widget, que se encapsulan en el 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
El host del widget llama a DeleteWidget cuando el usuario ha desanclado uno de los widgets de la aplicación del host del widget. Cuando esto ocurra, quitaremos el widget asociado de nuestra lista de widgets habilitados para que no enviemos más actualizaciones para ese widget.
// WidgetProvider.cs
public void DeleteWidget(string widgetId, string customState)
{
RunningWidgets.Remove(widgetId);
if(RunningWidgets.Count == 0)
{
emptyWidgetListEvent.Set();
}
}
En este ejemplo, además de quitar el widget con lo especificado de la lista de widgets habilitados, también comprobamos si la lista está vacía y, si es así, establecemos un evento que se usará más adelante para permitir que la aplicación se cierre cuando no haya widgets habilitados. Dentro de la definición de clase, agregue la declaración de ManualResetEvent y una función de descriptor de acceso público.
// WidgetProvider.cs
static ManualResetEvent emptyWidgetListEvent = new ManualResetEvent(false);
public static ManualResetEvent GetEmptyWidgetListEvent()
{
return emptyWidgetListEvent;
}
OnActionInvoked
El host del widget llama a OnActionInvoked cuando el usuario interactúa con una acción que definió en la plantilla de widget. Para el widget de contador usado en este ejemplo, se declaró una acción con un valor de verbo de "inc" en la plantilla JSON del widget. El código del proveedor de widgets usará este valor de verbo para determinar qué acción se realizará en respuesta a la interacción del usuario.
...
"actions": [
{
"type": "Action.Execute",
"title": "Increment",
"verb": "inc"
}
],
...
En el método OnActionInvoked, obtenga el valor de verbo comprobando la propiedad Verb del widgetActionInvokedArgs pasado al método. Si el verbo es "inc", sabemos que vamos a incrementar el recuento en el estado personalizado del widget. En WidgetActionInvokedArgs, obtenga el objeto WidgetContext y, a continuación, widgetId para obtener el identificador del widget que se está actualizando. Busque la entrada en nuestro mapa de widgets habilitados con el identificador especificado y, a continuación, actualice el valor de estado personalizado que se usa para almacenar el número de incrementos. Por último, actualice el contenido del widget con el nuevo valor con la función 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 obtener información sobre la sintaxis Action.Execute para Tarjetas adaptables, vea Action.Execute. Para obtener instrucciones sobre cómo diseñar la interacción para widgets, consulte Guía de diseño de interacción de widgets.
OnWidgetContextChanged
En la versión actual, solo se llama a OnWidgetContextChanged cuando el usuario cambia el tamaño de un widget anclado. Puede optar por devolver una plantilla o datos JSON diferentes al host del widget en función del tamaño solicitado. También puede diseñar el JSON de plantilla para admitir todos los tamaños disponibles mediante la representación condicional basada en el valor de host.widgetSize. Si no necesita enviar una nueva plantilla o datos para tener en cuenta el cambio de tamaño, puede utilizar el OnWidgetContextChanged con fines de telemetría.
// 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);
}
}
Activar y desactivar
Se llama al método Activar para notificar al proveedor del widget que el host del widget está interesado actualmente en recibir contenido actualizado del proveedor. Por ejemplo, podría significar que el usuario está viendo activamente el host del widget. Se llama al método Desactivar para notificar al proveedor de widgets que el host del widget ya no solicita actualizaciones de contenido. Estos dos métodos definen una ventana en la que el host del widget está más interesado en mostrar el contenido más actualizado. Los proveedores de widgets pueden enviar actualizaciones al widget en cualquier momento, como en respuesta a una notificación de inserción, pero como con cualquier tarea en segundo plano, es importante equilibrar la provisión de contenido actualizado con problemas de recursos como la duración de la batería.
Activar y Desactivar se activan en cada widget. En este ejemplo se realiza un seguimiento del estado activo de cada widget en la estructura auxiliar CompactWidgetInfo. En el método Activar, llamamos al método auxiliar UpdateWidget para actualizar el widget. Tenga en cuenta que el período de tiempo entre Activar y Desactivar puede ser pequeño, por lo que se recomienda intentar que la ruta de acceso del código de actualización del widget sea lo más rápida posible.
// 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;
}
}
Actualización de un widget
Defina el método auxiliar UpdateWidget para actualizar un widget habilitado. En este ejemplo, comprobamos el nombre del widget en la estructura auxiliar CompactWidgetInfo que se pasa al método y, a continuación, establecemos la plantilla y el JSON de datos adecuados en función del widget que se esté actualizando. Se inicializa WidgetUpdateRequestOptions con la plantilla, los datos y el estado personalizado del widget que se está actualizando. Llame a WidgetManager::GetDefault para obtener una instancia de la clase WidgetManager y, a continuación, llame a UpdateWidget para enviar los datos del widget actualizado al host del 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;
// 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);
}
Inicialización de la lista de widgets habilitados en el inicio
Cuando el proveedor de widgets se inicializa por primera vez, es una buena idea preguntar a WidgetManager si hay algún widget en ejecución que nuestro proveedor esté atendiendo actualmente. Ayudará a recuperar la aplicación al estado anterior en caso del reinicio del equipo o bloqueo del proveedor. Llame a WidgetManager::GetDefault para obtener la instancia predeterminada del administrador de widgets para la aplicación. A continuación, llame a GetWidgetInfos, que devuelve una matriz de objetos WidgetInfo. Copie los identificadores, los nombres y el estado personalizado de los widgets en la estructura auxiliar CompactWidgetInfo y guárdelo en la variable de miembro RunningWidgets. Pegue el código siguiente en el constructor de la clase 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 = widgetName, widgetName = widgetId };
try
{
// If we had any save state (in this case we might have some state saved for Counting widget)
// convert string to required type if needed.
int count = Convert.ToInt32(customState.ToString());
runningWidgetInfo.customState = count;
}
catch
{
}
RunningWidgets[widgetId] = runningWidgetInfo;
}
}
}
Implementación de un generador de clases que creará una instancia de WidgetProvider a petición
Para que el host del widget se comunique con nuestro proveedor de widgets, debemos llamar a CoRegisterClassObject. Esta función requiere que creemos una implementación de IClassFactory que creará un objeto de clase para nuestra clase WidgetProvider. Implementaremos nuestro generador de clases en una clase auxiliar independiente.
En Visual Studio, haga clic con el botón derecho en el proyecto ExampleWidgetProvider
en Explorador de soluciones y seleccione Agregar->clase. En el cuadro de diálogo Agregar clase, asigne a la clase el nombre "FactoryHelper" y, después, haga clic en Agregar.
Reemplace todo el contenido del archivo FactoryHelper.cs por el siguiente código. Este código define la interfaz IClassFactory e implementa dos métodos: CreateInstance y LockServer. Este código es el típico reemplazable para implementar un generador de clases y no es específico de la funcionalidad de un proveedor de widgets, excepto que indicamos que el objeto de clase que se está creando implementa la interfaz 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)
{
Marshal.ThrowExceptionForHR(CLASS_E_NOAGGREGATION);
}
if (riid == typeof(T).GUID || riid == Guid.Parse(COM.Guids.IUnknown))
{
// Create the instance of the .NET object
ppvObject = MarshalInspectable<IWidgetProvider>.FromManaged(new T());
}
else
{
// The object that ppvObject points to does not support the
// interface identified by riid.
Marshal.ThrowExceptionForHR(E_NOINTERFACE);
}
return 0;
}
int IClassFactory.LockServer(bool fLock)
{
return 0;
}
private const int CLASS_E_NOAGGREGATION = -2147221232;
private const int E_NOINTERFACE = -2147467262;
}
}
Creación de un GUID que representa el CLSID para el proveedor de widgets
A continuación, debe crear un GUID que represente el CLSID que se usará para identificar el proveedor de widgets para la activación COM. También se usará el mismo valor al empaquetar la aplicación. Para generar un GUID en Visual Studio, vaya a Herramientas->Crear GUID. Seleccione la opción de formato del Registro y haga clic en Copiar. A continuación, péguela en un archivo de texto para poder copiarla más tarde.
Registro del objeto de clase de proveedor de widgets con OLE
En el archivo Program.cs de nuestro ejecutable, llamaremos a CoRegisterClassObject para registrar nuestro proveedor de widgets con OLE, de forma que el host de widgets pueda interactuar con él. Reemplace el contenido de Program.cs por el código siguiente. Este código importa la función CoRegisterClassObject y la llama, pasando la interfaz WidgetProviderFactory definida en un paso anterior. Asegúrese de actualizar la declaración de variable CLSID_Factory para usar el GUID que generó en el paso 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);
Console.WriteLine("Registering Widget Provider");
uint cookie;
Guid CLSID_Factory = Guid.Parse("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX");
CoRegisterClassObject(CLSID_Factory, new WidgetProviderFactory<WidgetProvider>(), 0x4, 0x1, out cookie);
Console.WriteLine("Registered successfully. Press ENTER to exit.");
Console.ReadLine();
if (GetConsoleWindow() != IntPtr.Zero)
{
Console.WriteLine("Registered successfully. Press ENTER to exit.");
Console.ReadLine();
}
else
{
// Wait until the manager has disposed of the last widget provider.
using (var emptyWidgetListEvent = WidgetProvider.GetEmptyWidgetListEvent())
{
emptyWidgetListEvent.WaitOne();
}
CoRevokeClassObject(cookie);
}
Tenga en cuenta que este ejemplo de código importa la función GetConsoleWindow para determinar si la aplicación se ejecuta como una aplicación de consola, el comportamiento predeterminado de este tutorial. Si la función devuelve un puntero válido, se escribe información de depuración en la consola. De lo contrario, la aplicación se ejecuta como una aplicación de Windows. En ese caso, esperamos al evento que establecimos en el método DeleteWidget cuando la lista de widgets habilitados esté vacía y salimos de la app. Para obtener información sobre cómo convertir la aplicación de consola de ejemplo en una aplicación de Windows, consulte Conversión de la aplicación de consola en una aplicación de Windows.
Empaquetado de la aplicación del proveedor de widgets
En la versión actual, solo las aplicaciones empaquetadas se pueden registrar como proveedores de widgets. Los pasos siguientes le llevarán a través del proceso de empaquetado de la aplicación y la actualización del manifiesto de aplicación para registrar la aplicación con el sistema operativo como proveedor de widgets.
Creación de un proyecto de empaquetado MSIX
En Explorador de soluciones, haga clic con el botón derecho en la solución y seleccione Agregar->Nuevo proyecto.... En el cuadro de diálogo Agregar un nuevo proyecto, seleccione la plantilla "Proyecto de paquete de aplicación de Windows" y haga clic en Siguiente. Establezca el nombre del proyecto en "ExampleWidgetProviderPackage" y haga clic en Crear. Cuando se le solicite, establezca el destino en la versión 1809 o posterior y haga clic en Aceptar. A continuación, haga clic con el botón derecho en el proyecto ExampleWidgetProviderPackage y seleccione Agregar->Referencia de proyecto. Seleccione el proyecto ExampleWidgetProvider y haga clic en Aceptar.
Adición de la referencia del paquete SDK de Aplicaciones para Windows al proyecto de empaquetado
Debe agregar una referencia al paquete Nuget SDK de Aplicaciones para Windows al proyecto de empaquetado MSIX. En Explorador de soluciones, haga doble clic en el proyecto ExampleWidgetProviderPackage para abrir el archivo ExampleWidgetProviderPackage.wapproj. Agregue el siguiente XML dentro del elemento Proyecto.
<!--ExampleWidgetProviderPackage.wapproj-->
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.2.221116.1">
<IncludeAssets>build</IncludeAssets>
</PackageReference>
</ItemGroup>
Nota
Asegúrese de que la versión especificada en el elemento PackageReference coincide con la versión estable más reciente a la que se hizo referencia en el paso anterior.
Si la versión correcta del SDK de Aplicaciones para Windows ya está instalada en el equipo y no desea agrupar el entorno de ejecución del SDK en el paquete, puede especificar la dependencia del paquete en el archivo Package.appxmanifest para el proyecto ExampleWidgetProviderPackage.
<!--Package.appxmanifest-->
...
<Dependencies>
...
<PackageDependency Name="Microsoft.WindowsAppRuntime.1.2-preview2" MinVersion="2000.638.7.0" Publisher="CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US" />
...
</Dependencies>
...
Actualización del manifiesto del paquete
En el Explorador de soluciones, haga clic con el botón derecho en el archivo Package.appxmanifest
y seleccione Ver código para abrir el archivo xml del manifiesto. A continuación, debe agregar algunas declaraciones de espacio de nombres para las extensiones de paquete de la aplicación que usaremos. Agregue las siguientes definiciones de espacio de nombres al elemento de nivel superior Package.
<!-- 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 del elemento Application, cree un nuevo elemento vacío denominado Extensions. Asegúrese de que aparece después de la etiqueta de cierre de uap:VisualElements.
<!-- Package.appxmanifest -->
<Application>
...
<Extensions>
</Extensions>
</Application>
La primera extensión que necesitamos agregar es ComServer. Registra el punto de entrada del ejecutable en el sistema operativo. Esta extensión es el equivalente en aplicaciones empaquetadas a registrar un servidor COM estableciendo una clave del Registro, y no es específica de los proveedores de widgets. Agregue el siguiente elemento com:Extension como elemento secundario del elemento Extensions. Cambie el GUID en el atributo Id del elemento com:Class por el GUID que generó en un paso 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>
A continuación, agregue la extensión que registra la aplicación como proveedor de widgets. Pegue el elemento uap3:Extension en el siguiente fragmento de código, como elemento secundario del elemento Extensions. Asegúrese de reemplazar el atributo ClassId del elemento COM por el GUID que usó en los pasos 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="Couting 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 obtener descripciones detalladas e información de formato para todos estos elementos, vea Formato XML del manifiesto de paquete del proveedor de widgets.
Adición de iconos y otras imágenes al proyecto de empaquetado
En Explorador de soluciones, haga clic con el botón derecho en exampleWidgetProviderPackage y seleccione Agregar->Nueva carpeta. Asigne a esta carpeta el nombre ProviderAssets, ya que es lo que se utilizó en Package.appxmanifest
del paso anterior. Aquí es donde almacenaremos nuestros iconos y capturas de pantalla para nuestros widgets. Una vez que agregue los iconos y capturas de pantalla deseados, asegúrese de que los nombres de imagen coinciden con lo que viene después de Path=ProviderAssets\ en Package.appxmanifest
o los widgets no se mostrarán en el host de widgets.
Para obtener información sobre los requisitos de diseño de las imágenes de las capturas de pantalla y las convenciones de nomenclatura para las capturas de pantalla localizadas, consulte Integración con el selector de widgets.
Prueba del proveedor de widgets
Asegúrese de que ha seleccionado la arquitectura que coincide con la máquina de desarrollo en la lista desplegable Plataformas de soluciones, por ejemplo, "x64". En el Explorador de soluciones, haga clic con el botón secundario en la solución y seleccione Generar solución. Una vez hecho esto, haga clic con el botón derecho en ExampleWidgetProviderPackage y seleccione Implementar. En la versión actual, el único host de widgets admitido es el Panel de widgets. Para ver los widgets, deberá abrir el Panel de widgets y seleccionar Agregar widgets en la parte superior derecha. Desplácese hasta la parte inferior de los widgets disponibles y debería ver el widget meteorológico ficticio y el widget de recuento de Microsoft que se crearon en este tutorial. Haga clic en los widgets para anclarlos al panel de widgets y probar su funcionalidad.
Depuración del proveedor de widgets
Después de anclar los widgets, la plataforma de widgets iniciará la aplicación del proveedor de widgets para recibir y enviar información pertinente sobre el widget. Para depurar el widget en ejecución, puede asociar un depurador a la aplicación del proveedor de widgets en ejecución o configurar Visual Studio para que inicie automáticamente la depuración del proceso del proveedor de widgets una vez iniciado.
Para asocia un elemento al proceso en ejecución:
- En Visual Studio, haga clic en Depurar->Asociar al proceso.
- Filtre los procesos y busque la aplicación del proveedor de widgets deseada.
- Asocie el depurador.
Para asociar automáticamente el depurador al proceso cuando se inicia por primera vez:
- En Visual Studio, haga clic en Depurar -> Otros destinos de depuración -> Depurar paquete de aplicación instalado.
- Filtre los paquetes y busque el paquete de proveedor de widgets deseado.
- Selecciónelo y active la casilla que indica No iniciar, pero depurar mi código cuando se inicie.
- Haga clic en Adjuntar.
Conversión de la aplicación de consola en una aplicación de Windows
Para convertir la aplicación de consola creada en este tutorial en una aplicación de Windows, haga clic con el botón derecho en el proyecto ExampleWidgetProvider en el Explorador de soluciones y seleccione Propiedades. En Aplicación->General, cambie el Tipo de salida de "Aplicación de consola" a "Aplicación de Windows".
Publicación del widget
Después de haber desarrollado y probado el widget, puede publicar la aplicación en Microsoft Store para que los usuarios instalen los widgets en sus dispositivos. Para obtener instrucciones paso a paso para publicar una aplicación, consulta Publicación de la aplicación en Microsoft Store.
Colección de widgets en Store
Una vez publicada la aplicación en Microsoft Store, puede solicitar que la aplicación se incluya en la colección de widgets de Store, que ayuda a los usuarios a descubrir aplicaciones que incorporan widgets de Windows. Para enviar la solicitud, consulte Envío de la información del widget para agregarlo a la colección de Store.
Implementación de la personalización de widgets
A partir del SDK 1.4 de aplicaciones para Windows, los widgets pueden admitir la personalización del usuario. Cuando se implementa esta característica, se agrega una opción Personalizar widget al menú de puntos suspensivos situado encima de la opción Desanclar widget.
En los pasos siguientes, se resume el proceso de personalización del widget.
- En funcionamiento normal, el proveedor de widgets responde a las solicitudes del host de widgets con la plantilla y las cargas útiles de datos para la experiencia de widget normal.
- El usuario hace clic en el botón Personalizar widget en el menú de puntos suspensivos.
- El widget genera el evento OnCustomizationRequested en el proveedor de widgets para indicar que el usuario ha solicitado la experiencia de personalización del widget.
- El proveedor de widgets establece una marca interna para indicar que el widget está en modo de personalización. Mientras está en modo de personalización, el proveedor de widgets envía las plantillas JSON para la interfaz de usuario de personalización del widget en lugar de la interfaz de usuario normal del widget.
- Mientras está en modo de personalización, el proveedor de widgets recibe eventos OnActionInvoked a medida que el usuario interactúe con la interfaz de usuario de personalización y ajuste su configuración interna y comportamiento en función de las acciones del usuario.
- Cuando la acción asociada al evento OnActionInvoked sea la acción de "salir de la personalización" definida por la aplicación, el proveedor de widgets restablecerá su marca interna para indicar que ya no está en modo de personalización y se reanudará el envío de las plantillas JSON de datos y visuales para la experiencia normal del widget, lo que reflejará los cambios solicitados durante la personalización.
- El proveedor de widgets conserva las opciones de personalización en el disco o en la nube para que los cambios se conserven entre las invocaciones del proveedor de widgets.
Nota:
Hay un error conocido con el Panel de widgets de Windows para los widgets creados mediante el SDK de aplicaciones para Windows que hace que el menú de puntos suspensivos deje de responder después de que se muestre la tarjeta de personalización.
En escenarios típicos de personalización de widgets, el usuario elegirá qué datos se mostrarán en el widget o ajustará la presentación visual del widget. Para una mayor simplicidad, en el ejemplo de esta sección se agregará el comportamiento de personalización que permite al usuario restablecer el contador del widget de recuento implementado en los pasos anteriores.
Nota:
La personalización del widget solo se admite en el SDK 1.4 y versiones posteriores de aplicaciones para Windows. Asegúrese de actualizar las referencias del proyecto a la versión más reciente del paquete Nuget.
Actualice el manifiesto del paquete para declarar la compatibilidad con la personalización
Para que el host del widget sepa que el widget admite la personalización, agregue el atributo IsCustomizable al elemento Definición para el widget y establézcalo en verdadero.
...
<Definition Id="Counting_Widget"
DisplayName="Microsoft Counting Widget"
Description="CONFIG counting widget description"
IsCustomizable="true">
...
Seguimiento cuando un widget está en modo de personalización
En el ejemplo de este artículo se usa la estructura de aplicación auxiliar CompactWidgetInfo para realizar un seguimiento del estado actual de nuestros widgets activos. Agregue el campo inCustomization, que se usará para realizar el seguimiento cuando el host del widget espere que enviemos nuestra plantilla JSON de personalización en lugar de la plantilla de widget normal.
// 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;
}
Implementación de IWidgetProvider2
La característica de personalización del widget se expone a través de la interfaz IWidgetProvider2. Actualice la definición de clase WidgetProvider para implementar esta interfaz.
// WidgetProvider.cs
internal class WidgetProvider : IWidgetProvider, IWidgetProvider2
Agregue una implementación para la onCustomizationRequested devolución de llamada de la interfaz deIWidgetProvider2. Este método usa el mismo patrón que las otras devoluciones de llamada que hemos usado. Obtenemos el identificador para que el widget se personalice desde WidgetContext, busque la estructura auxiliar CompactWidgetInfo asociada a ese widget y establezca el campo inCustomization en verdadero.
// 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);
}
}
Por último, declare una variable de cadena que defina la plantilla JSON para la interfaz de usuario de personalización del widget. En este ejemplo, tenemos un botón "Restablecer contador" y un botón "Salir de la personalización", que indicarán a nuestro proveedor que vuelva al comportamiento normal del widget. Sitúe esta definición junto a las demás definiciones de plantilla.
// 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 plantilla de personalización en UpdateWidget
A continuación, actualizaremos nuestro método auxiliar UpdateWidget, que enviará nuestras plantillas JSON visuales y datos al host del widget. Cuando se actualiza el widget de recuento, se envía la plantilla de widget normal o la de personalización, en función del valor del campo inCustomization. Para una mayor brevedad, el código no relevante para la personalización se omite en este fragmento 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 acciones de personalización
Cuando los usuarios interactúan con entradas en nuestra plantilla de personalización, se llama al mismo controlador OnActionInvoked que cuando el usuario interactúa con la experiencia de widget normal. Para admitir la personalización, se buscan los verbos "reset" y "exitCustomization" en nuestra plantilla JSON de personalización. Si la acción fuera para el botón "Restablecer contador", se restablecerá el contador mantenido en el campo customState de nuestra estructura auxiliar a 0. Si la acción fuera para el botón "Salir de la personalización", se establecerá el campo inCustomization en falso para que cuando se llame a UpdateWidget, nuestro método auxiliar envíe las plantillas JSON normales y no la plantilla de personalización.
// 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);
}
}
}
Ahora, al implementar el widget, debería ver el botón Personalizar widget en el menú de puntos suspensivos. Al hacer clic en el botón Personalizar, se mostrará la plantilla de personalización.
Haga clic en el botón Restablecer contador para restablecer el contador a 0. Haga clic en el botón Salir de la personalización para volver al comportamiento normal del widget.