Flex 3の最近のブログ記事

メモリーリークの調査をしている中で、<mx:Style>タグ を使用したサブ画面をメイン画面からポップアップ表示すると、サブ画面を閉じてもインスタンスが1つ残るというメモリーリークパターンが見つかったので覚え書きしておきます。

【確認環境】
Flex Builder 3.0.2
Flex SDK 3.5

以下は、問題が起こっているサンプルソースです。

 サブ画面【PopWindow.mxml】 では<mx:Style>タグを記述し、対象コンポーネントの"styleName"プロパティでスタイルを参照しています

<?xml version="1.0" encoding="utf-8"?>

<mx:TitleWindow xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical"  horizontalAlign="center" verticalAlign="middle" width="400" height="300">
<mx:Style>
    .popLabel{
        color: #ff0000;
        font-size: 13;
        fontWeight: bold;
    }
</mx:Style>
<mx:Script>
    <![CDATA[
        import mx.managers.PopUpManager;
        
        private function onClickCloseBtn():void
        {
            PopUpManager.removePopUp(this);
        }
    ]]>
</mx:Script>
    <mx:Label text="下のボタンを押すとサブ画面が閉じます。" styleName="popLabel" />
    <mx:Button label="close" click="onClickCloseBtn()" />
</mx:TitleWindow>

 

 メイン画面【MainWindow.mxml】 ではサブ画面ポップアップ表示用のボタンを1つ用意します

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" horizontalAlign="center" verticalAlign="middle">
<mx:Script>
    <![CDATA[
        import mx.managers.PopUpManager;
        
        private function onClickHandler():void
        {
            var popWindow:PopWindow = PopWindow(PopUpManager.createPopUp(this, PopWindow, true));
            PopUpManager.centerPopUp(popWindow);        
        }
    
    ]]>
</mx:Script>
    <mx:Label text="下のボタンを押すとサブ画面をポップアップ表示します。" color="#ff0000" fontSize="13" fontWeight="bold" />
    <mx:Button label="Popup" click="onClickHandler()" />
</mx:Application>

サブ画面のポップアップ表示/閉じる を5回繰り返した後、ガベージコレクションをした結果をプロファイラーで見ると、 PopWindowのインスタンスが1つ残っています。
profile.pngのサムネール画像

ポップアップメモリーリークの解決策

解決策としては、以下の ①・② の2種類が候補に挙がり、結局②を採用しました。

① <mx:Style> に書いていた内容を該当コンポーネントのプロパティに追加する。
   <mx:Label text="下のボタンを押すとサブ画面をポップアップ表示します。" color="#ff0000" fontSize="13" fontWeight="bold" />
② Applicationファイルに外部ファイルとして作成したCSSを読み込ませ、それを参照させる。  

CSSファイル【Sample.css】

.popLabel{
    color: #ff0000;
    font-size: 13;
    fontWeight: bold;
}

メイン画面【MainWindow.mxml】

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" horizontalAlign="center" verticalAlign="middle">
<mx:Script>
    (略)
</mx:Script>

<mx:Style source="Sample.css"/>
    (略)
</mx:Application>

サブ画面【PopWindow.mxml】

<?xml version="1.0" encoding="utf-8"?>

<mx:TitleWindow xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical"  horizontalAlign="center" verticalAlign="middle" width="400" height="300">



<mx:Script>

    (略)

</mx:Script>

    <mx:Label text="下のボタンを押すとサブ画面が閉じます。" styleName="popLabel" />

    <mx:Button label="close" click="onClickCloseBtn()" />

</mx:TitleWindow>


 

Flex の DataGrid で、列の定義をするのに DataGridColumn を使いますが、それを、2つの DataGrid で同じ DataGridColumn を共有し、それぞれの DataGrid の列定義を同期させてみようという小ネタをご紹介します。
例として、上下に並べた DataGrid で、どちらかの列幅を変えたときにもう片方の列幅も一緒に変更されるといったことをやってみました。

