C++ で最新の台風情報を取得する

(1/1)
>C++ で最新の台風情報を取得する
インターネット経由で気象庁防災情報XMLにアクセスし、最新の台風情報(番号,名称,位置,中心気圧,最大瞬間風速,進路)を地図上にマッピングしたり、その情報をファイル保存するアプリケーションを作る。「PHPで台風情報を取得する」で作ったPHPプログラムをC++に移植したものである。
地図としては、地理院地図、オープンストリートマップのほか、Edgeブラウザに対応し64ビットアプリ化したことで、Googleマップが再び利用できるようになった。Googleマップ利用には、ユーザーにおいてGoogle Cloud APIキーを取得してほしい。

目次

サンプル・プログラム

圧縮ファイルの内容
typhoonwin.msiインストーラ
bin/typhoonwin.exe実行プログラム本体
bin/WebView2Loader.dll
bin/libcurl-x64.dll
実行時に必要になるDLL
bin/cacert.pemcURL通信で必要になるCASE証明書
bin/etc/help.chmヘルプ・ファイル
sour/earthquakewin.cppソース・プログラム
sour/resource.hリソース・ヘッダ
sour/resource.rcリソース・ファイル
sour/mystrings.cpp汎用文字列処理関数など(ソース)
sour/mystrings.h汎用文字列処理関数など(ヘッダ)
sour/pahooGeocode.cpp住所・緯度・経度に関わるクラス(ソース)
sour/pahooGeocode.hpp住所・緯度・経度に関わるクラス(ヘッダ)
sour/apikey.cppAPIキーの管理(ソース)
sour/apikey.hppAPIキーの管理(ヘッダ)
sour/WebView2.hWebView2に関わるヘッダ
sour/event.hWebView2用インターフェース(ヘッダ)
sour/event.cppWebView2用インターフェース(ソース)
sour/pahooWebView2.cppWebView2に関わる関数(ソース)
sour/pahooWebView2.hppWebView2に関わる関数(ヘッダ)
sour/pahooCache.cppキャッシュ処理に関わるクラス(ソース)
sour/pahooCache.hppキャッシュ処理に関わるクラス(ヘッダ)
sour/makefileビルド
typhoonwin.cpp 更新履歴
バージョン 更新日 内容
1.0.0 2026/06/20 初版
pahooGeocode.cpp 更新履歴
バージョン 更新日 内容
2.0.0 2026/05/31 makeMapLeaflet() アイコン・ラベル追加
1.9.1 2026/03/28 不要なデバッグ情報を抑制
1.9.0 2025/03/16 HTTPステータス・エラーをキャッチアップ
1.8.1 2025/02/23 setError() -- bug-fix
1.8.0 2024/05/03 getMyPath()をapikey.appのgetMyPath()関数に変更
pahooCache.cpp 更新履歴
バージョン 更新日 内容
1.1.0 2025/03/16 不具合修正
1.0 2021/04/14 初版
mystrings.cpp 更新履歴
バージョン 更新日 内容
1.4.0 2026/03/01 readWebContents() cURL 8.18.0対応
1.3.1 2025/03/16 readWebContents() リダイレクト有効に
1.3.0 2025/03/16 readWebContents() 引数httpStatus追加
1.2.0 2024/05/06 getModulePath() 追加
1.12 2021/01/31 readWebContents() 引数post追加
pahooWebView2.cpp 更新履歴
バージョン 更新日 内容
1.1.0 2025/06/07 TStrToWStr() -- C++17 対応
1.0.0 2024/04/27 初版
apikey.cpp 更新履歴
バージョン 更新日 内容
2.0.0 2024/04/29 createSetAPIkey, processSetAPIkey に統合
1.0 2020/09/30 初版

使用ライブラリ

気象庁防災情報XMLにアクセスするために、オープンソースのライブラリ Boost C++ライブラリcURL (カール)  および OpenSSL が必要になる。導入方法等については、「C++ 開発環境の準備」をご覧いただきたい。
また、地図表示にWebブラウザ・コントロールを利用するため "WebView2Loader.dll" を利用する。jchv / webview2-in-mingw からダウンロードできる。

リソースの準備

64ビット版の開発環境を用いる。
Eclipse を起動したら、新規プロジェクト typhoonwin を用意する。
ResEdit を起動したら、resource.rc を用意する。
Eclipse に戻り、ソース・プログラム "typhoonwin.cpp" を追加する。
リンカー・フラグを -s -mwindows -static -lstdc++ -lgcc -lwinpthread -lcurl -lssl -llzma -lz -lws2_32 "C:\(libcurl-x64.dllのフォルダ)\libcurl-x64.dll" "C:\(WebView2Loader.dllのフォルダ)\WebView2Loader.dll" に設定する。
また、コマンド行パターンをアレンジし "${COMMAND} ${FLAGS} ${OUTPUT_FLAG} ${OUTPUT_PREFIX}${OUTPUT} ${INPUTS} -luuid -loleaut32 -lole32" とする。

MSYS2 コマンドラインからビルドするのであれば、"makefile" を利用してほしい。

解説:定数など

typhoonwin.cpp

  47: // 初期値(START) ==============================================================
  48: #define MAKER       "pahoo.org"             // 作成者
  49: #define APPNAME     "typhoonwin"            // アプリケーション名
  50: #define APPNAMEJP   "最新の地震情報"        // アプリケーション名(日本語)
  51: #define APPVERSION  "1.0.0"                 // バージョン
  52: #define APPYEAR     "2026"                  // 作成年
  53: #define REFERENCE   "https://www.pahoo.org/e-soul/webtech/cpp01/cpp01-22-01.shtm"   // 参考サイト
  54: 
  55: // ヘルプ・ファイル
  56: #define HELPFILE    ".\\etc\\help.chm"
  57: 
  58: // デフォルト保存ファイル名
  59: #define SAVEFILE    "tp_%04d%02d%02d_%02d.csv"
  60: 
  61: // キャッシュ・ディレクトリ
  62: #define DIR_CACHE_FEED    "pcache_tw1\\"
  63: #define DIR_CACHE_FEED_L  "pcache_tw2\\"
  64: #define DIR_CACHE_DATA    "pcache_tw3\\"
  65: 
  66: // キャッシュ保持時間(分)
  67: #define LIFE_CACHE_FEED    15
  68: #define LIFE_CACHE_FEED_L  120
  69: #define LIFE_CACHE_DATA    (60 * 24 * 14)
  70: 
  71: // 古い台風情報を捨てる条件(秒)
  72: #define SCRAP_TIME  (60 * 60 * 12)
  73: 
  74: // 予報円を間引く条件(km)
  75: #define THIN_OUT    50
  76: 
  77: // 気象庁防災情報 XML フィード URL
  78: #define FEED    "https://www.data.jma.go.jp/developer/xml/feed/extra.xml"
  79: #define FEED_L  "https://www.data.jma.go.jp/developer/xml/feed/extra_l.xml"
  80: 
  81: // マップ設定
  82: #define MAP_ID      "map_id"
  83: #define MAP_WIDTH   640
  84: #define MAP_HEIGHT  480
  85: #define DEF_LONGITUDE   137.0
  86: #define DEF_LATITUDE     35.0
  87: #define DEF_ZOOM         6
  88: #define DEF_MAPTYPE     "GSISTD"
  89: #define INFO_WIDTH      (int)(MAP_WIDTH * 0.75)
  90: #define INFO_OFFSET_X   0
  91: #define INFO_OFFSET_Y   -10
  92: 
  93: // 地図描画色
  94: #define COLOR_NAME1    "#FF8800"    // 台風名称
  95: #define COLOR_LINE     "#0000FF"    // 過去経路
  96: #define COLOR_WIND1    "#FFFF00"    // 強風域
  97: #define COLOR_WIND2    "#FF0000"    // 暴風域
  98: #define COLOR_FORECAST "#FFFFFF"    // 予報円
  99: 
 100: // 基準座標(東京駅)
 101: #define LATITUDE00   35.681111
 102: #define LONGITUDE00 139.766667
 103: 
 104: // リストビュー列
 105: #define MAX_TYPHOON 30
 106: 
 107: // 初期値(END) ===============================================================

とくに注意記載が無い限り、定数は自由に変更できる。

解説:データ構造

