C++ で Googleニュース検索

(1/1)
C++でGoogleニュース検索
PHPでGoogleニュースの見出しを表示」で作った検索プログラムをC++に移植する。検索結果を画面上で並べ替えたり、CSVファイルに保存することができる。

(2024年3月6日)使用ライブラリ更新
(2023年10月14日)使用ライブラリ更新
(2023年6月10日)使用ライブラリ更新

目次

サンプル・プログラム

圧縮ファイルの内容
googlenewswin.msiインストーラ
bin/googlenewswin.exe実行プログラム本体
bin/libcrypto-1_1-x64.dll 実行時に必要になるDLL
bin/etc/help.chmヘルプ・ファイル
sour/googlenewswin.cppソース・プログラム
sour/resource.hリソース・ヘッダ
sour/resource.rcリソース・ファイル
sour/application.icoアプリケーション・アイコン
sour/makefileビルド
googlenewswin.cpp 更新履歴
バージョン 更新日 内容
1.5.4 2024/03/06 使用ライブラリ更新
1.5.3 2023/10/14 使用ライブラリ更新
1.5.2 2023/06/10 使用ライブラリ更新
1.5.1 2023/03/05 使用ライブラリ更新
1.5.0 2022/11/05 設定クリア機能,使用ライブラリ更新

Googleニュース検索

Googleニュース検索の結果はRSSファイルとして取得できる。詳しくは「PHPでGoogleニュースの見出しを表示」をご覧いただきたい。

使用ライブラリ

Googleニュース検索にアクセスするために、オープンソースのライブラリ Boost C++ライブラリcURL (カール)  および OpenSSL が必要になる。導入方法等については、「C++ 開発環境の準備」または「C++ 開発環境の準備 -MSYS2編-」をご覧いただきたい。

リソースの準備

Eclipse を起動し、新規プロジェクト googlenewswin を用意する。
ResEdit を起動し、resource.rc を用意する。
Eclipse に戻り、ソース・プログラム "googlenewswin.cpp" を追加する。
リンカー・フラグを -mwindows -static -lstdc++ -lgcc -lwinpthread -lcurl -lssl -llzma -lz -lws2_32 "C:\pleiades\eclipse\mingw\x86_64-w64-mingw32\bin\libcurl-x64.dll" に設定する。

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

解説:ヘッダファイル等

  10: // 初期化処理 ==============================================================
  11: #include <iostream>
  12: #include <stdio.h>
  13: #include <stdlib.h>
  14: #include <tchar.h>
  15: #include <time.h>
  16: #include <sstream>
  17: #include <string>
  18: #include <winsock2.h>
  19: #include <windows.h>
  20: #include <shlobj.h>
  21: #include <commctrl.h>
  22: #include <richedit.h>
  23: #include <curl/curl.h>
  24: #include <boost/property_tree/xml_parser.hpp>
  25: #include <boost/format.hpp>
  26: #include "resource.h"
  27: 
  28: using namespace std;
  29: using namespace boost;
  30: using namespace boost::property_tree;
  31: 
  32: #define MAKER       "pahoo.org"             //作成者
  33: #define APPNAME     "googlenewswin"         //アプリケーション名
  34: #define APPNAMEJP   "Googleニュース検索"    //アプリケーション名(日本語)
  35: #define APPVERSION  "1.5.4"                 //バージョン
  36: #define APPYEAR     "2020-2024"             //作成年
  37: #define REFERENCE   "https://www.pahoo.org/e-soul/webtech/cpp01/cpp01-08-01.shtm"   // 参考サイト
  38: 
  39: //char*バッファサイズ
  40: #define SIZE_BUFF       5120
  41: 
  42: //ListViewItemの最大文字長:変更不可
  43: #define MAX_LISTVIEWITEM    259
  44: 
  45: //標準フォント
  46: #define FONT_FACE       "MS UI Gothic"
  47: 
  48: //Googleニュース検索URL
  49: #define URL_GOOGLENEWS  "https://news.google.com/rss/search?hl=ja&gl=JP&ceid=JP:ja&q="
  50: 
  51: //現在のインターフェイス
  52: static HINSTANCE hInst;
  53: 
  54: //アプリケーション・ウィンドウ
  55: HWND hParent;
  56: 
  57: //アプリケーション・ウィンドウ位置
  58: unsigned hParent_X, hParent_Y;
  59: 
  60: //エラー・メッセージ格納用
  61: string ErrorMessage;
  62: 
  63: //検索キー格納用
  64: string Query;
  65: 
  66: //ヘルプ・ファイル
  67: #define HELPFILE    ".\\etc\\help.chm"
  68: 
  69: //デフォルト保存ファイル名
  70: #define SAVEFILE    "googlenewswin.csv"

cURLOpenSSLiconvBoost C++ライブラリから "format.hpp" を利用する関係で、includeするヘッダ・ファイルが多くなっている。

その他の定数は、自由に変更できる。

