반응형

 

다운주소 http://www.codeproject.com/Articles/87715/Native-WPF-4-PropertyGrid

 

 

 

 


출처 : http://gamedevforever.tistory.com/211

 

 

 

툴의 꽃 프러퍼티 그리드를 사용해보자 ( PropertyGrid for WPF )

 
 
 

게임을 제작하게 되면 게임에 등장하는 여러 오브젝트들을 만들게 됩니다. 그런데 이 오브젝트들은 많은 속성 값을 가지게 됩니다. 위치라든가, 크기, 속도, 텍스쳐는 어느 것을 쓰는지, 이 객체에 쓰일 이펙트는 어떤 것인지 같은 것 말입니다. 보통 이런 속성 값들은 툴을 이용해 제어하게 됩니다. 그런데, 이 속성 값들을 지정해주기 위해서는 입력 필드가 필요하겠지요?

 

윈폼이나 WPF에서는 체크박스, 텍스트박스 같은 기본 컨트롤등이 제공되고 있습니다. 이 컨트롤들을 이용해 위에서 말한 속성 값들을 정의 할 수 있습니다. 하지만 많은 수의 속성 값들을 제어 하기에는 역부족입니다. 각각의 오브젝트들이 같은 속성 값을 가지고 있는 것도 아니고, 오브젝트 종류에 따라 많게는 수십가지의 속성 값을 필요로 하는데 이 많은 속성 값들을 위한 입력 필드들을 재구성하고, 추가하는 것을 만들라 하면 누구라도 멘붕이 올 것입니다.

 

벌써 부터 하기 싫어진다...

 

수많은 속성 값들을 손쉽게 제어할수 있는 컨트롤이 있으면 어떨까요? 게다가 제작도 간편해야 합니다. 툴을 만들때 딱! 하니 뙇! 하고 나와주면 얼마나 좋겠습니까? 그런데 그게 실제로 존재합니다.

 


ㅋㅋㅋㅋ

 

그게 무엇인지 알아보기 전에 다른 이름 있는 게임 엔진 툴들은 이것을 어떻게 처리 하고 있는지 한번 살펴 보죠.

 

 

 

 

 

내노라 하는 게임 엔진들을 보면 항상 보이는 놈이있죠. 직접 툴을 만들어 보시면서 사용해보신 분들도 계실겁니다. 바로 프로퍼티 그리드 ( Property Grid )죠. 이 프로퍼티 그리드는 그 이름 답게 수많은 속성 값들을 제어할수 있게 도와주는 아주 유용한 컨트롤입니다. 특히 이 프로퍼티 그리드가 유용한 이유 중 하나가 바로 다양한 타입의 값들을 제어 할수 있다는 것입니다.


툴에서 사용되는 많은 속성 값들은 타입이 제각각 입니다. 단순 불린 ( Boolean ) 타입 부터 정수, 실수는 물론이고, Point, Vector3/4, 16진수 컬러값까지 다양한 값들이 존재합니다. 프로퍼티 그리드는 이런 다양한 타입들을 한 곳에서 제어할수 있는 수단을 제공해주죠. 불린은 체크박스 형태로, 스핀 컨트롤로 정교한 실수값 설정, 브러시 컨트롤을 이용한 색 추출, 그리고 포인트나 벡터 같이 동시에 다수의 값들이 필요한 타입까지도 말이지요.

 

좋습니다. 그렇다면 어떻게 해야 이 프로퍼티 그리드라는 놈을 내가 만드는 툴에 뙇~ 하고 넣을 수 있을까요? 안타깝게도 이 프로퍼티 그리드라는 놈은 WPF에서 기본으로 제공해주는 컨트롤이 아닙니다.


이런 고자 같은 WPF 같으니...

 

그렇다면 어떻게 WPF에서 프로퍼티 그리드를 사용해야 하는가? 고맙게도 이미 많은 능력자 분들께서 WPF에서 프로퍼티 그리드를 사용할수 있게 만들어 공개해두셨습니다. MS도 참 웃긴게 비쥬얼 스튜디오 Visual Studio 에서도 사용 되는 컨트롤을 왜 기본으로 제공 안해주는지 모르겠습니다(블렌드 Blend 같은 툴 팔아먹으려고 그러는 것 같아요).

 

일단 제가 찾아서 사용해본 몇몇 프로퍼티 그리드등이 있는데, 각각 구현 방식도 다르고, 비쥬얼도 다르지만 가장 제대로 작동하고, 사용하기 쉬웠던 것은 Jaime Olivares 라는 분이 만드신 Native WPF 4 PropertyGrid 였습니다.

 

 

