다음을 통해 공유


C# Windows 앱에서 위젯 공급자 구현

이 문서에서는 IWidgetProvider 인터페이스를 구현하는 간단한 위젯 공급자를 만드는 방법에 대해 설명합니다. 이 인터페이스의 메서드는 위젯 호스트에서 위젯을 정의하는 데이터를 요청하거나 위젯 공급자가 위젯에 대한 사용자 작업에 응답하도록 하기 위해 호출됩니다. 위젯 공급자는 단일 위젯 또는 여러 위젯을 지원할 수 있습니다. 이 예제에서는 두 개의 서로 다른 위젯을 정의합니다. 한 위젯은 적응형 카드 프레임워크에서 제공하는 몇 가지 서식 옵션을 보여 주는 모의 날씨 위젯입니다. 두 번째 위젯은 위젯에 표시되는 단추를 클릭할 때마다 증가되는 카운터를 기본 사용자 작업 및 사용자 지정 위젯 상태 기능을 보여 줍니다.

간단한 날씨 위젯의 스크린샷. 위젯에는 일부 날씨 관련 그래픽 데이터와 중간 크기 위젯에 대한 템플릿이 표시되고 있음을 보여주는 일부 진단 텍스트가 표시됩니다.

간단한 계산 위젯의 스크린샷. 위젯에는 증분할 숫자 값과 증분 레이블이 지정된 단추가 포함된 문자열과 작은 크기 위젯에 대한 템플릿이 표시되고 있음을 보여주는 일부 진단 텍스트가 표시됩니다.

이 문서의 이 샘플 코드는 Windows 앱 SDK 위젯 샘플에서 적용됩니다. C++/WinRT를 사용하여 위젯 공급자를 구현하려면 win32 앱에서 위젯 공급자 구현(C++/WinRT)을 참조하세요.

필수 조건

  • 디바이스에 개발자 모드가 설정되어 있어야 합니다. 자세한 내용은 개발에 디바이스 사용을 참조하세요.
  • Visual Studio 2022 이상(유니버설 Windows 플랫폼 개발 워크로드 포함). 선택적 드롭다운에서 C++(v143)에 대한 구성 요소를 추가해야 합니다.

새 C# 콘솔 앱을 만듭니다.

Visual Studio에서 새 프로젝트를 만듭니다. 새 프로젝트 만들기 대화 상자에서 언어 필터를 "C#"으로 설정하고 플랫폼 필터를 Windows로 설정한 다음 콘솔 앱 프로젝트 템플릿을 선택합니다. 새 프로젝트의 이름을 "ExampleWidgetProvider"로 지정합니다. 메시지가 표시되면 대상 .NET 버전을 8.0으로 설정합니다.

프로젝트가 로드되면 솔루션 탐색기에서 프로젝트 이름을 마우스 오른쪽 단추로 클릭하고 속성을 선택합니다. 일반 페이지에서 대상 OS까지 아래로 스크롤하고 "Windows"를 선택합니다. 대상 OS 버전에서 버전 10.0.19041.0 이상을 선택합니다.

.NET 8.0을 지원하도록 프로젝트를 업데이트하려면 솔루션 탐색기 프로젝트 이름을 마우스 오른쪽 단추로 클릭하고 프로젝트 파일 편집을 선택합니다. PropertyGroup 내에서 다음 RuntimeIdentifiers 요소를 추가합니다.

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

이 연습에서는 위젯이 활성화될 때 콘솔 창을 표시하는 콘솔 앱을 사용하여 쉽게 디버깅할 수 있습니다. 위젯 공급자 앱을 게시할 준비가 되면 콘솔 앱을 Windows 앱으로 변환의 단계에 따라 콘솔 애플리케이션을 Windows 애플리케이션으로 변환할 수 있습니다.

Windows 앱 SDK에 대한 참조 추가

