WPF LOL PlayButton 만들기 (CodeProject, Video)

안녕하세요, 처음 인사드립니다.
닷넷데브는 오래 전부터 알고 있었는데 이번에 처음으로 글을 올려보게 되었어요!

저는 :cn: 중국인이고 :kr: 한국에서 살고있는 개발자 Vicky라고 합니다. 저는 현재 WPF 일을 하고 있고, Jamesnet이라는 유튜브를 통해 영상 크리에이터 활동도 하고 있습니다. (중국의 유튜브인 BiliBili 플랫폼에서도 하고 있어요)

이 글은 LOL PlayButton 구현 방법에 대한 소개 내용을 담고 있습니다. 지난번 ThemeSwitch 구현 영상에 이어 두 번째 짧은 튜토리얼 영상이기도 합니다. Youtube를 통해 영상과 한글 자막으로도 볼 수 있습니다. 그리고 CodeProject를 통해서도 Article을 살펴보실 수 있습니다.


아래는 CodeProject를 통해 어제 기고했던 글을 한글로 번역한 내용입니다.
(GPT의 도움을 아주 많이 빌려 한국어 글을 작성했으니 부자연스러운 부분은 이해해주시면 감사하겠습니다 :sweat_smile:)

Abstract

이 글은 순수 WPF 기술을 사용하여 “리그 오브 레전드” 게임에서 영감을 받은 PLAY 버튼을 개발하는 과정을 자세히 설명하고 분석한 글입니다. WPF의 기능을 활용하여 다양한 사용자 인터페이스 구성 요소를 만드는 과정을 강조하며, 오픈 소스 개발에 대한 새로운 관점을 제시합니다. 또한 애니메이션과 트리거와 같은 고급 WPF 기능을 탐구하여 사용자 상호작용 경험을 향상시킵니다.

소개

사용자 인터페이스 구성 요소는 사용자 경험을 향상시키는 데 중요합니다. 게임에서 반응이 빠르고 시각적으로 매력적인 PLAY 버튼은 엔터테인먼트 세계로의 관문입니다. 이 글은 리치 데스크톱 애플리케이션을 구축하기 위한 강력한 프레임워크인 WPF를 사용하여 PLAY 버튼을 만드는 과정을 보여줍니다.

프로젝트 배경

이 글에서 논의된 프로젝트는 WPF 기술의 능력을 가능한 한 포괄적으로 보여주기 위해 노력합니다. 몇 년 전에 이 프로젝트를 출시한 후 긍정적인 반응을 받았으며, 이는 오픈 소스 개발에 계속 기여하도록 동기를 부여했습니다. .NET 기술이 발전함에 따라, GitHub에 공유된 코드를 지속적으로 업데이트하고 정제하고 있습니다. 이 전체 프로젝트에 포함된 방대한 내용을 고려하여, 각 부분의 구성과 기술적 초점을 자세히 분석하여 제공하기로 결정했습니다. 이는 더 많은 WPF 기술을 좋아하신분들이 학습 여정에서 도움을 받을 수 있기를 바라는 마음에서입니다.

버튼 구성

분석기를 사용하여 볼 때, 이 PLAY 버튼은 WPF ToggleButton에서 속성을 상속받습니다. 왼쪽에는 “리그 오브 레전드” 게임의 로고가 있고, 오른쪽에는 경계선, 이미지, 텍스트 등 다양한 디자인의 요소들이 포함되어 있습니다. 또한, 마우스오버 및 체크 트리거 효과가 추가되었습니다.

주요 내용 분석

1.비정형 형태 만들기

첫 번째와 두 번째 그래픽은 Border 컨트롤을 사용하여 쉽게 인코딩할 수 있습니다. 하지만, 끝이 뾰족하고 호가 있는 세 번째 그래픽은 단순한 Border를 사용하여 인코딩할 수 없습니다. 따라서 처음에는 다각형과 좌표를 사용하여 그릴 수 있지만, 다각형 속성은 호를 그리는 기능을 제공하지 않습니다. 그래서 Path 컨트롤을 사용하여 인코딩해야 합니다.

상세 분석

<Style TargetType="{x:Type Path}" x:Key="Arrow">
    <Setter Property="Fill" Value="#1E2328"/>
    <Setter Property="Stroke" Value="{StaticResource ArrowStroke}"/>
    <Setter Property="StrokeThickness" Value="2"/>
    <Setter Property="Data" Value="M 0,0 L 103,0 L 118,14 L 103,28 L 0,28 C 10,14 0,0 0,0 Z"/>
    <Setter Property="Margin" Value="40 5 4 -5"/>
    <Setter Property="Effect">
        <Setter.Value>
            <DropShadowEffect BlurRadius="5" ShadowDepth="2"/>
        </Setter.Value>
    </Setter>
