사용자 지정 가능한 모양이 있는 컨트롤 만들기
Windows Presentation Foundation (WPF)를 사용하면 모양을 사용자 지정할 수 있는 컨트롤을 만들 수 있습니다. 예를 들어 새 ControlTemplate을 만들면 속성 설정을 통해 변경할 수 있는 것 이상으로 CheckBox의 모양을 변경할 수 있습니다. 다음 그림에서는 기본 ControlTemplate을 사용하는 CheckBox와 사용자 지정 ControlTemplate을 사용하는 CheckBox를 보여 줍니다.
기본 컨트롤 템플릿을 사용하는 확인란
사용자 지정 컨트롤 템플릿을 사용하는 확인란
컨트롤을 만들 때 부분 및 상태 모델을 사용하면 컨트롤의 모양을 사용자 지정할 수 있습니다. Microsoft Expression Blend와 같은 디자이너 도구는 부분 및 상태 모델을 지원하므로 이 모델을 따르는 경우 해당 종류의 응용 프로그램에서 컨트롤을 사용자 지정할 수 있습니다. 이 항목에서는 부분 및 상태 모델 및 컨트롤을 만들 때 이 모델을 사용하는 방법에 대해 설명합니다. 이 항목에서는 사용자 지정 컨트롤의 예로 NumericUpDown을 사용하여 이 모델의 원리를 보여 줍니다. NumericUpDown 컨트롤은 사용자가 컨트롤의 단추를 클릭하여 늘리거나 줄일 수 있는 숫자 값을 표시합니다. 다음 그림에서는 이 항목에서 설명한 NumericUpDown 컨트롤을 보여 줍니다.
사용자 지정 NumericUpDown 컨트롤
이 항목에는 다음과 같은 단원이 포함되어 있습니다.
사전 요구 사항
부분 및 상태 모델
ControlTemplate에서 컨트롤의 시각적 구조 및 시각적 동작 정의
코드에서 ControlTemplate의 부분 사용
컨트롤 계약 제공
완성된 예제
사전 요구 사항
이 항목에서는 독자가 기존 컨트롤에 대한 새 ControlTemplate을 만드는 방법을 알고 있으며 컨트롤 계약의 요소에 익숙하고 ControlTemplate을 만들어 기존 컨트롤의 모양 사용자 지정에 설명된 개념을 이해하고 있다고 가정합니다.
참고 |
---|
모양을 사용자 지정할 수 있는 컨트롤을 만들려면 Control 클래스 또는 해당 서브클래스 중 UserControl 이외의 클래스에서 상속하는 컨트롤을 만들어야 합니다.UserControl에서 상속하는 컨트롤은 빠르게 만들 수 있지만 ControlTemplate을 사용하지 않으므로 모양을 사용자 지정할 수 없습니다. |
부분 및 상태 모델
부분 및 상태 모델에서는 컨트롤의 시각적 구조 및 시각적 동작을 정의하는 방법을 지정합니다. 부분 및 상태 모델을 사용하려면 다음을 수행해야 합니다.
컨트롤의 ControlTemplate에서 시각적 구조 및 시각적 동작을 정의합니다.
컨트롤의 논리가 컨트롤 템플릿의 부분과 상호 작용하는 경우 특정 최선의 방법을 따릅니다.
ControlTemplate에 반드시 포함되어야 하는 내용을 지정하는 컨트롤 계약을 제공합니다.
컨트롤의 ControlTemplate에 시각적 구조 및 시각적 동작을 정의한 경우 응용 프로그램 작성자는 코드를 작성하는 대신 새 ControlTemplate을 만들어 컨트롤의 시각적 구조 및 시각적 동작을 변경할 수 있습니다. ControlTemplate에 정의해야 하는 FrameworkElement 개체 및 상태를 지시하는 컨트롤 계약을 응용 프로그램 작성자에게 제공해야 합니다. ControlTemplate의 부분과 상호 작용할 때는 컨트롤이 불완전한 ControlTemplate을 적절하게 처리할 수 있도록 최선의 방법을 따라야 합니다. 이 세 가지 원칙을 따른다면 응용 프로그램 작성자는 WPF와 함께 제공되는 컨트롤의 경우와 마찬가지로 손쉽게 사용자 컨트롤에 대한 ControlTemplate을 만들 수 있습니다. 다음 단원에서는 이러한 권장 사항에 대해 자세히 설명합니다.
ControlTemplate에서 컨트롤의 시각적 구조 및 시각적 동작 정의
부분 및 상태 모델을 사용하여 사용자 지정 컨트롤을 만드는 경우에는 해당 논리 대신 해당 ControlTemplate에 컨트롤의 시각적 구조 및 시각적 동작을 정의합니다. 컨트롤의 시각적 구조는 컨트롤을 구성하는 FrameworkElement 개체를 합성한 것입니다. 시각적 동작은 컨트롤이 특정 상태에 있을 때 표시되는 방식입니다. 컨트롤의 시각적 구조 및 시각적 동작을 지정하는 ControlTemplate을 만드는 방법에 대한 자세한 내용은 ControlTemplate을 만들어 기존 컨트롤의 모양 사용자 지정을 참조하십시오.
NumericUpDown 컨트롤 예제에서 시각적 구조는 두 개의 RepeatButton 컨트롤과 하나의 TextBlock을 포함합니다. NumericUpDown 컨트롤의 코드(예: 해당 생성자 내부)에서 이러한 컨트롤을 추가하는 경우 이 컨트롤의 위치를 변경할 수 없습니다. 컨트롤의 시각적 구조 및 시각적 동작을 코드에서 정의하는 대신 ControlTemplate에서 정의해야 합니다. 이렇게 하면 ControlTemplate을 교체할 수 있으므로 응용 프로그램 개발자는 단추 및 TextBlock의 위치를 사용자 지정하고 Value가 음수일 때 발생하는 동작을 지정할 수 있습니다.
다음 예제에서는 Value를 늘리기 위한 RepeatButton, Value를 줄이기 위한 RepeatButton 및 Value를 표시하기 위한 TextBlock을 포함하는 NumericUpDown 컨트롤의 시각적 구조를 보여 줍니다.
<ControlTemplate TargetType="src:NumericUpDown">
<Grid Margin="3"
Background="{TemplateBinding Background}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Border BorderThickness="1" BorderBrush="Gray"
Margin="7,2,2,2" Grid.RowSpan="2"
Background="#E0FFFFFF"
VerticalAlignment="Center"
HorizontalAlignment="Stretch">
<!--Bind the TextBlock to the Value property-->
<TextBlock Name="TextBlock"
Width="60" TextAlignment="Right" Padding="5"
Text="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type src:NumericUpDown}},
Path=Value}"/>
</Border>
<RepeatButton Content="Up" Margin="2,5,5,0"
Name="UpButton"
Grid.Column="1" Grid.Row="0"/>
<RepeatButton Content="Down" Margin="2,0,5,5"
Name="DownButton"
Grid.Column="1" Grid.Row="1"/>
<Rectangle Name="FocusVisual" Grid.ColumnSpan="2" Grid.RowSpan="2"
Stroke="Black" StrokeThickness="1"
Visibility="Collapsed"/>
</Grid>
</Grid>
</ControlTemplate>
NumericUpDown 컨트롤의 시각적 동작은 값이 음수인 경우 값을 빨간색 글꼴로 표시하는 것입니다. Value가 음수일 때 코드에서 TextBlock의 Foreground를 변경하면 NumericUpDown은 항상 빨간색 음수 값을 표시하게 됩니다. ControlTemplate에 VisualState 개체를 추가하여 ControlTemplate에서 컨트롤의 시각적 동작을 지정합니다. 다음 예제에서는 Positive 및 Negative 상태에 대한 VisualState 개체를 보여 줍니다. Positive와 Negative는 상호 배타적이므로(즉 컨트롤은 항상 둘 중 하나의 상태에만 존재할 수 있음) 이 예제에서는 VisualState 개체를 단일 VisualStateGroup에 배치합니다. 컨트롤이 Negative 상태가 되면 TextBlock의 Foreground가 빨간색이 됩니다. 컨트롤이 Positive 상태에 있으면 Foreground가 원래 값으로 돌아갑니다. ControlTemplate에서 VisualState 개체를 정의하는 방법은 ControlTemplate을 만들어 기존 컨트롤의 모양 사용자 지정에서 더 자세하게 설명합니다.
참고 |
---|
ControlTemplate의 루트 FrameworkElement에는 연결된 속성 VisualStateManager.VisualStateGroups를 설정해야 합니다. |
<ControlTemplate TargetType="local:NumericUpDown">
<Grid Margin="3"
Background="{TemplateBinding Background}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="ValueStates">
<!--Make the Value property red when it is negative.-->
<VisualState Name="Negative">
<Storyboard>
<ColorAnimation To="Red"
Storyboard.TargetName="TextBlock"
Storyboard.TargetProperty="(Foreground).(Color)"/>
</Storyboard>
</VisualState>
<!--Return the TextBlock's Foreground to its
original color.-->
<VisualState Name="Positive"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
코드에서 ControlTemplate의 부분 사용
ControlTemplate 작성자가 의도적이거나 실수로 FrameworkElement 또는 VisualState 개체를 빠뜨릴 수 있지만 컨트롤의 논리가 올바르게 작동하려면 이러한 부분이 있어야 합니다. 부분 및 상태 모델에 따르면 ControlTemplate에 FrameworkElement 또는 VisualState 개체가 누락된 경우에도 컨트롤이 계속 작동할 수 있어야 합니다. ControlTemplate에 FrameworkElement, VisualState, 또는 VisualStateGroup이 없더라도 컨트롤에서 예외를 throw하거나 오류를 보고해서는 안됩니다. 이 단원에서는 FrameworkElement 개체와 상호 작용하고 상태를 관리하는 데 권장되는 방법에 대해 설명합니다.
누락된 FrameworkElement 개체 예상
ControlTemplate에 FrameworkElement 개체를 정의하는 경우 컨트롤의 논리에서 이러한 개체 중 일부와 상호 작용해야 하는 경우가 있을 수 있습니다. 예를 들어 NumericUpDown 컨트롤은 단추의 Click 이벤트를 구독하여 Value를 늘리거나 줄이고 TextBlock의 Text 속성을 Value로 설정합니다. 사용자 지정 ControlTemplate에 TextBlock 또는 단추가 누락된 경우 컨트롤이 일부 기능을 상실하는 것은 허용되지만 컨트롤에서 오류가 발생해서는 안됩니다. 예를 들어 ControlTemplate에 Value를 변경하는 단추가 없는 경우 NumericUpDown은 해당 기능을 상실하지만 ControlTemplate을 사용하는 응용 프로그램은 계속하여 실행됩니다.
다음 방법을 사용하면 누락된 FrameworkElement 개체에 컨트롤이 적절하게 응답할 수 있습니다.
코드에서 참조해야 하는 각 FrameworkElement에 대해 x:Name 특성을 설정합니다.
상호 작용해야 하는 각 FrameworkElement에 대해 전용 속성을 정의합니다.
FrameworkElement 속성의 set 접근자에서 컨트롤이 처리하는 모든 이벤트를 구독 및 구독 취소합니다.
2단계에서 정의한 FrameworkElement 속성을 OnApplyTemplate 메서드에서 설정합니다. 이 시점에서 컨트롤은 처음으로 ControlTemplate의 FrameworkElement를 사용할 수 있게 됩니다. x:Name을 사용하여 ControlTemplate에서 FrameworkElement를 가져옵니다.
해당 멤버에 액세스하기 전에 FrameworkElement가 null이 아닌지 확인합니다. null인 경우에도 오류를 보고하지 않습니다.
다음 예제에서는 앞의 목록에 있는 권장 사항에 맞게 NumericUpDown 컨트롤이 FrameworkElement 개체와 상호 작용하는 방법을 보여 줍니다.
ControlTemplate에서 NumericUpDown 컨트롤의 시각적 구조를 정의하는 예제에서 Value를 늘리는 RepeatButton의 x:Name 특성은 UpButton으로 설정됩니다. 다음 예제에서는 ControlTemplate에 선언된 RepeatButton을 나타내는 UpButtonElement라는 속성을 선언합니다. UpDownElement가 null이 아니면 set 접근자는 먼저 단추의 Click 이벤트에 대한 구독을 취소하고, 속성을 설정한 다음 Click 이벤트를 구독합니다. 다른 RepeatButton에 대해서는 DownButtonElement라는 속성이 정의되지만 여기에 표시되지는 않습니다.
Private m_upButtonElement As RepeatButton
Private Property UpButtonElement() As RepeatButton
Get
Return m_upButtonElement
End Get
Set(ByVal value As RepeatButton)
If m_upButtonElement IsNot Nothing Then
RemoveHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
End If
m_upButtonElement = value
If m_upButtonElement IsNot Nothing Then
AddHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
End If
End Set
End Property
private RepeatButton upButtonElement;
private RepeatButton UpButtonElement
{
get
{
return upButtonElement;
}
set
{
if (upButtonElement != null)
{
upButtonElement.Click -=
new RoutedEventHandler(upButtonElement_Click);
}
upButtonElement = value;
if (upButtonElement != null)
{
upButtonElement.Click +=
new RoutedEventHandler(upButtonElement_Click);
}
}
}
다음 예제에서는 NumericUpDown 컨트롤에 대한 OnApplyTemplate을 보여 줍니다. 이 예제에서는 GetTemplateChild 메서드를 사용하여 ControlTemplate에서 FrameworkElement 개체를 가져옵니다. 이 예제에서는 GetTemplateChild가 지정된 이름을 가졌지만 예상한 형식이 아닌 FrameworkElement를 찾는 경우에 대비합니다. 지정된 x:Name을 가지고 있지만 형식이 잘못된 요소를 무시하는 것도 최선의 방법에 포함됩니다.
Public Overloads Overrides Sub OnApplyTemplate()
UpButtonElement = TryCast(GetTemplateChild("UpButton"), RepeatButton)
DownButtonElement = TryCast(GetTemplateChild("DownButton"), RepeatButton)
UpdateStates(False)
End Sub
public override void OnApplyTemplate()
{
UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
//TextElement = GetTemplateChild("TextBlock") as TextBlock;
UpdateStates(false);
}
앞 예제에서 보여 준 방법을 따르면 ControlTemplate에 FrameworkElement가 없는 경우에도 컨트롤이 계속 실행됩니다.
VisualStateManager를 사용하여 상태 관리
VisualStateManager는 컨트롤 상태를 추적하고 상태 간 전환에 필요한 논리를 수행합니다. VisualState 개체를 ControlTemplate에 추가하는 경우 이 개체를 VisualStateGroup에 추가하고 VisualStateGroup을 VisualStateManager.VisualStateGroups 연결 속성에 추가하여 VisualStateManager가 해당 개체에 액세스할 수 있도록 합니다.
다음 예제에서는 컨트롤의 Positive 및 Negative 상태에 해당하는 VisualState 개체를 보여 주는 이전 예제를 반복합니다. Negative VisualState의 Storyboard는 TextBlock의 Foreground를 빨간색으로 바꿉니다. NumericUpDown 컨트롤이 Negative 상태에 있으면 Negative 상태의 스토리보드가 시작됩니다. 그런 다음 컨트롤이 Positive 상태로 돌아가면 Negative 상태의 Storyboard가 중지됩니다. Negative에 대한 Storyboard가 중지되면 Foreground가 원래 색으로 돌아가므로 Positive VisualState에는 Storyboard가 포함되지 않아도 됩니다.
<ControlTemplate TargetType="local:NumericUpDown">
<Grid Margin="3"
Background="{TemplateBinding Background}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="ValueStates">
<!--Make the Value property red when it is negative.-->
<VisualState Name="Negative">
<Storyboard>
<ColorAnimation To="Red"
Storyboard.TargetName="TextBlock"
Storyboard.TargetProperty="(Foreground).(Color)"/>
</Storyboard>
</VisualState>
<!--Return the TextBlock's Foreground to its
original color.-->
<VisualState Name="Positive"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
TextBlock의 이름이 지정되지만 컨트롤의 논리에서 TextBlock을 참조하지 않기 때문에 TextBlock은 NumericUpDown에 대한 컨트롤 계약에 포함되지 않습니다. ControlTemplate에서 참조되는 요소에 이름이 있지만 컨트롤에 대한 새 ControlTemplate이 해당 요소를 참조할 필요가 없기 때문에 컨트롤 계약에 포함되지 않아도 됩니다. 예를 들어 NumericUpDown에 대한 새 ControlTemplate을 만드는 사용자가 Foreground를 변경하여 Value가 negative인지 나타내지 않을 수 있습니다. 이러한 경우 코드와 ControlTemplate은 모두 이름을 사용하여 TextBlock을 참조하지 않습니다.
컨트롤 상태 변경은 컨트롤의 논리가 담당합니다. 다음 예제에서는 NumericUpDown 컨트롤이 Value가 0 이상일 때는 Positive 상태로, Value가 0보다 작을 때는 Negative 상태로 이동하기 위해 GoToState 메서드를 호출합니다.
If Value >= 0 Then
VisualStateManager.GoToState(Me, "Positive", useTransitions)
Else
VisualStateManager.GoToState(Me, "Negative", useTransitions)
End If
if (Value >= 0)
{
VisualStateManager.GoToState(this, "Positive", useTransitions);
}
else
{
VisualStateManager.GoToState(this, "Negative", useTransitions);
}
GoToState 메서드는 스토리보드를 적절히 시작하고 중지하는 데 필요한 논리를 수행합니다. 컨트롤이 해당 상태를 변경하기 위해 GoToState를 호출하면 VisualStateManager는 다음 작업을 수행합니다.
컨트롤이 이동하려는 VisualState에 Storyboard가 있으면 해당 스토리보드가 시작됩니다. 컨트롤이 현재 속해 있는 VisualState에 Storyboard가 있으면 해당 스토리보드는 종료됩니다.
컨트롤이 이미 지정된 상태에 있으면 GoToState는 아무런 동작도 취하지 않고 true를 반환합니다.
지정된 상태가 control의 ControlTemplate에 없으면 GoToState는 아무런 동작도 취하지 않고 false를 반환합니다.
VisualStateManager를 사용하는 최선의 방법
컨트롤의 상태를 유지 관리하기 위해 다음 작업을 수행하는 것이 좋습니다.
속성을 사용하여 상태를 추적합니다.
상태 간 전환을 위한 도우미 메서드를 만듭니다.
NumericUpDown 컨트롤은 해당 Value 속성을 사용하여 자신이 Positive 또는 Negative 중 어떤 상태에 있는지 추적합니다. 또한 NumericUpDown 컨트롤은 IsFocused 속성을 추적하는 Focused 및 UnFocused 상태를 정의합니다. 컨트롤의 속성에 자연스럽게 대응되지 않는 상태를 사용하는 경우 private 속성을 정의하여 상태를 추적할 수 있습니다.
모든 상태를 업데이트하는 단일 메서드는 VisualStateManager에 대한 호출을 중앙 집중화하고 코드를 관리 가능한 상태로 유지합니다. 다음 예제에서는 NumericUpDown 컨트롤의 도우미 메서드인 UpdateStates를 보여 줍니다. Value가 0보다 크거나 같은 경우 Control은 Positive 상태에 있고 Value가 0보다 작으면 Negative 상태에 있습니다. IsFocused가 true이면 컨트롤이 Focused 상태이고, 그렇지 않으면 Unfocused 상태입니다. 이 컨트롤은 변경하려는 상태에 상관 없이 상태를 변경해야 할 때마다 UpdateStates를 호출할 수 있습니다.
Private Sub UpdateStates(ByVal useTransitions As Boolean)
If Value >= 0 Then
VisualStateManager.GoToState(Me, "Positive", useTransitions)
Else
VisualStateManager.GoToState(Me, "Negative", useTransitions)
End If
If IsFocused Then
VisualStateManager.GoToState(Me, "Focused", useTransitions)
Else
VisualStateManager.GoToState(Me, "Unfocused", useTransitions)
End If
End Sub
private void UpdateStates(bool useTransitions)
{
if (Value >= 0)
{
VisualStateManager.GoToState(this, "Positive", useTransitions);
}
else
{
VisualStateManager.GoToState(this, "Negative", useTransitions);
}
if (IsFocused)
{
VisualStateManager.GoToState(this, "Focused", useTransitions);
}
else
{
VisualStateManager.GoToState(this, "Unfocused", useTransitions);
}
}
컨트롤이 이미 특정 상태에 있을 때 해당 상태 이름을 GoToState에 전달하면 GoToState는 아무런 동작도 하지 않으므로 컨트롤의 현재 상태를 확인할 필요가 없습니다. 예를 들어 Value가 음수에서 다른 음수로 변경되면 Negative 상태에 대한 스토리보드는 중단되지 않으며 사용자는 컨트롤의 변경 내용을 보지 못합니다.
GoToState를 호출하면 VisualStateManager는 VisualStateGroup 개체를 사용하여 어떤 상태를 종료할지 결정합니다. 컨트롤은 언제나 해당 ControlTemplate에 정의된 각 VisualStateGroup별로 하나의 상태에 있으며 동일한 VisualStateGroup 내에서 다른 상태로 이동할 때만 상태를 벗어납니다. 예를 들어 NumericUpDown 컨트롤의 ControlTemplate은 한 VisualStateGroup에서 Positive 및 Negative VisualState 개체를 정의하고 다른 그룹에서 Focused 및 Unfocused VisualState 개체를 정의합니다. 이 항목의 완성된 예제 단원에 정의된 Focused 및 Unfocused VisualState를 참조할 수 있습니다. 컨트롤이 Positive 상태에서 Negative 상태로 변경되거나 그 반대로 변경될 때 컨트롤은 Focused 또는 Unfocused 상태로 유지됩니다.
컨트롤 상태가 변경될 수 있는 다음과 같은 세 가지 일반적인 경우가 있습니다.
ControlTemplate이 Control에 적용되는 경우
속성이 변경되는 경우
이벤트가 발생하는 경우
다음 예제에서는 이러한 경우에 NumericUpDown 컨트롤의 상태를 업데이트합니다.
ControlTemplate이 적용될 때 컨트롤이 올바른 상태로 표시되도록 OnApplyTemplate 메서드에서 컨트롤의 상태를 업데이트해야 합니다. 다음 예제에서는 컨트롤이 적절한 상태에 있도록 하기 위해 OnApplyTemplate에서 UpdateStates를 호출합니다. 예를 들어 NumericUpDown 컨트롤을 만든 다음 해당 Foreground를 녹색으로, Value를 -5로 설정한다고 가정합니다. ControlTemplate이 NumericUpDown 컨트롤에 적용될 때 UpdateStates를 호출하지 않으면 컨트롤이 Negative 상태에 있지 않고 값이 빨강이 아닌 녹색입니다. UpdateStates를 호출하여 컨트롤을 Negative 상태로 만들어야 합니다.
Public Overloads Overrides Sub OnApplyTemplate()
UpButtonElement = TryCast(GetTemplateChild("UpButton"), RepeatButton)
DownButtonElement = TryCast(GetTemplateChild("DownButton"), RepeatButton)
UpdateStates(False)
End Sub
public override void OnApplyTemplate()
{
UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
//TextElement = GetTemplateChild("TextBlock") as TextBlock;
UpdateStates(false);
}
속성이 변경될 때 컨트롤 상태를 업데이트해야 하는 경우도 있습니다. 다음 예제에서는 전체 ValueChangedCallback 메서드를 보여 줍니다. Value가 변경되면 ValueChangedCallback이 호출되므로 이 메서드는 Value가 양수에서 음수로 변경되거나 그 반대인 경우 UpdateStates를 호출합니다. Value가 변경되지만 양수 또는 음수가 바뀌지 않는 경우에도 UpdateStates를 호출할 수 있는데 그 이유는 이 경우 컨트롤이 상태를 변경하지 않기 때문입니다.
Private Shared Sub ValueChangedCallback(ByVal obj As DependencyObject,
ByVal args As DependencyPropertyChangedEventArgs)
Dim ctl As NumericUpDown = DirectCast(obj, NumericUpDown)
Dim newValue As Integer = CInt(args.NewValue)
' Call UpdateStates because the Value might have caused the
' control to change ValueStates.
ctl.UpdateStates(True)
' Call OnValueChanged to raise the ValueChanged event.
ctl.OnValueChanged(New ValueChangedEventArgs(NumericUpDown.ValueChangedEvent, newValue))
End Sub
private static void ValueChangedCallback(DependencyObject obj,
DependencyPropertyChangedEventArgs args)
{
NumericUpDown ctl = (NumericUpDown)obj;
int newValue = (int)args.NewValue;
// Call UpdateStates because the Value might have caused the
// control to change ValueStates.
ctl.UpdateStates(true);
// Call OnValueChanged to raise the ValueChanged event.
ctl.OnValueChanged(
new ValueChangedEventArgs(NumericUpDown.ValueChangedEvent,
newValue));
}
이벤트가 발생할 때도 상태를 업데이트해야 하는 경우가 있습니다. 다음 예제에서는 NumericUpDown이 Control의 UpdateStates를 호출하여 GotFocus 이벤트를 처리합니다.
Protected Overloads Overrides Sub OnGotFocus(ByVal e As RoutedEventArgs)
MyBase.OnGotFocus(e)
UpdateStates(True)
End Sub
protected override void OnGotFocus(RoutedEventArgs e)
{
base.OnGotFocus(e);
UpdateStates(true);
}
VisualStateManager는 컨트롤 상태를 관리하는 데 도움을 줍니다. VisualStateManager를 사용하면 컨트롤의 상태 간 올바른 전환을 보장할 수 있습니다. 이 단원에 설명된 VisualStateManager 사용 관련 권장 사항을 따르면 컨트롤의 코드를 읽기 쉽고 유지 관리하기 쉽게 작성할 수 있습니다.
컨트롤 계약 제공
ControlTemplate 작성자가 템플릿에 무엇을 배치할지 알 수 있도록 컨트롤 계약을 제공합니다. 컨트롤 계약에는 다음 세 가지 요소가 있습니다.
컨트롤 논리가 사용하는 시각적 요소
컨트롤의 상태 및 각 상태가 속해 있는 그룹
컨트롤에 시각적으로 영향을 미치는 public 속성
새 ControlTemplate을 만드는 사람은 컨트롤의 논리가 사용하는 FrameworkElement 개체, 각 개체의 형식 및 이름에 대해 알고 있어야 합니다. 또한 ControlTemplate 작성자는 컨트롤이 속할 수 있는 가능한 각 상태의 이름 및 해당 상태가 속한 VisualStateGroup에 대해 알고 있어야 합니다.
NumericUpDown 예제로 돌아가면 이 컨트롤은 ControlTemplate에 다음 FrameworkElement 개체가 있을 것으로 예상합니다.
UpButton이라는 이름의 RepeatButton
DownButton이라는 이름의 RepeatButton
컨트롤은 다음 상태에 있을 수 있습니다.
ValueStates VisualStateGroup의 경우
Positive
Negative
FocusStates VisualStateGroup의 경우
Focused
Unfocused
컨트롤이 예상하는 FrameworkElement 개체를 지정하려면 예상되는 요소의 이름과 형식을 지정하는 TemplatePartAttribute를 사용합니다. 가능한 컨트롤 상태를 지정하려면 상태의 이름 및 상태가 속해 있는 VisualStateGroup을 지정하는 TemplateVisualStateAttribute를 사용합니다. 컨트롤의 클래스 정의에 TemplatePartAttribute 및 TemplateVisualStateAttribute를 배치합니다.
컨트롤의 모양에 영향을 미치는 모든 공용 속성도 컨트롤 계약의 일부입니다.
다음 예제에서는 NumericUpDown 컨트롤에 대한 상태 및 FrameworkElement 개체를 지정합니다.
<TemplatePart(Name:="UpButtonElement", Type:=GetType(RepeatButton))> _
<TemplatePart(Name:="DownButtonElement", Type:=GetType(RepeatButton))> _
<TemplateVisualState(Name:="Positive", GroupName:="ValueStates")> _
<TemplateVisualState(Name:="Negative", GroupName:="ValueStates")> _
<TemplateVisualState(Name:="Focused", GroupName:="FocusedStates")> _
<TemplateVisualState(Name:="Unfocused", GroupName:="FocusedStates")> _
Public Class NumericUpDown
Inherits Control
Public Shared ReadOnly BackgroundProperty As DependencyProperty
Public Shared ReadOnly BorderBrushProperty As DependencyProperty
Public Shared ReadOnly BorderThicknessProperty As DependencyProperty
Public Shared ReadOnly FontFamilyProperty As DependencyProperty
Public Shared ReadOnly FontSizeProperty As DependencyProperty
Public Shared ReadOnly FontStretchProperty As DependencyProperty
Public Shared ReadOnly FontStyleProperty As DependencyProperty
Public Shared ReadOnly FontWeightProperty As DependencyProperty
Public Shared ReadOnly ForegroundProperty As DependencyProperty
Public Shared ReadOnly HorizontalContentAlignmentProperty As DependencyProperty
Public Shared ReadOnly PaddingProperty As DependencyProperty
Public Shared ReadOnly TextAlignmentProperty As DependencyProperty
Public Shared ReadOnly TextDecorationsProperty As DependencyProperty
Public Shared ReadOnly TextWrappingProperty As DependencyProperty
Public Shared ReadOnly VerticalContentAlignmentProperty As DependencyProperty
Private _Background As Brush
Public Property Background() As Brush
Get
Return _Background
End Get
Set(ByVal value As Brush)
_Background = value
End Set
End Property
Private _BorderBrush As Brush
Public Property BorderBrush() As Brush
Get
Return _BorderBrush
End Get
Set(ByVal value As Brush)
_BorderBrush = value
End Set
End Property
Private _BorderThickness As Thickness
Public Property BorderThickness() As Thickness
Get
Return _BorderThickness
End Get
Set(ByVal value As Thickness)
_BorderThickness = value
End Set
End Property
Private _FontFamily As FontFamily
Public Property FontFamily() As FontFamily
Get
Return _FontFamily
End Get
Set(ByVal value As FontFamily)
_FontFamily = value
End Set
End Property
Private _FontSize As Double
Public Property FontSize() As Double
Get
Return _FontSize
End Get
Set(ByVal value As Double)
_FontSize = value
End Set
End Property
Private _FontStretch As FontStretch
Public Property FontStretch() As FontStretch
Get
Return _FontStretch
End Get
Set(ByVal value As FontStretch)
_FontStretch = value
End Set
End Property
Private _FontStyle As FontStyle
Public Property FontStyle() As FontStyle
Get
Return _FontStyle
End Get
Set(ByVal value As FontStyle)
_FontStyle = value
End Set
End Property
Private _FontWeight As FontWeight
Public Property FontWeight() As FontWeight
Get
Return _FontWeight
End Get
Set(ByVal value As FontWeight)
_FontWeight = value
End Set
End Property
Private _Foreground As Brush
Public Property Foreground() As Brush
Get
Return _Foreground
End Get
Set(ByVal value As Brush)
_Foreground = value
End Set
End Property
Private _HorizontalContentAlignment As HorizontalAlignment
Public Property HorizontalContentAlignment() As HorizontalAlignment
Get
Return _HorizontalContentAlignment
End Get
Set(ByVal value As HorizontalAlignment)
_HorizontalContentAlignment = value
End Set
End Property
Private _Padding As Thickness
Public Property Padding() As Thickness
Get
Return _Padding
End Get
Set(ByVal value As Thickness)
_Padding = value
End Set
End Property
Private _TextAlignment As TextAlignment
Public Property TextAlignment() As TextAlignment
Get
Return _TextAlignment
End Get
Set(ByVal value As TextAlignment)
_TextAlignment = value
End Set
End Property
Private _TextDecorations As TextDecorationCollection
Public Property TextDecorations() As TextDecorationCollection
Get
Return _TextDecorations
End Get
Set(ByVal value As TextDecorationCollection)
_TextDecorations = value
End Set
End Property
Private _TextWrapping As TextWrapping
Public Property TextWrapping() As TextWrapping
Get
Return _TextWrapping
End Get
Set(ByVal value As TextWrapping)
_TextWrapping = value
End Set
End Property
Private _VerticalContentAlignment As VerticalAlignment
Public Property VerticalContentAlignment() As VerticalAlignment
Get
Return _VerticalContentAlignment
End Get
Set(ByVal value As VerticalAlignment)
_VerticalContentAlignment = value
End Set
End Property
End Class
[TemplatePart(Name = "UpButtonElement", Type = typeof(RepeatButton))]
[TemplatePart(Name = "DownButtonElement", Type = typeof(RepeatButton))]
[TemplateVisualState(Name = "Positive", GroupName = "ValueStates")]
[TemplateVisualState(Name = "Negative", GroupName = "ValueStates")]
[TemplateVisualState(Name = "Focused", GroupName = "FocusedStates")]
[TemplateVisualState(Name = "Unfocused", GroupName = "FocusedStates")]
public class NumericUpDown : Control
{
public static readonly DependencyProperty BackgroundProperty;
public static readonly DependencyProperty BorderBrushProperty;
public static readonly DependencyProperty BorderThicknessProperty;
public static readonly DependencyProperty FontFamilyProperty;
public static readonly DependencyProperty FontSizeProperty;
public static readonly DependencyProperty FontStretchProperty;
public static readonly DependencyProperty FontStyleProperty;
public static readonly DependencyProperty FontWeightProperty;
public static readonly DependencyProperty ForegroundProperty;
public static readonly DependencyProperty HorizontalContentAlignmentProperty;
public static readonly DependencyProperty PaddingProperty;
public static readonly DependencyProperty TextAlignmentProperty;
public static readonly DependencyProperty TextDecorationsProperty;
public static readonly DependencyProperty TextWrappingProperty;
public static readonly DependencyProperty VerticalContentAlignmentProperty;
public Brush Background { get; set; }
public Brush BorderBrush { get; set; }
public Thickness BorderThickness { get; set; }
public FontFamily FontFamily { get; set; }
public double FontSize { get; set; }
public FontStretch FontStretch { get; set; }
public FontStyle FontStyle { get; set; }
public FontWeight FontWeight { get; set; }
public Brush Foreground { get; set; }
public HorizontalAlignment HorizontalContentAlignment { get; set; }
public Thickness Padding { get; set; }
public TextAlignment TextAlignment { get; set; }
public TextDecorationCollection TextDecorations { get; set; }
public TextWrapping TextWrapping { get; set; }
public VerticalAlignment VerticalContentAlignment { get; set; }
}
완성된 예제
다음 예제에서는 NumericUpDown 컨트롤에 대한 전체 ControlTemplate을 보여 줍니다.
<!--This is the contents of the themes/generic.xaml file.-->
<ResourceDictionary
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:VSMCustomControl">
<Style TargetType="{x:Type local:NumericUpDown}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:NumericUpDown">
<Grid Margin="3"
Background="{TemplateBinding Background}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="ValueStates">
<!--Make the Value property red when it is negative.-->
<VisualState Name="Negative">
<Storyboard>
<ColorAnimation To="Red"
Storyboard.TargetName="TextBlock"
Storyboard.TargetProperty="(Foreground).(Color)"/>
</Storyboard>
</VisualState>
<!--Return the control to its initial state by
return the TextBlock's Foreground to its
original color.-->
<VisualState Name="Positive"/>
</VisualStateGroup>
<VisualStateGroup Name="FocusStates">
<!--Add a focus rectangle to highlight the entire control
when it has focus.-->
<VisualState Name="Focused">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="FocusVisual"
Storyboard.TargetProperty="Visibility" Duration="0">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<!--Return the control to its initial state by
hiding the focus rectangle.-->
<VisualState Name="Unfocused"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Border BorderThickness="1" BorderBrush="Gray"
Margin="7,2,2,2" Grid.RowSpan="2"
Background="#E0FFFFFF"
VerticalAlignment="Center"
HorizontalAlignment="Stretch">
<!--Bind the TextBlock to the Value property-->
<TextBlock Name="TextBlock"
Width="60" TextAlignment="Right" Padding="5"
Text="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type local:NumericUpDown}},
Path=Value}"/>
</Border>
<RepeatButton Content="Up" Margin="2,5,5,0"
Name="UpButton"
Grid.Column="1" Grid.Row="0"/>
<RepeatButton Content="Down" Margin="2,0,5,5"
Name="DownButton"
Grid.Column="1" Grid.Row="1"/>
<Rectangle Name="FocusVisual" Grid.ColumnSpan="2" Grid.RowSpan="2"
Stroke="Black" StrokeThickness="1"
Visibility="Collapsed"/>
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
다음 예제에서는 NumericUpDown에 대한 논리를 보여 줍니다.
Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Controls.Primitives
Imports System.Windows.Input
Imports System.Windows.Media
<TemplatePart(Name:="UpButtonElement", Type:=GetType(RepeatButton))> _
<TemplatePart(Name:="DownButtonElement", Type:=GetType(RepeatButton))> _
<TemplateVisualState(Name:="Positive", GroupName:="ValueStates")> _
<TemplateVisualState(Name:="Negative", GroupName:="ValueStates")> _
<TemplateVisualState(Name:="Focused", GroupName:="FocusedStates")> _
<TemplateVisualState(Name:="Unfocused", GroupName:="FocusedStates")> _
Public Class NumericUpDown
Inherits Control
Public Sub New()
DefaultStyleKeyProperty.OverrideMetadata(GetType(NumericUpDown), New FrameworkPropertyMetadata(GetType(NumericUpDown)))
Me.IsTabStop = True
End Sub
Public Shared ReadOnly ValueProperty As DependencyProperty =
DependencyProperty.Register("Value", GetType(Integer), GetType(NumericUpDown),
New PropertyMetadata(New PropertyChangedCallback(AddressOf ValueChangedCallback)))
Public Property Value() As Integer
Get
Return CInt(GetValue(ValueProperty))
End Get
Set(ByVal value As Integer)
SetValue(ValueProperty, value)
End Set
End Property
Private Shared Sub ValueChangedCallback(ByVal obj As DependencyObject,
ByVal args As DependencyPropertyChangedEventArgs)
Dim ctl As NumericUpDown = DirectCast(obj, NumericUpDown)
Dim newValue As Integer = CInt(args.NewValue)
' Call UpdateStates because the Value might have caused the
' control to change ValueStates.
ctl.UpdateStates(True)
' Call OnValueChanged to raise the ValueChanged event.
ctl.OnValueChanged(New ValueChangedEventArgs(NumericUpDown.ValueChangedEvent, newValue))
End Sub
Public Shared ReadOnly ValueChangedEvent As RoutedEvent =
EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Direct,
GetType(ValueChangedEventHandler), GetType(NumericUpDown))
Public Custom Event ValueChanged As ValueChangedEventHandler
AddHandler(ByVal value As ValueChangedEventHandler)
Me.AddHandler(ValueChangedEvent, value)
End AddHandler
RemoveHandler(ByVal value As ValueChangedEventHandler)
Me.RemoveHandler(ValueChangedEvent, value)
End RemoveHandler
RaiseEvent(ByVal sender As Object, ByVal e As RoutedEventArgs)
Me.RaiseEvent(e)
End RaiseEvent
End Event
Protected Overridable Sub OnValueChanged(ByVal e As ValueChangedEventArgs)
' Raise the ValueChanged event so applications can be alerted
' when Value changes.
MyBase.RaiseEvent(e)
End Sub
#Region "NUDCode"
Private Sub UpdateStates(ByVal useTransitions As Boolean)
If Value >= 0 Then
VisualStateManager.GoToState(Me, "Positive", useTransitions)
Else
VisualStateManager.GoToState(Me, "Negative", useTransitions)
End If
If IsFocused Then
VisualStateManager.GoToState(Me, "Focused", useTransitions)
Else
VisualStateManager.GoToState(Me, "Unfocused", useTransitions)
End If
End Sub
Public Overloads Overrides Sub OnApplyTemplate()
UpButtonElement = TryCast(GetTemplateChild("UpButton"), RepeatButton)
DownButtonElement = TryCast(GetTemplateChild("DownButton"), RepeatButton)
UpdateStates(False)
End Sub
Private m_downButtonElement As RepeatButton
Private Property DownButtonElement() As RepeatButton
Get
Return m_downButtonElement
End Get
Set(ByVal value As RepeatButton)
If m_downButtonElement IsNot Nothing Then
RemoveHandler m_downButtonElement.Click, AddressOf downButtonElement_Click
End If
m_downButtonElement = value
If m_downButtonElement IsNot Nothing Then
AddHandler m_downButtonElement.Click, AddressOf downButtonElement_Click
End If
End Set
End Property
Private Sub downButtonElement_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
Value -= 1
End Sub
Private m_upButtonElement As RepeatButton
Private Property UpButtonElement() As RepeatButton
Get
Return m_upButtonElement
End Get
Set(ByVal value As RepeatButton)
If m_upButtonElement IsNot Nothing Then
RemoveHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
End If
m_upButtonElement = value
If m_upButtonElement IsNot Nothing Then
AddHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
End If
End Set
End Property
Private Sub upButtonElement_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
Value += 1
End Sub
Protected Overloads Overrides Sub OnMouseLeftButtonDown(ByVal e As MouseButtonEventArgs)
MyBase.OnMouseLeftButtonDown(e)
Focus()
End Sub
Protected Overloads Overrides Sub OnGotFocus(ByVal e As RoutedEventArgs)
MyBase.OnGotFocus(e)
UpdateStates(True)
End Sub
Protected Overloads Overrides Sub OnLostFocus(ByVal e As RoutedEventArgs)
MyBase.OnLostFocus(e)
UpdateStates(True)
End Sub
#End Region
End Class
Public Delegate Sub ValueChangedEventHandler(ByVal sender As Object,
ByVal e As ValueChangedEventArgs)
Public Class ValueChangedEventArgs
Inherits RoutedEventArgs
Private _value As Integer
Public Sub New(ByVal id As RoutedEvent,
ByVal num As Integer)
_value = num
RoutedEvent = id
End Sub
Public ReadOnly Property Value() As Integer
Get
Return _value
End Get
End Property
End Class
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
namespace VSMCustomControl
{
[TemplatePart(Name = "UpButtonElement", Type = typeof(RepeatButton))]
[TemplatePart(Name = "DownButtonElement", Type = typeof(RepeatButton))]
[TemplateVisualState(Name = "Positive", GroupName = "ValueStates")]
[TemplateVisualState(Name = "Negative", GroupName = "ValueStates")]
[TemplateVisualState(Name = "Focused", GroupName = "FocusedStates")]
[TemplateVisualState(Name = "Unfocused", GroupName = "FocusedStates")]
public class NumericUpDown : Control
{
public NumericUpDown()
{
DefaultStyleKey = typeof(NumericUpDown);
this.IsTabStop = true;
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(
"Value", typeof(int), typeof(NumericUpDown),
new PropertyMetadata(
new PropertyChangedCallback(ValueChangedCallback)));
public int Value
{
get
{
return (int)GetValue(ValueProperty);
}
set
{
SetValue(ValueProperty, value);
}
}
private static void ValueChangedCallback(DependencyObject obj,
DependencyPropertyChangedEventArgs args)
{
NumericUpDown ctl = (NumericUpDown)obj;
int newValue = (int)args.NewValue;
// Call UpdateStates because the Value might have caused the
// control to change ValueStates.
ctl.UpdateStates(true);
// Call OnValueChanged to raise the ValueChanged event.
ctl.OnValueChanged(
new ValueChangedEventArgs(NumericUpDown.ValueChangedEvent,
newValue));
}
public static readonly RoutedEvent ValueChangedEvent =
EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Direct,
typeof(ValueChangedEventHandler), typeof(NumericUpDown));
public event ValueChangedEventHandler ValueChanged
{
add { AddHandler(ValueChangedEvent, value); }
remove { RemoveHandler(ValueChangedEvent, value); }
}
protected virtual void OnValueChanged(ValueChangedEventArgs e)
{
// Raise the ValueChanged event so applications can be alerted
// when Value changes.
RaiseEvent(e);
}
private void UpdateStates(bool useTransitions)
{
if (Value >= 0)
{
VisualStateManager.GoToState(this, "Positive", useTransitions);
}
else
{
VisualStateManager.GoToState(this, "Negative", useTransitions);
}
if (IsFocused)
{
VisualStateManager.GoToState(this, "Focused", useTransitions);
}
else
{
VisualStateManager.GoToState(this, "Unfocused", useTransitions);
}
}
public override void OnApplyTemplate()
{
UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
//TextElement = GetTemplateChild("TextBlock") as TextBlock;
UpdateStates(false);
}
private RepeatButton downButtonElement;
private RepeatButton DownButtonElement
{
get
{
return downButtonElement;
}
set
{
if (downButtonElement != null)
{
downButtonElement.Click -=
new RoutedEventHandler(downButtonElement_Click);
}
downButtonElement = value;
if (downButtonElement != null)
{
downButtonElement.Click +=
new RoutedEventHandler(downButtonElement_Click);
}
}
}
void downButtonElement_Click(object sender, RoutedEventArgs e)
{
Value--;
}
private RepeatButton upButtonElement;
private RepeatButton UpButtonElement
{
get
{
return upButtonElement;
}
set
{
if (upButtonElement != null)
{
upButtonElement.Click -=
new RoutedEventHandler(upButtonElement_Click);
}
upButtonElement = value;
if (upButtonElement != null)
{
upButtonElement.Click +=
new RoutedEventHandler(upButtonElement_Click);
}
}
}
void upButtonElement_Click(object sender, RoutedEventArgs e)
{
Value++;
}
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
Focus();
}
protected override void OnGotFocus(RoutedEventArgs e)
{
base.OnGotFocus(e);
UpdateStates(true);
}
protected override void OnLostFocus(RoutedEventArgs e)
{
base.OnLostFocus(e);
UpdateStates(true);
}
}
public delegate void ValueChangedEventHandler(object sender, ValueChangedEventArgs e);
public class ValueChangedEventArgs : RoutedEventArgs
{
private int _value;
public ValueChangedEventArgs(RoutedEvent id, int num)
{
_value = num;
RoutedEvent = id;
}
public int Value
{
get { return _value; }
}
}
}
참고 항목
개념
ControlTemplate을 만들어 기존 컨트롤의 모양 사용자 지정