이 샘플에서는 안정적인 최신 Windows 앱 SDK NuGet 패키지를 사용합니다. 솔루션 탐색기에서 종속성을 마우스 오른쪽 단추로 클릭하고 NuGet 패키지 관리를 선택합니다. NuGet 패키지 관리자에서 찾아보기 탭을 선택하고 "Microsoft.WindowsAppSDK"를 검색합니다. 버전 드롭다운에서 안정적인 최신 버전을 선택한 다음 설치를 클릭합니다.

위젯 작업을 처리하는 WidgetProvider 클래스 추가

Visual Studio에서 솔루션 탐색기ExampleWidgetProvider프로젝트를 마우스 오른쪽 단추로 클릭하고 클래스 추가>를 선택합니다. 클래스 추가 대화 상자에서 클래스 이름을 "WidgetProvider"로 지정하고 추가를 클릭합니다. 생성된 WidgetProvider.cs 파일에서 클래스 정의를 업데이트하여 IWidgetProvider 인터페이스를 구현함을 나타냅니다.

// WidgetProvider.cs
internal class WidgetProvider : IWidgetProvider

사용 가능한 위젯 추적 준비

위젯 공급자는 단일 위젯 또는 여러 위젯을 지원할 수 있습니다. 위젯 호스트가 위젯 공급자를 사용하여 작업을 시작할 때마다 ID를 전달하여 작업과 연결된 위젯을 식별합니다. 각 위젯에는 사용자 지정 데이터를 저장하는 데 사용할 수 있는 연결된 이름 및 상태 값도 있습니다. 이 예제에서는 고정된 각 위젯의 ID, 이름 및 데이터를 저장하는 간단한 도우미 구조를 선언합니다. 위젯은 활성 상태일 수도 있습니다. 이 상태는 아래의 활성화 및 비활성화 섹션에서 설명하며 부울 값이 있는 각 위젯에 대해 이 상태를 추적합니다. ExampleWidgetProvider 네임스페이스 내부와 WidgetProvider 클래스 정의 외부의 WidgetProvider.cs 파일에 다음 정의를 추가합니다.

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

위젯 호스트는 사용자가 위젯 호스트에 앱의 위젯 중 하나를 고정한 경우 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

위젯 호스트는 사용자가 위젯 호스트에서 앱의 위젯 중 하나를 고정 해제한 경우 DeleteWidget을 호출합니다. 이 경우 해당 위젯에 대한 추가 업데이트를 보내지 않도록 활성화된 위젯 목록에서 연결된 위젯을 제거합니다.

// WidgetProvider.cs

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

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

이 예제에서는 사용 가능한 위젯 목록에서 지정된 위젯을 제거하는 것 외에도 목록이 비어 있는지 확인할 검사 있으며, 이 경우 활성화된 위젯이 없을 때 앱이 종료되도록 나중에 사용할 이벤트를 설정합니다. 클래스 정의 내에 ManualResetEvent 선언과 public 접근자 함수를 추가합니다.

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

public static ManualResetEvent GetEmptyWidgetListEvent()
{
    return emptyWidgetListEvent;
}

OnActionInvoked

위젯 호스트는 사용자가 위젯 템플릿에서 정의한 작업과 상호 작용할 때 OnActionInvoked를 호출합니다. 이 예제에 사용된 카운터 위젯의 경우 위젯에 대한 JSON 템플릿에서 동사 값이 "inc"인 동작이 선언되었습니다. 위젯 공급자 코드는 이 동사 값을 사용하여 사용자 상호 작용에 대한 응답으로 수행할 작업을 결정합니다.

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

OnActionInvoked 메서드에서 메서드에 전달된 WidgetActionInvokedArgs동사 속성을 검사하여 동사 값을 가져옵니다. 동사가 "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);
    }
}
    

활성화 및 비활성화