</Style>

WPF에서 Path 컨트롤은 다양한 형태와 윤곽을 그리는 데 사용되는 강력한 도구입니다. Path 컨트롤은 경로 데이터를 사용하여 형태를 정의하며, 경로 데이터는 형태를 그리는 방법을 지정하는 일련의 명령과 좌표로 구성됩니다.

Path 컨트롤의 기본 속성은 다음과 같습니다:

  • Data 속성: Data 속성은 Path 컨트롤의 중요한 속성으로, 형태의 윤곽을 설명하는 명령과 좌표로 구성된 경로 데이터를 지정하는 데 사용됩니다. 경로 데이터의 형식에는 MoveTo (M), LineTo (L), CurveTo (C), ClosePath (Z) 등 다양한 명령이 좌표와 결합되어 형태를 정의합니다.
  • Fill 속성: Fill 속성은 형태 내부를 채우는 색상을 지정하는 데 사용됩니다. 색상, 그라데이션, 패턴 또는 투명도를 사용하여 형태의 내부를 채울 수 있습니다.
  • Stroke 속성: Stroke 속성은 형태의 윤곽선 색상을 지정하는 데 사용됩니다. 다양한 색상을 사용하여 윤곽선의 색상을 정의할 수 있습니다.
  • StrokeThickness 속성: StrokeThickness 속성은 윤곽선의 두께를 지정하는 데 사용됩니다. 윤곽선의 너비를 결정합니다.
  • 명령과 좌표:경로 데이터는 명령과 좌표의 연속으로 구성되며, 이러한 명령은 WPF에게 한 지점에서 다른 지점까지 모양을 그리는 방법을 지시합니다. 일반적인 경로 명령에는 다음이 있습니다:

M (MoveTo): 지정된 좌표로 그리기 지점을 이동합니다.

L (LineTo): 지정된 좌표로 직선을 그립니다.

C (CurveTo): 제어 점을 사용하여 베지어 곡선을 그립니다.

Z (ClosePath): 경로를 닫아 현재 지점을 시작 지점에 연결하여 닫힌 모양을 형성합니다.

Data 속성은 경로 컨트롤의 중요한 속성으로, 모양의 윤곽을 정의하는 명령과 좌표를 포함하는 경로 데이터를 지정하는 데 사용됩니다. 경로 데이터는 경로의 윤곽을 설명하기 위해 명령의 연속을 사용합니다. 이 프로젝트의 경로 데이터에 대한 명령과 좌표의 자세한 설명은 다음과 같습니다.

이것을 간단하게 X/Y 좌표 축으로 해석할 수 있습니다. 이 모양의 길이를 118로 설정하고 너비를 28로 설정합니다:

M 0,0: 이것은 “MoveTo” 명령으로, 그리기 지점을 좌표 (0, 0)로 이동하여 시작 지점을 나타냅니다.

L 103,0: 이것은 “LineTo” 명령으로, 현재 지점 (0, 0)에서 좌표 (103, 0)로 직선을 그립니다. 이어서 (118, 14), (103, 28), (0, 28)로 선을 그립니다.

이것은 대칭 모양이므로 두 번째 선의 Y 좌표는 모양의 총 높이의 절반인 14입니다.

다음은 곡선을 그리는 부분입니다: C 10,14 0,0 0,0 z: 이것은 “베지어 곡선” 명령으로, 앞의 점이 제어 점이고 뒤의 점이 끝 점인 베지어 곡선을 정의합니다. 이 명령은 제어 점이 (10, 14)이고 끝 점이 (0, 0)인 베지어 곡선을 정의하며, ‘z’ 명령을 사용하여 경로를 닫아 시작 지점 (0, 0)에 연결합니다.

2.그라데이션 색상 만들기