프로퍼티 그리드를 사용하기 위해서는 먼저 C#의 프로퍼티에 대해 알아야합니다. 이 프로퍼티 기능은 C++ 식으로 말하면 일종의 Getter/Setter 인터페이스라고 할수 있습니다. 소스를 보시면 쉽게 이해 되실 겁니다.

  1. public class Person  
  2. {  
  3.     public enum Gender { Male, Female }  
  4.   
  5.     private string m_strName;  
  6.     private int m_iAge;  
  7.     private Gender m_eGender;  
  8.   
  9.     public Person()  
  10.     {  
  11.         m_strName = "친절한티스";  
  12.         m_iAge = 21;  
  13.         m_eGender = Gender.Male;  
  14.     }  
  15.   
  16.     public string nameProp  
  17.     {  
  18.         set { m_strName = value; }  
  19.         get { return m_strName; }  
  20.     }  
  21.   
  22.     public int ageProp  
  23.     {  
  24.         set { m_iAge = value; }  
  25.         get { return m_iAge; }  
  26.     }  
  27.   
  28.     public Gender genderProp  
  29.     {  
  30.         set { m_eGender = value; }  
  31.         get { return m_eGender; }  
  32.     }  
  33. }  

멤버 변수는 private 으로 설정 되어있고, 그 멤버 변수에 대한 nameProp, ageProp, genderProp 프로퍼티들이 선언 되어있습니다. 외부에서 멤버 변수의 값을 쓰거나 읽을때 이 프로퍼티들을 이용하게 되죠.

  1. Person prop = new Person();  
  2. prop.nameProp = "김포프";  
  3. prop.ageProp = 22;  
  4. prop.genderProp = Person.Gender.Female;  

...이렇게 말이죠.

 

갑자기 C#의 프로퍼티 기능에 대해서는 왜 말하는 거지? 하고 의아해 하실 수 있는데, 프로퍼티 그리드가 바로 이 프로퍼티 기능을 이용하기 때문입니다. 게다가 무려 프로퍼티를 선언해 두면 프로퍼티 그리드에서 알아서 컨트롤에 생성을 해줍니다!!!!!!!

 

그렇다면 정말로 프로퍼티 그리드에 위의 속성값들이 나오는지 만들어보겠습니다. 두큰두큰~ 위에서 받은 WpfPropertyGrid.cs 파일을 프로젝트에 추가 해주고 아래 그림과 같이 프로젝트에 참조를 추가 해줍니다.

 

 

그리고 프로퍼티 그리드를 추가할 윈도우 xaml 파일을 열어 아래와 같이 추가해줍니다.

 

 

이제 프로퍼티 그리드에 표시해줄 속성 값을 가지고 있는 오브젝트를 생성해서 적용해주면 됩니다.
  1. public MainWindow()  
  2. {  
  3.     InitializeComponent();  
  4.   
  5.     Person prop = new Person();  
  6.     prop.nameProp = "김포프";  
  7.     prop.ageProp = 22;  
  8.     prop.genderProp = Person.Gender.Female;  
  9.   
  10.     // 프로퍼티 그리드에 적용  
  11.     propertyGrid1.SelectedObject = prop;  
  12. }  

끝입니다. 참쉽죠? 정말 제대로 나올까요? 한번 실행해보겠습니다.

 

 

22살의 아리따리운 미소녀 김포프 프로퍼티 그리드가 나타났습니다. 우왕~ 그런데 좀 이상하군요. 카테고리 분류도 없고, 속성값 이름도 프로퍼티 이름이 그대로 나옵니다. 다른 엔진 툴에서 사용되는 프로퍼티 그리드를 보면 카테고리도 잘 분류되어있고, 속성값 이름들도 그럴싸하게 명명되어 있는데 말이죠. 이를 바꿔주기 위해서는 C#의 Attributes를 이용해주어야 합니다.

  1. public class Person  
  2. {  
  3.     public enum Gender { Male, Female }  
  4.   
  5.     private string m_strName;  
  6.     private int m_iAge;  
  7.     private Gender m_eGender;  
  8.   
  9.     public Person()  
  10.     {  
  11.         m_strName = "박기헌";  
  12.         m_iAge = 31;  
  13.         m_eGender = Gender.Male;  
  14.     }  
  15.   
  16.     [Category("인적사항")]  
  17.     [DisplayName("이름")]  
  18.     public string nameProp  
  19.     {  
  20.         set { m_strName = value; }  
  21.         get { return m_strName; }  
  22.     }  
  23.   
  24.     [Category("인적사항")]  
  25.     [DisplayName("나이")]  
  26.     public int ageProp  
  27.     {  
  28.         set { m_iAge = value; }  
  29.         get { return m_iAge; }  
  30.     }  
  31.   
  32.     [Category("기타등등")]  
  33.     [DisplayName("성별")]  
  34.     public Gender genderProp  
  35.     {  
  36.         set { m_eGender = value; }  
  37.         get { return m_eGender; }  
  38.     }  
  39. }  

