Macのキーボード入力、マウスクリックをJavaScriptで (JXA)

  • 126
    いいね
  • 4
    コメント
この記事は最終更新日から1年以上が経過しています。

JXAを知らない方はとりあえず以下の記事をどうぞ。

ここではキーボードやマウスの操作をエミュレートする方法を書きます。
各アプリに用意された機能はスクリプトエディタから見られる「用語説明」等に任せます。

ちょっとしたコードを試すにはosascriptコマンドのインタラクティブモードが便利です。

$ osascript -l JavaScript -i

で起動できます。

System Events

System Eventsはスクリプト専用の、GUIを持たないアプリケーションです。

SystemEventsを取得
var se = Application("System Events"); //参照を取得

これを使ってユーザの操作をエミュレートできます。
以下より、変数seにはApplication("System Events")を参照しているものとします。

キーボードの操作をエミュレート

キーボードを操作するには.keystroke().keyCode()を使います。

.keystroke()

渡された文字列のキーを打鍵します。

「A」キーを押す
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進数の数値も扱えるので、もちろんそのまま入力できます。

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にアクセスする許可を得なければいけません。
環境設定 > セキュリティとプライバシー > プライバシー > アクセシビリティ
を開き、スクリプトエディタかターミナル、使っているものに許可を与えてください。

スクリーンショット 2015-02-10 17.09.16.png

できましたか?
System EventsuiElementsEnabled()で確認できます。

GUIにアクセス可能か確認
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について」を押してみます。

「この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

スクリーンショット 2015-02-11 20.42.06.png

(これ使ってだいたいどんな感じか分かったら、ここから下は読まなくていいかもしれません。長いし。校正したいけどめんどいので無理です。)

ApplicationProcess

System EventsからuiElementsを取得していきます。

ApplicationProcessを取得
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が入っていました。

ApplicationProcessProcessのサブクラスで、ProcessUIElementのサブクラスです。
.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を辿ってみる

上のコードにある

「このMacについて」のパス
finder.menuBars[0].menuBarItems["Apple"].menus["Apple"].menuItems["この Mac について"];

みたいなパスを調べるために、.uiElementsを一つずつ辿ってみましょう。

いちいち表示の処理を書くのはめんどくさいので、子UI Elementの名前とクラスを全て表示する関数を用意しました。

子UIの情報を表示する(class,name,descriptionの順)
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(); //改行
};

さっそく表示していきます。

Finderのプロセス直下のuiElements
print(finder);

Finder (applicationProcess) has 2 UI Elements.
- 0: menuBar - null (メニューバー)
- 1: scrollArea - null (デスクトップ)

メニューバーとデスクトップが見えました。
メニューバーを辿ります。

menuBars
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」を選びます。

menuBarItems
print( finder.menuBars[0].menuBarItems["Apple"] );

menuBarItem - Apple (メニューバー項目) has 1 UI Elements.
- 0: menu - Apple (メニュー)

...メニューの中にメニューが複数あるかもしれませんしね。仕方ないね。

menus
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を呼ぶのです。

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で使える情報が表示されるわけではありませんが、ヒントにはなると思います。

スクリーンショット 2015-02-10 19.38.43.png

UI Browser

けっこう人気らしい。
$55です。30日間無料。

UI Browser

蛇足

Apple Scriptは真面目に勉強する気がおきず、最近JXAを始めたにわかなので何か間違った記述をしていたら教えていただけるとありがたいです。

殴り書きの文章すいませんした。

参考リンク