Storyboard XAML 문법의 난해함

UI 프로그래밍 기법에 있어서 WPF와 Silverlight의 가장 큰 특징은 Storyboard와 Animation 개체를 사용한 애니메이션의 구현이 아닐까 생각해요. 물론 비슷한 개념은 이미 플래쉬나 다른 곳에서도 볼 수 있었지만요.

그런데 이 스토리보드와 애니메이션을 수작업으로 XAML 코딩 한다는 건 만만치가 않습니다. 바로 애니메이션의 대상 개체와 대상 속성을 지정하는 문법의 귀찮음난해함 때문이죠.

간단하게 테스트 해볼까요?
페이지가 로드 되었을 때 다음과 같은 Rectangle 엘리먼트가 오른쪽으로 100픽셀 이동하면서 회전하는 애니메이션을 Expression Blend를 사용하지 않고 코딩해보세요. 단, Orcas를 사용해도 좋습니다.

<Rectangle Width="100" Height="100" Canvas.Left="50" Canvas.Top="100" Fill="Black" />

이걸 고민 없이 해내는 분이 있다면 쓰부로 모실테니 연락주세요!

당연한 얘기지만 전 그렇게 못해요. 그래서 이런 애니메이션을 만들 땐 곧바로 Expression Blend로 프로젝트를 열어보지요.
블렌드로 대충 끄적여 볼까요? 간단한 과정인데 캡쳐하기가 애매해서 동영상으로 캡쳐했어요.


원본은 800 x 600이니까 다운 받아서 보세요.
http://gongdo.tistory.com/attachment/cfile5.uf@25123E3A5878F5810E67EB.wmv


대략 아래와 같은 XAML 코드가 만들어질거에요. (루트 엘리먼트는 생략.)
XAML
<Canvas.Triggers>
    <EventTrigger RoutedEvent="Canvas.Loaded">
        <BeginStoryboard>
            <Storyboard x:Name="Timeline1">
                <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="rectangle" Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.X)">
                    <SplineDoubleKeyFrame KeyTime="00:00:01" Value="100"/>
                </DoubleAnimationUsingKeyFrames>
                <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="rectangle" Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[2].(RotateTransform.Angle)">
                    <SplineDoubleKeyFrame KeyTime="00:00:01" Value="360"/>
                </DoubleAnimationUsingKeyFrames>
            </Storyboard>
        </BeginStoryboard>
    </EventTrigger>
</Canvas.Triggers>

<Rectangle Width="100" Height="100" Canvas.Left="50" Canvas.Top="100" Fill="Black" RenderTransformOrigin="0.5,0.5" x:Name="rectangle" >
    <Rectangle.RenderTransform>
        <TransformGroup>
            <ScaleTransform ScaleX="1" ScaleY="1"/>
            <SkewTransform AngleX="0" AngleY="0"/>
            <RotateTransform Angle="0"/>
            <TranslateTransform X="0" Y="0"/>
        </TransformGroup>
    </Rectangle.RenderTransform>
</Rectangle>

와... 이거 코딩은 고사하고 해독하는데도 만만치가 않네요. 단지 몇 픽셀 옮기면서 돌렸을 뿐인데...
특히 이 코드의 백미는 Storyboard.TargetProperty의 복잡함이죠. 죄다 문자열이라서 인텔리센스의 도움을 받을 수 없는데 베이스 클래스의 이름부터 순서대로 쫘악 나열해야 하는데 중간에 배열 번호까지 들어가요. 해당 속성이 컬렉션일 경우엔 추가된 순서까지 맞춰서 코딩해야 한다는 거죠.

이쯤되면 수작업으로 작성하는 건 GG죠.

이런 이유로 실버라잇의 애니메이션은 그냥 블렌드를 사용하여 작성하는게 정신 건강에 훨씬 좋아요. 블렌드에서 대충 원하는 형태의 애니메이션을 만들고 세부적인 값을 수정하는 거죠.

현재 스토리보드의 문제점

그나마도 디자인타임에 애니메이션이 고정되는 건 블렌드와 약간의 수정을 통해 뭐라도 만들 수 있어요. 하지만 동적으로 순간순간 변화하는 변수를 적용해야 하는 애니메이션이 있다면? 그럼 더 머리 아파지죠.

