カテゴリー別アーカイブ: Flex 2

mx.states と戯れる #02

少し間が空いてしまいましたが、mx.states 話の続きです。
今回は、前回のエントリーで説明した内容のサンプルを用意しましたのでご覧ください。

StateSampleA (画面)
http://labs.taiga.jp/flex3/StateSampleA/

StateSampleA (ソース)
http://labs.taiga.jp/flex3/StateSampleA/srcview/

参考 URL
mx.states.AddChild (Flex 3)
http://livedocs.adobe.com/flex/3_jp/langref/mx/states/AddChild.html

F-site 発表資料

先日 (2/23) の F-site にお越しくださった皆様、ありがとうございました。
それなりに支度したつもりではありましたが、プレゼンについては、不手際や反省すべき点が多々あったと思います。申し訳ありません。

お詫びに…というわけではありませんが、Flex のソースを含めた発表資料を公開します。
下記 URL をご覧ください。

スライド [*1]
http://labs.taiga.jp/presentationMaterial/F-site/fsite023demo01/slide/

スライド(ソース) [*1]
http://labs.taiga.jp/presentationMaterial/F-site/fsite023demo01/slide/srcview/

野中先生ペンギン サンプル
http://labs.taiga.jp/presentationMaterial/F-site/fsite023demo01/penguinSample/

野中先生ペンギン サンプル(ソース)
http://labs.taiga.jp/presentationMaterial/F-site/fsite023demo01/penguinSample/srcview/

設計書サンプル
http://labs.taiga.jp/presentationMaterial/F-site/fsite023demo01/sampleDesignSpecifications.zip

*1
会場でご覧いただいたスライドと、フォントが異なります。
ご了承ください。

mx.states と戯れる #01

個人的な思いこみではありますが、ステートに馴染めないというか嫌いな方が多いのではないかと思い、回数を分けてまとめることにしました。もちろん MXML を使用しない方針で進めていきたいと思います。

今回は、まず基本説明から

  • UIComponent クラスには、states という配列プロパティが用意されており、この配列プロパティに対してステート情報( State クラスのインスタンス)を定義(追加)します。
  • ステート情報は、UIComponent クラスを継承しているアプリケーション/コンポーネントクラスのルート (this) に対して定義(追加)できます。
public function RootMXMLClass()
{
    …
    this.states = [StateInstanceA, StateInstanceB, StateInstanceC, … ];
}
  • ステート情報の内容は、State.overrides という配列プロパティに定義(追加)します。
  • State.overrides プロパティには、AddChild, RemoveChild などの mx.states パッケージクラスのインスタンスを定義(追加)することができます。( State, Transition クラスを除く)
  • State.name プロパティで定義した名前が RootMXMLClass.currentSate プロパティで利用されます。
public function RootMXMLClass()
{
    …
    var lState    :State    = new State();
    var lAddChild :AddChild = new AddChild();
    var lButton   :Button   = new Button();
    …
    lAddChild.target = lButton;
    lState.name = 'hoge';
    lState.overrides = [lAddChild, … ];
    this.states = [lState, … ];
}

以上のポイントさえ抑えてしまえば、AS で書いた方が可読性が高まり(個人的な意見ですが)、動的なステートが作成可能になるというメリットがオマケで付いてきます。 ただし、デザイナーと分業するようなプロジェクトでの使用はオススメできないという欠点もあるので、注意が必要です。(デザインビューでステートの確認ができなくなるため)

JVM 指定の罠

Flex Builder の話です。
Flex Builder は、デフォルトのとき付属の JRE1.4 の JVM 上で実行されますが( Flex Builder 2 の場合)、使用 JVM を変更するときには注意が必要です。

JVM を変更する方法には二通りあり、設定ファイル(スタンドアロン版の場合 FlexBuilder.ini 、プラグイン版の場合 eclipse.ini )に -vm オプションを追記する方法と、FlexBuilder.exe (eclipse.exe) のショートカットを作成して、ショートカットに引数を追記して渡す方法がありますが、困ったことに、それぞれの方法で構成情報に違いが生じてしまいます

