プログラミングやる前に先お風呂はいっちゃいなさい

89月/090

試しにMXMLでメニューを作ってみた

NativeMenuをMXMLで定義した図

前置き

今、AIRでタスクトレイ常駐型のアプリを作ってます。
それでタスクトレイに常駐した際にタスクアイコンをクリックするとメニューが出るようにしたいと思って
処理を書いてたんですが、これが結構面倒くさい。

僕はある程度静的な処理は極力AS3じゃなくてMXMLで書きたいという、無類のMXMLフェチです。
なので、タスクトレイに常駐したアイコンを右クリックした時に出るNativeMenuをいちいちAS3で、addMenuだのaddSubmenuだのするのはあまり好きくないわけです。

そこで、MXMLでNativeMenuが定義できちゃうようなクラスを作っちゃおうと考えて調べてみました。

自分でMXMLタグを定義する方法

まず、カスタムMXMLタグを定義します。

いくつか方法はあります。

  1. カスタムコンポーネントを作る
    1. 既存のクラスを継承する
    2. UIComponentを継承して作る
    3. IUIComponentインターフェースを実装して作る(神の領域)
  2. 非ビジュアライズコンポーネントを作る
    1. 既存のクラスを継承する
    2. IMXMLObjectインターフェースを実装して作る

今回の場合、カスタムコンポーネントでNativeなメニューが作れないので、IMXMLObjectインターフェースを実装して定義することにしました。

仕様

Nativeなメニューを定義するMXML(イメージ)

<?xml version="1.0" encoding="utf-8"?>
<menu:MenuTree xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:menu="me.rkome.da.mxml.menu.*">
    <menu:menuList>
        <menu:MenuTreeItem label="表示" select="setVisible(true)" />
        <menu:Separator />
        <menu:MenuTreeItem label="終了" select="onTerminate()" />
    </menu:menuList>
</menu:MenuTree>

こんな感じにNativeなメニューをSystemTrayMenu.mxmlというファイルにして定義できればいいなあと思います。

MenuTreeというのがメニューのグループになっていて、このクラスの子要素としてMenuTreeItemを書くと
メニューのラベルとクリックされた時のイベント内容を定義できるようになっています。

labelプロパティが表示する文字で、selectイベントがメニューがクリックされた時のイベントです。

Separatorはメニューラベル間を区切る為のメニューアイテムです。

こんな感じのMXMLを書いて、MenuTreeのインスタンスを使って、getMenu()とか呼び出すだけで
MXMLのツリー構造の通りのNativeなメニューが作れたら便利じゃーんというのが今回の仕様です。

実際にMXMLタグを作ってみた

さて、ではMXMLタグを定義する方法です。

FlexにはIMXMLObjectっていうinterfaceがあって、これを実装したクラスはコンポーネントじゃなくてもMXMLで記述できるようになるようです。

mx.core.IMXMLObject (Flex 3.2)
http://livedocs.adobe.com/flex/3_jp/langref/mx/core/IMXMLObject.html
IMXMLObjectの実装について
http://livedocs.adobe.com/flex/3_jp/docs/wwhelp/wwhimpl/common/html/wwhelp.htm?context=LiveDocs_Parts&file=00001702.html

まあ、これくらいは無類のMXMLフェチの名乗るくらいなので知っています。確認です。

MXMLでたとえばListタグの子要素として直接dataProviderの情報を書けたりするあれ。普通なら<mx:dataProvider></mx:dataProvider>で囲まないといけないのに、なんでーとか思ってたら、どうやら[DefaultProperty]っていうメタデータタグがあるらしい。
あと、コンポーネントのビヘイビアを記述するとき、Flex Builderで出てくる候補リストにはFadeとかMoveとかエフェクト関連しか出てこないのは、これもどうも[ArrayElementType]っていうメタデータタグのおかげらしい。

メタデータタグか・・。じゅるり。

さて、メタデータタグフェチになる前にとりあえずMXMLでNativeMenuを書けるようにしてみましょう。

MenuTreeもMenuTreeItemもSparatorも同等に扱えるように、メニューになりえるものが持っている共通項、インターフェースを定義してみる。

IMenuTree.as

package me.rkome.da.mxml.menu
{
    import flash.display.NativeMenuItem;
   
    import mx.core.IMXMLObject;
   
    public interface IMenuTree extends IMXMLObject
    {
        function getMenu():NativeMenuItem;
        function get label():String;
    }
}

 labelは分かりますね。メニューに表示する文字列です。

getMenu()関数はNativeMenuItemを返します。もしNativeMenuItemを複数持つような場合はsubmenuプロパティを参照してもらうようにすれば
NativeMenuもNativeMenuItemもカバーできるかなーっていう適当仕様。

まあ、日本語よりもソースコードのが分かりやすいという病気の人の為に(僕の日本語が下手なだけ)どんどんコードを載せます。

