なぜRemoteClassがうまくキャストされないのか

先日、久々にBlazeDSで新しいサービスを作成し、それに合わせてJavaのBeanとRemoteClassも新たに作成しました。
ところが、いざ作ってresultをとってみると、一番下の階層のRemoteClassがなぜかObjectに。
階層構造が深すぎるとだめなのかな?と思って、そのオブジェクトだけをやり取りするテストメソッドを作ってみるもダメ。
どうしてキャストされないのだろう?

原因は、FXUGの物知り長者pepeさんのブログを見ることで判明しました。
http://shigeru-nakagaki.com/index.cfm/2007/4/15/20070415-RemoteClass-meta-tag

RemoteClassを登録するためのこの自動生成コードは、プログラムコードの中に変数としての宣言がないと生成されません。
他のRemoteClassのプロパティとして登場するRemoteClassは、通常クラス定義の中で変数として登場するので生成されるはずですが、プロパティがコレクションの場合には、コード内ではRemoteClassの宣言を使用しません。
こういったRemoteClassは自動生成されないのです。

[RemoteClass(alias='HogeChild')]
public class HogeChild{
 ...
}
[RemoteClass(alias='HogeParent')]
public class HogeParent{
 ...
 //うまくキャストされる
 public function get child():HogeChild{...}
 public function set child(val:HogeChild):void{...}
 //キャストされない(Objectのコレクションになる)
 public function get children():ArrayCollection{...}
 public function set children(val:ArrayCollection):void{...}
}

これがわかってしまえば、対策は簡単です。
プロジェクトのどこかで、キャストさせたいRemoteClassの変数宣言をするだけでも解消されますし、registerClassAliasをアプリケーション初期化のタイミングで実行してもよいです。
ほっと一安心。

自動生成コードも、たまには見ないとFlexデバッグはできないことがある、というお話でした…。

ニコニコ動画風スクロールコメント

FXUGで作ってるっぽい人がいたので、ちょっと気になって試してみました。

前回よりも業の深いブログ ニコニコ動画風スクロールコメント

詳しい説明はリンク先で。
Flash10になってテキストが回転させられるようになったので、本当はもっとぐりんぐりんやってみたいところではあります。

あ、箇条書きボタンがありますが、これやるとちょっと表示がおかしくなります。まあいいや。

たまにはAIRのサンプルも置いてみる

みなさんお久しぶりです。

