AIRのドラッグ&ドロップ対応機能

今回は、AIRのドラッグ&ドロップ機能について解説する。

AIRのドラッグ&ドロップ機能は、ネイティブOSと完全に統合されており、非常に使い勝手の良いものだ。他のアプリケーションとの間でシームレスにデータの受け渡しを行うことが可能である。APIもコンパクトにまとまっており、覚えておいて損はない。

まず、AIRにおけるドラッグ&ドロップという動作の概要を理解しよう。

ドラッグ&ドロップとは、ドラッグ処理を開始する主体(イニシエータと呼ぶ)が処理を開始し、AIRアプリケーションの上をドラッグしながら通過し、最終的にどこかにドロップされる、という動作だと言える。

ここでは、「AIRアプリケーション内から外部(ネイティブOS環境)にデータをドラッグ&ドロップする」場合と、「外部からAIRアプリケーション内にデータをドラッグ&ドロップする」という場合に分けて考える。AIRアプリケーション内でドラッグを開始し、ドロップする場合は、これら二つの動作が組み合わさったものと考えればよい。

では、「AIRアプリケーション内から外部にデータをドラッグ&ドロップする」場合であるが、以下の図を見ていただきたい。

AIRアプリケーション内から外部にデータをドラッグ&ドロップする際のイメージ

この図から読み取れるのは、

  • flash.desktop.DragManagerクラスのstaticメソッドdoDrag()によりドラッグ処理が開始される
  • ドラッグされるデータは、flash.desktop.TransferableDataクラスのインスタンスにより表される

という点だ。

AIRのドラッグ&ドロップ処理は、この2つのクラスが中心となってAPIが構築されている。 flash.desktop.TransferableDataクラスは、ドラッグ中のデータを抽象化したクラスで、データ自体のほかにデータ形式の情報も保持している。

データの形式は非常に重要で、ドラッグ&ドロップを受け付けるか否かを判定するために使用されたり、外部アプリケーションとのデータをやり取りする際のプロトコルとしても利用される。

データ形式はflash.desktop.TransferableFormatsクラスに定数として定義されており、以下のようなものが利用できる。

フォーマット 説明 ActionScript型
BITMAP_FORMAT ビットマップ画像データ flash.display.BitmapData
FILE_LIST_FORMAT ファイル(複数) flash.filesystem.Fileの配列
TEXT_FORMAT 文字列 String
URL_FORMAT URL String

TransferableFormatsクラスのインスタンスからドラッグ中のデータを取得し、ActionScriptのオブジェクトとして取り扱うためには、データの形式に合わせて変換を行う必要がある。

その変換を自動で行うメソッドが、TransferableFormats.dataForFormat(フォーマット文字列)だ。以下のように使用する。

var transfer:TransferableData = ...
var draggedText:String = transfer.dataForFormat(TransferableFormats.TEXT_FORMAT) as String;

データの形式によって戻り値の型は異なる。形式とActionScriptにおける型の対応は、上の表を参照してほしい。

また重要なのが、ドラッグ&ドロップに伴って発生するイベントだ。 図中で緑色で表したイベント(NATIVE_DRAG_STARTやNATIVE_DRAG_COMPLETE)はドラッグ処理のイニシエータに、オレンジで表したイベントはドラッグの間に通過したコンポーネントに対して伝えられる。

各イベントの詳細な説明は後述する。

次に、「外部からAIRアプリケーション内にデータをドラッグ&ドロップする」場合だ。

外部からAIRアプリケーション内にデータをドラッグ&ドロップする際のイメージ

前の例と同じく、ドラッグされるデータは、flash.desktop.TransferableDataクラスのインスタンスにより表される。

注目すべきは、外部でドラッグが開始されたためイニシエータが存在せず、NATIVE_DRAG_STARTイベントやNATIVE_DRAG_COMPLETEイベントは発生しないということだ。

