情報科学屋さんを目指す人のメモ

方法・手順・解説を書き残すブログ。私と同じことを繰り返さずに済むように。

AutoHotkey:キー押しっぱなし病・ホットキーすり抜け病対策の研究

AutoHotkey (28) Windows (498) Windows 7 (62)

ずっと苦しんでいた「AutoHotkey」を利用したことによる副作用「押しっぱなし病」「ホットキーすり抜け病」について、がっつり取り組んでみました。その結果、だいぶ有力な対策を見つけるができたので、そこに至るまでのトライ&エラーの記録をシェアしたいと思います。

目次

押しっぱなし病・すり抜け病

「押しっぱなし病」とは、ホットキーを使っているうちに、CtrlやAlt、無変換など、修飾キーとして使っているキーが押したままになる現象のことです。例えばCtrlが押しっぱなしになると、Nキー単独の入力が、「Ctrl+N」になってしまいます。一度Ctrlキーを押せば状態は回復しますが、そもそもCtrlキーが押しっぱなしであることに気が付くまでに、いろいろと事故が起こります。

「すり抜け病」とは、ホットキーを使っているはずなのに、押しているキーがそのまま入力されてしまう現象のことです。例えば「Ctrl+L」を「→」にしているとき、「Ctrl+L」を押しっぱなしにしてずっと右まで移動しようとすると、いつの間にやら「l」が入力されてしまっています。気が付いて消すのも面倒ですし、うっかり気が付かないと、そのまま謎の文字が入力されたままになってしまいます。

前提:AutoHotkeyがホットキーを実行するまでのキーDown/Upの仕組み

「Ctrl+M」を「Enter」に割り当てた場合の挙動を紹介します。

.ahkに「^m::send, {Enter}」と書くと、「Ctrl+M」が「Enter」になります。それをどう実現しているか、実際に「Ctrl+M」を押したときの挙動を例に紹介します。

まずは、現在物理的に押されているCtrlキーを、一時的にKeyUp(u)して、Ctrl+Enterではなく、Enterのみを送信しようとするのがポイントで、Enterを論理的にDown/Upして、それが終わったらすぐにCtrlをKeyDown(d)して、状態を元に戻します。AutoHotkeyのkey history機能の表記に従うと、次のようになります。

d LControl
d M
u LControl
d Enter
u Enter
d LControl
u M
u LControl

ちなみに、mキーは修飾キーではないため、Ctrlと違って論理的に一時的にupしておく必要はありません。

典型的な押しっぱなし病発動例

典型的な押しっぱなし病発動例はこちらです。

A2 01D d 0.65 LControl
4D 032 h d 0.08 M
A2 01D i u 0.00 LControl
0D 01C i d 0.00 Enter
0D 01C i u 0.00 Enter
A2 01D i d 0.00 LControl
4D 032 # u 0.08 M
4D 032 h d 0.11 M
A2 01D i u 0.00 LControl
0D 01C i d 0.00 Enter
0D 01C i u 0.00 Enter
A2 01D i d 0.00 LControl
4F 018 d 0.08 O
4D 032 # u 0.05 M
4F 018 u 0.03 O
BC 033 d 0.12 ,
BC 033 u 0.11 ,

物理的にCtrlを上げたのに「u LControl」が適用されておらず、AutoHotkeyが「d LControl」したままになってしまっています。

こういう単純な発生例はいいのですが、key historyを見てもよくわからないきっかけで押しっぱなしになっている場合もあるので、厄介です。

4B  025	 	d	0.06	K              	
55  016	 	d	0.08	U              	
4B  025	 	u	0.03	K              	
55  016	 	u	0.08	U              	
57  011	 	d	0.00	W              	
57  011	 	u	0.06	W              	
4F  018	 	d	0.02	O              	
20  039	 	d	0.06	Space          	
4F  018	 	u	0.06	O              	
20  039	 	u	0.03	Space          	
A2  01D	 	d	0.03	LControl       	
4D  032	h	d	0.13	M              	
A2  01D	i	u	0.00	LControl       	
A2  01D	 	u	0.00	LControl       	
BD  00C	 	d	0.14	-              	
0D  01C	i	u	0.00	Enter          	
A2  01D	i	d	0.00	LControl       	
4D  032	s	u	0.08	M              	
4B  025	 	d	0.13	K              	
4B  025	 	u	0.08	K              	
49  017	 	d	0.09	I              	
49  017	 	u	0.06	I              	
BD  00C	 	d	0.17	-              	
BD  00C	 	u	0.09	-              	

??

推測される原因(単純には)

