WPF 라우팅 이벤트의 이해와 활용

라우팅 이벤트 개념

WPF의 라우팅 이벤트는 시각적 트리를 따라 이동하며 다양한 요소에서 처리될 수 있는 메커니즘이다. 이 이벤트는 실제 발생한 요소와 다른 요소에서도 처리 가능하며, 의존 속성과 마찬가로 표준 이벤트 핸들러 방식으로 사용할 수 있지만 내부 동작 원리를 이해해야 고급 기능을 활용할 수 있다.

이벤트 등록 및 정의 패턴

라우팅 이벤트는 읽기 전용 정적 필드로 선언되며, 정적 생성자에서 등록되고 표준 .NET 이벤트 문법으로 감싸진다. 다음은 커스텀 컨트롤에서의 구현 예시이다:

public abstract class CustomButtonBase : ContentControl
{
    public static readonly RoutedEvent PressEvent;

    static CustomButtonBase()
    {
        PressEvent = EventManager.RegisterRoutedEvent(
            "Press", 
            RoutingStrategy.Bubble,
            typeof(RoutedEventHandler), 
            typeof(CustomButtonBase));
    }

    public event RoutedEventHandler Press
    {
        add => AddHandler(PressEvent, value);
        remove => RemoveHandler(PressEvent, value);
    }
}

EventManager.RegisterRoutedEvent 메서드는 이벤트 식별자, 라우팅 전략, 대리자 타입, 소유 클래스 타입을 인자로 받는다. 이벤트 래퍼는 AddHandler/RemoveHandler를 통해 내부 등록 메커니즘에 연결된다.

이벤트 공유 메커니즘

여러 클래스가 동일한 이벤트를 재사용할 수 있다. UIElementContentElement가 공통으로 활용하는 포인터 이벤트가 대표적이다:

// UIElement에서 Mouse 포인터 이벤트 재사용
UIElement.PointerPressedEvent = PointerEvent.PressedEvent.AddOwner(typeof(UIElement));

이벤트 발생과 전달

라우팅 이벤트는 전통적인 .NET 이벤트 호출 대신 RaiseEvent 메서드로 발생시킨다:

var args = new RoutedEventArgs(CustomButtonBase.PressEvent, this);
RaiseEvent(args);

모든 WPF 이벤트는 일관된 서명을 따른다. 첫 번째 매개변수는 sender, 두 번째는 RoutedEventArgs를 상속한 이벤트 인자이다. 예를 들어 PointerPressedPointerEventArgs를 제공하여 눌린 버튼 정보를 전달한다.

이벤트 연결 방식

선언적 연결 (XAML)

<Image Source="photo.png" Stretch="None" 
       Name="thumbnail" PointerPressed="Thumbnail_Pressed"/>

프로그래밍적 연결

// 간편 구문
thumbnail.PointerPressed += Thumbnail_Pressed;

// 명시적 구문
thumbnail.PointerPressed += new PointerEventHandler(Thumbnail_Pressed);

// 저수준 API 활용
thumbnail.AddHandler(Image.PointerPressedEvent, 
    new PointerEventHandler(Thumbnail_Pressed));

// 정의 클래스 기준 참조
thumbnail.AddHandler(UIElement.PointerPressedEvent,
    new PointerEventHandler(Thumbnail_Pressed));

이벤트 분리

thumbnail.PointerPressed -= Thumbnail_Pressed;

// 또는
thumbnail.RemoveHandler(Image.PointerPressedEvent,
    new PointerEventHandler(Thumbnail_Pressed));

라우팅 전략 유형

전략동작 방식
직접(Direct)발생 요소에서만 처리, 전파 없음
버블링(Bubble)발생 지점에서 시작해 상위 노드로 전파
터널링(Tunnel)루트에서 시작해 발생 지점까지 하향 전파

RoutedEventArgs 핵심 속성

속성설명
Source이벤트를 발생시킨 논리적 객체. 키보드 이벤트에서는 포커스 요소, 포인터 이벤트에서는 커서 하위 최상위 요소
OriginalSource실제 이벤트 발생 원점. 시각적 트리의 더 깊은 계층일 수 있음
RoutedEvent처리 중인 라우팅 이벤트 식별자. 공통 핸들러에서 이벤트 구분에 활용
Handledtrue 설정 시 라우팅 중단. 후속 요소는 이벤트 수신 불가

버블링 이벤트 실증

다음 마크업은 중첩된 요소에서의 이벤트 전파를 시각화한다:

<Window x:Class="RoutingDemo.BubbleView"
        PointerReleased="OnPointerReleased">
  <Grid Margin="10" PointerReleased="OnPointerReleased">
    <Border Background="LightBlue" PointerReleased="OnPointerReleased">
      <StackPanel PointerReleased="OnPointerReleased">
        <TextBlock Text="상위 텍스트" PointerReleased="OnPointerReleased"/>
        <Image Source="sample.jpg" Stretch="None" 
               PointerReleased="OnPointerReleased"/>
        <TextBlock Text="하위 텍스트" PointerReleased="OnPointerReleased"/>
      </StackPanel>
    </Border>
  </Grid>
</Window>

공통 핸들러 구현:

private int _sequence = 0;

private void OnPointerReleased(object sender, PointerEventArgs e)
{
    _sequence++;
    var entry = $"#{_sequence}: Sender={sender.GetType().Name}, " +
                $"Source={e.Source}, OriginalSource={e.OriginalSource}";
    messageLog.Items.Add(entry);
    
    e.Handled = suppressCheckBox.IsChecked ?? false;
}

