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

第8回ファイルシステムAPI(その2)

ファイルのオープン/クローズ

前回はFileオブジェクトを使ってファイルのコピーや削除といった操作を行いました。AIRアプリケーションは、ファイルの内容を読み込んだり、ファイルにデータを書き込んだりもできます。ファイルの読み書きを行うには、Fileクラスの他にFileStreamクラスを使用します。

読み書きの対象となるファイルは、処理を行う前にオープンし、処理が終わったらクローズする必要があります。オープンしている間は、他のアプリケーションから見ると使用中ということです。オープンはFileStreamクラスのopen()メソッドまたはopenAsync()メソッドで行います。前者は同期メソッド、後者は非同期メソッドです。同期/非同期については前回の記事を参考にしてください。クローズはclose()メソッドで行います。

open()メソッドとopenAsync()メソッドは、どちらも2つの引数を取ります。第1引数は操作対象のファイルを指定したFileオブジェクト、第2引数はファイルモードです。ファイルモードとは、どのような操作を前提にファイルをオープンするか指定するものです。指定できる値はFileModeクラスの定数として定義されていて、次の4つがあります。

FileMode.READ読み込み専用。
FileMode.WRITE書き込み専用。ファイルは常に上書きされる。ファイルが存在しなければオープン時に作成される。
FileMode.APPEND書き込み専用。データは常にファイル末尾に追記される。ファイルが存在しなければオープン時に作成される。
FileMode.UPDATE読み書き両用。ファイルの任意の位置から読み書き可能。ファイルが存在しなければオープン時に作成される。

open()メソッドもopenAsync()メソッドも、READまたはUPDATEを指定してファイルをオープンすると、すぐにランタイム内部の入力バッファにデータを読み込み始めます。同期メソッドであるopen()の場合、バッファにすべてのデータが格納されてから次の処理に移りますが、非同期メソッドのopenAsync()の場合はバッファへの読み込みをイベントで監視して必要な処理を行います。

ファイルの読み込み

では、UTF-8のテキストファイルを読み込んでみましょう。次のサンプルは、同期処理でファイルを読み書きする際の基本的な流れです。ボタンをクリックするとデスクトップ上のsample.txtを読み込みテキストエリアに表示します。

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" width="340" height="300">
  <mx:Script>
    <![CDATA[
      import flash.filesystem.*;
      private function readData():void {
        var file:File = File.desktopDirectory.resolve("sample.txt");
        var stream:FileStream = new FileStream();
        try {
          stream.open(file, FileMode.READ);
          var str:String = stream.readUTFBytes(stream.bytesAvailable);
          output.text = str.replace(/\r\n/g, "\n");
        } catch (error:IOError) {
          trace(error.message);
        } finally {
          stream.close();
        }
      }
    ]]>
  </mx:Script>
  <mx:TextArea x="20" y="20" id="output" width="300" height="200" fontSize="16"/>
  <mx:Button x="136" y="246" label="読み込み" click="readData()"/>
</mx:WindowedApplication>
外部ファイルからデータを読み込む
外部ファイルからデータを読み込む

ここでは読み込みのみを行うので、open()メソッドでFileMode.READを指定してオープンします。続いて実行しているreadUTFBytes()がUTF-8のデータを読み込むメソッドです。ファイルが見つからなかったり読み込めなかったりした場合、これらのメソッドはIOErrorをスローします。エラーが発生するしないに関わらずファイルはクローズしておきたいので、finally節にclose()メソッドを記述しています。

readUTFBytes()メソッドには引数として読み込むデータの長さを指定します。ここで指定しているFileStream.bytesAvailableは、入力バッファから現在どれだけのデータが読み込み可能か示すプロパティです。前述のようにopen()メソッドの場合は次の処理に移った時点でバッファへの読み込みが完了しているので、すべてのデータを一度に取得できます。なお、改行コードCR+LFをそのまま表示すると2つの改行コードとして認識されるため、テキストエリアへ設定する前にString.replace()メソッドでLFのみに置換しています。

次は上記のサンプルを非同期処理に変更してみましょう。

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" width="340" height="300">
  <mx:Script>
    <![CDATA[
      import flash.filesystem.*;
      private var stream:FileStream;
      private function readData():void {
        var file:File = File.desktopDirectory.resolve("sample.txt");
        stream = new FileStream();
        stream.addEventListener(IOErrorEvent.IO_ERROR, onIOError);
        stream.addEventListener(ProgressEvent.PROGRESS, onInputProgress);
        stream.addEventListener(Event.COMPLETE, onInputComplete);
        stream.addEventListener(Event.CLOSE, onCloseFile);
        stream.openAsync(file, FileMode.READ);
      }
      private function onIOError(event:IOErrorEvent):void {
        trace("I/Oエラー");
        stream.close();
      }
      private function onInputProgress(event:ProgressEvent):void {
        trace(stream.bytesAvailable + "バイト読み込み済み");
      }
      private function onInputComplete(event:Event):void {
        try {
          var str:String = stream.readUTFBytes(stream.bytesAvailable);
          output.text = str.replace(/\r\n/g, "\n");
        } catch (error:IOError) {
          trace(error.message);
        } finally {
          stream.close();
        }
      }
      private function onCloseFile(event:Event):void {
        trace("ファイルをクローズしました");
      }
    ]]>
  </mx:Script>
  <mx:TextArea x="20" y="20" id="output" width="300" height="200" fontSize="16"/>
  <mx:Button x="136" y="246" label="読み込み" click="readData()"/>