解説:データ構造

  75: //ニュース格納用クラス
  76: #define SIZE_NEWS   500     //格納ニュース数
  77: class _News {
  78: public:
  79:     wstring title = L"";        //見出し
  80:     string link = "";           //記事リンク
  81:     string pubdate = "";        //配信日時
  82:     time_t ti = 0;              //配信日時(time_t型)
  83:     wstring description = L"";  //記事概要
  84:     wstring source = L"";       //メディア
  85: };
  86: unique_ptr<_News> News[SIZE_NEWS] = {};
  87: 
  88: //ソート用構造体
  89: struct _stNews {
  90:     wchar_t* title;             //見出し
  91:     char* pubdate;              //配信日時
  92:     time_t ti;                  //配信日時(time_t型)
  93:     char* link;                 //記事リンク
  94:     wchar_t* source;            //メディア
  95: };
  96: vector<_stNews> Vnews;

取得したRSSファイルをPHPの連想配列のようにして使うために、C++スマートポインタ unique_ptr を導入する。
RSSファイルの必要な部分をクラス _New に収め、このクラスの実体であるオブジェクトへのポインタに unique_p 配列tr を利用する。RSSの結果数が未知なので、オブジェクトは必要に応じて動的に確保する(つまり、配列の要素数は不定)。

データ要素としては、日本語テキストを格納する要素にはワイド文字列型(wstring型)を、それ以外には通常の文字列型(string型)を割り当てた。配信日時は、string型time_t型の両方を格納しておく。

また、g++ にはWindowsの ListView に対応する SetItemData メソッドが無いようである(見つけられなかっただけかもしれない💦)。
このため、ListView に表示した検索結果を並べ替えるのに、独自のソート関数を用意する必要がある。
実装後半になって気づいたことだが、unique_ptr配列はソートがやりにくく、かといって他のデータ構造に変更するには時間がかかりすぎる。仕方なく、ソート用にvectorクラスを別途導入することにした。クラス _New のメンバのうち、ソート可能な項目(文字列)へのポインタを vectorクラス へコピーすることになる。

