프로그램 개발/WPF: Style&Template

RadioButton

(ㅇㅅㅎ) 2022. 6. 16. 14:40
728x90
반응형

 

 안녕하세요, 이번 글에서는 Microsoft에서 제공하는 WPF [RadioButton의 스타일 및 템플릿] 예제를 톺아보겠습니다.

 

 

 

RadioButton

 사용자가 선택할 수는 있지만 선택을 취소할 수 없는 단추를 나타냅니다. RadioButton의 속성 중 IsChecked를 사용하여 프로그래밍 방식으로만 선택을 취소할 수 있습니다.

 

 

 

RadioButton Style

 예제의 XAML 코드를 RadioButton에 적용하면 위와 같은 결과가 나옵니다. 이러한 결과에 도달하도록 코드 순서로 속성을 살펴보면 다음과 같습니다.

 

 

RadioButton 속성

<Style x:Key="{x:Type RadioButton}" TargetType="{x:Type RadioButton}">
  <Setter Property="SnapsToDevicePixels" Value="true" />
  <Setter Property="OverridesDefaultStyle" Value="true" />
  <Setter Property="FocusVisualStyle" Value="{DynamicResource RadioButtonFocusVisual}" />
  <Setter Property="Template">
    <Setter.Value>
      ...
    </Setter.Value>
  </Setter>
</Style>

x:Key

 XAML 정의 사전에서 만들고 참고하는 요소를 고유하게 식별합니다. 문자를 이용하여 값을 설정하지만 예제에서처럼 x:Type을 사용하여 Control에 바로 적용하도록 설정할 수 있습니다.

 

 

TargetType

 이 Style을 적용할 형식을 설정합니다.

 

 

SnapsToDevicePixels

 렌더링 하는 동안 RadioButton의 렌더링에 디바이스 관련 픽셀 스냅(Pixel Snap)을 사용할지 여부를 설정합니다. 픽셀 스냅을 사용하면 값을 true로 설정하고, 그렇지 않으면 false로 설정합니다.

⭐ 픽셀 스냅(Pixel Snap)을 사용하는 이유

더보기

 WPF는 시스템 DPI 설정에 맞게 자동으로 크기를 조정하게 됩니다. 조정할 때 가장자리가 흐려지거나 반투명하게 표시되는 문제가 발생하는데, 이 문제를 해결하기 위해서 픽셀 스냅 기능을 사용하여 객체의 가장자리를 픽셀에 맞추어 고정시키는 기능을 제공합니다. 픽셀 스냅을 사용하면 객체에 작은 단위의 offset을 적용하여 객체의 크기를 픽셀에 맞추거나 일부분을 렌더링 시점에서 제거하여 해결하는 방법입니다.


 

 

OverridesDefaultStyle

 테마 스타일의 스타일 속성을 포함할지 여부를 설정합니다. 이 요소가 테마 스타일 속성을 사용하지 않으면 true이고, 사용하면 false입니다.

 

 

FocusVisualStyle

 RadioButton에 키보드 포커스가 있는 경우 RadioButton에 적용되는 스타일을 설정합니다. 예제에서는 RadioButtonFocusVisual로 설정했지만 RadioButtonFocusVisual로 미리 정의된 것이 없기 때문에 예제에서 없어도 되는 속성입니다. 만약 특정 스타일로 적용하고 싶을 경우 아래와 같이 설정하시면 됩니다.

<Style x:Key="RadioButtonFocusVisual">
  <Setter Property="Control.Template">
    <Setter.Value>
      <ControlTemplate>
        <Border>
          <Rectangle Margin="2" StrokeThickness="1" Stroke="#60000000" StrokeDashArray="1 2" />
        </Border>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

FocusVisualStyle

 

 

Template

<Setter Property="Template">
    <Setter.Value>
        <ControlTemplate TargetType="{x:Type RadioButton}">
            <BulletDecorator Background="Transparent">
                <BulletDecorator.Bullet>
                    <Grid ... >
                        <Ellipse x:Name="Border" ... >
                            ...
                        </Ellipse>

                        <Ellipse x:Name="CheckMark" ... >
                            ...
                        </Ellipse>
                    </Grid>
                </BulletDecorator.Bullet>

                <VisualStateManager.VisualStateGroups>
                    ...
                </VisualStateManager.VisualStateGroups>

                <ContentPresenter ... />
            </BulletDecorator>
        </ControlTemplate>
    </Setter.Value>
</Setter>

 RadioButton의 템플릿을 설정합니다. 예제에서 사용되는 Template는 RadioButton의 외형을 지정해줄 수 있는 ControlTemplate입니다.