체크박스 선택 시 Handled = true로 설정되어 첫 번째 처리 후 전파가 중단된다.

억제된 이벤트 강제 수신

일부 컨트롤은 이트를 소비하여 외부 전파를 차단한다. AddHandler의 세 번째 매개변수로 이러한 억제를 우회할 수 있다:

targetElement.AddHandler(
    UIElement.PointerReleasedEvent,
    new PointerEventHandler(OnForcedCapture),
    handledEventsToo: true);

이 방식은 설계상 신중히 적용해야 하며, 일반적인 상황에서는 권장되지 않는다.

부착 이벤트 패턴

컨테이너가 자식 요소의 이벤트를 일괄 처리해야 할 때 부착 이벤트를 활용한다:

<StackPanel ButtonBase.Click="ExecuteCommand">
  <Button Content="저장" />
  <Button Content="열기" />
  <Button Content="닫기" />
</StackPanel>

코드 비하인드에서의 등록:

buttonContainer.AddHandler(
    ButtonBase.ClickEvent, 
    new RoutedEventHandler(ExecuteCommand));

발생 원점 식별 방법:

private void ExecuteCommand(object sender, RoutedEventArgs e)
{
    // 소스 객체 직접 비교
    if (e.Source == saveBtn) { /* 저장 처리 */ }
    else if (e.Source == openBtn) { /* 열기 처리 */ }
    
    // 또는 Tag 속성 활용
    var identifier = ((FrameworkElement)e.Source).Tag as string;
}

터널링 이벤트

터링 이벤트는 Preview 접두사로 식별되며, 대응하는 버블링 이벤트보다 먼저 발생한다. 동일한 RoutedEventArgs 인스턴스를 공유하므로 터널링 단계에서 Handled를 설정하면 버블링 이벤트가 완전히 억제된다.

주요 활용은 입력 전처리 및 필터링이다. 예를 들어 특정 키 조합이나 포인터 동작을 선제적으로 차단할 수 있다.

이벤트 범주 체계

수명 주기 이벤트

이벤트특성설명
Initialized직접속성 초기화 완료, 스타일/바인딩 미적용 상태
Loaded직접시각화 트리 구성 완료, 렌더링 직전
Unloaded직접요소 분리 또는 컨테이너 종료 시

초기화는 하위에서 상위로, 로드는 상위에서 하위로 진행된다. 커스텀 초기화 로직은 Loaded 이벤트 또는 InitializeComponent 이후에 배치하는 것이 안전하다.

창 전용 수명 이트

이벤트발생 시점
SourceInitialized네이티브 핸들 획득 후, 표시 전
ContentRendered초기 렌더링 완료 후
Activated창이 전경으로 전환될 때
Deactivated창이 배경으로 전환될 때
Closing닫기 요청 시, 취소 가능
Closed닫기 완료 후, 객체는 여전히 유효

키보드 입력 처리

키보드 이벤트 순서

  1. PreviewKeyDown (터널)
  2. KeyDown (버블)
  3. PreviewTextInput (터널)
  4. TextInput (버블)
  5. PreviewKeyUp (터널)
  6. KeyUp (버블)

텍스트 입력 관련 이벤트는 문자 생성 시에만 발생하며, 수정자 키나 기능 키는 제외된다.

입력 검증 예시

private void OnPreviewTextInput(object sender, TextCompositionEventArgs e)
{
    if (!short.TryParse(e.Text, out _))
    {
        e.Handled = true; // 숫자 외 입력 차단
    }
}

private void OnPreviewKeyDown(object sender, KeyEventArgs e)
{
    if (e.Key == Key.Space)
    {
        e.Handled = true; // 공백키 별도 차단
    }
}

키 상태 조회

// 이벤트 인자를 통한 조회
if ((e.KeyboardDevice.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
{
    statusText.Text = "Ctrl 키 림";
}

// 정적 메서드를 통한 전역 조회
if (Keyboard.IsKeyDown(Key.LeftShift))
{
    statusText.Text = "좌측 Shift 눌림";
}

포인터 입력 처리

기본 포인터 이벤트

이벤트유형설명
PointerEntered직접커서가 요소 경계 내로 진입
PointerExited직접커서가 요소 경계 밖으로 이탈
PreviewPointerMoved터널커서가 요소 위에서 이동 중
PointerMoved버블커서가 요소 위에서 이동 중

포인터 위치 획득

private void TrackPosition(object sender, PointerEventArgs e)
{
    Point location = e.GetPosition(this);
    coordDisplay.Text = $"({location.X:F1}, {location.Y:F1})";
}

버튼 클릭 이벤트 계열

이벤트유형
PreviewPointerLeftButtonDown/PreviewPointerRightButtonDown터널
PointerLeftButtonDown/PointerRightButtonDown버블
PreviewPointerLeftButtonUp/PreviewPointerRightButtonUp터널
PointerLeftButtonUp/PointerRightButtonUp버블

PointerEventArgs는 클릭 횟수(ClickCount)를 포함하여 단일/이중 클릭을 구분할 수 있다.

포커스 관리

속성영향
Focusable요소가 포커스를 받을 수 있는지 결정
IsTabStopTab 순환에서 제외하되 다른 방식으로 포커스 가능
TabIndexTab 순서 지정, 기본값 Int32.MaxValue

비가시(Visibility.Collapsed) 또는 비활성(IsEnabled = false) 상태인 요소는 포커스 순서에 포함되지 않는다.

태그: WPF RoutedEvent BubbleEvent TunnelEvent AttachedEvent

7월 22일 11:07에 게시됨