2008年8月12日
何かと AS3 の Proxy クラスは使えないという声をよく目にします。
特に目立つのが他のクラスを継承できないこと。

例えば DisplayObject の Proxy を作ったとしても、元がProxyクラスなので addChild() が使えないとか。たぶんそんなの。

今回はその辺について、ちょっと思ったことを休み前にまとめとこうかと。

Progression使いたいけど、自前で拡張したクラスも組み合わせて、 なんか有効にいろんなライブラリに手を出せないかな。と思ったのが きっかけでして、そこで一番に思い当たるのはやっぱりProxyクラスだったわけです。

で最初はProxy使った方法をいろいろごにょごにょと考えてたんですけど、 結局 addChild() や removeChild() で指定するためには、どうしても DisplayObject を 参照しなきゃいけなくなって、それが参照できる時点でProxyの役目は破綻してしまうんです。

var parentObject:DisplayObjectContainer
var childObject:Proxy

parentObject.addChild(childObject.display);

例えば上記の場合、親のDisplayObjectにProxyクラスが内包するDisplayObjectを displayというプロパティ経由で入れ子にしようとしています。

でも、これをしてしまうと、以降、親DisplayObjectからは本来隠蔽しておきたいはずの 子DisplayObjectを参照できるようになってしまい、 Proxyを通さずに直接参照・変更ができてしまいます。

そもそも Proxy は dynamic ステートメントを付けておかないと 効果を発揮しません。

つまり、頑張って DisplayObjectProxy とか EventProxy とかを作っても、 それを継承して様々なクラスに派生させていくような作り方だと、 全部ダイナミッククラスになってしまって はちゃめちゃになってしまいます。

もしこういう使い方をしたいならば、 素直にDisplayObject のサブクラスを作って、中身をすべて 内包する他のオブジェクトに委譲したほうがいいかもしれません。

ただ、DisplayObject で Proxy クラスを作成した場合でも、 結局のところ addChild には若干問題が残ります。

Sprite や MovieClip なら、委譲先のオブジェクトを DisplayObjectContainer から作成した Proxy 用クラスのコンテナ内に追加しておけば addChild() 時に画面上に現れますが、TextField や Bitmap は親オブジェクトになれないので、これらを継承したProxyクラスは addChild しても画面に表示されません。
あくまで追加されるのは Proxy クラスだからです。

addChild はまだマシな方で、ADDEDイベント発生時に内部処理で置き換えが可能ですが、 removeChild 時にはどう頑張ってもクライアント側で一工夫しないといけなくなります。

結局いろいろ試した結果、僕は addChild の問題は放棄しました。
Proxyを末端の子オブジェクトとして捉える限り、この問題は解決しないように思えたからです。

ProxyならProxyらしくコンテナ側に回してやればいいんじゃないかと。


で、以下はDisplayNodeというProxyクラスを継承したサブクラスを作ってみた場合の一例です。

var rootNode:DisplayNode = new DisplayNode(new Sprite());
rootNode.child1 = new Sprite();
rootNode.child1.grandchild1 = new Shape();
rootNode.child1.grandchild2 = new Shape();
rootNode.child1.grandchild3 = new Sprite();
rootNode.child1.grandchild3.addChild(new Bitmap());
rootNode.child2 = new Sprite();
rootNode.child2.dummy = new Sprite();
rootNode.child3 = new Sprite();
delete rootNode.child2;

trace(rootNode);

出力結果
instance148 [object Sprite]
  child1 [object Sprite]
    grandchild1 [object Shape]
    grandchild2 [object Shape]
    grandchild3 [object Sprite]
        instance153 [object Bitmap]
  child3 [object Sprite]

何をやってるかというと、
private var _target:DisplayObjectContainer;

override flash_proxy function getProperty(name:*):* 
{
    var child:DisplayObject = _target.getChildByName(name);
    if (child is DisplayObjectContainer) 
        return new DisplayNode(child as DisplayObjectContainer);
    return child;
}