👀 Template에 대해서 좀 더 아시고 싶으신 분은 이 페이지를 참고하시길 바랍니다.

 

 

 

RadioButton Template 구성

 RadioButton은 BulletDecorator를 사용하여 Bullet 부분과 ContentPresenter로 나누어서 구성되어 있습니다. Bullet의 경우 Grid 내부에 2개의 Ellipse를 구성하여 이벤트를 통해서 RadioButton의 true/false를 표현했습니다.

 

BulletDecorator

 글머리 기호와 다른 시각적 개체를 맞추는 레이아웃 Control을 나타냅니다.

⭐ BulletDecorator 속성

더보기
<BulletDecorator Background="Transparent">
  <BulletDecorator.Bullet>
    ...
  </BulletDecorator.Bullet>
  ...
</BulletDecorator>

 

Background

 BulletDecorator의 배경색을 설정합니다. 예제에서는 Transparent(투명)로 설정했습니다.

 

Bullet

 BulletDecorator에서 글머리 기호로 사용할 개체를 설정합니다.


 

Grid

 열 및 행으로 구성되는 유연한 모눈 영역을 정의합니다.

⭐ Grid 속성

더보기
<Grid Width="13" Height="13">
  ...
</Grid>

 

Width

 Grid의 너비를 설정합니다.

 

Height

 Grid의 높이를 설정합니다.


 

Ellipse

 타원을 그립니다.

⭐ Ellipse 속성

더보기

[ Border ]

<Ellipse x:Name="Border" StrokeThickness="1">
    <Ellipse.Stroke>
        <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
            <GradientStop Color="{DynamicResource BorderLightColor}" Offset="0" />
            <GradientStop Color="{DynamicResource BorderDarkColor}" Offset="1" />
        </LinearGradientBrush>
    </Ellipse.Stroke>
    <Ellipse.Fill>
        <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
            <LinearGradientBrush.GradientStops>
                <GradientStopCollection>
                    <GradientStop Color="{DynamicResource ControlLightColor}" />
                    <GradientStop Color="{DynamicResource ControlMediumColor}" Offset="1.0" />
                </GradientStopCollection>
            </LinearGradientBrush.GradientStops>
        </LinearGradientBrush>
    </Ellipse.Fill>
</Ellipse>

[ CheckMark ]

<Ellipse x:Name="CheckMark" Margin="4" Visibility="Collapsed">
    <Ellipse.Fill>
        <SolidColorBrush Color="{DynamicResource GlyphColor}" />
    </Ellipse.Fill>
</Ellipse>

 

x:Name

 Ellipse에 이름을 설정합니다. 이름을 설정하는 것과 동시에 다른 곳에서 사용할 수 있도록 만들어줍니다. 

 

StrokeThickness

 도형 윤곽선의 두께를 설정합니다.

 

Stroke

 도형 윤곽선 색을 설정합니다. 예제에서는 LinearGradientBrush를 사용하여 그라데이션 색으로 설정했습니다.

 

Fill

 도형의 내부 색을 설정합니다. 예제에서는 LinearGradientBrush를 사용하여 그라데이션 색으로 설정하거나 SolidColorBrush를 사용하여 미리 정의한 색인 GlyphColor로 설정했습니다.

👀 그라데이션 색에 대해서 알고 싶으신 분은 이 페이지를 참고하시길 바랍니다.

 

Margin

 Ellipse의 외부 여백을 설정합니다.

 

Visibility

 Ellipse의 표시 유형을 설정합니다. 기본 값은 Visible이며 다른 값으로는 Collapsed와 Hidden이 있습니다.


 

ContentPresenter

 ContentControl의 내용을 표시합니다.

⭐ ContentPresenter 속성

더보기
<ContentPresenter Margin="4,0,0,0" VerticalAlignment="Center" HorizontalAlignment="Left" RecognizesAccessKey="True" />

 

Margin

 ContentPresenter의 외부 여백을 설정합니다. 예제에서는 4개(좌, 상, 우, 하)로 나누어서 설정했습니다.

 

VerticalAlignment

 ContentPresenter의 세로 정렬 위치를 설정합니다. 기본값은 Stretch이며, 다른 값으로는 Top, Center, Bottom이 있습니다.

 

HorizontalAlignment

 ContentPresenter의 가로 정렬 위치를 설정합니다. 기본값은 Stretch이며, 다른 값으로는 Lect, Center, Right가 있습니다.

 

