EternalWindows
コンソール / ハンドラ関数

ウインドウを表示する一般的なWindowsアプリケーションは、 システムからの通知をウインドウプロシージャという関数で受信するという特徴があります。 たとえば、システムがシャットダウンされようとしているときはWM_QUERYENDSESSIONが送られ、 アプリケーションはこのタイミングでクリーンアップ処理を行うことができます。 こうした通知をコンソールを表示するアプリケーションからでも検出したい場合は、 SetConsoleCtrlHandlerを呼び出してハンドラ関数を追加することになります。

BOOL WINAPI SetConsoleCtrlHandler(
  PHANDLER_ROUTINE HandlerRoutine,
  BOOL Add
);

HandlerRoutineは、ハンドラ関数のアドレスを指定します。 Addは、ハンドラ関数を追加する場合にTRUEを指定し、削除する場合にFALSEを指定します。

SetConsoleCtrlHandlerで追加したハンドラ関数には、 コンソールに対して発生したイベントを表す定数が送られます。 ハンドラ関数のプロトタイプは次のようになります。

BOOL WINAPI HandlerRoutine(
  DWORD dwCtrlType
);

dwCtrlTypeは、発生したイベントを表す定数が格納されます。 次に、定義されている定数を示します。

定数 意味
CTRL_C_EVENT CTRL+Cの組み合わせが押されたことを意味する。 FALSEを返すと既定のハンドラによってExitProcessが呼ばれる。 プロセスを終了させたくない場合はTRUEを返す。
CTRL_BREAK_EVENT CTRL+BREAKの組み合わせが押されたことを意味する。 FALSEを返すと既定のハンドラによってExitProcessが呼ばれる。 プロセスを終了させたくない場合はTRUEを返す。
CTRL_CLOSE_EVENT コンソールが閉じられたことを意味する。 FALSEを返すと既定のハンドラによってExitProcessが呼ばれる。 TRUEを返しても終了してしまうようである。
CTRL_LOGOFF_EVENT ユーザーがログオフしようとしていることを意味する。 FALSEを返すと既定のハンドラによってExitProcessが呼ばれる。 TRUEを返すと終了確認のダイアログが表示される。
CTRL_SHUTDOWN_EVENT システムがシャットダウンしようとしていることを意味する。 FALSEを返すと既定のハンドラによってExitProcessが呼ばれる。 TRUEを返すと終了確認のダイアログが表示される。

コンソールプロセスはハンドラ関数のリストを持っており、 既定のハンドラ関数が予め追加されています。 この既定のハンドラ関数はExitProcessを呼び出すだけですから、 アプリケーションがハンドラ関数を追加しないで上記のイベントが発生した場合は、 必ずプロセスが終了することになります。 アプリケーションがハンドラ関数でFALSEを返すと登録している次のハンドラ関数が呼ばれますが、 通常これは既定のハンドラ関数です。

アプリケーションが明示的にイベントを発生させたい場合は、GenerateConsoleCtrlEventを呼び出します。

BOOL WINAPI GenerateConsoleCtrlEvent(
  DWORD dwCtrlEvent,
  DWORD dwProcessGroupId
);

dwCtrlEventは、生成するイベントの種類を表す定数を指定します。 CTRL_C_EVENTとCTRL_BREAK_EVENTだけが指定可能です。 dwProcessGroupIdは、プロセスグループのIDを指定します。 0を指定すると、呼び出し側プロセスのコンソールを共有する全てのプロセスにイベントが発生します。 次に関数の呼び出し例を示します。

GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, 0);

このようにすると、CTRL_BREAK_EVENTが引数としてハンドラ関数が呼ばれることになります。 Breakキーがキーボードに存在しない場合は、このコードを実行することで効果を確認できます。

今回のプログラムは、SetConsoleCtrlHandlerを呼び出す例を示しています。

#include <windows.h>

BOOL WINAPI HandlerRoutine(DWORD dwCtrlType);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	AllocConsole();

	SetConsoleCtrlHandler(HandlerRoutine, TRUE);

	Sleep(INFINITE);

	return 0;
}

BOOL WINAPI HandlerRoutine(DWORD dwCtrlType)
{
	if (dwCtrlType == CTRL_C_EVENT) {
		MessageBox(NULL, TEXT("CTRL_C_EVENT"), TEXT("OK"), MB_OK);
		return TRUE;
	}

	if (dwCtrlType == CTRL_CLOSE_EVENT) {
		MessageBox(NULL, TEXT("CTRL_CLOSE_EVENT"), TEXT("OK"), MB_OK);
		return TRUE;
	}

	return FALSE;
}

