Windows::Data::JsonとC++/WinRTでJSON配列をCStringArrayへ安全に読み取る実装ガイド

C++/WinRT の Windows::Data::Json で JSON を扱いつつ、既存の MFC コード資産(CStringArray や CArray<int,int>)へきれいに橋渡ししたい――そんな場面は意外と多いです。この記事では、数値配列・文字列配列を JSON から安全に読み取り、「整数として使える配列」と「UI 表示用の文字列配列」を同時に構築する実装パターンを、実用コードとともに詳しく解説します。

目次

想定シナリオとゴール

この記事で扱うシナリオは次のようなものです。

{
  "Station": "X",
  "PositionArray": [1, 2],
  "IndexArray": [1, 2],
  "GroupIndexArray": ["White", "Black"]
}

この JSON から、以下のように値を取り出したいとします。

  • PositionArray … 整数配列(CArray<int,int>)+ 文字列配列(CStringArray)
  • IndexArray … 整数配列 + 文字列配列
  • GroupIndexArray … 文字列配列のみ(CStringArray)

つまり、

  • 数値は 整数として処理もしつつ
  • UI 表示のために 文字列としても保持したい

という要件です。以降のサンプルをプロジェクトにそのまま貼り付けるだけで、同じ構成の JSON を安全にパースできることを目標にします。

前提環境とヘッダー

前提として、以下のような構成を想定します。

  • Windows アプリ(MFC アプリ、あるいは MFC を併用した C++ アプリ)
  • C++/WinRT を利用
  • JSON パーサーとして Windows::Data::Json を使用

必要なヘッダーはおおよそ次のとおりです。

&lt;winrt/Windows.Data.Json.h&gt;
&lt;cmath&gt;     // std::llround
&lt;afx.h&gt;     // CString
&lt;afxtempl.h&gt; // CArray

MFC アプリでは通常 stdafx.h / pch.h に上記が含まれるように調整します。C++/WinRT を使う場合は、アプリ起動時に winrt::init_apartment(); を呼び出しておくことも忘れないでください。

JSON文字列をJsonObjectにパースする

最初に、JSON 文字列を JsonObject に変換する処理です。ここでは例として std::wstring を受け取る関数を用意します。

#include <winrt/Windows.Data.Json.h>
#include <cmath>
#include <afx.h>
#include <afxtempl.h>

using namespace winrt;
using namespace Windows::Data::Json;

bool ParseAndFill(const std::wstring& json)
{
JsonObject obj;
// JSON 文字列をパース
if (!JsonObject::TryParse(json, obj)) {
// ログを出したり、メッセージボックスを出したりしてもよい
return false;
}


// ここで obj から配列を取り出していく
// (後述の FillNumberArray / FillStringArray を呼び出す)
return true;


} 

JsonObject::TryParse は、失敗した場合に false を返し、例外を投げない点が扱いやすいポイントです。Web API から取得した JSON をそのまま流し込む場合でも、まずは TryParse で構文チェックしてから処理を進めるようにすると、落ちにくいコードになります。

数値配列をCStringArrayとCArray<int,int>へ格納する

次に、数値配列の処理です。PositionArray や IndexArray に対して同じロジックを使い回したいので、配列名を引数で受け取るヘルパー関数を作ります。

// 数値配列を CStringArray と CArray<int,int> の両方に格納
static void FillNumberArray(JsonObject const& obj,
                            wchar_t const* name,
                            CStringArray& asStrings,
                            CArray<int,int>& asInts)
{
    // キーがなければ何もしない(例外を出さないようにする)
    if (!obj.HasKey(name)) return;


JsonArray arr = obj.GetNamedArray(name);

// 範囲 for で全要素を走査
for (auto const& v : arr) {
    if (v.ValueType() == JsonValueType::Number) {
        // JSON の数値は double として返ってくる
        double d = v.GetNumber();
        int n = static_cast<int>(std::llround(d)); // 四捨五入で int へ

        CString s;
    #ifdef UNICODE
        s.Format(L"%d", n);
    #else
        s.Format("%d", n);
    #endif

        asStrings.Add(s); // 文字列として保存
        asInts.Add(n);    // 整数としても保存
    }
    else {
        // 数値以外が混ざっていた場合のログなどを入れてもよい
    }
}


} 

FillNumberArrayの処理フロー

ステップ内容
1HasKey(name) でキーの存在確認を行い、なければスキップ
2GetNamedArray(name) で JsonArray を取得
3範囲 for で各要素の JsonValue を取り出す
4ValueType() が JsonValueType::Number のときだけ処理
5GetNumber() で double を取得し、std::llround で整数に丸める
6CString::Format で文字列化して CStringArray に追加
7同じ整数値を CArray<int,int> にも追加