override flash_proxy function setProperty(name:*, value:*):void 
{
    if (value is DisplayObject)
    {
        value.name = name;
        _target.addChild(value);
    }
}
setProperty と getProperty を使って表示オブジェクトを 参照名で addChild していく。ってもの。
child2 は delete の実行によって、内部で removeChild してます。


このクラスの中身も一応参考までにアップしておきます。
こういう使い方もあるってことで。

DisplayNode


DisplayObjectを継承したProxyを作りたいときも、なんとなくそれっぽいものまでは作る事ができます。
要はダイナミッククラスな訳ですから、以下のような処理で各メソッドの呼び出し時の処理に手を加えることも。

package 
{
import flash.display.DisplayObject;
import flash.display.Sprite;
import flash.utils.describeType;

dynamic public class TestProxy extends Sprite
{

public function TestProxy(target:DisplayObject) 
{
    setProxyMethods(target);
}

public function setProxyMethods(target:DisplayObject):void
{
    var data:XML = describeType(target);
    var n:uint, i:uint, name:String;
    var methods:XMLList = data.elements("method");
    n = methods.length();
    for (i = 0; i < n; i++ )
    {
        name = methods[i].@name;
        if (!this.hasOwnProperty(name))
        {
            this[name] = function():*
            {
                var func:Function = target[arguments.callee.methodName];
                return func.apply(target, arguments);
            }
            this[name].methodName = name;
        }
    }
    
    var accessor:XMLList = data.elements("accessor");
    n = accessor.length();
    for (i = 0; i < n; i++ )
    {
        name = accessor[i].@name;
        if (!this.hasOwnProperty(name))
        {
            this[name] = function():*
            {
                if (arguments.length == 1) 
                    return target[arguments.callee.propName] = arguments[0];
                return target[arguments.callee.propName];
            }
            this[name].propName = name;
        }
    }
}

}// end of class

}// end of package
var proxy:TestProxy = new TestProxy(new TextField());
proxy.appendText("hoge");
trace(proxy.text());          // 出力 : hoge
trace(proxy.getTextFormat()); // 出力 : [object TextFormat]

テキストフォーマットを吐き出すSpriteとか。

ただし、getter と setter は動的に追加できないから、やっぱり完全なProxyクラスの代用は難しいと思う。
上の例の場合はプロパティへのアクセス用メソッドを用意していて、text("hoge") みたいにして使う感じ。

TestProxy


結局のところ TestProxy みたいに無尽蔵にメソッドを追加しちゃうようなものは
実際にはあんまり使いどころがない気がする。コードヒントとか出ないし。

現在 DisplayObject を継承した Proxy 用クラスで、比較的使用頻度の高いのは以下のようなクラス。
こういうクラスを予め用意しておくと、結構役に立ってくれたりします。

SpriteProxy

このクラスはこのままインスタンスを作ってもまったく意味のないクラスなので継承しないと使えません。


これって Proxy じゃなくて Decorator な気もするけど、
その辺そんなに詳しいわけでもないので気にしない。



今回は DisplayObject と一緒に使う事に絞ってみたけど、 Proxy クラス自体はアクセス方法がかなり自由なのでいろんな使い方ができると思います。
中でも MXML みたいな使い方は得意なんじゃないかと。

あんまり多用すると後が怖いですけども。

それにしても、とりとめのない文章だなあ。

2008年8月 4日
リンケージ設定JSFLコマンド
LinkageSettings.mxp

  • クラスパスに「/」が入力できなかったのを修正
  • 参照ボタンが正常に動作していなかったので修正
  • 「ActionScriptに書き出し」にデフォルトでチェックが入るように変更
  • 「ActionScriptに書き出し」にチェックが入っていないときは「クラスファイルを作成する」を選択できないように変更
上の2つはかなり致命的な欠陥だったのに今まで気づかなかった。
自分で使えよって話なんですが。

上記以外は変わってないですが、今回の修正で結構便利度アップしたと思いますよ。

例えば
/project
  /bin
    index.fla
  /src
    Main.as
