他のプロセスのコモンコントロールの情報を得る

他のアプリケーションが持つコモンコントロール(例えばタブコントロール)に対して情報を取得したい場合、以下のようなコードでは対象のアプリケーションにアクセス違反が起こって終了してしまいます。

// HWND hTab : 他のプロセスが持つタブコントロールのウィンドウハンドル
TC_ITEM item;
TCHAR szText[256];
item.mask = TCIF_IMAGE | TCIF_TEXT;
item.pszText = szText;
item.cchTextMax = 256;
SendMessage(hTab, TCM_GETITEM, 0, &item); //ここで対象のアプリがアクセス違反で落ちる

これは他のプロセスからは自動変数、つまりスタック領域にアクセスできないためです。アクセス違反を起こさせないためには他のプロセスからアクセスできるような領域を確保しなければなりません。

他のプロセスからアクセスできるような領域は CreateFileMappingMapViewOfFile を使用して確保します。情報を取得したあと、確保した領域の後始末を忘れないようにしてください。

// HWND hTab : 他のプロセスが持つタブコントロールのウィンドウハンドル
// 他のプロセスがアクセスできる領域としてTC_ITEMとpszTextの大きさを確保する
HANDLE hMapping = CreateFileMapping((HANDLE) 0xFFFFFFFF, NULL,
PAGE_READWRITE, 0, sizeof (TC_ITEM) + sizeof (TCHAR) * 256, NULL);
TC_ITEM* pItem = (TC_ITEM*) MapViewOfFile(hMapping, FILE_MAP_WRITE, 0, 0, 0);
pItem->mask = TCIF_IMAGE | TCIF_TEXT;
pItem->pszText = (LPTSTR) (pItem + 1);
pItem->cchTextMax = 256
SendMessage(hTab, TVM_GETITEM, 0, pItem);
... // pItemを使って何か処理をする
UnmapViewOfFile(pItem);
CloseHandle(hMapping);

タブコントロールの他にもツリービューコントロールやリストビューコントロール等でも同様です。

ただし、リストボックスやコンボボックスなど昔からあるようなコントロールでは上記のようなことをしなくても大丈夫なようです。互換性のためでしょうか?

(98/1/15追記)
リストボックスなどで普通に呼び出せば情報取得ができるのは、リストボックスはシステム寄りなコントロールであるからではなかろうかと思うようになってきた。 CS_GLOBALCLASS がたっていなくてもグローバルなクラスだし。
(以上98/1/15追記)

(98/1/23追記)
NTではファイルマッピングオブジェクトを使用するプロセスは用意する方、使う方のどちらもが CreateFileMappingOpenFileMapping のどちらかを呼んでいる必要があるらしく、上記の方法ではアクセス違反を起こしてしまいます。

ではどうすればいいかというとNT(ただし4以降)だけで使用できる VirtualAllocEx, VirtualFreeEx というAPI を使用します。この API は対象のプロセスに仮想メモリ領域の確保/開放を行うものです。ただし、この API で確保した領域はこちら側からは普通にはアクセスできないので WriteProcessMemory / ReadProcessMemory という APIを利用します。これは対象のプロセスが持つメモリ空間にアクセスするためのAPIです。これらを使って上の例を書き直すと以下のようになります。

// HWND hTab : 他のプロセスが持つタブコントロールのウィンドウハンドル
// 確保する領域のサイズ
DWORD dwSize = sizeof (TC_ITEM) + sizeof (TCHAR) * 256;
// 対象のプロセスのIDを取得
DWORD dwPID;
GetWindowThreadProcessId(hTab, &dwPID);
// IDからプロセスのハンドルを取得
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID);
// タブコントロールを持つプロセスにメモリを確保
TC_ITEM* pItemAP = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
// 確保した領域に書き込むデータを作成
TC_ITEM* pItem = (TC_ITEM*) alloca(dwSize);
pItem->mask = TCIF_IMAGE | TCIF_TEXT;
pItem->pszText = (LPTSTR) (pItemAP + 1);
pItem->cchTextMax = 256;
// 確保した領域にデータを書き込む
WriteProcessMemory(hProcess, pItemAP, pItem, dwSize);
SendMessage(hTab, TVM_GETITEM, 0, pItemAP);
// 確保した領域からデータを読込む
ReadProcessMemory(hProcess, pItemAP, pItem, dwSize);
... // pItemLocalを使って何か処理をする
// 確保した領域を開放
VirtualFree(hProcess, pItemAP, dwSize, MEM_RELEASE);
CloseHandle(hProcess);

