EternalWindows
コンソール / 低レベル入出力

これまでWriteConsoleを通じてコンソールへの出力を行ってきましたが、 この関数が文字を書き込む位置は、常に現在のカーソルの位置を始点としていました。 今回取り上げるWriteConsoleOutputCharacterはWriteConsoleよりも低レベルな関数であり、 任意の位置に文字を書き込む機能を提供します。

BOOL WINAPI WriteConsoleOutputCharacter(
  HANDLE hConsoleOutput,
  LPCTSTR lpCharacter,
  DWORD nLength,
  COORD dwWriteCoord,
  LPDWORD lpNumberOfCharsWritten
);

hConsoleOutputは、スクリーンバッファのハンドルを指定します。 lpCharacterは、スクリーンバッファに書き込む文字列を格納したバッファを指定します。 nLengthは、lpCharacterのサイズを指定します。 dwWriteCoordは、書き込み先の位置を格納したCOORD構造体を指定します。 文字の位置は、ピクセルではなく行列単位で指定します。 lpNumberOfCharsWrittenは、書き込まれた文字数を受け取る変数のアドレスを指定します。

入力において低レベルな関数は、ReadConsoleInputです。 この関数は何か1つでもキーが押された場合にそのキーの情報を返します。

BOOL WINAPI ReadConsoleInput(
  HANDLE hConsoleInput,
  PINPUT_RECORD lpBuffer,
  DWORD nLength,
  LPDWORD lpNumberOfEventsRead
);

hConsoleInputは、入力バッファのハンドルを指定します。 lpBufferは、入力されたデータを受け取るINPUT_RECORD構造体のアドレスを指定します。 nLengthは、lpBufferのレコード数を指定します。 lpNumberOfEventsReadは、読み取ったレコード数を受け取る変数のアドレスを指定します。

ReadConsoleInputを呼び出せば方向キーの押下など検出することができますから、 これに併せてカーソルの現在位置を変更したい場合があるかもしれません。 このような場合は、SetConsoleCursorPositionを呼び出します。

BOOL WINAPI SetConsoleCursorPosition(
  HANDLE hConsoleOutput,
  COORD dwCursorPosition
);

hConsoleOutputは、スクリーンバッファのハンドルを指定します。 dwCursorPositionは、新しいカーソルの位置を格納したCOORD構造体を指定します。 カーソルの現在位置を取得したい場合は、GetConsoleScreenBufferInfoを呼び出します。

今回のプログラムは、横方向に10個の文字をコンソールに書き込みます。 この文字を書き替えたい場合は方向キーを押してカーソルを移動させ、 書き替えたい文字を入力します。

#include <windows.h>

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	int          i;
	TCHAR        szBuf[256];
	HANDLE       hStdOutput, hStdInput;
	COORD        coord, coordCursor;
	DWORD        dwWriteByte, dwReadEvent;
	INPUT_RECORD inputRecord;
	
	AllocConsole();
	
	hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	hStdInput = GetStdHandle(STD_INPUT_HANDLE);
	
	coord.X = 0;
	coord.Y = 0;

	for (i = 0; i < 10; i++) {
		wsprintf(szBuf, TEXT("%c"), 'A' + i);
		WriteConsoleOutputCharacter(hStdOutput, szBuf, lstrlen(szBuf), coord, &dwWriteByte);
		coord.X++;
	}	
	
	coordCursor.X = 0;
	coordCursor.Y = 0;

	for (;;) {
		if (!ReadConsoleInput(hStdInput, &inputRecord, 1, &dwReadEvent))
			continue;

		if (inputRecord.EventType == KEY_EVENT && inputRecord.Event.KeyEvent.bKeyDown) {
			if (inputRecord.Event.KeyEvent.wVirtualKeyCode == VK_RETURN)
				break;
			else if (inputRecord.Event.KeyEvent.wVirtualKeyCode == VK_LEFT) {
				if (--coordCursor.X < 0)
					coordCursor.X = 0;
				else
					SetConsoleCursorPosition(hStdOutput, coordCursor);
			}
			else if (inputRecord.Event.KeyEvent.wVirtualKeyCode == VK_RIGHT) {
				if (++coordCursor.X > 9)
					coordCursor.X = 9;
				else
					SetConsoleCursorPosition(hStdOutput, coordCursor);
			}
			else {
				if (IsCharAlpha(inputRecord.Event.KeyEvent.wVirtualKeyCode)) {
					wsprintf(szBuf, TEXT("%c"), inputRecord.Event.KeyEvent.wVirtualKeyCode);
					WriteConsoleOutputCharacter(hStdOutput, szBuf, lstrlen(szBuf), coordCursor, &dwWriteByte);
				}
			}
		}
	}

	FreeConsole();

	return 0;
}

