MFC/C++/CXでRequestPurchaseAsyncがクラッシュする原因と対策|StoreContextのHWND設定とMicrosoft Storeサブスク実装

Microsoft Storeで配布している無料のMFCデスクトップアプリに、サブスクリプション型アドオン(アプリ内課金)を追加しようとして、Windows Runtime Component(C++/CX)からStoreContext::RequestPurchaseAsyncを呼ぶとクラッシュする――本記事では原因の切り分けから、IInitializeWithWindowによるHWND設定、プロジェクト設定(AppContainer=false)まで、動く形に落とし込みます。

目次

問題の全体像:MFC+Windows Runtime Componentで課金UIを出したいのに落ちる

Microsoft Storeで公開済みの無料MFCデスクトップアプリに、サブスクリプション型アドオン(アプリ内課金)を追加したい。そこで、Win32/MFC本体から直接UWP向けAPIを触らずに済むように、別プロジェクトとしてC++/CXのWindows Runtime Component(Universal Windows)を用意し、その中でWindows.Services.StoreのAPIを呼び出す構成はよく見かけます。

しかし、この構成でStoreContext::RequestPurchaseAsyncを呼ぶと、購入UIを出す前後で突然クラッシュしたり、GetAssociatedStoreProductsAsyncの結果が「何も返らない/期待と違う」ように見えたりします。多くの場合、原因はStore側ではなく、デスクトップアプリ特有のウィンドウ所有権(HWND)とプロジェクト種別、そして非同期処理の設計にあります。

前提:この話が刺さる構成・刺さらない構成

まず前提を揃えます。Windows.Services.Store(StoreContext)でアドオン課金を行うとき、環境やパッケージ形態によって挙動が変わります。今回の「RequestPurchaseAsyncでクラッシュ」は、特にWin32デスクトップアプリで購入UIを出すときに起きやすい典型例です。

項目今回の記事が想定する状態補足
アプリ種別MFC/Win32 デスクトップアプリ購入UIはデスクトップのウィンドウ階層にぶら下げる必要があります
課金方式Microsoft Store のアドオン(サブスク)Storeで発行される「Store ID(例:9N…)」を使うのが基本です
呼び出し経路C++/CX Windows Runtime Component経由コンポーネント側でStoreContext APIを扱う
パッケージStore配布(MSIX/APPXのパッケージIDあり)パッケージIDがない実行形態だと、Store API自体が制限される場合があります

再現しやすい症状

クラッシュ箇所は、だいたい次のようなコードの行です(C++/CX+PPLタスクの例)。

using namespace concurrency;
using namespace Windows::Services::Store;

StoreContext^ storeContext = StoreContext::GetDefault();

// ここでクラッシュする、またはUIが出ずに落ちる
create_task(storeContext->RequestPurchaseAsync(L"myAddonStoreID"))
  .then([](StorePurchaseResult^ result)
  {
      // 購入結果の処理
  }, task_continuation_context::get_current_winrt_context());

追加で「アドオンが存在するか」を確認するためにGetAssociatedStoreProductsAsyncを呼んでも、結果が空に見えたり、判定が常にfalseになったりします。

原因を最短で特定するための結論

結論から言うと、ハマりどころは次の3つがほとんどです。

よくある原因起きること最短の対策
オーナーウィンドウ未設定購入UIを出すはずがクラッシュ/例外/無反応IInitializeWithWindowでStoreContextにHWNDを渡す
WinRTコンポーネントがUWP(AppContainer=true)前提shobjidl.hが使えない/ビルドが通らないAppContainerApplication=false+/ZW+WindowsMetadata生成
非同期の戻り値設計ミスFindAddons()が必ずfalse、結果が反映されない戻り値をtask/IAsyncOperationにする、またはコールバック化

原因①:デスクトップアプリでStoreContextを使うなら「どのウィンドウの子か」を必ず指定する

RequestPurchaseAsyncは内部で購入フローのUI(モーダルに近いダイアログ)を表示します。UWP/WinUI系のアプリは自前のウィンドウ基盤(CoreWindowやXAML)と統合されているため、UI表示の前提が満たされています。

