EternalWindows
メッセージ管理 / 仮想入力キュー

仮想入力キューはVIQ(Virtual Input Queue)とも呼ばれ、 キーボードやマウス関連のメッセージが格納されます。 何故、これらのメッセージがポストメッセージキューに格納されないのか、 さらにポストメッセージキューが仮想入力キューより優先順位が高いのは何故かですが、 次のようなコードを見て考えると検討がつくと思われます。

case WM_KEYDOWN:
	if (LOWORD(wParam) == VK_SPACE)
		PostMessage(hwnd, WM_APP, 0, 0);
	return 0;

このコードは、スペースキーが押されたときにWM_APPをポストしているわけですが、 ユーザーがスペースキー押し続けた場合、WM_KEYDOWNも続けて仮想入力キューにポストされます。 このような場合、WM_KEYDOWNとWM_APPの2つのメッセージがキューに存在していますが、 この状態でWM_KEYDOWNの方を先に取得して処理するとなっては、 ポストされるWM_APPは、スペースキーを離さない限り処理されないことになってしまいます。

次に、仮想入力キューにメッセージがポストされるタイミングを説明します。 ユーザーがキーボードの何らかのキー押し続けた場合、 いくつものWM_KEYDOWNが発生するわけですが、 これらのメッセージが直ちに仮想入力キューにポストされるわけではありません。 最初のWM_KEYDOWNをウインドウプロシ−ジャで処理し終えてから、 次のWM_KEYDOWNが仮想入力キューにポストされるのです。 この仕様は、キーボードフォーカスを持つウインドウは変更される という事を端的に表しています。 たとえば、WM_KEYDOWNの処理の際中にユーザーがアクティブなウインドウを変更したり、 キーボードフォーカスを変更する関数を呼び出したりするとなると、 後続のWM_KEYDOWNはフォーカスを得た新しいウインドウに送られるべきです。 したがって、キーボードメッセージは押下された瞬間ではなく、 現時点でフォーカスを持つウインドウというのが重要といえます。

キーボードイベントが発生してもそれが直ちにポストされないということは、 システムにとっては、一連のキーボードメッセージを管理する必要があります。 システムはSHIQ(System Hardware Input Queue)と呼ばれるキューを持っており、 デバイスドライバはこのキューにメッセージをポストします。 これらポストされたメッセージはシステムのRIT(Raw Input Thread)と呼ばれるスレッドが 取得し、自身と接続しているスレッドの仮想入力キューにメッセージをポストします。 RITに接続しているスレッドとは、入力イベントを受け取ることのできるウインドウに関連する スレッドのことであると考えて構いません。

RITに接続していないスレッドは、自らキーボードフォーカスを取得したり、 ウインドウをアクティブにしたりすることはできません。 しかし、スレッド(システムスレッドは除く)のVIQは他のスレッドがアタッチできるようになっており、 アタッチ先スレッドのVIQに格納されたメッセージをアタッチ元スレッドも共有できる仕組みがあります。 つまり、RITに接続しているスレッドに対してアタッチすることで、 自スレッドもRITに接続しているように振る舞うことができます。 特定のスレッドにアタッチするには、AttachThreadInputを呼び出します。

BOOL WINAPI AttachThreadInput(
  DWORD idAttach,
  DWORD idAttachTo,
  BOOL fAttach
);

idAttachは、アタッチ元のスレッドのIDを指定します。 通常は、現在のスレッドのIDを指定します。 idAttachToは、アタッチ先のスレッドのIDを指定します。 通常は、RITに接続しているスレッドのIDを指定します。 fAttachは、スレッドにアタッチする場合はTRUEを指定し、 デタッチする場合はFALSEを指定します。

今回のプログラムは、ウインドウが表示されてから5秒後に自らを前面に表示します。 5秒経過する前に、予めこのウインドウを他のウインドウの後ろに隠しておき、 5秒後に本当に前面に表示されるかを確認してください。

#include <windows.h>

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:
		SetTimer(hwnd, 1, 5000, NULL);
		return 0;
	
	case WM_TIMER: {
		HWND  hwndTarget;
		DWORD dwThreadId;

		hwndTarget = GetForegroundWindow();
		dwThreadId = GetWindowThreadProcessId(hwndTarget, NULL);

		AttachThreadInput(GetCurrentThreadId(), dwThreadId, TRUE);
		FlashWindow(hwnd, TRUE);
		SetForegroundWindow(hwnd);
		AttachThreadInput(GetCurrentThreadId(), dwThreadId, FALSE);

		KillTimer(hwnd, 1);
		return 0;
	}

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

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

5秒後に送られるWM_TIMERでは、SetForegroundWindowを呼び出してウインドウを前面に表示します。 しかし、これだけではRITに接続されていない場合に意味を持たないため、 RITに接続されているスレッドにアタッチしておく必要があります。 このようなスレッドは現在、前面に表示されているウインドウに関連するスレッドになりますから、 まずGetForegroundWindowで前面のウインドウのハンドルを取得し、 次にGetWindowThreadProcessIdで関連するスレッドのIDを指定します。 そして、これをAttachThreadInputの第2引数に指定すれば、 第1引数のスレッド(現在のスレッド)は第2引数のスレッドにアタッチしたことになりますから、 SetForegroundWindowを呼び出すことができます。 FlashWindowの呼び出しは必須ではありませんが、 この関数を呼び出すとタスクバーのアイテムを点滅させることができます。


戻る