</mx:WindowedApplication>

非同期処理の場合、バッファへデータが入力される度にProgressEvent.PROGRESSイベントが配信され、バッファへの入力が完了するとEvent.COMPLETEイベントが配信されます。また、エラー発生時に配信されるIOErrorEvent.IO_ERRORイベント、ファイルのクローズ時に配信されるEvent.CLOSEイベントもあります。これらのイベントを処理するには、ファイルをオープンする前にイベントハンドラを登録しておく必要があります。

openAsync()メソッドでファイルをオープンするとバッファへのデータ入力が開始されるので、完了イベントを受けてreadUTFBytes()メソッドを実行します。このようにバッファからの読み込み自体は同期処理となります。

ファイルの書き出し

書き出しの手順も読み込みの場合とそれほど変わりません。同期処理の場合、ファイルのモードと書き出し時のメソッドが違うだけです。以下のサンプルは、ボタンをクリックするとテキストエリアの内容をsample.txtとして書き出します。

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" width="340" height="300">
  <mx:Script>
    <![CDATA[
      import flash.filesystem.*;
      private function writeData():void {
        var file:File = File.desktopDirectory.resolve("sample.txt");
        var stream:FileStream = new FileStream();
        try {
          stream.open(file, FileMode.WRITE);
          stream.writeUTFBytes(input.text);
        } catch (error:IOError) {
          trace(error.message);
        } finally {
          stream.close();
        }
      }
    ]]>
  </mx:Script>
  <mx:TextArea x="20" y="20" width="300" height="200" fontSize="16" editable="true" id="input"/>
  <mx:Button x="136" y="246" label="書き出し" click="writeData()"/>
</mx:WindowedApplication>

FileMode.READ以外のすべてのモードで書き出しが可能ですが、通常はファイル全体を書き換える処理がほとんどでしょう。その場合はFileMode.WRITEを指定します。UTF-8でテキストファイルを書き出す際にはwriteUTFBytes()メソッドを使い、書き出したい文字列を引数に指定します。

次は非同期処理のサンプルです。

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" width="340" height="300">
  <mx:Script>
    <![CDATA[
      import flash.filesystem.*;
      private var stream:FileStream;
      private function writeData():void {
        var file:File = File.desktopDirectory.resolve("sample.txt");
        stream = new FileStream();
        stream.addEventListener(IOErrorEvent.IO_ERROR, onIOError);
        stream.addEventListener(OutputProgressEvent.OUTPUT_PROGRESS, onOutputProgress);
        stream.addEventListener(Event.CLOSE, onCloseFile);
        stream.openAsync(file, FileMode.WRITE);
        try {
          stream.writeUTFBytes(input.text);
        } catch (error:IOError) {
          trace(error.message);
        } finally {
          stream.close();
        }
      }
      private function onIOError(event:IOErrorEvent):void {
        trace("I/Oエラー");
        stream.close();
      }
      private function onOutputProgress(event:OutputProgressEvent):void {
        trace("残り" + event.bytesPending + "バイト");
      }
      private function onCloseFile(event:Event):void {
        trace("ファイルをクローズしました");
      }
    ]]>
  </mx:Script>
  <mx:TextArea x="20" y="20" width="300" height="200" fontSize="16" editable="true" id="input"/>
  <mx:Button x="136" y="246" label="書き出し" click="writeData()"/>
</mx:WindowedApplication>

非同期処理で書き出す場合、出力バッファに格納されたデータが定期的にファイルに書き込まれます。進行状況の確認にはOutputProgressEvent.OUTPUT_PROGRESSイベントを使い、OutputProgressEvent.bytesPendingプロパティでバッファに残っているバイト数を確認できます。もっとも、書き出し処理の場合は残りのバイト数を気にすることなく、先にclose()メソッドでファイルをクローズできます。するとバックグラウンドで書き出し処理が終わった時にEvent.CLOSEイベントが配信されます。書き出し処理ではEvent.COMPLETEイベントは配信されません。

様々な読み書き用メソッド