一方、MFCなどのWin32デスクトップアプリにはHWND(ウィンドウハンドル)という概念があり、購入UIを「どのウィンドウの上に、どの親子関係で出すか」を明示しないと、内部で不正なウィンドウ参照になりやすくなります。これがクラッシュの温床です。

対策は、COMインターフェースIInitializeWithWindowを使って、StoreContextにオーナーとなるHWNDを登録してからRequestPurchaseAsyncを呼ぶことです。

なぜIInitializeWithWindowが必要なのか

  • 購入UIはOS側が表示しますが、親ウィンドウが分からないと、フォーカスやモーダル制御が破綻します。
  • 結果として「落ちる」「UIが出ない」「別画面の裏に出てユーザーが気付かない」などの不具合につながります。
  • 特にWin32アプリは複数ウィンドウを持てるため、メインウィンドウを明示するのが安全です。

原因②:Windows Runtime Componentのプロジェクト種別がUWPだと、デスクトップ向けヘッダーが使えない

Windows Runtime Componentプロジェクトが、.vcxproj内で<AppContainerApplication>true</AppContainerApplication>になっている(=UWP/AppContainer前提)場合、デスクトップ専用のCOMヘッダーであるshobjidl.h / shobjidl_core.hを使おうとするとビルドエラーになります。

ここで重要なのは「Store APIを呼ぶ部分はWinRTで書いているのに、購入UIの親指定はデスクトップのCOMインターフェースが必要」というねじれです。つまり、購入UIを扱うなら、コンポーネント側もデスクトップ前提の設定に寄せる必要があります。

設定のゴール:デスクトップから参照できるWinRTコンポーネント

狙う状態は次の通りです。

設定項目推奨値理由
AppContainerApplicationfalseデスクトップ用ヘッダー/COMを使えるようにする
Windows Runtime拡張/ZWC++/CXでWinRTを扱うため
Windows Metadata生成YesWinRTコンポーネントとして参照・型公開するため

原因③:非同期APIの結果を「同期的なbool」で返そうとしている

GetAssociatedStoreProductsAsyncのようなAPIは名前の通り非同期です。にもかかわらず、次のように「メソッドの戻り値をboolにして、thenの中でtrue/falseを決める」書き方をすると、非同期が終わる前にreturnが実行されるため、結果が必ずfalseになります。

bool FindAddons()
{
    StoreContext^ storeContext = StoreContext::GetDefault();

    create_task(storeContext->GetAssociatedStoreProductsAsync(...))
      .then([](StoreProductQueryResult^ addOns)
      {
          return true; // ここで返しても呼び出し元には届かない
      }, task_continuation_context::get_current_winrt_context());

    return false; // 非同期完了前に必ずここが返る
}

この設計ミスは「Storeが返していない」のではなく、「自分のコードが結果を捨てている」状態です。対策は、戻り値を非同期にする(taskやIAsyncOperationで返す)か、コールバック/イベントで通知することです。

解決策:クラッシュを止めて、購入フローを確実に動かす実装手順

手順1:Windows Runtime Componentをデスクトップ前提に変更する

プロジェクトをUWP前提のままにせず、デスクトップでCOMヘッダーを扱えるようにします。最小の変更は、.vcxprojのAppContainerApplicationをfalseにすることです。

<!-- もともと true の場合が多い -->
<AppContainerApplication>true</AppContainerApplication>

<!-- デスクトップ前提に変更 -->
<AppContainerApplication>false</AppContainerApplication>

あわせてVisual Studioのプロジェクトプロパティも確認します(表の通り)。

手順2:IInitializeWithWindowでStoreContextにHWNDを渡してからRequestPurchaseAsyncを呼ぶ

コンポーネント側でHWNDを取得できないケース(別スレッド呼び出し、アクティブウィンドウが無い等)もあるため、実務ではMFC側から明示的にHWNDを渡す設計が安定します。以下はその形の例です。

ポイント

  • #include <shobjidl.h> を入れて IInitializeWithWindow を使えるようにする
  • StoreContext^ を IInspectable/IUnknown として扱い、QueryInterface で IInitializeWithWindow を取得する
  • Initialize(hwnd) を呼んでから RequestPurchaseAsync を実行する
  • 可能なら購入開始はUIスレッドから行う(MFCのボタンクリック等)