型チェックと丸め処理のポイント

  • 必ず ValueType() を確認する
    JsonValueType::Number 以外(文字列や null)が混ざっているときに GetNumber() を呼ぶと、想定外の例外や動作の原因になります。数字しか来ない想定であっても、JSON は外部からやってくることが多いので、型チェックは保険として入れておくのがおすすめです。
  • JSON の数値は double で返る
    GetNumber() の戻り値は double なので、そのまま int にキャストすると小数点以下の扱いがあいまいになります。整数前提であれば std::llround / std::floor / std::ceil のどれかを明示的に選び、「どう丸めるか」をコードに残しておくほうが保守しやすくなります。
  • UNICODE/MBCS の両対応
    CString は UNICODE 設定時は CStringW、MBCS 時は CStringA になります。CString::Format のフォーマット文字列を #ifdef UNICODE で書き分けておくと、マルチバイト設定の古いプロジェクトにも移植しやすくなります。

文字列配列をCStringArrayに格納する

続いて、文字列配列(GroupIndexArray)を CStringArray に格納するヘルパー関数です。

// 文字列配列を CStringArray に格納
static void FillStringArray(JsonObject const& obj,
                            wchar_t const* name,
                            CStringArray& asStrings)
{
    if (!obj.HasKey(name)) return;


JsonArray arr = obj.GetNamedArray(name);

for (auto const& v : arr) {
    if (v.ValueType() == JsonValueType::String) {
        // WinRT の hstring -> const wchar_t* -> CString へ
        CString s(v.GetString().c_str());
        asStrings.Add(s);
    }
    else {
        // こちらも文字列以外が混ざっていたらログを出すなど
    }
}


} 

v.GetString() の戻り値は winrt::hstring です。.c_str() で const wchar_t* に変換できるので、CString のコンストラクタにそのまま渡せます(UNICODE プロジェクトを前提にしています)。

ParseAndFillでヘルパーをまとめて呼び出す

ここまでのヘルパーを使うと、先ほどの ParseAndFill は次のように書けます。

bool ParseAndFill(const std::wstring& json)
{
    JsonObject obj;
    if (!JsonObject::TryParse(json, obj)) {
        return false;
    }


CStringArray positionAsStr, indexAsStr, groupAsStr;
CArray<int,int> positionAsInt, indexAsInt;

// 数値配列
FillNumberArray(obj, L"PositionArray", positionAsStr, positionAsInt);
FillNumberArray(obj, L"IndexArray",    indexAsStr,    indexAsInt);

// 文字列配列
FillStringArray(obj, L"GroupIndexArray", groupAsStr);

// ここで取得した配列をメンバ変数にコピーしたり、ダイアログにバインドしたりする
// m_positionAsStr.Copy(positionAsStr); など

return true;


} 

ParseAndFillの利用イメージ

例えば、ダイアログクラスのメンバとして以下のような配列を持っておきます。

class CMyDialog : public CDialogEx
{
    // ...


CStringArray m_positionAsStr;
CArray<int,int> m_positionAsInt;
CStringArray m_indexAsStr;
CArray<int,int> m_indexAsInt;
CStringArray m_groupAsStr;

// ...


}; 

そして、JSON を読み込んだタイミングでヘルパーを使って埋めます。

bool CMyDialog::LoadJson(const std::wstring& json)
{
    JsonObject obj;
    if (!JsonObject::TryParse(json, obj)) {
        AfxMessageBox(L"JSON のパースに失敗しました。");
        return false;
    }


m_positionAsStr.RemoveAll();
m_positionAsInt.RemoveAll();
m_indexAsStr.RemoveAll();
m_indexAsInt.RemoveAll();
m_groupAsStr.RemoveAll();

FillNumberArray(obj, L"PositionArray", m_positionAsStr, m_positionAsInt);
FillNumberArray(obj, L"IndexArray",    m_indexAsStr,    m_indexAsInt);
FillStringArray(obj, L"GroupIndexArray", m_groupAsStr);

return true;


} 

こうしておけば、UI 側では CStringArray をそのままリストボックスやコンボボックスにバインドでき、計算ロジックでは CArray<int,int> を利用する、といった役割分担が明確になります。

よくあるミスと対処法

C++/WinRT と MFC を組み合わせて JSON を扱うときに、よくハマりがちなポイントを表にまとめます。