では、図中に登場したイベントについて簡単に説明する。

  • NATIVE_DRAG_START - AIRアプリケーション内からドラッグ処理が開始された際、イニシエータに対して通知される
  • NATIVE_DRAG_ENTER - コンポーネントの領域内に、ドラッグ状態でマウスカーソルが入ってきた際に呼び出される。AIRアプリケーション内でドラッグを開始すると、その直後にマウスカーソルの下にあるコンポーネントにこのイベントが通知される
  • NATIVE_DRAG_OVER - コンポーネントの領域内をドラッグしながらカーソルが通過すると断続的に発生する
  • NATIVE_DRAG_DROP - コンポーネント内にデータがドロップされた際、コンポーネントに対して通知される
  • NATIVE_DRAG_EXIT - ドラッグ状態でコンポーネント外にカーソルが移動した際、もしくはドラッグを途中でキャンセル(ESCキーを押すなどして)した際、コンポーネントに対してこのイベントが通知される
  • NATIVE_DRAG_COMPLETE - ドロップされたか、キャンセルされたかにかかわらず、ドラッグが終了した際、イニシエータに対して通知される

これで、AIRのドラッグ&ドロップ処理の概要は大体つかめたことと思う。では次に、サンプルアプリケーションを例に挙げて実際のコードを見ていこう。

サンプルアプリケーションによる解説

今回用意したサンプルアプリケーションは、中央のキャンバスにファイルをドラッグ&ドロップすることのできる単純なアプリケーションだ。ドラッグ&ドロップされると、その位置にファイルのアイコンと名前が張り付けられる。

画面中央のキャンバスにファイルをドラッグ&ドロップすると、ファイルのアイコンと名前が張り付けられる(左がドロップ前、右がドロップ後)。

また、貼りついたアイコンを外部にドラッグ&ドロップすると、ファイルがコピーされる。

ドラッグ&ドロップによるファイルのコピー

いまいち実用性には乏しいものの、ドラッグ&ドロップの基礎を学ぶには十分なサンプルだ。以下がそのサンプルコードで、そこそこ量があるように思えるが、順を追ってみていけば難しいことは何もないことがわかるだろう。

ポイントだけ押さえたい方は、リストの後の解説を参考にしていただきたい。