LinearGradientBrush의 주요 속성 및 사용법

  • StartPoint및 EndPoint:
    StartPoint는 그라데이션의 시작점을 지정하며 일반적으로 상대 좌표를 사용하여 표시됩니다. 여기서 (0, 0)은 좌상단이고 (1, 1)은 우하단을 나타냅니다. EndPoint는 상대 좌표를 사용하여 그라데이션의 끝점을 지정합니다.
  • GradientStops:
    GradientStops은 GradientStop 개체의 컬렉션으로, 각 개체는 색상과 상대 위치(Offset)를 정의합니다. GradientStop의 Color 속성은 지정된 위치의 색상을 정의하고, Offset 속성은 그라데이션 내에서 색상의 위치를 정의합니다. 일반적으로 0에서 1 사이의 범위입니다.
  • 그라데이션 방향:
    그라데이션의 방향은 StartPoint 및 EndPoint에 의해 결정됩니다. 예를 들어 StartPoint가 (0, 0)이고 EndPoint가 (1, 1)인 경우 그라데이션은 좌상단에서 우하단으로 전환됩니다.
  • 그라데이션 유형:
    LinearGradientBrush는 기본적으로 직선 그라데이션으로 설정되어 있으며 색상이 직선을 따라 전환됩니다. StartPoint와 EndPoint를 조정하여 그라데이션의 방향과 시작점을 변경하여 다양한 그라데이션 효과를 생성할 수 있습니다.

이 프로젝트에서는 모양의 중앙에서 시작하여 아래로 이동하는 수직 그라데이션을 생성하려고 합니다. 따라서 StartPoint를 (0.5, 0)으로 설정하여 그라데이션의 시작점을 상단 중앙으로 지정하고 EndPoint는 (0.5, 1)로 설정하여 그라데이션의 끝점을 하단 중앙으로 지정합니다.

그 다음, GradientStops 컬렉션에는 서로 다른 색상과 상대적인 위치를 정의하는 세 개의 GradientStop 개체가 포함되어 있습니다:

  • 첫 번째 GradientStop:
    Color는 #CC3FE7EE로 설정되어 색상 값을 나타냅니다.
    Offset은 0으로 설정되어 이 색상이 그라데이션의 시작점에 위치함을 나타냅니다.

  • 두 번째 GradientStop:
    Color는 #CC006D7D로 설정됩니다.
    Offset은 0.5로 설정되어 이 색상이 그라데이션의 중간점에 위치함을 나타냅니다.

  • 세 번째 GradientStop:
    Color는 #CC0493A7로 설정됩니다.
    Offset은 1로 설정되어 이 색상이 그라데이션의 끝점에 위치함을 나타냅니다.

3. Path 및 Border Thickness 처리에 대해서

Border 컨트롤에서:

Border 컨트롤의 테두리 선은 Border 내에 포함됩니다. 테두리 선의 두께는 BorderThickness 속성에 의해 제어되며, 테두리 선의 너비를 디바이스 독립적인 픽셀(DIPs)로 지정합니다.

Path 컨트롤에서:

Path 컨트롤의 테두리 선은 StrokeThickness 속성의 중심 위치를 기준으로 그려집니다. StrokeThickness는 테두리 선의 두께를 제어하며, 중심으로부터 테두리 선이 얼마나 뻗어나갈지를 나타냅니다.

이 고정 크기의 그래픽에서 Border와 Path의 두께를 모두 2로 설정하고 Margin은 4 4 4 4로 설정합니다. 그러나 이 설정에서 Path의 상단 테두리가 Border를 벗어난 것을 확인할 수 있습니다.

따라서 StrokeThickness를 고려하여 Path의 Margin을 조정해야 합니다. 왼쪽 Margin은 이미 40으로 설정되어 GreenLine을 덮을 수 있으므로 문제가 없습니다. 상단 Margin은 1픽셀 증가하여 5픽셀로 설정해야 하며, 오른쪽 및 하단 Margin은 변경할 필요가 없습니다. Path의 크기가 118x28로 고정되어 있으므로 Margin을 조정해야 하는 것은 왼쪽 및 상단 Margin 뿐입니다.

또한 상단 Margin이 5픽셀 증가하면 하단이 잘린 것처럼 보일 수 있습니다. 이를 방지하려면 하단 Margin을 -5픽셀로 설정할 수 있습니다. 이렇게 하면 상단에 추가된 5픽셀이 제거되어 레이아웃이 균형을 이룹니다. 또 다른 방법은 하단 Margin을 0픽셀로 유지하는 것입니다. 이러한 두 가지 방법을 사용하여 상단 Margin을 추가함으로써 하단이 잘리는 것을 방지할 수 있습니다.

4. Jamesnet.WPF Nuget를 사용한 애니메이션 생성

WPF에서는 사용자 인터페이스를 더욱 흥미롭게 만들기 위해 다양한 동적 애니메이션을 생성할 수 있습니다. 이 프로젝트에서는 TextBlock의 텍스트 부분에 흥미로운 애니메이션을 추가하기 위해 두께 애니메이션을 사용합니다.