よくあるミス症状対処法
HasKey を呼ばずに GetNamedArray するキーが存在しない JSON を渡すと例外が発生するif (!obj.HasKey(name)) return; のように存在チェックを必ず挟む
ループで毎回 GetAt(0) を呼ぶ配列の先頭要素しか読めず、全て同じ値になってしまう範囲 for 文やインデックス付きループで GetAt(i) を呼ぶ
ValueType() を見ないで GetNumber() / GetString()JSON の仕様変更で型が変わったときにランタイムエラーになるswitch や if で型を判定してから取得する
小数をそのまま int にキャスト意図しない切り捨てが起き、インデックスがずれるstd::llround 等で丸め方を明示してから int にキャストする
UNICODE と MBCS の混在文字化けや、コンパイルエラーに悩まされるプロジェクト全体を UNICODE に統一し、CString には wchar_t* を渡す

整数として扱う配列と文字列配列を両方持つ理由

数値配列をわざわざ「整数」と「文字列」の二重管理にするのは遠回りに見えるかもしれませんが、実際のアプリでは次のようなメリットがあります。

用途推奨する型理由
UI 表示(リスト、コンボボックスなど)CStringArrayそのまま AddString できる。フォーマットを後から変えやすい。
計算・検索(インデックスや座標として利用)CArray<int,int> or std::vector<int>整数のまま保持したほうが比較や演算が高速で安全。
ログ出力・デバッグ表示文字列(CString)ログ出力のたびに sprintf するより、最初から文字列を持っていると楽。

特に UI 表示では、1 を "001" として表示したい、などの要望が後から出てきがちです。その場合も、内部計算用の整数配列はそのままに、表示用の CStringArray だけフォーマットを変更すればよくなります。

応用:std::vectorとの併用や汎用化

新しいコードでは、CArray よりも std::vector を使いたい場面も多いはずです。その場合のヘルパーは次のように書き換えられます。

static void FillNumberArray(JsonObject const& obj,
                            wchar_t const* name,
                            CStringArray& asStrings,
                            std::vector<int>& asInts)
{
    if (!obj.HasKey(name)) return;
    JsonArray arr = obj.GetNamedArray(name);


asInts.clear();
asInts.reserve(arr.Size());

for (auto const& v : arr) {
    if (v.ValueType() == JsonValueType::Number) {
        double d = v.GetNumber();
        int n = static_cast<int>(std::llround(d));

        CString s;
    #ifdef UNICODE
        s.Format(L"%d", n);
    #else
        s.Format("%d", n);
    #endif

        asStrings.Add(s);
        asInts.push_back(n);
    }
}


} 

既存の MFC コードと新規 C++ コードが混在するプロジェクトでは、「外部との境界(JSON や UI)」では CStringArray、内部ロジックでは std::vector を使う、といった住み分けをしておくと徐々にモダナイズしやすくなります。

テスト用JSONとデバッグのコツ

JSON パースの実装が正しく動いているか確認するには、テスト用の JSON をいくつか用意しておくと便利です。

  • 通常ケース { "PositionArray": [1, 2, 3], "IndexArray": [10, 20], "GroupIndexArray": ["White", "Black"] }
  • キーの欠落(防御コードが効いているか) { "PositionArray": [1, 2, 3] }
  • 型が混ざっているケース(不正値をスキップできるか) { "PositionArray": [1, "2", 3.5] }

デバッグ時には、配列の中身を TRACE やログに出力すると確認しやすくなります。

for (INT_PTR i = 0; i &lt; positionAsStr.GetSize(); ++i) {
    TRACE(L"Pos[%d] str=%s int=%d\n",
          static_cast&lt;int&gt;(i),
          positionAsStr[i],
          positionAsInt[i]);
}

このように、JSON の入力を変えたときにログの中身がどう変化するかを確認できれば、実装の正しさに自信が持てるようになります。

まとめ

C++/WinRT の Windows::Data::Json で JSON 配列を扱い、MFC の CStringArray/CArray<int,int> に橋渡しする方法を解説しました。ポイントを整理すると次のようになります。

  • JsonObject::TryParse で JSON を安全にパースする
  • HasKey でキーの存在確認を行い、GetNamedArray で配列を取得する
  • 各要素の ValueType() を確認してから GetNumber()/GetString() を呼び出す
  • 数値は double として取得されるので、std::llround などで丸めた上で int に変換する
  • UI 表示用には CStringArray、処理用には整数配列(CArray<int,int> や std::vector<int>)の二段構えにしておくと運用が楽
  • 範囲 for を使うことで、インデックス計算のミス(毎回 GetAt(0) など)を避けられる

ここで紹介したヘルパー関数をそのままプロジェクトに取り込めば、数値配列・文字列配列を安全に CStringArray 等へ取り込む基盤が整います。あとはプロジェクト固有の要件(丸め方、エラーハンドリング、ログ出力など)に合わせて少しずつカスタマイズしていけば、堅牢で保守しやすい JSON パースコードを構築できるはずです。

この記事を書いた人

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

コメント

コメントする

目次