
目次
サンプル・プログラム
earthquakewin.msi | インストーラ |
bin/earthquakewin.exe | 実行プログラム本体 |
bin/WebView2Loader.dll bin/libcurl-x64.dll | 実行時に必要になるDLL |
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 | ビルド |
バージョン | 更新日 | 内容 |
---|---|---|
4.2.0 | 2025/03/15 | Leafletアクセス可否チェック追加,使用ライブラリ更新 |
4.1.2 | 2024/11/23 | 使用ライブラリ更新 |
4.1.1 | 2024/08/17 | 使用ライブラリ更新 |
4.1.0 | 2024/05/03 | enum eActionクラス導入,API入力処理を改良 |
4.0.0 | 2024/04/27 | Edgeブラウザ対応,64ビット対応 |
バージョン | 更新日 | 内容 |
---|---|---|
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.7.0 | 2024/04/27 | Edge対応に伴いデバッグ用コードを廃棄 |
1.6.0 | 2023/07/02 | getPointsGSI()メソッド追加 |
バージョン | 更新日 | 内容 |
---|---|---|
1.1.0 | 2025/03/16 | 不具合修正 |
1.0 | 2021/04/14 | 初版 |
バージョン | 更新日 | 内容 |
---|---|---|
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.11 | 2020/10/17 | htmlspecialchars() 追加 |
バージョン | 更新日 | 内容 |
---|---|---|
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 を起動したら、新規プロジェクト earthquakewin を用意する。
ResEdit を起動したら、resource.rc を用意する。
Eclipse に戻り、ソース・プログラム "earthquakewin.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" を利用してほしい。
解説:定数など
earthquakewin.cpp
45: // 定数など ==================================================================
46: #define MAKER "pahoo.org" // 作成者
47: #define APPNAME "earthquakewin" // アプリケーション名
48: #define APPNAMEJP "最近の地震情報" // アプリケーション名(日本語)
49: #define APPVERSION "4.2.0" // バージョン
50: #define APPYEAR "2020-25" // 作成年
51: #define REFERENCE "https://www.pahoo.org/e-soul/webtech/cpp01/cpp01-12-01.shtm" // 参考サイト
52:
53: // ListViewItemの最大文字長【変更不可】
54: #define MAX_LISTVIEWITEM 259
55:
56: // ヘルプ・ファイル
57: #define HELPFILE ".\\etc\\help.chm"
58:
59: // デフォルト保存ファイル名
60: #define SAVEFILE "eq_%04d%02d%02d_%02d%02d.csv"
61:
62: // キャッシュ・ディレクトリ
63: #define DIR_CACHE_FEED "pcache1\\"
64: #define DIR_CACHE_FEED_L "pcache2\\"
65: #define DIR_CACHE_DATA "pcache3\\"
66:
67: // キャッシュ保持時間(デフォルト;分)(0:キャッシュしない)
68: #define LIFE_CACHE_FEED 5 // 高頻度フィードに対して
69: #define LIFE_CACHE_FEED_L 120 // 長期フィードに対して
70: #define LIFE_CACHE_DATA 720 // 地震情報に対して
71:
72: // 気象庁防災情報XML:高頻度フィード - 地震火山【変更不可】
73: #define FEED_EVOL "https://www.data.jma.go.jp/developer/xml/feed/eqvol.xml"
74:
75: // 気象庁防災情報XML:長期フィード - 地震火山【変更不可】
76: #define FEED_EVOL_L "https://www.data.jma.go.jp/developer/xml/feed/eqvol_l.xml"
77:
78: // マップID
79: #define MAP_ID "map_id"
80: // 地図の大きさ
81: #define MAP_WIDTH 600 // 地図の幅(ピクセル)
82: #define MAP_HEIGHT 400 // 地図の高さ(ピクセル)
83: // 経度・緯度(初期値)
84: #define DEF_LONGITUDE 139.766667
85: double Longitude = DEF_LONGITUDE;
86: #define DEF_LATITUDE 35.681111
87: double Latitude = DEF_LATITUDE;
88: // 地図拡大率(初期値)
89: #define DEF_ZOOM 6
90: int Zoom = DEF_ZOOM;
91: // 地図の種類(初期値)
92: #define DEF_MAPTYPE "GSISTD"
93: string Maptype = DEF_MAPTYPE;
94: #define INFO_WIDTH (int)(MAP_WIDTH * 0.75) // 情報ウィンドウの最大幅
95: #define INFO_OFFSET_X 0 // 情報ウィンドウのオフセット位置(X)
96: #define INFO_OFFSET_Y -10 // 情報ウィンドウのオフセット位置(Y)
97:
98: // 地震情報の最大格納数
99: #define MAX_EARTHQUAKE 100
解説:データ構造
earthquakewin.cpp
144: // 地震情報を格納する構造体
145: struct _Earthquake {
146: int id = 0; // マッピングID
147: int year = 0; // 西暦年
148: int month = 0; // 月
149: int day = 0; // 日
150: int hour = 0; // 時
151: int minuite = 0; // 分
152: wstring location = L""; // 震源地
153: double latitude = 0.0; // 緯度
154: double longitude = 0.0; // 経度
155: double depth = 0.0; // 深さ
156: double magnitude = -1.0; // マグニチュード
157: int maxintensity = -1; // 最大震度
158: } Earthquake[MAX_EARTHQUAKE];
解説:キャッシュ・システム
- Atomフィード:高頻度フィード:地震火山
- Atomフィード:長期フィード:地震火山
- VXSE53
このキャッシュ・システムの仕組みについては、「解説:pahooCacheクラスとデータ構造 - C++ で週間天気予報を表示する」をご覧いただきたい。
earthquakewin.cpp
62: // キャッシュ・ディレクトリ
63: #define DIR_CACHE_FEED "pcache1\\"
64: #define DIR_CACHE_FEED_L "pcache2\\"
65: #define DIR_CACHE_DATA "pcache3\\"
66:
67: // キャッシュ保持時間(デフォルト;分)(0:キャッシュしない)
68: #define LIFE_CACHE_FEED 5 // 高頻度フィードに対して
69: #define LIFE_CACHE_FEED_L 120 // 長期フィードに対して
70: #define LIFE_CACHE_DATA 720 // 地震情報に対して
配布ファイルは、新しい地震情報が入ってくることを考え、フィードの方を短く、地震情報の方は長くキャッシュ保持時間を設定してある。
解説:ワイド文字列中の改行を他の文字列に置換
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: }
解説:地震情報取得
earthquakewin.cpp
499: /**
500: * 地震情報取得(気象庁防災情報XMLから)
501: * @param なし
502: * @return bool TRUE:取得成功/FALSE:失敗
503: */
504: bool getEarthquake(void) {
505: // 年月日時分
506: regex re1("([0-9]+)\\-([0-9]+)\\-([0-9]+)T([0-9]+)\\:([0-9]+)");
507: // 緯度・経度・深さ
508: regex re2("([\\+\\-][0-9\\.]+)([\\+\\-][0-9\\.]+)([\\-\\+\\/])([0-9]*)");
509: smatch mt1, mt2;
510: double lng, lat;
511: pahooCache* pCC; // pahooCacheオブジェクト
512:
513: // 地震情報URLを取得
514: static string urls[MAX_EARTHQUAKE];
515: bool res = jma_getLastEarthquakeURLs(urls);
516: if (res == FALSE) {
517: return FALSE;
518: }
519:
520: static string contents = "";
521: pCC = new pahooCache(LIFE_CACHE_DATA, getMyPath(APPNAME) + DIR_CACHE_DATA, UserAgent);
522: for (int i = 0; i < MAX_EARTHQUAKE; i++) {
523: if (urls[i] == "") {
524: // cout << "count = " << i << endl;
525: break;
526: }
527: int httpStatus = 0;
528: readWebContents(urls[i], UserAgent, &contents, &httpStatus);
529: if (pCC->load(urls[i], &contents) == FALSE) {
530: ErrorMessage = _WS(pCC->getError());
531: return FALSE;
532: }
533: // XML読み込み
534: try {
535: std::stringstream ss;
536: ss << contents;
537: ptree pt;
538: xml_parser::read_xml(ss, pt);
539:
540: // XML解釈
541: // 地震発生日時の取得
542: if (optional<string>str = pt.get_optional<string>("Report.Body.Earthquake.OriginTime")) {
543: // cout << str.value() << " ";
544: if (regex_search(str.value(), mt1, re1)) {
545: Earthquake[i].year = stoi(mt1[1].str());
546: Earthquake[i].month = stoi(mt1[2].str());
547: Earthquake[i].day = stoi(mt1[3].str());
548: Earthquake[i].hour = stoi(mt1[4].str());
549: Earthquake[i].minuite = stoi(mt1[5].str());
550: }
551: }
552: // 震源地の取得
553: if (optional<string>str = pt.get_optional<string>("Report.Body.Earthquake.Hypocenter.Area.Name")) {
554: Earthquake[i].location = _UW(str.value());
555: // cout << _WS(Earthquake[i].location) << " ";
556: }
557: if (optional<string>str = pt.get_optional<string>("Report.Body.Earthquake.Hypocenter.Area.jmx_eb:Coordinate")) {
558: if (regex_search(str.value(), mt2, re2)) {
559: pGC->tokyo_wgs84(stod(mt2[2].str()), stod(mt2[1].str()), &lng, &lat);
560: Earthquake[i].latitude = lat;
561: Earthquake[i].longitude = lng;
562: if (mt2[3].str() == "/") {
563: Earthquake[i].depth = (-1.0);
564: } else {
565: Earthquake[i].depth = stod(mt2[4].str());
566: }
567: // cout << "lat=" << lat << " lng=" << lng << " ";
568: }
569: }
570: // マグニチュードの取得
571: if (optional<string>str = pt.get_optional<string>("Report.Body.Earthquake.jmx_eb:Magnitude")) {
572: Earthquake[i].magnitude = stod(str.value());
573: // cout << "M=" << Earthquake[i].magnitude << " ";
574: }
575: // 最大震度の取得
576: if (optional<string>str = pt.get_optional<string>("Report.Body.Intensity.Observation.MaxInt")) {
577: Earthquake[i].maxintensity = stoi(str.value());
578: // cout << "max=" << Earthquake[i].maxintensity << endl;
579: }
580: // 読み込みエラー
581: } catch(xml_parser_error& e) {
582: ErrorMessage = "気象庁防災情報XMLにアクセスできません";
583: return FALSE;
584: }
585: contents.clear();
586: }
587: delete pCC;
588:
589: return TRUE;
590: }
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: if ((items[i].icon) == "" && (i > 25)) break;
836: // アイコンURLなく 'Z'を超えたら打ち止め
837: string mark = {(char)(65 + i)};
838: if (items[i].icon == "") {
839: icon = "https://www.google.com/mapfiles/marker" + mark + ".png";
840: } else {
841: icon = items[i].icon;
842: }
843: info = "";
844: if (items[i].description != L"") {
845: snprintf(buff, SIZE_BUFF, "', {maxWidth: %d, offset: [%d, %d] });", max_width, ofst_x, ofst_y);
846: info = "marker_" + mark + ".bindPopup('" + _WS(items[i].description) + buff;
847: }
848: snprintf(lat, SIZE_BUFF, "%.5f", items[i].latitude);
849: snprintf(lng, SIZE_BUFF, "%.5f", items[i].longitude);
850: icode += (boost::format(R"(
851: var icon_%1% = new L.icon({
852: iconUrl: '%2%',
853: iconAnchor: [10, 10] // 暫定
854: });
855: var marker_%1% = new L.Marker([%3%, %4%], {icon: icon_%1%}).addTo(map);
856: %5%
857: )")
858: %mark // マーカー識別子
859: % icon // マーカーURL
860: % lat // 緯度
861: % lng // 経度
862: % info // 情報
863: ).str();
864: }
865: }
866:
867: // 地図描画スクリプトの生成
868: string script = (boost::format(R"(
869: %6%
870: <link rel="stylesheet" href="https://unpkg.com/leaflet@latest/dist/leaflet.css" />
871: <script src="https://unpkg.com/leaflet@latest/dist/leaflet.js"></script>
872: %7%
873: <script>
874: window.onload = function() {
875: var map = L.map('%1%',{zoomControl:false});
876: map.setView([%2%, %3%], %4%);
877: L.control.scale({
878: maxWidth: 250,
879: position: 'bottomright',
880: imperial: false
881: }).addTo(map);
882: L.control.zoom({position:'topleft'}).addTo(map);
883:
884: // 地理院地図:標準地図
885: var GSISTD = new L.tileLayer(
886: 'https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png',
887: {
888: attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>",
889: minZoom: 0,
890: maxZoom: 18,
891: name: 'GSISTD'
892: });
893: // 地理院地図:淡色地図
894: var GSIPALE = new L.tileLayer(
895: 'https://cyberjapandata.gsi.go.jp/xyz/pale/{z}/{x}/{y}.png',
896: {
897: attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>",
898: minZoom: 2,
899: maxZoom: 18,
900: name: 'GSIPALE'
901: });
902: // 地理院地図:白地図
903: var GSIBLANK = new L.tileLayer(
904: 'https://cyberjapandata.gsi.go.jp/xyz/blank/{z}/{x}/{y}.png',
905: {
906: attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>",
907: minZoom: 5,
908: maxZoom: 14,
909: name: 'GSIBLANK'
910: });
911: // 地理院地図:写真
912: var GSIPHOTO = new L.tileLayer(
913: 'https://cyberjapandata.gsi.go.jp/xyz/seamlessphoto/{z}/{x}/{y}.jpg',
914: {
915: attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>",
916: minZoom: 2,
917: maxZoom: 18,
918: name: 'GSIPHOTO'
919: });
920: // OpenStreetMap
921: var OSM = new L.tileLayer(
922: 'https://tile.openstreetmap.jp/{z}/{x}/{y}.png',
923: {
924: attribution: "<a href='https://osm.org/copyright' target='_blank'>OpenStreetMap</a> contributors",
925: minZoom: 0,
926: maxZoom: 18,
927: name: 'OSM'
928: });
929: %8%
930:
931: // baseMapsオブジェクトにタイル設定
932: var baseMaps = {
933: "地理院地図" : GSISTD,
934: "淡色地図" : GSIPALE,
935: "白地図" : GSIBLANK,
936: "写真地図" : GSIPHOTO,
937: "オープンストリートマップ" : OSM
938: %9%
939: };
940:
941: // layersコントロールにbaseMapsオブジェクトを設定して地図に追加
942: L.control.layers(baseMaps).addTo(map);
943: %5%.addTo(map);
944:
945: // イベント追加
946: map.on('moveend', getPointData);
947: map.on('zoomend', getPointData);
948: map.on('baselayerchange', getPointData);
949:
950: // イベント発生時の地図情報を取得・格納
951: function getPointData() {
952: var pos = map.getCenter();
953: // 経度
954: if (document.getElementById('longitude') != null) {
955: document.getElementById('longitude').value = pos.lng;
956: }
957: // 緯度
958: if (document.getElementById('latitude') != null) {
959: document.getElementById('latitude').value = pos.lat;
960: }
961: // ズーム
962: if (document.getElementById('zoom') != null) {
963: document.getElementById('zoom').value = map.getZoom();
964: }
965: // タイプ
966: if (document.getElementById('maptype') != null) {
967: for (var k in baseMaps) {
968: if (map.hasLayer(baseMaps[k])) {
969: document.getElementById('maptype').value = baseMaps[k].options.name;
970: }
971: }
972: }
973: %11%
974: }
975: %10%
976: }
977: </script>
978: )")
979: %id // 地図ID
980: % latitude // 緯度
981: % longitude // 経度
982: % zoom // 地図拡大率
983: % type // 地図タイプ
984: % this->GoogleMap1 // Googleマップ描画用スクリプトURL
985: % this->GoogleMap2 // Leaftet:Googleマップ・アドオンURL
986: % this->GoogleMap3 // Leaftet:Googleマップ用レイヤ
987: % this->GoogleMap4 // Leaftet:Googleマップ選択肢
988: % icode
989: % call1
990: ).str();
991:
992: return script;
993: }
このメソッドは、「地理院地図・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);
解説:地図描画パラメータ
earthquakewin.cpp
161: /**
162: * パラメータの初期化
163: * @param なし
164: * @return なし
165: */
166: void initParameter(void) {
167: Longitude = DEF_LONGITUDE;
168: Latitude = DEF_LATITUDE;
169: Zoom = DEF_ZOOM;
170: Maptype = DEF_MAPTYPE;
171: hParent_X = 0;
172: hParent_Y = 0;
173: }
earthquakewin.cpp
238: /**
239: * パラメータの保存
240: * @param なし
241: * @return なし
242: */
243: void saveParameter(void) {
244: #ifndef CMDAPP
245: // アプリケーション・ウィンドウの位置取得
246: WINDOWINFO windowInfo;
247: windowInfo.cbSize = sizeof(WINDOWINFO);
248: GetWindowInfo(hParent, &windowInfo);
249: hParent_X = (unsigned)windowInfo.rcWindow.left;
250: hParent_Y = (unsigned)windowInfo.rcWindow.top;
251: if (hParent_X >= (unsigned)windowInfo.rcWindow.right) {
252: hParent_X = 0;
253: }
254: if (hParent_Y >= (unsigned)windowInfo.rcWindow.bottom) {
255: hParent_Y = 0;
256: }
257: #endif
258:
259: char lng[SIZE_BUFF + 1], lat[SIZE_BUFF + 1];
260: snprintf(lng, SIZE_BUFF, "%.5f", Longitude);
261: snprintf(lat, SIZE_BUFF, "%.5f", Latitude);
262:
263: // XMLファイルへ書き込む
264: ptree pt;
265: ptree& child1 = pt.add("parameter.param", lat);
266: child1.add("<xmlattr>.type", "latitude");
267: ptree& child2 = pt.add("parameter.param", lng);
268: child2.add("<xmlattr>.type", "longitude");
269: ptree& child3 = pt.add("parameter.param", to_string(Zoom));
270: child3.add("<xmlattr>.type", "zoom");
271: ptree& child4 = pt.add("parameter.param", Maptype);
272: child4.add("<xmlattr>.type", "maptype");
273: ptree& child5 = pt.add("parameter.param", (string)to_string(hParent_X));
274: child5.add("<xmlattr>.type", "wx");
275: ptree& child6 = pt.add("parameter.param", (string)to_string(hParent_Y));
276: child6.add("<xmlattr>.type", "wy");
277:
278: const int indent = 4;
279: write_xml(getMyPath(APPNAME) + APPNAME + ".xml", pt, std::locale(),
280: xml_writer_make_settings<std::string>(' ', indent));
281: }
ブラウザ・コントロールから、これらの値を取り出し、所定のXMLファイルへ保存するのが saveParameter である。
保存場所は、"C:\Users\(ユーザー名)\AppData\Roaming\pahoo.org\(アプリケーション名)" である。
なお、INPUT要素を取得する方法は、「INPUT要素の取得 - C++で最寄駅を検索」で紹介したとおりだ。
earthquakewin.cpp
175: /**
176: * パラメータの読み込み
177: * @param なし
178: * @return なし
179: */
180: void loadParameter(void) {
181: ptree pt;
182:
183: // 初期値設定
184: initParameter();
185:
186: // XMLファイル読み込み
187: try {
188: xml_parser::read_xml(getMyPath(APPNAME) + APPNAME + ".xml", pt);
189:
190: // XML解釈
191: try {
192: // 形式チェック
193: if (optional<string>str = pt.get_optional<string>("parameter")) {
194: } else {
195: return;
196: }
197: // パラメータ読み込み
198: for (auto it : pt.get_child("parameter")) {
199: string type= it.second.get_optional<string>("<xmlattr>.type").value();
200: if (type == "latitude") {
201: Latitude = stod(it.second.data());
202: } else if (type == "longitude") {
203: Longitude = stod(it.second.data());
204: } else if (type == "zoom") {
205: Zoom = stoi(it.second.data());
206: } else if (type == "maptype") {
207: Maptype = (string)it.second.data();
208: } else if (type == "wx") {
209: hParent_X = (unsigned)stoi(it.second.data());
210: } else if (type == "wy") {
211: hParent_Y = (unsigned)stoi(it.second.data());
212: }
213: }
214: // 解釈失敗したら初期値設定
215: } catch (xml_parser_error& e) {
216: initParameter();
217: return;
218: }
219: // 読み込み失敗したら初期値設定
220: } catch (xml_parser_error& e) {
221: initParameter();
222: return;
223: }
224:
225: // アプリケーション・ウィンドウの位置(デスクトップ範囲外なら原点移動)
226: HWND hDesktop = GetDesktopWindow();
227: WINDOWINFO windowInfo;
228: windowInfo.cbSize = sizeof(WINDOWINFO);
229: GetWindowInfo(hDesktop, &windowInfo);
230: if (hParent_X >= (unsigned)windowInfo.rcWindow.right) {
231: hParent_X = 0;
232: }
233: if (hParent_Y >= (unsigned)windowInfo.rcWindow.bottom) {
234: hParent_Y = 0;
235: }
236: }
解説:マップ表示用HTML生成
earthquakewin.cpp
727: /**
728: * 地図表示用HTML生成
729: * @param string 表示するインフォメーション
730: * @return string 生成したHTML文
731: */
732: string makeMapHTML(wstring info) {
733: int cnt = info2points();
734: string script = pGC->makeMapLeaflet(MAP_ID, Earthquake[0].longitude, Earthquake[0].latitude, Zoom, Maptype, pGC->Ppoints, cnt, "", "", INFO_WIDTH, INFO_OFFSET_X, INFO_OFFSET_Y);
735:
736: string html = (boost::format(R"(<!DOCTYPE html>
737: <html lang="ja">
738: <head>
739: <meta charset="SJIS">
740: <title>%9%</title>
741: <meta name="author" content="studio pahoo" />
742: <meta name="copyright" content="studio pahoo" />
743: <meta name="ROBOTS" content="NOINDEX,NOFOLLOW" />
744: <meta http-equiv="pragma" content="no-cache">
745: <meta http-equiv="cache-control" content="no-cache">
746: <meta http-equiv="X-UA-Compatible" content="IE=edge">
747: %8%
748: </head>
749: <body>
750: <div id="%1%" style="width:%2%px; height:%3%px;"></div>
751: <form>
752: <input id="latitude" type="hidden" value="%4%" />
753: <input id="longitude" type="hidden" value="%5%" />
754: <input id="zoom" type="hidden" value="%6%" />
755: <input id="maptype" type="hidden" value="%7%" />
756: </form>
757: </body>
758: </html>
759: )")
760: %MAP_ID // 地図ID
761: % MAP_WIDTH // 地図の幅
762: % MAP_HEIGHT // 地図の高さ
763: % Earthquake[0].latitude // 緯度
764: % Earthquake[0].longitude // 経度
765: % Zoom // 地図拡大率
766: % Maptype // 地図タイプ
767: % script // 地図描画スクリプト
768: % APPNAMEJP // アプリケーション名
769: ).str();
770:
771: return html;
772: }
解説:WebView2