こないだ仕事中に作っちゃったAIRアプリがサンプルとして結構逝けてるのでアップしてみました。
AIRファイルもそのうち置きますが、FC2へのアップの仕方を忘れてしまったので(ダメPG…)。

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical"
	paddingLeft="0" paddingRight="0" paddingBottom="0" paddingTop="0" 
	nativeDragEnter="dragEnterHandler(event)" nativeDragDrop="dragDropHandler(event)"
	horizontalScrollPolicy="off" verticalScrollPolicy="off"
	title=" PNGファイルの半透明を消しちゃうぞ1号"
	>
	<mx:Script>
		<![CDATA[
			import mx.graphics.ImageSnapshot;
			import mx.graphics.codec.PNGEncoder;
			import mx.controls.Image;
			import mx.containers.Canvas;

			private function dragEnterHandler(e:NativeDragEvent):void{
				if(e.clipboard.hasFormat(ClipboardFormats.FILE_LIST_FORMAT)){
					NativeDragManager.acceptDragDrop(this);
				}
			}
			private function dragDropHandler(e:NativeDragEvent):void{
				frontLabel.visible = false;
				hList.removeAllChildren();
				var fileName:String;
				try{
					var files:Array = e.clipboard.getData(ClipboardFormats.FILE_LIST_FORMAT) as Array;
					for each(var file:File in files){
						if((file.exists) && (file.extension == "png")){
							fileName = file.nativePath;
							var fs:FileStream = new FileStream();
							var bytes:ByteArray = new ByteArray();
							fs.open(file,FileMode.READ);
							fs.readBytes(bytes);
							fs.close();
							log.text += "読み込み開始:" + fileName+"\n";
							var loader:Loader = new Loader();
							loader.contentLoaderInfo.addEventListener(Event.COMPLETE, getFunction(file));
							loader.loadBytes(bytes);
						}
					}
				}catch(err:Error){
					trace("error:"+err.message);
					log.text+= "読み込みに失敗しました:" + fileName +"\n";
				}
			}
			private function getFunction(file:File):Function{
				return function(e:Event):void{
					var ld:Loader = LoaderInfo(e.currentTarget).loader;
					return addCanvas(file,ld);
				}
			}
			private function addCanvas(file:File,ld:Loader):void{
				if(file){
					var canvas:Canvas = new Canvas();
					canvas.setStyle("backgroundColor",cPicker.selectedColor);
					var img:Image = new Image();
					img.source = ld;
					canvas.addChild(img);
					canvas.setActualSize(img.width,img.height);
					canvas.data = file;
					hList.addChild(canvas);
				}
			}
			private function onUpdate(e:Event):void{
				if(hList.numChildren > 0){
					var canvas:Canvas = hList.getChildAt(0) as Canvas;
					if(canvas){
						save(canvas);
					}
					hList.removeChild(canvas);
				}
			}
			private function save(canvas:Canvas):void{
				var file:File = canvas.data as File;
				var pngEncoder:PNGEncoder = new PNGEncoder();
				var bmpData:BitmapData = ImageSnapshot.captureBitmapData(canvas);
				var pngBytes:ByteArray = pngEncoder.encode(bmpData);
				var fileName:String = file.nativePath;
				// FileStreamクラスのインスタンス作成
				try{
					var stream:FileStream = new FileStream( );
					stream.open(file, FileMode.WRITE);
					stream.writeBytes(pngBytes, 0, pngBytes.length);
					stream.close( );
					log.text+= "成功:" + fileName +"\n";
				}catch(err:Error){
					trace("error:"+err.message);
					log.text+= "書き込みに失敗しました:" + fileName +"\n";
				}
			}
		]]>
	</mx:Script>
	<mx:ApplicationControlBar width="100%" minWidth="0">
		<mx:Label text="背景色"/>
		<mx:ColorPicker id="cPicker" selectedColor="#ffffff"/>
	</mx:ApplicationControlBar>
	<mx:Panel width="100%" height="100%" horizontalScrollPolicy="off" verticalScrollPolicy="off" layout="absolute"
		
		headerHeight="0">
		<mx:TextArea id="log" editable="false" width="100%" height="100%" x="0" y="0" alpha="0.6" />
		<mx:ViewStack id="hList" x="0" y="0" resizeToContent="true" updateComplete="onUpdate(event)"/>
		<mx:Text id="frontLabel"
			width="100%" textAlign="center"
			verticalCenter="0" fontSize="14" fontWeight="bold"
			text="{'この辺めがけてドロップしてね\nいきなり書き換えるからバックアップを忘れずに'}">
			<mx:filters>
				<mx:DropShadowFilter color="0xcccccc"/>
			</mx:filters>
		</mx:Text>
	</mx:Panel>
</mx:WindowedApplication>

何をするプログラムかというと、ソースにも書いてますが「PNGファイルの半透明を他の色で置換する」というソフトです。
ドロップしたPNGファイルの中身をいきなり上書きします。
バックアップファイルも作らないしドロップ以外の読み込み方法もありません。人としてサイテーです。

こんなソフトを作成したのには理由があります。
最近、社内でヘルプを書いているのですが、大量にスクリーンショットを取る必要が生じ、そこでアプリケーション第1弾「MonkeyBinder」を作成しました。
MonkeyBinderは、Flexアプリケーションのコードにタグを一行入れるだけで、狙ったコンポーネントスクリーンショットをキー一発で作成してくれる優れものです。どうやってるか想像がついた?黙っててください。
これはまたそのうちどこかにアップするつもりですが、いろいろと一般人には受け入れがたい(?)問題点があるので、直してからにしようと思っています。