解説:世界標準時をローカル時間に変換

 333: /**
 334:  * 世界標準時をローカル時間に変換
 335:  * @param  char* gmt   世界標準時(文字列)  例)Fri, 14 Aug 2020 08:46:35
 336:  * @param  char* local ローカル時間を格納    例)2020-08-14 17:46
 337:  * @return なし
 338: */
 339: time_t gmt2local(char* gmt, char* local) {
 340:     tm t0, t1;
 341:     time_t dt;
 342: 
 343:     std::istringstream ss((char *)gmt);
 344:     ss >> std::get_time(&t0, "%a, %d %b %Y %H:%M:%S");
 345:     dt = mktime(&t0- timezone;
 346:     localtime_s(&t1, &dt);
 347:     strftime(local, SIZE_BUFF, "%Y-%m-%d %H:%M", &t1);
 348: 
 349:     return dt;
 350: }

RSSファイルには、ニュース記事の配信日時が世界標準時(文字列)で記されている。これをローカル時間(実際には日本標準時)を表す文字列に変換する関数が gmt2local である。

解説:文字コード変換

 277: /**
 278:  * テキスト・コード変換:UTF-8→wstring(Windows API使用)
 279:  * @param  string src UTF-8テキスト
 280:  * @return string 変換後テキスト
 281: */
 282: wstring __utf8_wstring(std::string src) {
 283:     auto const dest_size = ::MultiByteToWideChar(CP_UTF8, 0U, src.data(), -1, nullptr, 0U);
 284:     std::vector<wchar_t> dest(dest_size, L'\0');
 285:     if (::MultiByteToWideChar(CP_UTF8, 0U, src.data(), -1, dest.data(), dest.size()) == 0) {
 286:         throw std::system_error{static_cast<int>(::GetLastError()), std::system_category()};
 287:     }
 288:     dest.resize(std::char_traits<wchar_t>::length(dest.data()));
 289:     dest.shrink_to_fit();
 290: 
 291:     return std::wstring(dest.begin(), dest.end());
 292: }

RSSファイルのエンコードは UTF-8 であるが、これをC++で処理しやすいように wstring型 に変換するユーザー関数が __utf8_wstring である。よく使うので、マクロ _UW に割り当てた。
また、ソースプログラムやWindows APIで入力されるテキストはシフトJISであるため、これらを相互変換できるよう、下表の関数を用意した。
文字コード変換関数群
関数名マクロ名変換元変換先
__sjis_wstring_SWシフトJISwstring
__wstring_sjis_WSwstringシフトJIS
__utf8_wstring_UWUTF-8wstring
__wstring_utf8_WUwstringUTF-8
sjis_utf8_SUシフトJISUTF-8
utf8_sjis_USUTF-8シフトJIS

解説:Googleニュース検索URLを生成する

 622: /**
 623:  * Googleニュース検索URLを生成する(RSS2.0出力)
 624:  * @param   char* query 検索キー
 625:  * @param   char* url   URLを格納
 626:  * @param   size_t sz   urlのサイズ
 627:  * @return  なし
 628: */
 629: void getURL_GoogleNewsSearch(char* query, char* url, size_t sz) {
 630:     const string google = URL_GOOGLENEWS;
 631: 
 632:     static char buff1[SIZE_BUFF + 1], buff2[SIZE_BUFF + 1];
 633: 
 634:     strncpy(buff1, sjis_utf8((string)query).c_str(), SIZE_BUFF);
 635: 
 636:     CURL *curl = curl_easy_init();
 637:     strncpy(buff2, curl_easy_escape(curl, (const char *)buff1, strlen(buff1)), SIZE_BUFF);
 638: 
 639:     string ss = google + (string)buff2;
 640:     strncpy(url, ss.c_str(), sz);
 641: 
 642:     curl_easy_cleanup(curl);
 643: }

Googleニュース検索URLを生成する関数が getURL_GoogleNewsSearch である。
前述の文字コード変換関数を使って検索キーワードをUTF-8に変換した後、cURLライブラリにある curl_easy_escape 関数を使ってURLエスケープする。なお、cURLライブラリを使い始めるには curl_easy_init 関数を、終了するには curl_easy_cleanup 関数を呼び出す必要がある。

解説:検索結果を配列に格納する

 651: /**
 652:  * 検索結果を配列に格納する
 653:  * @param   char* query 検索キーワード
 654:  * @param   int   $i    カウンタ
 655:  * @return  int 検索結果の件数
 656: */
 657: int searchNews(char* query) {
 658:     //初期化
 659:     for (int i = 0i < SIZE_NEWSi++) {
 660:         News[i].reset();
 661:         News[i] = NULL;
 662:     }
 663: 
 664:     //cURLによる結果取得
 665:     CURL *curl;
 666:     CURLcode res = (CURLcode)0;
 667:     curl = curl_easy_init();
 668:     string chunk;
 669:     char url[SIZE_BUFF + 1];
 670:     getURL_GoogleNewsSearch(query, (char *)url, SIZE_BUFF);
 671: 
 672:     //cURLによるGoogleニュース検索
 673:     if (curl) {
 674:         curl_easy_setopt(curl, CURLOPT_URL, url);
 675:         curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
 676:         curl_easy_setopt(curl, CURLOPT_USERAGENT, UserAgent.c_str());
 677:         curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, callBackFunk);
 678:         curl_easy_setopt(curl, CURLOPT_WRITEDATA, (string*)&chunk);
 679:         res = curl_easy_perform(curl);
 680:         curl_easy_cleanup(curl);
 681:     }
 682:     if (res !CURLE_OK) {
 683:         ErrorMessage = "Google検索エラー";      //cURLエラー
 684:         return (-1);
 685:     }
 686: 
 687:     //XML読み込み
 688:     std::stringstream ss;
 689:     ss << chunk;
 690:     ptree pt;
 691:     xml_parser::read_xml(ss, pt);
 692: 
 693:     //XML解釈
 694:     ptree tree;
 695:     char buff[SIZE_BUFF + 1];
 696:     wstring title2;
 697:     int cnt = -1;
 698:     for (auto it : pt.get_child("rss.channel")) {
 699:         //item以外は読み飛ばし
 700:         if (it.first !"item")     continue;
 701:         cnt++;
 702:         News[cnt] = make_unique<_News>();
 703: 
 704:         //見出し
 705:         if (optional<string>title = it.second.get_optional<string>("title")) {
 706:             News[cnt]->title = _UW(title.value());
 707:         }
 708:         //記事リンク
 709:         if (optional<string>link = it.second.get_optional<string>("link")) {
 710:             News[cnt]->link = link.value();
 711:         }
 712:         //配信日時
 713:         if (optional<string>pubdate = it.second.get_optional<string>("pubDate")) {
 714:             News[cnt]->ti = gmt2local((char*)pubdate->c_str(), buff);
 715:             News[cnt]->pubdate = (string)buff;
 716:         }
 717:         //記事概要
 718:         if (optional<string>description = it.second.get_optional<string>("description")) {
 719:             News[cnt]->description = _UW(description.value());
 720:         }
 721:         //メディア
 722:         if (optional<string>source = it.second.get_optional<string>("source")) {
 723:             News[cnt]->source = _UW(source.value());
 724:         }
 725:     }
 726:     chunk.clear();
 727: 
 728:     return cnt;
 729: }

検索結果を _News 配列へ格納する関数が searchNews である。
まず、前述のユーザー関数 getURL_GoogleNewsSearch とcURLライブラリを使い、RSSファイルをstring変数 chunk に読み込む。

次に、RSSはXMLファイルの一種であるから、「C++でCPU情報を取得」で紹介した方法を使ってXML解釈を進める。