以下がサンプルソースになります。

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" xmlns:local="*"
	creationComplete="creationCompleteHandler(event)">
	<mx:Script>
		<![CDATA[
			import mx.events.DataGridEvent;
			import mx.events.FlexEvent;
			
			private var dgColumns:Array = new Array();
			
			[Bindable]
			private var dgData1:Array = [
			{no:1, name:'氏名1-1', age:21, address:'住所1-1'},
			{no:2, name:'氏名1-2', age:22, address:'住所1-2'},
			{no:3, name:'氏名1-3', age:23, address:'住所1-3'},
			{no:4, name:'氏名1-4', age:24, address:'住所1-4'},
			{no:5, name:'氏名1-5', age:25, address:'住所1-5'}
			];
			[Bindable]
			private var dgData2:Array = [
			{no:1, name:'氏名2-1', age:31, address:'住所2-1'},
			{no:2, name:'氏名2-2', age:32, address:'住所2-2'},
			{no:3, name:'氏名2-3', age:33, address:'住所2-3'},
			{no:4, name:'氏名2-4', age:34, address:'住所2-4'},
			{no:5, name:'氏名2-5', age:35, address:'住所2-5'}
			];
			
			public function creationCompleteHandler(event:FlexEvent):void
			{
				dataGrid2.columns = dataGrid1.columns;
				
				dataGrid1.addEventListener(DataGridEvent.COLUMN_STRETCH, dgColumnStretch);
				dataGrid2.addEventListener(DataGridEvent.COLUMN_STRETCH, dgColumnStretch);
			}
			
			public function dgColumnStretch(event:DataGridEvent):void
			{
				if (event.currentTarget == dataGrid1)
					dataGrid2.invalidateList();
				else
					dataGrid1.invalidateList();
			}
		]]>
	</mx:Script>
	
	<mx:VBox>

	<mx:DataGrid id="dataGrid1" dataProvider="{dgData1}">
		<mx:columns>
			<mx:DataGridColumn headerText="NO" dataField="no"/>
			<mx:DataGridColumn headerText="氏名" dataField="name"/>
			<mx:DataGridColumn headerText="年齢" dataField="age"/>
			<mx:DataGridColumn headerText="住所" dataField="address"/>
		</mx:columns>
	</mx:DataGrid>
	
	<mx:DataGrid id="dataGrid2" dataProvider="{dgData2}">
	</mx:DataGrid>

	</mx:VBox>
</mx:Application>


上記ソースでは、2つ目の DataGrid の columns プロパティを定義せずに、アプリケーションの creationCompleteイベントのハンドラーで、dataGrid1 の columns プロパティをそのままコピーしています。

	dataGrid2.columns = dataGrid1.columns;

この時点で、上下の DataGrid の列定義が同じになり、上の DataGrid と 下の DataGrid の列定義(ヘッダー文字列、表示する内容など)が同じものになります。
このままですと、同じ DataGridColumn を使っているだけですので、初期表示の列定義をコピーしただけになります。

そこで、それぞれの DataGrid の columnStretch イベントのハンドラーメソッドで、お互いの invalidateList メソッドを呼んで描画をリフレッシュするようにします。

	private function dgColumnStretch(event:DataGridEvent):void
	{
		if (event.currentTarget == dataGrid1)
			dataGrid2.invalidateList();
		else
			dataGrid1.invalidateList();
	}

すると、上下の DataGrid の列幅が、常に同期するということができるようになります。
同じ DataGridColumn を見に行っているので当たり前といえば当たり前です。
ただ、列幅変更時にリフレッシュしない場合、表面上はそれぞれの DataGrid の列幅が違いますが、裏側では同じですので、再表示のタイミングでいきなり列幅が変わるという見方によっては不具合と捉えられる怪しい挙動をしてしまいます。

以下が、上記ソースで作成したFlexアプリケーションのサンプルになります。
上下それぞれの DataGrid の列幅を変更して、実際の動作を試してみてください。

最後に、DataGrid の columns プロパティは Arrayクラスですが、値を取得または設定するとき、元データの一連のエレメントを取り出して、新しい配列で取得または設定する sliceメソッドを使用しています。

そのため、列の順序を変更した場合は、上記のやり方だけですと上下の DataGrid で別々の列順序になってしまいます。

列の順序を変更した場合は、DataGrid の headerShift イベントのハンドラーメソッドなどで、columns の順序も考慮した同期を取るようにしたほうが良いと思います。

データグリッドのアイテムレンダラーを使用していて、コンテナを使用しているカスタムコンポーネントでも、TABキーを押下して順々に各コントロールにフォーカスが移動できれば、慣れたユーザーなら操作が便利になるかと思います。

例えば、VBoxコンテナに TextInput を2つ並べたカスタムコンポーネントのアイテムレンダラーの場合、TABキーを押下したときに下記のような感じでフォーカスが移動するイメージです。

