어떤 분이 Flex 3.4환경에서 MenuBar에 색을 입히려하다가 잘 안되서 본인의 블로그에 질문을 해주셨다.


 <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml">
	<mx:Style>
		MenuBar { 
			font-weight:bold; 
			color:#FFFFFF;
			fill-colors:#0D0A92, #1B16F4, #37920A, #59EB12;
			fill-alphas:1.0,1.0;
		}
		MenuBarItem {
			theme-color:#0000FF;
		}
		Menu { 
			alternatingItemColors:#ffffff,#eff4fa;
			color:#000000;
			top:4px;
			bottom:2px;
			horizontalGap:2px;	
			roll-over-color:#00ff00;
		}
	</mx:Style>
	<mx:MenuBar id="myMenuBar" labelField="@label" >
		<mx:XMLList>
			<menuitem label="MenuItem A">
				<menuitem label="SubMenuItem A-1" enabled="false"/>
				<menuitem label="SubMenuItem A-2"/>
			</menuitem>
			<menuitem label="MenuItem B"/>
			<menuitem label="MenuItem C"/>
			<menuitem label="MenuItem D">
				<menuitem label="SubMenuItem D-1" 
						  type="radio" groupName="one"/>
				<menuitem label="SubMenuItem D-2" 
						  type="radio" groupName="one"
						  selected="true"/>
				<menuitem label="SubMenuItem D-3" 
						  type="radio" groupName="one"/>
			</menuitem>
		</mx:XMLList>
	</mx:MenuBar>
</mx:Application>



질문은 MenuBarItem의 themeColor를 #0000FF로 지정했는데 저렇게 나오냐는 것이였다. 참고로 MenuBar는 저 파란색의 바를 말하는 것이고 MenuBarItem은 MenuItem A, MenuItem B...등으로 표시된 것이다. 이들은 모두 UIComponent를 상속했다. 마지막으로 Menu는 서버메뉴로써 List를 확장한 것이다.

질문처럼 직관적으로 봐도 문제가 있어 보인다. 이렇게 나오는 이유를 알기 위해서는 스킨부분을 찾아봐야 한다.

(아래내용이 약간 복잡할 수 있으나 천천히 따라오면 그리 복잡한 것도 아니다. 원래 이런걸 말로 설명해야 빠른 건데 글로 설명하려 하니 어렵다. ^^;)


원인 찾기

MenuBar의 아이템렌더러는 menuBarItemRenderer속성에 지정된다. 기본적으로 MenuBarItem 클래스가 된다. 쉽게 이야기해 위 그림에서 MenuItem A, MenuItem B... 등은 다 MenuBarItem으로 생성되어 그려진 것이다. 만약 MenuBarItem을 다른 것으로 대체하고 싶다면 IMenuBarItemRenderer 인터페이스를 구현해서 다른 MenuBarItem을 만들면 되겠다. 아래는 MenuBar의 menuBarItemRenderer 속성을 설명하고 있다.

(문제의 원인을 파악하기 위해 가장 먼저 찾아야 할 것은 API 문서이다.)



MenuBarItem의 스킨은 무엇일까? 아래 MenuBar에서 Style부분을 보자. MenubarItem의 스킨은 바로 ActivatorSkin 클래스라는 것을 알 수 있다. 결국 스킨도 내 마음대로 바꿀 수 있는 것이다.

Flex 3.4에서 스킨은 크게 그래픽기반 스킨과 프로그램적 스킨(Programmatic Skin)이 있다. 그래픽기반 스킨은 이미지를 생각하면 된다. 하지만 프로그램적 스킨은 ActionScript 3.0 코드이다. MenuBar의 itemSkin 스타일 속성으로 mx.skins.halo.ActivatorSkin을 썼다는 것은 바로 프로그램적 스킨을 사용한다는 이야기이다.

(문서를 봐도 해결의 실마리가 안보인다면 SDK를 뜯어보자!)

그럼 이들이 어떻게 동작하는 것일까? MenuBar부터 시작해 MenuBarItem, ActivatorSkin으로 자연스럽게 찾아보면 된다. Flex 3.4 SDK 소스는 공개되어 있으므로 아래 그림처럼 Flash builder 4에서 쉽게 찾아볼 수 있다. Flex Builder 3를 사용한다면 이 기능은 사용할 수 없다.

Flex SDK 클래스를 보려면 framework.swc를 펼쳐보면 된다.



MenuBar 소스에서 mouseOverHandler, mouseUpHandler, mouseDownHandler 메소드들을 보자. 이들 메소드는 마우스 이벤트를 처리하는데 잘 보면 이런 코드들이 있다.

