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 を使うと、安定して横スクロールできました。
参考:
- Microsoft Learn:
DocumentWindow.ScrollIntoView - Microsoft Learn:
DocumentWindow.SmallScroll
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
を使う方がうまくいきました。