Category와 DisplayName Attributes를 추가 하였습니다. 어떻게 바뀌었을까요?



카테고리가 생겼고, 속성 값명도 지정해준대로 잘 나옵니다. 짝짝짝~~...계속
( 예상보다 내용이 너무 길어져서... 상하로 나눠서 써야 할것 같군요. 진짜 연재가 되어버렸네... )







 

 

지난 글에서 WPF에 프로퍼티 그리드를 적용하는 방법을 살펴 보았습니다. 이 것으로 다양한 속성 값들을 툴에서 컨트롤 할수 있게 되었죠. 그런데 앞서 언급했던 것처럼 여러 속성 값들의 타입이 각자 제각각입니다. 그 중 많이 쓰이는 타입 중 하나가 벡터3 ( Vector3 )이 있습니다. 위치를 나타낼때 많이 쓰이는 이 타입의 값을 프로퍼티 그리드에서 어떻게 표시해야 될까요? 아마 밑의 그림과 같이 표시할 수도 있을 겁니다.

 

 

벡터3의 x, y, z 성분들을 각각 하나의 값으로 보고 값을 입력 받을수 있도록 하는 것이죠. 하지만 벡터3의 타입이 많이 쓰이게 된다면 관리 해야될 속성값이 기하급수적으로 늘어나게 될 것입니다. 다른 방법은 없을까요?

 

보통 엔진단에서 보면 벡터를 하나의 타입으로 간주하고 사용을 합니다. 툴에서도 같은 식으로 써야 논리적으로 맞을것 같습니다. 벡터 클래스를 하나 만들기로 하죠.

  1. // 간단한 벡터3 타입 클래스  
  2. public class Vector3  
  3. {  
  4.     public float m_fX;  
  5.     public float m_fY;  
  6.     public float m_fZ;  
  7.   
  8.     public Vector3()  
  9.     {  
  10.         m_fX = 0;  
  11.         m_fY = 0;  
  12.         m_fZ = 0;  
  13.     }  
  14. }  

그리고 이 클래스를 프로퍼티 그리드에 등록하겠습니다.

  1. // 중복 코드는 생략  
  2. public class Person  
  3. {  
  4.     private Vector3 m_vLocate;  
  5.   
  6.     [Category("기타등등")]  
  7.     [DisplayName("위치")]  
  8.     public Vector3 locProp  
  9.     {  
  10.         set { m_vLocate = value; }  
  11.         get { return m_vLocate; }  
  12.     }  
  13. }  

한번 실행해 보죠.

 

 

프로퍼티 그리드에 등록은 되는 것 같습니다. 하지만 값을 입력할라 치면, 아래와 같은 오류 메시지가 뜹니다.

 

 

이유는 입력한 값을 Vector3 타입으로 변환 할수 없기 때문입니다. 즉, 입력한 값 ( 보통 문자열 )에서 Vector3 타입으로 변환해 줄수 있는 변환기가 필요하다는 것 입니다. 그렇다면 변환기를 만들어주도록 하죠.

  1. // Vector3 변환기  
  2. public class Vector3Convter : TypeConverter  
  3. {  
  4.     // string 으로 부터 변환이 가능한가?  
  5.     public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)  
  6.     {  
  7.         if (sourceType == typeof(string))  
  8.         {  
  9.             return true;  
  10.         }  
  11.         return base.CanConvertFrom(context, sourceType);  
  12.     }  
  13.   
  14.     // string 으로 부터 vector3로 변환  
  15.     public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)  
  16.     {  
  17.         if (value is string)  
  18.         {  
  19.             string[] v = ((string)value).Split(new char[] { ',' });  
  20.             return new Vector3(float.Parse(v[0]), float.Parse(v[1]), float.Parse(v[2]));  
  21.         }  
  22.         return base.ConvertFrom(context, culture, value);  
  23.     }  
  24.   
  25.     // vector3 에서 string으로 변환  
  26.     public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)  
  27.     {  
  28.         if (destinationType == typeof(string))  
  29.         {  
  30.             return ((Vector3)value).m_fX + "," + ((Vector3)value).m_fY + "," + ((Vector3)value).m_fZ;  
  31.         }  
  32.         return base.ConvertTo(context, culture, value, destinationType);  
  33.     }  
  34. }  

