EternalWindows
バンドオブジェクト / キー入力の対応

バンドオブジェクトで文字の入力を受け取るコントロールを扱う場合は、 ある1つの難しい問題に対処することになります。 それは、ユーザーによる一部のキー入力が既定でコントロールに反映されないという関係上、 バンドオブジェクトが明示的にキー入力を検出しなければならないというものです。 たとえば、バンドオブジェクトのエディットコントロールが現在フォーカスを持っており、 ユーザーがCtrl + Aを押下した場合は全ての文字列が選択されることが望みます。 しかし、実際にはそのようにならず、IE上に表示されている別のコントロールに対してCtrl + Aの通知が送られるようになっています。 また、このようなアクセラレータキーの入力に限らず、 方向キーやBackキー、Delキーの押下もフォーカスを持っているコントロールに送られません。 何故IEがこのような動作になっているのかは分かりませんが、 このままでは文字を入力するうえで不都合が生じてしまうため、 明示的にキー入力を検出する処理が必要になります。

バンドオブジェクトがキー入力を検出するためには、 バンドオブジェクトのフォーカスが変化した際にIEへ通知するようにします。 これにより、IEはキー入力に関する通知をバンドオブジェクトへ送るようになります。 IEへの通知にはIInputObjectSiteを使用するため、 これをCToolBand::SetSiteで取得しておきます。

pUnkSite->QueryInterface(IID_PPV_ARGS(&m_pSite));

m_pSiteの型はIInputObjectSiteであるため、 QueryInterfaceにはIID_IInputObjectSiteが指定されることになります。 この結果、IInputObjectSiteを取得することができます。

バンドオブジェクトにフォーカスが割り当てられた際というのは、どのような状況のことを意味するのでしょうか。 今回の場合、バンドオブジェクト上にはエディットコントロールを作成しているため、 このエディットコントロールにフォーカスが割り当てられた際を検出すればよいでしょう。

case WM_COMMAND:
	if (LOWORD(wParam) == ID_EDIT) {
		if (HIWORD(wParam) == EN_SETFOCUS)
			OnFocus(TRUE);
		else if (HIWORD(wParam) == EN_KILLFOCUS)
			OnFocus(FALSE);
		else
			;
	}

wParamの下位ワードがID_EDITであるならば、エディットコントロールに関する通知であることが分かります。 そして、wParamの上位ワードがEN_SETFOCUSならばフォーカスが設定されたこと、 EN_KILLFOCUSならばフォーカスを失われたことを意味します。 OnFocusという自作メソッドは、フォーカスの有無を受け取って次の処理を行います。

void CToolBand::OnFocus(BOOL bFocus)
{
	m_bHasFocus = bFocus;
	
	if (m_pSite != NULL)
		m_pSite->OnFocusChangeIS(static_cast<IInputObject *>(this), m_bHasFocus);
}

現在のフォーカスの状態は後でIEに返すことになるため、メンバ変数に保存しておくようにします。 IInputObjectSite::OnFocusChangeISを呼び出せば、フォーカスの変更をIEへ通知すると同時に、 キー入力を受け取るオブジェクトをIEに渡すことができます。 このオブジェクトはIInputObjectを実装していなければならず、上記ではthisポインタを指定しています。 よって、CToolBandはIInputObjectを実装し、QueryInterfaceでこれを返すようになっていなければなりません。

IEは、IInputObjectを通じて入力に関する通知をオブジェクトに送ります。 IInputObjectには3つのメソッドが含まれており、 このうちUIActivateIOとHasFocusIOの処理は次のようになります。

STDMETHODIMP CToolBand::UIActivateIO(BOOL fActivate, MSG *pMsg)
{
	if (fActivate)
		SetFocus(m_hwnd);

	return S_OK;
}

STDMETHODIMP CToolBand::HasFocusIO(VOID)
{
	return m_bHasFocus ? S_OK : S_FALSE;
}

UIActivateIOはオブジェクトのアクティブ状態が変化した際に呼ばれるとされていますが、 それがどのようなタイミングで発生するのかがよく分かりません。 調べたところ、オブジェクトをメニューから表示した場合やタブキーでフォーカスが割り当てた場合には呼ばれませんでした。 HasFocusIOは何かキーが入力された際に呼ばれます。 ここでは現在のフォーカスの状態を返すことになるため、OnFocusで保存したメンバ変数の値を参照します。

HasFocusIOでS_OKを返すと、TranslateAcceleratorIOが呼ばれるようになります。 このメソッドでS_OKを返すと、IEは入力されたキーをフォーカスがあるコントロールに送信しませんが、 S_FALSEを返すと入力されたキーをコントロールに送信します。