上記のようなディレクトリ構成で、index.flaを開いて作業いていた状態で
ステージ上に配置されたシンボル「HomeButton」「WorksButton」「ContactButton」の3つを選択して
このコマンドを実行した場合、
クラスパス: ../src
クラス名: demo.buttons.*
基本クラス: *
上記の設定で「クラスファイルを作成する」にチェックを入れて実行すると
/project
  /bin
    index.fla
  /src
    Main.as
    /demo
      /buttons
        HomeButton.as
        WorksButton.as
        ContactButton.as
上記のようにファイルが作成されます。
クラスファイルの記述もちょいちょい設定できます。

使い方は前回の記事のままです。
不明点があればどうぞ遠慮なく聞いてください。

以前の記事で、複数のシンボルがどうのこうのと注意事項で書いてますが、
特に気にしなくても使えると思います。

インスタンス名にシンボル名を設定するJSFLコマンド
setInstanceName.mxp

  • 先頭の文字を小文字に変換する
これはシンボル名が「TestButton」だった場合、このコマンドを実行すると、
ステージ上のインスタンス名は「testButton」になります。

変更はこれだけですが、AS3では思いのほか便利になったと思います。

上の「リンケージ設定コマンド」と併用するとさらに便利。
クラス名はシンボル名が適用されて、インスタンス名は先頭文字だけ小文字
って状況はAS3では良くあるんじゃないでしょうか。

思い当たる人は是非使ってみてくださいませ。

