라우팅 이벤트 개념
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를 통해 내부 등록 메커니즘에 연결된다.
이벤트 공유 메커니즘
여러 클래스가 동일한 이벤트를 재사용할 수 있다. UIElement와 ContentElement가 공통으로 활용하는 포인터 이벤트가 대표적이다:
// UIElement에서 Mouse 포인터 이벤트 재사용
UIElement.PointerPressedEvent = PointerEvent.PressedEvent.AddOwner(typeof(UIElement));
이벤트 발생과 전달
라우팅 이벤트는 전통적인 .NET 이벤트 호출 대신 RaiseEvent 메서드로 발생시킨다:
var args = new RoutedEventArgs(CustomButtonBase.PressEvent, this);
RaiseEvent(args);
모든 WPF 이벤트는 일관된 서명을 따른다. 첫 번째 매개변수는 sender, 두 번째는 RoutedEventArgs를 상속한 이벤트 인자이다. 예를 들어 PointerPressed는 PointerEventArgs를 제공하여 눌린 버튼 정보를 전달한다.
이벤트 연결 방식
선언적 연결 (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 | 처리 중인 라우팅 이벤트 식별자. 공통 핸들러에서 이벤트 구분에 활용 |
| Handled | true 설정 시 라우팅 중단. 후속 요소는 이벤트 수신 불가 |
버블링 이벤트 실증
다음 마크업은 중첩된 요소에서의 이벤트 전파를 시각화한다:
<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 | 닫기 완료 후, 객체는 여전히 유효 |
키보드 입력 처리
키보드 이벤트 순서
- PreviewKeyDown (터널)
- KeyDown (버블)
- PreviewTextInput (터널)
- TextInput (버블)
- PreviewKeyUp (터널)
- 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 | 요소가 포커스를 받을 수 있는지 결정 |
| IsTabStop | Tab 순환에서 제외하되 다른 방식으로 포커스 가능 |
| TabIndex | Tab 순서 지정, 기본값 Int32.MaxValue |
비가시(Visibility.Collapsed) 또는 비활성(IsEnabled = false) 상태인 요소는 포커스 순서에 포함되지 않는다.