AIRDragDropExample.mxml

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" applicationComplete="init()">
    <mx:Script>
        <![CDATA[
            import mx.controls.Label;
            import mx.core.UIComponent;
            import flash.desktop.*;
            import flash.display.Bitmap;
            import flash.filesystem.File;

            // ルート要素「WindowedApplication」のapplicationComplete属性に指定された初期化メソッド
            private function init():void {
                // (1) キャンバスにドラッグ&ドロップ関連のイベントリスナを追加
                canvas.addEventListener(NativeDragEvent.NATIVE_DRAG_ENTER, onCanvasDragEnter);
                canvas.addEventListener(NativeDragEvent.NATIVE_DRAG_DROP, onCanvasDragDrop);
            }
            // キャンバスにドラッグされた際呼び出されるリスナ
            private function onCanvasDragEnter(event:NativeDragEvent):void {
                var data:TransferableData = event.transferable;
                // (2) ドラッグされたデータがファイルであれば、ドラッグ&ドロップを受け付ける
                if (data.hasFormat(TransferableFormats.FILE_LIST_FORMAT)) {
                    DragManager.acceptDragDrop(canvas);
                }
            }
            // キャンバスにドロップされた際呼び出されるリスナ
            private function onCanvasDragDrop(event:NativeDragEvent):void {
                // (3) キャンバス内のドラッグ&ドロップの場合、無視する
                if (DragManager.dragInitiator != null) {
                    return;
                }
                // (4) 複数ファイルがドロップされたという前提でデータを取得
                var files:Array = event.transferable.dataForFormat(
                        TransferableFormats.FILE_LIST_FORMAT) as Array;
                // ドロップされたファイルをループしながら
                for each (var file:File in files) {
                    // (5) アイコンとラベルの貼り付け
                    var icon:Icon = file.icon;
                    for each (var bitmapData:BitmapData in icon.bitmaps) {
                        // 32x32のアイコンのみ対象とする
                        if (bitmapData.height == 32) {
                            // アイコン画像をコンポーネントとしてキャンバスに追加
                            var iconImage:UIComponent = new UIComponent();
                            iconImage.addChild(new Bitmap(bitmapData));
                            iconImage.x = event.localX; // マウスの現在座標に置く
                            iconImage.y = event.localY;
                            // ファイルのパスをコンポーネントの名前にしておく
                            iconImage.name = file.nativePath;
                            canvas.addChild(iconImage);

                            // アイコンのラベルをキャンバスに追加
                            var iconLabel:Label = new Label();
                            iconLabel.text = file.name;
                            iconLabel.x = iconImage.x + 32;
                            iconLabel.y = iconImage.y + 8;
                            canvas.addChild(iconLabel);

                            // アイコンをクリックされた際の処理
                            iconImage.addEventListener(MouseEvent.MOUSE_DOWN, onIconMouseDown);
                        }
                    }
                }
            }
            // アイコン上でマウスクリックされた際の処理
            private function onIconMouseDown(event:MouseEvent):void {
                var mouseTarget:UIComponent = event.target as UIComponent;
                // コンポーネント名をファイルのパスとし、Fileオブジェクト作成
                var filePath:String = mouseTarget.name;
                var file:File = new File(filePath);
                // クリックされたコンポーネントの子要素がビットマップデータ
                var iconBitmap:Bitmap = mouseTarget.getChildAt(0) as Bitmap;

                // (6) ドラッグ&ドロップするデータを作成
                var transfer:TransferableData = new TransferableData();
                transfer.addData(iconBitmap.bitmapData, TransferableFormats.BITMAP_FORMAT, true);
                transfer.addData([file], TransferableFormats.FILE_LIST_FORMAT, true);

                // (7) ドラッグを開始
                DragManager.doDrag(canvas, transfer, iconBitmap.bitmapData, new Point(20, 20), null);
            }
        ]]>
    </mx:Script>
    <mx:Label x="10" y="10" text="下のキャンバスにはファイルをドラッグ&ドロップできます。"/>
    <mx:Canvas id="canvas" y="36" width="100%" height="302" backgroundColor="#FCFAFA"/> 
</mx:WindowedApplication>

以下、ポイントを解説する。

(1) アプリケーションが起動した直後に行っているのは、ドラッグ&ドロップを受け付けるキャンバスに対して、ドラッグ関連のイベントリスナを登録する処理だ。キャンバスにドラッグ状態のマウスカーソルが入ってきた時にonCanvasDragEnterメソッドを、ドロップが行われた際にonCanvasDragDropメソッドが呼び出されるようにしている。

// (1) キャンバスにドラッグ&ドロップ関連のイベントリスナを追加
canvas.addEventListener(NativeDragEvent.NATIVE_DRAG_ENTER, onCanvasDragEnter);
canvas.addEventListener(NativeDragEvent.NATIVE_DRAG_DROP, onCanvasDragDrop);

(2) ドラッグ状態のマウスカーソルがキャンバスの上に差し掛かった際に行うことは、ドラッグ&ドロップを受け付けるかどうかをAIRランタイムに教えることだ。 イベントリスナに渡されるNativeDragEventは、ドラッグされているデータへの参照を保持しており、プロパティtransferableを参照すればアクセスすることができる。今回キャンバスがドラッグ&ドロップを受け付けるのはファイルのみなので、TransferableData.hasFormat(フォーマット文字列)を使用して、ドラッグされたデータの形式がファイルタイプを含むかどうかチェックしている。

