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?

PowerPointでShift+マウスホイールによる横スクロールをAutoHotkeyで実現

0
Posted at

PowerPointでShift+マウスホイールによる横スクロールをAutoHotkeyで実現した

PowerPoint の通常編集画面で、Shift + マウスホイール で横スクロールしたかったのですが、素直には動きませんでした。(なんで?)

やりたかったこと

  • PowerPoint の通常編集画面で
  • Shift + ホイール をすると
  • 横スクロールするようにしたい
  • 毎回手動起動は面倒なので、自動起動もしたい

結論

うまくいかなかった方法

  • WM_HSCROLL を PowerPoint ウィンドウに送る
  • WM_MOUSEHWHEEL を投げる
  • mouse_event(MOUSEEVENTF_HWHEEL) で横ホイール入力をエミュレートする

うまくいった方法

  • PowerPoint COM API の ActiveWindow.SmallScroll を呼ぶ

PowerPoint の編集面は通常のスクロールバー付きコントロールではなく、Office 独自 UI の上に乗っているため、
Windows メッセージをそのまま投げても反応しないケースがありました。

一方で、PowerPoint 自身が持っている DocumentWindow のスクロール API を使うと、安定して横スクロールできました。

参考:


PowerPoint の通常編集画面は標準的なスクロールビューではなく、実際には PPTFrameClass, MsoWorkPane, NetUIHWND, mdiClass などの独自ウィンドウ階層になっていました。

最終的な AutoHotkey v2 スクリプト

以下が最終版です。

; PowerPoint Shift+Wheel horizontal-scroll helper
; AutoHotkey v2
#Requires AutoHotkey v2.0
#SingleInstance Force
#UseHook

Persistent()

global POWERPOINT_WINDOW := "ahk_exe POWERPNT.EXE"
global HORIZONTAL_WHEEL_DELTA := 120
global WHEEL_REPEAT_COUNT := 3
global SCROLLINFO_MASK_ALL := 0x17
global SB_HORZ := 0
global WM_HSCROLL := 0x114
global WM_MOUSEHWHEEL := 0x020E
global SB_LINELEFT := 0
global SB_LINERIGHT := 1
global MK_SHIFT := 0x0004
global MOUSEEVENTF_HWHEEL := 0x1000
global BLOCKED_CONTROL_CLASSES := Map(
    "NetUIHWND", true,
    "NetUICtrlNotifySink", true,
    "MsoCommandBar", true,
    "MsoCommandBarDock", true,
    "RICHEDIT60W", true
)

+WheelUp::HandlePowerPointHorizontalScroll(-1)
+WheelDown::HandlePowerPointHorizontalScroll(1)

HandlePowerPointHorizontalScroll(direction) {
    if !WinActive(POWERPOINT_WINDOW) {
        return
    }

    if !IsPowerPointEditSurfaceUnderMouse() {
        return
    }

    if TryPowerPointComScroll(direction) {
        return
    }

    delta := direction * HORIZONTAL_WHEEL_DELTA

    for hwnd in GetCandidateScrollHandles() {
        if TryHScrollOnHandle(hwnd, direction) {
            return
        }

        if TryMouseHWheelOnHandle(hwnd, delta) {
            return
        }
    }

    ; Last resort: emit a native horizontal-wheel event.
    DllCall(
        "user32.dll\mouse_event",
        "UInt", MOUSEEVENTF_HWHEEL,
        "UInt", 0,
        "UInt", 0,
        "Int", delta * WHEEL_REPEAT_COUNT,
        "UPtr", 0
    )
}

TryPowerPointComScroll(direction) {
    try {
        ppt := ComObjActive("PowerPoint.Application")
        if !ppt {
            return false
        }

        activeWindow := ppt.ActiveWindow
        if !activeWindow {
            return false
        }

        step := 3
        if (direction > 0) {
            activeWindow.SmallScroll(0, 0, step, 0)
        } else {
            activeWindow.SmallScroll(0, 0, 0, step)
        }
        return true
    } catch as err {
        return false
    }
}

IsPowerPointEditSurfaceUnderMouse() {
    MouseGetPos ,, &windowHwnd, &controlHwnd, 2

    if !windowHwnd {
        return false
    }

    try processName := WinGetProcessName("ahk_id " windowHwnd)
    catch {
        return false
    }

    if processName != "POWERPNT.EXE" {
        return false
    }

    if !controlHwnd {
        return true
    }

    controlClass := ""
    try controlClass := WinGetClass("ahk_id " controlHwnd)

    return controlClass = "" || !BLOCKED_CONTROL_CLASSES.Has(controlClass)
}

