C# Windows アプリでウィジェット プロバイダーを実装する

この記事では、IWidgetProvider インターフェイスを実装する簡単なウィジェット プロバイダーを作成する手順について説明します。 ウィジェット ホストは、このインターフェイスのメソッドを呼び出して、ウィジェットを定義するデータを要求したり、ウィジェット プロバイダーがウィジェットのユーザー アクションに応答できるようにしたりします。 ウィジェット プロバイダーは、単一のウィジェットまたは複数のウィジェットをサポートできます。 この例では、2 つの異なるウィジェットを定義します。 1 つのウィジェットは、アダプティブ カード フレームワークによって提供されるいくつかの書式設定オプションを示す模擬天気ウィジェットです。 2 つ目のウィジェットでは、ウィジェットに表示されているボタンをユーザーがクリックするたびにインクリメントされるカウンターを維持することで、ユーザー アクションとカスタム ウィジェットの状態機能を示します。

A screenshot of a simple weather widget. The widget shows some weather-related graphics an data as well as some diagnostic text illustrating that the template for the medium size widget is being displayed.

A screenshot of a simple counting widget. The widget shows a string containing the numeric value to be incremented and a button labeled Increment, as well as some diagnostic text illustrating that the template for the small size widget is being displayed.

この記事のこのサンプル コードは、Windows App SDK ウィジェット サンプルにあります。 C++/WinRT を使ってウィジェット プロバイダーを実装するには、「win32 アプリでウィジェット プロバイダーを実装する (C++/WinRT)」をご覧ください。

前提条件

  • デバイスで開発者モードが有効になっている必要があります。 詳しくは、「デバイスを開発用に有効にする」をご覧ください。
  • ユニバーサル Windows プラットフォーム開発ワークロードを含む Visual Studio 2022 以降。 オプションのドロップダウンから C++ (v143) 用のコンポーネントを追加してください。

新しい C# コンソール アプリを作成する

Visual Studio で、新しいプロジェクトを作成します。 [新しいプロジェクトの作成] ダイアログで、言語フィルターを "C#" に設定し、プラットフォーム フィルターを Windows に設定してから、コンソール アプリ プロジェクト テンプレートを選びます。 新しいプロジェクトに "ExampleWidgetProvider" という名前を付けます。 メッセージが表示されたら、ターゲットの .NET バージョンを 6.0 に設定します。

プロジェクトが読み込まれたら、ソリューション エクスプローラーでプロジェクト名を右クリックして、[プロパティ] を選びます。 [全般] ページで、[ターゲット OS] まで下にスクロールして [Windows] を選びます。 [ターゲット OS バージョン] で、バージョン 10.0.19041.0 以降を選びます。

このチュートリアルでは、簡単にデバッグできるよう、ウィジェットがアクティブになったらコンソール ウィンドウを表示するコンソール アプリを使うことに注意してください。 ウィジェット プロバイダー アプリを発行する準備ができたら、「コンソール アプリを Windows アプリに変換する」の手順のようにして、コンソール アプリケーションを Windows アプリケーションに変換できます。

Windows App SDK と Windows 実装ライブラリの NuGet パッケージへの参照を追加する

このサンプルでは、最新の安定した Windows App SDK NuGet パッケージを使います。 ソリューション エクスプローラー[依存関係] を右クリックして、[NuGet パッケージの管理...] を選びます。NuGet パッケージ マネージャーで [参照] タブを選んで、"Microsoft.WindowsAppSDK" を検索します。 [バージョン] ドロップダウンで最新の安定バージョンを選んで、[インストール] をクリックします。

ウィジェットの操作を処理する WidgetProvider クラスを追加する

Visual Studio のソリューション エクスプローラーExampleWidgetProvider プロジェクトを右クリックして、[追加] > [クラス] を選びます。 [クラスの追加] ダイアログで、クラスに "WidgetProvider" という名前を付けて、[追加] をクリックします。 生成された WidgetProvider.cs ファイルで、クラスの定義を更新し、IWidgetProvider インターフェイスを実装していることを示します。

// WidgetProvider.cs
internal class WidgetProvider : IWidgetProvider

