Making Application using ASOC

AppleScriptObjC の覚書で色々と気がついた所を書きましたが、それらは実際に触ってみないと分からないことだったりします。Hello World を表示するアプリケーションを作りながら、色々と確認してみます。

Xcode や Interface Builder の使い方は、ここでは取り扱いません。それらは既に優秀な解説があります。知識としては、Cocoa はじめの一歩から手に入る Become An Xcoder ぐらいの知識が必要です(後は練習問題 12 ぐらいまでができれば大丈夫です)。訳者様に感謝です。

HelloWorld アプリケーションの UI は以下のようになります。

ASOCHelloWorld_UI.png

では、多くを端折っていきます。Xcode で Cocoa-AppleScript Application の新規プロジェクトを作成したら、アプリケーションのコントローラーを追加します。「ファイル」メニューの「新規ファイル...」から AppleScript class を選択します。NSObject のサブクラスにしておきます。ファイル名は AppController としておきます。

ASOCHelloWorld_AddFile.png

AppController.applescript を開き、アウトレットとアクションを追加します。

script AppController
    property parent : class "NSObject"

    -- Outlets
    property _window : missing value
    property textField : missing value
    property helloButton : missing value

    -- Actions
    on greeting_(sender)
        textField's setStringValue_("Hello, World")
    end greeting_
end script

アウトレットは property で宣言し、値は missing value にしておきます。アクションは引数を一つ取るハンドラとして定義します。引数を取るのでアンダースコアが必要です。Objective-C で書くなら次のようになります。

IBOutlet NSWindow *window;
IBOutlet NSTextField *textField;
IBOutlet NSButton *helloButton;

- (IBAction)greeting:(sender);

これらは通常ヘッダファイル(.h)の方に記述しますが、ASOC では、一つのスクリプトオブジェクトがヘッダファイル(.h)と実装ファイル(.m)を兼ねています。だから、上記のように書けてしまいます。あと、型の指定は当然できませんし、IBOutlet や IBAction などのキーワードも ASOC では利用できません。

なら、Interface Builder はどうやってそれらを識別するのかというと、識別されません。Interface Builder 上では全てが表示されます。ハンドラやプロパティが増えると、いささか鬱陶しいことになります。

ということで、MainMenu.xib を開いて Interface Builder で UI を作成し、アウトレットとアクションを接続します。

まず、MainMenu.xib のウィンドウに Library パレットから Object を追加します。

ASOCHelloWorld_AddObject.png

この Object を選択したまま、Inspector パレットで Class を AppController にします。

ASOCHelloWorld_ClassInspector.png

これで Interface Builder で AppController で定義したアウトレットとアクションが接続できるようになります。

ASOCHelloWorld_TargetAndAction.png

このような感じになります。保存したら Interface Builder での作業は終わりです。Xcode に戻って「ビルドと実行」を行います。アプリケーションが起動したら、ボタンを押してみてテキストフィールドに「Hello, World」と表示されるのを確認してください。

ASOCHelloWorld_UI.png

以上の作業は、AppleScript Studio での作業とは全く異なります。というより、Cocoa/Objective-C でアプリケーションを作るのとほとんど同じです。その気になれば、バインディングも使えます。

では、このアプリケーションを叩き台に色々と調べてみます。まず、スクリプトオブジェクトの外にハンドラを定義します。

on greeting()
    display dialog "Hello, World"
end greeting

greeting_ からこのハンドラを呼び出すように変更します。

on greeting_(sender)
    -- textField's setStringValue_("Hello, World")
    greeting()
end greeting_

実行してもエラーになると思います。これはどのようにしても無理なようで、スクリプトオブジェクト外にあるハンドラは呼び出すことができません。では、どうするかというとスクリプトオブジェクトにしてしまいます。

script HelloWorldDialog
    on greeting()
        display dialog "Hello, World"
    end greeting
end script

このようにしておけば、通常のようにスクリプトオブジェクトのハンドラを利用できます。

on greeting_(sender)
    -- textField's setStringValue_("Hello, World")
    HelloWorldDialog's greeting()
end greeting_

つまり、これはクラスの定義ではありません。スクリプトオブジェクトの定義です。ゆえに、このスクリプトオブジェクト内ではメソッドを作成するように引数の数だけアンダースコアは必要ありません。

しかし、下手にこのスクリプトオブジェクトの親を指定してしまうと、クラスとして扱われるようになってしまいます。

script HelloWorldDialog
    -- NSObject などを継承するとクラスになる
    property parent : class "NSObject"

    on greeting()
        display dialog "Hello, World"
    end greeting

    on greeting_(yourName) -- 引数があるのでアンダースコアが必要になる
        display dialog "Hello, " & yourName
    end greeting_
end script

スクリプトオブジェクトをそのまま利用するか、クラスとして利用するか。どちらも一長一短です。クラスとして利用するなら、別のファイルにしていても property で指定するだけでそのクラスのメソッド(ハンドラ)が利用できます。が、メソッドの命名規則は Objective-C に準じます。