RecognizesAccessKey

 ContentPresenter의 스타일에 AccessText가 사용되는지 여부를 설정합니다. AccessText가 사용되면 true이고, 그렇지 않으면 false입니다.

Alt + R

 

 

 

RadioButton 이벤트

 예제에서는 이벤트로 MouseOver, Pressed, Disabled와 Checked 변화를 VisualStateManager로 표현하였습니다.

 

VisualStateManager

<VisualStateManager.VisualStateGroups>
    <VisualStateGroup x:Name="CommonStates">
        ...
    </VisualStateGroup>
</VisualStateManager.VisualStateGroups>

 VisualState는 Control의 시각적 상태를 나타냅니다. 예제의 이벤트에서 시각적 변화를 주기 위해서 VisualStateGroup의 x:Name을 CommonStates와 CheckStates로 설정했습니다.

CommonStates

더보기
<VisualStateGroup x:Name="CommonStates">
    <VisualState x:Name="Normal" />
    <VisualState x:Name="MouseOver">
        <Storyboard>
            <ColorAnimationUsingKeyFrames Storyboard.TargetName="Border" Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)">
                <EasingColorKeyFrame KeyTime="0" Value="{StaticResource ControlMouseOverColor}" />
            </ColorAnimationUsingKeyFrames>
        </Storyboard>
    </VisualState>
    <VisualState x:Name="Pressed">
        <Storyboard>
            <ColorAnimationUsingKeyFrames Storyboard.TargetName="Border" Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)">
                <EasingColorKeyFrame KeyTime="0" Value="{StaticResource ControlPressedColor}" />
            </ColorAnimationUsingKeyFrames>
        </Storyboard>
    </VisualState>
    <VisualState x:Name="Disabled">
        <Storyboard>
            <ColorAnimationUsingKeyFrames Storyboard.TargetName="Border" Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)">
                <EasingColorKeyFrame KeyTime="0" Value="{StaticResource ControlLightColor}" />
            </ColorAnimationUsingKeyFrames>
            <ColorAnimationUsingKeyFrames Storyboard.TargetName="Border" Storyboard.TargetProperty="(Shape.Stroke).(GradientBrush.GradientStops)[1].(GradientStop.Color)">
                <EasingColorKeyFrame KeyTime="0" Value="#40000000" />
            </ColorAnimationUsingKeyFrames>
            <ColorAnimationUsingKeyFrames Storyboard.TargetName="Border" Storyboard.TargetProperty="(Shape.Stroke).(GradientBrush.GradientStops)[0].(GradientStop.Color)">
                <EasingColorKeyFrame KeyTime="0" Value="#40000000" />
            </ColorAnimationUsingKeyFrames>
        </Storyboard>
    </VisualState>
</VisualStateGroup>

 MouseOver & Pressed

 Storyboard의 내용을 풀어보면 ["Border"의 Fill GradientStops 묶음의 색 중 1번(0번부터 시작)을 변경하는 것]입니다.

 

Disabled

Storyboard의 내용을 풀어보면 ["Border"의 Fill GradientStops 묶음의 색 중 1번과 Stroke GradientStops 묶음의 색 0번과 1번을 변경하는 것]입니다.

 

ColorAnimationUsingKeyFrames

 KeyFrames 집합을 따라 Color 속성 값에 EasingColorKeyFrame의 애니메이션 효과를 줍니다.


  

CheckStates

더보기
<VisualStateGroup x:Name="CheckStates">
    <VisualState x:Name="Checked">
        <Storyboard>
            <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="CheckMark">
                <DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Visible}" />
            </ObjectAnimationUsingKeyFrames>
        </Storyboard>
    </VisualState>
    <VisualState x:Name="Unchecked" />
    <VisualState x:Name="Indeterminate" />
</VisualStateGroup>

 Checked

 Storyboard의 내용을 풀어보면 ["CheckMark"의 Visibility를 VIsible로 변경하는 것]입니다.

 

ObjectAnimationUsingKeyFrames

 지정된 Object에 대해 KeyFrames 집합을 따라 Duration 속성 값에 애니메이션 효과를 줍니다.

 

DiscreteObjectKeyFrame

 불연속 보간을 사용하여 이전 키 프레임의 Object 값에서 고유 Value로 애니메이션 효과를 적용합니다.


 

 

 


 

 

 

전체 코드

