Windows 11 の仮想デスクトップの操作を拡張

Windows 11用の仮想デスクトップの拡張ユーティリティが存在しないなあと思っていたら、どうやら AutoHotkey というユーティリティで自分でスクリプトを書くのが流行りのようだった。

必要なのは次の２つ

- [AutoHotkey](https://www.autohotkey.com/) （v2.0）
- [VirtualDesktopAccessor.dll](https://github.com/Ciantic/VirtualDesktopAccessor)

VirtualDesktopAccessor.dll は AutoHotkey のスクリプトファイル（.ahk）と同じディレクトリに配置する。


### 欲しい機能

- **Ctrl + Win + Alt + Left / Right** のホットキーで、フォーカス中のウィンドウを隣の仮想デスクトップへ移動する
- 画面最上端でマウスホイールを操作することで、仮想デスクトップを切り替える。


### VirtualDesktopAccessor.ahk

v2.0 用のスクリプはあまり見かけなかったので、chatGPT に作らせてみた。

```m68k
; VirtualDesktopAccessor DLL のパスを設定
DllPath := A_ScriptDir "\VirtualDesktopAccessor.dll"

; DLL をロード
DllCall("LoadLibrary", "Str", DllPath)

; ウィンドウを右のデスクトップに移動
#^!Right:: {
    hwnd := WinExist("A") ; フォーカス中のウィンドウハンドルを取得
    DllCall(DllPath "\MoveWindowToDesktopNumber", "Ptr", hwnd, "Int", GetCurrentDesktopNumber() + 1, "Int")
    SwitchToNextDesktop(1)
}

; ウィンドウを左のデスクトップに移動
#^!Left:: {
    hwnd := WinExist("A") ; フォーカス中のウィンドウハンドルを取得
    DllCall(DllPath "\MoveWindowToDesktopNumber", "Ptr", hwnd, "Int", GetCurrentDesktopNumber() - 1, "Int")
    SwitchToNextDesktop(-1)
}

; マウスホイールアップで前の仮想デスクトップへ
~WheelUp::
{
    if IsMouseAtScreenTop() {
        SwitchToNextDesktop(-1)
    }
}

; マウスホイールダウンで次の仮想デスクトップへ
~WheelDown::
{
    if IsMouseAtScreenTop() {
        SwitchToNextDesktop(1)
    }
}

; 現在の仮想デスクトップ番号を取得
GetCurrentDesktopNumber() {
    return DllCall(DllPath "\GetCurrentDesktopNumber", "Int")
}

; 仮想デスクトップを切り替える
SwitchToNextDesktop(direction) {
    currentDesktop := GetCurrentDesktopNumber()
    targetDesktop := currentDesktop + direction
    ; 左端、右端でループさせない
    maxDesktop := 3 ; 仮想デスクトップの数（調整が必要）
    if targetDesktop < 0 {
        targetDesktop := 0
    } else if targetDesktop >= maxDesktop {
        targetDesktop := maxDesktop - 1
    }
    DllCall(DllPath "\GoToDesktopNumber", "Int", targetDesktop)
}

; マウスカーソルが画面上端にいるか確認
IsMouseAtScreenTop() {
    CoordMode("Mouse", "Screen") ; 画面全体の座標を使用
    xpos := 0
    ypos := 0
    MouseGetPos(&xpos, &ypos) ; マウス位置を取得
    ;ToolTip("Mouse Position: (" . xpos . "," . ypos . ")")
    return (ypos < 10) ; 画面上端の範囲（10px 以内）
}
```


-----

追記

ホイール操作が過敏すぎて仮想デスクトップが３つ以上あるときに使いづらかったので、規定時間のうちに１回転以上回したときに反応するようにした。

規定時間内に１回転せずタイムアウトしたときにカウンタを初期化する処理を入れてたら、めちゃ複雑なスクリプトになってしまった。
自分で考えて作ったというより、AIに何度もツッコミを入れてブラッシュアップしたスクリプトだ。AI万歳！！

```m68k
; VirtualDesktopAccessor DLL のパスを設定
DllPath := A_ScriptDir "\VirtualDesktopAccessor.dll"
global WheelUpCount := 0
global WheelDownCount := 0
DebounceDelay := 100 ; milliseconds
RequiredCount := 1


; DLL をロード
DllCall("LoadLibrary", "Str", DllPath)

; ウィンドウを右のデスクトップに移動
#^!Right:: {
    hwnd := WinExist("A") ; フォーカス中のウィンドウハンドルを取得
    DllCall(DllPath "\MoveWindowToDesktopNumber", "Ptr", hwnd, "Int", GetCurrentDesktopNumber() + 1, "Int")
    SwitchToNextDesktop(1)
}

; ウィンドウを左のデスクトップに移動
#^!Left:: {
    hwnd := WinExist("A") ; フォーカス中のウィンドウハンドルを取得
    DllCall(DllPath "\MoveWindowToDesktopNumber", "Ptr", hwnd, "Int", GetCurrentDesktopNumber() - 1, "Int")
    SwitchToNextDesktop(-1)
}


; マウスホイール操作で前後の仮想デスクトップへ
~WheelUp::
{
    if IsMouseAtScreenTop() {
        global WheelUpCount
        WheelUpCount++
        SetTimer(HandleWheelUp, DebounceDelay)
    }
}

~WheelDown::
{
    if IsMouseAtScreenTop() {
        global WheelDownCount
        WheelDownCount++
        SetTimer(HandleWheelDown, DebounceDelay)
    }
}

HandleWheelUp() {
    global WheelUpCount
    if WheelUpCount > 0 {
        SetTimer(HandleWheelUp, -DebounceDelay) ; タイマを一度だけ実行
        ; MsgBox "WheelUpCount" . WheelUpCount
        if WheelUpCount >= RequiredCount {
            SwitchToNextDesktop(-1)
        }
        WheelUpCount := 0
    }
}

HandleWheelDown() {
    global WheelDownCount
    if WheelDownCount > 0 {
        SetTimer(HandleWheelDown, -DebounceDelay) ; タイマを一度だけ実行
        ; MsgBox "WheelDownCount" . WheelDownCount
        if WheelDownCount >= RequiredCount {
            SwitchToNextDesktop(1)
        }
        WheelDownCount := 0
    }
}

; 現在の仮想デスクトップ番号を取得
GetCurrentDesktopNumber() {
    return DllCall(DllPath "\GetCurrentDesktopNumber", "Int")
}

; 仮想デスクトップを切り替える
SwitchToNextDesktop(direction) {
    currentDesktop := GetCurrentDesktopNumber()
    targetDesktop := currentDesktop + direction
    ; 左端、右端でループさせない
    maxDesktop := 3
    if targetDesktop < 0 {
        targetDesktop := 0
    } else if targetDesktop >= maxDesktop {
        targetDesktop := maxDesktop - 1
    }
    DllCall(DllPath "\GoToDesktopNumber", "Int", targetDesktop)
}

; マウスカーソルが画面上端にいるか確認
IsMouseAtScreenTop() {
    CoordMode("Mouse", "Screen") ; 画面全体の座標を使用
    xpos := 0
    ypos := 0
    MouseGetPos(&xpos, &ypos) ; マウス位置を取得
    ;ToolTip("Mouse Position: (" . xpos . "," . ypos . ")")
    return (ypos < 10) ; 画面上端の範囲（10px 以内）
}
```