AIRでタスクトレイアプリケーションを作る

今日はAIRネタにする。
AIRで作るタスクトレイアプリケーション、てことで。


結構いろんなサイトに出てるから、目新しいものではないけど、
ウィジェットマネージャとして動作するアプリなら、タスクトレイに常駐してるっぽいので、
自分用の備忘録として残しておく。


タスクトレイアプリとして、出来なくちゃいけないこと。

  1. タスクバーからメインウィンドウを消す
  2. タスクトレイにアイコンを入れる
  3. タスクトレイを右クリックしたら終了メニューなどが出る

あと出来たらアイコンを簡単に変更したい。
というわけで、いきなりサンプルコード。

package {

    import flash.desktop.NativeApplication;
    import flash.desktop.SystemTrayIcon;
    import flash.display.BitmapData;
    import flash.display.NativeMenu;
    import flash.display.NativeMenuItem;
    import flash.events.Event;
    import flash.events.MouseEvent;
    import flash.system.System;

    import mx.core.BitmapAsset;
    import mx.core.WindowedApplication;
    import mx.events.FlexEvent;
    import mx.styles.CSSStyleDeclaration;
    import mx.styles.StyleManager;

    // トレイアイコン用のスタイルを新たに定義
    [Style(name="trayIcon", type = "Class", inherit = "no")]

    public class TaskTrayApplication extends WindowedApplication {
        private var _trayIcon:Class;

        private var systemTrayIcon:SystemTrayIcon;
        private var systemTrayMenu:NativeMenu;
        private var iconChanged:Boolean = true;

        private var exitMenuItem:NativeMenuItem = new NativeMenuItem("終了");

        // デフォルトのアイコン
        // これは各自で用意する
        [Embed(source="assets/icons/DefailtTrayIcon.png")]
        private static var _defaultTrayIcon:Class;

        public function TaskTrayApplication() {
            super();
            // visibleプロパティをfalseに。
            // this.visibleはオーバーライドしたいので、super.visibleで。
            super.visible = false; // …a
            this.addEventListener(FlexEvent.CREATION_COMPLETE, applicationCreationCompleteHandler);

        }

        override public function set visible(value:Boolean):void {
            // visibleプロパティはfalseに固定したい
            // 派生クラスで変更できないようにオーバーライド
        }

        public function get defaultTrayIcon():Class {
            return _defaultTrayIcon;
        }

        private function applicationCreationCompleteHandler(event:FlexEvent):void {
            if (NativeApplication.supportsSystemTrayIcon) {
                systemTrayIcon = NativeApplication.nativeApplication.icon as SystemTrayIcon;    // …b-1

                // システムトレイのメニューは右クリックが基本らしいので、
                // 必要があれば左クリック時の処理を追加する
                systemTrayIcon.addEventListener(MouseEvent.CLICK, systemTrayIconClickHandler);
            }
            setTrayIcon();
            setMenuItems();
        }

        private function setTrayIcon():void {
            if (systemTrayIcon != null) {
                if (_trayIcon == null)
                    return ;

                // アイコンクラスを生成して、Bitmapデータを取得
                var iconBitmap:BitmapData = (new _trayIcon() as BitmapAsset).bitmapData;

                // 取得したBitmapデータをシステムトレイアイコンに設定
                systemTrayIcon.bitmaps = [iconBitmap];    // …b-2

                // ついでにツールチップも設定
                systemTrayIcon.tooltip = this.name; // これはアプリ名をそのまま使う例
            }
        }

        protected function setMenuItems():void {
            if (systemTrayIcon != null) {
                if (systemTrayMenu == null)
                    systemTrayMenu = new NativeMenu();
                systemTrayIcon.menu = systemTrayMenu;

                // 「終了」のメニューを追加
                systemTrayMenu.addItemAt(exitMenuItem, 0); // …c-1
                exitMenuItem.addEventListener(Event.SELECT, exitMenuSelectHandler); // …c-2
                
                // 以降、任意のメニューを追加していく
                
            }
        }

        private function exitMenuSelectHandler(event:Event):void {
            // アプリ自体を終了する
            NativeApplication.nativeApplication.exit(0);
        };

        protected function systemTrayIconClickHandler(event:MouseEvent):void {
            trace("tray_click");
        }

        override public function styleChanged(styleProp:String):void {
            super.styleChanged(styleProp);
            // トレイアイコン変更時の処理
            if (styleProp == "trayIcon") {
                iconChanged = true;
                invalidateProperties();
            }
            invalidateDisplayList();
        }

        override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void {
            if (iconChanged == true) {
                _trayIcon = this.getStyle("trayIcon");
                setTrayIcon();
                iconChanged = false;
            }
            super.updateDisplayList(unscaledWidth, unscaledHeight);
        }

        // スタイルデフォルト値設定用の静的変数と関数
        // http://livedocs.adobe.com/flex/3_jp/html/help.html?content=skinstyle_3.html
        private static var classConstructed:Boolean = classConstruct();
        private static function classConstruct():Boolean {
            var currentStyleDeclaration:CSSStyleDeclaration = StyleManager.getStyleDeclaration("TaskTrayApplication");
            // TaskTrayApplicationクラスのスタイル定義が既に存在するかどうかを確認
            if (currentStyleDeclaration == null) {
                // スタイルが定義されていない
                // ここでスタイルが作成され、trayIconスタイルのデフォルト値を設定
                var newStyleDeclaration:CSSStyleDeclaration = new CSSStyleDeclaration();
                newStyleDeclaration.setStyle("trayIcon", _defaultTrayIcon);
                StyleManager.setStyleDeclaration("TaskTrayApplication", newStyleDeclaration, true);
            }
            return true;
        }
    }
}


