- 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 을 만들 수 있다.



TextBox 에 워터마크를 삽입하는 것은 이 전의 글에서 이야기했었고, 

동일한 방법으로 스타일을 만들어 Key와 TargetType 만 변경하여 PasswordBox에 적용하려 했으나 에러가 발생했다. 


이유는 PasswordBox 에는 'Text' 속성이 없고 이 대신 'Password' 속성이 있지만 이 속성은 의존속성(Dependency Property) 가 아니기 때문에 트리거로 사용할 수 없다. 


다시 TextBox의 예로 돌아와서 Trigger 부분을 살펴보자. 


4번째 줄에 보면 'Text' 속성이 "" 일 때에 대한 조건이 있는데 

해당 TextBox가 focus 되지 않고, 또한 내용이 입력되지 않았을 때 워터마크를 보여주도록 지정한다.


하지만 PasswordBox 에서는 이 'Text' 속성이 없고, 'Password' 속성이 의존속성이 아니기 때문에 다음의 사이트를 참조하여 구현한다. 


http://stackoverflow.com/questions/1607066/wpf-watermark-passwordbox-from-watermark-textbox



방법은 PasswordBoxMonitor 라는 의존객체를 만들어 Password 속성을 모니터링하고, 변경되었을 시 자체적으로 만든 'PasswordLength' 라는 속성을 통해 해당 PasswordBox 에 내용이 있는지, 혹은 없는지를 판단하도록 한다.




입력창, 특히 로그인 창에 회색으로 'ID를 입력하세요' 내지 'example@example.com ' 처럼 예시를 통해

사용자의 입력을 돕는 기능을 흔히 볼 수 있다.


아래는 네이버 로그인 창인데, '아이디' , '비밀번호' 부분을 보면 focus 가 되면 글씨가 사라지고 입력이 없고 focus되지 않으면 나타나 입력을 돕는다.




웹에서는 <input> 태그에서 'placeholder' 속성으로 구현을 하는데, WPF 의 Textbox 에는 이 기능이 없기 때문에 직접 구현해야 한다. 



1. Resource 를 통해 watermark 를 구현

2. 사용하고자 하는 TextBox에 1에서 정의한 Style 을 적용



1.

TextBox 에 스타일을 변경하여 watermark 를 삽입할 'Label' 하나를 추가하는 방법으로 구현한다.

해당 Label 은 Focus되거나 Textbox에 값이 있을 때는 보이지 않도록 Trigger 를 통해 조절한다.


해당 스타일은 별도의 ResourceDictionary 파일로 분리하여 사용했으나 기존에 사용하고 있던 Resource 파일이 있다면 거기에 추가하거나, 혹은 현재 파일 내부에 삽입해도 된다.


코드는 다음과 같으며, stackoverflow 의 게시물 을 참조하였다.






중간에 <Grid> 내부에서 Watermark 를 위한 Label 을 선언해주고, 

아래부분의 MultiTrigger 에서 각각 Focus되지 않았을 때(isFocused 부분) 와 내부에 Text가 없을 때(Text 부분) 를 정의해준다. 


Watermark 로 보여주고자 하는 내용은 해당 Textbox 의 'Tag' 속성과 바인딩 시켰다.


2. 

<TextBox> 에 Style="{StaticResource WatermarkedTextBox}"Tag="아이디" 속성 두 개를 추가




여기까지 제대로 적용했다면 다음과 같이 동작하는 것을 확인할 수 있다.




+ Recent posts