C++ で週間天気予報を表示する

(1/1)
>C++で週間天気予報を表示する
インターネット経由で気象庁週間天気予報サイトにアクセスし、地図や住所、ランドマーク,郵便番号等で指定した地点の週間天気予報を一覧表示するアプリケーションを作る。一覧情報をクリップボードにコピーしたり、CSV 形式ファイルに保存することができる。また、ユーザーが API キーを取得することで Google マップや Google 住所検索も利用できるようになる。
PHP で天気予報を求める(その 3)」で作った PHP プログラムを C++に移植したものである。

目次

サンプル・プログラム

圧縮ファイルの内容
weeklyweather.msiインストーラ
bin/weeklyweather.exe実行プログラム本体
bin/jmaweatherspots.xml予報地点情報ファイル
bin/cwebpage.dll
bin/libcrypto-1_1.dll
bin/libcurl.dll
bin/libssl-1_1.dll
実行時に必要になるDLL
bin/etc/help.chmヘルプ・ファイル
sour/weeklyweather.cppソース・プログラム
sour/resource.hリソース・ヘッダ
sour/resource.rcリソース・ファイル
sour/application.icoアプリケーション・アイコン
sour/mystrings.cpp汎用文字列処理関数など(ソース)
sour/mystrings.h汎用文字列処理関数など(ヘッダ)
sour/pahooGeocode.cpp住所・緯度・経度に関わるクラス(ソース)
sour/pahooGeocode.cpp住所・緯度・経度に関わるクラス(ヘッダ)
sour/pahooWeather.cpp気象情報に関わるクラス(ソース)
sour/pahooWeather.cpp気象情報に関わるクラス(ヘッダ)

使用ライブラリ

気象庁週間予報サイトにアクセスするために、オープンソースのライブラリ Boost C++ライブラリcURL (カール)  および OpenSSL が必要になる。導入方法等については、「C++ 開発環境の準備」をご覧いただきたい。

リソースの準備

今回は 32 ビット版の開発環境を用いる。
Eclipse を起動し、新規プロジェクト weeklyweather を用意する。
ResEdit を起動し、resource.rc を用意する。

Eclipse に戻り、ソース・プログラム "weeklyweather.cpp" を追加する。
リンカー・フラグを -Wl,--enable-stdcall-fixup -mwindows -lgdiplus -static -lstdc++ -lgcc -lwinpthread -lcurl -lssl "(任意のパス)\libcurl.dll" "(任意のパス)\cwebpage.dll" "C:\Windows\system32\GdiPlus.dll" に設定する。
また、コマンド行パターンを ${COMMAND} ${FLAGS} ${OUTPUT_FLAG} ${OUTPUT_PREFIX}${OUTPUT} ${INPUTS} -lole32 -loleaut32 -luuid にすること。

解説:定数など

0038: // 定数など ==================================================================
0039: #define APPNAME     "weeklyweather"        //アプリケーション名
0040: #define APPNAMEJP "週間天気予\報"      //アプリケーション名(日本語)
0041: #define APPVERSION "1.0"                //バージョン
0042: #define APPYEAR     "2020"                //作成年
0043: #define REFERENCE "https://www.pahoo.org/e-soul/webtech/cpp01/cpp01-16-01.shtm"  //参考サイト
0044: 
0045: //ヘルプ・ファイル
0046: #define HELPFILE ".\\etc\\help.chm"
0047: 
0048: //デフォルト保存ファイル名
0049: #define SAVEFILE "weeklyweather.csv"
0050: 
0051: //エラー・メッセージ格納用:変更不可
0052: string ErrorMessage;
0053: 
0054: //現在のインターフェイス
0055: HINSTANCE hInst;
0056: 
0057: //親ウィンドウ
0058: HWND hParent;
0059: 
0060: //ブラウザ・コントロール
0061: WebBrowser wBrowser;
0062: 
0063: //pahooWeatherオブジェクト
0064: pahooWeather *pWT;
0065: 
0066: //pahooGeocodeオブジェクト
0067: pahooGeocode *pGC;
0068: 
0069: //予報表のID
0070: #define IDC_CAL_LABEL 1501        //日付
0071: #define IDC_RES_LABEL 1601        //天気予報
0072: #define IDC_RES_IMAGE 1701        //天気アイコン
0073: #define IDC_TEMP_LABEL 1801        //降水確率・最低・最高気温
0074: 
0075: //予報表の座標
0076: #define IDC_RES_X     10
0077: #define IDC_RES_Y     430
0078: #define IDC_RES_WIDTH 80
0079: 
0080: //マップに表示するマーカー画像URL
0081: #define URL_MARKER "https://maps.google.co.jp/mapfiles/ms/icons/yellow-dot.png"
0082: #define MAP_WIDTH     530        //地図の幅(ピクセル)
0083: #define MAP_HEIGHT     300        //地図の高さ(ピクセル)
0084: 
0085: //マップID
0086: #define MAP_ID         "map_id"
0087: //経度・緯度(初期値)
0088: #define DEF_LONGITUDE 139.766667
0089: double Longitude = DEF_LONGITUDE;
0090: #define DEF_LATITUDE 35.681111
0091: double Latitude  = DEF_LATITUDE;
0092: //地図拡大率(初期値)
0093: #define DEF_ZOOM 6
0094: int Zoom = DEF_ZOOM;
0095: //地図の種類(初期値)
0096: #define DEF_MAPTYPE     "GSISTD"
0097: string Maptype = DEF_MAPTYPE;

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

