이제 본격적으로 컨트롤을 만들어보자.


프로젝트를 만들고, 새 아이템 추가를 해서 'Custom Control(WPF)'를 선택해서 추가한다.

이름은 일단 RowColumnSelector 라고 지정하였다.


생성하면 RowColumnSelector.cs 파일 하나와, Themes폴더가 생기고 Themes 폴더 안에 Generic.xaml 이라는 파일이 생성되는 것을 확인할 수 있다.

RowColumnSelector.cs 파일에서 해당 컨트롤의 동작을 구현하고, Generic.xaml 파일에서 해당 컨트롤의 뷰를 구현하게 된다.


Generic.xaml 파일을 보면 다음과 같은 코드가 작성되어 있다.



우리가 필요로 하는 건 앞에서 언급했듯이 

1. 팝업을 열기 위한 버튼(토글버튼)

2. 행, 열을 선택하기 위한 팝업 

이므로 각각을 작성해서 넣는다



ToggleButton과 Popup 컨트롤들을 각각 넣고, 

Popup 컨트롤의 IsOpen 속성을 ToggleButton의 IsChecked 속성과 바인딩한다.


확인을 위해 간단히 크기와 배경을 지정하여 다음과 같이 작성하였다.




여기까지 작성된 'RowColumnSelector' 를 MainWindow 에 넣고 실행시켜보면 다음과 같다.





버튼을 클릭하면 팝업이 나타나고, 팝업이 포커스를 잃으면 사라지도록 구현되었다.


이제 팝업에 원하는 기능을 구현하도록 하자.






WPF에 기본으로 제공하지 않는 컨트롤이 필요할 때가 있다. 이 때 기존의 것들을 억지로 조합하는 것보다 새로운 컨트롤을 생성하는 편이 더 쉬울 수 있다.


앞으로 몇 개의 포스팅을 통해 직접 만들어 사용했던 '행열 선택기' 컨트롤을 만드는 과정을 정리하려 한다.


우선 '행열 선택기(RowColumn Selector)' 라고 이름붙인 컨트롤은 Word나 한글에서 표를 삽입할 때 사용하는 컨트롤로, 역시 표를 생성할 때 행의 갯수, 열의 갯수를 선택하고자 할 때 사용하였다.




숫자가 아니라 도형을 통해 직관적으로 표의 모양을 선택할 수 있다는 장점을 갖고 있다.



먼저 이 컨트롤을 만들기 위한 구성요소를 생각해보면, 크게는

1. 팝업을 띄우기 위한 버튼

2. 팝업창 

으로 이루어져 있고, 팝업창 은 다시

2-1. 타이틀바

2-2. 사각형 선택 영역 

로 나누어져 있다.


다음 포스팅에서 간단히 뼈대를 만들어보도록 한다 !



- RequestBringIntoView


ScrollViewer 에는 내부의 객체에 Focus가 되었을 때, 자동으로 해당 객체를 향해 스크롤되도록 하는 기능이 있다. 

이 기능을 확인하기 위해 다음과 같은 구조를 만들어 보자.


<ScrollViewer>

     <StackPanel>

        <Button Height="200" Margin="10" Background="Red">Button 1</Button>

        <Button Height="200" Margin="10" Background="Blue">Button 2</Button>

        <Button Height="200" Margin="10" Background="Green">Button 3</Button>

    </StackPanel>

</ScrollViewer>



실행을 하고, Button2를 눌러보자. 그러면 스크롤뷰어가 Button2를 향해 움직이는 것을 확인할 수 있다.




이 기능을 막는 방법은 다음과 같다.

해당 ScrollViewer 내부의 객체, Focus 될 객체에서 RequestBringIntoView 라는 이벤트를 잡아 e.Handled=true 로 처리해주면 된다.


위의 예제에서는 다음과 같이 처리하면 된다.


xaml :

<Button Height="200" Margin="10" Background="Blue"

                        RequestBringIntoView="Button_RequestBringIntoView">Button 2</Button>


