rcmdnk's blog

20130723_AutoHotKey_200_200

Windowsでエディタ以外の場所でもh/j/k/lで左下上右に移動したり するようにする AutoHotkey製vim_ahk ですが、 Issueなどももらったりして手をかけてみようと思ってたまま結構放置してしまてましたが、 ちょっと見直しをしてみました。

vim_ahk

出来ることなどは上記などを参照。

起動すると、メモ帳などでEscを押すとノーマルモードになって h/j/k/lで左下上右に移動したり、yyでラインコピー、pでペースト、とか出来ます。

擬似的なコマンドモードとか検索とかもなんとなしに出来る様にしてますが、 もしノーマルモードでの動きとか、Vimにあってvim_ahkにないものであったらうれしいもの、 とかあったらIssueとか出してもらうと嬉しいです。

クラスの導入

Split script into logically seperated multiple files · Issue #34 · rcmdnk/vim_ahk

元々はファイルを小分けにしたい、というIssueが来ていてどうしたものかと思ってたところからです。

最初は小さなスクリプトだったこともあり、また、AutoHotkeyのスクリプトとしてそれだけ あると走る、という方が嬉しいかな、と思って一つのファイルで管理していたんですが、 結構大きくなってきたので分けたほうが色々見やすいかな、と言う状態に。

加えて、関数とかラベルとか結構ごちゃごちゃになっていたので整理したい、というところも。

それでどういうふうに分けたら良い中、と思ってたんですが、 そのまま分割していくだけとかだと余り嬉しくもないので、 いっそ色々書き直そうと思ってクラスとかを使って作り直すことにしました。

そうすればクラスごとにファイル分けたりすれば良いので。

というわけで一番上のスクリプトは

vim.ahk
1
2
3
4
5
; Auto-execute section
Vim := new VimAhk()
Return

#Include %A_LineFile%\..\lib\vim_ahk.ahk

といった感じの簡単な形にして、 lib\vim_ahk.ahkにある VimAhkクラスを中心に動くようにしました。

色々変更する中で気になったこととか知ったこととかがあったので、 ちょっとまとまってないかもしれませんがメモしておきたいと思います。

v0.6.0での新しい点

コード構成は結構変わりましたが、ユーザー側としてすぐわかる機能としては、 vim_ahkを有効にするかどうかを決めるアプリケーションリストに関して、 ウィンドウのタイトルでの指定がこれまでは完全に一致するものだけでしたが、 その文字列を含む、だったり正規表現も使える様にしました。

vim_ahkのトレイアイコンの右クリックからVimMenuSettingsに行って、 SetTitleMatchModeという項目があるので、これの最初の項を 2にすると、ウィンドウタイトルに関してそれを含むもの全てをマッチする様になります。

これまでと同じ動作は3にしたときで、ウィンドウタイトルが完全一致したものだけを取っていました。

ahk_exe notepad.exeみたいにClassやProcessを指定する場合は関係ありません。

これは、Windows 10でのOneNoteとかが、Process名はApplicationFrameHost.exeというもので 他のウィンドウズアプリと同じなため、まとめてこれを指定するのは嫌だな、という点があり ウィンドウタイトルを指定していたのですが、 これが今はrcmdnk - OneNoteみたいにユーザー名を含むようなタイトルになっていました。

doesn’t seem to work on onenote on windows 10 · Issue #37 · rcmdnk/vim_ahk

これによってOneNote上ではデフォルトでは有効にならないような状態でした。

勿論、自分でタイトルを調べてSettingsでそのタイトルを入れれば有効になるのですが、 上のようなSetTitleMatchModeの設定があったので導入してみました。

これで、例えばブラウザとかで特定のページに関して、とかもやりやすくなるかもしれません。

AutoHotkeyの色々

今まで書いたものとかについては以下から。

/blog/tags/autohotkey/

ちなみに今使っているのはAutoHotkey_Lのバージョン1の方です。

使っている今のバージョンはVersion 1.1.31.00。

AutoHotkey

Lexikos/AutoHotkey_L: AutoHotkey - macro-creation and automation-oriented scripting utility for Windows.