FileStreamクラスにはreadUTFBytes()やwriteUTFBytes()以外にも、データ形式に応じた20個以上の読み書き用メソッドがあります(メソッド名がreadやwriteで始まるもの⁠⁠。すべては紹介できないので、代表的なもの挙げておきましょう。

readMultiByte()、writeMultiByte()は任意の文字コードを使ってファイルを読み書きできます。サポートしている文字コードはActionScript 3.0のリファレンスを参考にしてください。Shift-JISであれば次のように読み込めます(エラー処理等は省略しています⁠⁠。

var file:File = File.desktopDirectory.resolve("shift_jis.txt");
var stream:FileStream = new FileStream();
stream.open(file, FileMode.READ);
var str:String = stream.readMultiByte(stream.bytesAvailable, "shift_jis");
stream.close();

また、OSのデフォルト文字コードがFile.systemCharsetプロパティで取得できるので、それを指定することもできます。

var str:String = stream.readMultiByte(stream.bytesAvailable, File.systemCharset);

readObject()、writeObject()はActionScriptオブジェクトの保存に使える便利なメソッドです。ActionScriptオブジェクトはAMF(Action Message Format)形式にシリアライズされて保存されます。例えば、次のコードはアプリケーションウィンドウの位置とサイズを示すNativeWindow.boundsプロパティをオブジェクトとして保存します。

registerClassAlias("flash.geom.Rectangle", Rectangle);
registerClassAlias("flash.geom.Point", Point);
var file:File = File.applicationStorageDirectory.resolve("prefs");
var stream:FileStream = new FileStream();
stream.open(file, FileMode.WRITE);
stream.writeObject(stage.window.bounds);
stream.close();

これをアプリケーション起動時にreadObject()で読み込み、boundsプロパティに設定し直せば、前回表示していた状態に戻すことができます。registerClassAlias()はオブジェクトの型を維持するために必要メソッドです。保存時だけでなく読み込み時にも必要な処理なので、アプリケーション起動時に一度実行しておくとよいでしょう。実行しない場合、汎用オブジェクトとして扱われます。

ランダムアクセス

FileStream.positionプロパティを使うと、ファイル内のどの位置から読み書きを行うか指定できます。ただし、常にファイル全体を上書きするWRITEモードや常にファイル末尾に追記するAPPENDモードでは効果がありません。また、非同期処理で読み込みを行っている場合には、バッファに入力されている範囲内でのみ移動できます。

ファイルへランダムアクセスができると、例えばMP3ファイルのID3タグを取得できます。ID3v1ではファイルの末尾128バイトに情報があります。ID3タグの仕様については触れませんが、曲名を取得したければ、先頭の"TAG"という識別子に続く30バイト分を読み込みます。

var file:File = File.desktopDirectory.resolve("sample.mp3");
var stream:FileStream = new FileStream();
stream.open(file, FileMode.READ);
stream.position = file.size - 128 + 3; //"TAG"をスキップ
var title:String = stream.readMultiByte(30, "shift_jis");
trace(title);
stream.close();

positionプロパティは読み書き処理を実行した分だけ自動的に移動します。上記の例で言うと、readMultiByte()実行後の値は30バイト分進んでいます。そのままさらに30バイト分読み込めばアーティスト名を取得できます。positionプロパティはメディアの記録再生ヘッドのようなものです。

ファイルピッカー

ファイルやファイルの保存先をユーザに選択させたい場合があります。Fileクラスにはそのためのメソッドが用意されており、OS標準のダイアログを利用できます。

browseForDirectory()フォルダ選択ダイアログを表示
browseForOpen()ファイル選択ダイアログを表示
browseForOpenMultiple()複数ファイル選択ダイアログを表示
browseForSave()ファイル保存ダイアログを表示

※Beta 1 リリースで筆者が確認した範囲では、Macintoshで動作するのはbrowseForSave()だけです。それ以外を実行するとアプリケーションが落ちてしまいます。Windowsでは問題ありません。

次のサンプルは、ボタンをクリックするとファイル選択ダイアログを表示します。ファイル形式のフィルタを指定して、JPG/PNG/BMPファイルのみが表示されるようにしています。

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute">
  <mx:Script>
    <![CDATA[
      import flash.filesystem.File;
      import flash.events.Event;
      import flash.net.FileFilter;
      private var file:File = new File();
      private function selectFile():void {
        try {
          var filter:FileFilter = new FileFilter("画像ファイル", "*.jpg;*.png;*.bmp");
          file.addEventListener(Event.SELECT, onSelecte);
          file.addEventListener(Event.CANCEL, onCancel);
          file.browseForOpen("ファイルの選択", [filter]);
        } catch (error:Error) {
          trace(error.message);
        }
      }
      private function onSelecte(event:Event):void {
        trace(event.target.nativePath);
      }
      private function onCancel(event:Event):void {
        trace("キャンセルされました");
      }
    ]]>
  </mx:Script>
  <mx:Button x="10" y="10" label="ファイル選択" click="selectFile()"/>
</mx:WindowedApplication>

ダイアログの選択ボタンやキャンセルボタンが押された時の処理は、Event.SELECTおよびEvent.CANCELイベントに対するイベントハンドラで行います。選択されたファイルやディレクトリはEvent.targetプロパティからFileオブジェクトとして取得できます。これは他のダイアログの場合も同様です。ただし、browseForOpenMultiple()だけはEvent.SELECTの代わりにFileListEvent.SELECT_MULTIPLEイベントを使います。この場合、選択されたファイルはFileListEvent.filesプロパティからFileオブジェクトの配列として取得します。

おすすめ記事

記事・ニュース一覧