実際にはVirtualAlloc(Ex) / VirtualFree(Ex) を使用する場合、例のように一々確保/開放を繰り返すのは良くないようです。何処かでまとめて確保/開放を行うようなクラスを作成したほうがよいでしょう。

また VirtualAllocEx / VirtualFreeEx はWindows95(少なくとも古いもの)では用意されていません。使用できないのではなく用意されていないのです。つまりVC5やVC6でビルドするとコンパイルは通りますが、95で実行すると「VirtualAllocEx という関数がありません」のようなメッセージが出て起動できません。 GetVersion などで場合分けして VirtualAllocEx を使わないようにしていてもです。

このような場合VC6であればDelayLoadするDLLにkernel32.dllを指定することで回避できますが(多分)、VC5以前で回避するためには自前でDelayLoadを行う必要があります。この場合には以下のようにすればよいでしょう。

static class CVirtualMemory {
private:
HMODULE m_hKernel32;
LPVOID (WINAPI* m_pfnVirtualAllocEx)(HANDLE, LPVOID, DWORD, DWORD, DWORD);
BOOL (WINAPI* m_pfnVirtualFreeEx)(HANDLE, LPVOID, DWORD, DWORD);
public:
CVirtualMemory():m_hKernel32(NULL), m_pfnVirtualAllocEx(NULL), m_pfnVirtualFreeEx(NULL)
{
m_hKernel32 = ::GetModuleHandle(szKernel32);
if (m_hKernel32 != NULL){
(FARPROC&)m_pfnVirtualAllocEx = ::GetProcAddress(m_hKernel32, szVirtualAllocEx);
(FARPROC&)m_pfnVirtualFreeEx = ::GetProcAddress(m_hKernel32, szVirtualFreeEx);
}
}
LPVOID _VirtualAllocEx(HANDLE hProcess, LPVOID lpAddress, DWORD dwSize, DWORD dwAllocationType, DWORD dwProtect)
{return (m_pfnVirtualAllocEx != NULL) ? (*m_pfnVirtualAllocEx)(hProcess, lpAddress, dwSize, dwAllocationType, dwProtect) : NULL;}
BOOL _VirtualFreeEx(HANDLE hProcess, LPVOID lpAddress, DWORD dwSize, DWORD dwFreeType)
{return (m_pfnVirtualFreeEx != NULL) ? (*m_pfnVirtualFreeEx)(hProcess, lpAddress, dwSize, dwFreeType) : FALSE;}
} vm;
#define VirtualAllocEx vm._VirtualAllocEx
#define VirtualFreeEx vm._VirtualFreeEx

こうすることでkernel32.dllに VirtualAllocEx / VirtualFreeEx が存在するときだけそれらを呼び出すようになります。
(以上98/1/23追記)

(98/1/23追記)
リストボックスではうまく動く理由について、作田さんから情報をいただきました。

Advanced Windows 改定第3版 ASCII出版 ISBN4-7561-2129-2 によると、
>>>
16ビット Windowsでは、あるアプリケーションが、別のアプリケーシ
ョンのウィンドウに対して LB_GETTEXT する可能性があった。(略)
簡単に移植できるようにするために、この動作を保証(略)
新しいコモンコントロールは、16ビット Windowsに存在しない(略)
<<<
だそうです。

なるほど、やっぱり互換性のためだったんですね。
(以上98/1/23追記)


戻る