図 01. FlexBuilder.ini (eclipse.ini) ファイルで -vm を指定したとき
図 01. FlexBuilder.ini (eclipse.ini) ファイルに -vm を指定したとき
なぜか -vm が二回定義されるだけでなく、eclpise.vm がデフォルトの JVM を指しています。これは困りますね。

図 02. FlexBuilder.exe (eclipse.exe) のショートカットを作成して、引数を渡して起動したとき
図 02. FlexBuilder.exe (eclipse.exe) のショートカットを作成して、引数を渡して起動したとき
ini ファイルで設定したときと違い、eclpise.vm が指定通りに定義されています。

結論:JVM を変更する場合には、ini ファイルで設定するのではなく、ショートカットに引数を渡して設定しましょう。

参考 URL

RadioButtonGroup のキーフォーカス対策

RadioButtonGroup をラップしたカスタムコンポーネントを、同一画面で複数回再利用するとき、ちょっとした工夫を施さないと、Tab キー操作によるキーフォーカス遷移が正しく機能しません。

どうやら RadioButtonGroup の id プロパティは、オブジェクトの階層とは関係なく、アプリケーション内において一意の名前でなければならない(重複してはいけない)ようです。

しかも RadioButtonGroup はなかなかの曲者で、id プロパティを定義する場合、MXML タグに直接書くことしかできません。id=”{hogehoge}”… のように、波括弧で変数を参照することすらできません。( AS では一切定義できません)

そこで、かなり強引な手段ですが、回避用のサンプルとソースを用意しましたので、下記 URL をご覧ください。

RadioButtonFocusSample (画面)
http://labs.taiga.jp/flex2/RadioButtonFocusSample/

RadioButtonFocusSample (ソース)
http://labs.taiga.jp/flex2/RadioButtonFocusSample/srcview/

簡単にポイントを箇条書きで説明します。

  • RadioButtonGroup タグは使用しません。
  • グルーピング処理は、RadioButton の groupName プロパティを使用します。
  • RadioButtonGroup のイベントを使用したい場合は、RadioButtonGroup インスタンスを作成し、groupName 定義済みの RadioButton の group プロパティを代入します。

trace() 話

ご存じの方が多いと思いますが、Flash コンテンツ内で使用している trace() の内容は、FlashTracer などのブラウザ拡張機能を使えば、簡単に丸見えになってしまいます。

Flash には、パブリッシュ(ビルド)の設定に 「 Trace アクションを省略」 という設定項目があり、この機能を使うことによって、パブリッシュ時に ActionScript のコード内にある trace() をすべて削除してから swf ファイルを生成することができます。ですが、FlexBuilder には、この機能に該当するオプションがありません。( mxmlc のオプションにもありません)

『 Flex アプリの開発では、trace() を使わない 』 という選択肢がベストなのですが、trace() を使用せざる得ない場合に、少しでもリスクを削減したいという方のために、簡単なクラスを作成しました。

package
{
    /**
     * FlexTrace.as
     * @author taiga
     * カスタム trace() クラス
     */
    public class FlexTrace
    {
        /**
         * @private
         */
        private static var mFlag:Boolean;
        /**
         * enabled
         * 書き込み専用プロパティ trace() の有効(true)/無効(false)
         */
        public static function set enabled(aFlag:Boolean):void
        {
            mFlag = aFlag;
        }
        /**
         * output
         * @param args trace() させる要素
         * enabled=true のとき、trace() を実行
         */
        public static function output(...args):void
        {
            if(!mFlag)
                return;
            var i:uint;
            var s:String = '';
            for(i = 0; i < args.length; i++)
                s += args[i].toString() + ' ';
            trace(s);
        }
    }
}

使い方ですが、適当にインポートしたあと、FlexTrace.output(‘hoge’, ‘fuga’); という具合に、trace() と同様に使うだけです。カンマ区切りで好きなだけ引数を追加できます。( ActionScript 3.0 では、… (rest) パラメータを使用することにより、任意の数のカンマ区切りのパラメータを受け入れる配列パラメータを指定できます)

あとは、FlexTrace.enabled の値を true か false に設定するだけで、trace() の使用/不使用を一括制御することができます。「 Trace アクションを省略」 のように完全にコードを削除できるわけではありませんが、緩和処置としては許せる範囲かなと思いますので、使いたい方はご自由にどうぞ。

