0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AHK v1でウィンドウスイッチャを作ってみた。

0
Last updated at Posted at 2026-01-05

概要

Windows11になり、使用していたフリーソフトが色々と使えなくなったりしました。
利用申請を出すのもめんどくさいので、なんやかんや手造りをしてみようと思います。・・・の第1弾。(シリーズ化未定)

AHK(いまだにv1)が試験の自動化用として入っていて、win11でもちゃんと使えたので、まずは手始めにタスクスイッチャを作ってみました。簡単なGUIも作れるんですね。今更ですがAHK凄いですね。

ひと先ずは簡単に実装できそうだったタスクスイッチャを正月休みにパパっと作成してみました。

環境

Windows11 (10.0.26100.7171、10.0.26200.7462、など)
AutoHotkey_1.1.37.01

AHKのv2系で作ったほうがいいのかもしれませんが、使ったことないし、いまだにv1で不満もないのでこのままいかせてもらいます。

経緯

今までは Tascher とか使ってたんですけどもね、64bit版は起動しなくなって、32bit版は動くんだけどとてもモッサリとした動きになってしまったうえに、なんか Win11 化の時に新しく入れることになった監視ツールで引っかかったんだか引っかからなかったんだか忘れましたが、色々あって使うのをやめました。

まじめな従業員なので、お上のお達しには速やかに無条件かつ無批判かつサイレントに従います。そのため昨年末から、まるで大量絶滅期の様相で色々なツールが使えなくなっています。
ランチャ、ファイラ、スイッチャ、・・・みんな、みぃんな、おらんくなってしもうた。。

トラックボールユーザーだ、トラックポイントユーザだと息巻いていましたが、しょせん大して使っていなかったということを身にしみてわからされました。非ポインティングなデバイスへの過度の依存により、ポインティングデバイスを軽視してしまっていた代償を支払わされている気分です。
 
どれくらいかっていうと、もうタッチパネルがありがたく感じるくらいマウスカーソル操作が苦痛。舌端現象のようなもどかしさ。手足をもがれたよう。何かしようとした瞬間に小脳レベルで指が動こうとするんだけど、もうそれはできないんだという、幻肢痛のような感覚に震えて過ごす毎日でした。(言い過ぎ)

ちなみに、エディタはvimが居てくれています。一服の精神安定剤と言っていいでしょう。いまはNetrwで糊口を凌いでいる状態です。もうキーボードファイラも自作するしか・・・。

 
スタートメニューや、エクスプローラーや、タスクバーなどを相手に、マウス片手にスッ、スッ、カチッ、・・・スッ、カチカチッ、スッ、ホイールクリクリ・・(×8万4千回over)とやっている人たちを見ると、畏敬の念を覚える今日のこの頃です。

 
閑話休題。

ということで、本題のスクリプトをば。

matchActive.ahk
matchActive.ahk
;vi: set ts=4 sts=4 sw=4 noet :
#NoEnv
#SingleInstance Force
#Persistent
#InstallKeybdHook
CoordMode, Mouse, Screen

SendMode Input
SetTitleMatchMode, 2	;部分一致

global all, filtered, current, listText, includeTokens, excludeTokens
global mru := {}   ; key: hwnd, value: A_TickCount

global switcherVisible := false
global limitToMouseMonitor := false



; ---------- GUI ----------
Gui, New, +AlwaysOnTop -Resize +ToolWindow +HwndhGui, Window Select
;Gui, Font, s10, MS Gothic ; Consolas
Gui, Font, s13, Consolas
Gui, Add, Edit, vFilter w900 gOnFilterChange
Gui, Add, Edit, vList w900 h240 -VScroll -WantReturn ReadOnly

Gui,Show,Hide
SetTimer, WatchFocus, 50		;フォーカス監視タイマー

return


; ---------- ロストフォーカス監視 ----------
WatchFocus:
    if (!switcherVisible)
        return

    WinGet, aid, ID, A
    if (aid != hGui) {
        switcherVisible := false
        WinHide, ahk_id %hGui%
    }
return


; ---------- 全ウィンドウ列挙 ----------
RefreshWindows:
	all := []

	WinGet, idList, List
	Loop, %idList%
	{
		thisID := idList%A_Index%
		WinGetTitle, title, ahk_id %thisID%

		if (title = "")
			continue
		if (title = "Window Select")	;自分自身は無視
			continue
		if (title = "Shell Handwriting Canvas")
			continue
		if (title = "Program Manager")
			continue

		mon := GetWindowMonitor(thisID)
		all.Push({ id: thisID, title: title, mon: mon })
	}

	all.Sort(Func(CompareMRU))