c# :

private void Button_RequestBringIntoView(object sender, RequestBringIntoViewEventArgs e)

{

        e.Handled = true;

}






유저컨트롤을 만들거나, 커스텀 컨트롤을 만들어서 사용할 때, 이벤트를 직접 만들어서 사용하고자 하는 경우가 있다. 직접 Routed Event를 만드는 방법을 알아보자.

Routed Event에 대한 개념은 이전 포스팅에 간단히 포함되어 있다.


방법은 간단하다.

1. 원하는 이벤트를 생성하고
2. 생성한 이벤트를 등록하고
3. 해당 이벤트를 원하는 시점에 발생

시켜주면 된다.

MSDN의 예제 코드를 통해 살펴보자.

public class MyButtonSimple: Button
{
    // Create a custom routed event by first registering a RoutedEventID
    // This event uses the bubbling routing strategy
    public static readonly RoutedEvent TapEvent = EventManager.RegisterRoutedEvent(
        "Tap", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(MyButtonSimple));

    // Provide CLR accessors for the event
    public event RoutedEventHandler Tap
    {
            add { AddHandler(TapEvent, value); } 
            remove { RemoveHandler(TapEvent, value); }
    }

    // This method raises the Tap event
    void RaiseTapEvent()
    {
            RoutedEventArgs newEventArgs = new RoutedEventArgs(MyButtonSimple.TapEvent);
            RaiseEvent(newEventArgs);
    }
    // For demonstration purposes we raise the event when the MyButtonSimple is clicked
    protected override void OnClick()
    {
        RaiseTapEvent();
    }
 
}
public static readonly ~ 부분에서 RegisterRoutedEvent 메서드를 통해 RoutedEvent를 등록한다. 이벤트의 이름은 Tap 이고 라우팅전략은 Bubble 이다. 이 후의 두 인수는 RoutedEventHandler, 이벤트를 소유하는 클래스의 타입이다.


다음 부분에서 이벤트를 생성해주고, 

RaiseTapEvent()라는 함수를 만들어 원하는 시점에 이 함수를 호출하여 이벤트를 발생시키도록 한다.

이벤트를 만들었다면 해당 메서드에서 MyButtonSimple.TapEvent 부분만 수정하면 된다.


원하는 시점에 RaiseTapEvent() 함수를 호출하면 되는데, 위의 예제에서는 버튼이 클릭될 때 호출되도록 하였다.


위와 동일한 과정을 통해 이벤트를 만들고, 발생시켜 사용할 수 있다.




WPF 에서 중요한 개념 중 하나는 Routed Event 이다.


간단히 버튼을 클릭했을 때 발생하는 Click 이벤트, 혹은 패널을 클릭했을 때 발생하는 MouseDown, MouseUp 등의 것들이 Routed Event 에 포함된다.

하지만 꼭 엘리먼트에서만 발생하는 것은 아니고 코드 상에서 임의로 발생시킬 수도 있기 때문에, MSDN의 정의를 참조하도록 하자.


. 기능 측면의 정의 : Routed Event 는 이벤트를 발생시킨 특정 개체 뿐 아니라 엘리먼트 트리의 여러 리스너(listener)의 핸들러(handler)를 호출할 수 있는 이벤트 형식이다.

. 구현 측면의 정의 : Routed Event 는 RoutedEvent 클래스 객체의 지원을 받으며, WPF의 이벤트 시스템에 의해 처리된다.


WPF에는 Routed Event 를 두가지로 분류할 수 있다. 이는 버블링(Bubbling) 과 터널링(Tunneling) 이다.

버블링 이벤트는 발생한 이벤트 소스의 이벤트 처리기가 호출되고, 이후 트리 루트에 도달할 때까지 부모 엘리먼트로 라우팅하며, 일반적으로 사용된다.

반면 터널링 이벤트는 엘리먼트의 트리 루트(root)의 이벤트 처리기를 호출하고 자식 엘리먼트로 라우팅하며 이벤트 소스 엘리먼트까지 전달된다.