typhoonwin.cpp

 109: // データ構造(START) ========================================================
 110: // 風域の半径情報
 111: struct TyphoonAxis {
 112:     wstring direction;
 113:     int     radius = 0;
 114: };
 115: 
 116: // 台風の1時点データ
 117: struct TyphoonRecord {
 118:     wstring kind;               // 実況 / 予報 / 過去
 119:     wstring dateTime;
 120:     wstring typhoonClass;
 121:     wstring areaClass;
 122:     wstring intensityClass;
 123:     wstring location;
 124:     double  latitude  = 0.0;
 125:     double  longitude = 0.0;
 126:     wstring direction;
 127:     wstring speed;
 128:     int     pressure        = 0;
 129:     int     centerWindSpeed = 0;
 130:     int     maxWindSpeed    = 0;
 131:     vector<TyphoonAxis> stormArea;
 132:     vector<TyphoonAxis> windArea;
 133:     vector<TyphoonAxis> forecastCircle;
 134: };
 135: 
 136: // 台風1個分
 137: struct TyphoonInfo {
 138:     wstring name;
 139:     wstring nameKana;
 140:     bool    valid = false;
 141:     vector<TyphoonRecord> records;
 142: };
 143: 
 144: // 台風番号(文字列)  TyphoonInfo
 145: using TyphoonItems = map<wstring, TyphoonInfo>;
 146: 
 147: // データ構造(END) =========================================================

取得した台風1個分の情報は構造体 TyphoonInfo に格納する。
構造体 TyphoonInfo の中には、これまでの位置情報などを格納した構造体 TyphoonRecord が複数格納している。これによって、いままでの台風の経路を地図上に描くことができる。

解説:キャッシュ・システム

最近の地震情報を取得するには、気象庁防災情報XMLから
  1. Atomフィード:高頻度フィード:随時
  2. Atomフィード:長期フィード:随時
  3. VPTW60
の3つのXMLファイルを読み込む必要がある。毎回、ロードすることは気象庁サイトへ負荷を掛けることになる。そこで、PHPプログラムの場合と同様、一定時間、ローカルドライブにXMLファイルを保持しておくキャッシュ・システムを導入した。
このキャッシュ・システムの仕組みについては、「解説:pahooCacheクラスとデータ構造 - C++ で週間天気予報を表示する」をご覧いただきたい。

typhoonwin.cpp

  61: // キャッシュ・ディレクトリ
  62: #define DIR_CACHE_FEED    "pcache_tw1\\"
  63: #define DIR_CACHE_FEED_L  "pcache_tw2\\"
  64: #define DIR_CACHE_DATA    "pcache_tw3\\"
  65: 
  66: // キャッシュ保持時間(分)
  67: #define LIFE_CACHE_FEED    15
  68: #define LIFE_CACHE_FEED_L  120
  69: #define LIFE_CACHE_DATA    (60 * 24 * 14)

キャッシュ保持時間、キャッシュ・ディレクトリともに、ローカル環境に応じて変更してほしい。天気予報系プログラムと別のキャッシュ・ディレクトリにした方が、お互いのキャッシュ保持時間の干渉を受けなくなる。
配布ファイルは、新しい地震情報が入ってくることを考え、フィードの方を短く、地震情報の方は長くキャッシュ保持時間を設定してある。

解説:ワイド文字列中の改行を他の文字列に置換

mystrings.cpp

 336: /**
 337:  * ワイド文字列中の改行を他の文字列に置換する
 338:  * @param  wstring str 置換対象の文字列
 339:  * @param  wstring rep 置換文字列
 340:  * @return wstring 置換後の文字列
 341:  */
 342: wstring wrepNL(wstring str, wstring rep) {
 343:     wstring strRet;
 344:     wstring::iterator ite = str.begin();
 345:     wstring::iterator iteEnd = str.end();
 346: 
 347:     if (0 < str.size()) {
 348:         wchar_t bNextChar = *ite++;
 349:         while (1) {
 350:             if (L'\r' == bNextChar) {
 351:                 // 改行確定
 352:                 strRet +rep;
 353:                 // EOF判定
 354:                 if (ite == iteEnd) {
 355:                     break;
 356:                 }
 357:                 // 1文字取得
 358:                 bNextChar = *ite++;
 359:                 if (L'\n' == bNextChar) {
 360:                     // EOF判定
 361:                     if (ite == iteEnd) {
 362:                         break;
 363:                     }
 364:                     // 1文字取得
 365:                     bNextChar = *ite++;
 366:                 }
 367:             } else if (L'\n' == bNextChar) {
 368:                 // 改行確定
 369:                 strRet +rep;
 370:                 // EOF判定
 371:                 if (ite == iteEnd) {
 372:                     break;
 373:                 }
 374:                 // 1文字取得
 375:                 bNextChar = *ite++;
 376:                 if (L'\r' == bNextChar) {
 377:                     // EOF判定
 378:                     if (ite == iteEnd) {
 379:                         break;
 380:                     }
 381:                     // 1文字取得
 382:                     bNextChar = *ite++;
 383:                 }
 384:             } else {
 385:                 // 改行以外
 386:                 strRet +bNextChar;
 387:                 // EOF判定
 388:                 if (ite == iteEnd) {
 389:                     break;
 390:                 }
 391:                 // 1文字取得
 392:                 bNextChar = *ite++;
 393:             }
 394:         };
 395:     }
 396:     return strRet;
 397: }

よく使う文字列処理関数は "mystrings.cpp" に分離した。
PHPの組み込み関数  nl2br  に相当する機能をワイド文字列用に拡張したのが wrepNL である。iterator を使ってワイド文字列を総なめにしている。

解説:台風情報取得