<Application x:Class="VickyPlayButton.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:james="https://jamesnet.dev/xaml/presentation"
             StartupUri="MainWindow.xaml">

    <Application.Resources>
        <Style TargetType="{x:Type ToggleButton}">
            <Setter Property="Height" Value="38"/>
            <Setter Property="Width" Value="165"/>
            <Setter Property="Foreground" Value="#FFFFFF"/>
            <Setter Property="Background" Value="Transparent"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ToggleButton}">
                        <ControlTemplate.Resources>
                            <Storyboard x:Key="Checked">
                                <james:ThickItem Mode="CubicEaseInOut" TargetName="play" Property="Margin" Duration="0:0:0:0.5" To="30 100 0 0"/>
                                <james:ThickItem Mode="CubicEaseInOut" TargetName="stop" Property="Margin" Duration="0:0:0:0.5" To="30 0 0 0"/>
                            </Storyboard>
                            <Storyboard x:Key="UnChecked">
                                <james:ThickItem Mode="CubicEaseInOut" TargetName="play" Property="Margin" Duration="0:0:0:0.5" To="30 0 0 0"/>
                                <james:ThickItem Mode="CubicEaseInOut" TargetName="stop" Property="Margin" Duration="0:0:0:0.5" To="30 0 0 100"/>
                            </Storyboard>
                        </ControlTemplate.Resources>
                        <Grid Background="{TemplateBinding Background}">
                            <Border Style="{StaticResource GoldLine}"/>
                            <Image Style="{StaticResource Emblem}"/>
                            <Border Style="{StaticResource GreenLine}"/>
                            <Path x:Name="path" Style="{StaticResource Arrow}"/>
                            <Grid>
                                <Grid.Clip>
                                    <RectangleGeometry Rect="0,5,165,28"/>
                                </Grid.Clip>
                                <TextBlock x:Name="play" Style="{StaticResource Play}"/>
                                <TextBlock x:Name="stop" Style="{StaticResource Stop}"/>
                            </Grid>
                        </Grid>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsMouseOver" Value="True">
                                <Setter TargetName="path" Property="Fill" Value="{StaticResource ArrowFillOver}"/>
                                <Setter TargetName="path" Property="Stroke" Value="{StaticResource ArrowStrokeOver}"/>
                                <Setter Property="Foreground" Value="#FFFCF1DC"/>
                                <Setter Property="Cursor" Value="Hand"/>
                            </Trigger>
                            <Trigger Property="IsChecked" Value="True">
                                <Setter TargetName="path" Property="Fill" Value="#1E2328"/>
                                <Setter TargetName="path" Property="Stroke" Value="#5C5B57"/>
                                <Setter Property="Foreground" Value="#3C3C41"/>
                                <Trigger.EnterActions>
                                    <BeginStoryboard Storyboard="{StaticResource Checked}"/>
                                </Trigger.EnterActions>
                                <Trigger.ExitActions>
                                    <BeginStoryboard Storyboard="{StaticResource UnChecked}"/>
                                </Trigger.ExitActions>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Application.Resources>
</Application>

애니메이션은 ControlTemplate.Resources를 사용하여 정의할 수 있으며 “Checked” 및 “UnChecked” 두 애니메이션 리소스를 정의할 수 있습니다. “Checked” 상태에서는 “Play” 텍스트가 제거되고 “Stop” 텍스트가 들어오며, “UnChecked” 상태에서는 “Stop” 텍스트가 제거되고 “Play” 텍스트가 들어옵니다. 이렇게 하면 뒤집는 효과를 연상시키는 애니메이션이 만들어집니다.

애니메이션을 생성하고 사용하기 쉽도록 WPF의 다양한 애니메이션을 Jamesnet.WPF Nuget 패키지로 컴파일하고 정리했습니다. 이 패키지를 추가하기만 하면 애니메이션을 쉽게 사용하고 작성할 수 있습니다.

5.Grid.Clip 속성 사용

Grid 내의 요소들이 서로 겹치기 때문에 텍스트가 위아래로 스크롤되는 애니메이션을 만들 때 텍스트가 테두리를 벗어나는 시각적 문제가 발생할 수 있습니다. 이를 해결하기 위해 <Grid.Clip> 속성을 사용합니다.

<Grid Background="{TemplateBinding Background}">
         <Border Style="{StaticResource GoldLine}"/>
         <Image Style="{StaticResource Emblem}"/>
         <Border Style="{StaticResource GreenLine}"/>
         <Path x:Name="path" Style="{StaticResource Arrow}"/>
      <Grid>
         <Grid.Clip>
             <RectangleGeometry Rect="0,5,165,28"/>
         </Grid.Clip>
             <TextBlock x:Name="play" Style="{StaticResource Play}"/>
             <TextBlock x:Name="stop" Style="{StaticResource Stop}"/>
      </Grid> 
  </Grid>