활성화 메서드는 위젯 공급자에게 위젯 호스트가 현재 공급자로부터 업데이트된 콘텐츠를 받는 데 관심이 있음을 알리기 위해 호출됩니다. 예를 들어 사용자가 현재 위젯 호스트를 적극적으로 보고 있음을 의미할 수 있습니다. 위젯 호스트가 더 이상 콘텐츠 업데이트를 요청하지 않는다는 것을 위젯 공급자에게 알리기 위해 비활성화 메서드가 호출됩니다. 이 두 메서드는 위젯 호스트가 최신 콘텐츠를 표시하는 데 가장 관심이 있는 창을 정의합니다. 위젯 공급자는 푸시 알림에 대한 응답과 같이 언제든지 위젯에 업데이트를 보낼 수 있지만, 백그라운드 작업과 마찬가지로 배터리 수명과 같은 리소스 문제와 최신 콘텐츠 제공의 균형을 맞추는 것이 중요합니다.

활성화비활성화는 위젯별로 호출됩니다. 이 예제에서는 CompactWidgetInfo 도우미 구조체에서 각 위젯의 활성 상태 추적합니다. 활성화 메서드에서는 UpdateWidget 도우미 메서드를 호출하여 위젯을 업데이트합니다. 활성화비활성화 사이의 기간은 작을 수 있으므로 위젯 업데이트 코드 경로를 최대한 빨리 만드는 것이 좋습니다.

// 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를 호출하여 앱의 기본 위젯 관리자 인스턴스를 가져옵니다. 그런 다음, WidgetInfo 개체의 배열을 반환하는 GetWidgetInfos를 호출합니다. 위젯 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 인터페이스를 정의하고 인터페이스의 두가지 메서드를 실행하며, 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를 만들어야 합니다. 앱을 패키징할 때도 동일한 값이 사용됩니다. 도구 >만들기 GUID로 이동하여 Visual Studio에서 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 이상으로 설정하고 확인을 클릭합니다. 그런 다음, ExampleWidgetProviderPackage 프로젝트를 마우스 오른쪽 단추로 클릭하고 >프로젝트 참조 추가를 선택합니다. ExampleWidgetProvider 프로젝트를 선택하고 확인을 클릭합니다.

패키징 프로젝트에 Windows 앱 SDK 패키지 참조를 추가합니다.

MSIX 패키징 프로젝트에 Windows 앱 SDK nuget 패키지에 대한 참조를 추가해야 합니다. 솔루션 탐색기에서 ExampleWidgetProviderPackage 프로젝트를 두 번 클릭하여 ExampleWidgetProviderPackage.wapproj 파일을 엽니다. 프로젝트 요소 내에 다음 xml을 추가합니다.

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

참고 항목

PackageReference 요소에 지정된 버전이 이전 단계에서 참조한 안정적인 최신 버전과 일치하는지 확인합니다.

올바른 버전의 Windows 앱 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.appmanifest -->
<Package
  ...
  xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
  xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"

애플리케이션 요소 내에서 Extensions라는 새 빈 요소를 만듭니다. uap:VisualElements에 대한 닫는 태그 후에 오는지 확인합니다.

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

    </Extensions>
</Application>

추가해야 하는 첫 번째 확장은 ComServer 확장입니다. 그러면 실행 파일의 진입점이 OS에 등록됩니다. 이 확장은 레지스트리 키를 설정하여 COM 서버를 등록하는 것과 동일한 패키지된 앱이며 위젯 공급자와 관련이 없습니다. 다음 com:Extension 요소를 확장 요소의 자식으로 추가합니다. 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 요소를 확장 요소의 자식으로 붙여넣습니다. 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를 마우스 오른쪽 단추로 클릭하고 추가->새 폴더를 선택합니다. 이전 단계의 Package.appxmanifest에서 사용한 대로 이 폴더의 이름을 ProviderAssets로 지정합니다. 여기서 위젯에 대한 아이콘스크린샷을 저장합니다. 원하는 아이콘과 스크린샷을 추가한 후에는 이미지 이름이 Package.appxmanifestPath=ProviderAssets\ 다음에 오는 것과 일치하거나 위젯이 위젯 호스트에 표시되지 않는지 확인합니다.

스크린샷 이미지의 디자인 요구 사항 및 지역화된 스크린샷의 명명 규칙에 대한 자세한 내용은 위젯 선택기와 통합을 참조하세요.

