Written in Japanese(Shift-JIS)
2005. 7. 7
INASOFT
/トップ/ユーティリティとゲーム/ListView to CSV/作ってみよう

Icon ListView to CSVを作ってみよう!!


ListView to CSVの簡単なコマンドライン版を作ってみようという、プログラミング解説講座です。
第3回では、他プロセスの所有するリストビュー コントロールから文字列を取得する方法について述べたいと思います。

第3回

第2回へ←   →第4回(最終回)へ


>■前回の失敗をふまえて

前回は、リストビューの文字列書き込みに関する特性をふまえずにコーディングを行ってしまったため、予期しない事態が起こってしまいました。

今回はリストビューの文字列書き込みに関する特性をふまえ、他プロセスにメモリ確保を行わせ、さらにリストビューに対してそのメモリに対して書き込みを行わせ、最後に他プロセスのメモリからデータを読み取るという手続きを取ることにします。

(ちなみに、逆のアプローチとして、リストビューの文字列書き込みを、自プロセスのメモリに対して行わせるというアイディアも思いつくかもしれませんが、これはできません。なぜなら、リストビューの機能はOSが提供するものであり、そのプログラムの内容に対して手を加えることはできないからです)

■サンプルのソースコード

リストビューから文字列を取得する方法として LVM_GETITEM を使うという点は、前回と同じです。今回は、LVM_GETITEM のための入出力領域を、他プロセス内のメモリを介して行います。

(ちなみに、下記に示すサンプルコードは、Windows NT/2000/XP用のものです。Windows 95/98/Me用のサンプルコードとバイナリ互換コードについては、次回解説いたします)


// 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);
}


// --------------------------------------------------------------
// 別プロセスにあるかもしれないリストビューのアイテム文字列を取得
// --------------------------------------------------------------
// 引数 :hListView = リストビューのハンドル
//     iItem     = 取得したい文字列を持つアイテムの番号
//     iSubItem  = 取得したい文字列を持つアイテムのカラム番号
//     szItemText= 取得したい文字列を格納するバッファへのアドレス
// 戻り値:TRUE=成功  FALSE=失敗
// --------------------------------------------------------------
LRESULT getLVTextInOtherProcess(HWND hListView, int iItem, int iSubItem, char *pszItemText)
{
    LVITEM lvItem;
    LRESULT lResult = FALSE;
    
    ZeroMemory(&lvItem, sizeof(LVITEM));
    lvItem.mask       = LVIF_TEXT;
    lvItem.cchTextMax = MAX_GETITEMTEXT_LENGTH;
    lvItem.iItem      = iItem;
    lvItem.iSubItem   = iSubItem;

    // 注意:Windows NT/2000/XP の場合にのみ有効
    DWORD dwProcessId = 0;
    HANDLE hProcess;

    // 対象となるリストビューのハンドルを保持するプロセスのIDを得る
    GetWindowThreadProcessId(hListView, &dwProcessId);
    hProcess = OpenProcess( PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE, FALSE, dwProcessId);

    if ( hProcess ) {
        // 他プロセス空間内にメモリを確保。
        // 他プロセスのリストビューは、そのリストビューを保持するプロセス内のメモリを
        // 読み書きの対象とするので。
        char   *pvbuf = (char *)VirtualAllocEx(hProcess, NULL, MAX_GETITEMTEXT_LENGTH+1, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
        LVITEM *pvlvi = (LVITEM *)VirtualAllocEx(hProcess, NULL, sizeof(LVITEM), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
        // 他プロセス内のバッファへのポインタを指す
        lvItem.pszText = pvbuf;
        // 他プロセス内に準備したLVITEMへ、LVM_GETITEM用のパラメタをコピー
        WriteProcessMemory(hProcess, pvlvi, &lvItem, sizeof(LVITEM), NULL);

        // 他プロセス内のポインタを指して、LVM_GETITEMを送る
        lResult = (int)SendMessage(hListView, LVM_GETITEM, 0, (LPARAM)pvlvi);

        if (lResult == TRUE) { // 成功していたら?
            // 他プロセス内に確保したバッファから、自プロセス内のバッファへ文字列をコピー
            ReadProcessMemory(hProcess, pvbuf, pszItemText, MAX_GETITEMTEXT_LENGTH, NULL);
        }

        // 他のプロセス内に確保したメモリを解放
        VirtualFreeEx(hProcess, pvlvi, 0, MEM_RELEASE);
        VirtualFreeEx(hProcess, pvbuf, 0, MEM_RELEASE);

        // 他プロセスのプロセスハンドルを閉じる
        CloseHandle(hProcess);
    }
    return lResult;
}

// --------------------------------------------------------------
// 指定されたハンドルのリストビューの内容を出力する
// --------------------------------------------------------------
// 引数 :hListView = リストビューのハンドル
//     iColumns  = 最大カラム数
// 戻り値:1=成功  0=失敗
// --------------------------------------------------------------
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);
            int iItem, iSubItem;

            // アイテムを表示
            for(iItem=0 ; iItem<iItemCount ; iItem++) {
                iSubItem = 0;
                // LVM_GETITEMでアイテムの文字列を取得
                for(iSubItem=0 ; iSubItem<iColumns ; iSubItem++) {
                    getLVTextInOtherProcess(hListView, iItem, iSubItem, szItemText);
                    // 最初のカラムでなければ、区切りのカンマを表示する
                    if (iSubItem > 0) {
                        cout << ",";
                    }

                    // 文字列を表示
                    cout << "\"" << szItemText << "\"";
                }

                // 改行を出力
                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));
    }
}

これを実行すると、次のような実行結果を得られます。

出力結果

■何を行っているのか?

VirtualAllocEx は、指定したプロセスにメモリ確保を行わせるためのAPIです。そして、ReadProcessMemory は、指定したプロセスからメモリの内容を読み込むためのAPIです。これらを組み合わせて、他プロセス内のリストビューから文字列を得ることができます。

図解

なお、これで他プロセス内の文字列を読み出すことができますが、これでは不十分です。

リストビューから文字列を取得するためには、LVM_GETITEM メッセージを使いますが、このメッセージは指示を受けるための構造体へのポインタを受け取ります。このポインタも、相手先プロセス内のメモリに無ければなりません。

そのため、この構造体自体も、VirtualAllocEx を使って相手先プロセス内にメモリ確保し、WriteProcessMemoryを使って指示データを書き込みます。

なお、VirtualAllocEx を使って相手先プロセス内にメモリ確保したら、VirtualFreeEx を使ってメモリを解放しなければなりません。

■次回は?

ところで今回のサンプルコードは、Windows NT/2000/XP上においてのみ動作します。Windows 95/98/Me においては、うまく動作しません。例えば、Windows 95では、次のようなダイアログが表示されます。

図解

これは、今回対策のために用いたAPI VirtualAllocExVirtualFreeEx は、Windows NT/2000/XP上においてのみ動作するAPIであるためです。このようなAPIを利用する場合、該当するAPIを使用している部分が動かないばかりか、プログラム全体が動作しなくなります。VirtualAllocEx / VirtualFreeEx に対する代替案と、プログラム全体が動作しなくなってしまうことについての解決策は、次回(最終回)にて説明したいと思います。

第2回へ←   →第4回(最終回)へ


ここに記載の内容は2005年に書かれたものです。そのため、内容の一部が古くなっていることがありますのでご注意ください。例えば、
/トップ/ユーティリティとゲーム/ListView to CSV/作ってみよう