여기에서 XAML과 코드-비하인드를 연계하여 코딩하는데 익숙하신 분이라면 XAML의 Canvas나 TransformGroup 엘리먼트처럼 하위 엘리먼트를 포함할 수 있는 경우 코드-비하인드에서는 object.Children 이라는 컬렉션 속성을 통해 하위 엘리먼트에 접근하거나 추가/삭제 할 수 있다는 걸 아실거에요. 예를 들어 위에서 작성한 XAML을 개체 트리로 나타내 보자면 다음과 같습니다.


이것을 다시 의사 코드로 작성해보면 아마도 아래와 같겠죠
C# (의사 코드)
Canvas canvas = new Canvas();    // 새 캔버스 생성

BeginStoryboard bs = new BeginStoryboard();    // 새 스토리보드 시작기 생성

Storyboard sb = new Storyboard();    // 새 스토리보드 생성

DoubleAnimationUsingKeyFrames dak = new DoubleAnimationUsingKeyFrames();    // 더블 애니메이션 생성
dak.KeyFrames.Add( new SplineDoubleKeyFrame() );    // 애니메이션에 키프레임 추가

sb.Children.Add( dak );    // 스토리보드에 생성한 애니메이션 추가

dak = new DoubleAnimationUsingKeyFrames();    // 다른 애니메이션 새로 추가
dak.KeyFrames.Add( new SplineDoubleKeyFrames() );

sb.Children.Add( dak );

bs.Storyboard = sb;    // 스토리보드를 설정

Rectangle rect = new Rectangle();    // 새 사각형 생성

TransformGroup tg = new TransformGroup();    // 새 변형 그룹 생성

tg.Children.Add( new ScaleTransform() );        // 변형 그룹에 변형 규칙 추가
tg.Children.Add( new SkewTransform() );
tg.Children.Add( new RotateTransform() );
tg.Children.Add( new TranslateTransform() );

rect.RenderTransform = tg;    // 사각형의 변형 속성을 설정

canvas.Children.Add( tg );    // 사각형을 캔버스에 추가

canvas.Triggers.Add( bs );    // 스토리보드 시작기를 캔버스의 트리거에 추가

꽤나 복잡해 보이지만 위의 개체 트리 구조를 보면 별로 어려울 건 없을 거에요. XAML에서 하위 요소는 코드-비하인드에서 일반적으로 같은 이름의 컬렉션으로 구현된다는 거죠.
이렇게라도 동작해주면 얼마나 좋겠습니까...만,
안됩니다.

TransformGroup.Children이나 Canvas.Children 또는 Canvas.Triggers와 같은 Collection 개체는 일반적으로 하위 개체를 생성하여 추가, 삭제, 접근할 수 있게 되어 있어요.

하지만 이상하게도 Storyboard의 Children 속성은 분명히 TimelineGroup이라는 이름의 클래스임에도 불구하고 실제로 들어가보면 단지 Timeline을 가리킬 뿐이에요. 다시 말해서 컬렉션이 아니기 때문에 하위 요소를 하나밖에 추가할 수 없다는 얘기죠.

예제에서 우리는 [오른쪽으로 이동] 이라는 애니메이션과 [360도 회전]이란 애니메이션을 동시에 처리하였죠? 그런데 스토리보드의 Children이 단일 요소만 가리키게 되어 있으니 이건 뭐 방법이 없는 거에요.

이 부분은 아마도 1.1 Alpha라는 테스트 중인 버전이기 때문에 그렇지 않을까... 정식판이 나오면 스토리보드의 Children이 Animation의 컬렉션으로 변경되지 않을까... 하는 기대를 할 수밖에 없네요.

XamlReader 클래스를 활용한 동적 스토리보드 작성

그렇다고 코드에서 스토리보드를 동적으로 생성하고 애니메이션을 추가하는게 불가능한 건 아니에요.
다만 효율이 조금 떨어질 뿐이죠.

실버라잇은 XamlReader라는 매우 리버럴한 클래스를 제공하는데요, XamlReader.Load 메서드를 사용하면 파라미터로 전달한 XAML 코드를 동적으로 파싱하여 개체로 반환해주죠.

예를 들어 새 사각형 개체를 XamlReader를 이용하여 동적으로 추가한다면...

C#
string strRect = @"<Rectangle Width='100' Height='100' Canvas.Left='100' Canvas.Top='100' Fill='Red' />";
Rectangle rect = (Rectangle)XamlReader.Load(strRect);
this.Children.Add(rect);