SetConsoleCtrlHandlerの第2引数にTRUEを指定していることから、 第1引数に指定した関数がハンドラ関数として追加されます。 CTRL+Cを押した場合はハンドラ関数にCTRL_C_EVENTが送られ、 上記の例ではTRUEを返しています。 よって、CTRL+Cの押下によってプロセスが終了することはありません。 ウインドウを閉じたときにはCTRL_CLOSE_EVENTが送られますが、 このイベントの場合はどんな値を返してもプロセスが終了することになるようです。 処理しないイベントについては、FALSEを返すようにします。

コンソールを単一で閉じる

コンソールを閉じるとプロセスが終了するという仕様は、場合によっては不都合になることがあります。 たとえば、ウインドウを表示するアプリケーションが、デバッグ目的でコンソールに逐次データを書き込むものとしましょう。 この場合、コンソールを閉じることはデータの確認が不要になるのを意味するだけですから、 単にコンソールのウインドウが閉じられればよいだけであり、 プロセスが終了してしまう必要はありません。 この問題を解決するには、コンソールが閉じられるタイミングを検出して終了を拒否しなければなりませんが、 これが非常に難しいのです。 たとえば、SetConsoleCtrlHandlerでCTRL_CLOSE_EVENTを検出しても、 戻り値を通じて終了を拒否することはできません。

コンソールといえどそれはウインドウですから、ウインドウをサブクラス化すればWM_CLOSEを検出できるように思えます。 しかし、コンソールに対するサブクラス化は必ずERROR_ACCESS_DENIEDが返るようであり、 サブクラス化が成功することはありません。 サブクラス化のようにメッセージを検出する仕組みとしてメッセージフックがありますが、 何故かコンソールへのメッセージはフックプロシージャに送られないため、 この方法も利用することができません。

妥協案として考えたのは、コンソールの「閉じる」ボタンを無効にする方法です。 そして、ユーザーには「閉じる」ボタンを押す代わりに、CTRL+Cでコンソールを閉じてもらいます。 CTRL+Cが押された場合はハンドラ関数にCTRL_C_EVENTが送られますが、 ここでTRUEを返せばプロセスが終了することはありません。

#include <windows.h>

BOOL WINAPI HandlerRoutine(DWORD dwCtrlType);
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)
{
	switch (uMsg) {

	case WM_CREATE: {
		HMENU hmenu;

		AllocConsole();
		
		SetConsoleCtrlHandler(HandlerRoutine, TRUE);

		hmenu = GetSystemMenu(GetConsoleWindow(), FALSE);
		RemoveMenu(hmenu, SC_CLOSE, MF_BYCOMMAND);
		
		return 0;
	}

	case WM_LBUTTONDOWN: {
		int   x, y;
		TCHAR szBuf[256];
		DWORD dwWriteByte;
		
		x = LOWORD(lParam);
		y = HIWORD(lParam);

		wsprintf(szBuf, TEXT("X %d : Y %d\n"), x, y);
		WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), szBuf, lstrlen(szBuf), &dwWriteByte, NULL);

		return 0;
	}

	case WM_DESTROY:
		FreeConsole();
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

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

BOOL WINAPI HandlerRoutine(DWORD dwCtrlType)
{
	if (dwCtrlType == CTRL_C_EVENT) {
		FreeConsole();
		return TRUE;
	}

	return FALSE;
}

「閉じる」ボタンを無効にするには、システムメニューの項目を削除するだけです。 システムメニューにはSC_CLOSEというIDを持った項目がありますから、これをRemoveMenuで削除します。 すると、「閉じる」ボタンが無効になることに加えて、システムメニューから「閉じる」を選択することができなくなります。 コンソールのシステムメニューを取得するには、GetConsoleWindowでコンソールのウインドウハンドルを取得し、 これをGetSystemMenuの第1引数に指定します。 ハンドラ関数では、CTRL_C_EVENTの際にTRUEを返すだけでなくFreeConsoleを呼び出していますが、 これは非常に重要です。 単にTRUEを返すだけでは、プロセスが終了せずコンソールも閉じられませんから、 コンソールだけが閉じられたように見せるためにFreeConsoleを呼び出す必要があります。



戻る