Flexで画像のロードが終わるまで待機したい(Sleepが無い!!)

どうも、うしねずみです。
Flexでの画像のロードについて、悩んだので書いてみます。どうやらFlexにはSleep関数が無いようで、そのせいで色々悩みました。まずは、画像をロードする方法をまとめてみます。

方法その1: LoaderをそのままaddChildする

例えばロードした表示するだけの時は以下のように書きます。

  1. public function test():void  
  2. {  
  3.     var loader:Loader = new Loader();  
  4.     var urlRequest:URLRequest = new URLRequest("http://example.com/hoge.jpg");  
  5.     var uic:UIComponent = new UIComponent;  
  6.     loader.load(urlRequest);  
  7.     uic.addChild(loader);  
  8.     this.addChild(uic);  
  9. }  

loaderをuicにaddChildしたものをaddChildすれば画像が表示されるというちょっと不可思議な感じ。

方法その2: COMPLETEイベントのリスナーを登録しておく

方法その1だと、画像を表示するだけなら出来ましたが、画像を扱ってどうにかしたいときは困ります。そういう時は以下の方法。

  1. public function test():void  
  2. {  
  3.     var loader:Loader = new Loader();  
  4.     var urlRequest:URLRequest = new URLRequest("http://example.com/hoge.jpg");  
  5.     loader.contentLoaderInfo.addEventListener(Event.COMPLETE, loadComplete);  
  6.     loader.load(urlRequest);  
  7. }  
  8. private function loadComplete(event:Event):void  
  9. {  
  10.     var loaderInfo:LoaderInfo = event.target as LoaderInfo;  
  11.     var bitmap:Bitmap = loaderInfo.content as Bitmap;  
  12.     // 画像を使った何らかの処理をここに書く  
  13. }  

4行目でloadComplete関数をロード完了時のイベントリスナに登録しています。よって、画像の読み込みが終わるとloadComplete関数に制御が飛んで、その中で画像を使った処理ができます。test関数はロードが完了する前にリターンされます。よってtest関数を呼んだ側の処理が画像のロードの間待たされるわけではありません。

さて、ここで問題が

実はこの「ロードが終わる前に制御が戻る」という性質が厄介だったりするのです。最近画像のラッパーを作っていたのですが、その時に「画像のロードが終了するまで呼び出し側の実行を待機したい」ということがありました。例えば以下のようなラッパーを作ったとします。

  1. public class MyImageWrapper  
  2. {  
  3.     public var imageAddress:String;  
  4.     public var imageData:Bitmap;  
  5.     public function MyImageWrapper(address:String)  
  6.     {  
  7.         // コンストラクタ内で画像のロード処理  
  8.         var loader:Loader = new Loader();  
  9.         var urlRequest:URLRequest = new URLRequest("http://example.com/hoge.jpg");  
  10.         loader.contentLoaderInfo.addEventListener(Event.COMPLETE, loadComplete);  
  11.         loader.load(urlRequest);  
  12.     }  
  13.     private function loadComplete(event:Event):void  
  14.     {  
  15.         // ロードが終わったらメンバ変数に格納しておく  
  16.         var loaderInfo:LoaderInfo = event.target as LoaderInfo;  
  17.         this.imageData = loaderInfo.content as Bitmap;  
  18.     }  
  19. }  

このクラスは、画像のアドレスを渡してnewすると、コンストラクタ内で画像をロードしておいてくれるクラスです。これだけだとアドレスと画像を持っているだけですが、他にもこの画像に関する様々な情報をまとめて持たせるためのクラスです。
さて、このクラスは例えば以下のように使われます。

  1. var myImage:MyImageWrapper = new MyImageWrapper("http://example.com/hoge.jpg");  
  2. this.addChild(myImage.imageData);  

ここで問題が生じるのです。MyImageWrapperのコンストラクタはロード処理を開始するだけで、ロードが終わるまで待ちません。なのでまだMyImageWrapperのloadCompleteが呼ばれないうちに制御が返ってきます。すると2行目でまだmyImage.imageDataはnullなので、「nullはaddChildできません」というエラーになるのです。
じゃー答えは簡単。imageDataがnullじゃなくなるまで待てばいいんだな、と思って次のように書き変えました。

  1. var myImage:MyImageWrapper = new MyImageWrapper("http://example.com/hoge.jpg");  
  2. while(myImage.imageData == null){  
  3.     ;  // 画像のロードが終了するまでは空ループ  
  4. }  
  5. this.addChild(myImage.imageData);  

ところが、このソースを実行してみると、永久にloadCompleteが呼ばれず、5行目にたどりつくことができません。Flex(Flash)の中で「CPUが空き状態になるまでloadCompleteの実行は待機する」という仕様になっているのでしょうか。

じゃあ空ループじゃなくてSleepすればいいんだな。と思って調べたのですが、どうやらFlexにはSleepが無い模様。
さて、困った。

Timerを使って実装することもできなくはないです。以下のようになります。

  1. var myImage = new MyImageWrapper("http://example.com/hoge.jpg");  
  2. var timer:Timer = new Timer(1000);  
  3. timer.addEventListener(TimerEvent.TIMER, function (te : TimerEvent):void{  
  4.     if(myImage.imageData != null){  
  5.         var uic : UIComponent = new UIComponent();  
  6.         uic.addChild(myImage.imageData);                  
  7.         this.addChild(uic);  
  8.         Timer(te.target).stop();  
  9.     }  
  10. });  
  11. timer.start();  

タイマは、一定時間ごとに登録したリスナを呼んでくれます。ここでは無名関数をその場で定義して登録しています。つまりこのタイマは100msごとに画像のロードが終わってるかどうかをチェックして、もし終わってたらaddChildを済ませて、自分自身(タイマ)を止めてくれます。
しかしこれはこれでなんか面倒だしすっきりしません。また、timer.start()のところで実行が待機するわけではないので別のところで画像データを使いたくなったときに既に画像のロードが終わっている保証がありません。

例えばユーザには「画像ロードボタン」と「画像編集ボタン」が提示されているとします。「画像ロードボタン」はユーザが指定したURLから画像をロードしてくるボタンです。「画像編集ボタン」はロードした画像に何か編集を加えるボタンです(モノクロに変換するなど、何でもよい)。仮にタイマを使っても、「画像ロードボタン」を押したあとロードが完了する前に制御がユーザに戻ってしまいます。その後すぐに「画像編集ボタン」を押されると、「画像編集ボタン」のイベントハンドラは既に画像が読み込まれていることを期待して読みに来ますが、実際は画像データはありません。

結局、対策としては、画像がロード済みかどうか(null)じゃないかどうか、使う前に毎回チェックして、ロードが済んでいなかったら何もしないっていうこと。それしかないのかなー。
なんとかSleep的な処理ができないかなーと思って疑似Threadライブラリ(そうめん?)なんかも試してみた。Threadが終了するまで待つことができるjoin関数が使えるかと思ったけど、Threadの終了を待てるのはThreadだけのようで、Threadではない実行部分で何かの処理を待機することはできない模様。
完全に手詰まりです。

ところで、Sleepが無い理由を自分なりに考えてみた。
Flexは基本的にイベント駆動型のアプリを作るためにあるはずなので、一つのイベントが全体をSleepさせるような処理を書くべきではない、ということなのだと思う。
でも、なんか、かゆい所に手が届かないむずむずというか、、、初めてSchemeを触った時に「破壊的代入を行わないプログラミング」が難しいパズルのようでどうしても頭に入ってこなかった時のような気持ち悪さ。
まー、修行が足りないってことですかね。