有効なウィジェットを追跡するための準備をする

ウィジェット プロバイダーは、単一のウィジェットまたは複数のウィジェットをサポートできます。 ウィジェット ホストは、ウィジェット プロバイダーでの操作を開始するたびに、操作に関連付けられているウィジェットを識別するための ID を渡します。 各ウィジェットには、カスタム データの格納に使用できる名前と状態値も関連付けられています。 この例では、ピン留めされた各ウィジェットの ID、名前、データを格納する単純なヘルパー構造体を宣言します。 ウィジェットはアクティブな状態にすることもできます。これについては後の「アクティブ化と非アクティブ化」セクションで説明されており、ブール値を使って各ウィジェットのこの状態を追跡します。 次の定義を、WidgetProvider.cs ファイルの ExampleWidgetProvider 名前空間の内側 (ただし、WidgetProvider クラス定義の外側) に追加します。

// WidgetProvider.cs

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

}

WidgetProvider.cs の WidgetProvider クラス定義の内側に、ウィジェット ID を各エントリのキーとして使って、有効なウィジェットのリストを保持するマップのメンバーを追加します。

// WidgetProvider.cs

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

ウィジェット テンプレートの JSON 文字列を宣言する

この例では、静的文字列をいくつか宣言して、各ウィジェットの JSON テンプレートを定義します。 使いやすくするため、これらのテンプレートは WidgetProvider クラスのメンバー変数に格納します。 テンプレート用の一般的な格納場所が必要な場合は、アプリケーション パッケージの一部として含めることができます (パッケージ ファイルへのアクセスに関する記事を参照)。 ウィジェット テンプレートの JSON ドキュメントの作成については、「アダプティブ カード デザイナーを使用してウィジェット テンプレートを作成する」をご覧ください。

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

IWidgetProvider メソッドを実装する

次の数セクションでは、IWidgetProvider インターフェイスのメソッドを実装します。 これらのメソッドの実装のいくつかで呼び出されるヘルパー メソッド UpdateWidget については、この記事で後ほど説明します。

注意

IWidgetProvider インターフェイスのコールバック メソッドに渡されるオブジェクトは、コールバック内でのみ有効であることが保証されます。 コールバックのコンテキストの外部での動作は定義されていないため、これらのオブジェクトへの参照は保存しないでください。

CreateWidget

ユーザーがウィジェット ホストでアプリのウィジェットの 1 つをピン留めすると、ウィジェット ホストは CreateWidget を呼び出します。 このメソッドは、最初に、関連付けられたウィジェットの ID と名前を取得して、ヘルパー構造体 CompactWidgetInfo の新しいインスタンスを有効なウィジェットのコレクションに追加します。 次に、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

ユーザーがウィジェット ホストからアプリのウィジェットの 1 つのピン留めを外すと、ウィジェット ホストは DeleteWidget を呼び出します。 それが発生したら、有効なウィジェットのリストから関連付けられているウィジェットを削除して、そのウィジェットにそれ以上更新が送信されないようにします。

// WidgetProvider.cs

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

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

この例では、有効なウィジェットのリストから指定されたウィジェットを削除するだけでなく、リストが空になったかどうかをチェックし、空の場合は、有効なウィジェットがないときにアプリが終了できるように後で使用するイベントを設定します。 クラス定義の内側に、ManualResetEvent の宣言とパブリック アクセサー関数を追加します。

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

public static ManualResetEvent GetEmptyWidgetListEvent()
{
    return emptyWidgetListEvent;
}

OnActionInvoked

ユーザーがウィジェット テンプレートで定義されているアクションを実行すると、ウィジェット ホストは OnActionInvoked を呼び出します。 この例で使われているカウンター ウィジェットの場合、アクションは、ウィジェットの JSON テンプレートで verb の値 "inc" によって宣言されています。 ウィジェット プロバイダーのコードでは、この verb の値を使って、ユーザーの操作に応答して実行するアクションを決定します。

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

