Adobe AIRで作るデスクトップアプリケーション

第11回コピー&ペーストとドラッグ&ドロップ

基本はClipboardクラス

デスクトップアプリケーションの多くは、コピー&ペーストやドラッグ&ドロップによるデータの受け渡しに対応しています。もちろんAdobe AIRにもそういったインターフェースを実装するためAPIが用意されていますので、今回はそれついて解説したいと思います。

コピー&ペーストにしてもドラッグ&ドロップにしても、受け渡しを行うデータを一時的に格納しておく場所が必要です。AIR APIでその役割を担うのがClipboardクラス(flash.desktopパッケージ)です。Clipboardオブジェクトには以下の形式のデータを格納できます。

形式ActionScriptのデータ型
ビットマップBitmapData
ファイルリストFileオブジェクトのArray
テキストString
URLString
シリアライズされたオブジェクト
オブジェクトの参照

ビットマップ/ファイルリスト/テキスト/URLの4つ(以下、標準フォーマット)は、AIRアプリケーションとその他のアプリケーション間でやりとりが可能です。ActionScriptのデータ型とOSのクリップボード形式は自動で相互変換されます。シリアライズされたオブジェクトはAIRアプリケーション間でのみ有効で、例えばActionScriptの基本オブジェクトをやりとりしたい場合などに使います。シリアライズ/デシリアライズはAIR側で自動的に行われますが、正しく処理できないデータは無効になります。オブジェクトの参照は、同一AIRアプリケーション内でのみ利用できます。

Clipboardオブジェクトには、複数の形式のデータを一度に設定することができます。これは別のアプリケーションから少しでもデータを利用しやすくするためです。例えばビットマップを渡したい場合でも、それを受け取ったアプリケーションが直接ビットマップを扱えるとは限りません。そういった場合に備えて、代替テキストやビットマップの元になったファイル等を一緒に渡しておくことができるわけです。

コピー&ペースト

では、具体的にコピー&ペーストの手順を見ていきましょう。通常、OSのクリップボードは一つしかないため、それに対応するClipboardオブジェクトも一つで十分です。Clipboardクラスにはそのためのシングルトンオブジェクトが用意されており、generalClipboardという静的プロパティでアクセスできる仕組みになっています。

var clipboard:Clipboard = Clipboard.generalClipboard;

このClipboardオブジェクトにデータを設定すれば、OSのクリップボードにデータをコピーしたことになります。データの設定にはsetData()メソッドを使います。次のサンプルは、ボタンをクリックするとクリップボードにテキストをコピーします。

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute">
  <mx:Script>
     <![CDATA[
      private function copyToClipboard():void {
        var clipboard:Clipboard = Clipboard.generalClipboard;
        clipboard.clear();
        var ok:Boolean = clipboard.setData(ClipboardFormats.TEXT_FORMAT, "AIRでコピー&ペースト");
      }
    ]]>
  </mx:Script>
  <mx:Button x="10" y="10" label="Copy" click="copyToClipboard()"/>
</mx:WindowedApplication>

このように、第1パラメータにデータ形式、第2パラメータに実際のデータを指定します。データが正しく設定されるとtrueが返ります。データ形式はClipboardFormatsクラス(flash.desktopパッケージ)の定数として定義されています。

ClipboardFormats.BITMAP_FORMATビットマップ
ClipboardFormats.FILE_LIST_FORMATファイルリスト
ClipboardFormats.TEXT_FORMATテキスト
ClipboardFormats.URL_FORMATURL

setData()を実行すると、同じ形式の古いデータは上書きされます。ただし違う形式のデータはそのまま残ってしまうため、あらかじめclear()メソッドで消去しています。

今度はクリップボードからのペーストです。Clipboardオブジェクトからデータを取得するにはgetData()メソッドを使います。また、hasFormat()メソッドで取得したいデータ形式が含まれているか調べられます。どちらのメソッドもパラメータにデータ形式を指定します。次のサンプルは、ボタンをクリックするとクリップボードのテキストをテキストエリアに表示します。

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute">
  <mx:Script>
    <![CDATA[
      private function pasteFromClipboard():void {
        var clipboard:Clipboard = Clipboard.generalClipboard;
        if (clipboard.hasFormat(ClipboardFormats.TEXT_FORMAT)) {
          output.text = clipboard.getData(ClipboardFormats.TEXT_FORMAT) as String;
        }
      }
    ]]>
  </mx:Script>
  <mx:Button x="10" y="10" label="Paste" click="pasteFromClipboard()"/>
  <mx:TextArea x="10" y="40" id="output"/>