변환기를 만들어 주기 위해서는 TypeConver 클래스를 상속 받아 CanConvertFrom, CanvertFrom, CanConvertTo, ConvertTo 4가지 함수를 재정의 해주어야 합니다. 여기서는 CanConvertTo가 그닥 필요치 않아 생략 되었습니다( 자세한 내용은 MSDN 참조 ). 변환기 클래스를 만들었으면 이제 벡터3 프로퍼티에 변환기를 등록해줍니다.

  1. // 중복 코드는 생략  
  2. public class Person  
  3. {  
  4.     private Vector3 m_vLocate;  
  5.   
  6.     [Category("기타등등")]  
  7.     [DisplayName("위치")]  
  8.     [TypeConverter(typeof(Vector3Convter))] // 변환기 등록  
  9.     public Vector3 locProp  
  10.     {  
  11.         set { m_vLocate = value; }  
  12.         get { return m_vLocate; }  
  13.     }  
  14. }  

이제 프로퍼티 그리드에서 벡터3 타입이 제대로 입출력 되는지 확인해보도록 하죠.


 

 

 

실행을 해보니 벡터3의 기본값인 0, 0, 0이 출력됩니다. 그리고 10, 55, 38을 입력하면, 아까와 같은 오류 없이 제대로 입력이 됩니다. ( 그외 맞지 않는 문자열등이 입력될 때를 대비해 변환기에서 예외처리를 해주면 좋습니다 )

 

이처럼 변환기를 통해 기본 타입 이외의 타입들을 관리 할수 있는 방법을 알아봤습니다. 하지만 여기서 좀더 유저 편의성을 높일수 있는 방법이 있습니다. 예로 특정 정수값이 있는데, 이 값은 0 ~ 100까지 값을 가집니다. 그런데 이 값을 직접 입력받는 것이 아니라 슬라이더 컨트롤러(Slider)를 이용해 값을 정할수 있다면 어떨까요? 사용자 입장에서도 꽤나 편리 할것 같습니다.
프로퍼티 그리드에 기본적으로 제공되는 기본 컨틀롤러 이외의 커스텀 컨트롤러를 추가하는 방법을 알아보겠습니다. 위에 이야기한 슬라이더 컨트롤러를 추가해보도록 하죠.
  1. public class CustomSlider : ExtendedPropertyValueEditor  
  2. {  
  3.     public CustomSlider()  
  4.     {  
  5.         // Template for normal view  
  6.         string template1 = @"  
  7.             <DataTemplate  
  8.                 xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'  
  9.                 xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'  
  10.                 xmlns:pe='clr-namespace:System.Activities.Presentation.PropertyEditing;assembly=System.Activities.Presentation'   
  11.                 xmlns:wpg='clr-namespace:PropertyGrid;assembly=PropertyGrid' >   
  12.                 <DockPanel LastChildFill='True'>  
  13.                         <TextBox Text='' Width='40' TextAlignment='Center' />  
  14.                         <Slider x:Name='slider1' Value='' Margin='2,0,0,0' Minimum='' Maximum='' />  
  15.                 </DockPanel>  
  16.             </DataTemplate>";  
  17.   
  18.         // Load templates  
  19.         using( var sr = new MemoryStream( Encoding.UTF8.GetBytes( template1 ) ) )  
  20.         {  
  21.             this.InlineEditorTemplate = XamlReader.Load( sr ) as DataTemplate;  
  22.         }  
  23.     }  
  24. }  