심플하죠? 그냥 동적으로 추가할 XAML 코드를 문자열로 저장하고 XamlRead.Load 메서드의 파라미터로 전달하고 원하는 타입으로 캐스팅만 하면 돼요.

마찬가지로 스토리보드와 일련의 요소들을 XAML 코드로 작성하여 XamlReader로 동적으로 생성할 수 있다는 거죠.
처음 Blend로 작성했던 예제를 조금 변형해서 이번엔 텍스트1을 클릭하면 오른쪽으로 100픽셀, 텍스트2를 클릭하면 왼쪽으로 100픽셀을 회전하며 이동하는 코드를 작성해보죠.

먼저 텍스트1, 2과 결과 확인을 위한 텍스트3을 추가하고 기존에 작성되었던 애니메이션은 제거하며 사각형은 그대로 놔둡니다. 역시 루트 엘리먼트인 Canvas는 생략했으니 주의...
XAML
<TextBlock x:Name="text1" Text="You spin me RIGHT round round round..." Canvas.Top="0" />
<TextBlock x:Name="text2" Text="You spin me LEFT round round round..." Canvas.Top="20" />
<TextBlock x:Name="text3" Text="O_O" Canvas.Top="40" />


<Rectangle Width="100" Height="100" Canvas.Left="50" Canvas.Top="100" Fill="Black" RenderTransformOrigin="0.5,0.5" x:Name="rectangle" >
    <Rectangle.RenderTransform>
        <TransformGroup>
            <ScaleTransform ScaleX="1" ScaleY="1"/>
            <SkewTransform AngleX="0" AngleY="0"/>
            <RotateTransform Angle="0"/>
            <TranslateTransform X="0" Y="0"/>
        </TransformGroup>
    </Rectangle.RenderTransform>
</Rectangle>

오른쪽/왼쪽으로 움직이는 스토리보드가 하나 있어야겠죠? 클래스의 멤버 변수로 스토리보드를 하나 선언합니다.
C#
Storyboard _story = new Storyboard();

그리고 페이지의 로드 이벤트 핸들러에 텍스트1과 2가 눌렸을 때 처리할 이벤트 핸들러를 추가합니다. 이벤트 핸들러는 아래에서 채워 넣을 거에요.
C#
// 텍스트 클릭 이벤트 핸들러 추가
text1.MouseLeftButtonDown += new MouseEventHandler(text1_MouseLeftButtonDown);
text2.MouseLeftButtonDown += new MouseEventHandler(text2_MouseLeftButtonDown);

이제 이벤트 핸들러에 각각 스토리보드를 로드하는 코드를 만들어야겠죠? 이 작업은 앞서 얘기했듯이 수작업으로 머리 싸매가면서 할 필요 없어요. 그냥 Expression Blend로 빈 프로젝트를 하나 만들고 애니메이션 타임라인을 생성하되 새 타임라인을 추가할 때 리소스로 만들기(Create as a Resource)를 선택하세요.


그리곤 원하는 애니메이션을 대충 만들면 <Canvas.Resources> 태그 내에 맨 처음에 Blend로 만들었던 Storyboard 코드가 만들어질 거에요. 여기에서 <Storyboard>의 x:Name이 Timeline1로 지정되어 있는데 이건 코드에서 동적으로 로드하기 때문에 필요 없으니 삭제하세요. 마지막으로 각 태그의 어트리뷰트 설정을 위해 겹따옴표가 있는데 이건 홑따옴표로 교체하시고 코드-비하인드에 문자열로 붙여넣으시면 다음과 같은 코드가 만들어지죠.
C#
void text1_MouseLeftButtonDown(object sender, MouseEventArgs e)
{
string strStory =
    @"<Storyboard>
        <DoubleAnimationUsingKeyFrames BeginTime='00:00:00' Storyboard.TargetName='rectangle' Storyboard.TargetProperty='(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.X)'>
            <SplineDoubleKeyFrame KeyTime='00:00:01' Value='100'/>
        </DoubleAnimationUsingKeyFrames>
        <DoubleAnimationUsingKeyFrames BeginTime='00:00:00' Storyboard.TargetName='rectangle' Storyboard.TargetProperty='(UIElement.RenderTransform).(TransformGroup.Children)[2].(RotateTransform.Angle)'>
            <SplineDoubleKeyFrame KeyTime='00:00:01' Value='360'/>
        </DoubleAnimationUsingKeyFrames>
    </Storyboard>";

    _story = (Storyboard)XamlReader.Load(strStory);
    _story.Completed += new EventHandler(_story_Completed);
    this.Resources.Add(_story);

    _story.Begin();
    text3.Text = "Spin started.";
}