</mx:WindowedApplication>

カスタムフォーマット

標準フォーマット以外の形式でやりとりしたい場合は、カスタムフォーマットを指定します。カスタムフォーマットの名前は任意に付けられますが、他のAIRアプリケーションと重複しないユニークなものが推奨されています("air:"または"flash:"で始まる名前は使えません⁠⁠。以下のコードではカスタムフォーマットでDateオブジェクトの受け渡しを行っています。

//コピーする側
var clipboard:Clipboard = Clipboard.generalClipboard;
clipboard.clear();
clipboard.setData("jp.gihyo.example", new Date(), true);

//ペーストする側
var clipboard:Clipboard = Clipboard.generalClipboard;
if (clipboard.hasFormat("jp.gihyo.example")) {
  var date:Date = clipboard.getData("jp.gihyo.example", ClipboardTransferMode.CLONE_ONLY) as Date;
  trace(date.getFullYear() + "/" + (date.getMonth() + 1) + "/" + date.getDate());
}

ここでは、カスタムフォーマットとして"jp.gihyo.example"を指定しているのが分かると思います。また、コピーする側ではsetData()メソッドの第3パラメータ(以下、serializableパラメータ)にtrueを指定しています。serializableパラメータをtrueにすると、クリップボードには指定オブジェクトの参照とシリアライズされたデータの両方が格納されます(trueの指定は省略可能⁠⁠。falseの場合は参照しか格納されず、他のAIRアプリケーションからはアクセスできません。

ペーストする側では、getData()を行う際に第2パラメータ(以下、transferModeパラメータ)を指定しています。これは、オブジェクトの参照とシリアライズされたデータのどちらを取得するかを指定するパラメータです。transferModeパラメータに指定する値は、ClipboardTransferModeクラス(flash.desktopパッケージ)の定数として定義されています。

ClipboardTransferMode.CLONE_ONLYシリアライズされたデータのみを取得
ClipboardTransferMode.CLONE_PREFERREDシリアライズされたデータを優先して取得、なければ参照を取得
ClipboardTransferMode.ORIGINAL_ONLY参照のみを取得
ClipboardTransferMode.ORIGINAL_PREFERRED参照を優先して取得、なければシリアライズされたデータを取得

なお、標準フォーマット対するtransferModeパラメータの指定は無視されます。

ディファード・レンダリング

クリップボードに格納するデータの作成自体に高い計算負荷がかかる場合は、ディファード・レンダリングという仕組みを利用してデータの作成を後回しにできます。具体的には、setData()メソッドで直接データを設定する代わりに、データを作成するためのレンダリング・ファンクションを用意してsetDataHandler()メソッドで設定しておきます。すると、実際にペーストする段階で初めてそのファンクションが実行され、データの取得が行われます。

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute">
  <mx:Script>
    <![CDATA[
      private var _clipboard:Clipboard = Clipboard.generalClipboard;
      private function copyToClipboard():void {
        _clipboard.setDataHandler(ClipboardFormats.TEXT_FORMAT, renderData);
      }
      private function pasteFromClipboard():void {
        if (_clipboard.hasFormat(ClipboardFormats.TEXT_FORMAT)) {
          output.text = _clipboard.getData(ClipboardFormats.TEXT_FORMAT) as String;
        }
      }
      private function renderData():String {
        // 実際にはここで負荷の高い処理を行う
        return "Deferred Rendering";
      }
    ]]>
  </mx:Script>
  <mx:Button x="10" y="10" label="Copy" click="copyToClipboard()"/>
  <mx:Button x="105" y="10" label="Paste" click="pasteFromClipboard()"/>
  <mx:TextArea x="10" y="40" id="output"/>
</mx:WindowedApplication>

ドラッグ&ドロップ

続いてドラッグ&ドロップの実装方法を見ていきましょう。最初に触れたように、データの移動にClipboardオブジェクトを介する点はコピー&ペーストと変わりません。ただし、Clipboard.generalClipboardプロパティではなく、その都度作成したClipboardオブジェクトを使う点が異なります。加えて、ユーザのマウス操作に応じたイベントハンドリングが必要です。処理の流れを大きく分けると、ドラッグ開始時の処理/ドラッグ中の処理/ドロップ時の処理の3ステップになります。もちろん外からのドロップを受け付けるだけであったりその逆の場合は、必要な処理だけ記述すれば構いません。

まず、ドラッグ開始時の処理です。AIRアプリケーション内でドラッグを開始させたい場合には、その処理を発生させる何らかのDisplayObjectを決める必要があります。これをイニシエータと呼びます。イニシエータには、マウスダウンイベントを受けてDragManagerクラス(flash.desktopパッケージ)のdoDrag()メソッドを呼び出す処理を設定します。また、イニシエータでは次のイベントが発生します。

NativeDragEvent.NATIVE_DRAG_STARTドラッグを開始した
NativeDragEvent.NATIVE_DRAG_COMPLETEドラッグ&ドロップを完了/中止した

ドラッグがAIRアプリケーション外で開始された場合は、イニシエータが無いため上記イベントも発生しません。以下はAIRアプリケーションからテキストデータをドラッグするサンプルです。DragManagerは静的クラスなのでインスタンス化は不要です。

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute">
  <mx:Script>
    <![CDATA[
      [Embed(source="myImage.png")]
      public var MyImage:Class;
      private function startDragAndDrop():void {
        var clipboard:Clipboard = new Clipboard();
        clipboard.setData(ClipboardFormats.TEXT_FORMAT, "AIRでドラッグ&ドロップ");
        var dragImage:BitmapData = new MyImage().bitmapData;
        var offset:Point = new Point(-16, -16);
        var options:DragOptions = new DragOptions();
        options.allowLink = false;
        DragManager.doDrag(_initiator, clipboard, dragImage, offset, options);
      }
    ]]>
  </mx:Script>
  <mx:Button x="10" y="10" label="Drag Start" id="_initiator" mouseDown="startDragAndDrop()"/>
</mx:WindowedApplication>
AIRアプリケーションから別のアプリケーションにデータをドラッグ&ドロップできる
AIRアプリケーションから別のアプリケーションにデータをドラッグ&ドロップできる

上記のように、doDrag()メソッドでは第1パラメータにイニシエータを、第2パラメータにClipboardオブジェクトを渡します。それ以外はオプションです。第3パラメータにはドラッグ中のイメージを指定できます。ドラッグ中のイメージの表示位置は第4パラメータで指定します。これはマウスカーソルのホットスポットを基準にした相対位置です。第5パラメータはドロップ時にどのようなアクションを許可するかという指定です。アクションにはコピー/移動/リンクがあり、DragOptionsオブジェクト(flash.desktopパッケージ)の3つのプロパティで許可または禁止します。デフォルトはすべてtrueです。省略時もすべて許可されます。

DragOptions.allowCopy:Booleantrueのときにコピーを許可
DragOptions.allowLink:Booleantrueのときにリンクを許可
DragOptions.allowMove:Booleantrueのときに移動を許可

もっとも、アクションの指定をしたからと言ってAIRランタイムの挙動が変わるわけではありません。ドラッグ&ドロップの行われる文脈に応じてデベロッパーが任意に指定し、引き続き行う処理の中でドロップを受け付ける際の判断基準として使います。

次はドラッグ中の処理です。データをドラッグしているマウスカーソルがDisplayObjectの上を通過すると、次の各イベントが発生します。

NativeDragEvent.NATIVE_DRAG_ENTERオブジェクトの領域に入った
NativeDragEvent.NATIVE_DRAG_EXITオブジェクトの領域から離れた
NativeDragEvent.NATIVE_DRAG_OVERオブジェクトの領域内にある

NativeDragEventオブジェクトにはドラッグ中のデータにアクセスするためのclipboardプロパティがあり、イベントハンドラ内でデータの形式をチェックできます。そこで、ドロップターゲットとなるDisplayObjectでNativeDragEvent.NATIVE_DRAG_ENTERイベントが発生した際に、受け付け可能なデータ形式が含まれいるか調べDragManager.acceptDragDrop()でドロップを許可するといった処理を行います。すると、ドロップターゲット上でデータをドロップした際に次のイベントが発生するようになります。

NativeDragEvent.NATIVE_DRAG_DROPデータがドロップされた

また、NativeDragEventオブジェクトのactionsAllowedプロパティを使えば許可されたアクションもチェックできるので、必要があればドロップ受け付けの判断基準に加えます。

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" applicationComplete="init()">
  <mx:Script>
    <![CDATA[
      private function init():void {
        _dropTarget.addEventListener(NativeDragEvent.NATIVE_DRAG_ENTER, dragEnterHandler);
      }
      private function dragEnterHandler(e:NativeDragEvent):void {
        if (e.clipboard.hasFormat(ClipboardFormats.TEXT_FORMAT) && e.actionsAllowed.allowCopy) {
          DragManager.acceptDragDrop(_dropTarget);
        }
      }
    ]]>
  </mx:Script>
  <mx:TextArea x="10" y="10" id="_dropTarget"/>
</mx:WindowedApplication>

上のコードのように、DragManager.acceptDragDrop()のパラメータにはドロップターゲットを渡します。これらの処理はNativeDragEvent.NATIVE_DRAG_OVERイベントで行っても構いません。このイベントはマウスカーソルが特定のDisplayObject上にある間は短い間隔で発生し続けるので、例えば地図の中の一部のエリアだけでドロップを受け付けたい場合等に利用できます。

最後にドロップ時の処理です。前述のように、AIRアプリケーション内でドロップを受け付けた場合にはNativeDragEvent.NATIVE_DRAG_DROPイベントが発生します。このイベントハンドラでClipboardオブジェクトから実際にデータを取得して必要な処理を行います。それと同時にイニシエータへのフィードバックとして、実際に行ったアクションをDragManager.dropActionに設定します。アクションの指定にはDragActionsクラス(flash.desktopパッケージ)の定数を使います。

DragActions.COPYコピー
DragActions.LINKリンク
DragActions.MOVE移動
DragActions.NONE中止

アクションの指定を省略すると、DragManager.doDrag()で許可したアクションの中からコピー、移動、リンクの順で設定されます。次のサンプルは先のコードにドロップ時の処理を加えたものです。

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" applicationComplete="init()">
  <mx:Script>
    <![CDATA[
      private function init():void {
        _dropTarget.addEventListener(NativeDragEvent.NATIVE_DRAG_ENTER, dragEnterHandler);
        _dropTarget.addEventListener(NativeDragEvent.NATIVE_DRAG_DROP, dragDropHandler);
      }
      private function dragEnterHandler(e:NativeDragEvent):void {
        if (e.clipboard.hasFormat(ClipboardFormats.TEXT_FORMAT) && e.actionsAllowed.allowCopy) {
          DragManager.acceptDragDrop(_dropTarget);
        }
      }
      private function dragDropHandler(e:NativeDragEvent):void {
        DragManager.dropAction = DragActions.COPY;
        _dropTarget.text = e.clipboard.getData(ClipboardFormats.TEXT_FORMAT) as String;
      }
    ]]>
  </mx:Script>
  <mx:TextArea x="10" y="10" id="_dropTarget"/>
</mx:WindowedApplication>

ドロップイベントに続いて、イニシエータではNativeDragEvent.NATIVE_DRAG_COMPLETEイベントが発生します。このイベントの時だけNativeDragEventオブジェクトのdropActionプロパティが有効になり、先に設定したアクションを取得できます。アクションが仮に⁠移動⁠であれば元のデータを消すなど、必要に応じた処理を行って一連の流れが完了となります。なお、ユーザがドラッグを中止した場合や、ドロップが受け付けられなかった場合もNativeDragEvent.NATIVE_DRAG_COMPLETEイベントが発生します。そのときのdropActionプロパティの値はDragActions.NONEになります。

※DragManager/DragOptions/DragActionsの各クラスは、次のランタイムのリリース時にはNativeDragManager/NativeDragOptions/NativeDragActionsに名称が変更される予定です。

おすすめ記事

記事・ニュース一覧