// キャンバスにドラッグされた際呼び出されるリスナ
private function onCanvasDragEnter(event:NativeDragEvent):void {
    var data:TransferableData = event.transferable;
    // (2) ドラッグされたデータがファイルであれば、ドラッグ&ドロップを受け付ける
    if (data.hasFormat(TransferableFormats.FILE_LIST_FORMAT)) {
        DragManager.acceptDragDrop(canvas);
    }
}

ドラッグを受け付ける場合は、DragManager.acceptDragDrop(*ドラッグを受け付けるコンポーネント*)を呼び出せば良い。そうすると、マウスカーソルがドラッグ可能を表すものに変化する。

(3) キャンバスにドラッグ&ドロップされた際呼び出されるリスナでは、まず同じキャンバス内からドラッグ&ドロップされたものなのか、それとも外部からドラッグ&ドロップされたものなのかをチェックしている。AIRアプリケーション外からのドラッグ&ドロップであれば、イニシエータが存在しないため、以下のようなチェックでその判定が行えるというわけだ。

// (3) キャンバス内のドラッグ&ドロップの場合、無視する
if (DragManager.dragInitiator != null) {
    return;
}

(4) ドラッグされたデータからActionScriptオブジェクトへの変換を行っている。前述したとおり、TransferableData.dataForFormat(データ形式)を呼び出すだけだ。これにより、ドラッグされたファイルをflash.filesystem.Fileオブジェクトの配列として取得している。

// (4) 複数ファイルがドロップされたという前提でデータを取得
var files:Array = event.transferable.dataForFormat(
        TransferableFormats.FILE_LIST_FORMAT) as Array;

(5) アイコンとラベルをキャンバスに貼り付けている部分の処理は、ドラッグ&ドロップと直接関係がないので詳細な説明は行わない。以下のようなコードで、ファイルに関連付けられているアイコンを取得することができる。こうした、ファイル関連のAPIをご存じなければ、こちらの記事を参照していただきたい。

// (5) アイコンとラベルの貼り付け
var icon:Icon = file.icon;
...

(6) (5)までは、キャンバスにドラッグ&ドロップ「された」時の処理であったが、ここからはキャンバスからファイルアイコンをドラッグ&ドロップ「する」処理の説明となる。 ファイルアイコン上でマウスのボタンを押されたら、まずはドラッグするデータとなるTransferableDataクラスのインスタンスを作成し、データ形式に合わせてデータを追加する必要がある。 TransferableData.addData(データデータ形式シリアライズするかどうか)メソッドを用いて、ファイルを追加しているのみならず、ここではアイコン画像自体もビットマップ形式で追加している。

// (6) ドラッグ&ドロップするデータを作成
var transfer:TransferableData = new TransferableData();
transfer.addData(iconBitmap.bitmapData, TransferableFormats.BITMAP_FORMAT, true);
transfer.addData([file], TransferableFormats.FILE_LIST_FORMAT, true);

(7) 最後に、DragManager.doDrag()メソッドを利用してドラッグを行う。doDrag()メソッドの引数を順番に説明すると、以下のようになる。

  • イニシエータ - 任意のコンポーネントを指定できる。ここではキャンバスを指定している
  • ドラッグするデータ - TransferableDataオブジェクトを指定する
  • ドラッグ中のアイコン - ドラッグ中のアイコンをビットマップ形式で指定する。省略可能。ここでは、ファイルアイコンを指定した
  • アイコンとマウスカーソルの位置関係 - 省略可能
  • ドラッグを許容するアクション - 省略可能。AIRでは、ドラッグ動作を"copy"(コピー)、"move"(移動)、"link"(リンクの作成)に大別し、「アクション」と呼んでいる。詳しくはDragManagerクラスのリファレンスを参照してほしい
// (7) ドラッグを開始
DragManager.doDrag(canvas, transfer, iconBitmap.bitmapData, new Point(20, 20), null);

今回は、AIRが持つドラッグ&ドロップ機能の基本を説明した。次回は、ドラッグ&ドロップに類似した処理としてコピー&ペーストについて説明する。