위젯 공급자 테스트

솔루션 플랫폼 드롭다운에서 개발 머신과 일치하는 아키텍처(예: "x64")를 선택했는지 확인합니다. 솔루션 탐색기에서 솔루션을 마우스 오른쪽 단추로 클릭하고 솔루션 빌드를 선택합니다. 이 작업이 완료되면 ExampleWidgetProviderPackage를 마우스 오른쪽 단추로 클릭하고 배포를 선택합니다. 현재 릴리스에서 유일하게 지원되는 위젯 호스트는 위젯 보드입니다. 위젯을 보려면 위젯 보드를 열고 오른쪽 위에서 위젯 추가를 선택해야 합니다. 사용 가능한 위젯의 아래쪽으로 스크롤하면 이 자습서에서 만든 모의 날씨 위젯Microsoft 카운팅 위젯이 표시됩니다. 위젯을 클릭하여 위젯 보드에 고정하고 해당 기능을 테스트합니다.

위젯 공급자 디버깅

위젯을 고정한 후 위젯 플랫폼은 위젯에 대한 관련 정보를 수신하고 보내기 위해 위젯 공급자 애플리케이션을 시작합니다. 실행 중인 위젯을 디버그하려면 실행 중인 위젯 공급자 애플리케이션에 디버거를 연결하거나 시작되면 위젯 공급자 프로세스 디버깅을 자동으로 시작하도록 Visual Studio를 설정할 수 있습니다.

실행 중인 프로세스에 연결하려면 다음을 수행합니다.

  1. Visual Studio에서 Debug ->프로세스에 연결을 클릭합니다.
  2. 프로세스를 필터링하고 원하는 위젯 공급자 애플리케이션을 찾습니다.
  3. 디버거를 연결합니다.

디버거가 처음 시작될 때 프로세스에 자동으로 연결하려면 다음을 수행합니다.

  1. Visual Studio에서 디버그->기타 디버그 대상->설치된 앱 패키지 디버그를 차례로 선택합니다.
  2. 패키지를 필터링하고 원하는 위젯 공급자 패키지를 찾습니다.
  3. 이 옵션을 선택하고 시작 안 함을 나타내는 상자를 검사 시작 시 코드를 디버그합니다.
  4. 연결을 클릭합니다.

콘솔 앱을 Windows 앱으로 변환

이 연습에서 만든 콘솔 앱을 Windows 앱으로 변환하려면 솔루션 탐색기ExampleWidgetProvider 프로젝트를 마우스 오른쪽 단추로 클릭하고 속성을 선택합니다. 애플리케이션 >일반에서 출력 형식을 "콘솔 애플리케이션"에서 "Windows 애플리케이션"으로 변경합니다.

출력 형식이 Windows 애플리케이션으로 설정된 C# 위젯 공급자 프로젝트 속성을 보여주는 스크린샷

위젯 게시

위젯 공급자를 개발하고 테스트한 후에는 사용자가 자신의 장치에 위젯을 설치하려면 Microsoft Store에 앱을 게시할 수 있습니다. 앱을 게시하기 위한 단계별 지침은 Microsoft Store에서 앱 게시를 참조하세요.

위젯 스토어 컬렉션

앱이 Microsoft Store에 게시된 후 사용자가 Windows 위젯을 사용하는 앱을 검색하는 데 도움이 되는 위젯 스토어 컬렉션에 앱을 포함하도록 요청할 수 있습니다. 요청을 제출하려면 Microsoft Store 컬렉션에 추가하기 위한 위젯 정보 제출을 참조하세요.

사용자가 Windows 위젯을 특징으로 하는 앱을 검색할 수 있는 위젯 컬렉션을 보여주는 Microsoft Store의 스크린샷.

위젯 사용자 지정 구현

Windows 앱 SDK 1.4부터 위젯은 사용자 사용자 지정을 지원할 수 있습니다. 이 기능을 구현하면 위젯 사용자 지정 옵션이 고정 해제 위젯 옵션 위의 줄임표 메뉴에 추가됩니다.