--追記
なんかディレクトリ構成の例が変だけど気にしないで(汗

--更に追記
Embedタグの記述箇所が変だったのと、symbolパラメータの値が正しく入力されてなかったので
再修正してアップしました。
LinkageSettings.mxp

--しつこくも更に追記
クラス作成時、インスタンス変数を追加する際に、変数の型をすべて「DisplayObject」にしていましたが
クラスの割り当てがされているインスタンスについては、設定されたクラスを追加するようにしました。
指定クラスファイルが存在しない場合は基本クラスに設定されているクラスが指定されます。
さらに基本クラスがデフォルトである場合は DisplayObject が指定されます。
(基本クラスが Sprite や MovieClip 等である場合、JSFLでは判定できないため)

また、上記変更に伴い、import 文が長く(複数行)になる場合があるので
import 文はソートした状態で書き加えられるように変更しました。(可読性向上のため)

LinkageSettings.mxp

2008年7月29日
一部更新、追加があったのでdocsも更新しときました。
といっても、このライブラリ使ってんの僕だけだったりして。

Utils
http://www.libspark.org/browser/as3/Utils/src/org/libspark/utils

asdocs
http://www.libspark.org/htdocs/as3/utils/

ダウンロード
http://www.libspark.org/svn/as3/Utils/docs/utils_docs.zip

2008年7月18日
是非挑戦してみてください。
先攻・後攻は「SETTING」ボタンから変更できます。


ルール
  • 同じ行なら何個でも取れます。
  • 間を飛ばして取ることはできません。
  • 最後の1つになったら負け


FlashDeverop + FlexSDK で作ってます。
Flash IDE は今回使ってません。
FlexSDK だと、書き出しが早くてすばらしいですね。

2008年7月14日
AS3 で FLV のメタデータの取り方が分からずに、かなり無駄に時間を費やしてしまった。。。
FLV のメタデータは NetStream から onMetaData を使って取得できるんだけども、
この onMetaData の使い方がさっぱり分からなかったので忘れないようにメモ。
経過があまりにもアホくさすぎて絶対忘れないけども。

ビデオメタデータの使用
NetStreamオブジェクトの client プロパティを指定して、
指定したオブジェクトに onMetaData メソッドを定義しておくと使える。

この client プロパティの存在に気づかずに四苦八苦してた自分が情けない。

挙句の果てに、ここ見て自前で ByteArray からメタデータ取ろうとしてしまった。

ByteArray から取得した FLV の値を出力した一例。
// FLV Header
Signature = FLV
Version = 1
Flags = 5
Offset = 9

// FLV Stream
PreviousTagSize = 0

// FLV Tag
Type = 18
BodyLength = 224
Timestamp = 0
TimestampExtended = 0
StreamId = 0

// MetaData
videodatarate = 1000
audiocodecid = 2
canSeekToEnd = true
duration = 8.866
audiodatarate = 128
framerate = 30
width = 450
videocodecid = 4
height = 400
audiodelay = 0.038

アホですよ。まったく。

ここまでやって、ちゃんと取得できることに気づいた。
そもそも何で AS3 なのに NetStream はこんな実装なんだ。
腹立たしい。アホか!

一応以下にソース残しときます。
結果的に中途半端なものになってますが、
なんか消してしまうのも勿体ない気がしてしまって。(費やした時間が。)

public function readFLV(data:ByteArray):void
{
    // FLV Header
    var Signature:String = data.readUTFBytes(3);
    var Version:uint = uint8(data);
    var Flags:uint = uint8(data);
    var Offset:uint = uint32(data);
    
    // FLV Stream
    var PreviousTagSize:uint = uint32(data);
    
    // FLV Tag
    var Type:uint = uint8(data);
    var BodyLength:uint = uint24(data);
    var Timestamp:uint = uint24(data);
    var TimestampExtended:uint = uint8(data);
    var StreamId:uint = uint24(data);
    var Body:ByteArray = new ByteArray();
    data.readBytes(Body, 0, BodyLength);
    
    
    // Body(FLV Tag Type = 0x12 : AMF)
    var a:uint = uint8(Body);// ???
    var b:uint = uint8(Body);// ???
    
    // MetaData
    readString(Body);
    var metadata:Object = readMixedArray(Body);
    
    
    trace(Signature, Version, Flags, Offset);
    trace(PreviousTagSize);
    trace(Type, BodyLength, Timestamp, TimestampExtended, StreamId);
    
    // MetaData の中身
    for (var val:String in metadata) {
        trace(val + "=" + metadata[val]);
    }
    
}

public function  uint8(data:ByteArray):uint { return data.readByte(); }
public function uint16(data:ByteArray):uint { return data.readUnsignedShort(); }
public function uint24(data:ByteArray):uint { return data[data.position++]<<16 | data[data.position++]<<8 | data[data.position++]; }
public function uint32(data:ByteArray):uint { return data.readUnsignedInt(); }

public function readNumber(data:ByteArray):Number
{
    return data.readDouble();
}
public function readBoolean(data:ByteArray):Boolean
{
    return data.readByte() == 0x01;
}
public function readString(data:ByteArray):String
{
    var len:uint = data.readByte();
    return data.readUTFBytes(len);
}
public function readObject(data:ByteArray):Object
{
    return null;
}
public function readMixedArray(data:ByteArray):Object
{
    data.readByte();
    var length:uint = data.readInt();
    var res:Object = { };
    var value:*;
    var name:String;
    var type:uint;
    var len:uint = data.length;
    for (;; ) {
        var separator:uint = data.readByte();
        name = readString(data);
        type = data.readByte();
        
        if (type == 0x09) break;
        
        switch(type) {
            case 0x00 : { value = readNumber(data); break; } // Object
            case 0x01 : { value = readBoolean(data); break; } // Object
            case 0x02 : { value = readString(data); break; } // Object
            case 0x03 : { value = readObject(data); break; } // Object
            //case 0x04 : { break; } // Movie Clip
            case 0x05 : { value = null; break; } // Object
            case 0x06 : { value = undefined; break; } // Object
            //case 0x07 : { break; } // Reference
            case 0x08 : { value = readMixedArray(data); break; } // MixedArray
            case 0x0a : { value = readArray(data); break; } // Array
            case 0x0b : { value = readDate(data); break; } // Date
        }
        res[name] = value;
    }
    return res;
}
public function readArray(data:ByteArray):Array
{
    return null;
}
public function readDate(data:ByteArray):String
{
    return null;
}