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を使用
必要なヘッダーはおおよそ次のとおりです。
<winrt/Windows.Data.Json.h>
<cmath> // std::llround
<afx.h> // CString
<afxtempl.h> // 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の処理フロー
| ステップ | 内容 |
|---|---|
| 1 | HasKey(name) でキーの存在確認を行い、なければスキップ |
| 2 | GetNamedArray(name) で JsonArray を取得 |
| 3 | 範囲 for で各要素の JsonValue を取り出す |
| 4 | ValueType() が JsonValueType::Number のときだけ処理 |
| 5 | GetNumber() で double を取得し、std::llround で整数に丸める |
| 6 | CString::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 < positionAsStr.GetSize(); ++i) {
TRACE(L"Pos[%d] str=%s int=%d\n",
static_cast<int>(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 パースコードを構築できるはずです。

コメント