他方、スクリプトオブジェクトとして利用すると、Objective-C の命名規則から逃れられますが、別ファイルにしたときに読み込みが面倒になります。awakeFromNib などを利用して読み込みますが、path to me は利用できないので、NSBundle を利用します。

on awakeFromNib()
    -- load script object.
    tell NSBundle's mainBundle()
        set theFile to pathForResource_ofType_("HelloWorldObject", "scpt")
    end tell
    set HelloWorldObject to (load script (theFile as text) as POSIX file)
end awakeFromNib

AppleScript の me は ASOC ではクラス自身を表すので、path to me は利用できません。また、上記では pathForResourceofType が返した NSString を AppleScript の text に型変換しています。AppleScript では NSString をそのまま利用できないようです。

Cocoa/Objective-C では色々と便利な関数が用意されています。それらは、ASOC では利用できません。例えば、矩形を表す NSRect を作成する NSMakeRect といった関数は使えません。これらはどのように作るのかというと、AppleScript の record クラスで代用します。

set rect to {origin:{x:10, y:10}, |size|:{|width|:100, height:100}}

このような record を作り、Objective-C のメソッドに渡せばいいようです。NSRange、NSSize、NSPoint なども同様です。NSLog は AppleScript の log 命令で代用できます。

アプリケーションを作成している時、特定のクラスを利用したい時があります。先ほどの NSBundle のように。Cocoa/Objective-C なら特に意識もしないで NSBundle のクラスメソッドを利用できたりするのですが、ASOC の場合、利用したいクラスを property で指定しておく必要があります。具体的には次のようになります。

property NSBundle : current application's class "NSBundle"

この property をどこで宣言しておくかというと、クラス(スクリプトオブジェクト)の外か、クラスの property として宣言します。

-- スクリプトのトップで宣言
property NSBundle : current application's class "NSBundle"

script AppController
    property parent : class "NSObject"
    -- クラスで宣言
    -- property NSBundle : current application's class "NSBundle"
end AppController

これで NSBundle を利用できるようになります。Objective-C の import のようなものかと思うけど、そうではないようで、一気にクラスを参照したいがために AppKit.h や Cocoa.h を指定してもだめなようです。

property AppKit : class "AppKit" -- 利用できない

このようにクラスを property で参照しておくと、クラスが持っている定数等が利用できるようになります。

-- NSBundle's notification key
-- log NSBundle's NSBundleDidLoadNotification
log current application's NSBundleDidLoadNotification

その際、current application を通して定数を指定するようにします。コメントアウトしている方は利用できません。NSWindow の初期化処理を参考に。

script CustomWindow
    property parent : class "NSWindow"

    on initWithContentRect_styleMask_backing_defer_(contentRect, aStyle, bufType, frag)
        continue initWithContentRect_styleMask_backing_defer_(contentRect, ¬
            current application's NSTexturedBackgroundWindowMask, ¬
            current application's NSBackingStoreBuffered, ¬
            false)

        return me
    end initWithContentRect_styleMask_backing_defer_
end CustomWindow

横長になるのは AppleScript の宿命です。このように利用します。初期化処理ではスーパークラスの初期化処理を最初に呼び出しますが、ASOC では continue を使ってスーパークラスの初期化を行います。また、初期化の最後に Objective-C では self を返しますが、ASOC では me を返します。

Cocoa/Objective-C のメソッドの中には AppleScript の予約語になっているものがあります。例えば、size や width もそうですし、set や center といった語は AppleScript で定義されています。NSWindow にはウィンドウをディスプレイの真ん中に移動させる center というメソッドがありますが、これを利用するには | でメソッドを囲むようにします。

_window's |center|()

ちなみに上記は以下のようにも記述できます。

tell _window to |center|()
|center|() of _window

どの書き方でも構いません。Objective-C は以下のようにメソッドを連続して記述することがあります。