AutoHotkey がキーボード入力を操作している間に(例では「i u LControl」「i d Enter」「i u Enter」「i d LControl」)物理キーがUPされてしまうと、AutoHotkeyの操作が終わった後に物理キーのUPが反映されずにそのままになってしまうのが原因だと予想されます。つまり、物理キーがUPされるまでに、AutoHotkeyの処理が完了する必要があるのです。つまり、すぐCtrlキーを離してしまう場合、AutoHotkeyの処理はかなり高速に終わる必要があります。

なので、例えば処理が重い「^!sc028::Send +{End}{BS}」からのAltキー押しっぱなし病なんかが典型です。また、「^M::Enter」も、文字変換の決定時に利用すると、少々重いので押しっぱなし病の原因になりやすいといえます。

ただし、こんなことを考えただけでは、さっぱり対策は分かりません。

基本的な対策

基本的な対策として、キーボードフックを使う(#InstallKeybdHook)など、#HotkeyModifierTimeoutに書かれている対策はチェック済みです。

#HotkeyModifierTimeout

いろいろな対策

ここから、いろいろな対策(ダメだったのも含めて)を紹介します。

強制UP

強制的にわざわざ手動で最後に「Send, {Control UP}」を差し込んでみます。

ちゃんとAutoHotkeyの処理が終わった後も物理的にCtrlキーが押されていれば、{Control UP}後に、物理キーの状態(down)と論理的な状態(up)との間に不整合が起こるので、勝手に「d LControl」になり、そのまま続きます。また、AutoHotkeyの処理が終わった時点で既に物理キーがUP済みであったとしても、あらかじめUPしてあることによる不都合はなさそうです。しかし、こんなやり方だと、Ctrl+Mを押したままにしても、Ctrlが効かないでそのままmが入力されてしまったり、むしろCtrl押しっぱなし病が頻発したりと、さんざんなことになりました。

suspend on/off

Ctrl downなどの後に「suspend on」「suspend off」を挿入する方法。副作用がヤバそうで試さず。

参考:stuck ctrl key? - Page 2 - Issues - AutoHotkey Community

SendMode InputThenPlay

AutoHotkeyでは「SendMode Input」が推奨されているので、普段はそれを使っていました(SendPlayでしか動かない部分は個別対応)。

これ、効果無し。

参考:stuck ctrl key? - Page 2 - Issues - AutoHotkey Community

コンパイルしてみる

動作が遅いのが原因なら、スクリプトをコンパイルすれば少し改善されるんじゃないかという発想で、.ahkを右クリックしてcompileしてみることに。

コンパイルすると、心なしか安定した(再発しにくく)なったものの、すり抜けは発生。他の方法に比べると、だいぶまともでした。

KeyWait

「KeyWait Alt」などで、物理KeyUpまで待機する方法で解決したと紹介されている場合がありますが、単発ならまだしも、押しっぱなしでリピートしたり、高速でタイプするのにはさっぱり向かないのでダメです。論外。

参考:「The Wrong Side of 50: How to Stop AutoHotkey from sticking the Shift Key

SetPointを終了させる

SetPointと競合する話がありましたが、これまたちょっと違うので効果無しです。

Criticalを入れる

いろんなホットキーが入り乱れるのが押しっぱなしの原因である面も大きいだろうということで、最初のコマンドに「Critical」を挿入して、別スレッドに割り込まれないようにしてみました。結果、普通にすり抜けました。しかし、押しっぱなし病は起こりにくくなったかも。正直いまいち。

SetBatchLines, -1

AutoHotkeyは、一定行(10ms)実行する毎に、一定時間(10ms)Sleepを行うようになっています。その自動挿入されるSleepを「SetBatchLines, -1」で無効化します。

これまた効果があるのかどうかわかりにくいのですが、負荷を減らして回避する・遭遇頻度を下げるという意味ではなかなか良い感じのように感じました。でも結局いまひとつ。

Windows VirtualPC を無効化する

「Windows の機能の有効化または無効化」から「Windows Virtual PC」のチェックを外して無効化しました。

特に効果なし。

カスペルスキーのキーロガー保護を無効化する

カスペルスキーの画面を開いて、「設定>データ入力の保護」から「入力情報の漏洩防止>物理キーボードの入力をキーロガーから保護する」をOFFに。

プロセスの優先度を上げる

Processコマンドを使って「Process, Priority, , High」のようにプロセスの優先度を上げてみました。

目に見えて分かる効果はありませんでした。

大本命:SendPlayを利用する

「SendMode Play」を記述し、デフォルトでSendPlayを利用することにしました。これにはかなり副作用があるものの、かなり押しっぱなし状態が発動しなくなった気がします。

というわけで、今回の研究の結果の大本命はこの、「SendPlayを利用する」というものです。高負荷で文字入力をしていますが、安定して入力を継続できています。

ただし、今まで「SendMode Input」で記述していた場合、突然「SendMode Play」にしてしまうと、問題が複数発生します。というわけで、問題が発生した部分に関しては、個別に対処することが求められます。(今のところ、)そこまで大したことではないので、以下の副作用に関するヒントを参考にしてみてください。

副作用1:Shiftなどの修飾キーの状態が反映されない

Send {Shift Down}
Send {Home Down}
Send {BS Down}
Sleep 30ms
Send {BS Up}
Send {Home Up}
Send {Shift Up}

のように書いても「Send +{Home}{BS}」相当にはならず、「Send {Home}{BS}」相当になってしまいます。この場合なら、普通に「Send +{Home}{BS}」を書いていれば大丈夫です。

副作用2:引っかかることがある

カーソルの移動をホットキーで実現していると、特に「Send, {Up}」の発動が引っかかることがあります。

副作用3:Winキーが発行されない

「Send, #{Left}」のように、Winキーを含むキーをSend(Play)する場合、Winキーがアプリケーションそのものに送られてしまい、OSが受け取ってくれず、WindowsによるWinキーの機能が発動しません。

これは、SendPlayを使う以上仕方のないことなので、「SendInput」や「SendEvent」と明示して回避します。

副作用4:その他SendInputに変更したもの

「Send {Esc}」「Send {WheelDown}」「Send {WheelUp}」「Send !{F15}」

「Send {Esc}」が使えないのはちょっとわからないのですが、「Send !{F15}」が使えなかったのは、Alt+F15がグローバルフックを使ったホットキーだったことによると思われます。SendPlayでは、選択中(フォーカスのある)のアプリケーションに直接メッセージが送られるので、グローバルな(アプリに依らない)ホットキーを呼び出すことができないようです。

副作用5:Criticalが挙動をおかしくする

次のコードで「Ctrl+Alt+K」を実行後に、単独でAltが入力されたような挙動になり、その後の別のホットキーの動きがおかしくなってしまいます。これは、その余計な「Critical」を削除することで解決しました。

^!k::
	Critical
	Send !{Left}
	Return

「Ctrl+Alt+H」→「無変換+h」のとき、「無変換+h」の無変換が効かない。

副作用6:なぜかATOKの確定アンドゥが発動する({Blind}の挙動が怪しい)

「sc07B & h::Send, {Blind}{BS}」(※)を設定した状態で、ATOKを使って文字入力→確定(Ctrl+M/無変換+M)した後に「無変換(sc07B)+h」を入力すると、なぜか確定アンドゥ(Ctrl+BackSpaceのみ、無変換はなし)が発動してしまいます。Ctrlを押したわけでもないのに、「Blind」の効果で「Ctrl」が送られてしまっている(?)ようです。「SendMode Input」環境下では発動しません。かといって、「SendInput, {Blind}{BS}」にしてしまうと、(sc07B & hはよく使うので、)せっかくのSendPlayによる対策に抜け道を用意してしまう感じです。よくよく考えてみると、「{Blind}」を使っている他のホットキーも怪しいので、「{Blind}」を回避するように、修飾キーをGetKeyStateで把握して、それぞれSendの内容を明示することで対策しました。

(※)これでは使いにくくて、正確には、

sc07B & h::
	If GetPrefixKeyState("^")
		Send, ^+{Left}{BS}
	Else
		Send, {Blind}{BS}
	Return

GetPrefixKeyStateはこちら:

; prefix_keysで指定された修飾キーが全て押されているならTrue
; GetKeyState関数の拡張
GetPrefixKeyState(prefix_keys) {
	state := 1
	Loop % StrLen(prefix_keys) {
		key := SubStr(prefix_keys, A_Index, 1)

		If (key = "^")
			key_name := "Ctrl"
		Else If (key = "+")
			key_name := "Shift"
		Else If (key = "!")
			key_name := "Alt"
		Else
			key_name := ""

		state := state && GetKeyState(key_name, "P")
	}
	Return state
}

ちなみに、ATOKの確定undoは、ATOKがアプリケーションに対して、変換後の文字数だけBackSpaceキーのDown/Upを送信(a=Artificial Key)してから、変換前の文字を流し込んでいるようです(こちらはAutoHotkeyのkey historyに残ってないけど)。

副作用7:「{Blind}」の挙動がやっぱりおかしい

次のように記述して「無変換(sc07B)+その他」の後、そのまま無変換キーを押したまま「無変換(sc07B)+Ctrl+n」としたとき、アプリケーション側に「Ctrl+Down」ではなく「Down」のみを送ってしまいます。1回目は「Down」のみ、2回目以降「Ctrl+Down」になってしまっているようです。

sc07B & n::
	Send, {Blind}{Down}
	Return

単純に、無変換が送られてしまっている(無変換の論理的UPが行われない)ので、「sc07B & ...::」に「{Blind}」を書かない方が良いってだけな気がしてきました。

副作用8:無変換+Shiftで「Shift」が残る

「無変換+Shift+k::Left」から、Shiftを離して「無変換+p::Up」をやると、なぜか「無変換+p::Up」のときにもShiftが発行してしまいます。

Blind対策(SendBlind)

とにかくBlindがなんでもかんでも素通ししすぎなので、Ctrl・Shift・Alt・Win限定のBlindを行いたいのですが、修飾キーの組合せを尽くし始めると分岐しすぎなので、次のようなSendBlind関数を作り、「sc07B & p::SendBlind("Up")」のように書くことにしました。

SendBlind(keys) {
	modifiers := GetModifiers()
	Send, %modifiers%%keys%
}

GetModifiers() {
	modifiers := ""
	If GetKeyState("Ctrl", "P")
		modifiers = %modifiers%^
	If GetKeyState("Shift", "P")
		modifiers = %modifiers%+
	If GetKeyState("Alt", "P")
		modifiers = %modifiers%!
	If GetKeyState("Win", "P")
		modifiers = %modifiers%`#
	Return %modifiers%
}

これで、微妙におかしい挙動から解放されました。

おまけ:押しっぱなし病・すり抜け病研究にあたってのヒント

ぐぐり方

押しっぱなし状態は、「keep pressing」とかではなく、「stick」や「stuck」と言われているので、こちらで検索するのがおすすめです。

再現の仕方

高速でCtrlや無変換を修飾キーとするホットキーを使いまくると発動しやすく、特に、CPU負荷がある程度あると(PCが重い状態)発動しやすいので、Webブラウザで動画を流しながら文章を書くとちょうど良い再発具合になります。

押しっぱなし状態の表示方法1:スクリーンキーボード

スクリーンキーボード(コントロール パネル\コンピューターの簡単操作\コンピューターの簡単操作センター>スクリーンキーボードを開始します)を表示しておくと、押しっぱなしになっているかどうかが分かりやすいです。しかし、AltにしろCtrlにしろ無変換にしろ、実際に押しっぱなし状態になってから、一度何らかのキーを押さないとスクリーンキーボード上に表示されなかったりするのでいまいちです。。

押しっぱなし状態の表示方法2:AutoHotkeyで表示

stuck ctrl key? - Page 2 - Issues - AutoHotkey Community(点滅しちゃう)」を参考に、Ctrlキーを監視して、押しっぱなしの状態をSplashImageを使って画面左下に表示するようにしました。

Settimer, CtrlEye, 300
Return

CtrlEye:
GetKeyState, state, Control
if state = D
{
	SetTimer, CtrlEye, Off
	SplashImage,,b x0 y715 H20 W80 ZY0 ZX0 fs9 ct1C3DB8 cw82BD3A, CONTROL, , Ctrl,Arial
	SetTimer, CheckCtrlUp, 200
	Return
	
	CheckCtrlUp:
	GetKeyState, state, Control
	if state = U
	{
		SplashImage, Off
		SetTimer, CheckCtrlUp, Off
		SetTimer, CtrlEye, 300
	}
	Return
}
Return

しかしこれ、少なくともAltの押しっぱなし状態をGetKeyStateで取得できないことが判明して無駄になりました。

AutoHotkey の画面を表示しておく

AutoHotkey のGUIを表示しておいて、再現できた時のキーのログ(key history and script info)と、スクリプトの実行ログ(Lines most recently executed)を確認できるようにしておくのがとてもよいです。

#KeyHistory 500 を書く

「#KeyHistory 500」を書いておくと、key historyの最大数が500まで大きくなり、検証しやすくなります(500が上限なので500)。

参考になりそう「だった」ページ

追記:続編(2024年6月12日追記)

コメント(1)

  1. sleepを入れたら改善
    2016年8月10日(水) 15:46

    いろいろと調べていたらたどり着きました。
    今のところの状況ですが、

    ^l::Send,{Right}
    を下記のようにしたら改善しましたのでご参考まで。
    ^l::
    Send,{Right}
    Sleep, 2
    return

新しいコメントを投稿