AutoHotkeyでNotepad++制御し、文字を括弧で囲むスクリプト作成
はじめに
この記事では、Notepad++を外部制御し、選択範囲の文字列を括弧で囲むAutoHotkey v2スクリプトを作成した話を紹介します。
Notepad++のエディタ部分は「Scintilla」というテキスト処理エンジンで構成されており、このスクリプトは、AutoHotkeyからScintillaコマンドを操作することで高度な制御を実現しています。
対象読者は、AutoHotkeyやWindows APIをある程度理解している中級者を想定しています。
必要な環境
- AutoHotkey バージョン2
- Notepad++
スクリプトの動作概要
Notepad++でテキストを選択した状態でショートカットキーを押すと、選択箇所が括弧付きテキストに置き換わります。
例として、 サンプル を選択してショートカットキーを押すと 「サンプル」 になり、もう一度押すと 『サンプル』 になり、さらに押すと括弧を外して最初に戻るような動作です。
内部的には括弧を付ける処理を行った後に、選択範囲を保持するようにしているため、繰り返し括弧の種類を切り替えられます。
技術的ポイント
- AutoHotkeyからWindowsのSendMessage経由でScintillaコマンドを送信し、Notepad++のエディタ操作を実現。
- テキストの送受信には、VirtualAllocExでNotepad++側プロセス空間にバッファを確保し、WriteProcessMemoryやReadProcessMemoryでデータを転送。
- UTF-8テキストの処理では、必要なメモリ領域を事前に計算し、日本語を含む文字列でも安全に扱えるようにしている。
- 選択範囲を再設定する仕組みにより、括弧切り替えを連続実行できる。
Scintillaコマンドの検証結果
検証の結果、次のように動作を確認しました。
- SCI_GETCURRENTPOS、SCI_GETSELECTIONSTART、SCI_GETSELECTIONEND、SCI_SETSELはSendMessageだけで取得可能。
- SCI_GETSELTEXTは、VirtualAllocExとReadProcessMemoryを併用する必要あり。
- SCI_REPLACESELは、VirtualAllocExとWriteProcessMemoryを併用する必要あり。
これら6種類のコマンドの動作を実際に確認したことで、Scintilla経由で多くの操作をAutoHotkeyから扱える見通しが立ちました。
AI活用と開発過程での学び
開発中はAIに繰り返し質問し、プロセス間通信や文字コード処理の壁を一つずつ突破していきました。
実現が難しそうな要件でも、躊躇せずAIに質問を投げることで、新しい視点や解決の糸口が得られました。最終的には、AIの提案を基に自分で検証・修正を重ねることで、期待通りに動作するスクリプトに仕上げることができました。
AIとの協働は、開発のスピードと発想の広がりを確実に高めてくれると実感しています。
スクリプト
#HotIf WinActive("ahk_exe notepad++.exe")
/**
* Notepad++の選択範囲を取得して返す。
* 選択範囲がない場合、-1を返す。
*/
notepadPlusPlus_GETSELTEXT() {
SCI_GETSELECTIONSTART := 2143
SCI_GETSELECTIONEND := 2145
SCI_GETSELTEXT := 2161
PROCESS_ALL_ACCESS := 0x1F0FFF
MEM_COMMIT_RESERVE := 0x1000 | 0x2000
PAGE_READWRITE := 0x04
MEM_RELEASE := 0x8000
npHwnd := WinGetID("A")
sciHwnd := ControlGetHwnd("Scintilla1", "ahk_id " npHwnd)
pid := WinGetPID("ahk_id " npHwnd)
hProcess := DllCall("OpenProcess", "UInt", PROCESS_ALL_ACCESS, "Int", False, "UInt", pid, "Ptr")
if !hProcess {
MsgBox "プロセスオープンに失敗"
ExitApp
}
; 選択範囲の長さ取得
startPos := SendMessage(SCI_GETSELECTIONSTART, 0, 0, sciHwnd)
endPos := SendMessage(SCI_GETSELECTIONEND, 0, 0, sciHwnd)
selLen := endPos - startPos
; 選択範囲がない場合、-1を返す。
if selLen <= 0 {
DllCall("CloseHandle", "Ptr", hProcess)
return -1
}
; リモートプロセスにバッファ確保(文字数+ヌル文字)
remoteBuf := DllCall("VirtualAllocEx", "Ptr", hProcess, "Ptr", 0, "UPtr", selLen + 1, "UInt", MEM_COMMIT_RESERVE, "UInt", PAGE_READWRITE, "Ptr")
if !remoteBuf {
MsgBox "VirtualAllocExに失敗"
DllCall("CloseHandle", "Ptr", hProcess)
ExitApp
}
; 第2引数にNULLを渡すことで必要なバイト数を取得
n_seltext_bytes := SendMessage(SCI_GETSELTEXT, 0, 0, sciHwnd)
if n_seltext_bytes != selLen {
MsgBox "Assertion Error, n_seltext_bytes != selLen"
ExitApp
}
SendMessage(SCI_GETSELTEXT, 0, remoteBuf, sciHwnd)
localBuf := Buffer(selLen + 1)
; リモートプロセスからテキスト読み込み
DllCall("ReadProcessMemory", "Ptr", hProcess, "Ptr", remoteBuf, "Ptr", localBuf.Ptr, "UPtr", selLen + 1, "UPtr*", 0)
selText := StrGet(localBuf.Ptr, selLen, "UTF-8")
; リソース解放
DllCall("VirtualFreeEx", "Ptr", hProcess, "Ptr", remoteBuf, "UPtr", 0, "UInt", MEM_RELEASE)
DllCall("CloseHandle", "Ptr", hProcess)
return selText
}
/**
* Notepad++の選択範囲をreplace_textと置換する。
* 置換後、replace_textを選択範囲にする。
*/
notepadPlusPlus_REPLACESEL(replace_text) {
SCI_GETSELECTIONSTART := 2143
SCI_SETSEL := 2160
SCI_REPLACESEL := 2170
PROCESS_ALL_ACCESS := 0x1F0FFF
npHwnd := WinGetID("A")
npPid := WinGetPID("A")
sciHwnd := ControlGetHwnd("Scintilla1", "ahk_id " npHwnd)
sel_start1 := SendMessage(SCI_GETSELECTIONSTART, 0, 0, , "ahk_id " sciHwnd)
hProc := DllCall("OpenProcess", "UInt", PROCESS_ALL_ACCESS, "Int", 0, "UInt", npPid, "Ptr")
if !hProc {
MsgBox("OpenProcess failed")
ExitApp
}
txt_len := StrLen(replace_text)
len := StrPut(replace_text, "UTF-8") ; UTF-8符号化された文字に必要なバイト数を計算(文字列の長さと異なる)
buf := Buffer(len)
StrPut(replace_text, buf.Ptr, "UTF-8")
mem_addr := DllCall("VirtualAllocEx", "Ptr", hProc, "Ptr", 0, "UInt", len, "UInt", 0x3000, "UInt", 0x4, "Ptr")
if !mem_addr {
MsgBox("VirtualAllocEx failed")
DllCall("CloseHandle", "Ptr", hProc)
ExitApp
}
; リモートプロセスにテキスト書き込み
DllCall("WriteProcessMemory", "Ptr", hProc, "Ptr", mem_addr, "Ptr", buf.Ptr, "UInt", len, "UInt*", 0)
SendMessage(SCI_REPLACESEL, 0, mem_addr, , "ahk_id " sciHwnd)
; リソース解放
DllCall("VirtualFreeEx", "Ptr", hProc, "Ptr", mem_addr, "UInt", 0, "UInt", 0x8000)
DllCall("CloseHandle", "Ptr", hProc)
sel_start2 := SendMessage(SCI_GETSELECTIONSTART, 0, 0, , "ahk_id " sciHwnd)
; 位置sel_start1からsel_start2までを選択範囲にする。
SendMessage(SCI_SETSEL, sel_start1, sel_start2, , "ahk_id " sciHwnd)
}
/**
* Notepad++において選択範囲を hoge → 「hoge」 → 『hoge』 → hoge でループする。
*/
括弧化() {
s_sel := notepadPlusPlus_GETSELTEXT()
if Type(s_sel) == "Integer" {
MsgBox "選択範囲がありません。"
return
}
if RegExMatch(s_sel, "s)^「(.*)」$", &m) {
s_sel := "『" m[1] "』"
} else if RegExMatch(s_sel, "s)^『(.*)』$", &m) {
s_sel := m[1]
} else {
s_sel := "「" s_sel "」"
}
notepadPlusPlus_REPLACESEL(s_sel)
}
^+![:: 括弧化()
#HotIf
おわりに
AutoHotkeyからNotepad++を直接制御することで、Scintillaの豊富な機能を有効活用できる可能性が広がります。
今回の取り組みはその一例であり、今後はこの手法を基盤としたより高度なNotepad++拡張や自動化ツールの開発も期待できそうです。
Discussion