ドキュメントを見るとV2の方もかなりできてる様ですが、根本的な言語体制が変わってる感じになってるので 移行するとしたら結構1から書き直さないといけない部分も出てくるかもしれません。

今回変更した中で色々調べたりもしたので気になった点とかをまとめておきます。

関数

Functions - Definition & Usage AutoHotkey

1
2
3
MyFunc(x, y="abc"){
  MsgBox %x% %y%
}

な感じで定義します。 引数にはデフォルトの値を与えることも出来ます。

関数を作るときには()を関数名と離してMyFunc ()みたいにするとエラーになってしまうので 必ずくっつける必要があります。

関数は引数による違う関数などは作れず、そのようなものを作った場合はDuplicationエラーになります。

引数にfunc(x="abc", y="efg")の様に引数名を指定した利用は出来ません。 ただ、このようなことをしてもエラーにならないのですが、 それはx="abc"というのがx"abc"が等しいかどうか、という式として認識されるので、 この場合はFalseで0が入力されるだけです。

ラベルと関数

AutoHotkeyではラベルという形で

1
2
3
MyLabel:
  MsgBox "This is MyLabel"
Return

というものが作れ、これを

1
  Goto, MyLabel

みたいな感じで呼ぶことが出来ます。 AutoHotkeyではキーバインドを

1
2
3
4
5
a::
  MsgBox "a"
  Goto, MyLabel
  MyFunc("myfunc")
Return

みたいに設定していきますが、同じような雰囲気でラベルというものを作って 作業をまとめたりしていました。

関数やらオブジェクトやらが後から追加されて、より近代的な雰囲気の書き方ができる様になった感じで、 基本的にラベルでやっていたことは関数で置き換えが可能です。

その際に注意することとしては、ラベルの中では変数がグローバルで扱われるのに対して、 関数の中では変数がローカライズされる点。 必要な場合はgloblを使って呼び込む必要があります。

===

ifとかで比較する際、AutoHotkeyだと同じかどうか判断するのに===の両方を使えます。

The == operator behaves identically to = except when either of the inputs is not numeric, in which case == is always case sensitive and = is always case insensitive (the method of insensitivity depends on StringCaseSense).

ということで、ほとんど同じですが、文字列比較の場合に==だと大文字小文字を区別し、=だと区別しない、ということです(ただし、StringCaseSenseの設定による)。

敢えて大文字小文字を区別しないで比較したい、ということもあるかもしれませんが、 StringLower / StringUpper という変換関数もあるので、それで変換して==で比較するようにした方が見やすくはなると思います。

文字列連結

文字列を連結するときは.でつなげるかそのまま繋げるかで出来ます。

Variables and Expressions - Definition & Usage AutoHotkey

When the dot is omitted, there should be at least one space between the items to be merged.

とあるんですが、実際には

1
2
3
x := "abc"
y := "xxx"x
MsgBox % y

としてもちゃんとyxxxabcとなります。(バグ?か記述の間違え?)

.を使う場合には両側にスペースが無いとうまくいきません。

通常どちらでも構わないんですが、後ろの文字列が++--で始まる場合には それ自体が特別な意味を持ってしまうので.を使って連結する必要があります。

const的なものを作るのはちょっと面倒

Is it possible to create constant? - Ask for Help - AutoHotkey Community

クラス

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
x := new MyClass("foo")
x.Show()
Return

class MyClass{
  static ClassVar := "class variable"
  InstanceVar := "instance variable"

  __New(name=""){
    this.Name := name
    MsgBox % "New MyClass: " this.Name
  }

  __Delete(){
    MsgBox % "Deleting " this.Name
  }

  Show(){
    MsgBox % "name: " this.name ", ClassVar: " MyClass.ClassVar ", InstanceVar: " this.InstanceVar
  }
}

みたいな感じでclass <Class Name>{...}と定義します。

Objects - Definition & Usage AutoHotkey

静的(クラス)変数が作られるタイミング

クラス内でstaticを使ってクラス変数を作る際、これらは auto-execute section(最初のReturnがある部分までのところ) が実行されるより前に定義されます。