MenuTree.as

package me.rkome.da.mxml.menu
{
    import flash.display.NativeMenu;
    import flash.display.NativeMenuItem;
   
    import mx.core.IMXMLObject;

    [DefaultProperty("menuList")]
    /**
     * Nativeなメニューを定義する為のMXMLタグ
     * このタグをルートにメニューを定義して、getNativeMenu()を呼び出すと
     * NativeMenuが生成される
     *
     * @author DarkOmeme
     *
     */

    public class MenuTree implements IMenuTree
    {
        public function MenuTree()
        {
        }

        public function initialized(document:Object, id:String):void
        {
        }
       
        private var _label:String;
        private var _menuList:Array;

        [ArrayElementType("me.rkome.da.mxml.menu.IMenuTree")]
        /**
         * メニューアイテム(子要素)
         * デフォルトのプロパティで、IMenuTreeが実装されたクラスの配列
         * @return
         *
         */
   
        public function get menuList():Array
        {
            return _menuList;
        }
        public function set menuList(value:Array):void
        {
            _menuList = value;
        }

        /**
         * メニューを生成するための関数
         * IMenuTree で定義されている
         * @return
         *
         */
           
        public function getMenu():NativeMenuItem
        {
            var menu:NativeMenu = new NativeMenu();
            for each (var item:IMenuTree in _menuList)
            {
                var menuItem:NativeMenuItem = item.getMenu();
                var subMenu:NativeMenu = menuItem.submenu;
                if (item.getMenu().submenu != null )
                {
                    menu.addSubmenu( subMenu, item.label );
                }
                else
                {
                    menu.addItem( item.getMenu() );
                }
            }
            var root:NativeMenuItem = new NativeMenuItem(_label);
            root.submenu = menu;
            return root;
        }

        /**
         * 自分が子要素を持たない時に呼ばれるメニュー生成用関数
         * @return
         *
         */
   
        public function getNativeMenu():NativeMenu
        {
            var item:NativeMenuItem = getMenu();
            return item.submenu;
        }

        /**
         * メニューのラベル
         * ルート要素の場合、この値に意味は持たない
         * (toolTipに表示されてもいいかもね)
         * IMenuTree で定義されている
         * @param value
         *
         */
   
        public function set label(value:String):void
        {
            _label = value;
        }
        public function get label():String
        {
            return _label;
        }
    }
}

デフォルトでタグの子要素がmenuListになるようになってます。

但し、mxmlファイルとして別ファイルで定義する場合、DefaultPropertyメタデータタグが利いてくれません。
ってことで、別のMXMLファイルで別途定義する場合はちゃんとmenuListというプロパティを指定してタグを書くようにしましょう。
(さっきのイメージは別のMXMLファイルで定義してある想定のMXMLソースでした)

MenuTreeItem.as

package me.rkome.da.mxml.menu
{
    import flash.display.NativeMenu;
    import flash.display.NativeMenuItem;
    import flash.events.Event;
    import flash.events.EventDispatcher;
   
    import mx.core.IMXMLObject;

    [Event(name="select", type="flash.events.Event")]
    /**
     * NativeMenuItemの役割を果たすクラス
     *
     * @author DarkOmeme
     *
     */

    public class MenuTreeItem extends EventDispatcher implements IMenuTree
    {
        public function MenuTreeItem()
        {
        }

        public function initialized(document:Object, id:String):void
        {
        }
       
        /**
         * submenuのない、NativeMenuItemを返す
         * クリックされた時のイベントをdispatchする為に、イベントハンドラを設定してます
         * @return
         *
         */
   
        public function getMenu():NativeMenuItem
        {
            var menuItem:NativeMenuItem = new NativeMenuItem(_label);
            menuItem.addEventListener(Event.SELECT, onSelect);
            return menuItem;
        }
       
        private var _label:String;

        public function set label(value:String):void
        {
            _label = value;
        }
        public function get label():String
        {
            return _label;
        }

        /**
         * クリックされたら、そのままdispatchEventでイベント通知
         * @param evt
         *
         */
   
        private function onSelect(evt:Event):void
        {
            dispatchEvent(evt);
        }
    }
}

 

クリックされた事を知る為に、selectイベントを通知するようにしてあります。
ここでもEventメタデータタグが大活躍。書いておくと、MXMLで書くときにFlex Builderで候補が出てくるようになります。

次回は絶対メタデータタグでなんかしたい。

Separator.as

 package me.rkome.da.mxml.menu
{
    import flash.display.NativeMenuItem;

    public class Separator implements IMenuTree
    {
        public function Separator()
        {
        }
       
        public function initialized(document:Object, id:String):void
        {
           
        }

        public function getMenu():NativeMenuItem
        {
            return new NativeMenuItem("", true);
        }
       
        public function get label():String
        {
            return null;
        }
    }
}