일반적으로 Tunneling 이벤트의 경우 접두사로 Preview가 붙고, PreviewMouseDown, PreviewDragDown 등으로 쓰여져 있으면 이를 Tunneling 이벤트라고 이해하면 된다.



- 상기 그림은 MSDN에서 퍼옴


위의 그림에서 element#2에서 이벤트가 발생했다고 하면 버블링, 터널링 이벤트가 발생하는 순서는 다음과 같다.


1. PreviewMouseDown (tunnel) on root element.

2. PreviewMouseDown (tunnel) on intermediate element #1.

3. PreviewMouseDown (tunnel) on source element #2.

4. MouseDown (bubble) on source element #2.

5. MouseDown (bubble) on intermediate element #1.

6. MouseDown (bubble) on root element.



* 이 때 더이상 이벤트가 라우팅 되지 않도록 하는 방법이 있다.

모든 라우팅된 이벤트는 공통 이벤트 데이터의 기본 클래스인 RoutedEventArgs를 공유한다. 이 클래스에는 Handled라는 속성이 존재하는데, 이 속성은 기본으로 false로 되어 있다. 한 이벤트 처리기에서 이 속성, 즉 RoutedEventArgs.Handled 를 true라고 설정하면 이 이벤트는 처리된 것으로 되어 더이상 라우팅 되지 않는다.


즉 위의 그림의 구조에서 3번 이벤트에 대한 이벤트 핸들러에서 e.Handled = true; 라는 문장을 써주면 4, 5, 6번의 이벤트는 라우팅되지 않는다.



WPF에는 Layout TransformRender Transform 의 두 가지 트랜스폼이 존재한다. 각각을 적용했을 때 다른 결과가 나타나서 둘의 차이가 뭔지 궁금해서 찾아본 결과 명료하게 정리된 포스팅이 있어 다시 정리해보려고 한다.


아래의 내용은 다음의 두 블로그를 참조했다.

http://www.scottlogic.co.uk/blog/colin/2008/12/layouttransform-vs-rendertransform-whats-the-difference/

http://www.vbdotnetheaven.com/uploadfile/7b0949/rendertransform-and-layouttransform-in-wpf/



WPF에서 레이아웃을 렌더링할 때는 다음의 순서를 거친다.

  • Measure : 각 엘리먼트의 DesiredSize 를 연산
  • Arrange : child 엘리먼트들의 위치를 부모 엘리먼트 기준으로 연산
  • Render  : 연산된 ui(user interface)를 화면에 렌더링


두 트랜스폼의 차이점은 트랜스폼이 이루어지는 시점인데, 각 트랜스폼은 다음의 시점에 이루어진다.


  • LayoutTransform
  • Measure
  • Arrange
  • RenderTransform
  • Render


따라서 LayoutTransform의 결과는 Measure, Arrange 의 연산 결과에 반영되지만 RenderTransform의 결과는 이에 반영되지 않고 렌더링 과정에만 반영된다. 따라서 퍼포먼스는 RenderTransform의 경우가 LayoutTransform의 결과보다 좋다.


차이를 확인하기 위해 간단한 프로젝트를 만들어서 테스트해보았다.


StackPanel 에 버튼을 3개씩 만들고, 두번째 버튼에 RotateTransform 을 각각 LayoutTransform, RenderTransform으로 적용한 결과이다.




결론은, 가능하다면 RenderTransform을 사용하는 것이 좋고(더 빠르기 때문), 사용이 불가능하다면 LayoutTransform을 사용하면 되겠다.



이미지, 혹은 엘리먼트를 확대/축소, 회전 시키는 것은 Transform을 통해 쉽게 구현할 수 있다.


WPF에는 여러 개의 Transform 들을 제공하는데, 확대/축소의 경우 ScaleTransform, 회전의 경우 RotateTransform 을 사용하여 구현한다.


간단하게, 이미지를 확대 축소, 회전 시키는 프로젝트를 만들어보자. 