tab-itemrenderer.jpg 画像1.TABキー押下時のフォーカス移動のイメージ

もっとスマートな方法もあるかと思いますが、とりあえずアイテムレンダラーに使うカスタムコンポーネントに以下のカスタマイズを加えることで自分がやりたいことを実現しました。

※今回の例では、フォーカス移動に焦点をあてているので、データを表示するアイテムレンダラーのカスタムコンポーネントとしては不完全ですのでご注意ください。

MXML

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute"	xmlns:local="*">
	<mx:Script>
		<![CDATA[
			[Bindable]
			private var dgData:Array = [
			{no:1, name:'氏名1', age:21, address:'住所1'},
			{no:2, name:'氏名2', age:22, address:'住所2'},
			{no:3, name:'氏名3', age:23, address:'住所3'},
			{no:4, name:'氏名4', age:24, address:'住所4'},
			{no:5, name:'氏名5', age:25, address:'住所5'}
			];
		]]>
	</mx:Script>
	<mx:DataGrid dataProvider="{dgData}" editable="true">
		<mx:columns>
			<mx:DataGridColumn headerText="列1" dataField="no" editable="false"/>
			<mx:DataGridColumn headerText="列2" rendererIsEditor="true" editorDataField="">
				<mx:itemRenderer>
					<mx:Component>
						<local:TabMove verticalScrollPolicy="off" horizontalScrollPolicy="off"/>
					</mx:Component>
				</mx:itemRenderer>
			</mx:DataGridColumn>
			<mx:DataGridColumn headerText="列3" rendererIsEditor="true">
				<mx:itemRenderer>
					<mx:Component>
						<local:TabMove verticalScrollPolicy="off" horizontalScrollPolicy="off"/>
					</mx:Component>
				</mx:itemRenderer>
			</mx:DataGridColumn>
		</mx:columns>
	</mx:DataGrid>
</mx:Application>

このMXMLは、下記のカスタムコンポーネントを実装しているだけです。注意点は↓の記事と同じになりますので参照してください。

[Flex]データグリッド内の単純なアイテムレンダラーでTABキーを押下してフォーカスを移動する

 

カスタムコンポーネント

package
{
	import flash.accessibility.AccessibilityProperties;
	import flash.display.DisplayObject;
	import flash.display.DisplayObjectContainer;
	import flash.display.LoaderInfo;
	import flash.display.Sprite;
	import flash.display.Stage;
	import flash.events.Event;
	import flash.events.FocusEvent;
	import flash.geom.Point;
	import flash.geom.Rectangle;
	import flash.geom.Transform;
	import flash.ui.Keyboard;
	
	import mx.containers.VBox;
	import mx.controls.TextInput;
	import mx.controls.listClasses.BaseListData;
	import mx.controls.listClasses.IDropInListItemRenderer;
	import mx.controls.listClasses.IListItemRenderer;
	import mx.managers.IFocusManager;
	import mx.managers.IFocusManagerComponent;
	import mx.managers.ISystemManager;

	public class TabMove extends VBox
	implements IListItemRenderer, IDropInListItemRenderer, IFocusManagerComponent
	{
		public function TabMove()
		{
			super();
		}
		
		/** テキストインプット1 */
		private var text1:TextInput = null;
		/** テキストインプット2 */
		private var text2:TextInput = null;

		public var text:String = "";

		//----------------------------------
		//  listData
		//----------------------------------
		
		private var _listData:BaseListData;

		[Bindable("dataChange")]
		[Inspectable(environment="none")]
		public function get listData():BaseListData
		{
			return this._listData;
		}
		
		public function set listData(value:BaseListData):void
		{
			this._listData = value;
		}
		
		override protected function createChildren():void
		{
			super.createChildren();
			
			this.text1 = new TextInput();
			this.text1.addEventListener(FocusEvent.KEY_FOCUS_CHANGE, keyFocusChangeHandler);
			addChild(this.text1);
			
			this.text2 = new TextInput();
			addChild(this.text2);
		}
		
		private function keyFocusChangeHandler(event:FocusEvent):void
		{
			if (event.keyCode == Keyboard.TAB &&
				!event.isDefaultPrevented())
			{
				event.preventDefault();
				
				if (event.currentTarget == this.text1)
				{
					var fm:IFocusManager = this.focusManager;
					fm.setFocus(IFocusManagerComponent(this.text2));
				}
			}
		}

		override public function setFocus():void
		{
			var fm:IFocusManager = this.focusManager;
			if (IFocusManagerComponent(this.text1) != fm.getFocus() &&
				IFocusManagerComponent(this.text2) != fm.getFocus())
				fm.setFocus(IFocusManagerComponent(this.text1));
		}
	}
}

