Written in Japanese(UTF-8)
2014.8.30
INASOFT


/トップ/ListView to CSV/ダウンロード/WebHelp/ヘルプトップ

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


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

第4回(最終回)

第3回へ←   →番外編へ


■何が起きていたのか

前回は、Windows NT/2000/XP上では問題なく動作するプログラムができたものの、Windows 95/98/Me では使用できないAPIを使っていたプログラムであったため、Windows 95/98/Me では正しく動作しないプログラムとなっていました。

特にWindows 95では、問題のAPIを用いている部分に到達する前に、プログラムの開始時点でエラーとなってしまいました。これは、プログラムとDLLが強く結びついていることにより起こる現象です。

対応策としては、次の2つがあります。

使用目的が限定されたプログラムであれば、後者のような選択肢も十分アリかと思いますが、ここでは前者を選ぶことにしましょう。

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

VirtualAllocEx / VirtualFreeEx の使用宣言を、プログラム開始時点で暗黙のうちに行うのではなく、VirtualAllocExVirtualFreeEx の使用直前まで遅延させます。また、OSの種別が Windows 95/98/Me であれば、「ファイルマッピングオブジェクト」という別の手段を使うことにします。OSの種類を取得するためには、GetVersionEx API を用います(この講座では説明を割愛します)。

(下記に示すサンプルコードは、Windows 95/98/Me/NT/2000/XPにおいて動作します)


// 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

// Windows NT系のみでしか利用できない関数の定義
typedef LPVOID (WINAPI *PF_VIRTUALALLOCEX)(HANDLE,LPVOID,DWORD,DWORD,DWORD);
typedef BOOL   (WINAPI *PF_VIRTUALFREEEX)(HANDLE,LPVOID,DWORD,DWORD);

// --------------------------------------------------------------
// 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;
    OSVERSIONINFO osvi;

    ZeroMemory(&osvi, sizeof(OSVERSIONINFO));
    osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
    GetVersionEx( &osvi );
    
    ZeroMemory(&lvItem, sizeof(LVITEM));
    lvItem.mask       = LVIF_TEXT;
    lvItem.cchTextMax = MAX_GETITEMTEXT_LENGTH;
    lvItem.iItem      = iItem;
    lvItem.iSubItem   = iSubItem;

    if ( osvi.dwPlatformId == VER_PLATFORM_WIN32_NT ) {
        // ※Windows NT/2000/XP の場合

       // Windows NT系でしか利用できない関数を、Windows 95系に感知させない形で
       // 呼び出すための工夫
       HMODULE hDLL = GetModuleHandle("kernel32.dll");
       PF_VIRTUALALLOCEX pVirtualAllocEx = (PF_VIRTUALALLOCEX)GetProcAddress(hDLL,"VirtualAllocEx");
       PF_VIRTUALFREEEX  pVirtualFreeEx  = (PF_VIRTUALFREEEX)GetProcAddress(hDLL, "VirtualFreeEx");

       // ※注意:本来は、GetModuleHandle() の戻り値やGetProcAddress() の
       //     戻り値をチェックしなければなりませんが、kernel32.dll が
       //     存在しない環境は考えられないことから、ここでは行いません。
       //     (本来の学習内容に集中したい、という意図もあります)
       //     また、kernel32 は必ずメモリに存在することから、LoadLibrary() は書かれていません。

        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 *)pVirtualAllocEx(hProcess, NULL, MAX_GETITEMTEXT_LENGTH+1, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
            LVITEM *pvlvi = (LVITEM *)pVirtualAllocEx(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);
            }

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

            // 他プロセスのプロセスハンドルを閉じる
            CloseHandle(hProcess);
        }
    }
    else {
        // ※Windows 95/98/Me の場合

        //共有メモリを作成(ファイルマッピングオブジェクト)
        HANDLE hFileMap1 = CreateFileMapping((HANDLE)0xFFFFFFFF, NULL, PAGE_READWRITE, 0, MAX_GETITEMTEXT_LENGTH,"tmp_pvbufc4");
        HANDLE hFileMap2 = CreateFileMapping((HANDLE)0xFFFFFFFF, NULL, PAGE_READWRITE, 0, sizeof(LVITEM), "tmp_lvitemc4");
        char *pvbuf = (char *)MapViewOfFile(hFileMap1, FILE_MAP_ALL_ACCESS, 0, 0, 0);
        LVITEM *pvlvi = (LVITEM *)MapViewOfFile(hFileMap2, FILE_MAP_ALL_ACCESS, 0, 0, 0);

        lvItem.pszText = pvbuf;
        memcpy(pvlvi, &lvItem, sizeof(LVITEM));
        
        // 共有メモリのポインタを指して、LVM_GETITEMを送る
        lResult = (int)SendMessage(hListView, LVM_GETITEM, 0, (LPARAM)pvlvi);

        if (lResult == TRUE) { // 成功していたら?
            memcpy(pszItemText, pvbuf, MAX_GETITEMTEXT_LENGTH);
        }

        //共有メモリを開放
        UnmapViewOfFile(pvbuf);
        CloseHandle(hFileMap1);
        UnmapViewOfFile(pvlvi);
        CloseHandle(hFileMap2);
    }
    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));
    }
}