item.menuBarItemState = "itemDownSkin";


여기서 item은 IMenuBarItemRenderer 인터페이스의 참조이다. 그리고 MenuBarItem 클래스는 이 인터페이스를 구현한 것이다. 결국 MenuBarItem의 menuBarItemState 속성에 itemDownSkin이라고 지정한 것이다.

그럼 MenuBarItem 소스의 menuBarItemState 부분을 보자.


    public function set menuBarItemState(value:String):void
    {
        _menuBarItemState = value;
        viewSkin(_menuBarItemState);
    }   

    private function viewSkin(state:String):void
    {  	
    	var newSkinClass:Class = Class(getStyle(state));
    	var newSkin:IFlexDisplayObject;
    	var stateName:String = "";
    	    	
    	if (!newSkinClass)
    	{
    		// Try the default skin
    		newSkinClass = Class(getStyle(skinName)); 		
    		    	
    		if (state == "itemDownSkin")
    			stateName = "down";
    		else if (state == "itemOverSkin")
    			stateName = "over";
    		else if (state == "itemUpSkin")
    			stateName = "up";
    		
    		// If we are using the default skin, then 
    		if (defaultSkinUsesStates)
    			state = skinName;
    		
     		if (!checkedDefaultSkin && newSkinClass)
    		{
	    		newSkin = IFlexDisplayObject(new newSkinClass());
	    		// Check if the skin class is a state client or a programmatic skin
	    		if (!(newSkin is IProgrammaticSkin) && newSkin is IStateClient)
	    		{
	    			defaultSkinUsesStates = true;
	    			state = skinName;
	    		}
	    		
	    		if (newSkin)
	    		{
		    		checkedDefaultSkin = true;
		    	}
    		}
    	}
    	
      	newSkin = IFlexDisplayObject(getChildByName(state));

        if (!newSkin)
        {
            if (newSkinClass)
            {
                newSkin = new newSkinClass();

                DisplayObject(newSkin).name = state;

                if (newSkin is ISimpleStyleClient)
                    ISimpleStyleClient(newSkin).styleName = this;

                addChildAt(DisplayObject(newSkin), 0);
            }
        }

        newSkin.setActualSize(unscaledWidth, unscaledHeight);

        if (currentSkin)
            currentSkin.visible = false;

        if (newSkin)
            newSkin.visible = true;

        currentSkin = newSkin;
        
        // Update the state of the skin if it accepts states and it implements the IStateClient interface.
		if (defaultSkinUsesStates && currentSkin is IStateClient)
		{
			IStateClient(currentSkin).currentState = stateName;
		}
    }

MenuBarItem의 menuBarItemState에 상태변화 값을 넣어주면 MenuBarItem의 viewSkin() 메소드에서 각 상태값에 따라서 스킨을 만들어준다. viewSkin 메소드에서 newSkin = IFlexDisplayObject(new newSkinClass()); 은 새로운 스킨을 만드는 부분이 newSkinClass가 바로 ActivatorSkin 클래스가 된다. 그리고 newSkin = IFlexDisplayObject(getChildByName(state));  부분은 이전에 만든 스킨이 있는지 확인하는 부분이다. viewSkin() 메소드는 그래픽적 스킨, 프로그램적 스킨등에 모두 대응되며 게으른(lazy) 스킨 생성을 한다. 여기서 게으른 생성이라는 것은 가령, 애플리케이션이 실행 당시에 마우스 Over스킨을 생성하는 것이 아니라 마우스Over시에 스킨은 바로 그때 생성해준다는 것이다. viewSkin() 메소드가 결국 ActivatorSkin 클래스로 하여금 스킨을 만들어주는 것이다.

