JXAを知らない方はとりあえず以下の記事をどうぞ。
- JavaScript for Automation Release Notes - 公式のドキュメント
- 鳶嶋工房 / AppleScript / JavaScript for Automation (JXA)
- dtinth/JXA-Cookbook
- 知らないうちにMacがシステム標準でJavaScriptで操作できるようになってた (JXA) - Qiita
ここではキーボードやマウスの操作をエミュレートする方法を書きます。
各アプリに用意された機能はスクリプトエディタから見られる「用語説明」等に任せます。
ちょっとしたコードを試すにはosascript
コマンドのインタラクティブモードが便利です。
$ osascript -l JavaScript -i
で起動できます。
System Events
System Events
はスクリプト専用の、GUIを持たないアプリケーションです。
var se = Application("System Events"); //参照を取得
これを使ってユーザの操作をエミュレートできます。
以下より、変数se
にはApplication("System Events")
を参照しているものとします。
キーボードの操作をエミュレート
キーボードを操作するには.keystroke()
と.keyCode()
を使います。
.keystroke()
渡された文字列のキーを打鍵します。
se.keystroke("a");
2つ目の引数で、同時押しする特殊キーを指定できます。
複数の場合は配列で渡します。
// command + h (ウィンドウを隠す)
se.keystroke("h", {using:"command down"});
// command + alt + h (他のウィンドウを隠す)
se.keystroke("h", {using:["command down", "option down"]});
同時押しに指定できる特殊キーは以下の4種類のみです。
文字列 | |
---|---|
⌘ | "command down" |
⌃ | "control down" |
⇧ | "shift down" |
⌥ | "option down" |
2文字以上なら順番に打鍵されます。
se.keystroke("abc");
abc
エスケープ文字も可能です。
\n
でエンターキーが押されますし、\b
でdeleteキーが押されます。
(キーボードの打鍵のエミュレートなので、日本語は無理です。)
(追記: と、思ったけど\n
はエンターじゃないっぽいです。ここちょっと怪しいのであんまり信じないでください。)
se.keystroke("//one\n//twoooooo\b\b\b\b\b\n");
//one
//two
2文字以上の文字列を渡した状態で特殊キーを指定すると すべての文字で 同時押しされます。
// 新しいウィンドウを3つ開く
se.keystroke("nnn", {using:"command down"});
大文字もそのまま入力できました。
大文字はshift + 小文字なので、"shift down"を書く必要はなくなります。
command + shift + nというショートカットキーを押したい場合、.keystroke("N", {using:"command down"})
だけでいけます。
.keyCode()
.keystroke
で押せないキーの場合は.keyCode
を使います。
.keystroke
と同様に、特殊キーの同時押しも可能です。
(追記: MacbookのJISキーボードでしか試していませんが、キーボードの配列によっても異なるっぽいのでここに書いてある通りにはならないかもしれません。)
各キーを表す数値を入力して使います。
たとえば、「英数」キーは102、「かな」キーは104です。
// 「かな」キーを押す
se.keyCode(104);
// 「英数」キーを押す
se.keyCode(102);
.keyCode
も複数の入力が可能です。
キーコードの配列を渡します。
se.keyCode(104); //「かな」キーを押す
se.keyCode([40, 31, 45, 45, 45, 34, 8, 4, 34, 13, 0]);
「こんにちわ」と入力されましたか?
キーコードの確認方法
だいたいこのページに書いてあります。
または以下のコマンドで、バーチャルキーボードのキーコードを全部見れます。たぶん全部。
grep 'kVK_.*=' /System/Library/Frameworks/Carbon.framework/Frameworks/HIToolbox.framework/Headers/Events.h
キーボードを押してキーコードを個別に確認したい場合、Karabiner (元KeyRemap4MacBook) のEvent Viewerが便利です。
Event Viewerは、押したキーのキーコードを16進数で表示してくれます。
JavaScriptは普通に16進数の数値も扱えるので、もちろんそのまま入力できます。
// true
102 == 0x66;
// 「英数」キーを押す
se.keyCode(0x66);
出来ないこと
- 押したままにすること (押すことしか無理)
- 特殊キー以外のキーを同時押しすること
(する方法があれば教えてください。)
あと、.keystroke
ではタブ、スペースをそのまま入力できないようです。
(渡す文字列にスペースやタブが含まれていると、なぜか日本語入力がONになります)
(追記: タブは"\t"で入力できました。コメントありがとうございます!)
.keycode
で代用する場合はスペースキーは0x31
、タブキーは0x30
です。
文字列をキーコードの配列にする関数を作ると面白いかもですね。
CODES = {"a":0,"b":11,"c":8,"d":2,"e":14,"f":3,"g":5,"h":4,"i":34,"j":38,"k":40,"l":37,"m":46,"n":45,"o":31,"p":35,"q":12,"r":15,"s":1,"t":17,"u":32,"v":9,"w":13,"x":7,"y":16,"z":6,"0":29,"1":18,"2":19,"3":20,"4":21,"5":23,"6":22,"7":26,"8":28,"9":25,"-":27,"+":24,"[":33,"]":30,"=":42,";":41,"‘":39,",":43,".":47,"/":44," ":49,"\t":48,"\n":52};
function codes(str){
var codes = [];
for (var i = 0, code, char; char = str[i++];) {
code = CODES[char.toLowerCase()];
if (code != undefined) codes.push(code);
};
return codes;
};
Application("System Events").keyCode([34, 49, 17, 4, 31, 32, 5, 4, 17, 49, 13, 4, 0, 17, 49, 34, 2, 49, 2, 31, 49, 13, 0, 1, 43, 49, 34, 2, 49, 35, 15, 14, 17, 14, 45, 2, 49, 34, 49, 13, 0, 1, 49, 31, 45, 14, 49, 31, 3, 49, 17, 4, 31, 1, 14, 49, 2, 14, 0, 3, 27, 46, 32, 17, 14, 1, 47]);
マウスクリックをエミュレート
マウスのクリックにもSystem Events
を使います。
こちらはキーボードのエミュレートとは少し勝手が違います。
「あのウィンドウのあのボタンを押したい」
というように、Macの画面内の部品(以下GUI)を指定する必要があります。
この機能を使うためには、GUIにアクセスする許可を得なければいけません。
環境設定 > セキュリティとプライバシー > プライバシー > アクセシビリティ
を開き、スクリプトエディタかターミナル、使っているものに許可を与えてください。
できましたか?
System Events
のuiElementsEnabled()
で確認できます。
ui = Application("System Events").uiElementsEnabled();
// Application("AppleScript Utility").guiScriptingEnabled(); も同じ。
console.log(ui ? "せっていできてるよ。" : "せっていできてないよ。");
ちなみにシステム環境設定もJXAで操作できるので、以下のスクリプトを実行すれば上の画像にある設定画面が開きます。
(指定した画面を表示できるだけ。スクリプトで設定変更はできない。)
var pref = Application("System Preferences");
pref.panes.byId("com.apple.preference.security").anchors["Privacy_Accessibility"].reveal();
pref.activate();
とりあえずクリックしてみる
Appleメニューの「このMacについて」を押してみます。
var finder = Application("System Events").processes["Finder"];
var e = finder.menuBars[0].menuBarItems["Apple"].menus["Apple"].menuItems["この Mac について"];
e.click(); // 要素を選んで.click()を呼ぶだけ
ちゃんと「このMacについて」が表示されましたか?
あ、配列を返すメソッドは[]
に文字列を渡すとnameを指定して要素を取得できます。
(byname
が呼び出されます。)
(追記:このコードはメニューバーをクリックしています。Macのいつかのアップデートでメニューバーは隠すように設定できるようになりました。Macのメニューバーが隠れている場合には動きません。)
UIElement
まずはSystem Eventsからアプリケーションのプロセスを取得します。
プロセスからメニューやウィンドウを取得し、ウィンドウからボタンを取得し、という風に子要素を取得していくことになります。
プロセスも含め、それらは 全てUIElement
のサブクラス です。
(System Events自体は違います。)
UIElementの子要素は.uiElements
で取得できます。
または.menuBars
や.windows
というようなクラス名の複数形プロパティでも取得できます。
System Events
から参照できる全てのUI Elementsを一覧するスクリプトを書いたので使ってみてください。
https://github.com/zakuroishikuro/JXA-scripts/blob/master/ui-finder.js
(これ使ってだいたいどんな感じか分かったら、ここから下は読まなくていいかもしれません。長いし。校正したいけどめんどいので無理です。)
ApplicationProcess
System Events
からuiElementsを取得していきます。
var se = Application("System Events");
var elements = se.uiElements();
console.log("参照可能なプロセス数: " + elements.length);
var e = elements[0]; //最初の一つを取り出す
console.log("最初のプロセス: ");
console.log(" class: " + e.class());
console.log(" name: " + e.name());
参照可能なプロセス数: 49
最初のプロセス:
class: applicationProcess
name: loginwindow
配列の最初には、loginwindowという名前のApplicationProcess
が入っていました。
ApplicationProcess
はProcess
のサブクラスで、Process
はUIElement
のサブクラスです。
.uiElements
だけでなく、そのサブクラスを複数形にしたプロパティ名でも取得できます。
System Eventsの .uiElements
.processes
.applicationProcesses
は全て同じです。好きな呼び方で構いません。
var elements = se.uiElements();
var processes = se.processes();
var app_processes = se.applicationProcesses();
console.log(elements.length);
console.log(processes.length);
console.log(app_processes.length);
49
49
49
あと、紛らわしいですがApplication
クラスとは異なることに注意してください。
System Events
自体はUIElements
でなくApplication
です。UIElementのメソッドは呼べません。
すべてのプロセスの名前を表示してみましょう。
クラスは全てApplicationProcess
です。
processes = se.processes();
processes.forEach(function(p, i){ console.log(i + ": " + p.name()) });
0: loginwindow
1: SystemUIServer
2: Dock
3: Finder
(中略)
33: com.apple.WebKit.WebContent
34: Kobito
35: JapaneseIM
36: System Events
UI Elementsを辿ってみる
上のコードにある
finder.menuBars[0].menuBarItems["Apple"].menus["Apple"].menuItems["この Mac について"];
みたいなパスを調べるために、.uiElements
を一つずつ辿ってみましょう。
いちいち表示の処理を書くのはめんどくさいので、子UI Elementの名前とクラスを全て表示する関数を用意しました。
function wrap(e){
return e.class() + " - " + e.name() + " (" + e.description() + ")";
}
function print(target){
var childs = target.uiElements();
console.log("\n" + wrap(target) + " has " + childs.length + " UI Elements.");
childs.forEach(function(child, i){
console.log("- " + i + ": " + wrap(child))
});
console.log(); //改行
};
さっそく表示していきます。
print(finder);
Finder (applicationProcess) has 2 UI Elements.
- 0: menuBar - null (メニューバー)
- 1: scrollArea - null (デスクトップ)
メニューバーとデスクトップが見えました。
メニューバーを辿ります。
print( finder.menuBars[0] );
menuBar - null (メニューバー) has 8 UI Elements.
- 0: menuBarItem - Apple (メニューバー項目)
- 1: menuBarItem - Finder (メニューバー項目)
- 2: menuBarItem - ファイル (メニューバー項目)
- 3: menuBarItem - 編集 (メニューバー項目)
- 4: menuBarItem - 表示 (メニューバー項目)
- 5: menuBarItem - 移動 (メニューバー項目)
- 6: menuBarItem - ウインドウ (メニューバー項目)
- 7: menuBarItem - ヘルプ (メニューバー項目)
クラス名に合わせて.menuBars
としていますが、.uiElements
でも同じです
メニューから「Apple」を選びます。
print( finder.menuBars[0].menuBarItems["Apple"] );
menuBarItem - Apple (メニューバー項目) has 1 UI Elements.
- 0: menu - Apple (メニュー)
...メニューの中にメニューが複数あるかもしれませんしね。仕方ないね。
print( finder.menuBars[0].menuBarItems["Apple"].menus[0] );
menu - Apple (メニュー) has 19 UI Elements.
- 0: menuItem - この Mac について (メニュー項目)
- 1: menuItem - システム情報... (メニュー項目)
- 2: menuItem - null (メニュー項目)
- 3: menuItem - システム環境設定... (メニュー項目)
- 14: menuItem - システム終了... (メニュー項目) - 15: menuItem - システム終了 (メニュー項目) - 16: menuItem - null (メニュー項目) - 17: menuItem - (USER) をログアウト... (メニュー項目) - 18: menuItem - (USER) をログアウト (メニュー項目)
...やっと見えましたね。お疲れさまでした。
普通はこんなめんどくさいことしません。
どうなっているのかを知っておけば便利というだけです。
メニューやウィンドウを持たないプロセスもあります。
たとえば、Dockのプロセスは自身のアイコンの配列しか持っていません。
子UIを全て取得 (.entireContents)
子UIを一括で取得する方法があります。
UIElementから.entireContents
を呼ぶのです。
all = finder.entireContents();
all.length;
338
all.slice(0, 5);
[Application("System Events").applicationProcesses.byName("Finder").menuBars.at(0), Application("System Events").applicationProcesses.byName("Finder").menuBars.at(0).menuBarItems.byName("Apple"), Application("System Events").applicationProcesses.byName("Finder").menuBars.at(0).menuBarItems.byName("Apple").menus.byName("Apple"), Application("System Events").applicationProcesses.byName("Finder").menuBars.at(0).menuBarItems.byName("Apple").menus.byName("Apple").menuItems.byName("この Mac について"), Application("System Events").applicationProcesses.byName("Finder").menuBars.at(0).menuBarItems.byName("Apple").menus.byName("Apple").menuItems.byName("システム情報...")]
...見辛い。
多すぎて手に余りますし、扱いも困難です。
.entireContents
からは[]
で位置や名前で指定して取り出すことはできません。
.uiElements["なまえ"]
みたいな絞り込みができないのです。
(これに気づくまで相当苦労しました。ネットで情報を探しても全く見当たりませんし...)
.entireContents()
を呼ぶとUIElementの配列が取得できるので、そこから位置で指定して使うしかありません。
目的の項目はforEachとかで地道に探すしかないです。
UIElementはconsole.log()
しても[object ObjectSpecifier]
としか表示されません。
標準出力にはこのパスっぽいものが表示されていますが、これをJXA内で文字列として得る方法は(たぶん)ないです。
(追記2015/2/17:ありました。普通にAppleのドキュメントに書いてありました。
Automation.getDisplayString(hoge)
JXAは最後に評価した式の結果を必ず出力するので、それをパイプでファイルに保存すれば一応は文字列で得られます。
あとはRubyかなんかで,
で分割して絞り込むとか・・・。
$ osascript -l JavaScript -e 'Application("System Events").processes["Finder"].entireContents()' > contents.txt
X, Y座標を指定してクリック
finder_app = Application("Finder");
finder_app.activate();
delay(0.1); // activateになるまで少しかかるので、0.1秒待つ
finder = Application("System Events").processes[finder_app.name()];
x = 100;
y = 10;
finder.click( {at:[x, y]} );
Process
クラスのclick()
には座標を渡せます。
このコードを実行した場合、Finderの「Finder」メニューがクリックされます。(されるはずです。)
[x, y]
はグローバル座標です。
UIElement内の相対的な座標ではなく、画面内の絶対座標です。
CSSピクセルですから、Retinaディスプレイでもそうでなくても同じ位置を指します。
Retinaディスプレイの場合はピクセル数 / 2の数値を指定してください。
「2560 * 1600」ピクセルのRetinaディスプレイの場合、xは「0〜1279」で、yは「0〜799」です。
その座標の位置にそのプロセスが持つUIElementが存在すればクリックされます。
そして、そのUIElementの参照が戻り値として返ってきます。
UIElementが存在しなければ何も起きず、nullが返ってきます。
そのプロセスが持っていないUIElementはクリックできません。
Finderのプロセスにドックをクリックさせようとしてもデスクトップの参照が返ってくるだけです。
メニューをクリックしたいなら、最前面のアプリのプロセスを指定する必要があります。
いまいち用途が分かりません。
ちなみにUIElementは.position()
を呼べば座標を取得できます。
出来ないこと
- 右クリック
- ホイールクリック
- ドラッグ
- ドラッグ&ドロップ
(する方法あれば教えてください。)
UI Elementを探索するツール
そろそろ力尽きてきたのである場所だけ。
Accessibility Inspector
XCodeの中にあります。
そのままJXAで使える情報が表示されるわけではありませんが、ヒントにはなると思います。
UI Browser
けっこう人気らしい。
$55です。30日間無料。
蛇足
Apple Scriptは真面目に勉強する気がおきず、最近JXAを始めたにわかなので何か間違った記述をしていたら教えていただけるとありがたいです。
殴り書きの文章すいませんした。