これをWindows 95/98/Me上で実行すると、次のような実行結果を得られます。

出力結果

■何を行っているのか?

まず、VirtualAllocEx / VirtualFreeEx の使用を、VirtualAllocEx / VirtualFreeEx の使用直前まで遅延させます。これは、下記の部分のコードが該当します。

// 冒頭部分
typedef LPVOID (WINAPI *PF_VIRTUALALLOCEX)(HANDLE,LPVOID,DWORD,DWORD,DWORD);
typedef BOOL   (WINAPI *PF_VIRTUALFREEEX)(HANDLE,LPVOID,DWORD,DWORD);

// getLVTextInOtherProcess内
HMODULE hDLL = GetModuleHandle("kernel32.dll");
PF_VIRTUALALLOCEX pVirtualAllocEx = (PF_VIRTUALALLOCEX)GetProcAddress(hDLL,"VirtualAllocEx");
PF_VIRTUALFREEEX  pVirtualFreeEx  = (PF_VIRTUALFREEEX)GetProcAddress(hDLL, "VirtualFreeEx");

GetModuleHandle にて、該当APIを提供するDLLに直接アクセスし、該当APIの関数ポインタを取得します。該当APIへは、関数ポインタを経由してアクセスします。これで、VirtualAllocEx / VirtualFreeEx の使用は、GetProcAddress の時点まで遅延させることができます。

次に、VirtualAllocEx / VirtualFreeEx の代替手段として、Windows 95/98/Me では、ファイルマッピングオブジェクトを使います。これは、ファイルにメモリ空間をマップして、そのファイルを経由してプロセス間のメモリ共有を図るものです。

使用するAPIは、CreateFileMappingMapViewOfFile です。

図示

なお、CreateFileMapping / MapViewOfFile で確保したメモリを解放するには、CloseHandle / UnmapViewOfFileを使います。

■おわりに

以上のような各種対策を行うことで、リストビューをCSV化してコンソールに出力するプログラムが完成します。

同様の方法により、ツリービューからのデータ取得を行うこともできますので、是非チャレンジしてみてください。

さて、以上でListView to CSV コマンドライン版のプログラミング講座は終わりです。ListView to CSVは、これまで公開してきたソフトの中では、かなり特殊なことを行っている部類のソフトウェアに入ります(すっきり!! デフラグ や いじくるつくーるのように、単純な命令の積み重ねでできるようなソフトウェアではないということです)。自分でも忘れないようにするために、この講座を書いたわけですが、何かのお役に立てたならば幸いです。

なお、ここに記載しているサンプルコードですが、特に筆者に許可を求めなくても、流用してかまいません。ご自由にお使い下さい。

第3回へ←   →番外編へ


ここに記載の内容は2005年に書かれたものです。そのため、内容の一部が古くなっていることがありますのでご注意ください。例えば、
  • ユーザーアカウント制御(UAC)下では、一般権限のプログラムは、管理権限のプログラムから情報を取得することができません。サンプルプログラム適用時にはご注意ください。
  • 本サンプルプログラムは、x86環境下での動作を前提としています。x64環境下ではポインタサイズなどを原因とするいくつかの問題が発生しますのでご注意ください。→詳細


※このページは、ソフトウェアに付属のヘルプファイルをWeb用に再構築したものです。大部分に自動変換を施しているため、一部は正しく変換しきれずに表示の乱れている箇所があるかもしれませんが、ご容赦下さい。また、本ドキュメントはアーカイブドキュメントであり、内容は、右上の作成日付の時点のものとなっております。一部、内容が古くなっている箇所があるかと思いますが、あらかじめご了承下さい。
※このページへは、自由にリンクしていただいてかまいません。

■このページに関するご意見をお待ちしております → フィードバックページ

/トップ/ListView to CSV/ダウンロード/WebHelp/ヘルプトップ