지난 글에서 WPF에 프로퍼티 그리드를 적용하는 방법을 살펴 보았습니다. 이 것으로 다양한 속성 값들을 툴에서 컨트롤 할수 있게 되었죠. 그런데 앞서 언급했던 것처럼 여러 속성 값들의 타입이 각자 제각각입니다. 그 중 많이 쓰이는 타입 중 하나가 벡터3 ( Vector3 )이 있습니다. 위치를 나타낼때 많이 쓰이는 이 타입의 값을 프로퍼티 그리드에서 어떻게 표시해야 될까요? 아마 밑의 그림과 같이 표시할 수도 있을 겁니다.
벡터3의 x, y, z 성분들을 각각 하나의 값으로 보고 값을 입력 받을수 있도록 하는 것이죠. 하지만 벡터3의 타입이 많이 쓰이게 된다면 관리 해야될 속성값이 기하급수적으로 늘어나게 될 것입니다. 다른 방법은 없을까요?
보통 엔진단에서 보면 벡터를 하나의 타입으로 간주하고 사용을 합니다. 툴에서도 같은 식으로 써야 논리적으로 맞을것 같습니다. 벡터 클래스를 하나 만들기로 하죠.
// 간단한 벡터3 타입 클래스
public class Vector3
{
public float m_fX;
public float m_fY;
public float m_fZ;
public Vector3()
{
m_fX = 0;
m_fY = 0;
m_fZ = 0;
}
}
그리고 이 클래스를 프로퍼티 그리드에 등록하겠습니다.
// 중복 코드는 생략
public class Person
{
private Vector3 m_vLocate;
[Category("기타등등")]
[DisplayName("위치")]
public Vector3 locProp
{
set { m_vLocate = value; }
get { return m_vLocate; }
}
}
한번 실행해 보죠.
프로퍼티 그리드에 등록은 되는 것 같습니다. 하지만 값을 입력할라 치면, 아래와 같은 오류 메시지가 뜹니다.
이유는 입력한 값을 Vector3 타입으로 변환 할수 없기 때문입니다. 즉, 입력한 값 ( 보통 문자열 )에서 Vector3 타입으로 변환해 줄수 있는 변환기가 필요하다는 것 입니다. 그렇다면 변환기를 만들어주도록 하죠.
// Vector3 변환기
public class Vector3Convter : TypeConverter
{
// string 으로 부터 변환이 가능한가?
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (sourceType == typeof(string))
{
return true;
}
return base.CanConvertFrom(context, sourceType);
}
// string 으로 부터 vector3로 변환
public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
{
if (value is string)
{
string[] v = ((string)value).Split(new char[] { ',' });
return new Vector3(float.Parse(v[0]), float.Parse(v[1]), float.Parse(v[2]));
}
return base.ConvertFrom(context, culture, value);
}
// vector3 에서 string으로 변환
public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
{
if (destinationType == typeof(string))
{
return ((Vector3)value).m_fX + "," + ((Vector3)value).m_fY + "," + ((Vector3)value).m_fZ;
}
return base.ConvertTo(context, culture, value, destinationType);
}
}
변환기를 만들어 주기 위해서는 TypeConver 클래스를 상속 받아 CanConvertFrom, CanvertFrom, CanConvertTo, ConvertTo 4가지 함수를 재정의 해주어야 합니다. 여기서는 CanConvertTo가 그닥 필요치 않아 생략 되었습니다( 자세한 내용은 MSDN 참조 ). 변환기 클래스를 만들었으면 이제 벡터3 프로퍼티에 변환기를 등록해줍니다.
// 중복 코드는 생략
public class Person
{
private Vector3 m_vLocate;
[Category("기타등등")]
[DisplayName("위치")]
[TypeConverter(typeof(Vector3Convter))] // 변환기 등록
public Vector3 locProp
{
set { m_vLocate = value; }
get { return m_vLocate; }
}
}
이제 프로퍼티 그리드에서 벡터3 타입이 제대로 입출력 되는지 확인해보도록 하죠.
실행을 해보니 벡터3의 기본값인 0, 0, 0이 출력됩니다. 그리고 10, 55, 38을 입력하면, 아까와 같은 오류 없이 제대로 입력이 됩니다. ( 그외 맞지 않는 문자열등이 입력될 때를 대비해 변환기에서 예외처리를 해주면 좋습니다 )
이처럼 변환기를 통해 기본 타입 이외의 타입들을 관리 할수 있는 방법을 알아봤습니다. 하지만 여기서 좀더 유저 편의성을 높일수 있는 방법이 있습니다. 예로 특정 정수값이 있는데, 이 값은 0 ~ 100까지 값을 가집니다. 그런데 이 값을 직접 입력받는 것이 아니라 슬라이더 컨트롤러(Slider)를 이용해 값을 정할수 있다면 어떨까요? 사용자 입장에서도 꽤나 편리 할것 같습니다.
프로퍼티 그리드에 기본적으로 제공되는 기본 컨틀롤러 이외의 커스텀 컨트롤러를 추가하는 방법을 알아보겠습니다. 위에 이야기한 슬라이더 컨트롤러를 추가해보도록 하죠.
public class CustomSlider : ExtendedPropertyValueEditor
{
public CustomSlider()
{
// Template for normal view
string template1 = @"
<DataTemplate
xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:pe='clr-namespace:System.Activities.Presentation.PropertyEditing;assembly=System.Activities.Presentation'
xmlns:wpg='clr-namespace:PropertyGrid;assembly=PropertyGrid' >
<DockPanel LastChildFill='True'>
<TextBox Text='{Binding Path=Value.Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}' Width='40' TextAlignment='Center' />
<Slider x:Name='slider1' Value='{Binding Path=Value.Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}' Margin='2,0,0,0' Minimum='{Binding Value.Min}' Maximum='{Binding Value.Max}' />
</DockPanel>
</DataTemplate>";
// Load templates
using( var sr = new MemoryStream( Encoding.UTF8.GetBytes( template1 ) ) )
{
this.InlineEditorTemplate = XamlReader.Load( sr ) as DataTemplate;
}
}
}
먼저 ExtendedPropertyValueEditor 클래스를 상속 받아 CustomSlider 클래스를 만듭니다. 그 다음 XAML을 통해 슬라이더 컨트롤러 DataTemplate을 구성합니다. 여기서 끝이 아닙니다. 이 컨트롤러와 바인딩할 객체가 있어야합니다. 즉, 슬라이드를 움직이면 그 슬라이더 값이 저장될 곳이 있어야 합니다. 위 코드를 보면 미리 Binding Value.xxx 형태로 바인딩이 적용되어있습니다.
슬라이더 컨트롤러의 경우 현재값, 최소값, 최대값, 단계값 총 4개의 값이 필요합니다. 이를 위해 따로 슬라이더 컨트롤러를 위한 클래스를 하나 정의해주도록 하겠습니다.
public class SliderValue<T>
{
public SliderValue(T value, T min, T max, T step)
{
Value = value;
Min = min;
Max = max;
Step = step;
}
public T Value { get; set; }
public T Max { get; set; }
public T Min { get; set; }
public T Step { get; set; }
}
이제 실제로 슬라이더 컨트롤러를 프로퍼티 그리드에 사용해보도록 하겠습니다.
// 중복 코드 생략
public class Person
{
private SliderValue<int> m_iAge;
public Person()
{
m_iAge = new SliderValue<int>(22, 0, 100, 1);
}
[Category("인적사항")]
[DisplayName("나이")]
[Editor(typeof(CustomSlider), typeof(PropertyValueEditor))]
public SliderValue<int> ageProp
{
set { m_iAge = value; }
get { return m_iAge; }
}
}
실제 슬라이더 컨트롤러가 적용된 스샷입니다. 슬라이더를 움직이면 0 부터 100까지 값을 조절할수 있고, 직접 값을 입력하면 슬라이더 컨트롤러도 같이 움직이는 것을 확인할 수 있습니다. 단, 직접 입력시에는 0~100까지 제한이라는 예외처리가 안되어있기 때문에 이를 위해서는 추가 작업을 해주어야 합니다.
위의 슬라이더 컨트롤러 XAML을 살펴보면 UpdateSourceTrigger=PropertyChanged 라는 구문이 있습니다. 이는 값이 변경 되었을때 PropertyChanged 이벤트를 날려주기 위함이지요. 이 이벤트를 받는 방법을 구현해보도록 하겠습니다. PropertyChanged 이벤트를 받기 위해서는 INotifyPropertyChanged 클래스를 상속 받아야 합니다. 앞서 구현한 SliderValue 클래스를 조금 수정하도록 하겠습니다.
// 중복 코드 생략
public class SliderValue<T> : INotifyPropertyChanged
{
private T mValue;
public T Value
{
get
{
return mValue;
}
set
{
mValue = value;
OnPropertyChanged("Value");
}
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
#endregion
}
이제 슬라이더 컨트롤러를 통해 값이 변경 되면 PropertyChanged 이벤트를 받을수 있게 되었습니다. 슬라이더 컨트롤러를 사용하는 Person 클래스에 슬라이더 컨트롤러의 PropertyChanged 이벤트를 받을수 있는 콜백 함수를 등록 하면 슬라이더 컨트롤러의 값이 변경 되었을 때의 작업을 수행할 수 있습니다.
public class Person
{
public Person()
{
m_iAge = new SliderValue<int>(22, 0, 100, 1);
m_iAge.PropertyChanged += new PropertyChangedEventHandler(SliderPropChanged);
}
// CustomSlider에서 값이 변경 되었다~
private void SliderPropChanged(object sender, PropertyChangedEventArgs e)
{
if (e != null)
{
}
}
}