そこではstatic x := A_AppData . "\foo"とか表現やAutoHotkeyの内部変数は使うことは出来ますが、 直前に定義したものに対してstatic y: = x . "\barとかは出来ません。(これはxが定義されてないので\barとなる)。

Static declarations are evaluated only once, before the auto-execute section, in the order they appear in the script. Each declaration stores a value in the class object. Any variable references in Expression are assumed to be global.

参照カウント

AutoHotkeyでデストラクタ(__Delete)を使うようなことは余りないかもしれませんが、 これがいつ呼ばれるか、というと AutoHotkeyではdeleteみたいなものはなく、消すためには 全ての参照しているものがなくなったときに消されるような参照カウントという方法を用いています。

vim.ahk
1
2
3
4
5
6
7
x := new MyClass("foo")
y := x
x :=
y :=
Return

...

とかすると、x:=とした時点ではyが最初に作ったMyClassのインスタンスを見ているので消えませんが、 y :=としてしまうと最初に作ったものを見ているものが居なくなるのでこの時点で__Delete()が走ります。

ただし、SetTimerとかでクラス内の変数とかを使ってるとちょっと振る舞いが予期せぬことがあるのでちょっと注意。

Suggestions on documentation improvements - Page 11 - AutoHotkey Community

Question about __Delete() behavior in class with timer - AutoHotkey Community

Guiのv Variable

Guiのテキストボックスとかいろいろな要素にはv variableという値を設定して、 Gui Submit した時にその値を設定したv variableに代入するようにしたり、 また、v variableを使ってGuiControlでその要素を指定したりすることもできます。

Gui - Syntax & Usage AutoHotkey

この値は

Gui, Add, Checkbox, xm vMyCheckBox, My CheckBox

の様にv + <name>で定義します。(vは大文字でもOK)

このv variableはグローバル変数か静的変数である必要があるので、 関数内とかで使う際にはglobalとかで呼び込む必要があります。

そうでないと

a control's variable must be global or static the following variabl name contains an iliegal character

と怒れれます。

関数内でstaticを使って変数を定義しても使えますが、 Gui Submitを使って変数を取得したい場合には別の場所で行なう事になるので staticをうまく使うことは出来ないかと思ってます。

globalを使いたくないな、と思って、クラスの静的変数を使えないものかと、

1
2
3
4
5
6
7
class MyClass{
  static var
  MyFunc(){
    ...
    Gui, Add, Checkbox, % "xm v" MyClass.__Class ".var", My CheckBox
  }
}

みたいにすると

The following variable name contains an illegal character

と怒られます。MyClass.__ClassMyClassという名前を呼んでるだけなので、この部分は vMyClass.varで、MyClass.varというのはクラス変数である意味グローバルに見えるものなので 使えるかな、と思ったのですが、 v variableに.を含む文字列を使えないためエラーになってしまうようです。

ということで、色々調べてみたんですが、今の所、v variableにglobalでない値を入れて操作するのは 難しいかな、という感じでした。

g-label

v variableと似たようなもので、Guiの要素に対してつけるg-label というものがあります。

v variable同様g + <name>で定義します。(gは大文字でもOK)

g-labelはその要素をクリックしたときや、要素の値が変化したときなどに 実行する動作を定義します。

1
2
3
4
5
6
...
Gui, Add, Text, gOpenURL cBlue, Link
...

OpenURL:
  Run https://example.com

みたいにすれば、LinkというテキストをクリックするとWebページが開かれます。

関数ももし引数なしの関数であれば同じ様に あてられます。

1
2
3
4
5
6
7
...
Gui, Add, Text, gOpenURL cBlue, Link
...

OpenURL(){
  Run https://example.com
}

引数ありの関数を使いたい場合、 BoundFunc Object を使ってGuiControlで後から加える形にします。

1
2
3
4
5
6
7
8
9
...
Gui, Add, Text, cBlue vLink, Link
func := Func("OpenURL").bind("http://example.com")
GuiControl, +G, Link, % func
...

OpenURL(url){
  Run %url%
}

