SkinnableComponentを拡張したツールチップ #1

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Buttonコンポーネントなど、Flexのコンポーネントはツールチップを表示する機能が標準で実装されており、プロパティを設定するだけで簡単に利用することができます。しかし、デフォルトで表示されるツールチップはとてもシンプルな外観をしており、アイコンのみのボタンを作成したときなどは外観をカスタマイズしたいときがあります。そこで、スキン可能なツールチップとそれを表示するボタンを作成してみました。コンポーネントの作成と同時に、話題としては今さら感がありますが、SkinnableComponentの拡張について再確認していきたいと思います。

作成するコンポーネント

今回作成するのは、以下のような外観をしたボタンとツールチップです。

このコンポーネントは以下の条件を満たすように作成します。

  • ツールチップを表示する位置はボタンの上下左右の4か所のうちいずれか
  • ツールチップを表示する位置はボタンのプロパティで設定
  • ツールチップの吹き出しは常にボタン側を向く

・ボタンの右側にツールチップを表示する例

ツールチップコンポーネントの作成

まずは、ツールチップコンポーネントを作成します。以下はツールチップコンポーネントのソースコードです。FlexSDK 4.5.1.21328Aで動作確認をしています。

package jp.classmethod.sample.components
{
    import flash.errors.IllegalOperationError;
   
    import jp.classmethod.sample.skins.FixedPositionToolTipSkin;
   
    import mx.core.IToolTip;
   
    import spark.components.supportClasses.SkinnableComponent;
    import spark.core.IDisplayText;
   
    //--------------------------------------
    //  Skin states
    //--------------------------------------
   
    [SkinState("top")]
    [SkinState("left")]
    [SkinState("right")]
    [SkinState("bottom")]
   
    //--------------------------------------
    //  Style
    //--------------------------------------
   
    [Style(name="fontFamily", type="String", inherit="yes")]
    [Style(name="fontLookup", type="String", enumeration="auto,device,embeddedCFF", inherit="yes")]
    [Style(name="fontSize", type="Number", format="Length", inherit="yes", minValue="1.0", maxValue="720.0")]
    [Style(name="fontStyle", type="String", enumeration="normal,italic", inherit="yes")]
    [Style(name="fontWeight", type="String", enumeration="normal,bold", inherit="yes")]
   
    public class FixedPositionToolTip extends SkinnableComponent implements IToolTip
    {
        public function FixedPositionToolTip()
        {
            super();
            setStyle("skinClass", FixedPositionToolTipSkin);
        }
       
        //--------------------------------------------------------------------------
        //
        //  Skin parts
        //
        //--------------------------------------------------------------------------
       
        [SkinPart(required="false")]
       
        public var labelDisplay:IDisplayText;
       
        //--------------------------------------------------------------------------
        //
        //  Overridden Properties
        //
        //--------------------------------------------------------------------------
       
        //----------------------------------
        //  text
        //----------------------------------
       
        private var _text:String;

        public function get text():String
        {
            return _text;
        }

        public function set text(value:String):void
        {
            _text = value;
            invalidateProperties();
        }
       
        //----------------------------------
        //  toolTipDirection
        //----------------------------------
       
        private var _toolTipDirection:String = FixedPositionToolTipDirection.TOP;
       
        public function get toolTipDirection():String
        {
            return _toolTipDirection;
        }
       
        public function set toolTipDirection(value:String):void
        {
            switch (value)
            {
                case FixedPositionToolTipDirection.TOP:
                case FixedPositionToolTipDirection.LEFT:
                case FixedPositionToolTipDirection.RIGHT:
                case FixedPositionToolTipDirection.BOTTOM:
                    _toolTipDirection = value;
                    break;
                default:
                    throw new IllegalOperationError("Invalid ToolTipDirection.");
            }
           
            invalidateSkinState();
        }
       
        //--------------------------------------------------------------------------
        //
        //  Overridden Methods
        //
        //--------------------------------------------------------------------------
       
        override protected function commitProperties():void
        {
            super.commitProperties();
            if (labelDisplay)
            {
                labelDisplay.text = text;
            }
        }
       
        override protected function getCurrentSkinState():String
        {
            var skinState:String = null;
            switch (toolTipDirection)
            {
                case FixedPositionToolTipDirection.TOP:
                    skinState = "top";
                    break;
                case FixedPositionToolTipDirection.LEFT:
                    skinState = "left";
                    break;
                case FixedPositionToolTipDirection.RIGHT:
                    skinState = "right";
                    break;
                case FixedPositionToolTipDirection.BOTTOM:
                    skinState = "bottom";
                    break;
            }
            return skinState;
        }
    }
}

ツールチップコンポーネントの定数クラスのソースコードです。

package jp.classmethod.sample.components
{
    public class FixedPositionToolTipDirection
    {
        //--------------------------------------------------------------------------
        //
        //  Class constants
        //
        //--------------------------------------------------------------------------
       
        public static const TOP:String = "top";
        public static const LEFT:String = "left";
        public static const RIGHT:String = "right";
        public static const BOTTOM:String = "bottom";
       
    }
}

ツールチップコンポーネントのスキンクラスのソースコードです。

<?xml version="1.0" encoding="utf-8"?>
<s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        xmlns:mx="library://ns.adobe.com/flex/mx"
        minWidth="55" minHeight="25" xmlns:local="*">
    <!-- host component -->
    <fx:Metadata>
        [HostComponent("jp.classmethod.sample.components.FixedPositionToolTip")]
    </fx:Metadata>
   
    <!-- states -->
    <s:states>
        <s:State name="top"/>
        <s:State name="left"/>
        <s:State name="right"/>
        <s:State name="bottom"/>
    </s:states>
   
    <!-- グラフィックの定義は省略 -->
   
    <s:Label id="labelDisplay"
             textAlign="center"
             verticalAlign="middle"
             maxDisplayedLines="1"
             left="10" left.right="21"
             right="10" right.left="21"
             top="2" top.bottom="13"
             bottom="2" bottom.top="13">
    </s:Label>