IE サポート終了に備え、マイクロソフトは64ビット環境でデスクトップアプリケーションでWebコンテンツを利用するために「Microsoft Edge WebView2」(以下、WebView2と記す)と呼ぶ仕組みを用意している。Windowsの標準ブラウザ Edgeのモジュールを利用し、Webコンテンツ(HTML,CSS,JavaScriptなど)をアプリに埋め込むことができる。
ところが、WebView2 は Visual Studio での開発を前提にした仕組みであるため、MinGW C++ で扱うには困難を極めた。とくに、WebView2 のWebコンテンツ上のパラメータを読み書きするのに、Visual C++/C# 固有インターフェースを使わなければならず、これを代替するのに手間取った。

最終的に、MinGW C++ を使って WebView2 を操作できるようになり、アプリを64ビット環境に対応させることができたのだが、後述するように、CからWindows APIを呼び出さなければならないモジュールが必要になり、WebView2操作処理をクラス化することができなかった。このため、いくつかのグローバル変数を追加せざるを得なかった。
解説:WebView2コンポーネントを扱う
また、jchv / webview2-in-mingw のサンプル・プログラムを参考に、"pahooWebView2.cpp", "pahooWebView2.hpp" を用意した。ここでは、"pahooWebView2.cpp" の内容について解説する。
pahooWebView2.cpp
129: /**
130: * WebView2を生成する.
131: * @param HINSTANCE hInst 現在のインターフェイス
132: * @param HWND hDlg 親ウィンドウ・ハンドラ
133: * @paramm int x, y WebView2の左上座標
134: * @paramm int width, height WebView2の幅、高さ
135: * @param LPCWSTR uri 表示するURI
136: * @return HWND WebView2へのハンドラ
137: */
138: HWND createWebView2(HINSTANCE hInst, HWND hDlg, int x, int y, int width, int height, LPCWSTR uri) {
139: //ウィンドウ クラス情報
140: static WNDCLASSEX wc{};
141: wc.cbSize = sizeof(WNDCLASSEX);
142: wc.hInstance = hInst;
143: wc.lpszClassName = TEXT("webview");
144: wc.lpfnWndProc = WndProc;
145: RegisterClassEx(&wc);
146:
147: //ウィンドウ生成
148: static HWND hWnd = CreateWindowEx(
149: 0,
150: TEXT("webview"),
151: TEXT("MinGW WebView2"),
152: WS_CHILD | WS_VISIBLE | ES_LEFT,
153: x, y, width, height,
154: hDlg,
155: nullptr,
156: hInst,
157: nullptr
158: );
159: ShowWindow(hWnd, SW_SHOW);
160: UpdateWindow(hWnd);
161: SetFocus(hWnd);
162:
163: //データ・パス取得
164: TCHAR szDataPath[MAX_PATH + 1];
165: GetDataPath(szDataPath, MAX_PATH);
166: //イベントハンドラ
167: static EventHandler handler{};
168:
169: handler.EnvironmentCompleted = [&](HRESULT result, ICoreWebView2Environment* created_environment) {
170: // cout << "EnvironmentCompleted" << endl;
171: if (FAILED(result)) {
172: FatalError(TEXT("Failed to create environment?"));
173: }
174: created_environment->lpVtbl->CreateCoreWebView2Controller(created_environment, hWnd, &handler);
175: return S_OK;
176: };
177:
178: handler.ControllerCompleted = [&](HRESULT result, ICoreWebView2Controller* new_controller) {
179: // cout << "ControllerCompleted" << endl;
180: if (FAILED(result)) {
181: FatalError(TEXT("Failed to create controller?"));
182: }
183: controller = new_controller;
184: controller->lpVtbl->AddRef(controller);
185: controller->lpVtbl->get_CoreWebView2(controller, &webView2);
186: webView2->lpVtbl->AddRef(webView2);
187: webView2->lpVtbl->Navigate(webView2, uri);
188: ResizeBrowser(hWnd);
189: webView2Ready = true;
190: return S_OK;
191: };
192:
193: HRESULT result = CreateCoreWebView2EnvironmentWithOptions(
194: nullptr,
195: TStrToWStr(szDataPath).c_str(),
196: nullptr,
197: &handler
198: );
199:
200: if (FAILED(result)) {
201: FatalError(TEXT("Call to CreateCoreWebView2EnvironmentWithOptions failed!"));
202: }
203:
204: return hWnd;
205: }
まず、ICoreWebView2Environment インターフェースを使って、WebView2を初期化し、WebView2ランタイムの状態管理やイベントハンドリングができるようにする。
次に、ICoreWebView2Controller インターフェースを使って、アプリケーション内に WebView2コントロールを生成する。
いずれも非同期で処理されるため、これらの処理が終了したことをグローバル変数 webView2Ready に代入しておく。
また、これらのインターフェースなどがCライブラリであるため、"pahooWebView2.cpp" は "CINTERFACE" とせざる得ず、上述のようにクラス化することができなかった。
earthquakewin.cpp
1099: /**
1100: * WebView2:ダイアログを初期化する
1101: * @param HWND hDlg 親ウィンドウ・ハンドラ
1102: * @return なし
1103: */
1104: void initDialog(HWND hDlg) {
1105: HICON hIcon;
1106: hIcon = (HICON)LoadImage(hInst, MAKEINTRESOURCE(IDI_ICON), IMAGE_ICON, 16, 16, 0);
1107: SendMessage(hDlg, WM_SETICON, ICON_SMALL, (LPARAM)hIcon);
1108: ErrorMessage = "";
1109:
1110: // カーソルを砂時計に
1111: SetCursor(LoadCursor(NULL, IDC_WAIT));
1112: // オプション読み込み
1113: loadParameter();
1114: // アプリケーション・ウィンドウ移動
1115: SetWindowPos(hDlg, NULL, hParent_X, hParent_Y, 0, 0, (SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER));
1116:
1117: // テンポラリ・ファイル名取得
1118: getTempFname(tmpFname);
1119: size_t wLen = 0;
1120: mbstowcs_s(&wLen, wUri, MAX_PATH * 2, tmpFname, MAX_PATH);
1121: // WebView2生成
1122: hWebView2 = createWebView2(hInst, hDlg, 10, 10, MAP_WIDTH + 20, MAP_HEIGHT + 20, wUri);
1123:
1124: // Leafletが利用可能かどうか調べる
1125: const string url = "https://unpkg.com/leaflet@latest/";
1126: string contents = "";
1127: int httpStatus = 0;
1128: bool res = readWebContents(url, UserAgent, &contents, &httpStatus);
1129: if (res == FALSE) {
1130: ErrorMessage = "地図描画ライブラリ(Leaflet)が利用できません";
1131: } else if ((httpStatus < 200) || (httpStatus > 299)) {
1132: ErrorMessage = "地図描画ライブラリ(Leaflet)が利用できません";
1133: }
1134:
1135: // メッセージ・ループ
1136: MSG msg;
1137: static bool flagWebView2 = false;
1138: while (GetMessage(&msg, nullptr, 0, 0)) {
1139: if (msg.message == WM_QUIT) {
1140: // オプション保存
1141: // saveOption();
1142: DestroyWindow(hDlg);
1143: }
1144: TranslateMessage(&msg);
1145: DispatchMessage(&msg);
1146: if (!flagWebView2 && isWeb2Ready()) {
1147: // カーソルを砂時計に
1148: SetCursor(LoadCursor(NULL, IDC_WAIT));
1149: // 地震情報取得
1150: getEarthquake();
1151: // ブラウザ・コントロールに表示
1152: viewBrowser(infoEarthquake, tmpFname);
1153: webView2->Reload();
1154: flagWebView2 = true;
1155: // 地震情報一覧フレーム作成
1156: makeListViewFrame(GetDlgItem(hDlg, IDC_LISTVIEW_EARTHQUAKE));
1157: // 地震情報一覧表示
1158: makeListView(GetDlgItem(hDlg, IDC_LISTVIEW_EARTHQUAKE));
1159: }
1160: }
1161: }
WebView2コントロールで表示するためのHTMLファイルをローカルに用意するため、ユーザー関数 getTempFname を使って、Windowsユーザーの AppDataフォルダにテンポラリファイルを作る。
次に、上述のユーザー関数 createWebView2 を呼び出して WebView2コントロールを用意するのだが、これが非同期処理であるため、メッセージループを用意しなければならない。
earthquakewin.cpp
802: /**
803: * ブラウザ・コントロールを表示
804: * @param wstring info 情報ウィンドウに表示するテキスト
805: * @param char* tmpname 読み込むHTMLファイル名
806: * @return なし
807: */
808: void viewBrowser(wstring info, const char* tmpname) {
809: string html;
810: string fname;
811: ofstream ofs;
812:
813: // 表示用HTMLファイル作成
814: if ((ErrorMessage == "") && (! pGC->isError())) {
815: html = makeMapHTML(info);
816: } else {
817: html = makeErrorHTML();
818: }
819: ofs.open(tmpname);
820: ofs << html;
821: ofs.close();
822:
823: fname = (string)tmpname;
824: std::replace(fname.begin(), fname.end(), '\\', '/');
825:
826: if (isWeb2Ready()) {
827: webView2->Reload();
828: }
829: }
earthquakewin.cpp
1163: /**
1164: * WebView2のパラメータを取り出す.
1165: * selectActionの値によって動作を変える。
1166: * @param string result JavaScript終了ハンドラから渡るデータ
1167: * @return なし
1168: */
1169: void execScriptCompleted(string result) {
1170: // パラメータを読み込む
1171: vector<string> tokens;
1172: split(tokens, result, is_any_of(",\""));
1173: int cnt = 0;
1174: for (string ss: tokens) {
1175: switch (cnt) {
1176: case 1:
1177: Longitude = stod(ss);
1178: break;
1179: case 2:
1180: Latitude = stod(ss);
1181: break;
1182: case 3:
1183: Zoom = stoi(ss);
1184: break;
1185: case 4:
1186: Maptype = ss;
1187: break;
1188: }
1189: cnt++;
1190: }
1191:
1192: switch (selectAction) {
1193: // プログラムを終了する.
1194: case eAction::Finish:
1195: // テンポラリ・ファイルを削除する.
1196: remove(tmpFname);
1197: // パラメータを保存する.
1198: saveParameter();
1199: // プログラムを終了する
1200: EndDialog(hParent, 0);
1201: DestroyWindow(hParent);
1202: break;
1203: // ブラウザ・コントロールに表示
1204: default:
1205: viewBrowser(infoEarthquake, tmpFname);
1206: break;
1207: }
1208: // cout << "Longitude: " << Longitude << endl;
1209: // cout << "Latitude: " << Latitude << endl;
1210: // cout << "Zoom: " << Zoom << endl;
1211: // cout << "Maptype: " << Maptype << endl;
1212: }
このためのJavaScriptを渡す関数が execScriptCompleted で、ハンドラは scriptCompletedHandler である。取得したいパラメータをカンマ区切りで受け取るスクリプトを渡している。(JSONを使うほどのパラメータ量ではないので😓)
earthquakewin.cpp
1253: /**
1254: * WebView2のパラメータを保存する.
1255: * selectActionの値によって動作を変える。
1256: * @param なし
1257: * @return なし
1258: */
1259: void execScriptAndAction(void) {
1260: // プログラム終了時に実行するJavaScript
1261: LPCWSTR js = L"document.getElementById('longitude').value + ',' + document.getElementById('latitude').value + ',' + document.getElementById('zoom').value + ',' + document.getElementById('maptype').value;";
1262:
1263: webView2->ExecuteScript(js, scriptCompletedHandler);
1264: }
カンマ区切りで受け取ったパラメータは 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キーを取得してほしい。
(2025年3月16日)ネット接続チェック強化,キャッシュシステム不具合修正,使用ライブラリ更新
(2024年11月23日)使用ライブラリ更新
(2024年8月17日)使用ライブラリ更新
(2024年5月3日)API入力処理を改良
(2024年4月27日)Edgeブラウザ対応,64ビット対応