【Xamarin.Mac】NSAppleEventManagerを使って、アプリケーションのファイルオープンで処理を実行する

【Xamarin.Mac】NSAppleEventManagerを使って、アプリケーションのファイルオープンで処理を実行する

Macのアプリケーションでファイルを受け取る方法

Macでは、Dockのアイコンにファイルをドラッグアンドドロップしたり、コマンドライン引数のような形でファイルを渡したりしてアプリケーションにファイルを渡す方法がサポートされています。

ファイルを受け取る側での対応方法は複数あり

  • NSApplicationDelegateのopenFilesメソッドを呼ぶ
  • NSAppleEventMangerのSetEventHandlerに、ファイルを処理するメソッドを登録する

というものがあります。

このうちopenFilesメソッドの方はMac OS(11以降?)のアップデートにより、読み込んだファイルが重複して渡されるという動作になってしまっています。そこでこの記事ではNSAppleEventManagerのSetEventHandlerを使ってファイルのオープンを処理します。

コード例

NSAppleEventManagerを使ってファイルオープンを処理する場合、SetEventHandlerメソッドを使って、ファイルが渡された時に実行するメソッドを指定します。以下の例では、ファイルパスを受け取って表示するメソッドをAppDelegateに定義し、そのメソッドをDidFinishLaunchingメソッド内でファイルオープン時のイベントハンドラとして登録しています。

AppDelegate.cs

// ファイルオープン時の処理内容の登録をアプリ起動完了時に実行する
public override void DidFinishLaunching(NSNotification notification)
{
    // (1) ファイルオープン時の処理をイベントハンドラとして登録
    NSAppleEventManager.SharedAppleEventManager // NSAppleEventManagerインスタンスの取得
        .SetEventHandler(
            this,                                   // メソッドを持っているインスタンス
            new Selector("OnOpenFile:replyEvent:"), // 実行するメソッドの指定
            AEEventClass.AppleEvent,                // どのイベントに対するハンドラなのかを指定
            AEEvent.OpenDocuments);                 // AppleEventのうち、アプリがドキュメントを開く際の処理であることを指定
}

// 実際にファイルをオープンする処理
[Export("OnOpenFile:replyEvent:")]
public void OnOpenFiles(NSAppleEventDescriptor descriptor, NSAppleEventDescriptor replyEventDescriptor) 
{
    // (2) パラメータを指定する4 character code
    const string keyDirectObject = "----";

    // (3) four-character codeをunsigned int型に変換する
    var uIntKeyword = 
        (uint) keyDirectObject [0] << 24 |
        (uint) keyDirectObject [1] << 16 |
        (uint) keyDirectObject [2] << 8 |
        (uint) keyDirectObject [3];

    // (4) AppleEventDescriptorに格納されたパラメータのディスクリプタを取得する
    var param = descriptor.ParamDescriptorForKeyword(uIntKeyword);

    // (5) 渡されたファイルの数だけ繰り返してコンソールに出力する
    var len = param.NumberOfItems;
    foreach(var i in Enumerable.Range(1, (int)len)
    {
        // (6) サンプルなのでファイルパスをコンソールに流すだけ
        Console.WriteLine(param.DescriptorAtIndex(i).StringValue)
    }
}

コード解説

(1) ファイルオープン時の処理をイベントハンドラとして登録

OpenDocuments Appleイベントが発生した場合に実行する処理をセレクタで指定します。セレクタはコンパイル後のプログラム内で通用する、メソッドを特定するためのIDのようなものです。objective-Cでは@selector(myFunction)、Swiftでは#selector(myFunction)のような記法ですが、Xamarin.macのC#ではnew演算子でインスタンスを生成する形となります。

なお、セレクタの文字列は引数を取らない関数なら”myFunction”、引数一つなら”myFunction:”、引数二つなら”myFunction:secondArg:”という風に、引数がある場合は末尾がコロンで終わるものになります。

(2) パラメータを指定するfour-character code

Apple Eventのすべての情報はAppleEventDescriptorの中に格納されており、必要なパラメータは4文字のコード(four-character code)を指定することにより取得します。OpenDocumentsイベントの場合、渡されたファイルパスが”—-“というキーワードのパラメータに格納されているようです。

four-character codeはAppleScriptで使用されるAppleによって定義されたコードです。

(3) four-character codeをunsigned int型に変換する

パラメータを取得するために指定するfour-character codeはuint(符号なし整数)型でParamDescriptorForKeyword()メソッドに渡す必要があります。

(4) AppleEventDescriptorに格納されたパラメータのディスクリプタを取得する

uint型に変換したfour-character codeを渡すことで、ファイルのURLを格納したパラメータのディスクリプタを取得します。ディスクリプタには他のディスクリプタやディスクリプタのリストを格納できるようになっており、取得したパラメータもまたディスクリプタのインスタンスとなっています。

(5) 渡されたファイルの数だけ繰り返してコンソールに出力する

パラメータに格納している要素数を取得し、要素数に応じた回数繰り返し出力しています。

実際には渡されるファイルが1つだけの場合もあり、その場合はparam.StringValueでファイルパスを取得することになるため、実際のアプリケーションでは条件分岐での対応が必要です。

(6) サンプルなのでファイルパスをコンソールに流すだけ

実際のアプリではファイルの内容をバッファに読み込むなどの処理をすることになると思いますが、サンプルなのでファイルパスを出力するだけです。

ファイルパスはfile://というURLスキーム付き、URLエンコードされた形で渡されるので読み込む際はスキーム部分を削除したりURLデコードなどが必要になります。

参考資料

Apple Event Discriptorやfour-character codeなどについては以下を参照してください。

A wrapper for the Apple event descriptor data type.
developer.apple.com

環境

本記事は以下の環境を前提としています。

macOS Monterey 12.3
Xamarin.Mac .NET Framework 4.8 (C#)