win32 アプリでウィジェット プロバイダーを実装する (C++/WinRT)

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

簡単な天気ウィジェットのスクリーンショット。このウィジェットには、天気関連のグラフィックスとデータ、および中サイズのウィジェットのテンプレートが表示されていることを示す診断テキストが表示されます。

簡単なカウント ウィジェットのスクリーンショット。このウィジェットには、インクリメントされる数値を含む文字列と、[インクリメント] というラベルが付いたボタン、および小サイズのウィジェットのテンプレートが表示されていることを示す診断テキストが表示されます。

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

前提条件

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

新しい C++/WinRT win32 コンソール アプリを作成する

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

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

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

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

プリコンパイル済みヘッダー ファイル pch.h に、次の include ディレクティブを追加します。

//pch.h 
#pragma once
#include <wil/cppwinrt.h>
#include <wil/resource.h>
...
#include <winrt/Microsoft.Windows.Widgets.Providers.h>

注意

最初に wil/cppwinrt.h ヘッダー、その後で WinRT のヘッダーをインクルードする必要があります。

ウィジェット プロバイダー アプリのシャットダウンを正しく処理するには、winrt::get_module_lock のカスタム実装が必要です。 SignalLocalServerShutdown メソッドを事前に宣言し、それを main.cpp ファイルで定義して、アプリに終了を通知するイベントを設定します。 pch.h ファイルの #pragma once ディレクティブのすぐ後、他の include の前に、次のコードを追加します。

//pch.h
#include <stdint.h>
#include <combaseapi.h>

// In .exe local servers the class object must not contribute to the module ref count, and use
// winrt::no_module_lock, the other objects must and this is the hook into the C++ WinRT ref counting system
// that enables this.
void SignalLocalServerShutdown();

namespace winrt
{
    inline auto get_module_lock() noexcept
    {
        struct service_lock
        {
            uint32_t operator++() noexcept
            {
                return ::CoAddRefServerProcess();
            }

            uint32_t operator--() noexcept
            {
                const auto ref = ::CoReleaseServerProcess();

                if (ref == 0)
                {
                    SignalLocalServerShutdown();
                }
                return ref;
            }
        };

        return service_lock{};
    }
}


#define WINRT_CUSTOM_MODULE_LOCK

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

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

IWidgetProvider インターフェイスを実装するクラスを宣言する

IWidgetProvider インターフェイスでは、ウィジェット ホストがウィジェット プロバイダーでの操作を開始するために呼び出すメソッドが定義されています。 WidgetProvider.h ファイルの空のクラス定義を次のコードに置き換えます。 このコードでは、IWidgetProvider インターフェイスを実装する構造体と、インターフェイス メソッドのプロトタイプを宣言しています。

// WidgetProvider.h
struct WidgetProvider : winrt::implements<WidgetProvider, winrt::Microsoft::Windows::Widgets::Providers::IWidgetProvider>
{
    WidgetProvider();

    /* IWidgetProvider required functions that need to be implemented */
    void CreateWidget(winrt::Microsoft::Windows::Widgets::Providers::WidgetContext WidgetContext);
    void DeleteWidget(winrt::hstring const& widgetId, winrt::hstring const& customState);
    void OnActionInvoked(winrt::Microsoft::Windows::Widgets::Providers::WidgetActionInvokedArgs actionInvokedArgs);
    void OnWidgetContextChanged(winrt::Microsoft::Windows::Widgets::Providers::WidgetContextChangedArgs contextChangedArgs);
    void Activate(winrt::Microsoft::Windows::Widgets::Providers::WidgetContext widgetContext);
    void Deactivate(winrt::hstring widgetId);
    /* IWidgetProvider required functions that need to be implemented */

    
};

また、プライベート メソッド UpdateWidget を追加します。これは、プロバイダーからウィジェット ホストに更新を送信するヘルパー メソッドです。

// WidgetProvider.h
private: 