ほとんど、説明不要ですが。
getMenu()関数ではセパレータを返します。

と言う感じで、これで一通りMXMLタグを作りました。

これでイメージ通り書けるのか?!

その結果は、ソースを落として実際に確認してみてください。

で、このポストの冒頭のスクリーンショットが結果です。

NativeMenuをMXMLで定義した図

コードも一緒に収めたんで少し画像が大きいです。

MenuTreeApp ソースコー [ zip ]

ふいー、ようやく、一個記事がかけた。

182月/090

AIRでメッセンジャーみたいにポップアップするウィンドウを表示する

ちょっと、タスク管理系のアプリを作ってて、情報をポップアップさせたいことがあったので、作ってみました。 ここにソースを載せておきます。

PopupWindow.mxml

  1. <mx:Window xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" width="305" height="305" systemchrome="none" transparent="true" resizable="false" alwaysinfront="true" type="utility" showtitlebar="false" showstatusbar="false" showgripper="false" backgroundalpha="0.0" borderstyle="none">
  2.     <mx:states>
  3.     <mx:state name="shown">
  4.       <mx:setproperty target="{windowContainer}" name="height" value="300"></mx:setproperty>
  5.       <mx:setproperty target="{windowContainer}" name="visible" value="true"></mx:setproperty>
  6.       <mx:setproperty target="{windowContainer}" name="y" value="0"></mx:setproperty>
  7.     </mx:state>
  8.   </mx:states>
  9.   <mx:titlewindow id="windowContainer" layout="absolute" x="0" y="300" width="300" height="0" title="やることリスト" alpha="0" fontsize="15" cornerradius="15" showclosebutton="true" roundedbottomcorners="true" bordercolor="#99A0FF" borderalpha="1" backgroundcolor="#99A0FF" backgroundalpha="1" horizontalscrollpolicy="off" verticalscrollpolicy="off" creationcomplete="this.setCurrentState('shown')" close="this.close()">
  10.       <mx:moveeffect>
  11.         <mx:parallel>
  12.           <mx:move duration="500">
  13. </mx:move>
  14.           <mx:animateproperty property="alpha" duration="500" fromvalue="0" tovalue="1">
  15. </mx:animateproperty>
  16.         </mx:parallel>
  17.       </mx:moveeffect>
  18.       <mx:resizeeffect>
  19.         <mx:resize duration="500">
  20. </mx:resize>
  21.       </mx:resizeeffect>
  22.       <mx:textarea width="100%" height="100%" cornerradius="15" verticalscrollpolicy="off" horizontalscrollpolicy="off" text="{todoText}" wordwrap="false" enabled="false" selectable="false" editable="false" disabledcolor="#000000" backgrounddisabledcolor="#E4E5FF" paddingtop="5" paddingleft="5" paddingright="5" paddingbottom="5">
  23.       </mx:textarea>
  24.   </mx:titlewindow>
  25.   <mx:string id="todoText">- DarkOmeme 今日の予定 -
  26.  * 世界征服
  27.    * 資金集め
  28.    * 根回し
  29.    * 物資調達
  30.    * 核武装
  31.    * クーデター
  32.    * 新法制定
  33.    * 宣戦布告
  34.  * 回覧板を田中さんに持って行く
  35.  * 今週はごみの当番
  36.  * おじいちゃんのお見舞いにいく
  37.   </mx:string>
  38.  </mx:window>

 PopupWindowSample.mxml

 

  1. <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" creationcomplete="onCreationComplete()">
  2.   <mx:script>
  3.     <!--[CDATA[
  4.       import me.rkome.da.window.PopupWindow;
  5.       private function onCreationComplete():void
  6.       {
  7.         var popup:PopupWindow = new PopupWindow();
  8.         popup.open();
  9.         popup.move(Screen.mainScreen.bounds.width - popup.width - 5, Screen.mainScreen.bounds.height - popup.height - 5);
  10.         popup.addEventListener(Event.CLOSE, function (evt:Event):void {
  11.           NativeApplication.nativeApplication.exit();
  12.         });
  13.       }
  14.     ]]-->
  15.   </mx:script>
  16.  </mx:Application>

 

あまり、解説することも少ないと思いますが、ミソとなるのは、土台となるmx:WindowのsystemChromeをnoneにして透明にすることで、自由な形のウィンドウを作れます。

また、この透明のウィンドウ上に表示するオブジェクトにはエフェクトなんかもかけられるので、Flash的なカッコいいエフェクトがかけれてAIRのメリットを最大限に活かせるんじゃないでしょうか。無駄にカッコいい的な。

PopupWindow.mxmlの方では、type属性をutilityにすることで、タスクバーに表示されなくなります。情報を見せるポップアップウィンドウなので、この設定が有効だと思います。

Flash Playerを9.0.115以上にアップデートしてください
ソース