OnActionInvoked メソッドでは、メソッドに渡された WidgetActionInvokedArgsVerb プロパティを調べて、verb の値を取得します。 verb が "inc" の場合は、ウィジェットのカスタム状態でカウントをインクリメントすることがわかります。 WidgetActionInvokedArgs から WidgetContext オブジェクトを取得し、さらに WidgetId を取得して、更新されているウィジェットの ID を取得します。 有効なウィジェットのマップで指定された ID を持つエントリを検索し、インクリメントの数の格納に使われているカスタム状態の値を更新します。 最後に、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);
            }
        }
}

アダプティブ カードの Action.Execute の構文については、「Action.Execute」をご覧ください。 ウィジェットの対話式操作の設計に関するガイダンスについては、「ウィジェットの対話式操作の設計のガイダンス」をご覧ください

OnWidgetContextChanged

現在のリリースの OnWidgetContextChanged は、ピン留めされたウィジェットのサイズをユーザーが変更した場合にのみ呼び出されます。 要求されたサイズに応じて、異なる JSON テンプレートとデータをウィジェット ホストに返すことができます。 host.widgetSize の値に基づく条件付きレンダリングを使って、使用できるすべてのサイズをサポートするようにテンプレートの JSON を設計することもできます。 サイズの変更に対応して新しいテンプレートまたはデータを送信する必要がない場合は、テレメトリのために OnWidgetContextChanged を使用できます。

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

アクティブ化と非アクティブ化

Activate メソッドは、ウィジェット ホストの現在の関心が、更新された内容をウィジェット プロバイダーから受け取ることであることを、プロバイダーに通知するために呼び出されます。 たとえば、ユーザーが現在ウィジェット ホストをアクティブに表示していることを意味する可能性があります。 Deactivate メソッドは、ウィジェット ホストが内容の更新を要求しなくなったことをウィジェット プロバイダーに通知するために呼び出されます。 これら 2 つのメソッドでは、ウィジェット ホストが最新の内容を表示することに最も関心を持っているウィンドウが定義されています。 ウィジェット プロバイダーは、プッシュ通知への応答など、いつでもウィジェットに更新を送信できますが、バックグラウンド タスクと同様に、最新の内容を提供することと、バッテリーの寿命などのリソースに関する配慮との、バランスを取することが重要です。

ActivateDeactivate は、ウィジェットごとに呼び出されます。 次の例では、CompactWidgetInfo ヘルパー構造体の各ウィジェットのアクティブな状態を追跡します。 Activate メソッドでは、UpdateWidget ヘルパー メソッドを呼び出してウィジェットを更新します。 ActivateDeactivate の間の間隔が短い場合があるため、ウィジェットの更新コード パスの時間をできる限り短縮することをお勧めします。

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

ウィジェットを更新する

有効なウィジェットを更新するには、UpdateWidget ヘルパー メソッドを定義します。 この例では、メソッドに渡された CompactWidgetInfo ヘルパー構造体でウィジェットの名前をチェックし、更新対象のウィジェットに基づいて適切なテンプレートとデータ JSON を設定します。 更新対象のウィジェットのテンプレート、データ、カスタム状態で、WidgetUpdateRequestOptions が初期化されます。 WidgetManager::GetDefault を呼び出して WidgetManager クラスのインスタンスを取得した後、UpdateWidget を呼び出して、更新されたウィジェット データをウィジェット ホストに送信します。

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

起動時に有効なウィジェットのリストを初期化する

ウィジェット プロバイダーが最初に初期化されるときに、プロバイダーが現在提供している実行中のウィジェットがあるかどうかを WidgetManager に確認することをお勧めします。 これは、コンピューターの再起動またはプロバイダーのクラッシュが発生した場合に、アプリを以前の状態に回復するのに役立ちます。 WidgetManager.GetDefault を呼び出して、アプリの既定のウィジェット マネージャー インスタンスを取得します。 次に、GetWidgetInfos を呼び出して、WidgetInfo オブジェクトの配列を取得します。 ウィジェットの ID、名前、カスタム状態をヘルパー構造体 CompactWidgetInfo にコピーして、RunningWidgets メンバー変数に保存します。 次のコードを、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;
        }
    }
}