typhoonwin.cpp

 580: /**
 581:  * 台風報取得(気象庁防災情報XMLから)
 582:  * @param   TyphoonItems& items     台風情報を格納
 583:  * @param   vector<wstring>& urls   情報XMLのURLを格納
 584:  * @param   wstring& errmsg         エラーメッセージ格納用
 585:  * @return  bool true:取得成功/false:失敗
 586: */
 587: static bool getTyphoon(TyphoonItems& items,
 588:                        vector<wstring>& urls,
 589:                        wstring& errmsg) {
 590:     map<wstring, bool> flagForecast;
 591: 
 592:     if (!jmaGetTyphoonURLs(urls, errmsg)) return false;
 593: 
 594:     pahooCache* pCC2 = new pahooCache(LIFE_CACHE_DATA,
 595:                    getMyPath(APPNAME+ DIR_CACHE_DATA, UserAgent);
 596: 
 597:     // "Forecast NN時間後" にマッチ
 598:     wregex rePat3(_SW("("+ wstring(L"\x4e88"+ _SW("報)[  ]*([0-90-9]+時間後)"),
 599:                       regex_constants::icase);
 600: 
 601:     for (const wstring& wptwUrl : urls) {
 602:         string vptw = _WS(wptwUrl);
 603:         string xmlStr;
 604: 
 605: /**
 606:         // デバッグ:気象庁防災情報URL
 607:         ofstream _dbg(".\\typhoonwin_url.txt", ios::app);
 608:         _dbg << "[urll] " << vptw << endl;
 609:         _dbg.close();
 610: **/
 611:         if (pCC2->load(vptw.c_str(), &xmlStr) == FALSE) {
 612:             errmsg = _SW("気象庁防災情報XMLから台風情報を取得できません");
 613:             delete pCC2;
 614:             return false;
 615:         }
 616: 
 617:         ptree pt;
 618:         try { pt = xmlFromString(xmlStr); }
 619:         catch (...) {
 620:             errmsg = _SW("気象庁防災情報XMLのパースに失敗しました");
 621:             delete pCC2;
 622:             return false;
 623:         }
 624: 
 625:         if (!pt.get_child_optional("Report.Body.MeteorologicalInfos")) {
 626:             errmsg = _SW("気象庁防災情報XMLから台風情報を取得できません");
 627:             delete pCC2;
 628:             return false;
 629:         }
 630: 
 631:         bool    isNew  = isNewTyphoon(xmlStr, items);
 632:         bool    flag_f = false;
 633:         wstring curNum;
 634: 
 635:         for (auto& infosIt : pt.get_child("Report.Body")) {
 636:             if (infosIt.first !"MeteorologicalInfos"continue;
 637: 
 638:             for (auto& infoIt : infosIt.second) {
 639:                 if (infoIt.first !"MeteorologicalInfo"continue;
 640:                 const ptree& info = infoIt.second;
 641: 
 642:                 string dtTypeStr;
 643:                 try { dtTypeStr = info.get<string>("DateTime.<xmlattr>.type"); }
 644:                 catch (...) { continue; }
 645:                 wstring dtType = _XW(dtTypeStr);
 646: 
 647:                 string dtStr;
 648:                 try { dtStr = info.get<string>("DateTime"); } catch (...) {}
 649: 
 650:                 // ── 実況 ──
 651:                 if (dtType == _SW("実況")) {
 652:                     for (auto& kindIt : info.get_child("Item")) {
 653:                         if (kindIt.first !"Kind"continue;
 654:                         const ptree& kind = kindIt.second;
 655: 
 656:                         string propTypeStr;
 657:                         try { propTypeStr = kind.get<string>("Property.Type"); }
 658:                         catch (...) { continue; }
 659:                         wstring propType = _XW(propTypeStr);
 660: 
 661:                         // 呼称
 662:                         if (propType == _SW("呼称")) {
 663:                             try {
 664:                                 string numStr = kind.get<string>(
 665:                                     "Property.TyphoonNamePart.Number");
 666:                                 if (numStr.empty()) break;
 667:                                 curNum = _XW(numStr);
 668: 
 669:                                 if (isNew && items.find(curNum) == items.end()) {
 670:                                     TyphoonInfo ti;
 671:                                     ti.name     = _XW(kind.get<string>(
 672:                                         "Property.TyphoonNamePart.Name", ""));
 673:                                     ti.nameKana = _XW(kind.get<string>(
 674:                                         "Property.TyphoonNamePart.NameKana", ""));
 675:                                     TyphoonRecord rec;
 676:                                     rec.kind     = _SW("実況");
 677:                                     rec.dateTime = _XW(dtStr);
 678:                                     time_t tt = time(nullptr- parseISO8601(dtStr);
 679:                                     ti.valid = (tt < SCRAP_TIME);
 680: /**
 681:                                     {
 682:                                         ofstream _dbg(".\\typhoonwin_url.txt", ios::app);
 683:                                         _dbg << "[登録] num=" << _WS(curNum)
 684:                                              << " dt=" << dtStr
 685:                                              << " age=" << (long long)tt << "sec"
 686:                                              << " valid=" << ti.valid << endl;
 687:                                         _dbg.close();
 688:                                     }
 689: **/
 690:                                     ti.records.push_back(rec);
 691:                                     items[curNum] = ti;
 692:                                     flagForecast[curNum] = false;
 693:                                 } else if (!isNew) {
 694:                                     string numStr2 = kind.get<string>(
 695:                                         "Property.TyphoonNamePart.Number", "");
 696:                                     if (!numStr2.empty()) {
 697:                                         curNum = _XW(numStr2);
 698:                                         TyphoonRecord rec;
 699:                                         rec.kind     = _SW("過去");
 700:                                         rec.dateTime = _XW(dtStr);
 701:                                         items[curNum].records.push_back(rec);
 702:                                     }
 703:                                 }
 704:                             } catch (...) {}
 705: 
 706:                         // 階級
 707:                         } else if (propType == _SW("階級"&& !curNum.empty()
 708:                                    && !items[curNum].records.empty()) {
 709:                             TyphoonRecord& rec = items[curNum].records.back();
 710:                             try {
 711:                                 rec.typhoonClass   = _XW(kind.get<string>("Property.ClassPart.jmx_eb:TyphoonClass",   ""));
 712:                                 rec.areaClass      = _XW(kind.get<string>("Property.ClassPart.jmx_eb:AreaClass",      ""));
 713:                                 rec.intensityClass = _XW(kind.get<string>("Property.ClassPart.jmx_eb:IntensityClass", ""));
 714: /**
 715:                                 {
 716:                                     ofstream _dbg(".\\typhoonwin_url.txt", ios::app);
 717:                                     _dbg << "  [階級] num=" << _WS(curNum)
 718:                                          << " class=\"" << _WS(rec.typhoonClass) << "\"" << endl;
 719:                                     _dbg.close();
 720:                                 }
 721: **/
 722:                             } catch (...) {}
 723:                         // 中心
 724:                         } else if (propType == _SW("中心"&& !curNum.empty()
 725:                                    && !items[curNum].records.empty()) {
 726:                             TyphoonRecord& rec = items[curNum].records.back();
 727:                             try { rec.location = _XW(kind.get<string>("Property.CenterPart.Location", "")); }
 728:                             catch (...) {}
 729:                             try {
 730:                                 for (auto& c : kind.get_child("Property.CenterPart")) {
 731:                                     if (c.first !"jmx_eb:Coordinate"continue;
 732:                                     double la, ln;
 733:                                     if (parseCoordinate(c.second.get_value<string>(), la, ln)) {
 734:                                         rec.latitude  = la;
 735:                                         rec.longitude = ln;
 736:                                     }
 737:                                 }
 738:                             } catch (...) {}
 739:                             try {
 740:                                 for (auto& d : kind.get_child("Property.CenterPart")) {
 741:                                     if (d.first !"jmx_eb:Direction"continue;
 742:                                     rec.direction = _XW(d.second.get_value<string>());
 743:                                     break;
 744:                                 }
 745:                             } catch (...) {}
 746:                             try {
 747:                                 for (auto& s : kind.get_child("Property.CenterPart")) {
 748:                                     if (s.first !"jmx_eb:Speed"continue;
 749:                                     string cond, unit;
 750:                                     try { cond = s.second.get<string>("<xmlattr>.condition"); } catch (...) {}
 751:                                     try { unit = s.second.get<string>("<xmlattr>.unit"); }      catch (...) {}
 752:                                     if (unit == "km/h"rec.speed = _XW(s.second.get_value<string>());
 753:                                 }
 754:                             } catch (...) {}
 755:                             try { rec.pressure = kind.get<int>("Property.CenterPart.jmx_eb:Pressure", 0); }
 756:                             catch (...) {}
 757: 
 758:                         // 風(暴風域・強風域)
 759:                         } else if (propType == _SW("風"&& !curNum.empty()
 760:                                    && !items[curNum].records.empty()) {
 761:                             TyphoonRecord& rec = items[curNum].records.back();
 762:                             try {
 763:                                 for (auto& ws : kind.get_child("Property.WindPart")) {
 764:                                     if (ws.first !"jmx_eb:WindSpeed"continue;
 765:                                     string cond, wtype, unit;
 766:                                     try { cond  = ws.second.get<string>("<xmlattr>.condition"); } catch (...) {}
 767:                                     try { wtype = ws.second.get<string>("<xmlattr>.type"); }      catch (...) {}
 768:                                     try { unit  = ws.second.get<string>("<xmlattr>.unit"); }      catch (...) {}
 769:                                     int v = ws.second.get_value<int>(0);
 770:                                     wstring wtype_w = _XW(wtype);
 771:                                     bool isMaxInstant = (wtype_w.find(_SW("瞬間")) !wstring::npos);
 772:                                     if (!isMaxInstant && !cond.empty() && unit == "m/s"rec.centerWindSpeed = v;
 773:                                     else if (isMaxInstant && unit == "m/s"rec.maxWindSpeed = v;
 774:                                 }
 775:                             } catch (...) {}
 776:                             try {
 777:                                 for (auto& wap : kind.get_child("Property")) {
 778:                                         if (wap.first !"WarningAreaPart"continue;
 779:                                     wstring atype = _XW(wap.second.get<string>("<xmlattr>.type", ""));
 780:                                     auto axesCh = wap.second.get_child_optional("jmx_eb:Circle.jmx_eb:Axes");
 781:                                     if (!axesChcontinue;
 782:                                     auto axes = parseAxes(*axesCh);
 783:                                     if (atype == wstring(L"\x66b4\x98a8\x57df")) rec.stormArea = axes;
 784:                                     else if (atype == wstring(L"\x5f37\x98a8\x57df")) rec.windArea  = axes;
 785:                                 }
 786:                             } catch (...) {}
 787:                         }
 788:                     } // for kindIt(実況)
 789: 
 790:                 // ── 予報 ──
 791:                 } else {
 792:                     wsmatch m3;
 793:                     if (!regex_search(dtType, m3, rePat3)) continue;
 794:                     if (curNum.empty()) continue;
 795:                     if (!isNew && flagForecast.count(curNum&& flagForecast[curNum]) continue;
 796: 
 797:                     TyphoonRecord rec;
 798:                     rec.kind     = L"\u4e88\u5831";
 799:                     rec.dateTime = _XW(dtStr);
 800:                     flag_f = true;
 801: 
 802:                     for (auto& kindIt : info.get_child("Item")) {
 803:                         if (kindIt.first !"Kind"continue;
 804:                         const ptree& kind = kindIt.second;
 805:                         string propTypeStr;
 806:                         try { propTypeStr = kind.get<string>("Property.Type"); } catch (...) { continue; }
 807:                         wstring propType = _XW(propTypeStr);
 808: 
 809:                         if (propType == _SW("階級")) {
 810:                             try {
 811:                                 rec.typhoonClass   = _XW(kind.get<string>("Property.ClassPart.jmx_eb:TyphoonClass",   ""));
 812:                                 rec.areaClass      = _XW(kind.get<string>("Property.ClassPart.jmx_eb:AreaClass",      ""));
 813:                                 rec.intensityClass = _XW(kind.get<string>("Property.ClassPart.jmx_eb:IntensityClass", ""));
 814:                             } catch (...) {}
 815:                         } else if (propType == _SW("中心")) {
 816:                             try {
 817:                                 for (auto& bp : kind.get_child("Property.CenterPart.ProbabilityCircle")) {
 818:                                     if (bp.first !"jmx_eb:BasePoint"continue;
 819:                                     double la, ln;
 820:                                     if (parseCoordinate(bp.second.get_value<string>(), la, ln)) {
 821:                                         rec.latitude  = la;
 822:                                         rec.longitude = ln;
 823:                                     }
 824:                                 }
 825:                                 auto axesCh = kind.get_child_optional(
 826:                                     "Property.CenterPart.ProbabilityCircle.jmx_eb:Axes");
 827:                                 if (axesChrec.forecastCircle = parseAxes(*axesCh);
 828:                             } catch (...) {}
 829:                         } else if (propType == _SW("風")) {
 830:                             try {
 831:                                 for (auto& ws : kind.get_child("Property.WindPart")) {
 832:                                     if (ws.first !"jmx_eb:WindSpeed"continue;
 833:                                     string cond, wtype, unit;
 834:                                     try { cond  = ws.second.get<string>("<xmlattr>.condition"); } catch (...) {}
 835:                                     try { wtype = ws.second.get<string>("<xmlattr>.type"); }      catch (...) {}
 836:                                     try { unit  = ws.second.get<string>("<xmlattr>.unit"); }      catch (...) {}
 837:                                     int v = ws.second.get_value<int>(0);
 838:                                     wstring wtype_w = _XW(wtype);
 839:                                     bool isMaxInstant = (wtype_w.find(_SW("瞬間")) !wstring::npos);
 840:                                     if (!isMaxInstant && !cond.empty() && unit == "m/s"rec.centerWindSpeed = v;
 841:                                     else if (isMaxInstant && unit == "m/s"rec.maxWindSpeed = v;
 842:                                 }
 843:                             } catch (...) {}
 844:                         }
 845:                     }
 846:                     items[curNum].records.push_back(rec);
 847:                 } // else 予報
 848:             } // for infoIt
 849:         } // for infosIt
 850: 
 851:         if (flag_f && !curNum.empty())
 852:             flagForecast[curNum] = true;
 853:     } // for url
 854: 
 855:     delete pCC2;
 856: /**
 857:     // デバッグ:取得した items をダンプ
 858:     {
 859:         ofstream _dbg(".\\typhoonwin_url.txt", ios::app);
 860:         _dbg << "[getTyphoon finished] items=" << items.size() << endl;
 861:         for (auto& kv : items) {
 862:             const TyphoonRecord& r0 = kv.second.records.empty()
 863:                 ? TyphoonRecord() : kv.second.records[0];
 864:             _dbg << "  num=" << _WS(kv.first)
 865:                  << " name=" << _WS(kv.second.nameKana)
 866:                  << " class=" << _WS(r0.typhoonClass)
 867:                  << " valid=" << kv.second.valid
 868:                  << " recs=" << kv.second.records.size() << endl;
 869:         }
 870:         _dbg.close();
 871:     }
 872: **/
 873:     return true;
 874: }

気象庁防災情報XMLからコンテンツを取り込むには、readWebContents で読み込んだXMLファイルを解釈していく。
XMLファイルの構造については、「PHPで直近の地震情報を表示する」をご覧いただきたい。

今回も、ワイド文字列に対する正規表現を使うことにした。ソースはSJISで書いているので、ユーザーマクロ関数 _SW を使ってワイド文字列に変換し、これを使って正規表現によるパターンマッチングを行う。

解説:マップを生成する

pahooGeocode.cpp

 800: /**
 801:  * 地図描画スクリプトを生成する
 802:  * @param   string id        マップID
 803:  * @param   double longitude 中心座標:経度(世界測地系)
 804:  * @param   double latitude  中心座標:緯度(世界測地系)
 805:  * @param   int    zoom      拡大率
 806:  * @param   string type      マップタイプ
 807:  *                              GSISTD:地理院地図(標準):省略時
 808:  *                              GSIPALE:地理院地図(淡色地図)
 809:  *                              GSIBLANK:地理院地図(白地図)
 810:  *                              GSIPHOTO:地理院地図(写真)
 811:  *                              OSM:OpenStreetMap
 812:  *                              GMRD:Googleマップ(ROADMAP);APIキー有効時
 813:  * @param   ppoints_t* items  地点情報配列(省略可能)
 814:  * @param   size_t size       地点情報配列の数(省略可能)
 815:  * @param   string call1      イベント発生時にコールする関数(省略可)
 816:  * @param   string call2      追加スクリプト(省略可)
 817:  * @param   int    max_width  情報ウィンドウの最大幅(省略時:200)
 818:  * @param   int    ofst_x     情報ウィンドウのオフセット位置(X)(省略時:0)
 819:  * @param   int    ofst_y     情報ウィンドウのオフセット位置(Y)(省略時:0)
 820:  * @return  string 生成したスクリプト
 821:  */
 822: string pahooGeocode::makeMapLeaflet(
 823:     string id, double longitude, double latitude, int zoom,
 824:     string type, ppoints_t* items, size_t size, string call1, string call2,
 825:     int max_width, int ofst_x, int ofst_y) {
 826: 
 827:     // 地点情報スクリプトの生成
 828:     char lat[SIZE_BUFF + 1], lng[SIZE_BUFF + 1];
 829:     char buff[SIZE_BUFF + 1];
 830:     string icode = "";
 831:     if (items !NULL) {
 832:         string icon = "";
 833:         string info = "";
 834:         for (size_t i = 0i < sizei++) {
 835:             size_t i2;
 836:             if ((items[i].icon) == "" && (i > 999)) break;  // 最大999箇所まで
 837:             i2 = i;
 838:             if (i2 > 26)    i2 = 26;
 839:             string mark = {(char)(65 + i2)};
 840:             if (items[i].icon == "") {
 841:                 icon = "https://www.google.com/mapfiles/marker" + mark + ".png";
 842:             } else {
 843:                 icon = items[i].icon;
 844:             }
 845:             info = "";
 846:             if (items[i].description !L"") {
 847:                 snprintf(buff, SIZE_BUFF, "', {maxWidth: %d, offset: [%d, %d] });", max_width, ofst_x, ofst_y);
 848:                 info = "marker_" + mark + ".bindPopup('" + _WS(items[i].description+ buff;
 849:             }
 850:             snprintf(lat, SIZE_BUFF, "%.5f", items[i].latitude);
 851:             snprintf(lng, SIZE_BUFF, "%.5f", items[i].longitude);
 852: 
 853:             // アイコン・ラベル
 854:             if (items[i].label !L"") {
 855:                 icode += (boost::format(R"(
 856:                     let icon_%1% =  new L.divIcon({
 857:                         html: '<span style="color:%2%; font-size:%3%px; font-weight:%4%; white-space:nowrap;">%5%</span>',
 858:                         iconSize: [0, 0],
 859:                         iconAnchor: [%3%, %3%],
 860:                     });
 861:                     let marker_%1% = new L.Marker([%6%, %7%], {icon: icon_%1%}).addTo(map);
 862:                     %8%
 863: )")
 864:  %mark                   // マーカー識別子
 865: % items[i].label_color  // アイコン・ラベルの色
 866: % items[i].label_size   // アイコン・ラベルのサイズ
 867: % items[i].label_weight // アイコン・ラベルの太さ
 868: % _WS(items[i].label)   // アイコン・ラベル
 869: % lat                   // 緯度
 870: % lng                   // 経度
 871: % info                  // 情報
 872: ).str();
 873: 
 874:             // 通常アイコン
 875:             } else {
 876:                 icode += (boost::format(R"(
 877:                     let icon_%1% =  new L.icon({
 878:                         iconUrl: '%2%',
 879:                         iconAnchor: [10, 10]    // 暫定
 880:                     });
 881:                     let marker_%1% = new L.Marker([%3%, %4%], {icon: icon_%1%}).addTo(map);
 882:                     %5%
 883: )")
 884:  %mark           // マーカー識別子
 885: % icon          // マーカーURL
 886: % lat           // 緯度
 887: % lng           // 経度
 888: % info          // 情報
 889: ).str();
 890:             }
 891:         }
 892:     }
 893: 
 894:     // 地図描画スクリプトの生成
 895:     string script = (boost::format(R"(
 896: %6%
 897: <link rel="stylesheet" href="https://unpkg.com/leaflet@latest/dist/leaflet.css" />
 898: <script src="https://unpkg.com/leaflet@latest/dist/leaflet.js"></script>
 899: %7%
 900: <script>
 901: window.onload = function() {
 902:     var map = L.map('%1%',{zoomControl:false});
 903:     map.setView([%2%, %3%], %4%);
 904:     L.control.scale({
 905:         maxWidth: 250,
 906:         position: 'bottomright',
 907:         imperial: false
 908:     }).addTo(map);
 909:     L.control.zoom({position:'topleft'}).addTo(map);
 910: 
 911:     // 地理院地図:標準地図
 912:     var GSISTD = new L.tileLayer(
 913:         'https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png',
 914:         {
 915:             attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>",
 916:             minZoom: 0,
 917:             maxZoom: 18,
 918:             name: 'GSISTD'
 919:         });
 920:     // 地理院地図:淡色地図
 921:     var GSIPALE = new L.tileLayer(
 922:         'https://cyberjapandata.gsi.go.jp/xyz/pale/{z}/{x}/{y}.png',
 923:         {
 924:             attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>",
 925:             minZoom: 2,
 926:             maxZoom: 18,
 927:             name: 'GSIPALE'
 928:         });
 929:     // 地理院地図:白地図
 930:     var GSIBLANK = new L.tileLayer(
 931:         'https://cyberjapandata.gsi.go.jp/xyz/blank/{z}/{x}/{y}.png',
 932:         {
 933:             attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>",
 934:             minZoom: 5,
 935:             maxZoom: 14,
 936:             name: 'GSIBLANK'
 937:         });
 938:     // 地理院地図:写真
 939:     var GSIPHOTO = new L.tileLayer(
 940:         'https://cyberjapandata.gsi.go.jp/xyz/seamlessphoto/{z}/{x}/{y}.jpg',
 941:         {
 942:             attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>",
 943:             minZoom: 2,
 944:             maxZoom: 18,
 945:             name: 'GSIPHOTO'
 946:         });
 947:     // OpenStreetMap
 948:     var OSM = new L.tileLayer(
 949:         'https://tile.openstreetmap.jp/{z}/{x}/{y}.png',
 950:         {
 951:             attribution: "<a href='https://osm.org/copyright' target='_blank'>OpenStreetMap</a> contributors",
 952:             minZoom: 0,
 953:             maxZoom: 18,
 954:             name: 'OSM'
 955:         });
 956: %8%
 957: 
 958:     // baseMapsオブジェクトにタイル設定
 959:     var baseMaps = {
 960:         "地理院地図" : GSISTD,
 961:         "淡色地図" : GSIPALE,
 962:         "白地図" : GSIBLANK,
 963:         "写真地図" : GSIPHOTO,
 964:         "オープンストリートマップ" : OSM
 965:         %9%
 966:     };
 967: 
 968:     // layersコントロールにbaseMapsオブジェクトを設定して地図に追加
 969:     L.control.layers(baseMaps).addTo(map);
 970:     %5%.addTo(map);
 971: 
 972:     // イベント追加
 973:     map.on('moveend', getPointData);
 974:     map.on('zoomend', getPointData);
 975:     map.on('baselayerchange', getPointData);
 976: 
 977:     // イベント発生時の地図情報を取得・格納
 978:     function getPointData() {
 979:         var pos = map.getCenter();
 980:         // 経度
 981:         if (document.getElementById('longitude'!null) {
 982:             document.getElementById('longitude').value = pos.lng;
 983:         }
 984:         // 緯度
 985:         if (document.getElementById('latitude'!null) {
 986:             document.getElementById('latitude').value = pos.lat;
 987:         }
 988:         // ズーム
 989:         if (document.getElementById('zoom'!null) {
 990:             document.getElementById('zoom').value = map.getZoom();
 991:         }
 992:         // タイプ
 993:         if (document.getElementById('maptype'!null) {
 994:             for (var k in baseMaps) {
 995:                 if (map.hasLayer(baseMaps[k])) {
 996:                     document.getElementById('maptype').value = baseMaps[k].options.name;
 997:                 }
 998:             }
 999:         }
1000:         %11%
1001:     }
1002:     %10%
1003: }
1004: </script>
1005: )")
1006:  %id                 // 地図ID
1007: % latitude              // 緯度
1008: % longitude             // 経度
1009: % zoom                  // 地図拡大率
1010: % type                  // 地図タイプ
1011: % this->GoogleMap1      // Googleマップ描画用スクリプトURL
1012: % this->GoogleMap2      // Leaftet:Googleマップ・アドオンURL
1013: % this->GoogleMap3      // Leaftet:Googleマップ用レイヤ
1014: % this->GoogleMap4      // Leaftet:Googleマップ選択肢
1015: % icode
1016: % call1
1017: ).str();
1018: 
1019:     return script;
1020: }

地図描画は "pahooGeocode.cpp" に分離し、クラス pahooGeocode のメソッド makeMapLeaflet としている。
このメソッドは、「地理院地図・OSM描画 -PHPで住所・ランドマークから最寄り駅を求める」で紹介した手法をそのまま移植した。無償のJavaScriptライブラリLeafletを利用している。
後述するように、Googleマップが利用できるときには、必要なスクリプトを変数 GoogleMap1GoogleMap4 から追加するようにした。

pahooGeocode.cpp

  35: /**
  36:  * コンストラクタ
  37:  * @param   string appname アプリケーション名
  38:  */
  39: pahooGeocode::pahooGeocode(std::string appname) {
  40:     this->appname = appname;
  41:     this->readGoogleApiKey();
  42:     // ホットペッパーグルメWebサービス APIキー読み込み
  43:     readApiKey(FNAME_YAHOO_API, &this->YahooAPIkey);

クラス pahooGeocode のコンストラクタは、後述する Google Cloud Platform のAPIキーと、今回は使用しないが [Yahoo!JAPANデベロッパーネットワーク] のAPIキーを読み込み、キーが存在していればプロパティに代入し、クラウドサービスで利用できるようにする。

解説:地図描画パラメータ

地図描画のパラメータとして、緯度は Latitude、経度は Longitude、拡大率は Zoom、地図形式は Maptype のグローバル変数に、それぞれ代入している。

typhoonwin.cpp

 194: /**
 195:  * パラメータの初期化
 196:  * @param   なし
 197:  * @return  なし
 198:  */
 199: void initParameter(void) {
 200:     Longitude  = DEF_LONGITUDE;
 201:     Latitude   = DEF_LATITUDE;
 202:     Zoom       = DEF_ZOOM;
 203:     Maptype    = DEF_MAPTYPE;
 204:     hParent_X  = 0;
 205:     hParent_Y  = 0;
 206: }

パラメータは、initParamete によって初期化する。

typhoonwin.cpp

 271: /**
 272:  * パラメータの保存
 273:  * @param   なし
 274:  * @return  なし
 275:  */
 276: void saveParameter(void) {
 277: #ifndef CMDAPP
 278:     // アプリケーション・ウィンドウの位置取得
 279:     WINDOWINFO windowInfo;
 280:     windowInfo.cbSize = sizeof(WINDOWINFO);
 281:     GetWindowInfo(hParent, &windowInfo);
 282:     hParent_X = (unsigned)windowInfo.rcWindow.left;
 283:     hParent_Y = (unsigned)windowInfo.rcWindow.top;
 284:     if (hParent_X >= (unsigned)windowInfo.rcWindow.right) {
 285:         hParent_X = 0;
 286:     }
 287:     if (hParent_Y >= (unsigned)windowInfo.rcWindow.bottom) {
 288:         hParent_Y = 0;
 289:     }
 290: #endif
 291: 
 292:     char lng[SIZE_BUFF + 1], lat[SIZE_BUFF + 1];
 293:     snprintf(lng, SIZE_BUFF, "%.5f", Longitude);
 294:     snprintf(lat, SIZE_BUFF, "%.5f", Latitude);
 295: 
 296:     // XMLファイルへ書き込む
 297:     ptree pt;
 298:     ptree& child1 = pt.add("parameter.param", lat);
 299:     child1.add("<xmlattr>.type", "latitude");
 300:     ptree& child2 = pt.add("parameter.param", lng);
 301:     child2.add("<xmlattr>.type", "longitude");
 302:     ptree& child3 = pt.add("parameter.param", to_string(Zoom));
 303:     child3.add("<xmlattr>.type", "zoom");
 304:     ptree& child4 = pt.add("parameter.param", Maptype);
 305:     child4.add("<xmlattr>.type", "maptype");
 306:     ptree& child5 = pt.add("parameter.param", (string)to_string(hParent_X));
 307:     child5.add("<xmlattr>.type", "wx");
 308:     ptree& child6 = pt.add("parameter.param", (string)to_string(hParent_Y));
 309:     child6.add("<xmlattr>.type", "wy");
 310: 
 311:     const int indent = 4;
 312:     write_xml(getMyPath(APPNAME+ APPNAME + ".xml", pt, std::locale(),
 313:         xml_writer_make_settings<std::string>(' ', indent));
 314: }

地図描画のパラメータは、地図に対する操作で随時変化する。この変化は、上述の地図描画JavaScriptによって、HTMLのINPUT要素の値として代入されている。
ブラウザ・コントロールから、これらの値を取り出し、所定のXMLファイルへ保存するのが saveParameter である。
保存場所は、"C:\Users\(ユーザー名)\AppData\Roaming\pahoo.org\(アプリケーション名)" である。
なお、INPUT要素を取得する方法は、「INPUT要素の取得 - C++で最寄駅を検索」で紹介したとおりだ。

typhoonwin.cpp

 208: /**
 209:  * パラメータの読み込み
 210:  * @param   なし
 211:  * @return  なし
 212:  */
 213: void loadParameter(void) {
 214:     ptree pt;
 215: 
 216:     // 初期値設定
 217:     initParameter();
 218: 
 219:     // XMLファイル読み込み
 220:     try {
 221:         xml_parser::read_xml(getMyPath(APPNAME+ APPNAME + ".xml", pt);
 222: 
 223:         // XML解釈
 224:         try {
 225:             // 形式チェック
 226:             if (optional<string>str = pt.get_optional<string>("parameter")) {
 227:             } else {
 228:                 return;
 229:             }
 230:             // パラメータ読み込み
 231:             for (auto it : pt.get_child("parameter")) {
 232:                 string typeit.second.get_optional<string>("<xmlattr>.type").value();
 233:                 if (type == "latitude") {
 234:                     Latitude = stod(it.second.data());
 235:                 } else if (type == "longitude") {
 236:                     Longitude = stod(it.second.data());
 237:                 } else if (type == "zoom") {
 238:                     Zoom = stoi(it.second.data());
 239:                 } else if (type == "maptype") {
 240:                     Maptype = (string)it.second.data();
 241:                 } else if (type == "wx") {
 242:                     hParent_X = (unsigned)stoi(it.second.data());
 243:                 } else if (type == "wy") {
 244:                     hParent_Y = (unsigned)stoi(it.second.data());
 245:                 }
 246:             }
 247:         // 解釈失敗したら初期値設定
 248:         } catch (xml_parser_error& e) {
 249:             initParameter();
 250:             return;
 251:         }
 252:     // 読み込み失敗したら初期値設定
 253:     } catch (xml_parser_error& e) {
 254:         initParameter();
 255:         return;
 256:     }
 257: 
 258:     // アプリケーション・ウィンドウの位置(デスクトップ範囲外なら原点移動)
 259:     HWND hDesktop = GetDesktopWindow();
 260:     WINDOWINFO windowInfo;
 261:     windowInfo.cbSize = sizeof(WINDOWINFO);
 262:     GetWindowInfo(hDesktop, &windowInfo);
 263:     if (hParent_X >= (unsigned)windowInfo.rcWindow.right) {
 264:         hParent_X = 0;
 265:     }
 266:     if (hParent_Y >= (unsigned)windowInfo.rcWindow.bottom) {
 267:         hParent_Y = 0;
 268:     }
 269: }

アプリケーション起動時に地図描画のパラメータを読み出す関数が loadParameter である。saveParameter によって保存されたXMLファイルがあれば、その値を読み込む。無ければ、initParamete によって初期化する。

解説:マップ表示用HTML生成

typhoonwin.cpp

1199: /**
1200:  * 地図表示用HTML生成
1201:  * @param   なし
1202:  * @return  string 生成したHTML文
1203:  */
1204: static string makeMapHTML(void) {
1205:     // Leaflet ベースマップ + 台風円・経路スクリプト
1206:     string typhoonJs = jsTyphoonMapScript(gTyphoonItems);
1207: 
1208:     // マーカー(台風現在位置)
1209:     string script = pGC->makeMapLeaflet(
1210:         MAP_ID, Longitude, Latitude, Zoom, Maptype,
1211:         pGC->Ppoints, (size_t)gTyphoonCount,
1212:         typhoonJs, "",          // call1 に台風描画スクリプトを挿入
1213:         INFO_WIDTH, INFO_OFFSET_X, INFO_OFFSET_Y);
1214:     // makeMapLeaflet が生成した window.onload 末尾に setTimeout を注入
1215:     {
1216:         size_t pos = script.rfind("}\n</script>");
1217:         if (pos !string::npos) {
1218:             script.insert(pos, "\tsetTimeout(function(){ map.fire('moveend'); }, 200);\n");
1219:         }
1220:     }
1221: 
1222:     return (boost::format(R"(<!DOCTYPE html>
1223: <html lang="ja">
1224: <head>
1225: <meta charset="SJIS">
1226: <title>%1%</title>
1227: <meta name="author" content="studio pahoo" />
1228: <meta name="ROBOTS" content="NOINDEX,NOFOLLOW" />
1229: <meta http-equiv="pragma" content="no-cache">
1230: <meta http-equiv="cache-control" content="no-cache">
1231: %3%
1232: </head>
1233: <body>
1234: <div id="%4%" style="width:%2%px; height:%5%px;"></div>
1235: <form>
1236: <input id="latitude"  type="hidden" value="%6%" />
1237: <input id="longitude" type="hidden" value="%7%" />
1238: <input id="zoom"      type="hidden" value="%8%" />
1239: <input id="maptype"   type="hidden" value="%9%" />
1240: </form>
1241: </body>
1242: </html>
1243: )")
1244:  %APPNAMEJP     // %1  <title>
1245: % MAP_WIDTH     // %2  地図の幅
1246: % script        // %3  Leaflet CSS+JS
1247: % MAP_ID        // %4  地図 div ID
1248: % MAP_HEIGHT    // %5  地図高さ
1249: % Latitude      // %6  hidden latitude
1250: % Longitude     // %7  hidden longitude
1251: % Zoom          // %8  hidden zoom
1252: % Maptype       // %9  hidden maptype
1253: ).str();
1254: }

マップ表示用HTML文を生成するのがユーザー関数 makeMapHTML である。
地震情報を引数にして、前述のメソッド makeMapLeaflet を呼び出してHTML文を生成する。

解説:WebView2コンポーネントを扱う

WebView2コンポーネントを扱うためのファイルとして、jchv / webview2-in-mingw から入手した "event.cpp", "event.h", "WebView2.h" および実行時に "WebView2Loader.dll" を使用する。
また、jchv / webview2-in-mingw のサンプル・プログラムを参考に、"pahooWebView2.cpp", "pahooWebView2.hpp" を用意した。ここでは、"pahooWebView2.cpp" の内容について解説する。

pahooWebView2.cpp

 136: /**
 137:  * WebView2を生成する.
 138:  * @param   HINSTANCE hInst     現在のインターフェイス
 139:  * @param   HWND hDlg           親ウィンドウ・ハンドラ
 140:  * @paramm  int x, y            WebView2の左上座標
 141:  * @paramm  int width, height   WebView2の幅、高さ
 142:  * @param   LPCWSTR uri         表示するURI
 143:  * @return  HWND                WebView2へのハンドラ
 144: */
 145: HWND createWebView2(HINSTANCE hInst, HWND hDlg, int x, int y, int width, int height, LPCWSTR uri) {
 146:     //ウィンドウ クラス情報
 147:     static WNDCLASSEX wc{};
 148:     wc.cbSize = sizeof(WNDCLASSEX);
 149:     wc.hInstance = hInst;
 150:     wc.lpszClassName = TEXT("webview");
 151:     wc.lpfnWndProc = WndProc;
 152:     RegisterClassEx(&wc);
 153: 
 154:     //ウィンドウ生成
 155:     static HWND hWnd = CreateWindowEx(
 156:         0,
 157:         TEXT("webview"),
 158:         TEXT("MinGW WebView2"),
 159:         WS_CHILD | WS_VISIBLE | ES_LEFT,
 160:         x, y, width, height,
 161:         hDlg,
 162:         nullptr,
 163:         hInst,
 164:         nullptr
 165:     );
 166:     ShowWindow(hWnd, SW_SHOW);
 167:     UpdateWindow(hWnd);
 168:     SetFocus(hWnd);
 169: 
 170:     //データ・パス取得
 171:     TCHAR szDataPath[MAX_PATH + 1];
 172:     GetDataPath(szDataPath, MAX_PATH);
 173:     //イベントハンドラ
 174:     static EventHandler handler{};
 175: 
 176:     handler.EnvironmentCompleted = [&](HRESULT result, ICoreWebView2Environment* created_environment) {
 177: //      cout << "EnvironmentCompleted" << endl;
 178:         if (FAILED(result)) {
 179:             FatalError(TEXT("Failed to create environment?"));
 180:         }
 181:         created_environment->lpVtbl->CreateCoreWebView2Controller(created_environment, hWnd, &handler);
 182:         return S_OK;
 183:     };
 184: 
 185:     handler.ControllerCompleted = [&](HRESULT result, ICoreWebView2Controller* new_controller) {
 186: //      cout << "ControllerCompleted" << endl;
 187:         if (FAILED(result)) {
 188:             FatalError(TEXT("Failed to create controller?"));
 189:         }
 190:         controller = new_controller;
 191:         controller->lpVtbl->AddRef(controller);
 192:         controller->lpVtbl->get_CoreWebView2(controller, &webView2);
 193:         webView2->lpVtbl->AddRef(webView2);
 194:         webView2->lpVtbl->Navigate(webView2, uri);
 195:         ResizeBrowser(hWnd);
 196:         webView2Ready = true;
 197:         return S_OK;
 198:     };
 199: 
 200:     HRESULT result = CreateCoreWebView2EnvironmentWithOptions(
 201:         nullptr,
 202:         TStrToWStr(szDataPath).c_str(),
 203:         nullptr,
 204:         &handler
 205:     );
 206: 
 207:     if (FAILED(result)) {
 208:         FatalError(TEXT("Call to CreateCoreWebView2EnvironmentWithOptions failed!"));
 209:     }
 210: 
 211:     return hWnd;
 212: }

ユーザー関数 createWebView2 は、プログラムの冒頭で呼び出すもので、WebView2を利用できるように準備を整える。
まず、ICoreWebView2Environment インターフェースを使って、WebView2を初期化し、WebView2ランタイムの状態管理やイベントハンドリングができるようにする。
次に、ICoreWebView2Controller インターフェースを使って、アプリケーション内に WebView2コントロールを生成する。
いずれも非同期で処理されるため、これらの処理が終了したことをグローバル変数 webView2Ready に代入しておく。
また、これらのインターフェースなどがCライブラリであるため、"pahooWebView2.cpp" は "CINTERFACE" とせざる得ず、上述のようにクラス化することができなかった。

typhoonwin.cpp

1603: /**
1604:  * WebView2:ダイアログを初期化する
1605:  * @param   HWND hDlg   親ウィンドウ・ハンドラ
1606:  * @return  なし
1607: */
1608: static void initDialog(HWND hDlg) {
1609:     HICON hIcon = (HICON)LoadImage(hInst, MAKEINTRESOURCE(IDI_ICON),
1610:                                    IMAGE_ICON, 16, 16, 0);
1611:     SendMessage(hDlg, WM_SETICON, ICON_SMALL, (LPARAM)hIcon);
1612:     ErrorMessage = "";
1613: 
1614:     SetCursor(LoadCursor(NULL, IDC_WAIT));
1615:     loadParameter();
1616:     SetWindowPos(hDlg, NULL, hParent_X, hParent_Y, 0, 0,
1617:                  SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER);
1618: 
1619:     getTempFname(tmpFname);
1620:     size_t wLen = 0;
1621:     mbstowcs_s(&wLen, wUri, MAX_PATH*2, tmpFname, MAX_PATH);
1622:     hWebView2 = createWebView2(hInst, hDlg, 10, 40,
1623:                                MAP_WIDTH + 20, MAP_HEIGHT + 20, wUri);
1624: 
1625:     // Leaflet 疎通確認
1626:     string contentsint httpStatus = 0;
1627:     if (!readWebContents("https://unpkg.com/leaflet@latest/", UserAgent,
1628:                          &contents, &httpStatus)
1629:         || httpStatus < 200 || httpStatus > 299) {
1630:         ErrorMessage = "地図描画ライブラリ(Leaflet)が利用できません";
1631:     }
1632: 
1633:     MSG msg;
1634:     static bool flagWebView2 = false;
1635:     while (GetMessage(&msg, nullptr, 0, 0)) {
1636:         if (msg.message == WM_QUIT) { DestroyWindow(hDlg); }
1637:         TranslateMessage(&msg);
1638:         DispatchMessage(&msg);
1639:         if (!flagWebView2 && isWeb2Ready()) {
1640:             SetCursor(LoadCursor(NULL, IDC_WAIT));
1641:             loadTyphoonData();
1642:             viewBrowser(tmpFname);
1643:             webView2->Reload();
1644:             flagWebView2 = true;
1645:             makeListViewFrame(GetDlgItem(hDlg, IDC_LISTVIEW_TYPHOON));
1646:             makeListView(GetDlgItem(hDlg, IDC_LISTVIEW_TYPHOON));
1647:             makeTitle(hDlg);
1648:         }
1649:     }
1650: 
1651: }

次にメイン・プログラム "earthquakewin.cpp" 側だが、まず、WebView2コントロールを含むダイアログを初期化する関数 initDialog を用意する。
WebView2コントロールで表示するためのHTMLファイルをローカルに用意するため、ユーザー関数 getTempFname を使って、Windowsユーザーの AppDataフォルダにテンポラリファイルを作る。
次に、上述のユーザー関数 createWebView2 を呼び出して WebView2コントロールを用意するのだが、これが非同期処理であるため、メッセージループを用意しなければならない。

typhoonwin.cpp

1276: /**
1277:  * ブラウザ・コントロールを表示
1278:  * @param   wstring info 情報ウィンドウに表示するテキスト
1279:  * @param   char* tmpname 読み込むHTMLファイル名
1280:  * @return  なし
1281: */
1282: static void viewBrowser(const char* tmpname) {
1283:     string html = (ErrorMessage == "" && !pGC->isError())
1284:                   ? makeMapHTML()
1285:                   : makeErrorHTML();
1286:     // WebView2 表示用テンポラリファイルへ書き込む
1287:     ofstream ofs(tmpname);
1288:     ofs << html;
1289:     ofs.close();
1290: 
1291: /**
1292:     // デバッグ用:カレントディレクトリに HTML ファイルを保存する
1293:     {
1294:         ofstream dbg(".\\typhoonwin_debug.html");
1295:         dbg << html;
1296:         dbg.close();
1297:     }
1298: **/
1299: 
1300:     if (isWeb2Ready()) webView2->Reload();
1301: }

WebView2コントロールを、実際に画面に表示するのがユーザー関数 viewBrowser である。

typhoonwin.cpp

1653: /**
1654:  * WebView2のパラメータを取り出す.
1655:  * selectActionの値によって動作を変える。
1656:  * @param   string result   JavaScript終了ハンドラから渡るデータ
1657:  * @return  なし
1658: */
1659: static void execScriptCompleted(string result) {
1660:     vector<string> tokens;
1661:     split(tokens, result, is_any_of(",\""));
1662:     int cnt = 0;
1663:     for (const string& ss : tokens) {
1664:         switch (cnt) {
1665:             case 1: Longitude = stod(ss); break;
1666:             case 2: Latitude  = stod(ss); break;
1667:             case 3: Zoom      = stoi(ss); break;
1668:             case 4: Maptype   = ss;       break;
1669:         }
1670:         cnt++;
1671:     }
1672:     switch (selectAction) {
1673:     case eAction::Finish:
1674:         remove(tmpFname);
1675:         saveParameter();
1676:         EndDialog(hParent, 0);
1677:         DestroyWindow(hParent);
1678:         break;
1679:     default:
1680:         viewBrowser(tmpFname);
1681:         break;
1682:     }
1683: }

解説:地図描画パラメータ」で紹介した、緯度、経度、拡大率、地図形式はマップ上で時々刻々と変化する。これを取得するには、WebView2コントロールと通信しなければならない。IE コントールであればCOM通信(ActiveX)が利用できたのだが、マイクロソフトは脆弱性があるActiveXを廃止してしまったため、WebView2コントロールでは JavaScript を送信し、応答データを非同期で受信しなければならない。
このためのJavaScriptを渡す関数が execScriptCompleted で、ハンドラは scriptCompletedHandler である。取得したいパラメータをカンマ区切りで受け取るスクリプトを渡している。(JSONを使うほどのパラメータ量ではないので😓)

typhoonwin.cpp

1713: /**
1714:  * WebView2のパラメータを保存する.
1715:  * selectActionの値によって動作を変える。
1716:  * @param   なし
1717:  * @return  なし
1718: */
1719: static void execScriptAndAction(void) {
1720:     LPCWSTR js =
1721:         L"document.getElementById('longitude').value + ',' "
1722:      L"+ document.getElementById('latitude').value + ',' "
1723:      L"+ document.getElementById('zoom').value + ',' "
1724:      L"+ document.getElementById('maptype').value;";
1725:  webView2->ExecuteScript(js, scriptCompletedHandler);
1726: }

ハンドラ scriptCompletedHandler が呼び出されるのはプログラム終了時(パラメータを保存する)、もしくは再描画(前回パラメータを参照する)の2種類であるから、グローバル変数 flagFinishProgram で識別する。
カンマ区切りで受け取ったパラメータは Boost C++ の split関数を使って分解し、変数に代入する。
その他の関数、ヘルプファイルやインストーラー作成方法については、これまでの連載で説明してきたとおりである。
なお、WebView2 対応にしたことで64ビット化し、それ以前の32ビット・アプリとインストール場所が変更になることから、インストーラーで MinumumVersion を指定し、メジャー・アップグレード扱いにしてそれ以前のバージョンを削除できるようになっている。

解説:APIキーの管理

Google Cloud Platform - 各種WebAPIの登録方法」で紹介したように、Googleマップを利用するには、Google Cloud PlatformAPIキーをプログラム利用者が取得する必要がある。Google Cloud Platform は利用量によって課金される。2024年(令和6年)5月現在、Googleマップ関連サービスは毎月200ドルまでは無料だが、それ以上の利用量があると課金対象となり、登録したクレジットカードに請求される。
そこで本プログラムでは、ユーザーが APIキーを取得した場合、それをプログラムから入力・保存できるようにするダイアログを用意した。

mystrings.cpp

  31: /**
  32:  * AppDataのパスを取得
  33:  * @param   char* appname アプリケーション名
  34:  * @return  string パス
  35:  */
  36: string getMyPath(const char* appname) {
  37:     static TCHAR myPath[MAX_PATH] = "";
  38: 
  39:     if (strlen(myPath) == 0) {
  40:         if (SHGetSpecialFolderPath(NULL, myPath, CSIDL_APPDATA, 0)) {
  41:             TCHAR *ptmp = _tcsrchr(myPath, _T('\\'));
  42:             if (ptmp !NULL) {
  43:                 ptmp = _tcsinc(ptmp);
  44:                 *ptmp = _T('\0');
  45:             }
  46:             strcat(myPath, _T("Roaming"));
  47:             CreateDirectory((LPCTSTR)myPath, NULL);
  48:             strcat(myPath, _T("\\pahoo.org"));
  49:             CreateDirectory((LPCTSTR)myPath, NULL);
  50:             strcat(myPath, _T("\\"));
  51:             strcat(myPath, _T(appname));
  52:             CreateDirectory((LPCTSTR)myPath, NULL);
  53:             strcat(myPath, _T("\\"));
  54:         } else {
  55:         }
  56:     }
  57:     return (string)myPath;
  58: }

ユーザー関数 getMyPath は、APIキーを保存するフォルダを取得する。ログインユーザーのUserフォルダの下、"\Roaming\pahoo.org" に格納する。この関数は "mystrings.cpp" にある。

apikey.cpp

  66: /**
  67:  * APIキーを書き込む
  68:  * @param   string fname    書き込むファイル名(パスを除く)
  69:  * @param   string key      書き込むAPIキー
  70:  * @return  bool TRUE:書込成功/FALSE:失敗
  71:  */
  72: bool writeApiKey(std::string fname, std::string key) {
  73:     bool ret = TRUE;
  74:     ofstream ofs;
  75: 
  76:     ofs.open((string)getMyPath(NULL+ fname);
  77:     ofs << key;
  78:     if(ofs.bad()) {
  79:         ret = FALSE;
  80:     }
  81:     ofs.close();
  82: 
  83:     return ret;
  84: }

APIキーにかかわる関数群は "apikey.cpp" にある。
ユーザー関数 writeApiKey は、ユーザー関数 getMyPath で指定するフォルダに、変数 key に格納したAPIキーを、ファイル名 "fname" で保存する。保存したAPIキーを、valKey で指定する変数に格納する。

apikey.cpp

  34: /**
  35:  * APIキーを読み込む
  36:  * @param   string fname 読み込むファイル名(パスを除く)
  37:  * @param   string *key  APIキーを格納する変数
  38:  * @return  bool TRUE:読込成功/FALSE:ファイルがない
  39:  */
  40: bool readApiKey(std::string fname, std::string* key) {
  41:     string readKey;
  42:     bool ret = FALSE;
  43: 
  44:     ifstream ifs((string)getMyPath(NULL+ fname);
  45:     if (!ifs)   return ret;
  46: 
  47:     ifs >> readKey;
  48:     if(ifs.bad()) {
  49:         ifs.close();
  50:         return FALSE;
  51:     }
  52:     ifs.close();
  53: 
  54:     //APIキーを格納する
  55:     if (readKey.length() > 0) {
  56:         *key = readKey;
  57:         ret = TRUE;
  58:     //APIキーを削除する
  59:     } else {
  60:         *key = "";
  61:     }
  62: 
  63:     return ret;
  64: }

ユーザー関数 readApiKey は、ユーザー関数 getMyPath で指定するフォルダから、ファイル名 "fname" で保存されたAPIキーを読み出し、key で指定する変数に格納する。

pahooGeocode.cpp

 106: /**
 107:  * Google Cloud Platform APIキーを読み込む
 108:  * @param   なし
 109:  * @return  bool TRUE:読込成功/FALSE:ファイルがない
 110:  */
 111: bool pahooGeocode::readGoogleApiKey(void) {
 112:     string key;
 113:     bool ret = FALSE;
 114: 
 115:     ifstream ifs((string)getMyPath(this->appname.c_str()) + FNAME_GOOGLE_API);
 116:     // APIキー・ファイルが無ければ初期化
 117:     if (!ifs) {
 118:         this->GoogleAPIkey = "";
 119:         this->GoogleMap1 = "";
 120:         this->GoogleMap2 = "";
 121:         this->GoogleMap3 = "";
 122:         this->GoogleMap4 = "";
 123:         return ret;
 124:     }
 125: 
 126:     // APIキー読み込み
 127:     ifs >> key;
 128:     if(ifs.bad()) {
 129:         this->errmsg = _SW("Google Cloud Platform APIキーの読み込みに失敗しました");
 130:         ifs.close();
 131:         this->GoogleAPIkey = "";
 132:         this->GoogleMap1 = "";
 133:         this->GoogleMap2 = "";
 134:         this->GoogleMap3 = "";
 135:         this->GoogleMap4 = "";
 136:         return FALSE;
 137:     }
 138:     ifs.close();
 139: 
 140:     // APIキーなどの設定
 141:     if (key.length() > 0) {
 142:         this->GoogleAPIkey = key;
 143:         this->GoogleMap1 = "<script src='https://maps.googleapis.com/maps/api/js?key="
 144:                 + key + "' async defer></script>";
 145:         this->GoogleMap2 =
 146:                 "<script src='https://unpkg.com/leaflet.gridlayer.googlemutant@0.10.2/Leaflet.GoogleMutant.js'></script>";
 147:         this->GoogleMap3 =
 148:                 "var GMRD = L.gridLayer.googleMutant({type:'roadmap', name:'GMRD'});\nvar GMST = L.gridLayer.googleMutant({type:'satellite', name:'GMST'});\nvar GMHB = L.gridLayer.googleMutant({type:'hybrid', name:'GMHB'});";
 149:         this->GoogleMap4 = ",'Googleマップ(標準)' : GMRD,\n'Googleマップ(写真)' : GMST,\n'Googleマップ(混合)' : GMHB";
 150:         ret = TRUE;
 151:         // APIキーが無ければ初期化
 152:     } else {
 153:         this->GoogleAPIkey = "";
 154:         this->GoogleMap1 = "";
 155:         this->GoogleMap2 = "";
 156:         this->GoogleMap3 = "";
 157:         this->GoogleMap4 = "";
 158:     }
 159:     return ret;
 160: }

ただし、本プログラムで Google Cloud Platform を読み込む処理は、"pahooGeocode.cpp" にメソッド readGoogleApiKey として用意した。
これは、前述の makeMapLeaflet メソッドにGoogleマップを追加するためのスクリプトを変数 GoogleMap1GoogleMap4 に代入する必要があるためである。

共通手順、モジュールなど

その他の関数、ヘルプファイルやインストーラー作成方法については、これまでの連載で説明してきたとおりである。

参考サイト

(この項おわり)
header