上記の例ではコンテナ内のコンポーネントは2つですが、ソースを工夫することで3つでも4つでも問題は無いと思います。

TABキー押下時にカスタムコンポーネントでフォーカス移動を実現させるために、カスタムコンテナに IFocusManagerComponent インタフェースを実装します。
これによりDataGrid コンポーネントで、アイテムレンダラー間のフォーカスの移動をやってくれます。

次に、コンテナ内の子コンポーネント作成時に、TABキー押下時に次のフォーカスをあてるコンポーネントが、同じカスタムコンポーネント内の場合は、KEY_FOCUS_CHANGE イベントを追加しておきます。

今回の例では、上段の TextInput にイベントを追加しています。

this.text1.addEventListener(FocusEvent.KEY_FOCUS_CHANGE, keyFocusChangeHandler);


そして、そのイベントリスナーメソッドで、TABキーを押されたときはイベントをキャンセルし、次にフォーカスを移動させたいコンポーネントに対してフォーカス移動を行います。

				event.preventDefault();
				
				if (event.currentTarget == this.text1)
				{
					var fm:IFocusManager = this.focusManager;
					fm.setFocus(IFocusManagerComponent(this.text2));
				}

こうしないと、TABキーを押下したときに、カスタムコンポーネント内ではなく、DataGrid のカスタムコンポーネント間でフォーカスが移動してしまいます。

tab-itemrenderer2.jpg

画像1.カスタムコンポーネント間でフォーカスが移動してしまう例

次に、SetFocus メソッドをオーバーライドします。

		override public function setFocus():void
		{
			var fm:IFocusManager = this.focusManager;
			if (IFocusManagerComponent(this.text1) != fm.getFocus() &&
				IFocusManagerComponent(this.text2) != fm.getFocus())
				fm.setFocus(IFocusManagerComponent(this.text1));
		}

DataGrid コンポーネントは、TABキーなどでフォーカスを移動するときはアイテムエディターコンポーネント自体にフォーカスを移動します。
今回の例では VBox コンテナにフォーカスが移動してしまうわけです。
使う側からすれば、VBox コンテナにフォーカスが移動しても困るので、ここの処理で、フォーカスがコンテナの子コンポーネント以外のときは、一番最初にフォーカスを当てたいコンポーネントにフォーカスを移動させています。

ここまでで、大体やりたいことが出来るようになりました。

ただし、アイテムエディターでは、Enterキーを押したときは横方向ではなく下方向にフォーカスが移動していきます。
今回の方法では、Enterキーについてはカスタムコンポーネント間でフォーカスが移動してしまいます。
業務での優先順位が低いこともあり、そこはただいま調査中です。

データグリッドでアイテムエディターを使用すると、TABキーを押下したときに次のコントロールに遷移してくれます。

しかし、アイテムレンダラーを使用すると、TABキーを押下しても次のコントロールに遷移してくれません。

どうすれば実現できるかを調べたところ、単純なアイテムレンダラーであれば、アイテムエディターのようにTABキーを押下したときに次のコントロールに遷移させる方法は、簡単なプロパティの設定だけで実現できることがわかりました。

以下、その方法を簡単にご紹介します。

①データグリッドコントロールの editable プロパティを trueにします。
<mx:DataGrid editable="true">

※このプロパティを true に設定すると、データグリッド内の各列に アイテムエディターが作成されるので、不要な列は DataGridColumnの editable プロパティを false に設定します。

②アイテムエディターのインスタンスが必須ですので、アイテムレンダラーとアイテムエディターが同じ場合は、DataGridColumnの rendererIsEditor プロパティを false に設定します。
    <mx:DataGridColumn headerText="列2" dataField="name"
        rendererIsEditor="true" itemRenderer="mx.controls.TextInput"/>