ActivatorSkin 클래스의 일부분을 보자.

	private static function calcDerivedStyles(themeColor:uint,  fillColor0:uint, fillColor1:uint):Object
	{
		var key:String = HaloColors.getCacheKey(themeColor,fillColor0, fillColor1);
				
		if (!cache[key])
		{
			var o:Object = cache[key] = {};
			
			// Cross-component styles.
			HaloColors.addHaloColors(o, themeColor, fillColor0, fillColor1);
		}
		
		return cache[key];
	}

	/**
	 *  @private
	 */
	override protected function updateDisplayList(w:Number, h:Number):void
	{
		super.updateDisplayList(w, h);

		if (!getStyle("translucent"))
			drawHaloRect(w, h);
		else
			drawTranslucentHaloRect(w, h);
	}

	/**
	 *  @private
	 */
	private function drawHaloRect(w:Number, h:Number):void
	{
		var fillAlphas:Array = getStyle("fillAlphas");
		var fillColors:Array = getStyle("fillColors");
		var highlightAlphas:Array = getStyle("highlightAlphas");				
		var themeColor:uint = getStyle("themeColor");

		var themeColorDrk1:Number =
			ColorUtil.adjustBrightness2(themeColor, -25);

		// Derivative styles.
		var derStyles:Object = calcDerivedStyles(themeColor, fillColors[0], fillColors[1]);
												 
		graphics.clear();

		switch (name)
		{
			case "itemUpSkin": // up/disabled
			{
				// invisible hit area
				drawRoundRect(
					x, y, w, h, 0,
					0xFFFFFF, 0);
				break;
			}

			case "itemDownSkin":
			{
				// face
				drawRoundRect(
					x + 1, y + 1, w - 2, h - 2, 0,
					[ derStyles.fillColorPress1, derStyles.fillColorPress2], 1,
					verticalGradientMatrix(0, 0, w, h )); 
									
				// highlight
				drawRoundRect(
					x + 1, y + 1, w - 2, h - 2 / 2, 0,
					[ 0xFFFFFF, 0xFFFFFF ], highlightAlphas,
					verticalGradientMatrix(0, 0, w - 2, h - 2));

				break;
			}

			case "itemOverSkin":
			{
				var overFillColors:Array;
				if (fillColors.length > 2)
					overFillColors = [ fillColors[2], fillColors[3] ];
				else
					overFillColors = [ fillColors[0], fillColors[1] ];

				var overFillAlphas:Array;
				if (fillAlphas.length > 2)
					overFillAlphas = [ fillAlphas[2], fillAlphas[3] ];
  				else
					overFillAlphas = [ fillAlphas[0], fillAlphas[1] ];

				// face
				drawRoundRect(
					x + 1, y + 1, w - 2, h - 2, 0,
					overFillColors, overFillAlphas,
					verticalGradientMatrix(0, 0, w, h )); 

				// highlight
				drawRoundRect(
					x + 1, y + 1, w - 2, h - 2 / 2, 0,
					[ 0xFFFFFF, 0xFFFFFF ], highlightAlphas,
					verticalGradientMatrix(0, 0, w - 2, h - 2));
				
				break;
			}
		}

		filters = [ new BlurFilter(2, 0) ];
	}


그림을 그릴때 drawHaloRect() 메소드를 호출해서 각 상태값에 따라서 그림을 그려준다. 우리가 처음 themeColor를 지정했음에도 불구하고 다른 색으로 보였던 부분을 알기 위해서는 drawHaloRect()에 switch분기점에서 "itemDownSkin"을 보면 된다. derStyles.fillColorPress1, derStyles.fillColorPress2 색을 사용하는 것을 볼 수 있으며 코드에서 derStyles는 calcDerivedStyles(themeColor, fillColors[0], fillColors[1]);에 의해 만들어진다. calcDerivedStyles() 메소드를 보면 결국 HaloColors.addHaloColors(o, themeColor, fillColor0, fillColor1); 으로 색이 결정됨을 볼 수있다. 여기서 HaloColors 클래스내에서 addHaloColors() 정적 메소드를 보면 다음과 같다.

public static function addHaloColors(colors:Object,  themeColor:uint,  fillColor0:uint, fillColor1:uint):void
{
	var key:String = getCacheKey(themeColor, fillColor0, fillColor1); 
	var o:Object = cache[key];
	
	if (!o)
	{
		o = cache[key] = {};
		
		// Cross-component styles
		o.themeColLgt = ColorUtil.adjustBrightness(themeColor, 100);
		o.themeColDrk1 = ColorUtil.adjustBrightness(themeColor, -75);
		o.themeColDrk2 = ColorUtil.adjustBrightness(themeColor, -25);
		o.fillColorBright1 = ColorUtil.adjustBrightness2(fillColor0, 15);
		o.fillColorBright2 = ColorUtil.adjustBrightness2(fillColor1, 15);
		o.fillColorPress1 = ColorUtil.adjustBrightness2(themeColor, 85);
		o.fillColorPress2 = ColorUtil.adjustBrightness2(themeColor, 60);
		o.bevelHighlight1 = ColorUtil.adjustBrightness2(fillColor0, 40);
		o.bevelHighlight2 = ColorUtil.adjustBrightness2(fillColor1, 40);
	}
	
	colors.themeColLgt = o.themeColLgt;
	colors.themeColDrk1 = o.themeColDrk1;
	colors.themeColDrk2 = o.themeColDrk2;
	colors.fillColorBright1 = o.fillColorBright1;
	colors.fillColorBright2 = o.fillColorBright2;
	colors.fillColorPress1 = o.fillColorPress1;
	colors.fillColorPress2 = o.fillColorPress2;
	colors.bevelHighlight1 = o.bevelHighlight1;
	colors.bevelHighlight2 = o.bevelHighlight2;
}