NSString *string = [[[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding] autorelease];

ASOC で同じように記述するなら、s を使った以下の書き方が分かりやすいかもしれません。

set str to NSString's alloc()'s initWithData_encoding_(theData,current application's NSASCIIStringEncoding)'s autorelease()

横長なのは Objective-C でも AppleScript でも同じですね。今回は Objective-C の視点から ASOC を見てみました。ASOC のいいところは、AppleScript で Objective-C のような構文を記述しながら、既存の AppleScript を混在できるところにあります。

on awkeFromNib()
    tell application "Finder"
        set loc to insertion location as text
        textField's setStringValue_(loc)
    end tell
end awakeFromNib

Cocoa/Objective-C ほど面倒ではなく、AppleScript Studio ほどイライラせずにすみます。PyObjC のプロジェクトテンプレートが Xcode 3 からなくなったのには驚きましたが(古い Xcode から移植しましたら使えました)。ASOC がいつまでサポートしてくれるのでしょう。AppleScript Studio や PyObjC に比べると、サポートの手間は少ないと思うのですが。

AppleScriptObjC 覚書

いまさら感がありありなのですが、AppleScriptObjC。通称、ASOC。Mac OS X 10.6 Snow Leopard とともに紹介されたので、既に登場から 1 年以上経っているわけですが。そろそろ、資料も揃ったことだろうとネット上を検索してみるが...これがなんとも少ない。

困ったものです。

日本で AppleScriptObjC をキャッチアップしているのは唯一ぴよまるソフトウェアさんぐらいしか見当たりません。

...困ったものです。とりあえず、Mac OS X Automation で公開されているチュートリアル動画を参考にいくつかアプリケーションを作ってみました。

アプリケーションを作っているときに気がついたことを箇条書きで書いていきます。

  • ASOC でアプリケーションを作るのは、Cocoa/Objective-C でアプリケーションを作るのと同じ。
  • AppleScript Studio とは異なる。
  • AppleScript の知識より Cocoa/Objective-C の知識が必要。
  • ASOC は、Cocoa/Objective-C でアプリケーションを作成したことがあるなら、すぐに理解できる(と思う)。
  • ASS アプリケーションを ASOC にそのまま移植するのは難しい。
  • 逆に Cocoa/Objective-C アプリケーションを ASOC に移植するのが簡単かというと、諸般の事情により一筋縄にはいかない。
  • ASOC は Cocoa の機能を全て利用できる...といったことを言われるが、そんなことはない。現状では利用できないものも多々ある。例えば、モーダルダイアログは利用できるが、シートは利用できない。

次にプログラムを書いているときに気がついたことなど。

  • ASOC のプロジェクトには登録されてるファイルはアプリケーションのデリゲートとして登録されている。
  • ASOC のクラスはヘッダファイル(.h)と実装ファイル(.m)がない。
  • ASOC のクラスは一つのスクリプトファイル(というより、スクリプトオブジェクト)で定義する。
  • スクリプトオブジェクト = クラスなので、一つのスクリプトファイルで複数のクラスを定義できる。
  • 継承は AppleScript の parent 属性を利用する。
  • テンプレートでは parent に NSObject が指定されているが、実はなくてもいい。スーパークラスに NSObject が密かに指定されている...みたい(継承をたどっていくと BAGenericObject に突き当たる)。
  • 親の指定がされていない AppleScript のスクリプトオブジェクトを継承元に指定することができる。
  • 継承元が指定されていない AppleScript のスクリプトオブジェクトをクラスとして扱うことができる。
  • 継承元が指定されていない AppleScript のスクリプトオブジェクトをクラスとして扱えば、クラスとして振る舞う。AppleScript のスクリプトオブジェクトとして扱えば、スクリプトオブジェクトとして振る舞う。
  • クラスで利用する変数(クラス変数やインスタンス変数)は AppleScript の property で定義する。
  • クラスのメソッドは AppleScript のハンドラで定義する。
  • Objective-C のメソッドはコロンで引数を表すが、ASOC ではアンダースコアで表す。drawRect: は drawRect_ になる。
  • このため ASOC 内でメソッドを定義した時、引数の数だけアンダースコアが必要になる。2 つの引数を取る append(x, y) があったとする。これは、ASOC のクラス内では appendWithX_andY_(x,y) といった形になる。Objective-C でメソッドを定義するのと同じだ。
  • メソッドの返り値や引数に型の指定はない。
  • Objective-C に用意されているメソッドの中には AppleScript で予約されているものがある。set や center 等。それらのメソッドを利用するには | でメソッドを囲む。
  • クラス内で他のクラスを利用するには、そのクラスを property を利用して参照する。
  • クラス(スクリプトオブジェクト)定義の外でハンドラを定義しても利用できない。Objective-C の関数のようなことはできない。
  • スーパークラスのメソッドは、continue を使って呼び出す。
  • NSRect や NSSize、NSPoint 等は AppleScript のレコードで定義できる。
  • 引数に NSArrayを取る Objective-C のメソッドには AppleScript の list クラスを渡すことができる。
  • 引数に NSDictionary を取る Objective-C のメソッドには AppleScript の record クラスを渡すことができる。
  • NSString と AppleScript の text は同じではない。引数に NSString を取る Objective-C のメソッドには AppleScript の text クラスを渡すことができる。が、NSString を AppleScript で利用するには型変換が必要(な時がある)。
  • Objective-C のメソッドは ASOC で利用できるが、簡易に利用するために用意されている関数群(NSMakeRect() や NSLog())等は、ASOC では利用できない。
  • NSTimer や通知などに利用するセレクタは文字列で指定する。その際、引数はコロンで表す。アンダースコアではない。
  • Objective-C の nil は AppleScript の missing value で代用。
  • path to me は利用できない。ASOC では me は自身のクラスを表す。
  • ASOC は既存の AppleScript の言語仕様を拡張しているわけではない。利用できるのは AppleScript 言語のみ。これが、いくつかの面倒を引き起こす。例えば、論理積や論理和を取りたいとき。C 言語の & や | は利用できない。
  • ASOC で Objective-C のカテゴリやプロトコルを定義することはできない。

と、思いつくままに羅列。読んだだけでは意味が分からないものもあると思いますが、細かいことはまた次回。