void
_story_Completed(object sender, EventArgs e)

{
    text3.Text = "Spin completed.";
}

text2를 눌렀을 때의 이벤트 핸들러 코드는 생략했어요. (갈수록 날로 먹으려는 이 수작!)
뭐 text1의 핸들러와 똑같고 두 애니메이션의 Value값을 '0'으로만 바꾸면 되니까 직접 해보세요. -ㅇ-

여기서 주의할 점은 코드로 제어하는 스토리보드는 반드시 루트 엘리먼트의 리소스로 등록이 되어야 한다는 것과 XamlReader로 동적으로 로드하는 XAML 엘리먼트는 x:Name이 설정되면 안된다는 사실. 잊지 마세요.

작성된 프로젝트는 다음에 첨부했으니 코딩이 귀찮으신 분들은 다운 받아보세요. :)
StoryboardTest.zip

스토리보드 테스트 프로젝트



동작 데모는 다음과 같습니다.

http://gongdo.tistory.com/attachment/cfile27.uf@253353395878F57F1002F7.wmv

여전히 남아있는 불합리성

여기까지 더운데 땀 삐질삐질 흘려가며 작성하셨다면 지금쯤 꽤나 열이 받으셨을 거에요.

"블렌드를 써서 만든 코드를 복사해서 도로 붙여 넣고 따옴표 바꿔주고 문자열로 만들고 코드 몇 줄 더 추가하고... 이럴 바에야 스토리보드를 각각에 상황에 맞게 더 만들고 말겠다!"

네, 정답입니다. 위의 예제 같은 경우는 구태여 코드에서 동적으로 만들 필요성 따윈 전혀 없어요.
단순한 애니메이션이라면 그냥 Blend를 써서 만드는게 훨씬 빠르고 합리적이죠.

하지만 이런 경우를 생각해보죠. 만약 애니메이션의 이동 범위가 런타임에 수시로 바뀐다면? 심지어 이동 방향 조차 변경될 수 있다면?
또는 마우스 커서가 이동하면 마우스 커서에서 시작되는 애니메이션을 만들고 싶다면?

이럴 때 위와 같이 문자열로 XAML 코드를 옮기되 동적으로 수정하고 싶은 부분을 {0}, {1}, {2} ... 이렇게 치환해놓고 String.Format 메서드로 포매팅하면 되겠죠. 예를 들어 위의 예제에서 진행시간, 이동 범위를 동적으로 변경한다면...
C#
string strStory =
    @"<Storyboard>
        <DoubleAnimationUsingKeyFrames BeginTime='00:00:00' Storyboard.TargetName='rectangle' Storyboard.TargetProperty='(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.X)'>
            <SplineDoubleKeyFrame KeyTime='{0}' Value='{1}'/>
        </DoubleAnimationUsingKeyFrames>
        <DoubleAnimationUsingKeyFrames BeginTime='00:00:00' Storyboard.TargetName='rectangle' Storyboard.TargetProperty='(UIElement.RenderTransform).(TransformGroup.Children)[2].(RotateTransform.Angle)'>
            <SplineDoubleKeyFrame KeyTime='{0}' Value='360'/>
        </DoubleAnimationUsingKeyFrames>
    </Storyboard>";

    _story = (Storyboard)XamlReader.Load( string.Format( strStory, new object[] { "00:00:05", "200" } ) );

이렇게 문자열의 포매팅을 활용할 수 있겠죠.

휴...
스토리보드, 애니메이션, 타임라인은 다른 개체들이 상당히 직관적으로 구성되어 있는데 비해 굉장히 난해한 편인 것 같아요. 그냥 개체나 멤버의 이름만 보고 직관적인 코딩이 어렵다는 거죠. 코드만 가지고 적절하게 스토리보드를 만들지 못하고 상당히 지저분한 방법을 동원하게 되었는데요, 다시 얘기하지만 이런 점은 정식판이 나오면서 개선되었으면 하는 바램이에요.

신고
Posted by gongdo


티스토리 툴바