結構前に作ったものをベースに、参考サイトからの情報を付け加えていってるので、
無駄な処理もあるかも。


メインウィンドウを消す処理は、コンストラクタでやっているように、自身のvisibleプロパティをfalseに設定し(a)、
且つ、アプリケーション記述XMLの値にfalseにする。
これで、メインウィンドウは消え、タスクバー上にもウィンドウタイトルが表示されなくなる。


次にタスクトレイにトレイアイコンを入れる処理。
アプリケーションが起動した時に、SystemTrayIconクラスをNativeApplicationクラスから取得する(b-1)。
次に、取得したSystemTrayIconのbitmapsプロパティ(Array型)に、アイコンイメージを設定すればOK(b-2)。


またSystemTrayIconのmenuプロパティに、"終了"を表示するNativeMenuItemクラスを格納したNativeMenuクラスを設定すると(c-1)、
トレイアイコンを右クリックしたときに、"終了"メニューが表示される。
そのメニューが選択されたときの処理は、exitMenuSelectHandlerがNativeMenuItemにイベントハンドラーとして登録されている(c-2)。


あと、"trayIcon"という名前でスタイルを新規に追加して、TaskTrayApplicationのsetStyleメソッドで、アプリ実行中でもトレイアイコンが変更できるようにした。
例えば、Timerクラスを使って数秒毎にアイコンを(点滅っぽく)変更する、とかもできる。


ただスタイルを新規で作るとデフォルト値の扱いが問題になるらしく、
上記の例では予め用意しておく画像ファイル("assets/icons/DefailtTrayIcon.png")をデフォルトアイコンとしたかったので、
クラス定義の最後に、このクラスがnewされる最初だけ実行する静的変数/関数を定義して、トレイアイコンのデフォルト値を設定している。


このクラスでメインのアプリを作ってもいいし、メインのアプリのスーパークラスとして使ってもいいと思う。
# 後者の場合は、トレイアイコンをクリックしたときに出るメニューの追加には一手間いるかも

(4/27追記)
どうやら、ActionScriptで定義したApplication派生クラスから直接Flexアプリケーションはできないようなので、
やはり、上のクラスをスーパークラスにして、MXMLのクラスを作る必要があるみたい。
なので、setMenuItems()メソッドはprotectedに変更して、派生先のクラスでは、

        override protected function setMenuItems():void {
            super.setMenuItems();

            // 以降、任意のトレイアイコンメニューを追加
        }

とやるのがまともっぽいね。

参考サイト


# はてなシンタックスハイライトって、ActionScriptには対応してませんよね
# 上のサンプルコードはJavascriptを指定してけど、どれを指定するのが一番見やすいんだろう?