ちなみに、私は Flex アプリ開発の仕事では、trace() を一切使用していません。

Repeater で DataGrid もどき

実は、つい最近まで Repeater コンポーネントの存在を知らなかった taiga です。こんにちは。

Repeater コンポーネントの存在を知ってドキュメントを読んだとき、サンプルコードには、<mx:Script> タグを使用していたり、MXML 内で波括弧を使用して AS を埋め込んでいたりと、あまり模範にしたくない断罪サンプルしか掲載されてなかったので、自分好みのサンプルを作成しました。カスタムコンポーネントを Repeater で繰り返し処理させるサンプルです。

RepeaterSample (画面)
http://labs.taiga.jp/flex2/RepeaterSample/

RepeaterSample (ソース)
http://labs.taiga.jp/flex2/RepeaterSample/srcview/

ソースの説明をする前に、Repeater コンポーネントを知らない人向けに簡単に説明しておきます。
Repeater コンポーネントは、実行時に動的 or 静的な配列に基いて UI コンポーネントを繰り返し処理するコンポーネントです。Repeater.dataProvider = hogeArray 的な使い方ができるので、 ComboBox や DataGrid を使ったことのある人であれば、容易に理解できると思います。他にも、for 文で new して addChild() して…といった AS コーディングの手間を省くことができるというメリットもあります。

ソースの説明に話を移しますが、Repeater コンポーネントを持つ親コンポーネント(RepeaterSample.mxml, ApplicationBase.as)では、特別な処理を行っていません。 今回注目したいのは、繰り返し表示されるカスタム子コンポーネントクラス (SubRowBase.as) の中身です。

public function SubRowBase()
{
    addEventListener(FlexEvent.INITIALIZE, initializeHandler, false, 0, true);
}
private function initializeHandler(event:FlexEvent):void
{
    var lColor:uint = (0 == repeaterIndex % 2) ? 0xffffff : 0xeeeeee;
    mComponent = event.currentTarget as SubRow;
    with (mComponent) {
        compCheckBox.addEventListener(Event.CHANGE, changeHandler, false, 0, true);
        compCheckBox.selected = repeater.currentItem.check;
        comp_0_Label.text     = repeater.currentItem.labelFirst;
        comp_1_Label.text     = repeater.currentItem.labelSecond;
        setStyle('backgroundColor', lColor);
        setStyle('backgroundAlpha', 0.5);
    }
}

Repeater コンポーネントにネストされた子コンポーネントは、親コンポーネントから渡された配列の値を、自分自身の repeater.currentItem プロパティを使用して参照することができます。( repeater プロパティは、UIComponent クラスから派生したすべてのコンポーネントに実装されているプロパティです)

このとき注意しなければならないのは、FlexEvent.CREATION_COMPLETE イベント発行のタイミングで repeater プロパティを参照すると、null 参照になり例外エラーが発生するということです。実際に FlexEvent.INITIALIZE イベントを addEventListener している理由は、この例外エラーを回避するためなのです。

次に、子コンポーネント内のデータの変更方法について、CheckBox の Event.CHANGE イベントリスナー関数を例に説明します。

private function changeHandler(event:Event):void
{
    var lRowIndex:int     = repeaterIndex;
    var lSelected:Boolean = event.currentTarget.selected;
    repeater.dataProvider[lRowIndex].check = lSelected;
}

repeaterIndex プロパティは、親コンポーネントの Repeater.dataProvider プロパティ内にある、子コンポーネントのインデックスを取得することができます。( repeaterIndex プロパティも、repeater プロパティと同じく、UIComponent クラスから派生したすべてのコンポーネントに実装されているプロパティです。余談ですが、サンプル画面のストライプの表現でも、repeaterIndex プロパティを使用して、インデックスが偶数か奇数かを判別して背景色を切り替える処理を行っています。)

そして、repeater.dataProvider プロパティを使用することによって、親コンポーネントの Repeater.dataProvider プロパティを参照できるので、子コンポーネントのデータが更新されたときには、repeater.dataProvider[repeaterIndex] に対してデータの変更(上書き)処理を行えばよいわけです。

