WPF의 이벤트 시스템은 라우트 이벤트(Routed Events)라는 개념을 사용하여 UI 요소 간의 이벤트 흐름을 관리한다.
라우트 이벤트는 이벤트가 특정 요소에서 발생한 후, WPF의 논리 트리(Logical Tree) 또는 비주얼 트리(Visual Tree)를 따라 전달되는 이벤트이다. 일반적인 이벤트와 달리, 라우트 이벤트는 트리를 따라 이동하며 여러 UI 요소에 의해 처리될 수 있다.
버블링 이벤트는 이벤트가 가장 안쪽의 자식 요소에서 발생한 뒤, 부모 요소로 전달되는 방식이다. 이 방식은 이벤트가 트리의 아래에서 위로 "거품이 올라가듯" 전달된다고 해서 버블링이라고 명명한다.
버블링 이벤트의 대표적인 예는 Click 이벤트이다다. 사용자가 버튼을 클릭하면 버튼 요소에서 이벤트가 발생하고, 이 이벤트는 버튼의 부모 요소(예: 패널, 윈도우)로 전달된다.
e.Handled = true라면 멈춤터널링 이벤트는 부모 요소에서 시작해 자식 요소로 전달되는 방식이다. 이는 이벤트가 트리의 위에서 아래로 전달되므로 터널링이라고 명명한다.
터널링 이벤트는 "Preview" 접두사가 붙은 이벤트로 사용된다다. 예를 들어, PreviewMouseDown 이벤트는 터널링 이벤트이고, MouseDown 이벤트는 버블링 이벤트이다.
PreviewKeyDown 이벤트는 터널링 이벤트의 대표적인 예이다다. 키보드 입력이 발생하면 윈도우(루트 요소)에서 시작하여 해당 입력이 전달될 자식 요소로 이벤트가 전파된다.
e.Handled = true라면 멈춤| 특징 | 터널링(Tunneling) | 버블링(Bubbling) |
|---|---|---|
| 이벤트 전달 방향 | 부모 요소 → 자식 요소 | 자식 요소 → 부모 요소 |
| 이벤트 이름 접두사 | Preview | (없음) |
| 처리 우선순위 | 부모가 자식보다 먼저 이벤트를 처리 | 자식이 부모보다 먼저 이벤트를 처리 |
| 주요 사용 사례 | 입력 이벤트 사전 처리 | 사용자 상호작용 처리 |
Handled 속성 활용
라우트 이벤트가 처리되었음을 명시하기 위해 e.Handled = true를 설정할 수 있다. 이렇게 하면 이벤트가 더 이상 트리를 따라 전파되지 않는다.
터널링과 버블링의 조합
터널링 이벤트는 사전 검사를 위해 사용하고, 버블링 이벤트는 최종 처리를 위해 사용하는 방식으로 조합할 수 있다.
좌측은 버블링, 우측은 터널링이다.

<Window x:Class="WPF01.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WPF01"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="1*"/>
<RowDefinition Height="50"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<Button Grid.Row="0" Grid.Column="0"
Background="Red" Margin="35,15,35,15"
Click ="Button_Click">
<Border Background="Blue" Width="250" Height="250"
MouseDown="Border_MouseDown">
<Rectangle Fill="Green" Width="150" Height="150"
MouseDown="Rectangle_MouseDown"/>
</Border>
</Button>
<Button Grid.Row="0" Grid.Column="1"
Background="Red" Margin="35,15,35,15"
PreviewMouseDown="Button_PreviewMouseDown">
<Border Background="Blue" Width="250" Height="250"
PreviewMouseDown="Border_PreviewMouseDown">
<Rectangle Fill="Green" Width="150" Height="150"
PreviewMouseDown="Rectangle_PreviewMouseDown"/>
</Border>
</Button>
<TextBlock x:Name="tbBubbling" Grid.Row="1" Grid.Column="0"
FontSize="20" Text="버블링 순서도"
VerticalAlignment="Center"
HorizontalAlignment="Center"/>
<TextBlock x:Name="tbTunneling" Grid.Row="1" Grid.Column="1"
FontSize="20" Text="터널링 순서도"
VerticalAlignment="Center"
HorizontalAlignment="Center"/>
</Grid>
</Window>
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
namespace WPF01
{
public partial class MainWindow : Window
{
List<string> list = new List<string>();
public MainWindow()
{
InitializeComponent();
}
private void SetBubblingTextBox()
{
tbBubbling.Text = string.Join(">", list);
AsyncClearList();
}
private void SetTunnelingTextBox()
{
tbTunneling.Text = string.Join(">", list);
AsyncClearList();
}
private async void AsyncClearList()
{
await Task.Delay(100);
list.Clear();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
list.Add("Red Button");
SetBubblingTextBox();
}
private void Border_MouseDown(object sender, MouseButtonEventArgs e)
{
// 더 이상 이벤트를 전달하지 않음
// e.Handled = true;
list.Add("Blue Border");
SetBubblingTextBox();
}
private void Rectangle_MouseDown(object sender, MouseButtonEventArgs e)
{
list.Add("Green Rectangle");
SetBubblingTextBox();
}
private void Button_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
list.Add("Red Button");
SetTunnelingTextBox();
}
private void Border_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
// 더 이상 이벤트를 전달하지 않음
// e.Handled = true;
list.Add("Blue Border");
SetTunnelingTextBox();
}
private void Rectangle_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
list.Add("Green Rectangle");
SetTunnelingTextBox();
}
}
}