#include <shobjidl.h>
#include <windows.services.store.h>
#include <ppltasks.h>

using namespace concurrency;
using namespace Windows::Services::Store;
using namespace Platform;

int WindowsRuntimeComponent1::Class1::Purchase(long long ownerHwnd, String^ storeId)
{
    StoreContext^ storeContext = StoreContext::GetDefault();

    // 渡されたHWNDを使う(MFC側で AfxGetMainWnd()->GetSafeHwnd() などを渡す)
    HWND hwnd = reinterpret_cast<HWND>(ownerHwnd);

    IInitializeWithWindow* initWindow = nullptr;
    HRESULT hr = reinterpret_cast<IUnknown*>(storeContext)
                   ->QueryInterface(IID_PPV_ARGS(&initWindow));

    if (SUCCEEDED(hr) && initWindow != nullptr)
    {
        hr = initWindow->Initialize(hwnd);
        initWindow->Release();
        initWindow = nullptr;
    }

    // ここまでで「購入UIの親」が確定している状態にする
    create_task(storeContext->RequestPurchaseAsync(storeId))
      .then([](StorePurchaseResult^ result)
      {
          // 例:結果をログに出す、状態に応じてUIを更新する等
          switch (result->Status)
          {
          case StorePurchaseStatus::Succeeded:
              // 購入成功
              break;
          case StorePurchaseStatus::AlreadyPurchased:
              // 既に所有している
              break;
          case StorePurchaseStatus::NotPurchased:
              // ユーザーがキャンセル等
              break;
          default:
              // NetworkError / ServerError など
              break;
          }
      }, task_continuation_context::get_current_winrt_context());

    return 0;
}

MFC側から呼ぶイメージは次の通りです(概略)。

// 例:MFCのボタンハンドラなどUIスレッドで実行
HWND hwnd = AfxGetMainWnd() ? AfxGetMainWnd()->GetSafeHwnd() : nullptr;

auto comp = ref new WindowsRuntimeComponent1::Class1();
comp->Purchase(reinterpret_cast<long long>(hwnd), ref new Platform::String(L"myAddonStoreID"));

「コンポーネント内でGetActiveWindow()を呼べばいいのでは?」と思いがちですが、スレッドやフォーカス状況によってNULLが返ることがあります。購入UIを確実に表示したいなら、呼び出し元(MFC)が責任を持って親HWNDを渡すほうが事故が減ります。

手順3:GetAssociatedStoreProductsAsyncは“結果が返せる形”に作り直す

「アドオンが存在するか」を判定したいなら、設計を非同期にします。C++/CX内で完結させたい場合、戻り値をconcurrency::task<bool>にするのが分かりやすいです。

concurrency::task<bool> WindowsRuntimeComponent1::Class1::FindAddonAsync(Platform::String^ addonStoreId)
{
    using namespace Windows::Foundation::Collections;

    StoreContext^ storeContext = StoreContext::GetDefault();

    auto filterList = ref new Platform::Collections::Vector<Platform::String^>();
    filterList->Append(ref new Platform::String(L"Consumable"));
    filterList->Append(ref new Platform::String(L"Durable"));
    filterList->Append(ref new Platform::String(L"Subscription"));
    filterList->Append(ref new Platform::String(L"UnmanagedConsumable"));

    return concurrency::create_task(storeContext->GetAssociatedStoreProductsAsync(filterList))
      .then([addonStoreId](StoreProductQueryResult^ r)
      {
          // Store側のエラーも確認する(空に見える原因がここに隠れることが多い)
          if (FAILED(r->ExtendedError))
          {
              return false;
          }

          // Products は storeId をキーにしたマップ
          return r->Products->HasKey(addonStoreId);
      });
}

呼び出し側(MFC側)もタスクとして受け取り、thenで扱います。

create_task(comp->FindAddonAsync(ref new Platform::String(L"myAddonStoreID")))
  .then([](bool exists)
  {
      if (exists)
      {
          // アドオンが見つかった
      }
      else
      {
          // 見つからない、またはエラー
      }
  });

「どうしても同期的にboolが欲しい」という設計は、UIを止めたりデッドロックの原因になったりしがちです。StoreのAPIはユーザー操作を伴うため、基本は非同期前提で組み立てるのが安全です。