以上のポイントさえ抑えておけば、Repeater コンポーネントを使用したときでも、MXML コーディングはシンプルかつ最小限に留め、画面のコントロールはすべて AS クラスにまとめる…という、ステキアーキテクチャが割と簡単にできあがるハズです。

DateField 小ネタ

時分秒 (hh:mm:ss) の値を含んだ Date オブジェクトを selectedDate プロパティにセットすると、アイコンをクリックしたとき表示されるカレンダーに、セットした日にち (DD) がフォーカスされない…という不具合が発生します。

この現象、Flex 3 では解消されていますが、時間情報を含んだ日付情報を扱うような Flex 2 アプリを作るときには、注意が必要です。(余談ですが、fla ファイル (AS 2.0) で扱える DateField コンポーネントでは、上記現象は発生しません)

以下検証コード

<?xml version="1.0" encoding="utf-8"?>
<!-- DateFieldShotTest -->
<mx:Application
    xmlns:mx         = "http://www.adobe.com/2006/mxml"
    layout           = "vertical"
    creationComplete = "init();">

    <mx:Script>
        <![CDATA[
            private function dateOutput():void
            {
                traceText.text = String(comp_0_DF.selectedDate)+'\n'+
                                 String(comp_1_DF.selectedDate)+'\n'+
                                 String(comp_2_DF.selectedDate)+'\n'+
                                 String(comp_3_DF.selectedDate)+'\n'+
                                 String(comp_4_DF.selectedDate);
            }
            private function init():void
            {
                traceText.text = '';
                comp_0_DF.selectedDate = new Date();
                comp_1_DF.selectedDate = new Date(2007, 0, 23);
                comp_2_DF.selectedDate = new Date(2007, 0, 23, 0, 0, 0);
                comp_3_DF.selectedDate = new Date(2007, 0, 23, null, null, null);
                comp_4_DF.selectedDate = new Date(2007, 0, 23, 12, 34, 56);
            }
        ]]>
    </mx:Script>

    <mx:Style>
        Label
        {
            font-weight: bold;
        }
    </mx:Style>

    <mx:VBox width="400" textAlign="left">

        <mx:HBox>
            <mx:DateField id="comp_0_DF" />
            <mx:Label text="new Date();" />
        </mx:HBox>

        <mx:HBox>
            <mx:DateField id="comp_1_DF" />
            <mx:Label text="new Date(2007, 0, 23);" />
        </mx:HBox>

        <mx:HBox>
            <mx:DateField id="comp_2_DF" />
            <mx:Label text="new Date(2007, 0, 23, 0, 0, 0);" />
        </mx:HBox>

        <mx:HBox>
            <mx:DateField id="comp_3_DF" />
            <mx:Label text="new Date(2007, 0, 23, null, null, null);" />
        </mx:HBox>

        <mx:HBox>
            <mx:DateField id="comp_4_DF" />
            <mx:Label text="new Date(2007, 0, 23, 12, 34, 56);" color="#CC0000" />
        </mx:HBox>

    </mx:VBox>

    <mx:TextArea  id="traceText" width="400" height="100" />

    <mx:HBox>
        <mx:Button label="trace" click="{dateOutput()}" />
        <mx:Button label="reset" click="{init()}" />
    </mx:HBox>

</mx:Application>

どう書く? itemRenderer (ComboBox)

前回のエントリーに引き続き、itemRenderer について書きます。

サンプルは以下の通り。

ItemRendererComboBoxSample (画面)
http://labs.taiga.jp/flex2/ItemRendererComboBoxSample/

ItemRendererComboBoxSample (ソース)
http://labs.taiga.jp/flex2/ItemRendererComboBoxSample/srcview/

まず、ItemRendererComboBoxSample.mxml の <mx:DataGridColumn> タグに着目します。

<!-- カスタム ComboBox -->
<mx:DataGridColumn
    headerText       = "colmn01"
    dataField        = "combo"
    itemRenderer     = "lib.SubComboBox"
    rendererIsEditor = "true"
    editorDataField  = "selectedIndex"
    editable         = "true"
/>

