目次
サンプル・プログラム
| typhoonwin.msi | インストーラ |
| bin/typhoonwin.exe | 実行プログラム本体 |
|
bin/WebView2Loader.dll bin/libcurl-x64.dll | 実行時に必要になるDLL |
| bin/cacert.pem | cURL通信で必要になる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.cpp | APIキーの管理(ソース) |
| sour/apikey.hpp | APIキーの管理(ヘッダ) |
| sour/WebView2.h | WebView2に関わるヘッダ |
| sour/event.h | WebView2用インターフェース(ヘッダ) |
| sour/event.cpp | WebView2用インターフェース(ソース) |
| sour/pahooWebView2.cpp | WebView2に関わる関数(ソース) |
| sour/pahooWebView2.hpp | WebView2に関わる関数(ヘッダ) |
| sour/pahooCache.cpp | キャッシュ処理に関わるクラス(ソース) |
| sour/pahooCache.hpp | キャッシュ処理に関わるクラス(ヘッダ) |
| sour/makefile | ビルド |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 1.0.0 | 2026/06/20 | 初版 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 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()関数に変更 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 1.1.0 | 2025/03/16 | 不具合修正 |
| 1.0 | 2021/04/14 | 初版 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 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追加 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 1.1.0 | 2025/06/07 | TStrToWStr() -- C++17 対応 |
| 1.0.0 | 2024/04/27 | 初版 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 2.0.0 | 2024/04/29 | createSetAPIkey, processSetAPIkey に統合 |
| 1.0 | 2020/09/30 | 初版 |
使用ライブラリ
また、地図表示にWebブラウザ・コントロールを利用するため "WebView2Loader.dll" を利用する。jchv / webview2-in-mingw からダウンロードできる。
リソースの準備
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) =========================================================
構造体 TyphoonInfo の中には、これまでの位置情報などを格納した構造体 TyphoonRecord が複数格納している。これによって、いままでの台風の経路を地図上に描くことができる。
解説:キャッシュ・システム
- Atomフィード:高頻度フィード:随時
- Atomフィード:長期フィード:随時
- VPTW60
このキャッシュ・システムの仕組みについては、「解説: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: }
解説:台風情報取得
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 (!axesCh) continue;
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 (axesCh) rec.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ファイルの構造については、「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 = 0; i < size; i++) {
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: }
このメソッドは、「地理院地図・OSM描画 -PHPで住所・ランドマークから最寄り駅を求める」で紹介した手法をそのまま移植した。無償のJavaScriptライブラリLeafletを利用している。
後述するように、Googleマップが利用できるときには、必要なスクリプトを変数 GoogleMap1~GoogleMap4 から追加するようにした。
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);
解説:地図描画パラメータ
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: }
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: }
ブラウザ・コントロールから、これらの値を取り出し、所定の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 type= it.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: }
解説:マップ表示用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: }
解説:WebView2コンポーネントを扱う
また、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: }
まず、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 contents; int 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: }
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: }
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: }
このための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: }
カンマ区切りで受け取ったパラメータは Boost C++ の split関数を使って分解し、変数に代入する。
なお、WebView2 対応にしたことで64ビット化し、それ以前の32ビット・アプリとインストール場所が変更になることから、インストーラーで MinumumVersion を指定し、メジャー・アップグレード扱いにしてそれ以前のバージョンを削除できるようになっている。
解説:APIキーの管理
そこで本プログラムでは、ユーザーが 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: }
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: }
ユーザー関数 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: }
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: }
これは、前述の makeMapLeaflet メソッドにGoogleマップを追加するためのスクリプトを変数 GoogleMap1~GoogleMap4 に代入する必要があるためである。
共通手順、モジュールなど
- C++ 開発環境の準備:ぱふぅ家のホームページ
- C++ 開発環境の準備 -MSYS2編-
- WiX によるWindowsインストーラー作成:ぱふぅ家のホームページ
- C++ でダイアログボックスを使う
- C++ でイベント駆動型アプリを作る
- 解説:ニュース一覧作成|C++ で Googleニュース検索
- 解説:検索結果をCSVファイルに保存|C++ で Googleニュース検索
- 解説:クリップボード|C++ でパスワード生成機を作る
- 解説:APIキーの管理|C++ で直近の地震情報を取得する
- 解説:WebView2|C++ で直近の地震情報を取得する
- 解説:解説:マップ表示用HTML生成|C++ で直近の地震情報を取得する
参考サイト
- PHPで台風情報を取得する:ぱふぅ家のホームページ

地図としては、地理院地図、オープンストリートマップのほか、Edgeブラウザに対応し64ビットアプリ化したことで、Googleマップが再び利用できるようになった。Googleマップ利用には、ユーザーにおいてGoogle Cloud APIキーを取得してほしい。