Win32/C++ の既存デスクトップアプリに XAML Islands を組み込み、「Hello World from Xaml Islands!」を表示したのに画面が空白のまま——この症状の90%は DesktopWindowXamlSource の寿命管理ミスです。SDK を追加する前に、ウィンドウメッセージ駆動のライフサイクルと STA/レイアウトの基本だけで解決できます。この記事では原因の切り分けと最小修正、実運用で効く設計パターンまで具体的に解説します。
Win32 デスクトップアプリで XAML Islands のテキストが表示されない問題
質問の背景
C++/Win32 デスクトップアプリで DesktopWindowXamlSource を使い、XAML の TextBlock に「Hello World from Xaml Islands!」を表示させようとしたが、実行しても何も表示されない。
補助的な NuGet(Microsoft.Toolkit.Win32.UI.SDK など)を導入していないことが原因か知りたい。
結論(先に要点)
- 最大の原因は寿命(ライフサイクル)…
DesktopWindowXamlSourceを if ブロックなどのローカル変数で生成すると、ブロック終了時に破棄され、子 HWND と XAML ツリーも消えます。 - SDK の追加は必須ではない…
Microsoft.Toolkit.Win32.UI.SDKが無くても C++/WinRT だけで XAML Islands をホスト可能です。 - 正しい保持スコープ…
WindowsXamlManagerとDesktopWindowXamlSourceをウィンドウの寿命と同じスコープ(グローバル/メンバ/GWLP_USERDATA)で管理し、WM_CREATEで生成、WM_SIZEでレイアウト更新、WM_DESTROYで明示解放します。
原因と解決策(要約表)
| 主な原因 | 解決策 | 補足・ポイント |
|---|---|---|
DesktopWindowXamlSource の寿命が短すぎるif ブロック内のローカル変数として生成し、直後に破棄。 | DesktopWindowXamlSource と WindowsXamlManager をウィンドウの存続期間と同じスコープで保持する(グローバル、ウィンドウクラスのメンバ、または GWLP_USERDATA に格納)。 | 推奨フロー: 1) WM_CREATE で InitializeForCurrentThread+AttachToWindow2) WM_SIZE で MoveWindow/SetWindowPos3) WM_DESTROY で Close() |
| STA で初期化していない COM アパートメントが MTA のまま。 | winrt::init_apartment(winrt::apartment_type::single_threaded) を UI スレッドで一度だけ呼ぶ。 | XAML(WinUI)は STA 想定。二重初期化や別スレッドでの呼び出しは不具合の温床。 |
| 子 HWND の表示・サイズ更新をしていない | IDesktopWindowXamlSourceNative::get_WindowHandle で取得した子 HWND に対し、SWP_SHOWWINDOW を付けて SetWindowPos、WM_SIZEで追随。 | 初期表示時は幅高さ 0 のことがある。Z オーダーや可視フラグも確認。 |
| DPI/スケーリングで見切れている | WM_DPICHANGED に応答し、提示の推奨矩形に MoveWindow。 | Per-Monitor V2 を有効化(マニフェストまたは SetThreadDpiAwarenessContext)。 |
| 対象 OS 未満 | Windows 10 1903 (build 18362) 以降が必須。満たさない環境では処理をスキップし、ユーザーに通知。 | ApiInformation::IsApiContractPresent でランタイムチェックを入れておくと安全。 |
| SDK を入れていないから動かない? | いいえ。Microsoft.Toolkit.Win32.UI.SDK は必須ではない。C++/WinRT のみでホストできる。 | ただし将来的に Windows App SDK/WinUI 3 へ移行するなら NuGet 管理が便利。 |
最小修正(コンセプト)
// グローバル or ウィンドウクラスのメンバで保持
winrt::WindowsXamlManager g_xamlManager{nullptr};
winrt::DesktopWindowXamlSource g_xamlSource{nullptr};
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch (msg)
{
case WM_CREATE:
{
// STA(一度だけ)
winrt::init_apartment(winrt::apartment_type::single_threaded);
// XAML ランタイム初期化
g_xamlManager = winrt::WindowsXamlManager::InitializeForCurrentThread();
// Islands を作成
g_xamlSource = winrt::DesktopWindowXamlSource();
// ネイティブ interop で Win32 ウィンドウにアタッチ
auto interop = g_xamlSource.as<IDesktopWindowXamlSourceNative>();
winrt::check_hresult(interop->AttachToWindow(hWnd));
// 子 HWND を取得して初期表示
HWND hIsland{};
interop->get_WindowHandle(&hIsland);
SetWindowPos(hIsland, nullptr, 0, 0, 800, 200, SWP_SHOWWINDOW);
// 表示する XAML ツリー
winrt::StackPanel panel{};
panel.Background(winrt::SolidColorBrush{ winrt::Colors::LightGray() });
winrt::TextBlock tb{};
tb.Text(L"Hello World from XAML Islands!");
tb.HorizontalAlignment(winrt::HorizontalAlignment::Center);
tb.VerticalAlignment(winrt::VerticalAlignment::Center);
tb.FontSize(64);
panel.Children().Append(tb);
g_xamlSource.Content(panel);
return 0;
}
case WM_SIZE:
{
// 親サイズに追随
RECT rc{};
GetClientRect(hWnd, &rc);
HWND hIsland{};
g_xamlSource.as<IDesktopWindowXamlSourceNative>()->get_WindowHandle(&hIsland);
MoveWindow(hIsland, 0, 0, rc.right, rc.bottom, TRUE);
return 0;
}
case WM_DESTROY:
// 明示的にクローズ(先に XamlSource、その後 XamlManager)
if (g_xamlSource) g_xamlSource.Close();
if (g_xamlManager) g_xamlManager.Close();
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hWnd, msg, wParam, lParam);
}
「寿命」で何が起きているのか(内部の見え方)
DesktopWindowXamlSourceは内部的に「XAML ツリー」+「子 HWND(Island)」を管理します。- ローカル変数のままスコープを抜けると参照カウントが 0 になり解放、子 HWND も破棄されて 見えない(表示されない) 状態になります。
- Win32 はメッセージ駆動なので、
WM_CREATEで生成し、WM_DESTROYまで生かし続けるのが最も自然です。
実運用での保持パターン比較
| 保持先 | メリット | 注意点 |
|---|---|---|
| グローバル変数 | サンプルを最短で動かせる。学習向け。 | 多ウィンドウ対応やテスト容易性に劣る。 |
| ウィンドウクラスのメンバ(C++ クラス) | RAII と相性が良く、拡張しやすい。 | WndProc をメンバ化する設計が必要。 |
GWLP_USERDATA にポインタ格納 | 無名関数型の WndProc でもメンバへアクセスしやすい。 | 寿命管理(WM_NCDESTROY)とキャスト安全性に注意。 |
フルの最小サンプル(ビルドしやすい形)
以下は、単一ファイルでビルドできる最小構成(概念)。ヘッダーは Windows SDK 10.0.18362 以降を前提とします。
#include <windows.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.UI.Xaml.Hosting.h>
#include <winrt/Windows.UI.Xaml.Controls.h>
#include <winrt/Windows.UI.Xaml.Media.h>
#include <windows.ui.xaml.hosting.desktopwindowxamlsourcenative.h>
using namespace winrt;
using namespace Windows::UI::Xaml;
using namespace Windows::UI::Xaml::Hosting;
using namespace Windows::UI::Xaml::Controls;
using namespace Windows::UI::Xaml::Media;
WindowsXamlManager g_xamlManager{nullptr};
DesktopWindowXamlSource g_xamlSource{nullptr};
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
int APIENTRY wWinMain(HINSTANCE hInst, HINSTANCE, PWSTR, int nCmd)
{
init_apartment(apartment_type::single_threaded);
const wchar_t* kClass = L"XamlIslandsSample";
WNDCLASSEXW wc{ sizeof(wc) };
wc.lpfnWndProc = WndProc;
wc.hInstance = hInst;
wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wc.lpszClassName = kClass;
RegisterClassExW(&wc);
HWND hWnd = CreateWindowExW(0, kClass, L"XAML Islands Sample",
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 1000, 600,
nullptr, nullptr, hInst, nullptr);
ShowWindow(hWnd, nCmd);
UpdateWindow(hWnd);
MSG msg{};
while (GetMessageW(&msg, nullptr, 0, 0))
{
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
return (int)msg.wParam;
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch (msg)
{
case WM_CREATE:
{
g_xamlManager = WindowsXamlManager::InitializeForCurrentThread();
g_xamlSource = DesktopWindowXamlSource();
auto interop = g_xamlSource.as<IDesktopWindowXamlSourceNative>();
winrt::check_hresult(interop->AttachToWindow(hWnd));
HWND hIsland{};
interop->get_WindowHandle(&hIsland);
// 初期レイアウト
RECT rc{}; GetClientRect(hWnd, &rc);
SetWindowPos(hIsland, nullptr, 0, 0, rc.right, rc.bottom, SWP_SHOWWINDOW);
StackPanel panel{};
panel.Background(SolidColorBrush{ Windows::UI::Colors::LightGray() });
TextBlock tb{};
tb.Text(L"Hello World from XAML Islands!");
tb.HorizontalAlignment(HorizontalAlignment::Center);
tb.VerticalAlignment(VerticalAlignment::Center);
tb.FontSize(64);
panel.Children().Append(tb);
g_xamlSource.Content(panel);
return 0;
}
case WM_SIZE:
{
HWND hIsland{};
g_xamlSource.as<IDesktopWindowXamlSourceNative>()->get_WindowHandle(&hIsland);
MoveWindow(hIsland, 0, 0, LOWORD(lParam), HIWORD(lParam), TRUE);
return 0;
}
case WM_DPICHANGED:
{
// 推奨矩形に合わせると滲みが減る
RECT* const prcNewWindow = reinterpret_cast<RECT*>(lParam);
SetWindowPos(hWnd, nullptr,
prcNewWindow->left, prcNewWindow->top,
prcNewWindow->right - prcNewWindow->left,
prcNewWindow->bottom - prcNewWindow->top,
SWP_NOZORDER | SWP_NOACTIVATE);
return 0;
}
case WM_DESTROY:
if (g_xamlSource) g_xamlSource.Close();
if (g_xamlManager) g_xamlManager.Close();
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hWnd, msg, wParam, lParam);
}
OS 条件チェックの入れ方
開発機では動くのに検証機で表示されない場合、OS 条件を満たしていない可能性があります。ランタイムチェックを入れておきましょう。
#include <winrt/Windows.Foundation.Metadata.h>
using namespace Windows::Foundation::Metadata;
// 例: UniversalApiContract の存在で早期判定
if (!ApiInformation::IsApiContractPresent(L"Windows.Foundation.UniversalApiContract", 8))
{
MessageBoxW(hWnd, L"XAML Islands は Windows 10 1903 以降が必要です。", L"環境要件", MB_OK | MB_ICONWARNING);
return -1; // WM_CREATE 失敗でクラシック UI にフォールバックなど
}
レイアウトと DPI(見えない=実は極小サイズ問題のことも)
- 初期化直後の子 HWND は サイズ 0×0 のことがあります。必ず
SetWindowPosかMoveWindowを呼びます。 - 最初の表示時は
SWP_SHOWWINDOWを付けて可視化。Z オーダーを崩したくないときはSWP_NOZORDERを併用。 - 高 DPI 環境では
WM_DPICHANGEDを処理し、推奨矩形を採用することでボケや欠けを防げます。
スレッドモデル(STA は必須)
WindowsXamlManager::InitializeForCurrentThread() の実行スレッドは STA である必要があります。
以下を満たさないと、表示されない/時々落ちる/描画が止まるなどの不定挙動を招きます。
- UI を扱うスレッドで
winrt::init_apartment(winrt::apartment_type::single_threaded)を一度だけ。 - 別スレッドから XAML ツリーを触らない(必要ならディスパッチ)。
診断のコツ(開発者の実務ワークフロー)
- Spy++/WinDbg で子 HWND が存在するかを確認。無ければ寿命か Attach ミス。
- ログ…
AttachToWindowとget_WindowHandleの 戻り値 を必ず検証(check_hresult)。 - 可視状態…
IsWindowVisible(hIsland)、矩形はGetWindowRectで確認。 - スケーリング… 200% 以上のディスプレイで領域が 0 になっていないか。
よくある落とし穴
- コンストラクタ内で Islands を作って すぐ 破棄してしまう(メンバ化忘れ)。
WM_CREATEで作ったあと、WM_SIZEを実装しておらず、裏に貼り付いた極小ウィンドウ のまま。Close()の順序が逆(先にWindowsXamlManagerを閉じる)で例外や未定義動作。- ハンドル漏れ・二重初期化(
InitializeForCurrentThreadを同一スレッドで複数回)。
設計パターン:メンバ保持と GWLP_USERDATA
WndProc スタイルの既存コードに最小侵襲で組み込むなら、GWLP_USERDATA に this ポインタを入れ、メンバへディスパッチするのが定番です。
struct MainWindow {
HWND hwnd{};
winrt::WindowsXamlManager xamlManager{nullptr};
winrt::DesktopWindowXamlSource xamlSource{nullptr};
void OnCreate() { /* ... 上述の初期化 ... */ }
void OnSize(WORD w, WORD h) { /* ... MoveWindow ... */ }
void OnDestroy() { xamlSource.Close(); xamlManager.Close(); }
};
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM w, LPARAM l)
{
MainWindow* self = reinterpret_cast(GetWindowLongPtrW(hWnd, GWLP_USERDATA));
if (msg == WM_NCCREATE) {
self = new MainWindow{};
self->hwnd = hWnd;
SetWindowLongPtrW(hWnd, GWLP_USERDATA, reinterpret_cast(self));
}
if (!self) return DefWindowProcW(hWnd, msg, w, l);
switch (msg) {
case WM_CREATE: self->OnCreate(); return 0;
case WM_SIZE: self->OnSize(LOWORD(l), HIWORD(l)); return 0;
case WM_DESTROY: self->OnDestroy(); delete self; SetWindowLongPtrW(hWnd, GWLP_USERDATA, 0); PostQuitMessage(0); return 0;
}
return DefWindowProcW(hWnd, msg, w, l);
}
WPF/WinForms との違い(混同しがちなポイント)
- WPF/WinForms の ホストコントロール は生存がフレームワークにより保証されますが、Win32 直叩きでは 開発者が寿命を保証 する必要があります。
- メッセージ駆動でレイアウトが変わるたびに
MoveWindowを呼ぶ点もフレームワークと発想が異なります。
Windows App SDK / WinUI 3 への見通し
現行の XAML Islands(UWP XAML ベース)でも問題なく動作しますが、将来的に WinUI 3(Windows App SDK)へ移行する場合は NuGet で依存を一元管理しやすくなります。とはいえ、「表示されない」根本原因は同じく寿命とレイアウトです。まずは本記事のチェックリストで健全化し、その後に移行を検討すると安全です。
チェックリスト(そのまま現場で使える)
- UI スレッドで
init_apartment(STA)を一度だけ呼んでいる WindowsXamlManagerとDesktopWindowXamlSourceをウィンドウ寿命で保持しているAttachToWindowの戻り値を検証している- 子 HWND 取得後、初回に
SWP_SHOWWINDOW付きでサイズ設定している WM_SIZEとWM_DPICHANGEDを処理しているClose()の順序(XamlSource→XamlManager)を守っている- OS 条件未満では処理をスキップしユーザーへ通知している
FAQ
- Q:
Microsoft.Toolkit.Win32.UI.SDKを入れないと表示できませんか?
A: いいえ。必須ではありません。C++/WinRT だけで十分です。 - Q: たまに表示され、リサイズすると消えます。
A: 子 HWND のレイアウト未更新が原因です。WM_SIZEでMoveWindowを必ず実行してください。 - Q: スレッドを分けたい。
A: UI を跨ぐ操作は非推奨。UI スレッドにディスパッチする設計にしてください。
まとめ
- 表示されない最大の原因はライフサイクル。
DesktopWindowXamlSourceとWindowsXamlManagerをウィンドウ寿命と同じスコープで保持し、WM_CREATE/WM_SIZE/WM_DESTROYで制御します。 - 追加 SDK は不要。C++/WinRT ベースで問題なくホスト可能です。
- 実装のコツ:初回表示に
SWP_SHOWWINDOW、DPI 変化に追随、Close()の順序を厳守。
参考:トラブル対処フローチャート(簡易)
- STA 初期化済みか? → 未実施なら
init_apartment(STA) InitializeForCurrentThreadとAttachToWindowの戻り値 OK? → 失敗ならデバッグログ確認- 子 HWND 取得できたか? → できていなければ寿命・権限・ハンドルを再点検
- 初期レイアウト(
SWP_SHOWWINDOW付き)を実施したか? WM_SIZE/WM_DPICHANGED対応はあるか?- OS 条件を満たしているか?
チェック用コード断片(差分採用しやすい版)
// 1) グローバル or メンバ
winrt::WindowsXamlManager g_mgr{ nullptr };
winrt::DesktopWindowXamlSource g_src{ nullptr };
// 2) WM_CREATE
g_mgr = winrt::WindowsXamlManager::InitializeForCurrentThread();
g_src = winrt::DesktopWindowXamlSource();
auto interop = g_src.as();
winrt::check_hresult(interop->AttachToWindow(hWnd));
HWND hIsland{}; interop->get_WindowHandle(&hIsland);
RECT rc{}; GetClientRect(hWnd, &rc);
SetWindowPos(hIsland, nullptr, 0, 0, rc.right, rc.bottom, SWP_SHOWWINDOW);
// 3) コンテンツ
winrt::TextBlock tb{}; tb.Text(L"Hello World from XAML Islands!"); tb.FontSize(64);
winrt::StackPanel panel{}; panel.Children().Append(tb);
g_src.Content(panel);
// 4) WM_SIZE
g_src.as()->get_WindowHandle(&hIsland);
MoveWindow(hIsland, 0, 0, LOWORD(lParam), HIWORD(lParam), TRUE);
// 5) WM_DESTROY(順序に注意)
g_src.Close();
g_mgr.Close();

コメント