void UpdateWidget(CompactWidgetInfo const& localWidgetInfo);

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

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

// WidgetProvider.h
struct CompactWidgetInfo
{
    winrt::hstring widgetId;
    winrt::hstring widgetName;
    int customState = 0;
    bool isActive = false;
};

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

// WidgetProvider.h
struct WidgetProvider : winrt::implements<WidgetProvider, winrt::Microsoft::Windows::Widgets::Providers::IWidgetProvider>
{
...
    private:
        ...
        static std::unordered_map<winrt::hstring, CompactWidgetInfo> RunningWidgets;

        

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

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

// WidgetProvider.h
const std::string weatherWidgetTemplate = R"(
{
    "$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 std::string countWidgetTemplate = R"(
{                                                                     
    "type": "AdaptiveCard",                                         
    "body": [                                                         
        {                                                               
            "type": "TextBlock",                                    
            "text": "You have clicked the button ${count} times"    
        },
        {
             "text":"Rendering Only if Medium",
             "type":"TextBlock",
             "$when":"${$host.widgetSize==\"medium\"}"
        },
        {
             "text":"Rendering Only if Small",
             "type":"TextBlock",
             "$when":"${$host.widgetSize==\"small\"}"
        },
        {
         "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 については、この記事で後ほど説明します。 インターフェイスのメソッドに移る前に、WidgetProvider.cpp の include ディレクティブの後に次の行を追加します。これらの行では、ウィジェット プロバイダーの API を winrt 名前空間にプルし、前のステップで宣言したマップへのアクセスを許可しています。

注意

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

// WidgetProvider.cpp
namespace winrt
{
    using namespace Microsoft::Windows::Widgets::Providers;
}

std::unordered_map<winrt::hstring, CompactWidgetInfo> WidgetProvider::RunningWidgets{};

CreateWidget

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

// WidgetProvider.cpp
void WidgetProvider::CreateWidget(winrt::WidgetContext widgetContext)
{
    auto widgetId = widgetContext.Id();
    auto widgetName = widgetContext.DefinitionId();
    CompactWidgetInfo runningWidgetInfo{ widgetId, widgetName };
    RunningWidgets[widgetId] = runningWidgetInfo;
    
    // Update the widget
    UpdateWidget(runningWidgetInfo);
}

DeleteWidget

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

// WidgetProvider.cpp
void WidgetProvider::DeleteWidget(winrt::hstring const& widgetId, winrt::hstring const& customState)
{
    RunningWidgets.erase(widgetId);
}

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.cpp
void WidgetProvider::OnActionInvoked(winrt::WidgetActionInvokedArgs actionInvokedArgs)
{
    auto verb = actionInvokedArgs.Verb();
    if (verb == L"inc")
    {
        auto 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:
        auto data = actionInvokedArgs.Data();
        if (const auto iter = RunningWidgets.find(widgetId); iter != RunningWidgets.end())
        {
            auto& localWidgetInfo = iter->second;
            // Increment the count
            localWidgetInfo.customState++;
            UpdateWidget(localWidgetInfo);
        }
    }
}

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

OnWidgetContextChanged

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

// WidgetProvider.cpp
void WidgetProvider::OnWidgetContextChanged(winrt::WidgetContextChangedArgs contextChangedArgs)
{
    auto widgetContext = contextChangedArgs.WidgetContext();
    auto widgetId = widgetContext.Id();
    auto widgetSize = widgetContext.Size();
    if (const auto iter = RunningWidgets.find(widgetId); iter != RunningWidgets.end())
    {
        auto localWidgetInfo = iter->second;

        UpdateWidget(localWidgetInfo);
    }
}
    

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

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

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

void WidgetProvider::Activate(winrt::Microsoft::Windows::Widgets::Providers::WidgetContext widgetContext)
{
    auto widgetId = widgetContext.Id();

    if (const auto iter = RunningWidgets.find(widgetId); iter != RunningWidgets.end())
    {
        auto& localWidgetInfo = iter->second;
        localWidgetInfo.isActive = true;

        UpdateWidget(localWidgetInfo);
    }
}

void WidgetProvider::Deactivate(winrt::hstring widgetId)
{
    if (const auto iter = RunningWidgets.find(widgetId); iter != RunningWidgets.end())
    {
        auto& localWidgetInfo = iter->second;
        localWidgetInfo.isActive = false;
    }
}

ウィジェットを更新する

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

// WidgetProvider.cpp
void WidgetProvider::UpdateWidget(CompactWidgetInfo const& localWidgetInfo)
{
    winrt::WidgetUpdateRequestOptions updateOptions{ localWidgetInfo.widgetId };

    winrt::hstring templateJson;
    if (localWidgetInfo.widgetName == L"Weather_Widget")
    {
        templateJson = winrt::to_hstring(weatherWidgetTemplate);
    }
    else if (localWidgetInfo.widgetName == L"Counting_Widget")
    {
        templateJson = winrt::to_hstring(countWidgetTemplate);
    }

    winrt::hstring dataJson;
    if (localWidgetInfo.widgetName == L"Weather_Widget")
    {
        dataJson = L"{}";
    }
    else if (localWidgetInfo.widgetName == L"Counting_Widget")
    {
        dataJson = L"{ \"count\": " + winrt::to_hstring(localWidgetInfo.customState) + L" }";
    }

    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(winrt::to_hstring(localWidgetInfo.customState));
    winrt::WidgetManager::GetDefault().UpdateWidget(updateOptions);
}

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

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

// WidgetProvider.cpp
WidgetProvider::WidgetProvider()
{
    auto runningWidgets = winrt::WidgetManager::GetDefault().GetWidgetInfos();
    for (auto widgetInfo : runningWidgets )
    {
        auto widgetContext = widgetInfo.WidgetContext();
        auto widgetId = widgetContext.Id();
        auto widgetName = widgetContext.DefinitionId();
        auto customState = widgetInfo.CustomState();
        if (RunningWidgets.find(widgetId) == RunningWidgets.end())
        {
            CompactWidgetInfo runningWidgetInfo{ widgetId, widgetName };
            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 = std::stoi(winrt::to_string(customState));
                runningWidgetInfo.customState = count;
            }
            catch (...)
            {

            }
            RunningWidgets[widgetId] = runningWidgetInfo;
        }
    }
}

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

アプリの main.cpp ファイルの先頭にある include に、WidgetProvider クラスを定義するヘッダーを追加します。 ここで mutex もインクルードします。

// main.cpp
...
#include "WidgetProvider.h"
#include <mutex>

アプリの終了をトリガーするイベントと、イベントを設定する SignalLocalServerShutdown 関数を宣言します。 main.cpp に次のコードを貼り付けます。

// main.cpp
wil::unique_event g_shudownEvent(wil::EventOptions::None);

void SignalLocalServerShutdown()
{
    g_shudownEvent.SetEvent();
}

次に、COM のアクティブ化でウィジェット プロバイダーを識別するために使われる CLSID を作成する必要があります。 Visual Studio で [ツール] > [GUID の作成] に移動して、GUID を生成します。 オプション "static const GUID =" を選んで [コピー] をクリックし、それを main.cpp に貼り付けます。 次の C++/WinRT 構文を使って GUID の定義を更新し、GUID の変数名を widget_provider_clsid に設定します。 コメントになっている GUID のバージョンは、後でアプリをパッケージ化するときにこの形式が必要になるため、そのままにします。

// main.cpp
...
// {80F4CB41-5758-4493-9180-4FB8D480E3F5}
static constexpr GUID widget_provider_clsid
{
    0x80f4cb41, 0x5758, 0x4493, { 0x91, 0x80, 0x4f, 0xb8, 0xd4, 0x80, 0xe3, 0xf5 }
};

次のクラス ファクトリの定義を main.cpp に追加します。 これは主に、ウィジェット プロバイダーの実装に固有ではない定型コードです。 アプリが終了する前、CoWaitForMultipleObjects はシャットダウン イベントがトリガーされるのを待機することに注意してください。

// main.cpp
template <typename T>
struct SingletonClassFactory : winrt::implements<SingletonClassFactory<T>, IClassFactory>
{
    STDMETHODIMP CreateInstance(
        ::IUnknown* outer,
        GUID const& iid,
        void** result) noexcept final
    {
        *result = nullptr;

        std::unique_lock lock(mutex);

        if (outer)
        {
            return CLASS_E_NOAGGREGATION;
        }

        if (!instance)
        {
            instance = winrt::make<WidgetProvider>();
        }

        return instance.as(iid, result);
    }

    STDMETHODIMP LockServer(BOOL) noexcept final
    {
        return S_OK;
    }

private:
    T instance{ nullptr };
    std::mutex mutex;
};

int main()
{
    winrt::init_apartment();
    wil::unique_com_class_object_cookie widgetProviderFactory;
    auto factory = winrt::make<SingletonClassFactory<winrt::Microsoft::Windows::Widgets::Providers::IWidgetProvider>>();

    winrt::check_hresult(CoRegisterClassObject(
        widget_provider_clsid,
        factory.get(),
        CLSCTX_LOCAL_SERVER,
        REGCLS_MULTIPLEUSE,
        widgetProviderFactory.put()));

    DWORD index{};
    HANDLE events[] = { g_shudownEvent.get() };
    winrt::check_hresult(CoWaitForMultipleObjects(CWMO_DISPATCH_CALLS | CWMO_DISPATCH_WINDOW_MESSAGES,
        INFINITE,
        static_cast<ULONG>(std::size(events)), events, &index));

    return 0;
}

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

現在のリリースでは、ウィジェット プロバイダーとして登録できるのはパッケージ化されたアプリのみです。 次の手順では、アプリをウィジェット プロバイダーとして 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 アプリに変換するには:

  1. ソリューション エクスプローラーで ExampleWidgetProvider プロジェクトを右クリックして、[プロパティ] を選びます。 [リンカー] > [システム] に移動し、[サブシステム] を [コンソール] から [Windows] に変更します。 これは、<SubSystem>Windows</SubSystem> を .vcxproj の <Link>..</Link> セクションに追加することで行うこともできます。
  2. main.cpp で、int main()int WINAPI wWinMain(_In_ HINSTANCE /*hInstance*/, _In_opt_ HINSTANCE /*hPrevInstance*/, _In_ PWSTR pCmdLine, _In_ int /*nCmdShow*/) に変更します。

出力の種類が Windows アプリケーションに設定されている、C++ ウィジェット プロバイダー プロジェクトのプロパティを示すスクリーンショット

ウィジェットを発行する

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

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

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

Windows ウィジェットを備えたアプリをユーザーが見つけられるウィジェット コレクションを示す Microsoft Store のスクリーンショット。

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

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

カスタマイズ ダイアログが表示されたウィジェットを示すスクリーンショット。

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

  1. 通常の操作では、ウィジェット プロバイダーは、通常のウィジェット エクスペリエンス用のビジュアルおよびデータ JSON テンプレートを使用して、ウィジェット ホストからの要求に応答します。
  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">
...

WidgetProvider.h を更新する

この記事の前の手順で作成したウィジェットにカスタマイズのサポートを追加するには、ウィジェット プロバイダー (WidgetProvider.h) のヘッダー ファイルを更新する必要があります。

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

// WidgetProvider.h
struct CompactWidgetInfo
{
    winrt::hstring widgetId;
    winrt::hstring widgetName;
    int customState = 0;
    bool isActive = false;
    bool inCustomization = false;
};

IWidgetProvider2 インターフェイスを実装するように WidgetProvider 宣言を更新します。

// WidgetProvider.h

struct WidgetProvider : winrt::implements<WidgetProvider, winrt::Microsoft::Windows::Widgets::Providers::IWidgetProvider, winrt::Microsoft::Windows::Widgets::Providers::IWidgetProvider2>

IWidgetProvider2 インターフェイスの OnCustomizationRequested コールバックの宣言を追加します。

// WidgetProvider.h

void OnCustomizationRequested(winrt::Microsoft::Windows::Widgets::Providers::WidgetCustomizationRequestedArgs args);

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

// WidgetProvider.h
const std::string countWidgetCustomizationTemplate = R"(
{
    "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"
})";

WidgetProvider.cpp を更新する

今すぐ WidgetProvider.cpp ファイルを更新して、ウィジェットのカスタマイズ動作を実装します。 このメソッドは、使用した他のコールバックと同じパターンを使用します。 WidgetContext からカスタマイズされるウィジェットの ID を取得し、そのウィジェットに関連付けられている CompactWidgetInfo ヘルパー構造体を検索し、inCustomization フィールドを true に設定します。

//WidgetProvider.cpp
void WidgetProvider::OnCustomizationRequested(winrt::WidgetCustomizationRequestedArgs args)
{
    auto widgetId = args.WidgetContext().Id();

    if (const auto iter = RunningWidgets.find(widgetId); iter != RunningWidgets.end())
    {
        auto& localWidgetInfo = iter->second;
        localWidgetInfo.inCustomization = true;

        UpdateWidget(localWidgetInfo);
    }
}

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

//WidgetProvider.cpp
void WidgetProvider::UpdateWidget(CompactWidgetInfo const& localWidgetInfo)
{
    ...
    else if (localWidgetInfo.widgetName == L"Counting_Widget")
    {
        if (!localWidgetInfo.inCustomization)
        {
            std::wcout << L" - not in customization " << std::endl;
            templateJson = winrt::to_hstring(countWidgetTemplate);
		}
        else
        {
            std::wcout << L" - in customization " << std::endl;
            templateJson = winrt::to_hstring(countWidgetCustomizationTemplate);
		}
    }
    ...
    
    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(winrt::to_hstring(localWidgetInfo.customState));
    winrt::WidgetManager::GetDefault().UpdateWidget(updateOptions);
}

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

//WidgetProvider.cpp
void WidgetProvider::OnActionInvoked(winrt::WidgetActionInvokedArgs actionInvokedArgs)
{
    auto verb = actionInvokedArgs.Verb();
    if (verb == L"inc")
    {
        auto 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:
        auto data = actionInvokedArgs.Data();
        if (const auto iter = RunningWidgets.find(widgetId); iter != RunningWidgets.end())
        {
            auto& localWidgetInfo = iter->second;
            // Increment the count
            localWidgetInfo.customState++;
            UpdateWidget(localWidgetInfo);
        }
    }
    else if (verb == L"reset") 
    {
        auto widgetId = actionInvokedArgs.WidgetContext().Id();
        auto data = actionInvokedArgs.Data();
        if (const auto iter = RunningWidgets.find(widgetId); iter != RunningWidgets.end())
        {
            auto& localWidgetInfo = iter->second;
            // Reset the count
            localWidgetInfo.customState = 0;
            localWidgetInfo.inCustomization = false;
            UpdateWidget(localWidgetInfo);
        }
    }
    else if (verb == L"exitCustomization")
    {
        auto 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:
        auto data = actionInvokedArgs.Data();
        if (const auto iter = RunningWidgets.find(widgetId); iter != RunningWidgets.end())
        {
            auto& localWidgetInfo = iter->second;
            // Stop sending the customization template
            localWidgetInfo.inCustomization = false;
            UpdateWidget(localWidgetInfo);
        }
    }
}

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

ウィジェットのカスタマイズ UI を示すスクリーンショット。

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