で、Adobe様謹製のヘルプ作成ソフト「RoboHelp7」でゴニョゴニョ書いたヘルプに、スクリーンショットを入れて…とやってるうちに、気がついてしまいました。IE6では、PNG画像の半透明部分の表示にバグがあり(というか未対応らしい)、おかしな色になってしまうのです。
せめてJPEGやGIFで書き出しておけば良かったのですが、100枚ものスクリーンショットを取り直すのは問題外。
GIFに変換するのも検討しましたが、ヘルプファイルのHTML内のファイル名を全部.GIFにするのも面倒くさい。
PNGのままで使う画像もあるし…。

というわけで、仕事が終わってから翌日までの間に布団の中で作成したのがこの「PNGファイルの半透明を消しちゃうぞ1号」です。
ヘタをすりゃこのエントリ書いてる時間の方が長いかも…。

では、ソースの解説。
ドラッグ&ドロップした画像ファイルを開き、書き戻すところなんかは参考になるかもしれません。
もっとも、書き込み禁止のファイルとかフェイルセーフについてはガン無視です。
ビットマップからAlphaを消すのは面倒だったので、単色のCanvasの上に書いて、まるごとスナップショットを取り直しています。
こんな方法をとってしまったが故に、画像を書く前に保存しようとしてbitmapDataのエラーを吐かれてしまいました。
なので、画面の更新が終わってから、そのイベントで保存させるという、何とも情けない方法をとっています。ショートプログラムだから許されるようなもんですね。

まぁ、実際のところ、自分の用さえ済んでしまえばあんまり手直しする気も起こらないわけで…。
ただ、こんな感じで画像加工プログラムをAIRで作っておくと、いろいろ便利かもしれません。
画像じゃなくても、コード整形なんかにも有効ですね。
HTMLの画像ファイル名を書き換えてくれるプログラムとか…あれ?

同じFlexページの同時起動を禁止する

今作っているアプリケーションはStrutsで作成した登録ウィザードの移植なのですが、トランザクションをテストしようと思って同じページを複数開いたら、ウィザードの登録データが混ざってしまって大変なことに。

なぜかというと、ウィザードの進行中の一時データはサーバー側のセッションに持たせているからです。
IEでサイトを開いてログインし、別のウィンドウを(ファイル->新規作成で)開いて、そっちでもログインさせると、Java側が同じセッションIDを受け取ってしまうので、混じってしまうのです。
しかも、片方でログアウトすると、もう片方も強制ログアウトされてしまう。
痛い。とても痛い。

現象自体は、セッションのデータをさらにウィンドウIDごとに持たせ、さらにセッションIDへの参照をカウントアップさせれば、(大まかには)何とかなります。
しかし、そんなことしてたらせっかくサーバー側を流用した意味がない・・・。

てか、一人で同時に登録処理なんかさせてたまるかって話です。
ということで、アプリケーションのほうを同時起動させないようにしました。

やり方は簡単。LocalConnectionを同一アプリケーションで監視するだけです。

package
{
  import flash.net.LocalConnection;
  public dynamic class SingleApplicationEnforcer extends LocalConnection
  {
     public function SingleApplicationEnforcer(connectionName:String)
     {
        try{
          connect(connectionName);       
        }catch (error:ArgumentError){
          //connectionが有効なものかどうか試す
          var ping:Function =function():void{
            trace("ping!");
          }
          try{
            send(connectionName,"ping");
            //他のインスタンスが有効だと例外が出ない
            //つまり二重起動
            _isExistOther = true;
          }catch(e:*){
            //接続は残っていてもアプリケーションはない
            //継続してOK

            //接続確認用コールバックメソッド
            this["ping"] = ping;
          }
       }
    }
  }
}
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute"
  creationComplete="completeHandler(event)">
  <mx:Script>
    <![CDATA[
      import mx.controls.Alert;
      private var ls:SingleApplicationEnforcer;

      private function completeHandler(e:Event):void{
        ls = new SingleApplicationEnforcer('connSample');
        if(ls.isExistOther){
          Alert.show("Second Applications");
          enabled = false;
        }
      }
    ]]>
  </mx:Script>