위에서 우리가 주목할 점은 o.fillColorPress1와 o.fillColorPress2를 설정할 때 ColorUtil.adjustBrightness2(themeColor, 85);와 같은 방법으로 새로 색을 설정하는 부분이다.

결국 무엇인가? 지금까지 MenuBar, MenuBarItem, ActivatorSkin 클래스를 쭉 따라가며 themaColor 스타일값이 원하는대로 보여지지 않은 것은 바로 HaloColors 클래스의 addHaloColors() 부분에서 찾을 수 있는 것이다.


해결하기

위처럼 themeColor가 원하는대로 적용되지 않은 것 처럼 보인 이유를 분명히 알았다. Flex 3.4가 매우 잘 만들어졌다고 할지라도 이것도 결국 사람이 만든거라 완벽히 원하는대로 기능을 쓸 수 없는 경우가 생긴다. 그러면서 발생하는 문제를 해결하기 위해 단지 피상적으로 문서만 의지해서 해결하려고 하면 절대 답을 못찾을 수 있다. 표면상 문제를 해결할 수 없다면 문제의 부분을 찾기위해 SDK 소스를 위와 같이 찾아보는 스킬을 길러야 한다.
 
어떤 문제인지 파악했으니 이제 해결해보자. 일단 기본적으로 CSS로 themeColor 와 같은 설정값으로 해결못하는 것을 알았으니 MenuBarItem에 새로운 ActivatorSkin 와 비슷한 MyActivatorSkin 을 만들고 더불어 HaloColors도 동일하게 MyHaloColors를 만들자. 이들은 기존 ActivatorSkin.as와 HaloColors.as 소스를 복사해서 만들면 된다. 물론 package부분 설정 및 include부분 설정으로 인한 에러는 제거해주자.

MyHaloColors 클래스내에 o.fillColorPress1, o.fillColorPress2에서 ColorUtil.adjustBrightness2(themeColor, 85); 처럼 된 것을 ColorUtil.adjustBrightness2(themeColor, 0);으로 바꾸자.

그런 다음 메인소스에 CSS에서 MenuBar 셀렉터에 itemSkin을 다음처럼 추가한다.

MenuBar { 
	font-weight:bold; 
	color:#FFFFFF;
	fill-colors:#0D0A92, #1B16F4, #37920A, #59EB12;
	fill-alphas:1.0,1.0;
	itemSkin: ClassReference("skins.MyActivatorSkin");
}


문제가 해결되었다.


결론
나는 개인적으로 Flex 프레임워크가 완벽하다고 보지 않는다. 그래도 전체적으로 잘 구조가 잘 잡혀있는 만큼 구조를 이해하고 잘 따라가보면 표면상으로만 해결하지 못한 부분을 해결할 수 있다. 너무 책과 문서에만 의존하지 말고 개발자라면 Flex 소스도 조금씩 분석해보길 바란다.

참고로 몇가지 덧붙여 말하면...
사실 본인은 Flex 3를 손놓은지 꽤 되었다. Flex 4도 지금 입문단계정도이다. 기본적으로 실무에서 Flex 프레임워크로 개발한다기 보다 애플리케이션의 퍼포먼스나 개발적 스타일의 문제로 ActionScript 3.0기반으로 개발한다. Flex 는 너무 유연하게 만들려고 노력해서 그런지 전체적으로 무겁다는 것을 지울 수 없어 간단히 또 빠르고 사용자에게 불편함이 없는 그런 개발이 필요할때는 Flex를 이용하지만 속도,메모리가 중요한 개발은 무조건 ActionScript 3.0으로 개발한다.

Flex는 ActionScript 3.0으로 만들어진 프레임워크 일종이며 mxml이라는 것은 이 Flex 프레임워크를 손쉽게 쓰기 위해 도입된 xml이다. 이를 파싱해서 ActionScript 3.0으로 해석해 다시 swf로 만드는 컴파일러가 compc, mxmlc인 것이다. Flex에 대해서 이러한 점을 알고 공부하고 개발하신다면 여러가지로 도움이 될 것이다. 


참고글

[Flex] 자식 컴포넌트에 CSS를 적용시키는 통상적인 방법 소개
[Flex] 스킨(Skin)을 이용하여 확장가능한 게이지 컴포넌트 제작


글쓴이 : 지돌스타(http://blog.jidolstar.com/627)

+ Recent posts