EternalWindows
コンソール / コンソールへのアタッチ

コンソールへのアタッチとは、他のプロセスが表示しているコンソールに対して入出力を行えるようにすることです。 コンソールにアタッチしてGetStdHandleを呼び出した場合、 そのコンソールの標準入出力ハンドルを取得することができるため、 コンソールに書き込まれている文字を読み取ることが可能になります。 通常、コンソールへの入出力をアプリケーションが検出したい場合はリダイレクトを使用しますが、 AllocConsoleによって明示的にコンソールを表示するアプリケーションではこの方法が使用できないため、 このような場合はコンソールへのアタッチが必要になります。 コンソールへアタッチするには、AttachConsoleを呼び出します。

BOOL WINAPI AttachConsole(
  DWORD dwProcessId
);

dwProcessIdは、アタッチ先のコンソールを表示しているプロセスのIDを指定します。

アタッチ先のコンソールの標準出力ハンドルがあれば、 ReadConsoleOutputCharacterでコンソールに書き込まれている文字を取得できます。

BOOL WINAPI ReadConsoleOutputCharacter(
  HANDLE hConsoleOutput,
  LPTSTR lpCharacter,
  DWORD nLength,
  COORD dwReadCoord,
  LPDWORD lpNumberOfCharsRead
);

hConsoleOutputは、スクリーンバッファのハンドルを指定します。 lpCharacterは、読み取った文字列を受け取るバッファを指定します。 nLengthは、読み取りたい文字数を指定します。 dwReadCoordは、文字の読み取りを開始する位置を表すCOORD構造体のアドレスを指定します。 lpNumberOfCharsReadは、読み取られた文字数を受け取る変数のアドレスを指定します。

今回のプログラムは、起動時と同時にコマンドプロンプトを起動します。 アッタチ先のコンソールはコマンドプロンプトが表示するコンソールとし、 このコンソールに表示されている内容をエディットコントロールに表示します。

#include <windows.h>

#define ID_ATTACH 100

void InitializeMenuItem(HMENU hmenu, LPTSTR lpszItemName, int nId);
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR      szAppName[] = TEXT("sample");
	HWND       hwnd;
	MSG        msg;
	WNDCLASSEX wc;

	wc.cbSize        = sizeof(WNDCLASSEX);
	wc.style         = 0;
	wc.lpfnWndProc   = WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = hinst;
	wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = szAppName;
	wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (RegisterClassEx(&wc) == 0)
		return 0;

	hwnd = CreateWindowEx(0, szAppName, szAppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hinst, NULL);
	if (hwnd == NULL)
		return 0;

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);
	
	while (GetMessage(&msg, NULL, 0, 0) > 0) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return (int)msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	static HWND  hwndEdit = NULL;
	static DWORD dwProcessId = 0;
	
	switch (uMsg) {

	case WM_CREATE: {
		HMENU               hmenu;
		STARTUPINFO         startupInfo;
		PROCESS_INFORMATION processInformation;

		hmenu = CreateMenu();
		InitializeMenuItem(hmenu, TEXT("アタッチ(&A)"), ID_ATTACH);
		SetMenu(hwnd, hmenu);

		hwndEdit = CreateWindowEx(0, TEXT("EDIT"), TEXT(""), WS_CHILD | WS_VISIBLE | ES_MULTILINE | ES_READONLY | WS_VSCROLL, 0, 0, 0, 0, hwnd, (HMENU)2, ((LPCREATESTRUCT)lParam)->hInstance, NULL);	
		
		ZeroMemory(&startupInfo, sizeof(STARTUPINFO));
		startupInfo.cb = sizeof(STARTUPINFO);
		CreateProcess(TEXT("C:\\Windows\\System32\\cmd.exe"), NULL, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInformation);
		dwProcessId = processInformation.dwProcessId;
		CloseHandle(processInformation.hThread);
		CloseHandle(processInformation.hProcess);

		return 0;
	}

	case WM_COMMAND: {
		int                        i, n;
		HANDLE                     hStdOutput;
		DWORD                      dwReadByte;
		TCHAR                      szBuf[256];
		COORD                      coord;
		CONSOLE_SCREEN_BUFFER_INFO screenBuffer;

		if (LOWORD(wParam) != ID_ATTACH)
			return 0;
		
		if (!AttachConsole(dwProcessId)) {
			MessageBox(NULL, TEXT("アタッチに失敗しました。"), NULL, MB_ICONWARNING);
			return 0;
		}
		
		hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	
		GetConsoleScreenBufferInfo(hStdOutput, &screenBuffer);
		coord.X = 0;
		coord.Y = 0;
		n = 0;

		SetWindowText(hwndEdit, TEXT(""));

		for (i = 0; i < 40; i++) {
			ReadConsoleOutputCharacter(hStdOutput, szBuf, screenBuffer.dwSize.X, coord, &dwReadByte);
			szBuf[dwReadByte - 1] = '\0';
			SendMessage(hwndEdit, EM_SETSEL, n, n + 1);
			SendMessage(hwndEdit, EM_REPLACESEL, 0, (LPARAM)szBuf);

			n += screenBuffer.dwSize.X;
			SendMessage(hwndEdit, EM_SETSEL, n, n + 1);
			SendMessage(hwndEdit, EM_REPLACESEL, 0, (LPARAM)TEXT("\r\n"));
			n += 2;

			coord.Y++;
		}
		
		FreeConsole();
		return 0;
	}

	case WM_SIZE:
		MoveWindow(hwndEdit, 0, 0, LOWORD(lParam), HIWORD(lParam), TRUE);
		return 0;
	
	case WM_CTLCOLORSTATIC:
		return (LRESULT)GetStockObject(WHITE_BRUSH);

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

void InitializeMenuItem(HMENU hmenu, LPTSTR lpszItemName, int nId)
{
	MENUITEMINFO mii;
	
	mii.cbSize     = sizeof(MENUITEMINFO);
	mii.fMask      = MIIM_ID | MIIM_TYPE;
	mii.fType      = MFT_STRING;
	mii.wID        = nId;
	mii.dwTypeData = lpszItemName;
	
	InsertMenuItem(hmenu, nId, FALSE, &mii);
}

WM_CREATEではアッタチを開始するためのメニューと、 コンソールの内容を表示するためのエディットコントロールを作成します。 また、CreateProcessでコマンドプロンプトを起動し、プロセスIDを保存しています。 メニューの「アタッチ」が選択された場合は、AttachConsoleの呼び出しとコンソールの内容を読み取る処理が行われます。 まずは、次の部分から確認します。

if (!AttachConsole(dwProcessId)) {
	MessageBox(NULL, TEXT("アタッチに失敗しました。"), NULL, MB_ICONWARNING);
	return 0;
}

hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);