要求時に WidgetProvider をインスタンス化するクラス ファクトリを実装する

ウィジェット ホストがウィジェット プロバイダーと通信するには、CoRegisterClassObject を呼び出す必要があります。 この関数では、WidgetProvider クラスのクラス オブジェクトを作成する IClassFactory の実装を作成する必要があります。 このクラス ファクトリを自己完結型ヘルパー クラスに実装します。

Visual Studio のソリューション エクスプローラーExampleWidgetProvider プロジェクトを右クリックして、[追加] > [クラス] を選びます。 [クラスの追加] ダイアログで、クラスに "FactoryHelper" という名前を付けて、[追加] をクリックします。

FactoryHelper.cs ファイルの内容を次のコードに置き換えます。 このコードでは、IClassFactory インターフェイスを定義し、その 2 つのメソッド CreateInstanceLockServer を実装します。 これは、クラス ファクトリを実装するための一般的な定型コードであり、作成されるクラス オブジェクトが 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;

    }
}

ウィジェット プロバイダーの CLSID を表す GUID を作成する

次に、COM のアクティブ化でウィジェット プロバイダーを識別するために使われる CLSID を表す GUID を作成する必要があります。 同じ値が、アプリをパッケージ化するときにも使われます。 Visual Studio で [ツール] > [GUID の作成] に移動して、GUID を生成します。 レジストリ形式のオプションを選び、[コピー] をクリックして、後でコピーできるようにテキスト ファイルに貼り付けます。

ウィジェット プロバイダー クラスのオブジェクトを OLE に登録する

実行可能ファイルの Program.cs ファイルで、CoRegisterClassObject を呼び出してウィジェット プロバイダーを OLE に登録し、ウィジェット ホストがそれを操作できるようにします。 Program.cs の内容を次のコードに置き換えます。 このコードは、CoRegisterClassObject 関数をインポートして呼び出し、前のステップで定義した WidgetProviderFactory インターフェイスを渡します。 前のステップで生成した GUID を使うように、CLSID_Factory 変数宣言を必ず更新してください。

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

このコード例では、GetConsoleWindow 関数をインポートして、アプリがコンソール アプリケーション (このチュートリアルの既定の動作) として実行されているかどうかを判断していることに注意してください。 関数から有効なポインターが返される場合は、デバッグ情報をコンソールに書き込みます。 それ以外の場合、アプリは Windows アプリとして実行されます。 その場合は、有効なウィジェットのリストが空のときに DeleteWidget メソッドで設定されるイベントを待って、アプリを終了します。 コンソール アプリの例を Windows アプリに変換する方法については、「コンソール アプリを Windows アプリに変換する」をご覧ください。

ウィジェット プロバイダー アプリをパッケージ化する

現在のリリースでは、ウィジェット プロバイダーとして登録できるのはパッケージ化されたアプリのみです。 次の手順では、アプリをウィジェット プロバイダーとして OS に登録するために、アプリをパッケージ化してアプリ マニフェストを更新するプロセスについて説明します。

MSIX パッケージ プロジェクトを作成する

ソリューション エクスプローラーでソリューションを右クリックして、[追加] > [新しいプロジェクト...] を選びます。[新しいプロジェクトの追加] ダイアログで "Windows アプリケーション パッケージ プロジェクト" テンプレートを選んで、[次へ] をクリックします。 プロジェクト名を "ExampleWidgetProviderPackage" に設定して、[作成] をクリックします。 求められたらターゲット バージョンをバージョン 1809 以降に設定し、[OK] をクリックします。 次に、ExampleWidgetProviderPackage プロジェクトを右クリックして、[追加] > [プロジェクト参照] を選びます。 ExampleWidgetProvider プロジェクトを選んで、[OK] をクリックします。

Windows App SDK のパッケージ参照をパッケージ プロジェクトに追加する

Windows App SDK Nuget パッケージへの参照を、MSIX パッケージ プロジェクトに追加する必要があります。 ソリューション エクスプローラーで ExampleWidgetProviderPackage プロジェクトをダブルクリックして、ExampleWidgetProviderPackage.wapproj ファイルを開きます。 Project 要素内に次の XML を追加します。

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