解説:クラスとデータ構造

今回は、気象庁の週間予報ページをスクレイピングすることで情報を取り出すのだが、必要なメソッドとデータ構造をクラス pahooWeather クラス としてコーディングした。ソースは "pahooWeather.cpp" ヘッダは "pahooWeather.hpp" である。

0021: //予報情報
0022: typedef struct _forecast {
0023:     int year;                        //西暦年
0024:     int month;                       //月
0025:     int day;                     //日
0026:     std::wstring day_of_week;      //曜日
0027:     std::wstring weather;          //天気予報
0028:     std::wstring image;                //天気予報アイコンURL
0029:     std::wstring rainy;                //降水確率
0030:     std::wstring temp_max;         //最高気温
0031:     std::wstring temp_min;         //最低気温
0032:     std::wstring city;             //予報地点名
0033:     std::wstring id;               //ID
0034: forecast_t;

1 日分の予報情報は、構造体 forecast_t に格納する。これを 7 日分、配列として管理する。

0037: //予報地点情報格納用クラス ===================================================
0038: #define SIZE_SPOTS 500            //格納上限
0039: class _Spots {
0040: public:
0041:     wstring id = L"";           //ID
0042:     wstring address = L"";      //住所
0043:     wstring query = L"";        //検索キー
0044:     double latitude  = 0.0;       //緯度
0045:     double longitude = 0.0;       //経度
0046: };
0047: unique_ptr<_SpotsSpots[SIZE_SPOTS] = {};

予報地点のデータは、あらかじめ FILE_SPOTS で示される XML ファイルに格納しておく。このファイルは、「PHP で地図で指定した場所の天気予報を求める」で紹介したプログラムを使って作成することができる。

0183: /**
0184:  * 指定した緯度・経度に最も近い予報地点IDを返す
0185:  * @param object $pgc pahooGeoCodeオブジェクト
0186:  * @param double latitude  緯度(世界測地系)
0187:  * @param double longitude 経度(世界測地系)
0188:  * @return wstring 予報地点ID
0189:  */
0190: wstring pahooWeather::getJmaNearSpot(double longitudedouble latitude) {
0191:     wstring id = L"";
0192:     double d0  = 999999999.9;
0193:     for (int i = 0; i < SIZE_SPOTSi++) {
0194:         if (Spots[i] == NULL) {
0195:             break;
0196:         }
0197:         double d1 = this->distance(Spots[i]->longitudeSpots[i]->latitudelongitudelatitude);
0198:         if (d1 < d0) {
0199:             id = Spots[i]->id;
0200:             d0 = d1;
0201:         }
0202:     }
0203:     return id;
0204: }

メソッド getJmaNearSpot は、緯度・経度を引数として、前述の予報地点ファイル FILE_SPOTS を参照し、一番近い予報地点 ID を返す。

0407: /**
0408:  * 指定したIDの週間天気予報を読み込む
0409:  * @param wstring id    地点ID
0410:  * @param int*    cnt   都市番号を格納
0411:  * @return bool TRUE:読込成功/FALSE:失敗
0412: */
0413: bool pahooWeather::jma_readWeeklyWeatherById(const wstring idint *cnt) {
0414:     wsmatch mt;
0415:     wregex re(_SW("^([0-9]{4})([0-9]{2})$"));
0416: 
0417:     //地点IDのベリフィケーション
0418:     if (! regex_search(idmtre)) {
0419:         this->errmsg = _SW("地点ID(") + id + _SW(")が不正です");
0420:         return FALSE;
0421:     }
0422: 
0423:     //読み込みURL
0424:     char buff[SIZE_BUFF + 1];
0425:     snprintf(buffSIZE_BUFF, "%03d.html", stoi(_WS(mt[1].str()).c_str()));
0426:     string url = JMA_WEEKLY_URL + (string)buff;
0427:     *cnt = stoi(mt[2].str());
0428: 
0429:     this->__jma_readWeeklyWeather(0, 0, url);
0430: 
0431:     return TRUE;
0432: }

メソッド jma_readWeeklyWeatherById は、予報地点 ID を引数として、その週間天気予報情報を読み込む。

0228: /**
0229:  * 指定したURLから週間天気予報を読み込む
0230:  * @param int    num   都道府県番号(1:北海道〜)
0231:  * @param int    start 開始都市番号
0232:  * @param string url   URL
0233:  * @return int 読み込んだ最後の都市番号/(-1):読込失敗
0234: */
0235: int pahooWeather::__jma_readWeeklyWeather(int numint startconst string url) {
0236:     static wstring week_name[] = {_SW(""), _SW(""), _SW(""), _SW(""), _SW(""), _SW(""), _SW("")};
0237: 
0238:     //cURLによる結果取得
0239:     string contents = "";
0240:     bool res = readWebContents(url, &contents);
0241:     if (! res) {
0242:         this->errmsg = _SW("気象庁サイトへの接続エラー");
0243:         return (-1);
0244:     }
0245: 
0246:     //コンテンツの解釈
0247:     setlocale(LC_ALL, "Japanese");
0248:     stringstream ss;
0249:     string ss0;
0250:     wstring ws;
0251:     wsmatch mt1mt2;
0252:     wregex re01(_SW("<th\\s+class\\=\"weekday\"\\s+colspan\\=\"2\">"));
0253:     wregex re02(_SW("<th\\s+class\\=\"[^\"]+\">([0-9]+)<"));
0254:     wregex re11(_SW("<input\\s+class\\=\"linkbtn\"\\s+type\\=\"button\"\\s+title\\=\"府県天気"));
0255:     wregex re12(_SW("<td\\s+class\\=\"for\"\\s+nowrap>([^\\>]+)<br><img\\s+src\\=\"([^\"]+)\"\\s+align\\=\"middle\""));
0256:     wregex re21(_SW("<td\\s+colspan\\=\"2\"\\s+class\\=\"normal\">"));
0257:     wregex re22(_SW("<td\\s+class\\=\"for\"><font\\s+class\\=\"pop\">([^\\>]+)<\\/font><\\/td>"));
0258:     wregex re31(_SW("<th\\s+class\\=\"cityname\"\\s+rowspan\\=\"2\">([^\\>]+)<\\/th>"));
0259:     wregex re41(_SW("<td\\s+class\\=\"for\"\\s+nowrap><font\\s+class\\=\"maxtemp\">([^\\<]+)<"));
0260:     wregex re42(_SW("低\\(℃\\)"));
0261:     wregex re43(_SW("<td\\s+class\\=\"for\"\\s+nowrap><font\\s+class\\=\"mintemp\">([^\\<]+)<"));
0262:     wregex re44(_SW("<td\\s+class\\=\"for\">/<\\/td>"));
0263:     wregex re51(_SW("\\/([0-9]+)\\.html$"));
0264: 
0265:     int flag = 0;
0266:     int cnt = start;
0267:     int dd = 0;
0268:     int year = 0, month = 0, day = 0;
0269:     int i = 0;
0270:     time_t now = time(NULL);
0271:     struct tmpnow = localtime(&now);
0272:     namespace gr = boost::gregorian;
0273:     gr::date dt;
0274:     static char buff[SIZE_BUFF + 1];
0275:     wstring wurl;
0276: 
0277:     ss << contents;
0278:     while (ss && getline(ssss0)) {
0279:         //1行をwstring変換
0280:         ws = _UW(ss0);
0281:         switch (flag) {
0282:             //日付開始
0283:             case 0:
0284:                 if (regex_search(wsmt1re01)) {
0285:                     flag = 1;
0286:                 }
0287:                 break;
0288:             //日付の解釈
0289:             case 1:
0290:                 if (regex_search(wsmt1re02)) {
0291:                     if (dd == 0) {
0292:                         day   = pnow->tm_mday;
0293:                         month = pnow->tm_mon + 1;
0294:                         year  = pnow->tm_year + 1900;
0295:                         //開始日は1日だが、内蔵時計が1日でない場合(タイムラグ補正)
0296:                         if ((day != 1) && (stoi(mt1[1].str().c_str()) == 1)) {
0297:                             month++;
0298:                             if (month > 12) {
0299:                                 year++;
0300:                                 month = 1;
0301:                             }
0302:                         } else {
0303:                             day = stoi(mt1[1].str().c_str());
0304:                         }
0305:                         dt = gr::date(yearmonthday);
0306:                     } else {
0307:                         dt = dt + gr::days(1);
0308:                     }
0309:                     this->Forecast[cnt][dd].year  = dt.year();
0310:                     this->Forecast[cnt][dd].month = dt.month();
0311:                     this->Forecast[cnt][dd].day   = dt.day();
0312:                     this->Forecast[cnt][dd].day_of_week = week_name[dt.day_of_week()];
0313:                     dd++;
0314:                 } else if (regex_search(wsmt1re11)) {
0315:                     flag = 2;
0316:                     dd = 0;
0317:                 }
0318:                 break;
0319:             //天候の解釈
0320:             case 2:
0321:                 if (regex_search(wsmt1re12)) {
0322:                     if (mt1[1].str() == L"") {
0323:                         this->Forecast[cnt][dd].weather = L"--";
0324:                     } else {
0325:                         this->Forecast[cnt][dd].weather = mt1[1].str();
0326:                     }
0327:                     if (mt1[2].str() == L"") {
0328:                         this->Forecast[cnt][dd].image = L"";
0329:                     } else {
0330:                         this->Forecast[cnt][dd].image = mt1[2].str();
0331:                     }
0332:                     dd++;
0333:                 } else if (regex_search(wsmt1re21)) {
0334:                     //2番目以降の年の日付代入
0335:                     if (cnt >= 1) {
0336:                         for (int i = 0; i < 7; i++) {
0337:                             this->Forecast[cnt][i].year = this->Forecast[cnt - 1][i].year;
0338:                             this->Forecast[cnt][i].month = this->Forecast[cnt - 1][i].month;
0339:                             this->Forecast[cnt][i].day = this->Forecast[cnt - 1][i].day;
0340:                             this->Forecast[cnt][i].day_of_week = this->Forecast[cnt - 1][i].day_of_week;
0341:                         }
0342:                     }
0343:                     flag = 3;
0344:                     dd = 0;
0345:                 }
0346:                 this->Forecast[cnt][dd].rainy = L"";
0347:                 break;
0348:             //降水確率の解釈
0349:             case 3:
0350:                 if (regex_search(wsmt1re22)) {
0351:                     //複数の数字がある場合は平均値を計算
0352:                     this->Forecast[cnt][dd].rainy = _SW(to_string((int)waverage(mt1[1].str())));
0353:                     dd++;
0354:                 //都市名+ID
0355:                 } else if (regex_search(wsmt1re31)) {
0356:                     wurl = _SW(url);
0357:                     if (! regex_search(wurlmt2re51)) {
0358:                         this->errmsg = _SW("気象庁サイトの解析に失敗しました");
0359:                         return FALSE;
0360:                     }
0361:                     //id = URLの数字(4桁)+連番(2桁)
0362:                     snprintf(buffSIZE_BUFF, "%04d%02d", stoi(_WS(mt2[1].str())), cnt - start);
0363:                     for (i = 0; i < 7; i++) {
0364:                         this->Forecast[cnt][i].city = mt1[1].str();
0365:                         this->Forecast[cnt][i].id = _SW(buff);
0366:                     }
0367:                     flag = 4;
0368:                     dd = 0;
0369:                 }
0370:                 this->Forecast[cnt][dd].temp_max = L"";
0371:                 this->Forecast[cnt][dd].temp_min = L"";
0372:                 break;
0373:             //最高気温の解釈
0374:             case 4:
0375:                 if (regex_search(wsmt1re41)) {
0376:                     this->Forecast[cnt][dd].temp_max = mt1[1].str();
0377:                     dd++;
0378:                 } else if (regex_search(wsmt1re42)) {
0379:                     flag = 5;
0380:                     dd = 0;
0381:                 }
0382:                 break;
0383:             //最低気温の解釈
0384:             case 5:
0385:                 if (regex_search(wsmt1re43)) {
0386:                     this->Forecast[cnt][dd].temp_min = mt1[1].str();
0387:                     dd++;
0388:                 } else if (regex_search(wsmt1re44)) {
0389:                     this->Forecast[cnt][dd].temp_min = L"";
0390:                     dd++;
0391:                 //次の地域
0392:                 } else if (regex_search(wsmt1re11)) {
0393:                     flag = 2;
0394:                     cnt++;
0395:                     dd = 0;
0396:                 }
0397:                 break;
0398:             default:
0399:                 break;
0400:         }
0401:     }
0402:     contents.clear();
0403: 
0404:     return cnt;
0405: }

メソッド __jma_readWeeklyWeather は、前述のメソッド jma_readWeeklyWeatherById の下請けで、気象庁週間予報サイトをスクレイピングし、週間天気予報情報を前述の構造体 forecast_t に格納してゆく。

解説:天気予報画像を表示

0201: /**
0202:  * 天気予報画像を表示
0203:  * @param HWND hWnd  表示するウィンドウ・ハンドラ
0204:  * @param string url 画像アイコンURL
0205:  * @return bool TRUE:表示成功/FALSE:失敗
0206:  */
0207: bool readURLimage(HWND hWndconst string url) {
0208:     if (pWT->isError()) {
0209:         return FALSE;
0210:     }
0211: 
0212:     //cURLによるアイコン画像取得
0213:     string contents = "";
0214:     bool res = readWebContents(url, &contents);
0215:     if (! res) {
0216:         ErrorMessage = "気象庁サイトへの接続エラーです";
0217:         return FALSE;
0218:     }
0219: 
0220:     //表示領域クリア
0221:     IStream *pStream;
0222:     Graphics mygraphics(hWnd);
0223:     mygraphics.Clear(Gdiplus::Color(255, 255, 255, 255));
0224:     Bitmap *image;
0225:     //ストリーム読み込み準備
0226:     HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLEcontents.size());
0227:     //ロックしてポインタ取得
0228:     LPVOID lpBuf = GlobalLock(hGlobal);
0229:     //メモリ読み込み
0230:     memcpy(lpBufcontents.c_str(), contents.size());
0231:     //アンロック
0232:     GlobalUnlock(hGlobal);
0233:     //ストリーム変換
0234:     CreateStreamOnHGlobal(hGlobalTRUE, &pStream);
0235:     //ストリームから読み込み
0236:     image = Bitmap::FromStream(pStream);
0237:     //画面表示
0238:     mygraphics.DrawImage(image, 10, 10);
0239: 
0240:     //メモリ解放
0241:     GlobalFree(hGlobal);
0242:     contents.clear();
0243: 
0244:     return TRUE;
0245: }

天気予報アイコンは、著作権の関係があるので、その都度、気象庁週間予報サイトからダウンロードして表示している。
以前紹介した「C++で撮影場所をマッピング -画像ファイルを読み込む]」と同じく GDI+:blue を用いているが、ファイルではなく、cURL を使って気象庁週間予報サイトからメモリへダウンロードし、メモリからロードして画面に表示させるようにしている。
その他の関数、マップ描画、ジオコーディング処理、ブラウザ・コントロール、ヘルプファイルやインストーラー作成方法については、これまでの連載で説明してきたとおりである。

参考サイト

(この項おわり)
header