return


; ---------- 並び変え ----------
CompareMRU(a, b) {
	global mru

	t1 := mru.HasKey(a.id) ? mru[a.id] : 0
	t2 := mru.HasKey(b.id) ? mru[b.id] : 0

	; 新しいものを先頭へ
	return (t2 - t1)
}


; ---------- ホットキー ----------
!q::	;Alt+Q
	Critical
	limitToMouseMonitor := false
	if(!switcherVisible){			;スイッチャが非表示の場合
		GoSub, ShowSwitcher			;スイッチャを表示
	}else{							;スイッチャが表示中の場合
		current := 2				;2番目のものを選択
		GoSub, ShowWindow			;選択
	}
return

+!q::  ; Alt+Shift+Q
	Critical
	limitToMouseMonitor := true
	if (!switcherVisible) {
		GoSub, ShowSwitcher
	} else {
		current := 2
		GoSub, ShowWindow
	}
return

ShowSwitcher:
	switcherVisible := true				;表示中に設定
	GoSub, RefreshWindows				;現時点の全ウィンドウ列挙しなおす

	MouseGetPos, mx, my					;表示モニタは常にマウス位置で決める。
	targetMon := GetMonitorFromPoint(mx, my)

	if (targetMon = 0)					;モニタが不明な場合メインモニタにフォールバック
		targetMon := 1

	ShowGuiOnMonitorCenter(targetMon)	;マウス側モニタ中央に表示

	UpdateModeIndicator()				;タイトル変更

	GoSub, OnFilterChange				;フィルタ適用&描画
	SetTimer, FixFocusToFilter, -1		;ワンショットタイマでフォーカス設定
return

UpdateModeIndicator() {
	global hGui
	title := "Window Select"

	MouseGetPos, mx, my
	mon := GetMonitorFromPoint(mx, my)
	title .= " [MON:" mon "]"

    WinSetTitle, ahk_id %hGui%, , %title%
}


FixFocusToFilter:
    ; 念のためGUIがアクティブな時だけ
    WinGet, aid, ID, A
    if (aid != hGui)
        return

    ; Filter(Edit1)にフォーカス
    ControlFocus, Edit1, ahk_id %hGui%

    ; 反転選択解除&キャレットを文末
    ControlGetText, q, Edit1, ahk_id %hGui%
    len := StrLen(q)

    ; Edit1 に対して EM_SETSEL を送る
    ControlGet, hEdit1, Hwnd,, Edit1, ahk_id %hGui%
    SendMessage, 0xB1, len, len,, ahk_id %hEdit1%

return


; ---------- 描画 ----------
Render:
	listText := ""						;リストに表示するテキスト
	linePos := 0
	caretPos := 0

	for i, w in filtered {				;フィルタ済みのウィンドウ一覧
		if (i = current)				;現在行の場合
			caretPos := linePos			;キャレット位置を保持

		shown := Highlight(w.title)		;ウィンドウタイトルをハイライト
		prefix := (i = current) ? "[*] " : "[ ] "	;現在行の場合[*]マークを付ける。

		tag := " @MON" w.mon
		line := prefix shown tag "`r`n"	;マーク+ウィンドウタイトル+モニタ番号+改行

		listText .= line				;リストに表示するテキストに加える
		linePos += StrLen(line)			;全量の長さを保持
	}

	ControlSetText, Edit2, %listText%, ahk_id %hGui%

	;キャレットを現在行へ(リストの表示範囲よりも下まで行ったときスクロールするため)
	SendMessage, 0xB1, caretPos, caretPos, Edit2, ahk_id %hGui%	;EM_SETSEL
	SendMessage, 0xB7, 0, 0, Edit2, ahk_id %hGui%				;EM_SCROLLCARET
	
return