itemRenderer プロパティ値に、カスタム ComboBox クラスを定義しています。
rendererIsEditor プロパティと editorDataField プロパティを設定している箇所は、前回のカスタム CheckBox と同じです。
そして今回、editorDataField プロパティには、ComboBox のプロパティ名である slectedIndex を定義しています。

次に、SubComboBox.as に着目します。

package lib
{
    import flash.events.Event;
    import mx.collections.ArrayCollection;
    import mx.controls.ComboBox;
    import mx.controls.dataGridClasses.DataGridListData;
    /**
     * SubComboBox
     * itemRenderer 用 ComboBox サブクラス
     */
    public class SubComboBox extends ComboBox
    {
        /**
         * mValue
         * @private
         * クラス内部で保持する selectedIndex 値
         */
        private var mValue:uint;
        /**
         * SubComboBox
         * コンストラクタ
         */
        public function SubComboBox()
        {
            super();
            width        = 100;
            dataProvider = [ {label:'Label_A', data:10},
                             {label:'Label_B', data:20},
                             {label:'Label_C', data:30} ];
            addEventListener(Event.CHANGE, changeHandler, false, 0, true);
        }
        /**
         * tmpValue
         * @private
         * selectedIndex 値(内部保持用)
         */
        private function get tmpValue():uint
        {
              return mValue;
        }
        /**
         * @private
         */
        private function set tmpValue(aValue:uint):void
        {
            mValue = aValue;
            invalidateProperties();
        }
        /**
         * data
         * @param aValue itemRenderer の値
         * @private
         * プロパティ ComboBox の selectedIndex 値
         */
        override public function set data(aObject:Object):void
        {
            super.data = aObject;
            if (null != dataProvider)
            {
                selectedIndex = getLabelIndex( data[DataGridListData(listData).dataField] );
            }
        }
        /**
         * commitProperties
         * @private
         * Binding しているプロパティの更新
         */
        override protected function commitProperties():void
        {
            this.selectedIndex = getLabelIndex( data[DataGridListData(listData).dataField] );
            super.commitProperties();
        }
        /**
         * getLabelIndex
         * @param aValue dataProvider の値
         * @return selectedIndex 値
         * @private
         * 引数と ComboBox 内のデータを評価して、マッチしたら該当インデックスを返却
         */
        private function getLabelIndex(aValue:int):uint
        {
            var lArrayCollection:ArrayCollection = dataProvider as ArrayCollection;
            var lLength:Number = lArrayCollection.length;
            for (var i:uint = 0; i < lLength; i++)
            {
                if (lArrayCollection.getItemAt(i).data == aValue)
                {
                    return i;
                }
            }
            return 0;
        }
        /**
         * changeHandler
         * @param event Event オブジェクト
         * @private
         * ComboBox の change イベント
         */
        private function changeHandler(event:Event):void
        {
            var lArrayCollection:ArrayCollection = dataProvider as ArrayCollection;
            if (listData)
            {
                tmpValue = lArrayCollection.getItemAt(this.selectedIndex).data;
                data[DataGridListData(listData).dataField] = tmpValue;
            }
        }
    }
}

コンストラクタで ComboBox の label 値と data 値を定義します。

DataGrid をスクロールさせると ComboBox.commitProperties() メソッドが実行されます。
このとき ComboBox の label 値を変更していると初期表示状態に戻ってしまうので、この現象を回避するために ComboBox.commitProperties() メソッドをオーバーライドし、changeHandler() メソッドで保持しておいた data 値をセットさせる処理を追記しています。

DataGrid.dataProvider プロパティにセットする ComboBox の値ですが、ComboBox の data 値のみ使用します。( label は、ただのお飾りということです)
なぜかというと、本来 ComboBox.selectedIndex プロパティには、label 値と data 値を内包したオブジェクト(例: {label:”hoge”, data:”fuga”} )を代入しないと正しく動作しないのですが、DataGrid.dataProvider プロパティに例のようなオブジェクトをセットしてしまうと、DataGird のヘッダをクリックしてソート処理を行ったとき、DataGird がソート対象を判別できず、例外エラーが発生してしまうためです。

