WPF 강좌: 다국어 지원 및 국제화(I18N) 방법
오늘날의 글로벌화된 세상에서 응용 프로그램의 다국어 지원은 매우 중요합니다. 특히 WPF(Windows Presentation Foundation)를 사용하여 개발할 때, 국제화(I18N)와 다국어 지원을 고려해야 합니다. 이 글에서는 WPF에서 다국어 지원을 구현하는 방법과 이를 위해 사용하는 기술에 대해 자세히 설명하도록 하겠습니다.
1. 국제화와 지역화의 이해
국제화(I18N, Internationalization)는 소프트웨어를 다양한 언어와 문화를 지원할 수 있도록 설계하는 과정을 말합니다. 반면 지역화(L10N, Localization)는 특정 언어와 문화에 맞는 콘텐츠를 제공하는 작업을 의미합니다. WPF에서는 이 두 가지 과정을 지원하는 다양한 기능이 있습니다.
2. WPF에서 다국어 지원을 위한 기본 개념
WPF는 XAML(Extensible Application Markup Language)을 사용하여 UI를 정의합니다. 다국어 지원을 위해서는 리소스 파일(.resx)을 사용하여 각 언어에 대한 문자열 및 기타 리소스를 관리합니다.
2.1 리소스 파일 생성
리소스 파일은 각각의 언어에 대한 고유한 문자열 데이터를 저장하는 파일입니다. Visual Studio에서 다음과 같은 방법으로 리소스 파일을 생성할 수 있습니다:
- Visual Studio에서 프로젝트를 마우스 오른쪽 버튼으로 클릭하고 Add -> New Item을 선택합니다.
- Resources File을 선택하고 파일 이름을 Strings.resx로 지정합니다.
- 기본 리소스 파일이 생성되고, 여기에서 각 문자열의 키와 값을 추가합니다.
2.2 문화별 리소스 파일 생성
기본 리소스 파일이 준비되면, 다른 언어를 지원하기 위해 문화별 리소스 파일을 생성해야 합니다. 예를 들어, 한국어와 영어를 지원하고 싶다면 다음과 같은 파일을 생성합니다:
- Strings.en.resx (영어)
- Strings.ko.resx (한국어)
각 파일에서 해당 언어에 맞는 문자열을 입력합니다. 이후에 WPF는 현재 문화에 맞는 리소스 파일을 자동으로 사용합니다.
3. UI에 리소스 파일 연결하기
리소스 파일을 준비한 후, XAML 파일에서 이를 사용하는 방법은 다음과 같습니다. WPF에서는 {x:Static} 마크업 확장을 사용하여 리소스 파일의 값을 가져올 수 있습니다.
3.1 리소스 파일 사용 예제
예를 들어, 버튼의 텍스트를 다국어로 지원하도록 설정할 수 있습니다:
<Button Content="{x:Static properties:Strings.MyButtonText}" />
여기서 MyButtonText는 리소스 파일에서 정의한 문자열의 키입니다. 버튼의 텍스트는 현재 문화에 따라 적절한 문자열 값으로 표시됩니다.
4. 현재 문화 변경하기
응용 프로그램에서 사용자가 언어를 직접 변경할 수 있도록 하려면, 현재 문화 정보를 변경해야 합니다. 다음은 현재 문화 정보를 변경하는 방법의 예입니다:
CultureInfo.CurrentUICulture = new CultureInfo("ko-KR");
위 코드는 현재 UI 문화를 한국어로 설정합니다. 이를 통해 사용자는 원활하게 다양한 언어를 사용할 수 있습니다.
5. 날짜 및 숫자 형식 처리
국제화된 응용 프로그램에서 숫자와 날짜의 형식은 매우 중요합니다. WPF에서는 CultureInfo를 사용하여 이러한 형식을 처리할 수 있습니다. 예를 들어, 날짜를 현재 문화에 맞게 형식화하려면 DateTime 개체의 ToString 메서드를 사용할 수 있습니다:
string formattedDate = DateTime.Now.ToString("D", CultureInfo.CurrentCulture);
위 코드는 현재 날짜를 현재 문화에 맞는 형식으로 반환합니다.
6. 정리 및 결론
WPF에서 다국어 지원 및 국제화를 구현하는 것은 현대 소프트웨어 개발에서 필수적인 요소입니다. 우리는 리소스 파일을 생성하고, UI 요소와 연결하며, 현재 문화를 변경하는 방법에 대해 알아보았습니다. 마지막으로, 숫자와 날짜의 형식을 처리하는 방법도 살펴보았습니다.
이 과정들을 통해 WPF 응용 프로그램이 다양한 문화적 배경을 가진 사용자에게 친숙하고 이해하기 쉬운 경험을 제공할 수 있도록 만드는 것이 목표입니다. 이를 통해 글로벌 시장에서 경쟁력을 갖춘 소프트웨어를 개발할 수 있을 것입니다.
xml 파싱
public void LoadXml(string filepath, Rect? area = null)
{
ViewModel.Shapes.Clear();
var shapes = BaseShapeViewModel.LoadFromXml(filepath);
if (area.HasValue)
{
// 선택된 영역 내의 도형만 필터링
var filteredShapes = shapes.Where(shape =>
{
var shapeBounds = new Rect(shape.X, shape.Y, shape.Width, shape.Height);
return area.Value.IntersectsWith(shapeBounds);
});
foreach (var shape in filteredShapes)
{
// 도형의 좌표를 area 기준으로 조정
shape.X -= area.Value.X;
shape.Y -= area.Value.Y;
ViewModel.Shapes.Add(shape);
}
// Canvas 크기를 area 크기로 조정
MainCanvas.Width = area.Value.Width;
MainCanvas.Height = area.Value.Height;
}
else
{
// 전체 도형 로드
foreach (var shape in shapes)
{
ViewModel.Shapes.Add(shape);
}
}
FitCanvasToView();
}
multi-combo
<!-- MultiSelectComboBox.xaml -->
<UserControl x:Class="wpf_FMB.MultiSelectComboBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:wpf_FMB">
<UserControl.Resources>
<!-- Toggle Button Style -->
<Style x:Key="ComboBoxToggleButton" TargetType="{x:Type ToggleButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Border x:Name="Border"
Background="#FAFAFA"
BorderBrush="#E8E8E8"
BorderThickness="1"
CornerRadius="6">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="32"/>
</Grid.ColumnDefinitions>
<TextBlock x:Name="DisplayText"
Margin="12,0,0,0"
VerticalAlignment="Center"
Foreground="#333333"
FontSize="13"
Text="{Binding RelativeSource={RelativeSource AncestorType=local:MultiSelectComboBox}, Path=DisplayText}"/>
<Path Grid.Column="1"
Data="M0,0 L6,6 L12,0"
Stroke="#666666"
StrokeThickness="1.5"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Width="12"
Height="6"/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#F5F5F5" TargetName="Border"/>
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter Property="Background" Value="#F0F0F0" TargetName="Border"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Checkbox Style -->
<Style x:Key="CustomCheckBox" TargetType="{x:Type CheckBox}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type CheckBox}">
<Grid>
<Border x:Name="Border"
Width="16"
Height="16"
Background="White"
BorderBrush="#DDDDDD"
BorderThickness="1"
CornerRadius="3">
<Path x:Name="Checkmark"
Data="M2,6 L6,10 L14,2"
Stroke="White"
StrokeThickness="2"
Visibility="Collapsed"/>
</Border>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter Property="Background" Value="#1677FF" TargetName="Border"/>
<Setter Property="BorderBrush" Value="#1677FF" TargetName="Border"/>
<Setter Property="Visibility" Value="Visible" TargetName="Checkmark"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ComboBoxItem Style -->
<Style x:Key="ComboBoxItemStyle" TargetType="{x:Type ComboBoxItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ComboBoxItem}">
<Border x:Name="Border"
Padding="8,8,12,8"
Background="Transparent">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<CheckBox IsChecked="{Binding IsSelected}"
Style="{StaticResource CustomCheckBox}"
Margin="0,0,8,0"
VerticalAlignment="Center"/>
<TextBlock Text="{Binding Text}"
Grid.Column="1"
FontSize="13"
Foreground="#333333"
VerticalAlignment="Center"/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#F5F5F5" TargetName="Border"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<Grid>
<ToggleButton x:Name="PART_ToggleButton"
Style="{StaticResource ComboBoxToggleButton}"
Height="36"/>
<Popup x:Name="PART_Popup"
PlacementTarget="{Binding ElementName=PART_ToggleButton}"
IsOpen="{Binding IsChecked, ElementName=PART_ToggleButton}"
StaysOpen="False"
AllowsTransparency="True">
<Border Background="White"
BorderBrush="#E8E8E8"
BorderThickness="1"
CornerRadius="6"
Margin="0,4,0,0">
<Border.Effect>
<DropShadowEffect BlurRadius="8"
ShadowDepth="2"
Opacity="0.15"
Color="#000000"/>
</Border.Effect>
<ItemsControl ItemsSource="{Binding Items}"
MaxHeight="300"
ScrollViewer.VerticalScrollBarVisibility="Auto">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ComboBoxItem Style="{StaticResource ComboBoxItemStyle}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
</Popup>
</Grid>
</UserControl>
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Linq;
namespace wpf_FMB
{
public class ComboBoxItem : INotifyPropertyChanged
{
private string _text;
private bool _isSelected;
public string Text
{
get => _text;
set
{
_text = value;
OnPropertyChanged(nameof(Text));
}
}
public bool IsSelected
{
get => _isSelected;
set
{
if (_isSelected != value)
{
_isSelected = value;
OnPropertyChanged(nameof(IsSelected));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public partial class MultiSelectComboBox : UserControl, INotifyPropertyChanged
{
public static readonly DependencyProperty PlaceholderTextProperty =
DependencyProperty.Register("PlaceholderText", typeof(string), typeof(MultiSelectComboBox),
new PropertyMetadata("Select items..."));
public string PlaceholderText
{
get => (string)GetValue(PlaceholderTextProperty);
set => SetValue(PlaceholderTextProperty, value);
}
private ObservableCollection<ComboBoxItem> _items;
public ObservableCollection<ComboBoxItem> Items
{
get => _items;
set
{
_items = value;
OnPropertyChanged(nameof(Items));
UpdateDisplayText();
}
}
public string DisplayText
{
get
{
var selectedItems = Items?.Where(i => i.IsSelected).Select(i => i.Text).ToList();
if (selectedItems == null || !selectedItems.Any())
return PlaceholderText;
if (selectedItems.Count <= 2)
return string.Join(", ", selectedItems);
return $"{selectedItems[0]}, {selectedItems[1]}, +{selectedItems.Count - 2}";
}
}
public MultiSelectComboBox()
{
InitializeComponent();
DataContext = this;
Items = new ObservableCollection<ComboBoxItem>();
Items.CollectionChanged += (s, e) => UpdateDisplayText();
}
private void UpdateDisplayText()
{
OnPropertyChanged(nameof(DisplayText));
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public void AddItem(string text, bool isSelected = false)
{
var item = new ComboBoxItem { Text = text, IsSelected = isSelected };
item.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(ComboBoxItem.IsSelected))
{
UpdateDisplayText();
}
};
Items.Add(item);
}
}
}
progressbar
<UserControl x:Class="YourNamespace.STKInspectionControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel>
<TextBlock Text="title"
FontWeight="SemiBold"
FontSize="13"
Margin="0,0,0,8"/>
<StackPanel Margin="0,4">
<!-- STK01 -->
<DockPanel>
<TextBlock Text="STK01"
FontSize="12"
VerticalAlignment="Center"/>
<TextBlock Text="81/98"
FontSize="12"
Foreground="#666666"
HorizontalAlignment="Right"
Margin="0,0,8,0"/>
<TextBlock Text="82%"
FontSize="12"
Foreground="#666666"
HorizontalAlignment="Right"/>
</DockPanel>
<ProgressBar Value="82"
Maximum="100"
Height="6"
Margin="0,2">
<ProgressBar.Style>
<Style TargetType="ProgressBar">
<Setter Property="Background" Value="#F5F5F5"/>
<Setter Property="Foreground" Value="#2196F3"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Value, RelativeSource={RelativeSource Self},
Converter={StaticResource GreaterThanConverter}, ConverterParameter=90}"
Value="True">
<Setter Property="Foreground" Value="#1976D2"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ProgressBar.Style>
</ProgressBar>
<!-- STK02 -->
<DockPanel Margin="0,8,0,0">
<TextBlock Text="STK02"
FontSize="12"
VerticalAlignment="Center"/>
<TextBlock Text="97/98"
FontSize="12"
Foreground="#666666"
HorizontalAlignment="Right"
Margin="0,0,8,0"/>
<TextBlock Text="99%"
FontSize="12"
Foreground="#666666"
HorizontalAlignment="Right"/>
</DockPanel>
<ProgressBar Value="99"
Maximum="100"
Height="6"
Margin="0,2">
<ProgressBar.Style>
<Style TargetType="ProgressBar">
<Setter Property="Background" Value="#F5F5F5"/>
<Setter Property="Foreground" Value="#2196F3"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Value, RelativeSource={RelativeSource Self},
Converter={StaticResource GreaterThanConverter}, ConverterParameter=90}"
Value="True">
<Setter Property="Foreground" Value="#1976D2"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ProgressBar.Style>
</ProgressBar>
<!-- STK03 -->
<DockPanel Margin="0,8,0,0">
<TextBlock Text="STK03"
FontSize="12"
VerticalAlignment="Center"/>
<TextBlock Text="81/98"
FontSize="12"
Foreground="#666666"
HorizontalAlignment="Right"
Margin="0,0,8,0"/>
<TextBlock Text="82%"
FontSize="12"
Foreground="#666666"
HorizontalAlignment="Right"/>
</DockPanel>
<ProgressBar Value="82"
Maximum="100"
Height="6"
Margin="0,2">
<ProgressBar.Style>
<Style TargetType="ProgressBar">
<Setter Property="Background" Value="#F5F5F5"/>
<Setter Property="Foreground" Value="#2196F3"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Value, RelativeSource={RelativeSource Self},
Converter={StaticResource GreaterThanConverter}, ConverterParameter=90}"
Value="True">
<Setter Property="Foreground" Value="#1976D2"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ProgressBar.Style>
</ProgressBar>
</StackPanel>
</StackPanel>
</UserControl>
using System;
using System.Globalization;
using System.Windows.Data;
namespace YourNamespace
{
public class GreaterThanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is double doubleValue && parameter is string stringParameter)
{
if (double.TryParse(stringParameter, out double threshold))
{
return doubleValue > threshold;
}
}
return false;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
port graph
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:YourNamespace">
<Style TargetType="{x:Type local:PortStatsChart}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:PortStatsChart}">
<Grid>
<!-- Chart Canvas -->
<Canvas x:Name="PART_Canvas" />
<!-- Legend -->
<ItemsControl ItemsSource="{TemplateBinding ItemsSource}"
VerticalAlignment="Bottom"
Margin="0,20,0,10">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Margin="8,4"
Padding="8,4"
Background="#F8F8F8"
CornerRadius="4"
BorderThickness="1"
BorderBrush="#E5E5E5">
<StackPanel Orientation="Horizontal" Spacing="8">
<Rectangle Width="12"
Height="12"
Fill="{Binding Color}"
RadiusX="2"
RadiusY="2" />
<TextBlock Text="{Binding Name}"
FontWeight="SemiBold"
VerticalAlignment="Center"/>
<TextBlock Text="|"
Foreground="#666666"
VerticalAlignment="Center"/>
<TextBlock VerticalAlignment="Center">
<Run Text="{Binding Count}"/>
<Run Text="개"/>
</TextBlock>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- Tooltip -->
<Popup x:Name="PART_Tooltip"
AllowsTransparency="True"
Placement="Relative"
PlacementTarget="{Binding ElementName=PART_Canvas}"
IsOpen="False">
<Border Background="#CC000000"
CornerRadius="4"
Padding="8,6">
<TextBlock x:Name="PART_TooltipText"
Foreground="White"
FontWeight="Medium"/>
</Border>
</Popup>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
// PortStatItem.cs
public class PortStatItem : INotifyPropertyChanged
{
private string _name;
private int _count;
private Color _color;
public string Name
{
get => _name;
set
{
_name = value;
OnPropertyChanged();
}
}
public int Count
{
get => _count;
set
{
_count = value;
OnPropertyChanged();
}
}
public Color Color
{
get => _color;
set
{
_color = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
// PortStatsChart.cs
public class PortStatsChart : Control
{
static PortStatsChart()
{
DefaultStyleKeyProperty.OverrideMetadata(
typeof(PortStatsChart),
new FrameworkPropertyMetadata(typeof(PortStatsChart)));
}
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register(
nameof(ItemsSource),
typeof(IEnumerable<PortStatItem>),
typeof(PortStatsChart),
new PropertyMetadata(null, OnItemsSourceChanged));
public IEnumerable<PortStatItem> ItemsSource
{
get => (IEnumerable<PortStatItem>)GetValue(ItemsSourceProperty);
set => SetValue(ItemsSourceProperty, value);
}
private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is PortStatsChart chart)
{
chart.InvalidateVisual();
}
}
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
if (ItemsSource == null || !ItemsSource.Any()) return;
var centerX = ActualWidth / 2;
var centerY = ActualHeight * 0.4; // Move center up to make room for legend
var radius = Math.Min(centerX, centerY) * 0.8;
var innerRadius = radius * 0.6; // For donut hole
var total = ItemsSource.Sum(item => item.Count);
var startAngle = 180.0;
foreach (var item in ItemsSource)
{
var sweepAngle = (item.Count / (double)total) * 180.0;
var path = CreateArcGeometry(
centerX, centerY,
radius, innerRadius,
startAngle, sweepAngle);
var brush = new SolidColorBrush(item.Color);
drawingContext.DrawGeometry(brush, new Pen(Brushes.White, 1), path);
startAngle += sweepAngle;
}
}
private Geometry CreateArcGeometry(
double centerX, double centerY,
double radius, double innerRadius,
double startAngle, double sweepAngle)
{
var startRadian = startAngle * Math.PI / 180;
var endRadian = (startAngle + sweepAngle) * Math.PI / 180;
var outerStart = new Point(
centerX + radius * Math.Cos(startRadian),
centerY + radius * Math.Sin(startRadian));
var outerEnd = new Point(
centerX + radius * Math.Cos(endRadian),
centerY + radius * Math.Sin(endRadian));
var innerStart = new Point(
centerX + innerRadius * Math.Cos(startRadian),
centerY + innerRadius * Math.Sin(startRadian));
var innerEnd = new Point(
centerX + innerRadius * Math.Cos(endRadian),
centerY + innerRadius * Math.Sin(endRadian));
var figure = new PathFigure { StartPoint = outerStart };
figure.Segments.Add(new ArcSegment(
outerEnd,
new Size(radius, radius),
0,
false,
SweepDirection.Counterclockwise,
true));
figure.Segments.Add(new LineSegment(innerEnd, true));
figure.Segments.Add(new ArcSegment(
innerStart,
new Size(innerRadius, innerRadius),
0,
false,
SweepDirection.Clockwise,
true));
figure.IsClosed = true;
return new PathGeometry { Figures = { figure } };
}
}
checked combo
public class CheckBoxItem : INotifyPropertyChanged
{
private bool _isChecked;
private string _content;
public bool IsChecked
{
get => _isChecked;
set
{
_isChecked = value;
OnPropertyChanged();
}
}
public string Content
{
get => _content;
set
{
_content = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class CheckBoxComboBox : ComboBox
{
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.Register(nameof(SelectedItems), typeof(ObservableCollection<CheckBoxItem>),
typeof(CheckBoxComboBox), new PropertyMetadata(new ObservableCollection<CheckBoxItem>()));
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register(nameof(Text), typeof(string),
typeof(CheckBoxComboBox), new PropertyMetadata(string.Empty));
public ObservableCollection<CheckBoxItem> SelectedItems
{
get => (ObservableCollection<CheckBoxItem>)GetValue(SelectedItemsProperty);
set => SetValue(SelectedItemsProperty, value);
}
public string Text
{
get => (string)GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
public CheckBoxComboBox()
{
SelectedItems = new ObservableCollection<CheckBoxItem>();
}
static CheckBoxComboBox()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CheckBoxComboBox),
new FrameworkPropertyMetadata(typeof(CheckBoxComboBox)));
}
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{
base.OnItemsSourceChanged(oldValue, newValue);
if (oldValue is INotifyCollectionChanged oldCollection)
{
oldCollection.CollectionChanged -= OnItemsChanged;
}
if (newValue is INotifyCollectionChanged newCollection)
{
newCollection.CollectionChanged += OnItemsChanged;
UpdateText();
}
}
private void OnItemsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach (CheckBoxItem item in e.NewItems)
{
item.PropertyChanged += OnItemPropertyChanged;
}
}
if (e.OldItems != null)
{
foreach (CheckBoxItem item in e.OldItems)
{
item.PropertyChanged -= OnItemPropertyChanged;
}
}
}
private void OnItemPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(CheckBoxItem.IsChecked))
{
UpdateText();
}
}
private void UpdateText()
{
if (ItemsSource == null) return;
var selectedItems = ((IEnumerable<CheckBoxItem>)ItemsSource)
.Where(item => item.IsChecked)
.Select(item => item.Content)
.ToList();
if (selectedItems.Count == 0)
{
Text = string.Empty;
return;
}
if (selectedItems.Count <= 2)
{
Text = string.Join(", ", selectedItems);
}
else
{
Text = $"{selectedItems[0]}, {selectedItems[1]}, +{selectedItems.Count - 2}";
}
}
}
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:wpf_FMB">
<Style TargetType="{x:Type local:CheckBoxComboBox}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:CheckBoxComboBox}">
<Grid>
<ToggleButton x:Name="ToggleButton"
BorderBrush="#E8E8E8"
Background="#FAFAFA"
IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}">
<ToggleButton.Template>
<ControlTemplate>
<Border x:Name="Border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="1"
CornerRadius="6">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="32"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Text, RelativeSource={RelativeSource AncestorType=local:CheckBoxComboBox}}"
Margin="12,0,0,0"
FontSize="13"
Foreground="#333333"
VerticalAlignment="Center"/>
<Path Grid.Column="1"
Data="M0,0 L6,6 L12,0"
Stroke="#666666"
StrokeThickness="1.5"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#F5F5F5"/>
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter Property="Background" Value="#F0F0F0"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</ToggleButton.Template>
</ToggleButton>
<Popup IsOpen="{TemplateBinding IsDropDownOpen}"
Placement="Bottom"
AllowsTransparency="True">
<Border Background="White"
BorderBrush="#E8E8E8"
BorderThickness="1"
CornerRadius="6"
Margin="0,4,0,0">
<Border.Effect>
<DropShadowEffect BlurRadius="8"
ShadowDepth="2"
Opacity="0.15"/>
</Border.Effect>
<ScrollViewer MaxHeight="300"
VerticalScrollBarVisibility="Auto">
<StackPanel>
<ItemsPresenter/>
</StackPanel>
</ScrollViewer>
</Border>
</Popup>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate>
<CheckBox Margin="8,8,12,8"
IsChecked="{Binding IsChecked, Mode=TwoWay}">
<CheckBox.Template>
<ControlTemplate TargetType="CheckBox">
<Grid Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Border x:Name="CheckBoxBorder"
Width="16" Height="16"
Background="White"
BorderBrush="#DDDDDD"
BorderThickness="1"
CornerRadius="3"
Margin="0,0,8,0">
<Path x:Name="CheckMark"
Data="M2,6 L6,10 L14,2"
Stroke="White"
StrokeThickness="2"
Visibility="Collapsed"/>
</Border>
<TextBlock Grid.Column="1"
Text="{Binding Content}"
FontSize="13"
Foreground="#333333"
VerticalAlignment="Center"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="CheckBoxBorder" Property="Background" Value="#1677FF"/>
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="#1677FF"/>
<Setter TargetName="CheckMark" Property="Visibility" Value="Visible"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#F5F5F5"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</CheckBox.Template>
</CheckBox>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
status
<?xml version="1.0" encoding="utf-8"?>
<UserControl x:Class="WpfApp.StatusPanel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp">
<UserControl.Resources>
<ObservableCollection x:Key="StatusItems"
xmlns="clr-namespace:System.Collections.ObjectModel;assembly=System"
xmlns:local="clr-namespace:WpfApp">
<local:StatusItem Name="Run" IconColor="#4CAF50" Count="45"/>
<local:StatusItem Name="Ready" IconColor="#FFC107" Count="32"/>
<local:StatusItem Name="PREPD" IconColor="#FF9800" Count="15"/>
<local:StatusItem Name="LOADD" IconColor="#9E9E9E" Count="28"/>
<local:StatusItem Name="BM" IconColor="#F44336" Count="12"/>
<local:StatusItem Name="CALLM" IconColor="#E91E63" Count="8"/>
<local:StatusItem Name="ERROR" IconColor="#FF0000" Count="5"/>
<local:StatusItem Name="MAINT" IconColor="#2196F3" Count="3"/>
</ObservableCollection>
<Style x:Key="AllButtonStyle" TargetType="Button">
<Setter Property="Background" Value="#EEEEEE"/>
<Setter Property="BorderBrush" Value="#DDDDDD"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="8,2"/>
<Setter Property="Height" Value="22"/>
<Setter Property="FontSize" Value="11"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="StatusItemStyle" TargetType="Grid">
<Setter Property="Margin" Value="0,3"/>
<Setter Property="Height" Value="26"/>
</Style>
<Style x:Key="StatusIconStyle" TargetType="Border">
<Setter Property="Width" Value="20"/>
<Setter Property="Height" Value="20"/>
<Setter Property="CornerRadius" Value="4"/>
<Setter Property="Margin" Value="0,0,8,0"/>
</Style>
<local:TotalCountConverter x:Key="TotalCountConverter"/>
</UserControl.Resources>
<Border Background="White"
CornerRadius="8"
BorderThickness="1"
BorderBrush="#E5E5E5">
<StackPanel Margin="16,12,16,16">
<Grid Height="28">
<DockPanel>
<TextBlock Text="장비상태"
FontSize="13"
FontWeight="SemiBold"
VerticalAlignment="Center"/>
<Button Content="ALL"
Style="{StaticResource AllButtonStyle}"
Width="40"
Margin="8,0"/>
<TextBlock VerticalAlignment="Center"
HorizontalAlignment="Right"
Foreground="#666666">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource TotalCountConverter}">
<Binding Source="{StaticResource StatusItems}" Path="Count"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</DockPanel>
</Grid>
<ItemsControl x:Name="StatusList" Margin="0,8,0,0">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Style="{StaticResource StatusItemStyle}">
<DockPanel>
<Border Style="{StaticResource StatusIconStyle}"
Background="{Binding IconColor}"/>
<TextBlock Text="{Binding Name}"
VerticalAlignment="Center"
FontSize="12"/>
<TextBlock Text="{Binding CountDisplay}"
HorizontalAlignment="Right"
VerticalAlignment="Center"
FontSize="12"
Foreground="#666666"/>
</DockPanel>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button Content="상태 더보기 ▼"
Background="Transparent"
BorderThickness="0"
HorizontalAlignment="Center"
Margin="0,8,0,0"
Padding="12,4"
FontSize="11"
Foreground="#666666"
Click="ToggleExpand_Click"/>
</StackPanel>
</Border>
</UserControl>
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
namespace WpfApp
{
public partial class StatusPanel : UserControl
{
private bool isExpanded = false;
public StatusPanel()
{
InitializeComponent();
}
private void ToggleExpand_Click(object sender, RoutedEventArgs e)
{
isExpanded = !isExpanded;
var button = sender as Button;
if (button != null)
{
button.Content = isExpanded ? "상태 숨기기 ▲" : "상태 더보기 ▼";
}
}
}
public class StatusItem : INotifyPropertyChanged
{
private string name;
private string iconColor;
private int count;
public event PropertyChangedEventHandler PropertyChanged;
public string Name
{
get => name;
set
{
name = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
}
}
public string IconColor
{
get => iconColor;
set
{
iconColor = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IconColor)));
}
}
public int Count
{
get => count;
set
{
count = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count)));
}
}
}
}
font-size
using System.Windows;
using System.Windows.Media;
using System.Globalization;
public static class TextMeasurement
{
public static Size MeasureText(string text, string fontFamily, double fontSize)
{
var formattedText = new FormattedText(
text,
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
new Typeface(fontFamily),
fontSize,
Brushes.Black,
VisualTreeHelper.GetDpi(Application.Current.MainWindow).PixelsPerDip);
return new Size(formattedText.Width, formattedText.Height);
}
}
// Usage example:
// Size textSize = TextMeasurement.MeasureText("Sample Text", "Arial", 14);
// double width = textSize.Width;
// double height = textSize.Height;
animation
// PortStatItem.cs
public class PortStatItem : INotifyPropertyChanged
{
private string _name;
private int _count;
private Color _color;
public string Name
{
get => _name;
set
{
_name = value;
OnPropertyChanged();
}
}
public int Count
{
get => _count;
set
{
_count = value;
OnPropertyChanged();
}
}
public Color Color
{
get => _color;
set
{
_color = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
// PortStatsChart.cs
public class PortStatsChart : Control
{
private readonly List<double> _currentAngles = new();
private readonly List<double> _targetAngles = new();
private readonly Storyboard _animationStoryboard = new();
private bool _isAnimating;
static PortStatsChart()
{
DefaultStyleKeyProperty.OverrideMetadata(
typeof(PortStatsChart),
new FrameworkPropertyMetadata(typeof(PortStatsChart)));
}
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register(
nameof(ItemsSource),
typeof(IEnumerable<PortStatItem>),
typeof(PortStatsChart),
new PropertyMetadata(null, OnItemsSourceChanged));
public IEnumerable<PortStatItem> ItemsSource
{
get => (IEnumerable<PortStatItem>)GetValue(ItemsSourceProperty);
set => SetValue(ItemsSourceProperty, value);
}
private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is PortStatsChart chart)
{
chart.StartAnimation();
}
}
private void StartAnimation()
{
_animationStoryboard.Stop();
_animationStoryboard.Children.Clear();
if (ItemsSource == null || !ItemsSource.Any()) return;
var total = ItemsSource.Sum(item => item.Count);
var startAngle = 180.0;
_targetAngles.Clear();
foreach (var item in ItemsSource)
{
var sweepAngle = (item.Count / (double)total) * 180.0;
_targetAngles.Add(startAngle + sweepAngle);
startAngle += sweepAngle;
}
// Initialize current angles if needed
if (_currentAngles.Count != _targetAngles.Count)
{
_currentAngles.Clear();
_currentAngles.AddRange(Enumerable.Repeat(180.0, _targetAngles.Count));
}
var animation = new DoubleAnimation
{
From = 0,
To = 1,
Duration = TimeSpan.FromMilliseconds(500),
EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut }
};
_isAnimating = true;
animation.Completed += (s, e) => _isAnimating = false;
var clockGroup = new AnimationClock(animation, true);
clockGroup.CurrentTimeInvalidated += (s, e) =>
{
if (clockGroup.CurrentProgress.HasValue)
{
for (int i = 0; i < _currentAngles.Count; i++)
{
_currentAngles[i] = 180 + (_targetAngles[i] - 180) * clockGroup.CurrentProgress.Value;
}
InvalidateVisual();
}
};
_animationStoryboard.Children.Add(animation);
_animationStoryboard.Begin(this, true);
}
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
if (ItemsSource == null || !ItemsSource.Any()) return;
var centerX = ActualWidth / 2;
var centerY = ActualHeight * 0.4;
var radius = Math.Min(centerX, centerY) * 0.8;
var innerRadius = radius * 0.6;
var items = ItemsSource.ToList();
var prevAngle = 180.0;
for (int i = 0; i < items.Count; i++)
{
var currentAngle = _isAnimating ? _currentAngles[i] : _targetAngles[i];
var sweepAngle = currentAngle - prevAngle;
var path = CreateArcGeometry(
centerX, centerY,
radius, innerRadius,
prevAngle, sweepAngle);
var brush = new SolidColorBrush(items[i].Color);
drawingContext.DrawGeometry(brush, new Pen(Brushes.White, 1), path);
prevAngle = currentAngle;
}
}
private Geometry CreateArcGeometry(
double centerX, double centerY,
double radius, double innerRadius,
double startAngle, double sweepAngle)
{
var startRadian = startAngle * Math.PI / 180;
var endRadian = (startAngle + sweepAngle) * Math.PI / 180;
var outerStart = new Point(
centerX + radius * Math.Cos(startRadian),
centerY + radius * Math.Sin(startRadian));
var outerEnd = new Point(
centerX + radius * Math.Cos(endRadian),
centerY + radius * Math.Sin(endRadian));
var innerStart = new Point(
centerX + innerRadius * Math.Cos(startRadian),
centerY + innerRadius * Math.Sin(startRadian));
var innerEnd = new Point(
centerX + innerRadius * Math.Cos(endRadian),
centerY + innerRadius * Math.Sin(endRadian));
var figure = new PathFigure { StartPoint = outerStart };
figure.Segments.Add(new ArcSegment(
outerEnd,
new Size(radius, radius),
0,
false,
SweepDirection.Counterclockwise,
true));
figure.Segments.Add(new LineSegment(innerEnd, true));
figure.Segments.Add(new ArcSegment(
innerStart,
new Size(innerRadius, innerRadius),
0,
false,
SweepDirection.Clockwise,
true));
figure.IsClosed = true;
return new PathGeometry { Figures = { figure } };
}
}
햄버거버튼
<!-- 햄버거 메뉴 클릭 애니메이션 -->
<Storyboard x:Key="MenuClickAnimation">
<DoubleAnimation
Storyboard.TargetName="MenuIcon"
Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)"
From="0" To="180" Duration="0:0:0.3">
<DoubleAnimation.EasingFunction>
<CubicEase EasingMode="EaseInOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<!-- 햄버거 메뉴 마우스 오버 애니메이션 -->
<Storyboard x:Key="MenuHoverAnimation">
<DoubleAnimation
Storyboard.TargetName="MenuIcon"
Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)"
To="90" Duration="0:0:0.2">
<DoubleAnimation.EasingFunction>
<CubicEase EasingMode="EaseInOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<!-- 햄버거 메뉴 마우스 리브 애니메이션 -->
<Storyboard x:Key="MenuLeaveAnimation">
<DoubleAnimation
Storyboard.TargetName="MenuIcon"
Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)"
To="0" Duration="0:0:0.2">
<DoubleAnimation.EasingFunction>
<CubicEase EasingMode="EaseInOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<!-- 햄버거 메뉴 버튼 -->
<Button x:Name="MenuButton"
Grid.Column="0"
Width="40" Height="40"
Background="Transparent"
BorderThickness="0"
Click="MenuButton_Click"
MouseEnter="MenuButton_MouseEnter"
MouseLeave="MenuButton_MouseLeave">
<Grid x:Name="MenuIcon" RenderTransformOrigin="0.5,0.5">
<Grid.RenderTransform>
<RotateTransform/>
</Grid.RenderTransform>
<Path Data="M4,6 L20,6 M4,12 L20,12 M4,18 L20,18"
Stroke="#333333"
StrokeThickness="2"
Width="24"
Height="24"
Stretch="None"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"/>
</Grid>
</Button>
private void MenuButton_Click(object sender, RoutedEventArgs e)
{
var storyboard = (Storyboard)FindResource("MenuClickAnimation");
if (_isMenuOpen)
{
storyboard.AutoReverse = true;
storyboard.Begin();
}
else
{
storyboard.AutoReverse = false;
storyboard.Begin();
}
_isMenuOpen = !_isMenuOpen;
}
private void MenuButton_MouseEnter(object sender, MouseEventArgs e)
{
if (!_isMenuOpen)
{
_isHovered = true;
var storyboard = (Storyboard)FindResource("MenuHoverAnimation");
storyboard.Begin();
viewModel.ShowPanel();
}
}
private void MenuButton_MouseLeave(object sender, MouseEventArgs e)
{
if (!_isMenuOpen && _isHovered)
{
_isHovered = false;
var storyboard = (Storyboard)FindResource("MenuLeaveAnimation");
storyboard.Begin();
}
}
private void OnClickProfile(object sender, RoutedEventArgs e)
{
var logon = new LogOn();
logon.Show();
}
private void SlidingPanel_MouseLeave(object sender, MouseEventArgs e)
{
// 마우스가 햄버거 버튼 위에 있는지 확인
Point mousePos = e.GetPosition(MenuButton);
if (mousePos.X < 0 || mousePos.Y < 0 ||
mousePos.X > MenuButton.ActualWidth ||
mousePos.Y > MenuButton.ActualHeight)
{
viewModel.HidePanel();
}
}
라운딩컴포넌트들
<TextBox Style="{StaticResource RoundedTextBoxStyle}">
<TextBox.Resources>
<Style x:Key="RoundedTextBoxStyle" TargetType="TextBox">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TextBox">
<Border Background="White"
BorderBrush="#E5E5E5"
BorderThickness="1"
CornerRadius="20">
<Grid>
<TextBox Text="{Binding Path=Text,
RelativeSource={RelativeSource TemplatedParent},
UpdateSourceTrigger=PropertyChanged}"
Background="Transparent"
BorderThickness="0"
Padding="20,10"
VerticalContentAlignment="Center"/>
<TextBlock Text="영역 이름을 작성해 주세요."
Foreground="#999999"
Margin="20,0,0,0"
VerticalAlignment="Center"
Visibility="{Binding Path=Text.IsEmpty,
RelativeSource={RelativeSource TemplatedParent},
Converter={StaticResource BooleanToVisibilityConverter}}"/>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</TextBox.Resources>
</TextBox>
<ComboBox Text="Horizontal" Width="200" Height="40">
<ComboBox.Resources>
<Style TargetType="ComboBox">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ComboBox">
<Grid>
<Border x:Name="border"
CornerRadius="8"
Background="White"
BorderBrush="#E5E5E5"
BorderThickness="1">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="30"/>
</Grid.ColumnDefinitions>
<ContentPresenter
Content="{TemplateBinding SelectionBoxItem}"
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
Margin="12,0,0,0"
VerticalAlignment="Center"/>
<Path Grid.Column="1"
Data="M0,0 L5,5 L10,0"
Stroke="#666666"
StrokeThickness="2"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Grid>
</Border>
<Popup IsOpen="{TemplateBinding IsDropDownOpen}"
Placement="Bottom"
PlacementTarget="{Binding ElementName=border}">
<Border Background="White"
BorderBrush="#E5E5E5"
BorderThickness="1"
CornerRadius="8"
Margin="0,4,0,0">
<ScrollViewer MaxHeight="200" VerticalScrollBarVisibility="Auto">
<StackPanel IsItemsHost="True" Margin="0,4"/>
</ScrollViewer>
</Border>
</Popup>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="border" Property="BorderBrush" Value="#CCCCCC"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="ComboBoxItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ComboBoxItem">
<Border Padding="12,8" Background="Transparent">
<ContentPresenter/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#F5F5F5"/>
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="#E3F2FD"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ComboBox.Resources>
<ComboBoxItem>Horizontal</ComboBoxItem>
<ComboBoxItem>Vertical</ComboBoxItem>
<ComboBoxItem>Diagonal</ComboBoxItem>
</ComboBox>