解説:ニュース記事のソート

 802: /**
 803:  * ソート用変数に代入
 804:  * @param   const _stNews& left, right
 805:  * @return  比較結果
 806: */
 807: _stNews __push_stNews(unique_ptr<_News> &ptr) {
 808:     _stNews s;
 809:     s.title     = (wchar_t*)ptr->title.c_str();
 810:     s.source    = (wchar_t*)ptr->source.c_str();
 811:     s.pubdate   = (char*)ptr->pubdate.c_str();
 812:     s.ti        = (time_t)ptr->ti;
 813:     s.link      = (char*)ptr->link.c_str();
 814:     return s;
 815: }

 817: /**
 818:  * ニュース記事のソート
 819:  * @param   int c  ソート条件
 820:  * @return  なし
 821: */
 822: void sortNews(int c) {
 823:     struct {
 824:         int id;
 825:         bool (*fn)(const _stNews& left, const _stNews& right);
 826: table[] = {
 827:     { SORT_TITLE_ASC,   __cmpTitleAsc },
 828:     { SORT_TITLE_DEC,   __cmpTitleDec },
 829:     { SORT_MEDIA_ASC,   __cmpMediaAsc },
 830:     { SORT_MEDIA_DEC,   __cmpMediaDec },
 831:     { SORT_PUBDATE_ASC, __cmpPubDateAsc },
 832:     { SORT_PUBDATE_DEC, __cmpPubDateDec }
 833: };
 834:     int i;
 835:     bool (*fn)(const _stNews& left, const _stNews& right) = NULL;
 836: 
 837:     //ソート用配列の初期化
 838:     Vnews.clear();
 839:     Vnews.shrink_to_fit();
 840: 
 841:     //ニュース記事をソート用配列へ
 842:     for (i = 0i < SIZE_NEWSi++) {
 843:         if (News[i!NULL) {
 844:             Vnews.push_back(__push_stNews(News[i]));
 845:         }
 846:     }
 847: 
 848:     //ソート用関数の選択
 849:     for (i = 0i < (int)(sizeof(table) / sizeof(*table)); i++) {
 850:         if (c == table[i].id) {
 851:             fn = table[i].fn;
 852:             break;
 853:         }
 854:     }
 855: 
 856:     //ソート実行
 857:     if (fn !NULL) {
 858:         stable_sort(Vnews.begin(), Vnews.end(), fn);
 859:     }
 860: }

前述のように、_News 配列をソートするために複数の関数群を用意した。
unique_ptrvector へコピーするための関数が __push_stNews である。
vectorクラス配列をソートするための関数が sortNews である。ソートする項目、昇順/降順の違いによって、__cmpで始まる名前の6つの関数を用意した。

解説:検索結果をCSVファイルに保存

 562: /**
 563:  * 検索結果をCSVファイルに保存する
 564:  * @param   const char *fname 保存ファイル名
 565:  * @return  なし
 566: */
 567: void __saveCSV(const char *fname) {
 568:     ofstream outputfile(fname);
 569: 
 570:     //ラベル
 571:     outputfile << "\"" << "タイトル" << "\",";
 572:     outputfile << "\"" << "メディア" << "\",";
 573:     outputfile << "\"" << "配信日時" << "\",";
 574:     outputfile << "\"" << "記事URL" << "\"" << endl;
 575: 
 576:     //データ本体
 577:     for (int i = 0i < SIZE_NEWSi++) {
 578:         if (News[i!NULL) {
 579:             outputfile << "\"" << _WS(News[i]->title<< "\",";
 580:             outputfile << "\"" << _WS(News[i]->source<< "\",";
 581:             outputfile << "\"" << News[i]->pubdate.c_str() << "\",";
 582:             outputfile << "\"" << News[i]->link.c_str() << "\"" << endl;
 583:         }
 584:         if (outputfile.bad()) {
 585:             ErrorMessage = "CSVファイル書き込みエラー";
 586:             break;
 587:         }
 588:     }
 589:     outputfile.close();
 590: }

 592: /**
 593:  * 検索結果をCSVファイルに保存する
 594:  * @param   なし
 595:  * @return  なし
 596: */
 597: void saveCSV(void) {
 598:     static char fname[MAX_PATH + 1];
 599:     strcpy(fname, SAVEFILE);
 600:     OPENFILENAME of;
 601: 
 602:     //OPENFILENAME構造体のサイズをセット
 603:     memset(&of, 0, sizeof(OPENFILENAME));
 604:     of.lStructSize = sizeof(OPENFILENAME);
 605:     //ダイアログボックスを所有するウィンドウへのハンドル
 606:     of.hwndOwner = hParent;
 607:     of.lpstrFilter = TEXT("CSVファイル(*.csv;*.txt)\0*.csv;*.txt\0すべてのファイル(*.txt)\0*.txt\0\0");
 608:     //ファイル名を格納したバッファのアドレス
 609:     of.lpstrFile = (LPTSTR)fname
 610:     //lpstrFileメンバで指定されるバッファのサイズ
 611:     of.nMaxFile = MAX_PATH;
 612:     of.Flags = OFN_OVERWRITEPROMPT;
 613:     //デフォルトの拡張子を格納したバッファのアドレス
 614:     of.lpstrDefExt = TEXT("csv");
 615:     //コモンダイアログの表示
 616:     GetSaveFileName(&of);
 617:     //ファイル保存
 618:     __saveCSV(fname);
 619: }

_News 配列をCSVファイルに保存するユーザー関数が saveCSV である。
saveCSV 関数はファイル・ダイアログのコントロールを担当し、実際にファイルへの書き込みを行うのはユーザー関数 __saveCSV である。C++のファイルストリーム機能を使って書き込みを行う。

解説:ニュース一覧作成

 870: /**
 871:  * ニュース一覧のフレーム作成
 872:  * @param   HWND hDlg  ウィンドウハンドル
 873:  * @return  なし
 874: */
 875: void makeListViewFrame(HWND hWnd) {
 876:     LVCOLUMNA lvcol;
 877:     lvcol.mask = LVCF_TEXT | LVCF_SUBITEM | LVCF_WIDTH | LVCF_FMT;
 878:     lvcol.fmt = LVCFMT_LEFT;
 879: 
 880:     lvcol.cx = 500;
 881:     lvcol.pszText = (char *)"タイトル";
 882:     SendMessage(hWnd, LVM_INSERTCOLUMNA, COL_TITLE, (WPARAM)&lvcol);
 883:     lvcol.cx = 150;
 884:     lvcol.pszText = (char *)"メディア";
 885:     SendMessage(hWnd, LVM_INSERTCOLUMNA, COL_SOURCE, (WPARAM)&lvcol);
 886:     lvcol.cx = 150;
 887:     lvcol.pszText = (char *)"配信日時";
 888:     SendMessage(hWnd, LVM_INSERTCOLUMNA, COL_PUBDATE, (WPARAM)&lvcol);
 889:     lvcol.cx = 0;
 890:     lvcol.pszText = (char *)"URL";
 891:     SendMessage(hWnd, LVM_INSERTCOLUMNA, COL_LINK, (WPARAM)&lvcol);
 892: 
 893:     //罫線表示
 894:     DWORD dwStyle = ListView_GetExtendedListViewStyle(hWnd);
 895:     dwStyle = dwStyle | LVS_EX_GRIDLINES | LVS_EX_ONECLICKACTIVATE;
 896:     ListView_SetExtendedListViewStyle(hWnd, dwStyle);
 897: }

 899: /**
 900:  * ニュース一覧作成
 901:  * @param   HWND hDlg  ウィンドウハンドル
 902:  * @return  なし
 903: */
 904: void makeListView(HWND hWnd) {
 905:     LVITEMA item;
 906: 
 907:     //初期化
 908:     for (int i = 0i < SIZE_NEWSi++) {
 909:         item.iItem = i;
 910:         SendMessage(hWnd, LVM_DELETEITEM, 0,(WPARAM)&item);
 911:     }
 912: 
 913:     //データ行
 914:     for (int i = 0i < SIZE_NEWSi++) {
 915:         if (News[i] == NULL)    break;
 916:         //タイトル
 917:         item.mask = LVIF_TEXT;
 918:         item.iItem = i;
 919:         item.iSubItem = COL_TITLE;
 920:         item.pszText = (LPSTR)_WS(Vnews[i].title).substr(0, MAX_LISTVIEWITEM).c_str();
 921:         SendMessage(hWnd, LVM_INSERTITEMA, 0,(WPARAM)&item);
 922:         //メディア
 923:         item.iSubItem = COL_SOURCE;
 924:         item.pszText = (LPSTR)_WS(Vnews[i].source).substr(0, MAX_LISTVIEWITEM).c_str();
 925:         SendMessage(hWnd, LVM_SETITEMA, 0,(WPARAM)&item);
 926:         //配信日時
 927:         item.iSubItem = COL_PUBDATE;
 928:         item.pszText = (LPSTR)Vnews[i].pubdate;
 929:         SendMessage(hWnd, LVM_SETITEMA, 0,(WPARAM)&item);
 930:         //リンク
 931:         item.iSubItem = COL_LINK;
 932:         item.pszText = (LPSTR)Vnews[i].link;
 933:         SendMessage(hWnd, LVM_SETITEMA, 0,(WPARAM)&item);
 934:     }
 935: }

_News 配列の内容は ListView クラスを使って一覧表示する。
ユーザー関数 makeListViewFrame によりフレームを用意し、makeListView により vectorクラス 配列の内容をListViewにセットする。
なお、ListViewItemには259バイトという上限があるため、これをあらかじめ定数 MAX_LISTVIEWITEM に定義しておき、substr()メソッドを使ってカットしている。

解説:イベントハンドラ:バージョン表示ダイアログ

 515: /**
 516:  * イベントハンドラ:バージョン表示ダイアログ
 517:  * @param   HWND hDlg           ウィンドウ・ハンドラ
 518:  * @paramm  UINT uMsg           メッセージ識別子
 519:  * @param   WPARAM wParam       メッセージの最初のパラメータ
 520:  * @paramL  PARAM lParam        メッセージの2番目のパラメータ
 521:  * @return  INT_PTR CALLBACK    TRUE:メッセージ処理完了/FALSE:未完了
 522:  */
 523: INT_PTR CALLBACK processHelp(HWND hDlg, UINT uMsg,
 524:                     WPARAM wParam, LPARAM lParam) {
 525:     switch (uMsg) {
 526:     //ダイアログ初期化
 527:     case WM_INITDIALOG:
 528:         CenterWindow(hDlg);
 529:         setStrEditBox(hDlg, IDC_TEXT_HELP, Version);
 530:         break;
 531: 
 532:     //ボタン押下
 533:     case WM_COMMAND:
 534:         switch (LOWORD(wParam)) {
 535:         //実行
 536:         case IDC_BUTTON_OK:
 537:             EndDialog(hDlg, 0);
 538:             break;
 539:         default:
 540:             return 1;
 541:         }
 542:         break;
 543:     //プログラム終了
 544:     case WM_CLOSE:
 545:         EndDialog(hDlg, 0);
 546:         break;
 547:     }
 548:     return 0;
 549: }

バージョン情報をモーダル・ダイアログで表示させる。このダイアログに関わるイベント・ハンドラが processHelp である。
表示メッセージはC++の 生文字リテラルを使っているが、ここで変数を利用できるように、Boost C++ライブラリから "format.hpp" を利用した。

解説:ウィンドウ位置・検索キーの読込・保存

 325: /**
 326:  * AppDataのパスを取得
 327:  * @param   char* appname アプリケーション名
 328:  * @return  string パス
 329:  */
 330: string getMyPath(const char* appname) {
 331:     static TCHAR myPath[MAX_PATH] = "";
 332: 
 333:     if (strlen(myPath) == 0) {
 334:         if (SHGetSpecialFolderPath(NULL, myPath, CSIDL_APPDATA, 0)) {
 335:             TCHAR *ptmp = _tcsrchr(myPath, _T('\\'));
 336:             if (ptmp !NULL) {
 337:                 ptmp = _tcsinc(ptmp);
 338:                 *ptmp = _T('\0');
 339:             }
 340:             strcat(myPath, _T("Roaming"));
 341:             CreateDirectory((LPCTSTR)myPath, NULL);
 342:             strcat(myPath, _T("\\pahoo.org"));
 343:             CreateDirectory((LPCTSTR)myPath, NULL);
 344:             strcat(myPath, _T("\\"));
 345:             strcat(myPath, _T(appname));
 346:             CreateDirectory((LPCTSTR)myPath, NULL);
 347:             strcat(myPath, _T("\\"));
 348:         } else {
 349:         }
 350:     }
 351:     return (string)myPath;
 352: }

 423: /**
 424:  * オプションの保存
 425:  * @param   string option オプション
 426:  * @return  なし
 427:  */
 428: void saveOption(string option) {
 429: #ifndef CMDAPP
 430:     //アプリケーション・ウィンドウの位置取得
 431:     WINDOWINFO windowInfo;
 432:     windowInfo.cbSize = sizeof(WINDOWINFO);
 433:     GetWindowInfo(hParent, &windowInfo);
 434:     hParent_X = (unsigned)windowInfo.rcWindow.left;
 435:     hParent_Y = (unsigned)windowInfo.rcWindow.top;
 436:     if (hParent_X >= (unsigned)windowInfo.rcWindow.right) {
 437:         hParent_X = 0;
 438:     }
 439:     if (hParent_Y >= (unsigned)windowInfo.rcWindow.bottom) {
 440:         hParent_Y = 0;
 441:     }
 442: #endif
 443: 
 444:     //XMLファイルへ書き込む
 445:     ptree pt;
 446:     ptree& child1 = pt.add("parameter.param", option);
 447:     child1.add("<xmlattr>.type", "option");
 448:     ptree& child2 = pt.add("parameter.param", (string)to_string(hParent_X));
 449:     child2.add("<xmlattr>.type", "wx");
 450:     ptree& child3 = pt.add("parameter.param", (string)to_string(hParent_Y));
 451:     child3.add("<xmlattr>.type", "wy");
 452: 
 453: //  clog << "option=" << option << endl;
 454: 
 455:     const int indent = 4;
 456:     write_xml(getMyPath(APPNAME+ APPNAME + ".xml", pt, std::locale(),
 457:         xml_writer_make_settings<std::string>(' ', indent));
 458: }

プログラム終了時に、saveOption 関数によって、ウィンドウ位置と検索キーをXMLファイルに保存する。保存場所は、getMyPath 関数によって、ユーザーのAppDataの下にアプリケーションフォルダを用意する。

 366: /**
 367:  * オプションの読み込み
 368:  * @param   なし
 369:  * @return  string オプション
 370:  */
 371: string loadOption(void) {
 372:     ptree pt;
 373: 
 374:     //初期値設定
 375:     string option = initOption();
 376: 
 377:     //XMLファイル読み込み
 378:     try {
 379:         xml_parser::read_xml(getMyPath(APPNAME+ APPNAME + ".xml", pt);
 380: 
 381:         //XML解釈
 382:         try {
 383:             //形式チェック
 384:             if (optional<string>str = pt.get_optional<string>("parameter")) {
 385:             } else {
 386:                 return OPTION_INIT;
 387:             }
 388:             //パラメータ読み込み
 389:             for (auto it : pt.get_child("parameter")) {
 390:                 string typeit.second.get_optional<string>("<xmlattr>.type").value();
 391:                 if (type == "option") {
 392:                     option =(string)it.second.data();
 393:                 } else if (type == "wx") {
 394:                     hParent_X = (unsigned)stoi(it.second.data());
 395:                 } else if (type == "wy") {
 396:                     hParent_Y = (unsigned)stoi(it.second.data());
 397:                 }
 398:             }
 399:         //解釈失敗したら初期値設定
 400:         } catch (xml_parser_error& e) {
 401:             return initOption();
 402:         }
 403:     //読み込み失敗したら初期値設定
 404:     } catch (xml_parser_error& e) {
 405:         return initOption();
 406:     }
 407: 
 408:     //アプリケーション・ウィンドウの位置(デスクトップ範囲外なら原点移動)
 409:     HWND hDesktop = GetDesktopWindow();
 410:     WINDOWINFO windowInfo;
 411:     windowInfo.cbSize = sizeof(WINDOWINFO);
 412:     GetWindowInfo(hDesktop, &windowInfo);
 413:     if (hParent_X >= (unsigned)windowInfo.rcWindow.right) {
 414:         hParent_X = 0;
 415:     }
 416:     if (hParent_Y >= (unsigned)windowInfo.rcWindow.bottom) {
 417:         hParent_Y = 0;
 418:     }
 419: 
 420:     return option;
 421: }

プログラム起動時に loadOption 関数によって、前回終了時のウィンドウ位置と検索キーを読み込む。

解説:イベントハンドラ:メインウィンドウ

 938: /**
 939:  * イベントハンドラ:メインウィンドウ
 940:  * @param   HWND hDlg           親ウィンドウ・ハンドラ
 941:  * @paramm  UINT uMsg           メッセージ識別子
 942:  * @param   WPARAM wParam       メッセージの最初のパラメータ
 943:  * @paramL  PARAM lParam        メッセージの2番目のパラメータ
 944:  * @return  INT_PTR CALLBACK    TRUE:メッセージ処理完了/FALSE:未完了
 945: */
 946: INT_PTR CALLBACK processMain(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) {
 947:     HICON hIcon;
 948:     char *str;
 949:     LVITEM item;
 950:     LV_HITTESTINFO lvinfo;
 951:     NM_LISTVIEW *pNMLV;
 952:     TCHAR buff[SIZE_BUFF + 1];
 953:     static int subsort[100];
 954: 
 955:     switch(uMsg){
 956:     //ダイアログ初期化
 957:     case WM_INITDIALOG:
 958:         hParent = hDlg;
 959:         hIcon = (HICON)LoadImage(hInst, MAKEINTRESOURCE(IDI_ICON), IMAGE_ICON, 16, 16, 0);
 960:         SendMessage(hParent, WM_SETICON, ICON_SMALL, (LPARAMhIcon);
 961:         ErrorMessage = "";
 962:         //オプション読み込み
 963:         loadParameter();
 964:         setStrEditBox(hDlg, IDC_EDIT_QUERY, Query);
 965:         //アプリケーション・ウィンドウ移動
 966:         SetWindowPos(hParent, NULL, hParent_X, hParent_Y, 0, 0, (SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER));
 967:         makeListViewFrame(GetDlgItem(hDlg, IDC_LISTVIEW_NEWS));
 968:         break;
 969: 
 970:     //ボタン押下
 971:     case WM_COMMAND:
 972:         switch (LOWORD(wParam)) {
 973:         //実行
 974:         case IDM_EXEC:
 975:         case IDC_BUTTON_SEARCH:
 976:             ErrorMessage = "";
 977:             //カーソルを砂時計に
 978:             SetCursor(LoadCursor(NULL, IDC_WAIT));
 979:             Query = getStrEditBox(hDlg, IDC_EDIT_QUERY);
 980:             //Googleニュース検索
 981:             searchNews((char *)Query.c_str());
 982:             sortNews(SORT_NONE);
 983:             //一覧表示
 984:             makeListView(GetDlgItem(hDlg, IDC_LISTVIEW_NEWS));
 985:             setStrEditBox(hDlg, IDC_TEXT_ERROR, ErrorMessage);
 986:             break;
 987:         //保存
 988:         case IDC_BUTTON_SAVE:
 989:         case IDM_SAVE:
 990:             saveCSV();
 991:             break;
 992:         //設定クリア+アプリ終了
 993:         case IDM_CLEAR_PARAMETER:
 994:             delParameter();
 995:             EndDialog(hParent, 0);
 996:             return 0;
 997:             break;
 998:         //ヘルプ
 999:         case IDM_HELP:
1000:             ShellExecute(hParent, _T("open"), _T(HELPFILE), NULL, NULL, SW_RESTORE);
1001:             break;
1002:         //バージョン表示
1003:         case IDM_VERSION:
1004:             createHelp(hParent, processHelp);
1005:             break;
1006:         //解説サイト
1007:         case IDM_PAHOO:
1008:             ShellExecute(NULL, _T("open"), _T(REFERENCE), NULL, NULL, SW_RESTORE);
1009:             break;
1010:         //コピー
1011:         case IDM_COPY:
1012:             setClipboardData(getStrEditBox(hParent, IDC_EDIT_QUERY));
1013:             break;
1014:         //貼り付け
1015:         case IDM_PASTE:
1016:             str = getClipboardData();
1017:             if (str !NULL) {
1018:                 setStrEditBox(hParent, IDC_EDIT_QUERY, str);
1019:             }
1020:             break;
1021:         //切り取り
1022:         case IDM_DELETE:
1023:             setStrEditBox(hParent, IDC_EDIT_QUERY, "");
1024:             break;
1025:         //プログラム終了
1026:         case IDM_QUIT:
1027:             //オプション保存
1028:             saveParameter();
1029:             EndDialog(hParent, 0);
1030:             return 0;
1031:         default:
1032:             return 1;
1033:     }
1034:     break;
1035: 
1036:     //通知
1037:     case WM_NOTIFY:
1038:         switch(((LPNMHDR)lParam)->idFrom) {
1039:         case IDC_LISTVIEW_NEWS:
1040:             switch (((LPNMLISTVIEW)lParam)->hdr.code) {
1041:             //一覧のラベルがクリック
1042:             case LVN_COLUMNCLICK:
1043:                 pNMLV = (NM_LISTVIEW *)lParam;
1044:                 if (subsort[pNMLV->iSubItem] == SORT_ASC) {
1045:                     subsort[pNMLV->iSubItem] = SORT_DEC;
1046:                 } else {
1047:                     subsort[pNMLV->iSubItem] = SORT_ASC;
1048:                 }
1049:                 //ニュース記事の並べ替え
1050:                 sortNews(pNMLV->iSubItem * 2 + subsort[pNMLV->iSubItem]);
1051:                 //一覧表示
1052:                 makeListView(GetDlgItem(hDlg, IDC_LISTVIEW_NEWS));
1053:                 break;
1054:             //ニュース一覧の1行を選択
1055:             case LVN_ITEMCHANGED:
1056:             //ニュース一覧の1行をダブルクリック
1057:             //case NM_DBLCLK:
1058:                 GetCursorPos((LPPOINT)&lvinfo.pt);
1059:                 ScreenToClient(((LPNMLISTVIEW)lParam)->hdr.hwndFrom, &lvinfo.pt);
1060:                 ListView_HitTest(((LPNMLISTVIEW)lParam)->hdr.hwndFrom, &lvinfo);
1061:                 if ((lvinfo.flags & LVHT_ONITEM!0) {
1062:                     item.mask = TVIF_HANDLE | TVIF_TEXT;
1063:                     item.iItem = lvinfo.iItem;
1064:                     item.iSubItem = COL_LINK;
1065:                     item.pszText = buff;
1066:                     item.cchTextMax = SIZE_BUFF;
1067:                     ListView_GetItem(((LPNMLISTVIEW)lParam)->hdr.hwndFrom, &item);
1068:                     //ブラウザ起動
1069:                     ShellExecute(hParent, _T("open"), _T(buff), NULL, NULL, SW_RESTORE);
1070:                 }
1071:                 break;
1072:             default:
1073:                 break;
1074:         }
1075:         break;
1076:     default:
1077:         break;
1078:     }
1079:     break;
1080: 
1081:     //プログラム終了
1082:     case WM_CLOSE:
1083:         //オプション保存
1084:         saveParameter();
1085:         EndDialog(hParent, 0);
1086:         return 0;
1087:     }
1088:     return 0;
1089: }

メインウィンドウに関わるイベント・ハンドラが processMain である。いくつかのボタンとメニューは共通の機能を担っている。
ダイアログ初期化(WM_INITDIALOG)の際に makeListViewFrame を使ってニュース一覧のフレームを用意する。

検索実行イベント発生時(IDC_BUTTON_SEARCH または IDM_EXEC)、Googleニュース検索にアクセスして応答が返ってくるまで少し時間がかかるので、まずカーソルを砂時計にする。続けて searchNews を呼び出し、Googleニュース検索を行い、結果を _News 配列に代入する。
sortNews(SORT_NONE)_News 配列の内容を vectorクラス配列へコピーするだけで、ソートは行わない。

ListViewクラスのラベルをクリックしたときは WM_NOTIFY イベントが発生、コードとして LVN_COLUMNCLICK が渡る。ここから、クリックされたラベル(列)の番号 pNMLV->iSubItem を取り出す。
現在の列の状態が昇順(SORT_ASC)か降順(SORT_DEC)かは、列番号を要素に持つ配列 subsort に入れておき、その値によってソート関数 sortNews を呼び出し、一覧表示 makeListView を実行する。

記事業をシングルクリックすると、LVN_ITEMCHANGED が渡される。その行にあるリンクURL(列番号 COL_LINK)を文字列 buff にコピーしてやり、ShellExecute 関数を使ってブラウザに渡す。

解説:Windowsメインプログラム

1092: /**
1093:  * Windowsメインプログラム
1094:  * @param   HINSTANCE hInstance         インスタンスハンドル
1095:  * @paramm  HINSTANCE hPrevInstance     未使用(常にNULL):Win16時代の名残
1096:  * @param   LPSTR lpCmdLine             コマンドライン引数
1097:  * @paramL  int nShowCmd                ウィンドウの表示方法
1098:  * @return  int リターンコード
1099: */
1100: int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) {
1101:     LoadLibrary("RICHED20.DLL");
1102: 
1103:     //UserAgent生成
1104:     static OSVERSIONINFOEX os;
1105:     GetVersion2(&os);
1106:     UserAgent = (string)"Mozilla/5.0 (" + APPNAME + "/"
1107:         + APPVERSION + "/" + MAKER
1108:         + ", Windows NT " + to_string(os.dwMajorVersion+ "."
1109:         + to_string(os.dwMinorVersion+ ")";
1110: 
1111:     hInst = hInstance;
1112:     DialogBox(hInstance, MAKEINTRESOURCE(IDD_MAIN), NULL, (DLGPROC)processMain);
1113:     return 0;
1114: }

検索キーワードを入力するエディットボックスは、コントロールキーが使えるように RichEdit にしてある。プログラムの冒頭で "RICHED20.DLL" を読み込んでやる必要がある。

解説:ヘルプファイルの作成

.chm 形式のヘルプファイルは、doc2htmlhelp(s7taka氏)を利用した。Microsoft Word と Microsoft HTML Help Workshop が必要。

参考サイト

(この項おわり)
header