</s:Skin>

では、ソースコードを見ていきましょう。

スキン可能ツールチップコンポーネント

以下はツールチップコンポーネントクラスの宣言部です。

public class FixedPositionToolTip extends SkinnableComponent implements IToolTip

このコンポーネントはSkinnableComponentを継承したスキン可能コンポーネントとして実装しています。

また、IToolTipインターフェースを実装して、ToolTipManagerがこのコンポーネントをツールチップとして利用できるようにしています。IToolTipインターフェースはscreen(読み取り専用)とtextの2つのプロパティの実装を要求しますが、screenプロパティはUIComponentで実装されていますので、今回はtextプロパティのみ実装することになります。

文字列の表示

ツールチップ上の文字列は、表示用の部品をコンポーネント上に配置して、外部から受け取った文字列をセットして表示させます。まずは、ツールチップ上の文字列を表示する部品をスキンパートとして宣言する部分を見ていきます。

コンポーネントのスキンパートの宣言です。

[SkinPart(required="false")]
public var labelDisplay:IDisplayText;

スキンのLabelの宣言です。

<s:Label id="labelDisplay"
         textAlign="center"
         verticalAlign="middle"
         maxDisplayedLines="1"
         left="10" left.right="21"
         right="10" right.left="21"
         top="2" top.bottom="13"
         bottom="2" bottom.top="13">
</s:Label>

コンポーネント側では、基本的には部品の参照を保持する変数を宣言するだけです。実際の部品のインスタンスはスキン側で宣言しておくことによって生成されます。コンポーネント側の変数名とスキン側のidを同じ名前にしておくと、スキンのインスタンス化時に生成されたスキンパートの参照がコンポーネント側にセットされてアクセスできるようになります。ただし、これらの処理はSkinnableComponentで実装されているため、この仕組みを利用するためにはSkinnableComponentクラスを継承することが必須になります。

次に、表示する文字列を取得する部分を見ていきます。

コンポーネントのtextプロパティのセッタです。

public function set text(value:String):void
{
    _text = value;
    invalidateProperties();
}

コンポーネントのcommitPropertiesメソッドです。

override protected function commitProperties():void
{
    super.commitProperties();
    if (labelDisplay)
    {
        labelDisplay.text = text;
    }
}

IToolTipの実装であるtextプロパティは、ToolTipManagerから表示する文字列を受け取ります。表示文字列を受け取った際にinvalidatePropertiesを呼び出してcommitPropertiesをトリガし、画面更新前に呼び出されるcommitPropertiesでlabelDisplayに表示文字列をセットしています。

ここまでの実装で、ツールチップコンポーネントに文字列が表示される仕組みが出来上がりました。

表示位置による外観の変更

今回作成するツールチップコンポーネントは表示位置によって外観が異なるので、コンポーネントの状態に応じて外観を変化させる必要があります。これを実現するためにスキンステートを利用します。

では、スキンステートをコンポーネントクラスとスキンクラスに宣言する部分を見ていきましょう。

コンポーネントのスキンステートの宣言です。

[SkinState("top")]
[SkinState("left")]
[SkinState("right")]
[SkinState("bottom")]

スキンのステートの宣言です。

<s:states>
    <s:State name="top"/>
    <s:State name="left"/>
    <s:State name="right"/>
    <s:State name="bottom"/>
</s:states>

スキンステートは名前の通りコンポーネントの外観の状態を表します。今回は表示位置によって外観を変化させたいので、表示位置ごとのスキンステートを宣言しています。

次に、宣言したスキンステートを利用して外観を変化させる部分を見ていきましょう。

コンポーネントのtoolTipDirectionプロパティのセッタです。

public function set toolTipDirection(value:String):void
{
    switch (value)
    {
        case FixedPositionToolTipDirection.TOP:
        case FixedPositionToolTipDirection.LEFT:
        case FixedPositionToolTipDirection.RIGHT:
        case FixedPositionToolTipDirection.BOTTOM:
            _toolTipDirection = value;
            break;
        default:
            throw new IllegalOperationError("Invalid ToolTipDirection.");
    }
    
    invalidateSkinState();
}

toolTipDirectionはツールチップの表示位置を設定するプロパティです。ツールチップの表示位置が変更された際にinvalidateSkinStateを呼び出すことで、画面更新の前にスキンクラスのステートの再評価が行われます。その際に、変更するべきステート名を取得するためにgetCurrentSkinStateが呼び出されます。getCurrentSkinStateで現在のコンポーネントの状態に応じたステート名を返すことで、スキンクラスのステートが変更されて適切な外観に変更されます。

以下はgetCurrentSkinState内での処理です。

var skinState:String = null;
switch (toolTipDirection)
{
    case FixedPositionToolTipDirection.TOP:
        skinState = "top";
        break;
    case FixedPositionToolTipDirection.LEFT:
        skinState = "left";
        break;
    case FixedPositionToolTipDirection.RIGHT:
        skinState = "right";
        break;
    case FixedPositionToolTipDirection.BOTTOM:
        skinState = "bottom";
        break;
}
return skinState;

ここでは、toolTipDirectionにセットされた値を元にスキンステートを決定してステート名を返しています。これによって、表示位置が変更された際にコンポーネントが適切な外観に変化するようになります。

ここまでの実装でツールチップコンポーネントが完成しました。次回は今回作成したツールチップを利用するボタンコンポーネントを作成したいと思います。