リモートデスクトップ接続でキーボードレイアウトを再初期化するプログラム

わたしは英語キーボードを使っているのですが,Linux からリモートデスクトップ接続したときに,入力ロケールが EN になってしまって困っていました。

-k en-us のように英語キーマップを指定するとうまくいく……のですが,ログイン時に Windows 側の「入力言語」が「英語」になってしまいます(ログインダイアログに「EN」と表示される)。入力言語は Alt + Shift キーを押すと切り替えることができます。

一応,コントロールパネルの「地域と言語のオプション」から「言語」タブの「テキストサービスと入力言語」で「詳細」ボタンを押し,「既定の言語」が「英語(米国) - US」になっているので「Microsoft Natual Input 2002」を選択すればいいはず……ですが,ログインするたびに再び EN になってしまうのが困り者です。

rdesktop の tips - daily dayflower

つまり,-k en-us のように英語キーマップを指定するとキーボードの表記と入力される文字が一致する一方,入力ロケールが英語になるので IME がそのままでは(Alt + Grave を押しても)使えない,と。Alt + Shift を押して入力ロケールを切り替えることができるけど,いちいちそれをするのがめんどい。

これわたしのとこだけの特殊な状況かなと思ってたら,フランス語と英語の両キーボードを使ってる人も困っていたのでほっと?しました。


んで,いちいちコンパネ立ち上げて直すのも大変なのでちょっとしたコードを書いてみました。

#include <windows.h>
#include <shlwapi.h>
#pragma comment(lib, "shlwapi.lib")

static void ShowErrorMessageBox(DWORD dwError = 0);

int WINAPI
WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
	LSTATUS	r;
	HKL		hkl;
	TCHAR	layoutId[32];
	DWORD	dwRegType, cbLayoutId;

	if (! GetSystemMetrics(SM_REMOTESESSION))
		return 0;

	cbLayoutId = sizeof(layoutId);

	r = SHRegGetValue(HKEY_CURRENT_USER, TEXT("Keyboard Layout\\Preload"), TEXT("1"),
	                  SRRF_RT_REG_SZ, &dwRegType, layoutId, &cbLayoutId);
	if (r != ERROR_SUCCESS) {
		ShowErrorMessageBox();
		return 1;
	}
#if 0
	MessageBox(NULL, layoutId, TEXT("Layout ID"), MB_OK | MB_ICONINFORMATION);
#endif

	hkl = LoadKeyboardLayout(layoutId, KLF_SUBSTITUTE_OK | KLF_ACTIVATE);
	if (! hkl) {
		ShowErrorMessageBox();
		return 1;
	}

	if (! SystemParametersInfo(SPI_SETDEFAULTINPUTLANG, 0, &hkl, SPIF_SENDCHANGE)) {
		ShowErrorMessageBox();
		return 1;
	}

	return 0;
}

static void
ShowErrorMessageBox(DWORD dwError /* = 0 */)
{
    /* ほげほげ……ほんとは FormatMessage() して MessageBox() を出してるけど略 */
}

ポイントは,

  • レジストリではローカルログオン時の入力ロケールの優先順位が HKCU\Keyboard Layout\Preload 以下にあるんだけど,リモートデスクトップ接続するとなぜかシステム的にオーバーライドされる。なので,レジストリから入力ロケールを読み取って SystemParametersInfo(SPI_SETDEFAULTINPUTLANG) で再設定してる。これでうまくいく。
  • GetSystemMetrics(SM_REMOTESESSION) で 0 以外(一応 1)が帰ると,それはリモートデスクトップ接続した環境。0 だとローカルログインした環境。これが一番ポータブルで安全で楽な確認手段。
  • HKL をそのまま SystemParametersInfo() に渡していてハマりました。実際は HKL のさらにポインタ。
  • SHRegGetValue() は deprecated で RegGetValue() 使いなよ,と最近の SDK に書いてあるけど,使えるのは XP 64bit 以降(Vista 含む)。なので,あえて使ってます。EXPAND_SZ とかにも対応できるのでこれ系の関数を使うと楽。

レイアウトコードの長さが(ゆとりはみてるものの)決め打ちなのがいまいちですが,めんどいのでこれで。


注意点としては,これを実行した後に起動するプロセスに対してのみ入力ロケールの変更が効くところです。つまり,スタートアップとかに仕掛けても,たとえばそれ以前に起動したプロセスには効いていないし,エクスプローラ自身にも効いていません。まぁ,エクスプローラ自身は Alt + Shift で対処してください。