win32 앱에서 위젯 공급자 구현(C++/WinRT)
이 문서에서는 IWidgetProvider 인터페이스를 구현하는 간단한 위젯 공급자를 만드는 방법에 대해 설명합니다. 이 인터페이스의 메서드는 위젯 호스트에서 위젯을 정의하는 데이터를 요청하거나 위젯 공급자가 위젯에 대한 사용자 작업에 응답하도록 하기 위해 호출됩니다. 위젯 공급자는 단일 위젯 또는 여러 위젯을 지원할 수 있습니다. 이 예제에서는 두 개의 서로 다른 위젯을 정의합니다. 한 위젯은 적응형 카드 프레임워크에서 제공하는 몇 가지 서식 옵션을 보여 주는 모의 날씨 위젯입니다. 두 번째 위젯은 위젯에 표시되는 단추를 클릭할 때마다 증가되는 카운터를 기본 사용자 작업 및 사용자 지정 위젯 상태 기능을 보여 줍니다.
이 문서의 이 샘플 코드는 Windows 앱 SDK 위젯 샘플에서 적용됩니다. C#을 사용하여 위젯 공급자를 구현하려면 win32 앱에서 위젯 공급자 구현(C#)을 참조하세요.
필수 조건
- 디바이스에 개발자 모드가 설정되어 있어야 합니다. 자세한 내용은 개발에 디바이스 사용을 참조하세요.
- Visual Studio 2022 이상(유니버설 Windows 플랫폼 개발 워크로드 포함). 선택적 드롭다운에서 C++(v143)에 대한 구성 요소를 추가해야 합니다.
새 C++/WinRT win32 콘솔 앱을 만듭니다.
Visual Studio에서 새 프로젝트를 만듭니다. 새 프로젝트 만들기 대화 상자에서 언어 필터를 "C++"로 설정하고 플랫폼 필터를 Windows로 설정한 다음, Windows 콘솔 애플리케이션(C++/WinRT) 프로젝트 템플릿을 선택합니다. 새 프로젝트의 이름을 "ExampleWidgetProvider"로 지정합니다. 메시지가 표시되면 앱의 대상 Windows 버전을 1809 이상으로 설정합니다.
Windows 앱 SDK 및 Windows 구현 라이브러리 NuGet 패키지에 대한 참조 추가
이 샘플에서는 안정적인 최신 Windows 앱 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>
참고 항목
WinRT 헤더 앞에 wil/cppwinrt.h 헤더를 먼저 포함해야 합니다.
위젯 공급자 앱 종료를 올바르게 처리하려면 winrt::get_module_lock 사용자 지정 구현이 필요합니다. 기본.cpp 파일에 정의되고 앱을 종료하도록 알리는 이벤트를 설정하는 SignalLocalServerShutdown 메서드를 미리 선언합니다. #pragma once
지시문 바로 아래의 pch.h 파일에 다음 코드를 추가합니다.
//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 구조체 선언 위에 다음 정의를 WidgetProvider.h 파일에 추가합니다.
// WidgetProvider.h
struct CompactWidgetInfo
{
winrt::hstring widgetId;
winrt::hstring widgetName;
int customState = 0;
bool isActive = false;
};
WidgetProvider.h의 WidgetProvider 선언 내에 위젯 ID를 각 항목의 키로 사용하여 사용하도록 설정된 위젯 목록을 기본 가져올 맵에 대한 멤버를 추가합니다.
// WidgetProvider.h
#include <unordered_map>
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은 이 문서의 뒷부분에 나와 있습니다. 인터페이스 메서드로 전환하기 전에 다음 줄을 include 지시문 다음 WidgetProvider.cpp
에 추가하여 위젯 공급자 API를 winrt 네임스페이스로 끌어오고 이전 단계에서 선언한 맵에 대한 액세스를 허용합니다.
참고 항목
IWidgetProvider 인터페이스의 콜백 메서드에 전달된 개체는 콜백 내에서만 유효합니다. 콜백 컨텍스트 외부의 동작이 정의되지 않았으므로 이러한 개체에 대한 참조를 저장해서는 안 됩니다.
// WidgetProvider.cpp
namespace winrt
{
using namespace Microsoft::Windows::Widgets::Providers;
}
std::unordered_map<winrt::hstring, CompactWidgetInfo> WidgetProvider::RunningWidgets{};
CreateWidget
위젯 호스트는 사용자가 위젯 호스트에 앱의 위젯 중 하나를 고정한 경우 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
위젯 호스트는 사용자가 위젯 호스트에서 앱의 위젯 중 하나를 고정 해제한 경우 DeleteWidget을 호출합니다. 이 경우 해당 위젯에 대한 추가 업데이트를 보내지 않도록 활성화된 위젯 목록에서 연결된 위젯을 제거합니다.
// WidgetProvider.cpp
void WidgetProvider::DeleteWidget(winrt::hstring const& widgetId, winrt::hstring const& customState)
{
RunningWidgets.erase(widgetId);
}
OnActionInvoked
위젯 호스트는 사용자가 위젯 템플릿에서 정의한 작업과 상호 작용할 때 OnActionInvoked를 호출합니다. 이 예제에 사용된 카운터 위젯의 경우 위젯에 대한 JSON 템플릿에서 동사 값이 "inc"인 동작이 선언되었습니다. 위젯 공급자 코드는 이 동사 값을 사용하여 사용자 상호 작용에 대한 응답으로 수행할 작업을 결정합니다.
...
"actions": [
{
"type": "Action.Execute",
"title": "Increment",
"verb": "inc"
}
],
...
OnActionInvoked 메서드에서 메서드에 전달된 WidgetActionInvokedArgs의 동사 속성을 검사하여 동사 값을 가져옵니다. 동사가 "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);
}
}
활성화 및 비활성화
활성화 메서드는 위젯 공급자에게 위젯 호스트가 현재 공급자로부터 업데이트된 콘텐츠를 받는 데 관심이 있음을 알리기 위해 호출됩니다. 예를 들어 사용자가 현재 위젯 호스트를 적극적으로 보고 있음을 의미할 수 있습니다. 위젯 호스트가 더 이상 콘텐츠 업데이트를 요청하지 않는다는 것을 위젯 공급자에게 알리기 위해 비활성화 메서드가 호출됩니다. 이 두 메서드는 위젯 호스트가 최신 콘텐츠를 표시하는 데 가장 관심이 있는 창을 정의합니다. 위젯 공급자는 푸시 알림에 대한 응답과 같이 언제든지 위젯에 업데이트를 보낼 수 있지만, 백그라운드 작업과 마찬가지로 배터리 수명과 같은 리소스 문제와 최신 콘텐츠 제공의 균형을 맞추는 것이 중요합니다.
활성화 및 비활성화는 위젯별로 호출됩니다. 이 예제에서는 CompactWidgetInfo 도우미 구조체에서 각 위젯의 활성 상태 추적합니다. 활성화 메서드에서는 UpdateWidget 도우미 메서드를 호출하여 위젯을 업데이트합니다. 활성화와 비활성화 사이의 기간은 작을 수 있으므로 위젯 업데이트 코드 경로를 최대한 빨리 만드는 것이 좋습니다.
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를 호출하여 앱의 기본 위젯 관리자 인스턴스를 가져옵니다. 그런 다음, WidgetInfo 개체의 배열을 반환하는 GetWidgetInfos를 호출합니다. 위젯 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를 인스턴스화하는 클래스 팩터리 등록
WidgetProvider 클래스를 정의하는 헤더를 앱의 main.cpp
파일의 맨 위에 있는 포함 항목에 추가합니다. 우리는 또한 여기에 뮤텍스를 포함할 것입니다.
// 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를 만들어야 합니다. 도구 >만들기 GUID로 이동하여 Visual Studio에서 GUID를 생성합니다. "static const GUID =" 옵션을 선택하고 복사를 클릭한 다음 main.cpp
에 붙여넣습니다. GUID 정의를 다음 C++/WinRT 구문으로 업데이트하고 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 이상으로 설정하고 확인을 클릭합니다. 그런 다음, 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.appxmanifest
의 Path=ProviderAssets\ 다음에 오는 것과 일치하거나 위젯이 위젯 호스트에 표시되지 않는지 확인합니다.
스크린샷 이미지의 디자인 요구 사항 및 지역화된 스크린샷의 명명 규칙에 대한 자세한 내용은 위젯 선택기와 통합을 참조하세요.
위젯 공급자 테스트
솔루션 플랫폼 드롭다운에서 개발 머신과 일치하는 아키텍처(예: "x64")를 선택했는지 확인합니다. 솔루션 탐색기에서 솔루션을 마우스 오른쪽 단추로 클릭하고 솔루션 빌드를 선택합니다. 이 작업이 완료되면 ExampleWidgetProviderPackage를 마우스 오른쪽 단추로 클릭하고 배포를 선택합니다. 현재 릴리스에서 유일하게 지원되는 위젯 호스트는 위젯 보드입니다. 위젯을 보려면 위젯 보드를 열고 오른쪽 위에서 위젯 추가를 선택해야 합니다. 사용 가능한 위젯의 아래쪽으로 스크롤하면 이 자습서에서 만든 모의 날씨 위젯 및 Microsoft 카운팅 위젯이 표시됩니다. 위젯을 클릭하여 위젯 보드에 고정하고 해당 기능을 테스트합니다.
위젯 공급자 디버깅
위젯을 고정한 후 위젯 플랫폼은 위젯에 대한 관련 정보를 수신하고 보내기 위해 위젯 공급자 애플리케이션을 시작합니다. 실행 중인 위젯을 디버그하려면 실행 중인 위젯 공급자 애플리케이션에 디버거를 연결하거나 시작되면 위젯 공급자 프로세스 디버깅을 자동으로 시작하도록 Visual Studio를 설정할 수 있습니다.
실행 중인 프로세스에 연결하려면 다음을 수행합니다.
- Visual Studio에서 Debug ->프로세스에 연결을 클릭합니다.
- 프로세스를 필터링하고 원하는 위젯 공급자 애플리케이션을 찾습니다.
- 디버거를 연결합니다.
디버거가 처음 시작될 때 프로세스에 자동으로 연결하려면 다음을 수행합니다.
- Visual Studio에서 디버그->기타 디버그 대상->설치된 앱 패키지 디버그를 차례로 선택합니다.
- 패키지를 필터링하고 원하는 위젯 공급자 패키지를 찾습니다.
- 이 옵션을 선택하고 시작 안 함을 나타내는 상자를 검사 시작 시 코드를 디버그합니다.
- 연결을 클릭합니다.
콘솔 앱을 Windows 앱으로 변환
이 연습에서 만든 콘솔 앱을 Windows 앱으로 변환하려면 다음을 수행합니다.
- 솔루션 탐색기에서 ExampleWidgetProvider 프로젝트를 마우스 오른쪽 단추로 클릭하고 속성을 선택합니다. 링커 - >시스템으로 이동하고 하위 시스템을 "콘솔"에서 "Windows"로 변경합니다. 이 작업은 링크에 <하위 시스템>Windows/<하위 시스템>을 .vcxproj의 <링크>..</링크 >섹션에 추가하여 수행할 수도 있습니다.
- 기본.cpp에서
int main()
을int WINAPI wWinMain(_In_ HINSTANCE /*hInstance*/, _In_opt_ HINSTANCE /*hPrevInstance*/, _In_ PWSTR pCmdLine, _In_ int /*nCmdShow*/)
로 바꾸세요.
위젯 게시
위젯 공급자를 개발하고 테스트한 후에는 사용자가 자신의 장치에 위젯을 설치하려면 Microsoft Store에 앱을 게시할 수 있습니다. 앱을 게시하기 위한 단계별 지침은 Microsoft Store에서 앱 게시를 참조하세요.
위젯 스토어 컬렉션
앱이 Microsoft Store에 게시된 후 사용자가 Windows 위젯을 사용하는 앱을 검색하는 데 도움이 되는 위젯 스토어 컬렉션에 앱을 포함하도록 요청할 수 있습니다. 요청을 제출하려면 Microsoft Store 컬렉션에 추가하기 위한 위젯 정보 제출을 참조하세요.
위젯 사용자 지정 구현
Windows 앱 SDK 1.4부터 위젯은 사용자 사용자 지정을 지원할 수 있습니다. 이 기능을 구현하면 위젯 사용자 지정 옵션이 고정 해제 위젯 옵션 위의 줄임표 메뉴에 추가됩니다.
다음 단계에서는 위젯 사용자 지정 프로세스를 요약합니다.
- 일반적인 작업에서 위젯 공급자는 일반 위젯 환경에 대한 시각적 개체 및 JSON 템플릿을 사용하여 위젯 호스트의 요청에 응답합니다.
- 사용자가 줄임표 메뉴에서 위젯 사용자 지정 단추를 클릭합니다.
- 위젯은 위젯 공급자에서 OnCustomizationRequested 이벤트를 발생시켜 사용자가 위젯 사용자 지정 환경을 요청했음을 나타냅니다.
- 위젯 공급자는 위젯이 사용자 지정 모드임을 나타내기 위해 내부 플래그를 설정합니다. 사용자 지정 모드에서 위젯 공급자는 일반 위젯 UI 대신 위젯 사용자 지정 UI에 대한 JSON 템플릿을 보냅니다.
- 사용자 지정 모드에서 위젯 공급자는 사용자가 사용자 지정 UI와 상호 작용하고 사용자의 작업에 따라 내부 구성 및 동작을 조정할 때 OnActionInvoked 이벤트를 받습니다.
- OnActionInvoked 이벤트와 연결된 작업이 앱에서 정의한 "종료 사용자 지정" 작업인 경우 위젯 공급자는 더 이상 사용자 지정 모드에 있지 않음을 나타내기 위해 내부 플래그를 다시 설정하며 사용자 지정 중에 요청된 변경 내용을 반영하여 일반 위젯 환경에 대한 시각적 개체 및 데이터 JSON 템플릿 전송을 다시 시작합니다.
- 위젯 공급자는 위젯 공급자의 호출 간에 변경 내용이 유지되도록 디스크 또는 클라우드에 대한 사용자 지정 옵션을 유지합니다.
참고 항목
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">
...
WidgetProvider.h 업데이트
이 문서의 이전 단계에서 만든 위젯에 사용자 지정 지원을 추가하려면 위젯 공급자인 WidgetProvider.h에 대한 헤더 파일을 업데이트해야 합니다.
먼저 CompactWidgetInfo 정의를 업데이트합니다. 이 도우미 구조체는 활성 위젯의 현재 상태를 추적하는 데 도움이 됩니다. 위젯 호스트가 일반 위젯 템플릿이 아닌 사용자 지정 json 템플릿을 보낼 것으로 예상되는 시기를 추적하는 데 사용되는 inCustomization 필드를 추가합니다.
// WidgetProvider.h
struct CompactWidgetInfo
{
winrt::hstring widgetId;
winrt::hstring widgetName;
int customState = 0;
bool isActive = false;
bool inCustomization = false;
};
WidgetProvider 선언을 업데이트하여 IWidgetProvider2 인터페이스를 구현합니다.
// 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"
})";
Update 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);
}
}
}
이제 위젯을 배포할 때 줄임표 메뉴에 위젯 사용자 지정 단추가 표시됩니다. 사용자 지정 단추를 클릭하면 사용자 지정 템플릿이 표시됩니다.
카운터 재설정 단추를 클릭하여 카운터를 0으로 다시 설정합니다. 사용자 지정 종료 단추를 클릭하여 위젯의 일반 동작으로 돌아갑니다.
Windows developer