; ---------- ハイライト処理 ----------
Highlight(title) {
	global includeTokens
	out := title

	for _, tok in includeTokens {
		if (tok = "")
			continue

		; 壊れたトークンが来ても表示を壊さないように
		esc := EscapeRegexLiteral(tok)
		if (esc = "")
			continue

		;大文字小文字無視"i)"、でマッチした「元の文字列」"[$0]"を囲む
		try out := RegExReplace(out, "i)" esc, "[$0]")
		catch e {
			;ToolTip, bad tok: %tok%`nesc: %esc%
			continue
		}
	}
	return out
}


EscapeRegexLiteral(s) {
	; まず \ を倍化(末尾の \ でも死なないように)
	s := StrReplace(s, "\", "\\")

	; 正規表現のメタ文字をエスケープ
	; . ^ $ * + ? ( ) [ ] { } |
	s :=RegExReplace(s, "([.\^\$\*\+\?\(\)\[\]\{\}\|])", "\$1")
	return s
}


; ---------- フィルタ ----------
OnFilterChange:
	GuiControlGet, q,, Filter
	includeTokens := []								;包含リスト。Renderで引用するためにglobal
	excludeTokens := []								;除外リストはローカルでok
	monitorFilter := []								; @1, @2 など
	filtered := []									;フィルタ済みのウィンドウを格納(global)

	q := Trim(q)

	mouseMon := 0
	if (limitToMouseMonitor) {
		MouseGetPos, mx, my
		mouseMon := GetMonitorFromPoint(mx, my)
		if (mouseMon = 0)
			mouseMon := 1
	}


	if (q != ""){									;フィルタ設定ありの場合
		tokens := StrSplit(q,A_Space)				;トークンに分解
		for _,p in tokens {							;全トークンに対して、
			p := Trim(p)
			if (p = "")								;空は無視
				continue

			; --- \@ はエスケープ(文字としての @) ---
			if (SubStr(p, 1, 2) = "\@") {
				includeTokens.Push(SubStr(p, 2))
				continue
			}

			; --- @ はモニタ指定(予約語) ---
			if (SubStr(p, 1, 1) = "@") {
				num := SubStr(p, 2)
				if (num ~= "^\d+$")
					monitorFilter.Push(num)
				continue
			}

			; --- NOT条件 ---
			if (SubStr(p, 1, 1) = "!") {				;!で始まる場合
				if (StrLen(p) > 1)						;かつ、2文字目以降もあり
					excludeTokens.Push(SubStr(p, 2))	;除外リストに追加
				continue
			}

			; --- 通常トークン ---
			includeTokens.Push(p)						;包含リストに追加

		}
	}


	for i, w in all {								;全ウィンドウリストに対して、
		if (limitToMouseMonitor) {					;モニタ内に限定する場合
			if (w.mon != mouseMon)					;現在のモニタと違ったら抜ける
				continue
		}

		; --- @mon ---
		if (monitorFilter.Length() > 0) {			;モニタ指定ありの場合
			ok := false
			for _, mn in monitorFilter {
				if (w.mon = mn) {
					ok := true
					break
				}
			}
			if (!ok)
				continue
		}

		if (q != ""){								;フィルタ設定ありの場合
			matched := true							;初期値はマッチ

			;AND 条件
			for _, t in includeTokens {				;包含リストのすべてが、
				if (!InStr(w.title, t)){			;単語が含まれていない場合は
					matched := false				;マッチしないウィンドウに判定
					break							;トークンの繰り返しを抜けて次のウィンドウに
				}
			}
			if(!matched)							;AND条件判定を満たした場合だけ、
				continue							;NOT条件判定へ進む。

			;NOT 条件
			for _, t in excludeTokens {				;除外リストのすべてが、
				if (InStr(w.title, t)){				;単語が含まれてた場合は
					matched := false				;マッチしないウィンドウに判定
					break							;トークンの繰り返しを抜けて次のウィンドウに
				}
			}
			if(!matched)							;NOT条件判定後に、マッチしてしまった場合は
				continue							;次のウィンドウの判定に移る。
		}
		filtered.Push(w)							;ここまで来たら、フィルタ済み配列に詰める。
	}

	current := (filtered.Length() > 0) ? 1 : 0
	GoSub, Render
return


; ---------- IME状態確認 ----------
IME_GetState() {
	WinGet, hWnd, ID, A
	return DllCall("imm32\ImmGetOpenStatus"
		, "Ptr", DllCall("imm32\ImmGetContext", "Ptr", hWnd))
}


; ---------- マルチモニター関係 ----------
GetMonitorFromPoint(x, y) {
    SysGet, count, MonitorCount
    Loop, %count% {
        SysGet, mon, Monitor, %A_Index%
        if (x >= monLeft && x < monRight && y >= monTop && y < monBottom)
            return A_Index
    }
    return 0
}

GetWindowMonitor(hwnd) {
    WinGetPos, x, y, w, h, ahk_id %hwnd%
    ; 中心点で判定(画面またぎでも破綻しにくい)
    cx := x + w//2
    cy := y + h//2
    return GetMonitorFromPoint(cx, cy)
}


ShowGuiOnMonitorCenter(monIndex) {
    global hGui

    ; 表示(非表示だと位置移動が効かないことがある)
    WinShow, ahk_id %hGui%

    if (monIndex <= 0) {
        WinActivate, ahk_id %hGui%
        return
    }

    ; モニタ矩形
    SysGet, mon, Monitor, %monIndex%

    ; GUIサイズ(取れないときはフォールバック)
    WinGetPos, gx, gy, gw, gh, ahk_id %hGui%
    if (gw = 0 || gh = 0) {
        gw := 900
        gh := 300
    }

    cx := monLeft + (monRight - monLeft - gw) // 2
    cy := monTop  + (monBottom - monTop  - gh) // 2

    ; 位置移動(サイズ変更はしない)
    WinMove, ahk_id %hGui%, , %cx%, %cy%

    WinActivate, ahk_id %hGui%
}



; ---------- キー操作 ----------
#If WinActive("ahk_class AutoHotkeyGUI")

Down::									;下
^n::									;Ctrl+N
^j::									;Ctrl+J
!j::									;Alt+J	※Alt+Qで起動した後そのままjkで上下
	if (current < filtered.Length()) {
		current++
	;}else{								;上下でワープするのははしない方がいいかも。
	;	current := 1					;(優先度最低と最高でつながってしまうので)
	}
	GoSub, Render
return

Up::									;上
^p::									;Ctrl+P
^k::									;Ctrl+K
!k::									;Alt+K	※Alt+Qで起動した後そのままjkで上下
	if (current > 1) {
		current--
	;}else{								;上下でワープするのははしない方がいいかも。
	;	current := filtered.Length()	;(優先度最低と最高でつながってしまうので)
	}
	GoSub, Render
return

^u::
	ControlSetText, Edit1,, ahk_id %hGui%	;フィルタ解除
return

^w::
	GuiControlGet, q,, Filter		; Filter(Edit1) の内容を取得

	q := RTrim(q)					; 末尾空白を削除

	pos := InStr(q, " ", false, 0)	; 後ろから検索

	if (pos > 0)
		;q := SubStr(q, 1, pos - 1)	; 最後の空白より前を残す
		q := SubStr(q, 1, pos)		; 最後の空白より前を残す(空白を残す)
	else							; トークンが1個だけだった場合は
		q := ""						; 善削除

	ControlSetText, Edit1, %q%, ahk_id %hGui%		; 再作成したフィルタを設定して、

	len := Strlen(q)					;キャレットを文末に
	SendMessage, 0xB1, len, len, Edit1, ahk_id %hGui%	; EM_SETSEL

	GoSub, OnFilterChange			; フィルタ再実行
return

Enter::
+Enter::
	if (IME_GetState()){	;IME変換中は
		Send {Enter}		;確定するだけ
		return
	}

	if (filtered.Length() = 0)
		return
	
	GoSub, ShowWindow

return

ShowWindow:
	switcherVisible := false
	selId := filtered[current].id
	WinActivate, ahk_id %selId%

	; --- MRU更新 ---
	mru[selId] := A_TickCount

	;Gui, Hide
	WinHide, ahk_id %hGui%

	; --- Shift+Enter かつ Edge の場合だけ ---
	WinGet, exe, ProcessName, ahk_id %selId%
	if (exe = "msedge.exe" && GetKeyState("Shift", "P")) {
		; アクティブ化が安定するのを少し待つ
		Sleep, 50
		SendInput, ^+a	  ; Ctrl+Shift+Aで「タブの検索」
	}

return


Esc::
	switcherVisible := false
	;Gui, Hide
	WinHide, ahk_id %hGui%
return


#If

使い方

ahkファイルを、ahk.exeに関連付けてあるなら、起動するだけで常駐を開始します。

  • Alt+Qで表示されます。
  • 現在存在するウィンドウを一覧表示します。
  • テキストボックスに入力した文字列でフィルタを行います。
  • スペース区切りで入力したワードで、AND条件で絞り込みを行います。
  • ワードの先頭に!をつけてNOT条件を指定できます。
  • 上下カーソルや、Ctrl+n,pなど(詳しくはスクリプト参照のこと)で、リストを選ぶことができます。
  • Enterで選択したウィンドウをアクティブにして自身は非表示になります。
  • ESCでキャンセルします。
  • フォーカスを失った時には非表示になります。
  • 次回Alt+Qで表示したときには、前回入力したフィルタが残ったままになっています。
  • リストの順序は、直近で選択した順で並び替えを行います。
  • Ctrl+Uでフィルタ条件を全消去します。
  • Ctrl+Eでフィルタ条件をワード単位で消去します。
  • Alt+Qをもう一度押すと、リストの2番目のウィンドウに切り替える動作をします。(直近2枚のウィンドウをスイッチ)

後は、使いながら不満点があれば修正していこうかと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?