STDMETHODIMP CToolBand::TranslateAcceleratorIO(LPMSG lpMsg)
{
	const int nEntries = 5;
	int       i;
	ACCEL     accel[nEntries];
	WORD      wCmd;
	WORD      wCmdArray[] = {0x41, 0x5a, 0x58, 0x43, 0x56};

	if (m_haccel == NULL) {
		for (i = 0; i < nEntries; i++) {
			accel[i].key   = wCmdArray[i]; 
			accel[i].cmd   = wCmdArray[i];
			accel[i].fVirt = FCONTROL | FVIRTKEY;
		}

		m_haccel = CreateAcceleratorTable(accel, nEntries);
	}
	
	if (lpMsg->message == WM_KEYDOWN) {
		if (lpMsg->wParam == VK_DELETE || lpMsg->wParam == VK_LEFT || lpMsg->wParam == VK_UP || lpMsg->wParam == VK_RIGHT || lpMsg->wParam == VK_DOWN) {
			SendMessage(lpMsg->hwnd, lpMsg->message, lpMsg->wParam, lpMsg->lParam);
			return S_OK;
		}
		else if (lpMsg->wParam == VK_BACK) {
			SendMessage(lpMsg->hwnd, WM_CHAR, lpMsg->wParam, lpMsg->lParam);
			return S_OK;
		}
		else {
			if (IsAccelerator(m_haccel, nEntries, lpMsg, &wCmd)) {
				if (wCmd == 0x41)
					SendMessage(lpMsg->hwnd, EM_SETSEL, 0, -1);
				else if (wCmd == 0x5a)
					SendMessage(lpMsg->hwnd, WM_UNDO, 0, 0);
				else if (wCmd == 0x58)
					SendMessage(lpMsg->hwnd, WM_CUT, 0, 0);
				else if (wCmd == 0x43)
					SendMessage(lpMsg->hwnd, WM_COPY, 0, 0);
				else if (wCmd == 0x56)
					SendMessage(lpMsg->hwnd, WM_PASTE, 0, 0);
				else
					;
				return S_OK;
			}
		}
	}

	return S_FALSE;
}

望ましい動作というのは、入力された全てのキーがフォーカスを持つコントロールに送信されることですが、 単純にS_FALSEを返すだけではそのような動作は実現できません。 既に述べたように、方向キーの入力やBackキーの入力、及びアクセラレータの入力は既定でコントロールに送信されませんから、 これらのキーが入力された場合は明示的にコントロールへそれを送信することになります。 また、このように独自にキーを処理した場合はS_OKを返すようにします。 それでは各種処理を順に見ていきます。

if (m_haccel == NULL) {
	for (i = 0; i < nEntries; i++) {
		accel[i].key   = wCmdArray[i]; 
		accel[i].cmd   = wCmdArray[i];
		accel[i].fVirt = FCONTROL | FVIRTKEY;
	}

	m_haccel = CreateAcceleratorTable(accel, nEntries);
}

アクセラレータの入力を検出するために、CreateAcceleratorTableでアクセラレータテーブルを作成します。 検出したい組み合わせは、全て選択を意味するCtrl+A(0x4a)、元に戻すことを意味するCtrl+Z(0x5a)、 切り取りを意味するCtrl+X(0x58)、コピーを意味するCtrl+C(0x43)、貼り付けを意味するCtrl+V(0x56)の合計5個です。 ACCEL構造に仮想キーコードを指定しているため、fVirtにはFVIRTKEYを指定し、 Ctrlキーの組み合わせであるためFCONTROLも指定します。

if (lpMsg->message == WM_KEYDOWN) {
	if (lpMsg->wParam == VK_DELETE || lpMsg->wParam == VK_LEFT || lpMsg->wParam == VK_UP || lpMsg->wParam == VK_RIGHT || lpMsg->wParam == VK_DOWN) {
		SendMessage(lpMsg->hwnd, lpMsg->message, lpMsg->wParam, lpMsg->lParam);
		return S_OK;
	}
	else if (lpMsg->wParam == VK_BACK) {
		SendMessage(lpMsg->hwnd, WM_CHAR, lpMsg->wParam, lpMsg->lParam);
		return S_OK;
	}
	else {
		if (IsAccelerator(m_haccel, nEntries, lpMsg, &wCmd)) {
			if (wCmd == 0x41)
				SendMessage(lpMsg->hwnd, EM_SETSEL, 0, -1);
			else if (wCmd == 0x5a)
				SendMessage(lpMsg->hwnd, WM_UNDO, 0, 0);
			else if (wCmd == 0x58)
				SendMessage(lpMsg->hwnd, WM_CUT, 0, 0);
			else if (wCmd == 0x43)
				SendMessage(lpMsg->hwnd, WM_COPY, 0, 0);
			else if (wCmd == 0x56)
				SendMessage(lpMsg->hwnd, WM_PASTE, 0, 0);
			else
				;
			return S_OK;
		}
	}
}

入力されたキーがWM_KEYDOWNである場合は、押下されたキーをwParamから確認することになります。 Delキー及び方向キーに関しては、lpMsg->hwndにWM_KEYDOWNを送るだけで構いません。 一方、Backキーに関してはWM_CHARを送るようにします。 いずれにも属さなかった場合は、IsAcceleratorでアクセラレータの入力であるかを調べ、 そうである場合は入力されたキーに応じた処理を実行します。 0x41の場合は全てを選択しなければならないため、 EM_SETSELのWPARAMに0を指定してLPARAMに-1を指定します。 残りのキーについては、その動作の通りのメッセージを送信します。 なお、今回の場合lpMsg->hwndはエディットコントロールのウインドウハンドルになります。


戻る