注意

PackageReference 要素で指定されている Version が、前のステップで参照した最新の安定バージョンと一致していることを確認します。

正しいバージョンの Windows App SDK が既にコンピューターにインストールされていて、SDK のランタイムをパッケージにバンドルしたくない場合は、ExampleWidgetProviderPackage プロジェクトの Package.appxmanifest ファイルでパッケージの依存関係を指定できます。

<!--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>
...

パッケージ マニフェストを更新する

ソリューション エクスプローラーPackage.appxmanifest ファイルを右クリックし、[コードの表示] を選んでマニフェストの xml ファイルを開きます。 次に、使用するアプリ パッケージ拡張機能用の名前空間の宣言をいくつか追加する必要があります。 最上位の 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"

Application 要素内に、Extensions という名前の新しい空の要素を作成します。 これは、uap:VisualElements の終了タグの後に必ず追加します。

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

    </Extensions>
</Application>

追加する必要がある最初の拡張機能は、ComServer 拡張機能です。 これにより、実行可能ファイルのエントリ ポイントが OS に登録されます。 この拡張機能は、レジストリ キーを設定することで COM サーバーを登録するのと同等のパッケージ アプリの機能であり、ウィジェット プロバイダーに固有のものではありません。次の com:Extension 要素を、Extensions 要素の子として追加します。 com:Class 要素の Id 属性の GUID を、前のステップで生成した GUID に変更します。

<!-- 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>

次に、アプリをウィジェット プロバイダーとして登録する拡張機能を追加します。 次のコード スニペットの uap3:Extension 要素を、Extensions 要素の子として貼り付けます。 必ず、COM 要素の ClassId 属性を、前の手順で使った GUID に置き換えてください。

<!-- 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>

これらの要素の詳細な説明と形式の情報については、「ウィジェット プロバイダー パッケージ マニフェストの XML 形式」をご覧ください。

アイコンと他の画像をパッケージ プロジェクトに追加する

ソリューション エクスプローラーExampleWidgetProviderPackage を右クリックして、[追加] > [新しいフォルダー] を選びます。 このフォルダーに ProviderAssets という名前を付けます。これは、前のステップの Package.appxmanifest で使ったものです。 ここに、ウィジェットのアイコンスクリーンショットを格納します。 必要なアイコンとスクリーンショットを追加したら、画像の名前が Package.appxmanifestPath=ProviderAssets\ の後に続く部分と一致することを確認します。そうでないと、ウィジェットはウィジェット ホストに表示されません。

スクリーンショットの画像の設計要件とローカライズされたスクリーンショットの名前付け規則については、「ウィジェット ピッカーと統合する」をご覧ください。

ウィジェット プロバイダーをテストする

[ソリューション プラットフォーム] ドロップダウンで選んだアーキテクチャが、開発用コンピューターと一致していることを確認します (例: "x64")。 ソリューション エクスプローラーでソリューションを右クリックして、[ソリューションのビルド] を選びます。 これが済んだら、ExampleWidgetProviderPackage を右クリックして [配置] を選びます。 現在のリリースでサポートされているウィジェット ホストは、ウィジェット ボードのみです。 ウィジェットを表示するには、ウィジェット ボードを開き、右上の [ウィジェットの追加] を選ぶ必要があります。 使用できるウィジェットの一番下までスクロールすると、このチュートリアルで作成した模擬天気ウィジェットMicrosoft カウント ウィジェットが表示されます。 ウィジェットをクリックしてウィジェット ボードにピン留めし、その機能をテストします。

ウィジェット プロバイダーをデバッグする

ウィジェットをピン留めすると、ウィジェットに関する関連情報を受信および送信するために、ウィジェット プラットフォームによってウィジェット プロバイダー アプリケーションが起動されます。 実行中のウィジェットをデバッグするには、実行中のウィジェット プロバイダー アプリケーションにデバッガーをアタッチするか、ウィジェット プロバイダー プロセスの起動後にそのデバッグを自動的に開始するよう Visual Studio を設定することができます。