</mx:Application>

ブラウザが違う場合でも有効なのは、果たしていいことなのか・・・。
まぁ、人によっては役に立つこともあろうかと思います。

しかし、このセッションの使い方のミスは痛恨。
調べてないけど、FlexSessionだとFlashインスタンスごとに割り振られたりとかしないのかなぁ・・・。

[2009/3/4追記]
このコードだと、同じサイトを開いていないのに検出してしまう場合があることがわかりました。
Flash Playerは、Flashアプリケーションが一つでも起動している間はすべてのLocalConnectionを保持しているみたいです。
つまり、全く関係のないFlashアプリケーションが動いている状態では、一度アプリケーションを閉じて2度目に立ち上げたときに、以前のLocalConnectionを検出するのです。
これを防ぐためには、アプリケーションを閉じる際に必ず明示的にConnectionを閉じればいいのですが、Flashではそれができない場合も多いので、LocalConnectionのコールバックメソッドがあるかどうかで判定することになるかと。
そのうちこのサンプルにも手を入れます。

[2009/3/8追記]
上の通り、サンプルコード修正しました。
手抜きでdynamicクラスにしています。

フィルタ効果つきボタン

開発中に作った、簡単なカスタムコンポーネントの一例。
マウスオーバーやクリック時にGlowフィルタをかけるボタンです。
利用不可の時にはモノクロにします。
フリーのアイコンをボタンに利用する際なんかに重宝するかも。

<?xml version="1.0" encoding="utf-8"?>
<mx:Button xmlns:mx="http://www.adobe.com/2006/mxml"
  rollOver="filterByMouseEvent(event)"
  rollOut="filterByMouseEvent(event)"
  mouseDown="filterByMouseEvent(event)"
  mouseUp="filterByMouseEvent(event)">
  <mx:Script>
    <![CDATA[
      [Bindable]
      /**
       * glowEffectの色(デフォルトはグレー)
       */
      public var glowColor:uint = 0x999999;

      protected function filterByMouseEvent(e:MouseEvent):void{
        //ボタンへのフィルタをかける
        if(enabled){
          switch(e.type){
            //ロールオーバー
            case MouseEvent.ROLL_OVER:
            case MouseEvent.MOUSE_UP:
              filters = [new GlowFilter(glowColor,1)];
              break;
            //マウスダウン
            case MouseEvent.MOUSE_DOWN:
              filters = [new GlowFilter(glowColor,0.5)];
              break;
            default:
              filters = [];
              break;
          }
        }
      }

      protected override function commitProperties():void
      {
        super.commitProperties();
        if(!enabled){
          //モノクロ
          var mat:Array = [
            1/3, 1/3, 1/3, 0, 0,
            1/3, 1/3, 1/3, 0, 0,
            1/3, 1/3, 1/3, 0, 0,
            0, 0, 0, 1, 0
            ];
          filters = [new ColorMatrixFilter(mat)];
        }else{
          //とりあえず実際の動作では問題ない
          filters = [];
        }
      }
    ]]>
  </mx:Script>
</mx:Button>

粉々エフェクト

突然ですが、メインのブログをはてなから移行しようかと考えています。
新しいコンテンツはこちらを中心に書いていきますので、よろしく。

前回よりも業の深いブログ

クリックするとコントロールが粉々に粉砕される(!)デモをソース付きでアップしました。

内部のPopUpButtonを事前に閉じさせる

PopUpButtonが開いている間に親のパネルを最小化したい、というときに、そのままだとポップアップだけがその場に開いたままで都合が悪いことがあります。
とはいえパネルの中は複雑で、いちいちすべてのPopUpButtonを管理していられない・・・。

そんな状況に陥ったので解決策を探していたんですが、一応

  • 影響の出ない座標で事前にMouseDownイベントをディスパッチする

という方法で解決しました。
PopUpButtonのMouseDownOutSideイベントが働くので、きれいに閉じてくれます。