そして、正しく動作しない selectedIndex プロパティを正しく動作させるために、getLabelIndex() というメソッドを作成し、ComboBox の data 値と DataGrid.dataProvider プロパティにセットされた値を、総当たりでマッチングさせます。(賢い方法ではありませんが)

どう書く? itemRenderer (CheckBox)

今日も、みんなが大嫌いな(?) DataGird について書きます。

ソースの可読性や管理のしやすさを考慮した個人的な「宗教」に基づき、見た目に直接関与しない MXML タグは、可能な限り AS クラスに記述したいと思っています。
DataGrid のヘルプや Tips などで見かける <mx:itemRenderer> タグも、私の「断罪対象」にカテゴライズされるので、サンプルを用意しました。

ItemRendererCheckBoxSample (画面)
http://labs.taiga.jp/flex2/ItemRendererCheckBoxSample/

ItemRendererCheckBoxSample (ソース)
http://labs.taiga.jp/flex2/ItemRendererCheckBoxSample/srcview/

まず、ItemRendererCheckBoxSample.mxml の <mx:DataGridColumn> タグに着目します。

<!-- カスタム CheckBox -->
<mx:DataGridColumn
    headerText       = "colmn01"
    dataField        = "checkBoxValue"
    itemRenderer     = "lib.SubCheckBox"
    rendererIsEditor = "true"
    editorDataField  = "selected"
    editable         = "true"
/>

<!-- 標準の CheckBox -->
<mx:DataGridColumn
    headerText       = "colmn02"
    dataField        = "hoge"
    itemRenderer     = "mx.controls.CheckBox"
    rendererIsEditor = "true"
    editorDataField  = "selected"
    editable         = "true"
/>

itemRenderer プロパティ値に、カスタム CheckBox クラスを定義しています。(ついでに 2 カラム目には、標準の CheckBox を定義しました)
他には、rendererIsEditor プロパティと、editorDataField プロパティを設定していますが、どちらも CheckBox の編集に関わる大切なプロパティです。
rendererIsEditor プロパティ値を true と定義することによって、アイテムレンダラーがアイテムエディタであることを示します。( itemEditor プロパティを使用するより良いみたいです)
editorDataField プロパティには、アイテムエディタのプロパティ名が設定できるので、今回は CheckBox のプロパティ名である slected を定義しています。

その他 itemRenderer 周りの詳しい解説は、@IT に掲載されているクラスメソッド社の成瀬氏の記事に詳しく書かれているので、興味のある方は下記リンク先を見てください。


参考: 現場で使えるFlex実践テクニック(4)
Page1
Page2
Page3

次に、SubCheckBox.as に着目します。

/**
 * commitProperties
 * @private
 * Binding しているプロパティの更新
 */
override protected function commitProperties():void
{
    selected = mValue;
    super.commitProperties();
}
/**
 * updateDisplayList
 * @param aUnscaledWidth 親コンテナにより決定されるコンポーネントの幅
 * @param aUnscaledHeight 親コンテナにより決定されるコンポーネントの高さ
 * @private
 * CheckBox の再描画と再配置
 */
override protected function updateDisplayList(aUnscaledWidth:Number, aUnscaledHeight:Number):void
{
    super.updateDisplayList(aUnscaledWidth, aUnscaledHeight);
    var lObject:IFlexDisplayObject = mx_internal::currentIcon;
    if(lObject)
    {
        var lIconWidth :uint = lObject.width;
        var lIndent    :uint = (width - lIconWidth) / 2;
        lObject.x = lIndent;
    }
}

itemRenderer のコーディングお作法という趣旨から若干脱線しますが、commitProperties() メソッドと updateDisplayList() メソッドをオーバーライドして、dataProvider の内容を更新させたり再描画させたりしています。
この処理を記述しないと、2 カラム目に表示させている標準 CheckBox のような状態になってしまいます。

commitProperties() メソッドと updateDisplayList() メソッドについての詳しい解説は、仲間内で神ブログと呼ばれている前回よりは成長したブログさんの記事に詳しく書かれているので、興味のある方は下記リンク先を見てください。


参考:前回よりは成長したブログ
[Flex]asでカスタムコンポーネント(3)