<Grid.Clip>은 자식 요소의 가시 영역을 제한하는 클리핑 영역을 정의하는 데 사용되는 XAML 요소입니다. 클리핑 영역은 일반적으로 사각형과 같은 모양이며, 클리핑 영역 내의 내용만 표시되고 그 밖의 내용은 숨겨집니다.

이 프로젝트에서는 <Grid.Clip> 영역이 Path의 크기 내에서 설정됩니다: Rect=“0,5,165,28”. 이렇게 하면 텍스트가 이 영역 내에서만 나타나므로 Path 내에서 위아래로 스크롤되는 효과를 얻을 수 있습니다.


끝으로, 과정을 영상으로 보고싶다면 아래 한글 자막과 함께 준비되어있습니다.

이것으로 내용을 마칩니다! 혹시 궁금한점은 댓글로 부탁드립니다.

22개의 좋아요

신규글은 링크를 1개 이상 추가할 수 없다고 해서 아래 추가적인 링크를 추가했습니다.

6개의 좋아요

환영합니다! 상세하면서 감성을 느낄 수 있는 글이였습니다!

5개의 좋아요

안녕하세요, 이재웅입니다.

어느 중국의 개발자분께서 영상의 잘못된 설명을 지적해주셔서 그 내용을 가져왔습니다.

11:34贝塞尔曲线里C不是弧线的中点吧,控制点是在线外边的

“베지어 곡선에서 C는 곡선의 중간점이 아니에요. 제어점은 선 외부에 있어요.”

실제로 11:34 영상을 찾아보면 아래와 같습니다. (본문에 설명과 동일한 이미지)

다시 확인해보니 이해를 완전 잘못한 체로 설명이 되어 있었네요. 뜨끔 하네요…

빨리 정정해보도록 하겠습니다.



먼저, 영상에 사용된 Path는 아래와 같습니다.

M 0,0 L 103,0 L 118,14 L 103,28 L 0,28 C 10,14 0,0 0,0 Z

아래 Y축의 0, 0 기준이 반전된 것은 크게 문제 없으니 이해해주세요.

GPT를 통해 만들어본 그래프를 보면, C (10, 14)의 제어점 위치는 아이패드 설명에서의 곡선에 걸친 점 위치가 아닌 바로 보이는 것처럼 곡선을 벗어난 좀 더 안쪽의 x 지점입니다.

바로 이것을 두고 “제어점이 곡선 외부에 있다” 지적해주셨습니다.



이제, Cubic Bezier의 곡선 메커니즘에 대해 좀 더 자세히 살펴보겠습니다.

기본적으로 P0, P1, P2, P3 총 4개의 포인트가 필요합니다.

따라서 현재 Geometry Path 데이터를 기준으로 값을 각각 매핑해보면 아래와 같습니다.

P0: 0,28 (시작점)
P1: 10,14 (제어점1)
P2: 0,0 (제어점2)
P3: 0,0 (끝점)

그리고, 그려지는 곡선에 시간을 추가해서 타임라인 과정도 한번 만들어보았습니다.

파란 라인의 시작과 끝점이 제어점을 기준으로 움직이면서 곡선을 그리게 되는데, 이건 시각화된 자료로 보시는게 좋습니다. 여기 애니메이션으로 잘 정리된 블로그를 확인해보시기 바랍니다.

유튜브에도 여기로 링크를 걸어두어야겠네요…

혹시 궁금한 점 있으시면, 아래 댓글로 남겨주세요.

9개의 좋아요

UI 는 역시 정성이네요.

많이 배워갑니다.

4개의 좋아요

위의 상황에서는 Cubic Bezier 보다는 Quadratic Bezier 를 쓰는 게 나은 거 같네요

2개의 좋아요

Cubic Bezier

M 0,0 L 103,0 L 118,14 L 103,28 L 0,28 C 10,14 0,0 0,0 Z

Quadratic Bezier

M 0,0 L 103,0 L 118,14 L 103,28 L 0,28 Q 10,14 0,0 Z

더 적은 제어점으로 표현할 수 있기 때문에 지금 같은 경우에는 Quadratic Bezier를 사용하는 것이 맞는 것 같습니다.

@나그네 말씀이 맞습니다.

5개의 좋아요

이런거 보면 저는 역시 백엔드 개발이 맞는거 같아요…

3개의 좋아요

극히 공감합니다…
HTML JS CSS 보기만 해도 우엑…ㅠㅠ;;

2개의 좋아요