ループ文でWriteConsoleOutputCharacterを10回呼び出していることから、 コンソールに10個の文字が書き込まれることになります。 書き込み位置はCOORD構造体のcoordで表され、初期値が(0, 0)になっていることから、 文字はコンソールの左上隅から書き込まれることになります。 coord.Xが常に増加し、coord.Yが0のまま固定であることから、 1行目の1列目から1行目の10列目まで文字が書き込まれます。

キー入力を検出するループでは、まずReadConsoleInputを呼び出して入力イベントが発生したかを確認します。 一見するとこの関数は入力イベントが発生するまで制御を返さないように思えますが、 実際にはイベントが発生していなくても制御を返すことになっています。 しかし、内部で多少の待機処理を行っているのか、ループによってCPU使用率が上昇するようなことはないようです。 関数が成功した場合は何らかのイベントが発生したことを意味するため、 INPUT_RECORD構造体のEventTypeからイベントの種類を確認することになります。 この値がKEY_EVENTで、さらにEvent.KeyEvent.bKeyDownがTRUEである場合はキーが押されたことになります。

if (inputRecord.EventType == KEY_EVENT && inputRecord.Event.KeyEvent.bKeyDown) {
	if (inputRecord.Event.KeyEvent.wVirtualKeyCode == VK_RETURN)
		break;
	else if (inputRecord.Event.KeyEvent.wVirtualKeyCode == VK_LEFT) {
		if (--coordCursor.X < 0)
			coordCursor.X = 0;
		else
			SetConsoleCursorPosition(hStdOutput, coordCursor);
	}
	else if (inputRecord.Event.KeyEvent.wVirtualKeyCode == VK_RIGHT) {
		if (++coordCursor.X > 9)
			coordCursor.X = 9;
		else
			SetConsoleCursorPosition(hStdOutput, coordCursor);
	}
	else {
		DWORD dwCharCode;

		dwCharCode = MapVirtualKey(inputRecord.Event.KeyEvent.wVirtualKeyCode, 2);
		if (IsCharAlpha(LOWORD(dwCharCode))) {
			wsprintf(szBuf, TEXT("%c"), LOWORD(dwCharCode));
			WriteConsoleOutputCharacter(hStdOutput, szBuf, lstrlen(szBuf), coordCursor, &dwWriteByte);
		}
	}
}

押されたキーの種類は、Event.KeyEvent.wVirtualKeyCodeから確認することができます。 これがVK_RETURNである場合はEnterキーが押されたことを意味し、 今回はこの時にループを抜けるようにしています。 左右の方向キーが押された場合はVK_LEFTかVK_RIGHTが送られるため、 目的の方向にcoordCursor.Xを増加させ、 限界値を超えていない場合はSetConsoleCursorPositionでカーソルの新しい位置に設定します。 それ以外のキーが押された場合は、IsCharAlphaを呼び出しで文字がアルファベッドであるかを確認し、 アルファベッドである場合はWriteConsoleOutputCharacterで文字を書き込みます。 ここでは、カーソルの現在位置を考慮するWriteConsoleを呼び出すこともできますが、 この関数を呼び出すとカーソルが自動で移動することになります。

WriteConsoleOutputについて

今回は、WriteConsoleOutputCharacterで任意の位置に文字を書き込みましたが、 これはWriteConsoleOutputを呼び出すことでも可能です。 次に例を示します。

CHAR_INFO  charInfo[10];
COORD      coordSize, coordPos;
SMALL_RECT rcDest;

for (i = 0; i < 10; i++) {
	charInfo[i].Char.UnicodeChar = 'A' + i;
	charInfo[i].Attributes = FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED;
}

// コピー元のサイズ
coordSize.X = 10;
coordSize.Y = 1;

// コピー元の位置
coordPos.X = 0;
coordPos.Y = 0;

// コピー先の位置とサイズ
rcDest.Left   = 0;
rcDest.Top    = 0;
rcDest.Right  = 10;
rcDest.Bottom = 1;

WriteConsoleOutput(hStdOutput, charInfo, coordSize, coordPos, &rcDest);

WriteConsoleOutputの第2引数には、CHAR_INFO構造体の配列を指定します。 この構造体には、書き込みたい文字とその属性(前景色や背景色)を指定することができます。 今回は白色の前景色にするということで、青と緑と赤の前景色を示す定数を指定しています。 WriteConsoleOutputの第3引数には、コピー元のサイズを指定します。 charInfoの要素数が10であるため、XとYを掛けて10になれば値は問いません。 たとえば、Xを5にしてYを2にした場合は、2行5列で文字が表示されることになります。 WriteConsoleOutputの第4引数には、コピー元の位置を指定します。 たとえば、Xに1を指定すると、最初の文字はコピーされないことになります。 WriteConsoleOutputの第5引数には、コピー先の位置とサイズを指定します。 たとえば、Leftに1を指定すると、2列目の位置から文字が書き込まれることになります。



戻る