1. 확대 축소 시 전체 이미지를 볼 수 있도록 ScrollViewer 를 하나 만들고,

2. 이미지를 하나 삽입한다.

3. 확대, 축소, 회전을 위해 버튼을 3개 만들어보자



자 그럼 준비는 됐으니 Transform을 적용해보자.

우리가 Transform을 적용하고자 하는 엘리먼트는 <Image> 이므로, 하위에 LayoutTransform 에

TransformGroup 을 하나 만들고, 그 하위에 ScaleTransform 과 RotateTransform 을 각각 생성한다. C#측에서 변경해야 하기 때문에 각각의 트랜스폼에 적당히 이름을 달아주도록 한다.

해당 부분 코드는 아래와 같다.


자, 그럼 모든 준비는 끝났다. 버튼을 눌렀을 때 확대/축소의 경우엔 ScaleTransform 의 ScaleX, ScaleY 속성을, 회전의 경우엔 RotateTransform 의 Angle 속성을 변경하면 된다.

함수는 간단하게 다음과 같이 작성하면 된다.



여기까지 작성하고 실행해보면 아래와 같이 제대로 동작함을 알 수 있다.





WPF 의 컨트롤들 중 ContentControl 이라는 것이 있다. Control 중에 'Content' 속성을 지닌 컨트롤을 의미한다. Button 이나 Label, Frame과 같은 컨트롤들이 'ContentControl' 에 속한다.


상황에 따라 이 ContentControl 을 커스텀화 해서 사용하고 싶은 경우가 있다. 가령 Panel 처럼 생겼지만 우측 상단에 접었다 폈다 할 수 있는 기능을 가지는 버튼을 일괄적으로 넣고 싶다거나, border, label 등으로 일괄적으로 변경을 하려고 하는 등의 경우이다. 

물론 코드상에서 적용하고자 하는 부분에 모두 border, button, label 등을 추가할 수 있지만, 이를 템플릿화하여 ContentControl 로 만들어놓는다면 재사용성을 높일 수 있을 것이다.




내가 구현하고자 했던 컨트롤은 상단에 두 개의 버튼을 갖는 ContentControl 이다. 이 컨트롤 내부에 어떤 내용이 들어갈 지는 모르지만 공통된 기능을 위해 두 개의 버튼을 달았고, 이 컨트롤은 템플릿처럼 여러 부분에서 일괄적으로 사용하려고 한다.

컨트롤을 생성한 후 사용은 Button 이나 Label 에서와 동일하다. 즉,

<local:MyControl>

<StackPanel>

<Canvas />

<Button />

</StackPanel>

</local:MyControl>

다음과 같이 사용할 수 있다.



1. 먼저 '아이템 추가' 에서 WPF > Custom Control (WPF) 를 추가한다.




 

하면 다음과 같은 파일이 생기며, 

프로젝트 부분을 잘 보면 'Generic.xaml' 파일이 생성(되거나 이미 있다면 해당 Custom ContentControl 부분이 추가) 되어 있는 것을 확인할 수 있다.



2. ContentControl 클래스를 상속받도록 한다.

  - 기존에는 MyCustomClass : Control 과 같이 선언되어 있을 것이다.

  - 이를 MyCustomClass : ContentControl 로 변경한다.


3. 'Generic.xaml' 코드에 알맞게 작성한다.

  - 처음 생성했을 때 Generic.xaml 코드 상에는 'Content' 속성에 해당하는 부분이 들어갈 부분이 없다. 따라서 Content 를 넣을 부분에 'ContentControl' 컨트롤을 삽입하고,

  - 그 외 부분을 원하는대로 적절하게 작성하면 된다.

  - ContentControl 을 상속받았기 때문에 바인딩 시켜줘야할 변수들이 제법 많다 (Background, Alignment, Margin 등등). 하여 간단히 작성한 코드를 첨부한다.






이를 응용하면 자유자재로 원하는 형태의 ContentControl 을 만들 수 있다.


+ Recent posts