2014.8.30
INASOFT
/トップ/ListView to CSV/ダウンロード/WebHelp/ヘルプトップ
ListView to CSVを作ってみよう!!
ListView to CSVの簡単なコマンドライン版を作ってみようという、プログラミング解説講座です。
第2回では、他プロセスの所有するリストビュー コントロールから文字列を取得する際の失敗例について述べたいと思います。
第2回
■サンプルのソースコード
リストビューから文字列を取得する方法として真っ先に思い浮かぶのは、LVM_GETITEM か LVM_GETITEMTEXT メッセージを使って文字列を取得する方法です。
まずは、他プロセスのリストビューから文字列を取得する際の失敗例として、サンプルコードから示したいと思います。このコードはうまくうごきません。
// ListView to CSV for CommandLine (lvcsvcom.cpp) // Copyright(C) 2005 T.Yabuki #include <windows.h> #include <commctrl.h> #include <stdio.h> #pragma comment(lib, "comctl32.lib") #include <iostream> #include <iomanip> using namespace std; #define MAX_GETITEMTEXT_LENGTH 1024 #define MAX_GETWINDOWTEXT 79 #define MAX_GETWINDOWCLASS 79 // -------------------------------------------------------------- // EnumChildWindowsにより呼び出されるコールバック関数 // -------------------------------------------------------------- // 引数 :hWnd = コントロールのハンドル // p_lpszTitleBar = タイトルバー文字列へのポインタ // 戻り値:TRUE(必ずEnumChildWindowsを継続させる) // -------------------------------------------------------------- BOOL CALLBACK callback_EnumChildWindowsProc(HWND hCtrl, LPARAM p_lpszTitleBar) { char szClassName[MAX_GETWINDOWCLASS+1]; char *lpszTitleBar = (char *)p_lpszTitleBar; // クラス名を取得する if (GetClassName(hCtrl, szClassName, MAX_GETWINDOWCLASS)) { if ( strcmp(szClassName, WC_LISTVIEW) == 0 ) { // クラス名がWC_LISTVIEW(文字列"SysListView32"を表す)であれば // リストビューであると見なす→ウィンドウハンドルと要素数を表示。 // 各ウィンドウにつき1回ずつ、タイトルバー文字列を表示する。 // タイトルバー文字列を1回表示したら、lpszTitleBarの最初の // 1バイトをNULL文字にしておく。これにより、タイトルバー文字列が // 表示済みであることを表すことにする。 if ( lpszTitleBar && lpszTitleBar[0] ) { cout << "--------------------" << endl << lpszTitleBar << endl; lpszTitleBar[0] = '\0'; } // ウィンドウハンドルを8桁の16進数で表示 cout << " ハンドル番号:" << setw(8) << setfill('0') << hex << (UINT)hCtrl; // 要素数を表示 cout << " アイテム数:" << dec << ListView_GetItemCount(hCtrl) << endl; } } return TRUE; } // -------------------------------------------------------------- // EnumWindowsにより呼び出されるコールバック関数 // -------------------------------------------------------------- // 引数 :hWnd=ウィンドウハンドル // 戻り値:TRUE(必ずEnumWindowsを継続させる) // -------------------------------------------------------------- BOOL CALLBACK callback_EnumWindowsProc(HWND hWnd, LPARAM) { char szTitleBar[MAX_GETWINDOWTEXT+1] = "????????????????"; // 調査中のウィンドウのタイトルバーを取得しておく // (コマンドプロンプトの窓は横幅80カラムであると仮定するので、 // タイトルバーの文字列は最大で79バイトまで取得する) GetWindowText(hWnd, szTitleBar, MAX_GETWINDOWTEXT); // もし、タイトルバーが空っぽであれば、「タイトルなし」の文字列を代入する。 if (szTitleBar[0] == '\0') { strcpy(szTitleBar, "タイトルなし"); } // コントロールの一覧を列挙する // (コントロール1つにつき1回ずつ、callback_EnumChildWindowsProc() が呼ばれる) EnumChildWindows(hWnd, (WNDENUMPROC)callback_EnumChildWindowsProc, (LPARAM)szTitleBar); // 返却値TRUE:EnumWindowsによるウィンドウハンドルの列挙を継続する return TRUE; } // -------------------------------------------------------------- // リストビューコントロールの一覧を作成 // -------------------------------------------------------------- // 引数 :なし // 戻り値:なし // -------------------------------------------------------------- void showListViewControls() { // ウィンドウの一覧を列挙する // (ウィンドウ1つにつき1回ずつ、callback_EnumWindowsProc() が呼ばれる) EnumWindows((WNDENUMPROC)callback_EnumWindowsProc, 0); } // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx // 指定されたハンドルのリストビューの内容を出力する(間違い) // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx // 引数 :hListView = リストビューのハンドル // iColumns = 最大カラム数 // 戻り値:1=成功 0=失敗 // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx int showListViewItems(HWND hListView, int iColumns) { char szClassName[MAX_GETWINDOWCLASS+1]; char szItemText[MAX_GETITEMTEXT_LENGTH+1]; // クラス名を取得する if (GetClassName(hListView, szClassName, MAX_GETWINDOWCLASS)) { if ( strcmp(szClassName, WC_LISTVIEW) == 0 ) { // クラス名がWC_LISTVIEW(文字列"SysListView32"を表す)であれば // リストビューであると見なす // アイテム数を取得 int iItemCount = ListView_GetItemCount(hListView); LVITEM lvItem; ZeroMemory(&lvItem, sizeof(LVITEM)); lvItem.mask = LVIF_TEXT; lvItem.cchTextMax = MAX_GETITEMTEXT_LENGTH; lvItem.pszText = szItemText; // アイテムを表示 for(lvItem.iItem=0 ; lvItem.iItem<iItemCount ; lvItem.iItem++) { lvItem.iSubItem = 0; // LVM_GETITEMでアイテムの文字列を取得 // FALSEが返されたら一番右端の文字列を読んだ後とみなして次のアイテムへ for(lvItem.iSubItem=0 ; lvItem.iSubItem<iColumns ; lvItem.iSubItem++) { SendMessage(hListView, LVM_GETITEM, 0, (LPARAM)&lvItem); // 最初のカラムでなければ、区切りのカンマを表示する if (lvItem.iSubItem > 0) { cout << ","; } // 文字列を表示 cout << "\"" << lvItem.pszText << "\""; } // 改行を出力 cout << endl; } return 1; } else { cerr << "指定されたハンドルはリストビュー コントロールではありません。" << endl; return 0; } } else { cerr << "指定されたハンドルのクラス名を取得できません。" << endl; return 0; } } // -------------------------------------------------------------- // メイン関数 // -------------------------------------------------------------- // 引数:1個の場合→環境内のリストビューのハンドル番号一覧を表示 // 2個の場合→エラー // 2個以上の場合→1つめのパラメタ(16進数)で指定されたハンドル番号のリストビューを出力 // 2つめのパラメタ(10進数)で、リストビューのカラムの数を指定する // 戻り値:0=成功 1=失敗 // -------------------------------------------------------------- int main(int argc, char* argv[]) { // コモンコントロールの初期化 InitCommonControls(); // コマンドラインの解析 if ( argc < 2 ) { // コマンドライン引数が何も指定されていない場合は、 // リストビューコントロールのハンドル番号一覧を表示。 showListViewControls(); return 0; } else if ( argc < 3 ) { // エラー。 cerr << "リストビューのカラム数を指定してください。" << endl; return 1; } else { DWORD dwHandle; int iColumns; sscanf(argv[1], "%x", &dwHandle); sscanf(argv[2], "%d", &iColumns); return (!showListViewItems((HWND)dwHandle, iColumns)); } }
■どうなってしまうか?
まず、上のサンプルコードのような方法だと、失敗します。以下の説明では、新たに作ったプログラムの実行ファイル名を「lvcsvcom.exe」であるとして扱っています。
上記のコードを実行するためには、まずは何の引数指定もなしでこのプログラムを実行して目的のリストビューのハンドルを調べて、次に、目的のリストビューのハンドルと取得したいかサブ項目数をオプション指定して実行します。
C:\>lvcsvcom -------------------- いじくるつくーる ハンドル番号:000005d4 アイテム数:41 -------------------- エクスプローラ - Windows95(C:) ハンドル番号:000002a8 アイテム数:9 -------------------- Program Manager ハンドル番号:000000fc アイテム数:12 C:\>lvcsvcom 5d4 5 (ハンドル番号 5d4 のリストビューから、5カラムのサブアイテムまでを取得する) |
lvcsvcom 5d4 5を実行すると、次のような画面になります。
![出力結果](./chapter2a.png)
なぜ、エラーとなってしまうのでしょう? また、エラーを起こしているのはlvcsvcomではなく、Rnsf7.exe (=いじくるつくーる の実行ファイル名)となっています。なぜ、このようなことになってしまうのでしょう?
■なぜできないのか
まず、次のことを知っておかなければなりません。
- 複数のプロセスが動作する環境では、各プロセスごとに独自のメモリ空間を持っている。
各プロセスは、それぞれ独自の環境で動いているかのように見せかけられます。メモリのアドレスに対して言えば、あるプロセスにて 0x11223344 という数値で表されるメモリ位置と、別のプロセスで 0x11223344 という数値で表されるメモリ位置は、全く別のアドレスを指すことになります。
- 他のプロセスのメモリを読み書きすることは、通常はできない。
あるプロセスが他のプロセスからメモリ領域を侵害されることは、通常は起こりえません。強制的行おうとしても失敗します。
- リストビューやツリービューなどのコモンコントロールは、それを所持するプロセスのメモリ空間に対して読み書きを行う。
他のプロセスにあるリストビューに、メモリへの文字列書き込みを指示した場合、リストビューは他のプロセス内のメモリに対して書き込みを行います。自プロセスにあるメモリに書き込みを行ってはくれません。
- 文字列データは大きいので、文字列データが格納されたメモリ領域の先頭アドレスを指し示す数(ポインタ)を介して、データのやりとりを行う。
前回示したリストビューのアイテム数の取得の場合と違って、文字列データは大きいため、文字列はいったんメモリに格納され、そのメモリの先頭アドレスを引き渡す方法にて、データのやりとりが行われます。
つまり、図示すると、こんな状態になっています。
![なぜか?](./chapter2b.png)
メモリへの書き込み指示を出したのはlvcsvcomですが、実際に書き込みを行ったのはリストビューを所持する Rnsf7.exe であることから、エラーは Rnsf7.exe にて発生することになります。ちなみにこのエラーの詳細を調べると、「ページ違反」となっています。
■次回は?
この問題を解決するためには、他プロセスにメモリ確保を行わせ、さらにリストビューに対してそのメモリに対して書き込みを行わせ、最後に他プロセスのメモリからデータを読み取るという手続きを取らなければなりません。その方法については、次回に述べたいと思います。
ここに記載の内容は2005年に書かれたものです。そのため、内容の一部が古くなっていることがありますのでご注意ください。例えば、
- ユーザーアカウント制御(UAC)下では、一般権限のプログラムは、管理権限のプログラムから情報を取得することができません。サンプルプログラム適用時にはご注意ください。
- 本サンプルプログラムは、x86環境下での動作を前提としています。x64環境下ではポインタサイズなどを原因とするいくつかの問題が発生しますのでご注意ください。→詳細
※このページは、ソフトウェアに付属のヘルプファイルをWeb用に再構築したものです。大部分に自動変換を施しているため、一部は正しく変換しきれずに表示の乱れている箇所があるかもしれませんが、ご容赦下さい。また、本ドキュメントはアーカイブドキュメントであり、内容は、右上の作成日付の時点のものとなっております。一部、内容が古くなっている箇所があるかと思いますが、あらかじめご了承下さい。
※このページへは、自由にリンクしていただいてかまいません。
■このページに関するご意見をお待ちしております → フィードバックページ