GetConsoleScreenBufferInfo(hStdOutput, &screenBuffer);
coord.X = 0;
coord.Y = 0;
n = 0;

SetWindowText(hwndEdit, TEXT(""));

AttachConsoleの呼び出しに成功した場合、GetStdHandleが返すハンドルはアタッチ先コンソールのハンドルになります。 よって、これをReadConsoleOutputCharacterに指定して、コンソールの内容を読み取ることができるようになります。 GetConsoleScreenBufferInfoを呼び出しているのは、コンソールのスクリーンバッファのサイズを取得するためです。 スクリーンバッファから1行の文字列を読み取るためには、コンソールのスクリーンバッファの幅(列単位)が必要です。 coordは、スクリーンバッファにおける読み取り開始位置を表します。 通常は、1行1列目から読み取りを開始するべきですから、(0, 0)を指定します。 nは、エディットコントロール上での文字の位置を格納します。 SetWindowTextに空文字を指定しているのは、エディットコントロールの内容をクリアするためです。 後続の処理は次のようになっています。

for (i = 0; i < 40; i++) {
	ReadConsoleOutputCharacter(hStdOutput, szBuf, screenBuffer.dwSize.X, coord, &dwReadByte);
	szBuf[dwReadByte - 1] = '\0';
	SendMessage(hwndEdit, EM_SETSEL, n, n + 1);
	SendMessage(hwndEdit, EM_REPLACESEL, 0, (LPARAM)szBuf);

	n += screenBuffer.dwSize.X;
	SendMessage(hwndEdit, EM_SETSEL, n, n + 1);
	SendMessage(hwndEdit, EM_REPLACESEL, 0, (LPARAM)TEXT("\r\n"));
	n += 2;

	coord.Y++;
}

FreeConsole();

コンソールから何行読み取るかにによって、iの上限値は変化します。 上記の例では40としていますが、全ての行を読み取りたい場合はscreenBuffer.dwSize.Yを指定します。 ReadConsoleOutputCharacterの第3引数には、1行の文字数であるscreenBuffer.dwSize.Xを指定します。 これで、コンソールの1行を読み取れたことになるので、文字列の終端にNULL文字を追加し、 EM_REPLACESELでエディットコントロールに追加することになります。 ただし、EM_REPLACESELを送信する場合は、追加位置を明確にするためにEM_SETSELを先に送信します。 1行を読み取ったら次の行を読み取る必要がありますが、 その前にエディットコントロールを次の行へ改行させる必要があります。 まずは、さきほど追加した文字列の分だけnを増加させ、 それからEM_REPLACESELで改行コードを追加します。 エディットコントロールは改行コードも文字数としてカウントするため、nを2つ増加さしておきます。 コンソールの次の行を読み取るには、coord.Yを1つ増加させるだけです。 コンソールへのアタッチは、FreeConsoleの呼び出しによって終了します。


戻る