引数がある関数の場合にはbind(x, y, z...)の様に bindの中に引数を入れて作ります。

GuiControlではv variableを使って要素を指定していますが、 v labelを付けない場合、

1
2
3
4
5
6
7
8
9
...
Gui, Add, Text, +HwndLinkHwnd cBlue, Link
func := Func("OpenURL").bind("http://example.com")
GuiControl, +G, % Wwnd, % func
...

OpenURL(url){
  Run %url%
}

の様にウィンドウハンドルを使った指定も出来ます。

+Hwnd<name>とすると、その要素のウィンドウハンドル<name>に入れることができ(値はウィンドウハンドルにあたる数字)、 この数字をGuiControlにv variableの代わりに渡ことで要素を指定できます。

もしくはクラス内でクラスメソッドを使いたい場合、

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyClass{

  SetGui(){
    ...
    Gui, Add, Text, cBlue vLink, Link
    func := ObjBindMethod(this, "OpenURL", "https://example.com")
    GuiControl, +G, Link, % func
    ...
  }

  OpenURL(url){
    Run %url%
  }
}

の様にObjBindMethodを使った方法もあります。 使う関数の引数はObjBindMethodの第3引数以降に指定します。

もし、クラスメソッドでも引数がなく、かつ中でインスタンス変数などを使っていないのであれば、

1
Gui, Add, Text, % "g" MyClass.__Class  ".OpenURL cBlue", Link

とすることも可能です。MyClass.OpenURLをg-labelとして指定する、ということ。

v variableの場合と違ってこちらは中に.とかが入っていても構わないのでクラスメソッドを直接指定できます。

A_GuiControlを持つためにはg-labelが必要

A_GuiControlという値は現在フォーカスがあたっている値が入っています。

これとToolTipを使って、Guiで表示しているボタンやテキストなどの説明をポップアップ表示する設定を作れます。

vim_ahk/vim_tooltip.ahk at master · rcmdnk/vim_ahk

OnMessage(0x200, <func>)で、0x200がマウスが動いたとき、を捉えられ、 そのときの動作をfuncで指定できます。 したがって、この時にA_GuiControlを調べて、ToolTipを出したいものならセットする、 というのが上のvim_tooltip.ahkの中でやっていること。

g-label、v variableを共に付けていると、A_GuiControlはv variableの名前になります。

g-labelはv variableとは関係ない名前でも良いわけですが、 g-labelがついてない場合、A_GuiControlは空になります。

この辺は今の環境でやってみてなのでもしかしたら何か勘違いしているかもしれませんが、 手元の環境ではそうなっています。

したがって、例えばただのテキストとかで特に動作が必要なく、g-labelをつける必要がない場合でも、 ToolTipを出したい場合、何もしないDummyのラベルか関数を作ってg-labelをつける必要があります。

vim_ahk/vim_setting.ahk at master · rcmdnk/vim_ahk

この中では、DisableUnusedTextとか、ただのテキストなんですが、この上にマウスオーバーしたときでも ツールチップを出したかったのでダミー関数を作ってg-labelにあてています。

GuiClose, GuiEscapeにクラスメソッドを

Guiを作ると、自動的にGuiClose(ウィンドウをばつ印を押すなどして閉じるときの操作)、GuiEscape(Escを押した時の操作)という g-labelが自動的に加わります。

Gui - Syntax & Usage AutoHotkey

グローバルなGuiCloseGuiEscapeといったラベルや関数があるとそれがウィンドウのばつ印を押したときとかに実行されます。

これは全てのGuiで共通ですが、各Guiのものに関しては<Guiのラベル> + Close という名前でラベルや関数を定義することでその動作を指定できます。

1
2
3
4
5
6
gui new, +LabelMyGui
...

MyGuiClose(){
  MsgBox "Close MyGui"
}

クラス内でGuiを作って、クラス内の関数を使いたい場合、

1
2
3
4
5
6
7
8
9
10
11
class MyClass{

  SetGui(){
    gui new, % "+Label" . this.__Class "."
    ...
  }

  Close(){
    MsgBox "Close MyGui"
  }
}