더보기
<Style x:Key="{x:Type RadioButton}"
       TargetType="{x:Type RadioButton}">
  <Setter Property="SnapsToDevicePixels"
          Value="true" />
  <Setter Property="OverridesDefaultStyle"
          Value="true" />
  <Setter Property="FocusVisualStyle"
          Value="{DynamicResource RadioButtonFocusVisual}" />
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="{x:Type RadioButton}">
        <BulletDecorator Background="Transparent">
          <BulletDecorator.Bullet>
            <Grid Width="13"
                  Height="13">
              <Ellipse x:Name="Border"
                       StrokeThickness="1">
                <Ellipse.Stroke>
                  <LinearGradientBrush EndPoint="0.5,1"
                                       StartPoint="0.5,0">
                    <GradientStop Color="{DynamicResource BorderLightColor}"
                                  Offset="0" />
                    <GradientStop Color="{DynamicResource BorderDarkColor}"
                                  Offset="1" />
                  </LinearGradientBrush>
                </Ellipse.Stroke>
                <Ellipse.Fill>
                  <LinearGradientBrush StartPoint="0,0"
                                       EndPoint="0,1">
                    <LinearGradientBrush.GradientStops>
                      <GradientStopCollection>
                        <GradientStop Color="{DynamicResource ControlLightColor}" />
                        <GradientStop Color="{DynamicResource ControlMediumColor}"
                                      Offset="1.0" />
                      </GradientStopCollection>
                    </LinearGradientBrush.GradientStops>
                  </LinearGradientBrush>
                </Ellipse.Fill>
              </Ellipse>
              <Ellipse x:Name="CheckMark"
                       Margin="4"
                       Visibility="Collapsed">
                <Ellipse.Fill>
                  <SolidColorBrush Color="{DynamicResource GlyphColor}" />
                </Ellipse.Fill>
              </Ellipse>
            </Grid>
          </BulletDecorator.Bullet>
          <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="CommonStates">
              <VisualState x:Name="Normal" />
              <VisualState x:Name="MouseOver">
                <Storyboard>
                  <ColorAnimationUsingKeyFrames Storyboard.TargetName="Border"
                                                Storyboard.TargetProperty="(Shape.Fill).
                    (GradientBrush.GradientStops)[1].(GradientStop.Color)">
                    <EasingColorKeyFrame KeyTime="0"
                                         Value="{StaticResource ControlMouseOverColor}" />
                  </ColorAnimationUsingKeyFrames>
                </Storyboard>
              </VisualState>
              <VisualState x:Name="Pressed">
                <Storyboard>
                  <ColorAnimationUsingKeyFrames Storyboard.TargetName="Border"
                                                Storyboard.TargetProperty="(Shape.Fill).
                    (GradientBrush.GradientStops)[1].(GradientStop.Color)">
                    <EasingColorKeyFrame KeyTime="0"
                                         Value="{StaticResource ControlPressedColor}" />
                  </ColorAnimationUsingKeyFrames>
                </Storyboard>
              </VisualState>
              <VisualState x:Name="Disabled">
                <Storyboard>
                  <ColorAnimationUsingKeyFrames Storyboard.TargetName="Border"
                                                Storyboard.TargetProperty="(Shape.Fill).
                    (GradientBrush.GradientStops)[1].(GradientStop.Color)">
                    <EasingColorKeyFrame KeyTime="0"
                                         Value="{StaticResource ControlLightColor}" />
                  </ColorAnimationUsingKeyFrames>
                  <ColorAnimationUsingKeyFrames Storyboard.TargetName="Border"
                                                Storyboard.TargetProperty="(Shape.Stroke).
                    (GradientBrush.GradientStops)[1].(GradientStop.Color)">
                    <EasingColorKeyFrame KeyTime="0"
                                         Value="#40000000" />
                  </ColorAnimationUsingKeyFrames>
                  <ColorAnimationUsingKeyFrames Storyboard.TargetName="Border"
                                                Storyboard.TargetProperty="(Shape.Stroke).
                    (GradientBrush.GradientStops)[0].(GradientStop.Color)">
                    <EasingColorKeyFrame KeyTime="0"
                                         Value="#40000000" />
                  </ColorAnimationUsingKeyFrames>
                </Storyboard>
              </VisualState>
            </VisualStateGroup>
            <VisualStateGroup x:Name="CheckStates">
              <VisualState x:Name="Checked">
                <Storyboard>
                  <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)"
                                                 Storyboard.TargetName="CheckMark">
                    <DiscreteObjectKeyFrame KeyTime="0"
                                            Value="{x:Static Visibility.Visible}" />
                  </ObjectAnimationUsingKeyFrames>
                </Storyboard>
              </VisualState>
              <VisualState x:Name="Unchecked" />
              <VisualState x:Name="Indeterminate" />
            </VisualStateGroup>
          </VisualStateManager.VisualStateGroups>
          <ContentPresenter Margin="4,0,0,0"
                            VerticalAlignment="Center"
                            HorizontalAlignment="Left"
                            RecognizesAccessKey="True" />
        </BulletDecorator>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>