GetCandidateScrollHandles() {
    handles := []
    seen := Map()

    AddHandle(hwnd) {
        if !hwnd || seen.Has(hwnd) {
            return
        }

        seen[hwnd] := true
        handles.Push(hwnd)
    }

    pptHwnd := WinExist(POWERPOINT_WINDOW)
    if !pptHwnd {
        return handles
    }

    MouseGetPos ,, &windowHwnd, &controlHwnd, 2

    if controlHwnd {
        AddHandle(controlHwnd)

        current := controlHwnd
        Loop 4 {
            current := DllCall("user32.dll\GetAncestor", "Ptr", current, "UInt", 1, "Ptr")
            if !current || current = pptHwnd {
                break
            }
            AddHandle(current)
        }
    }

    for controlName in ["mdiClass1", "MDIClient1", "MsoWorkPane2"] {
        try AddHandle(ControlGetHwnd(controlName, POWERPOINT_WINDOW))
    }

    AddHandle(pptHwnd)
    return handles
}

TryHScrollOnHandle(hwnd, direction) {
    infoBefore := GetHorizontalScrollInfo(hwnd)
    if !infoBefore["ok"] {
        return false
    }

    if (infoBefore["max"] - infoBefore["min"]) <= 0 {
        return false
    }

    command := direction < 0 ? SB_LINELEFT : SB_LINERIGHT
    Loop WHEEL_REPEAT_COUNT {
        SendMessage WM_HSCROLL, command, 0,, "ahk_id " hwnd
    }

    Sleep 10
    infoAfter := GetHorizontalScrollInfo(hwnd)
    return infoAfter["ok"] && infoAfter["pos"] != infoBefore["pos"]
}

TryMouseHWheelOnHandle(hwnd, delta) {
    infoBefore := GetHorizontalScrollInfo(hwnd)
    MouseGetPos &mouseX, &mouseY
    wParam := ((delta & 0xFFFF) << 16) | MK_SHIFT
    lParam := ((mouseY & 0xFFFF) << 16) | (mouseX & 0xFFFF)

    Loop WHEEL_REPEAT_COUNT {
        PostMessage WM_MOUSEHWHEEL, wParam, lParam,, "ahk_id " hwnd
    }

    if !infoBefore["ok"] {
        return false
    }

    Sleep 10
    infoAfter := GetHorizontalScrollInfo(hwnd)
    return infoAfter["ok"] && infoAfter["pos"] != infoBefore["pos"]
}

GetHorizontalScrollInfo(hwnd) {
    info := Map(
        "ok", false,
        "min", 0,
        "max", 0,
        "page", 0,
        "pos", 0,
        "trackPos", 0
    )

    if !hwnd {
        return info
    }

    scrollInfo := Buffer(28, 0)
    NumPut("UInt", 28, scrollInfo, 0)
    NumPut("UInt", SCROLLINFO_MASK_ALL, scrollInfo, 4)

    if !DllCall("user32.dll\GetScrollInfo", "Ptr", hwnd, "Int", SB_HORZ, "Ptr", scrollInfo, "Int") {
        return info
    }

    info["ok"] := true
    info["min"] := NumGet(scrollInfo, 8, "Int")
    info["max"] := NumGet(scrollInfo, 12, "Int")
    info["page"] := NumGet(scrollInfo, 16, "UInt")
    info["pos"] := NumGet(scrollInfo, 20, "Int")
    info["trackPos"] := NumGet(scrollInfo, 24, "Int")
    return info
}

実装のポイント

1. まずは PowerPoint COM を使う

本命はここです。

ppt := ComObjActive("PowerPoint.Application")
activeWindow := ppt.ActiveWindow
activeWindow.SmallScroll(0, 0, step, 0)

右へスクロールしたいときは ToRight、左へスクロールしたいときは ToLeft を指定しています。

PowerPoint 自身の API を使っているので、
単純な SendMessage よりもずっと信頼性が高かったです。

2. 編集画面っぽい場所だけで反応させる

PowerPoint 全体で無差別に反応すると、リボンやテキスト編集時にも発火してしまいます。

そこで MouseGetPos でカーソル下のウィンドウを見て、
明らかに除外したいクラスを弾いています。

global BLOCKED_CONTROL_CLASSES := Map(
    "NetUIHWND", true,
    "NetUICtrlNotifySink", true,
    "MsoCommandBar", true,
    "MsoCommandBarDock", true,
    "RICHEDIT60W", true
)

自動起動したい場合

Windows のスタートアップにショートカットを置けば OK です。

  • ターゲット:
    C:\Program Files\AutoHotkey\v2\AutoHotkey64.exe
  • 引数:
    "C:\Users\YOUR_NAME\ppt_hscroll.ahk"

これでログイン後に自動起動できます。

まとめ

PowerPoint で Shift + ホイール 横スクロールを実現したい場合、
Windows メッセージを直接投げるよりも、

  • PowerPoint COM API
  • ActiveWindow.SmallScroll

を使う方がうまくいきました。

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?