とすれば、MyClass.Close()を指定することになりクラス内の関数を使えます。 ですが、この場合はクラスメソッドとして呼ぶので、インスタンス変数などは参照できません。

Event(guiescape) call function problem in class - AutoHotkey Community

インスタンスの操作をきちんとしたい場合、上のようなラベルを使った指定ではうまくいきません。

これをなんとかするには、OnMessageを使った方法があります。

vim_ahk/vim_gui.ahk at master · rcmdnk/vim_ahk

1
2
OnMessage(0x112, ObjBindMethod(this, "OnClose"))
OnMessage(0x100, ObjBindMethod(this, "OnEscape"))

0x112はGuiのウィンドウに何らかの操作が行われた時、 0x100は何らかのキーが押されたときを捉えます。

OnCloseの方ではその後、

1
2
3
4
5
OnClose(wp, lp, msg, hwnd){
  if(wp == 0xF060 && hwnd == this.Hwnd){
    this.Hide()
  }
}

としてますが、第一引数に何の操作が行われたか、という情報が入り、 0xF060はウィンドウを閉じる操作が行われた際の番号です。

第4引数にその操作が行われたウィンドウハンドルが入っていて、 これをあらかじめ取得しておいたGuiのウィンドウハンドルと比べて同じならウィンドウを隠す、ということをしています。

このOnMessageは呼ぶたびに新しくそのトリガーに関する動作を加えます。

1
2
3
OnMessage(0x112, ObjBindMethod(this, "OnClose"))
OnMessage(0x112, ObjBindMethod(this, "OnClose"))
OnMessage(0x112, ObjBindMethod(this, "OnClose"))

とかすると、OnCloseが3回呼ばれる事に。 上の例のVimGuiというクラスは、それを継承したGuiを作るクラスをいくつか作るんですが、 それらが作られるたびにOnMessageが追加され、 どのウィンドウが閉じられるときにも全てのOnCloseが走る、ということになります。

なので、例えばGuiをNewしてDestroyして、みたいなことを頻繁にやるのであれば このやり方はちょっと良くないです(その分無駄にOnMessgeの動作が加わっていくので。

vim_ahkでは、設定ウィンドウで以前は初期値を設定し直すのが面倒だったので、 毎回Gui NewしてDestroyして、みたいなことをしていましたが、 上記の理由でHideに置き換えました。表示する設定値は毎回別途アップデートするように。 (そうでなくてもHideにしたほうが多分良かったとは思います。)

もう一つ、Escapeに関しては

1
2
3
4
5
6
7
8
9
10
OnEscape(wp, lp, msg, hwnd){
  if(wp == 27){
    for i, h in this.HwndAll {
      if(hwnd == h){
        this.Hide()
        Return
      }
    }
  }
}

な感じ。

今度はwpに押したキーの10進法でのASCIIコードが入っていて、27がESCです。

その後、Closeの場合には必ずウィンドウ自体のウィンドウハンドルが入りますが、 Escapeを押したタイミングではその時にフォーカスが当たっている要素のウィンドウハンドルが入ります。

ボタンとか何も無ければ全体のウィンドウのウィンドウハンドルがありますが、 ボタンやDropdownなどの要素がある場合にはそれらのうちフォーカスが当たったものになります。

なので、Guiを作る際に

1
2
Gui, % this.Hwnd ":Add", Button, +HwndOK X200 W100 Default, &OK
this.HwndAll.Push(OK)

みたいにフォーカスが当たる可能性のある要素の全てについてウィンドウハンドルを取って リストに詰めておいて上のように全てと比較するようなことをしています。

SetTimerにマイナスの値

1
SetTimer, Label, period

で、periodの間隔でLabelを実行するように出来ますが、periodOffにすればこのタイマーが止まります。

正の整数だとその間隔(ミリ秒)で実行します。

もしマイナスの値を与えると一回だけ、指定ミリ秒後に実行してタイマーを止めます。

Sponsored Links
Sponsored Links

« macOS Catalina 10.15.3 (19D76)にアップデート brew-fileのUbuntuなどDebian系Linuxへの対応 »

}