簡単なサンプルを下記に示します。

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" xmlns:local="*">
	<mx:Script>
		<![CDATA[
			[Bindable]
			private var dgData:Array = [
			{no:1, name:'氏名1', age:21, address:'住所1'},
			{no:2, name:'氏名2', age:22, address:'住所2'},
			{no:3, name:'氏名3', age:23, address:'住所3'},
			{no:4, name:'氏名4', age:24, address:'住所4'},
			{no:5, name:'氏名5', age:25, address:'住所5'}
			];
		]]>
	</mx:Script>
	
	<mx:DataGrid dataProvider="{dgData}" editable="true">
		<mx:columns>
			<mx:DataGridColumn headerText="列1" dataField="no" editable="false"/>
			<mx:DataGridColumn headerText="列2" dataField="name"
				rendererIsEditor="true" itemRenderer="mx.controls.TextInput"/>
			<mx:DataGridColumn headerText="列3" dataField="address"
				rendererIsEditor="true" itemRenderer="mx.controls.TextInput"/>
		</mx:columns>
	</mx:DataGrid>
</mx:Application>

単純な アイテムレンダラー(コンテナが無いコンポーネント)であれば、これだけです。一見したら何もしていないようにも見えます(^^;)

この方法は、FXUGのフォーラム(⇒ここ)で見つけました。

しかし、コンテナがあるアイテムレンダラー(1つのアイテムレンダラー内に複数のコンポーネントがある)のように、まずアイテムレンダラー内だけでフォーカス遷移し、最後のコンポーネントでTABキーを押下されたときに、次のアイテムレンダラーに移るといったような複雑なことを実現するには、少々アイテムレンダラーに使用するコンポーネントをカスタマイズする必要があります。

それについては、次回ご紹介します。
 

ブラウザのタイトル設定方法を紹介します。
設定方法は難しくないのですが、いくつか注意すべき点が存在します。


mxmlの<Application>のpageTitleプロパティを使用すると、
ブラウザのタイトル部分に表示するタイトルを設定する事が可能です。

■メインアプリケーション(mxml)
 

<?xml version="1.0" encoding="utf-8"?>

	



実際にswfコンテンツがのっているhtmlは、ビルド時に
index.template.htmlを元に生成されますが、この時<Application>のpageTitleの値を参照し
htmlの<titlle>に設定を行います。


■index.template.html

<title>${title}</title>

  ↓ビルドでhtmlが生成される

■XXXXX.html

<title>mxmlで設定したタイトル名</title>


わざわざアプリケーション側で設定しなくても、index.template.htmlのtitleを直接変更すれば
シンプルな設定となりますが、実はこの方法ではtitleに日本語を設定してしまうと
タイトルが文字化けしてしまいます。



■index.template.html

<title>テストページ</title>

  ↓
■XXXXX.html

<title>繝�繧ケ繝医�壹�シ繧ク</title>


その為、タイトルの設定は<Application>のpageTitleプロパティで設定する必要がありますが、更に注意点が存在します。

通常、自動生成されるhtmlのエンコードはUTF-8ですが、
エンコーディングをUTF-8以外に変更した場合、title部分が文字化けしてしまいます。

文字化けしてしまった部分を直接修正する方法もアリですが、
ビルドの度に生成されるファイルなので、都度修正が必要になってしまいます。
(デグレード等を引き起こす可能性があるため危険です)

そこでオススメなのが、JavaScriptでタイトルを設定する方法です。
この方法ならば文字化けも解消でき、かつ動的にタイトルの設定を行う事が可能となります。
以下がサンプルです。
■アプリケーション側

// ウインドウの切り替え
ExternalInterface.call('setTitle',viewName);


■html側(JavaScript)

<script language="JavaScript" type="text/javascript">

	// windowのタイトルを設定
	function setTitle(ttl)
	{
		window.document.title = ttl;
	}
</script>

html側(index.template.htmlに実装)にタイトル設定用のJavaScriptを用意し タイトル設定を行うタイミング時、画面側でExternalInterfaceを使用しJavaScriptの呼び出しを行います。 この方法を使用した場合、動的にタイトルの値を変更する事も可能となるのでオススメです。

<<前のページ 1234

このブログについて

このブログは吉祥寺にあるブレインチャイルド株式会社の社員で投稿しています。
業務ではまってしまったことや発見したこと。
自分達で新たに学習してみようと思って勉強し始めたことなどを綴っています。
こんな社員が働いているブレインチャイルドに興味がわいててきたなら、是非お問い合わせください。
会社案内
求人案内
先輩のコメント