実行中のプロセスにアタッチするには:

  1. Visual Studio で、[デバッグ] > [プロセスにアタッチ] をクリックします。
  2. プロセスをフィルター処理し、目的のウィジェット プロバイダー アプリケーションを見つけます。
  3. デバッガーをアタッチします。

プロセスが最初に開始されるときにデバッガーを自動的にアタッチするには:

  1. Visual Studio で、[デバッグ] > [その他のデバッグ ターゲット] > [インストールされているアプリ パッケージのデバッグ] の順にクリックします。
  2. パッケージをフィルター処理し、目的のウィジェット プロバイダー パッケージを見つけます。
  3. それを選び、[起動はしないが、開始時にコードをデバッグする] チェック ボックスをオンにします。
  4. [アタッチ] をクリックします。

コンソール アプリを Windows アプリに変換する

このチュートリアルで作成したコンソール アプリを Windows アプリに変換するには、ソリューション エクスプローラーExampleWidgetProvider プロジェクトを右クリックして、[プロパティ] を選びます。 [アプリケーション] > [全般] で、[出力の種類] を [コンソール アプリケーション] から [Windows アプリケーション] に変更します。

A screenshot showing the C# widget provider project properties with the output type set to Windows Application

ウィジェットを発行する

ウィジェットを開発してテストした後は、ユーザーがウィジェットをデバイスにインストールできるように、Microsoft Store でアプリを発行する必要があります。 アプリを発行するための詳細なガイダンスについては、「Microsoft Store でアプリを発行する」をご覧ください。

ウィジェット ストア コレクション

Microsoft Store でアプリを発行した後は、ユーザーが Windows ウィジェットを備えたアプリを見つけやすいように、アプリをウィジェット ストア コレクションに追加することを要求できます。 要求を送信するには、「ストア コレクションに追加するためにウィジェットの情報を送信する」をご覧ください。

Screenshot of the Microsoft Store showing the widgets collection that allows users to discover apps that feature Windows Widgets.

ウィジェットのカスタマイズの実装

Windows アプリ SDK 1.4 以降では、ウィジェットはユーザーのカスタマイズをサポートできます。 この機能を実装すると、[ウィジェットのピン留めを外す] オプションの上にある省略記号メニューに [ウィジェットのカスタマイズ] オプションが追加されます。

A screenshot showing a widget with the customization dialog displayed.

次の手順は、ウィジェットをカスタマイズするためのプロセスをまとめたものです。

  1. 通常の操作では、ウィジェット プロバイダーは、通常のウィジェット エクスペリエンス用のテンプレートおよびデータ ペイロードを使用して、ウィジェット ホストからの要求に応答します。
  2. ユーザーが省略記号メニューの [ウィジェットのカスタマイズ] ボタンをクリックします。
  3. ウィジェットは、ユーザーがウィジェットのカスタマイズ エクスペリエンスを要求したことを示すために、ウィジェット プロバイダーで OnCustomizationRequested イベントを発生させます。
  4. ウィジェット プロバイダーは、ウィジェットがカスタマイズ モードであることを示す内部フラグを設定します。 カスタマイズ モードの間、ウィジェット プロバイダーは、通常のウィジェット UI ではなく、ウィジェットカスタマイズ UI の JSON テンプレートを送信します。
  5. カスタマイズ モードでは、ユーザーがカスタマイズ UI を操作し、ユーザーのアクションに基づいて内部構成と動作を調整するときに、ウィジェット プロバイダーは OnActionInvoked イベントを受け取ります。
  6. OnActionInvoked イベントに関連付けられているアクションがアプリ定義の "カスタマイズ終了" アクションである場合、ウィジェット プロバイダーはその内部フラグをリセットして、カスタマイズ モードではなくなったことを示し、通常のウィジェット エクスペリエンス用のビジュアルおよびデータ JSON テンプレートの送信を再開し、カスタマイズ時に要求された変更を反映します。
  7. ウィジェット プロバイダーは、ウィジェット プロバイダーの呼び出し間での変更が保持されるように、カスタマイズ オプションをディスクまたはクラウドに保持します。

Note