먼저 ExtendedPropertyValueEditor 클래스를 상속 받아 CustomSlider 클래스를 만듭니다. 그 다음 XAML을 통해 슬라이더 컨트롤러 DataTemplate을 구성합니다. 여기서 끝이 아닙니다. 이 컨트롤러와 바인딩할 객체가 있어야합니다. 즉, 슬라이드를 움직이면 그 슬라이더 값이 저장될 곳이 있어야 합니다. 위 코드를 보면 미리 Binding Value.xxx 형태로 바인딩이 적용되어있습니다. 
슬라이더 컨트롤러의 경우 현재값, 최소값, 최대값, 단계값 총 4개의 값이 필요합니다. 이를 위해 따로 슬라이더 컨트롤러를 위한 클래스를 하나 정의해주도록 하겠습니다.
  1. public class SliderValue<T>  
  2. {  
  3.     public SliderValue(T value, T min, T max, T step)  
  4.     {  
  5.         Value = value;  
  6.         Min = min;  
  7.         Max = max;  
  8.         Step = step;  
  9.     }  
  10.   
  11.     public T Value { getset; }  
  12.     public T Max { getset; }  
  13.     public T Min { getset; }  
  14.     public T Step { getset; }  
  15. }  
이제 실제로 슬라이더 컨트롤러를 프로퍼티 그리드에 사용해보도록 하겠습니다.
  1. // 중복 코드 생략  
  2. public class Person  
  3. {  
  4.     private SliderValue<int> m_iAge;  
  5.   
  6.     public Person()  
  7.     {  
  8.         m_iAge = new SliderValue<int>(22, 0, 100, 1);  
  9.     }  
  10.   
  11.     [Category("인적사항")]  
  12.     [DisplayName("나이")]  
  13.     [Editor(typeof(CustomSlider), typeof(PropertyValueEditor))]  
  14.     public SliderValue<int> ageProp  
  15.     {  
  16.         set { m_iAge = value; }  
  17.         get { return m_iAge; }  
  18.     }  
  19. }  


실제 슬라이더 컨트롤러가 적용된 스샷입니다. 슬라이더를 움직이면 0 부터 100까지 값을 조절할수 있고, 직접 값을 입력하면 슬라이더 컨트롤러도 같이 움직이는 것을 확인할 수 있습니다. 단, 직접 입력시에는 0~100까지 제한이라는 예외처리가 안되어있기 때문에 이를 위해서는 추가 작업을 해주어야 합니다.
위의 슬라이더 컨트롤러 XAML을 살펴보면 UpdateSourceTrigger=PropertyChanged 라는 구문이 있습니다. 이는 값이 변경 되었을때 PropertyChanged 이벤트를 날려주기 위함이지요. 이 이벤트를 받는 방법을 구현해보도록 하겠습니다. 

PropertyChanged 이벤트를 받기 위해서는 INotifyPropertyChanged 클래스를 상속 받아야 합니다. 앞서 구현한 SliderValue 클래스를 조금 수정하도록 하겠습니다.

  1. // 중복 코드 생략  
  2. public class SliderValue<T> : INotifyPropertyChanged  
  3. {  
  4.     private T mValue;  
  5.     public T Value  
  6.     {  
  7.         get  
  8.         {   
  9.             return mValue;  
  10.         }  
  11.         set  
  12.         {  
  13.             mValue = value;  
  14.             OnPropertyChanged("Value");  
  15.         }  
  16.     }  
  17.  
  18.     #region INotifyPropertyChanged Members  
  19.     public event PropertyChangedEventHandler PropertyChanged;  
  20.     private void OnPropertyChanged(String info)  
  21.     {  
  22.         if (PropertyChanged != null)  
  23.         {  
  24.             PropertyChanged(thisnew PropertyChangedEventArgs(info));  
  25.         }  
  26.     }  
  27.     #endregion  
  28. }  

이제 슬라이더 컨트롤러를 통해 값이 변경 되면 PropertyChanged 이벤트를 받을수 있게 되었습니다. 슬라이더 컨트롤러를 사용하는 Person 클래스에 슬라이더 컨트롤러의 PropertyChanged 이벤트를 받을수 있는 콜백 함수를 등록 하면 슬라이더 컨트롤러의 값이 변경 되었을 때의 작업을 수행할 수 있습니다.

  1. public class Person  
  2. {  
  3.     public Person()  
  4.     {  
  5.         m_iAge = new SliderValue<int>(22, 0, 100, 1);  
  6.         m_iAge.PropertyChanged += new PropertyChangedEventHandler(SliderPropChanged);  
  7.     }  
  8.   
  9.     // CustomSlider에서 값이 변경 되었다~  
  10.     private void SliderPropChanged(object sender, PropertyChangedEventArgs e)  
  11.     {  
  12.         if (e != null)  
  13.         {  
  14.         }  
  15.     }  
  16. }  
  17.  

 

 

반응형

+ Recent posts