사용자 지정 대화 상자가 표시된 위젯을 보여주는 스크린샷.

다음 단계에서는 위젯 사용자 지정 프로세스를 요약합니다.

  1. 일반적인 작업에서 위젯 공급자는 일반 위젯 환경에 대한 템플릿 및 데이터 페이로드를 사용하여 위젯 호스트의 요청에 응답합니다.
  2. 사용자가 줄임표 메뉴에서 위젯 사용자 지정 단추를 클릭합니다.
  3. 위젯은 위젯 공급자에서 OnCustomizationRequested 이벤트를 발생시켜 사용자가 위젯 사용자 지정 환경을 요청했음을 나타냅니다.
  4. 위젯 공급자는 위젯이 사용자 지정 모드임을 나타내기 위해 내부 플래그를 설정합니다. 사용자 지정 모드에서 위젯 공급자는 일반 위젯 UI 대신 위젯 사용자 지정 UI에 대한 JSON 템플릿을 보냅니다.
  5. 사용자 지정 모드에서 위젯 공급자는 사용자가 사용자 지정 UI와 상호 작용하고 사용자의 작업에 따라 내부 구성 및 동작을 조정할 때 OnActionInvoked 이벤트를 받습니다.
  6. OnActionInvoked 이벤트와 연결된 작업이 앱에서 정의한 "종료 사용자 지정" 작업인 경우 위젯 공급자는 더 이상 사용자 지정 모드에 있지 않음을 나타내기 위해 내부 플래그를 다시 설정하며 사용자 지정 중에 요청된 변경 내용을 반영하여 일반 위젯 환경에 대한 시각적 개체 및 데이터 JSON 템플릿 전송을 다시 시작합니다.
  7. 위젯 공급자는 위젯 공급자의 호출 간에 변경 내용이 유지되도록 디스크 또는 클라우드에 대한 사용자 지정 옵션을 유지합니다.

참고 항목

Windows 앱 SDK 사용하여 빌드된 위젯의 경우 사용자 지정 카드 표시된 후 줄임표 메뉴가 응답하지 않는 Windows 위젯 보드의 알려진 버그가 있습니다.

일반적인 위젯 사용자 지정 시나리오에서 사용자는 위젯에 표시되는 데이터를 선택하거나 위젯의 시각적 프레젠테이션을 조정합니다. 간단히 하기 위해 이 섹션의 예제에서는 사용자가 이전 단계에서 구현된 계산 위젯의 카운터를 다시 설정할 수 있는 사용자 지정 동작을 추가합니다.

참고 항목

위젯 사용자 지정은 Windows 앱 SDK 1.4 이상에서만 지원됩니다. 프로젝트의 참조를 최신 버전의 Nuget 패키지로 업데이트해야 합니다.

사용자 지정 지원을 선언하도록 패키지 매니페스트 업데이트

위젯 호스트가 위젯에서 사용자 지정을 지원한다는 사실을 알리려면 위젯의 정의 요소에 IsCustomizable 특성을 추가하고 true로 설정합니다.

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

위젯이 사용자 지정 모드에 있는 경우 추적

이 문서의 예제에서는 도우미 구조체 CompactWidgetInfo를 사용하여 활성 위젯의 현재 상태를 추적합니다. 위젯 호스트가 일반 위젯 템플릿이 아닌 사용자 지정 json 템플릿을 보낼 것으로 예상되는 시기를 추적하는 데 사용되는 inCustomization 필드를 추가합니다.

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

이제 위젯을 배포할 때 줄임표 메뉴에 위젯 사용자 지정 단추가 표시됩니다. 사용자 지정 단추를 클릭하면 사용자 지정 템플릿이 표시됩니다.

위젯 사용자 지정 UI를 보여주는 스크린샷.

카운터 재설정 단추를 클릭하여 카운터를 0으로 다시 설정합니다. 사용자 지정 종료 단추를 클릭하여 위젯의 일반 동작으로 돌아갑니다.