Windows アプリ SDK を使用して構築されたウィジェットには、Windows ウィジェット ボードに関する既知のバグがあります。これにより、カスタマイズ カードが表示された後、省略記号メニューが応答しなくなります。

一般的なウィジェットのカスタマイズ シナリオでは、ユーザーはウィジェットに表示されるデータを選択するか、ウィジェットの視覚的な表示を調整します。 わかりやすくするために、このセクションの例では、前の手順で実装したカウント ウィジェットのカウンターをユーザーがリセットできるようにするカスタマイズ動作を追加します。

Note

ウィジェットのカスタマイズは、Windows アプリ SDK 1.4 以降でのみサポートされています。 プロジェクト内の参照を最新バージョンの Nuget パッケージに更新してください。

パッケージ マニフェストを更新してカスタマイズのサポートを宣言する

ウィジェットがカスタマイズをサポートしていることをウィジェット ホストに知らせるために、ウィジェットの Definition 要素に IsCustomizable 属性を追加し、true に設定します。

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

ウィジェットがカスタマイズ モードであることを追跡する

この記事の例では、ヘルパー構造体 CompactWidgetInfo を使用して、アクティブなウィジェットの現在の状態を追跡します。 inCustomization フィールドを追加します。これは、ウィジェット ホストが通常のウィジェット テンプレートではなく、カスタマイズ JSON テンプレートを送信することが想定されるタイミングを追跡するために使用されます。

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

IWidgetProvider2 を実装する

ウィジェットのカスタマイズ機能は、IWidgetProvider2 インターフェイスを介して公開されます。 WidgetProvider クラス定義を更新して、このインターフェイスを実装します。

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

IWidgetProvider2 インターフェイスの OnCustomizationRequested コールバックの実装を追加します。 このメソッドは、使用した他のコールバックと同じパターンを使用します。 WidgetContext からカスタマイズされるウィジェットの ID を取得し、そのウィジェットに関連付けられている CompactWidgetInfo ヘルパー構造体を検索し、inCustomization フィールドを 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);
    }
}

ここで、ウィジェット カスタマイズ UI の JSON テンプレートを定義する文字列変数を宣言します。 この例では、[カウンターのリセット] ボタンと、プロバイダーに通常のウィジェットの動作に戻る通知を表示する [カスタマイズの終了] ボタンがあります。 この定義は、他のテンプレート定義の横に配置します。

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

UpdateWidget でカスタマイズ テンプレートを送信する

次に、データとビジュアル JSON テンプレートをウィジェット ホストに送信する UpdateWidget ヘルパー メソッドを更新します。 カウント ウィジェットを更新する場合は、inCustomization フィールドの値に応じて、通常のウィジェット テンプレートまたはカスタマイズ テンプレートのいずれかを送信します。 簡潔にするために、このコード スニペットではカスタマイズに関連しないコードは省略されます。

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

カスタマイズ アクションに対応する

ユーザーがカスタマイズ テンプレートの入力を対話操作すると、ユーザーが通常のウィジェット エクスペリエンスと対話操作する場合と同じ OnActionInvoked ハンドラーが呼び出されます。 カスタマイズをサポートするために、カスタマイズ JSON テンプレートから動詞 "reset" と "exitCustomization" を検索します。 アクションが [カウンターのリセット] ボタンの場合は、ヘルパー構造体の customState フィールドに保持されているカウンターを 0 にリセットします。 アクションが [カスタマイズの終了] ボタンの場合はinCustomization フィールドを false に設定して、UpdateWidget を呼び出す場合に、ヘルパー メソッドがカスタマイズ テンプレートではなく通常の JSON テンプレートを送信するようにします。

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

これで、ウィジェットをデプロイすると、省略記号メニューに [ウィジェットのカスタマイズ] ボタンが表示されるようになります。 カスタマイズ ボタンをクリックすると、カスタマイズ テンプレートが表示されます。

A screenshot showing the widgets customization UI.

[カウンターのリセット] ボタンをクリックして、カウンターを 0 にリセットします。 [カスタマイズの終了] ボタンをクリックして、ウィジェットの通常の動作に戻ります。