<!--Control colors.-->
<Color x:Key="WindowColor">#FFE8EDF9</Color>
<Color x:Key="ContentAreaColorLight">#FFC5CBF9</Color>
<Color x:Key="ContentAreaColorDark">#FF7381F9</Color>

<Color x:Key="DisabledControlLightColor">#FFE8EDF9</Color>
<Color x:Key="DisabledControlDarkColor">#FFC5CBF9</Color>
<Color x:Key="DisabledForegroundColor">#FF888888</Color>

<Color x:Key="SelectedBackgroundColor">#FFC5CBF9</Color>
<Color x:Key="SelectedUnfocusedColor">#FFDDDDDD</Color>

<Color x:Key="ControlLightColor">White</Color>
<Color x:Key="ControlMediumColor">#FF7381F9</Color>
<Color x:Key="ControlDarkColor">#FF211AA9</Color>

<Color x:Key="ControlMouseOverColor">#FF3843C4</Color>
<Color x:Key="ControlPressedColor">#FF211AA9</Color>


<Color x:Key="GlyphColor">#FF444444</Color>
<Color x:Key="GlyphMouseOver">sc#1, 0.004391443, 0.002428215, 0.242281124</Color>

<!--Border colors-->
<Color x:Key="BorderLightColor">#FFCCCCCC</Color>
<Color x:Key="BorderMediumColor">#FF888888</Color>
<Color x:Key="BorderDarkColor">#FF444444</Color>

<Color x:Key="PressedBorderLightColor">#FF888888</Color>
<Color x:Key="PressedBorderDarkColor">#FF444444</Color>

<Color x:Key="DisabledBorderLightColor">#FFAAAAAA</Color>
<Color x:Key="DisabledBorderDarkColor">#FF888888</Color>

<Color x:Key="DefaultBorderBrushDarkColor">Black</Color>

<!--Control-specific resources.-->
<Color x:Key="HeaderTopColor">#FFC5CBF9</Color>
<Color x:Key="DatagridCurrentCellBorderColor">Black</Color>
<Color x:Key="SliderTrackDarkColor">#FFC5CBF9</Color>

<Color x:Key="NavButtonFrameColor">#FF3843C4</Color>

<LinearGradientBrush x:Key="MenuPopupBrush"
                     EndPoint="0.5,1"
                     StartPoint="0.5,0">
  <GradientStop Color="{DynamicResource ControlLightColor}"
                Offset="0" />
  <GradientStop Color="{DynamicResource ControlMediumColor}"
                Offset="0.5" />
  <GradientStop Color="{DynamicResource ControlLightColor}"
                Offset="1" />
</LinearGradientBrush>

<LinearGradientBrush x:Key="ProgressBarIndicatorAnimatedFill"
                     StartPoint="0,0"
                     EndPoint="1,0">
  <LinearGradientBrush.GradientStops>
    <GradientStopCollection>
      <GradientStop Color="#000000FF"
                    Offset="0" />
      <GradientStop Color="#600000FF"
                    Offset="0.4" />
      <GradientStop Color="#600000FF"
                    Offset="0.6" />
      <GradientStop Color="#000000FF"
                    Offset="1" />
    </GradientStopCollection>
  </LinearGradientBrush.GradientStops>
</LinearGradientBrush>

 

 

 


 

 

 

 이 글의 내용은 아래의 사이트에서 기초합니다.

https://docs.microsoft.com/ko-kr/dotnet/desktop/wpf/controls/radiobutton-styles-and-templates?view=netframeworkdesktop-4.8 

 

RadioButton 스타일 및 템플릿 - WPF .NET Framework

RadioButton 스타일 및 템플릿 아티클 05/08/2022 읽는 데 3분 걸림 기여자 1명 이 문서의 내용 --> 이 항목에서는 RadioButton 컨트롤에 대한 스타일 및 템플릿을 설명합니다. 기본값을 수정할 수 있습니다

docs.microsoft.com

 

반응형

'프로그램 개발 > WPF: Style&Template' 카테고리의 다른 글

ScrollBar①  (0) 2022.06.17
RepeatButton  (0) 2022.06.16
ProgressBar  (0) 2022.06.15
PasswordBox  (0) 2022.06.15
NavigationWindow③  (0) 2022.06.14