非同期の戻し方:選択肢と向き不向き

戻し方実装のしやすさ呼び出し側の扱いやすさ向いている場面
boolを直接return簡単に見える実は破綻しやすい非同期APIには不向き(避ける)
concurrency::task<T>中PPLのthenで扱えるC++/CX内で完結する処理
IAsyncOperation<T>中〜やや難WinRTとして公開しやすい他言語(C#/WinUI等)にも公開したい
コールバック/イベント設計が必要UI更新に強い購入完了時に画面を切り替える等

MSIX(デスクトップブリッジ)での注意点:TrustLevelと“Invalid window”

Win32アプリをMSIXでパッケージ化してStoreに載せる場合でも、実行プロセスがAppContainer(部分信頼)として動いていると、IInitializeWithWindowで渡したHWNDが「無効なウィンドウ」と見なされ、0x80070578(Invalid window)相当の問題に遭遇するケースがあります。

このときは、購入フローを実行するプロセスがフルトラスト(Full trust)で動作しているか、マニフェストやパッケージ構成(fullTrustProcess等)を見直す必要があります。Storeの課金UIはウィンドウ階層との結びつきが強いため、プロセスの信頼レベル/コンテナ境界が影響しやすい点は押さえておくと調査が速くなります。

実装時のチェックリスト(落ちない・迷子にならないために)

チェック項目確認ポイントNGだと起きやすい症状
購入開始はUIスレッドかMFCのボタンなどメッセージループ上で開始UIが出ない、フォーカスが飛ぶ
HWNDは有効かNULLでない/既に破棄されていないクラッシュ、Invalid window
IInitializeWithWindowを呼んだかRequestPurchaseAsyncの前にInitializeクラッシュ、親なしダイアログ
WinRTコンポーネントがデスクトップ前提かAppContainer=false、/ZW、WindowsMetadata=Yesshobjidl.hが使えない、ビルド不能
Store IDは正しいかアドオンのStore ID(例:9N…)を指定NotPurchased/空結果、想定外のID参照
ExtendedErrorを見ているかQueryResult/ PurchaseResult のExtendedErrorをログ“空に見える”原因が追えない

よくある落とし穴と実務的な回避策

「クラッシュは止まったが、購入UIが別モニターや裏側に出る」

親HWNDの指定はできていても、親が最前面でない場合、ダイアログが意図しない場所に出ることがあります。可能なら、購入開始前に親ウィンドウを前面化(ユーザー操作を尊重しつつ)し、親が実際に表示されている状態でRequestPurchaseAsyncを呼ぶと、体感の不具合が減ります。

「GetAssociatedStoreProductsAsyncが空に見える」

まずはExtendedErrorを確認してください。空に見えても、内部エラーやネットワーク起因で失敗していることがあります。また、Productsは「全部のアドオンが必ず返る」ではなく、フィルターや環境によって返り方が変わるため、判定ロジックはProductsのキー(storeId)を確認する形に寄せると堅牢です。

「Windows Runtime Componentで型の公開ができない」

WindowsMetadata生成(Generate WindowsMetadata)をYesにしていないと、コンポーネントとして参照できない/メタデータが出力されず呼べない、といった問題が起きます。/ZWとセットで見直してください。

まとめ:MFCでMicrosoft Storeサブスク課金を安定させる鍵

  • MFCなどのデスクトップアプリでRequestPurchaseAsyncを呼ぶなら、IInitializeWithWindowで必ずHWNDを渡す。
  • WinRTコンポーネントをUWP前提(AppContainer=true)のままにすると、デスクトップ用COMヘッダーが使えず詰まりやすい。AppContainer=false+/ZW+WindowsMetadata生成で「デスクトップから使うWinRT」に寄せる。
  • GetAssociatedStoreProductsAsyncなどの結果が「返らない」ように見えるときは、Store以前に非同期処理の設計(return falseが先に走っていないか)を疑う。
  • MSIX化したWin32アプリでは、信頼レベルやプロセス形態によってInvalid windowが出ることがある。購入フローはフルトラスト側で実行できる構成にしておくと調査が速い。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次