KeyDown Event

// MainWindow.xaml
<Window
    x:Class="WpfApp3.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:local="clr-namespace:WpfApp3"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    KeyDown="Window_KeyDown"
    Width="800"
    Height="450"
    mc:Ignorable="d">
    <StackPanel>
        <TextBox />
    </StackPanel>
</Window>
// MainWindow.xaml.cs
using System.Diagnostics;
using System.Windows;
using System.Windows.Input;

namespace WpfApp3;
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void Window_KeyDown(object sender, KeyEventArgs e)
    {
        Debug.WriteLine("");
    }
}

MainWindow에 TextBox가 있습니다.

여기에 포커스를 주고 키 입력을 했을때

당연히 TextBox에 키 입력이 먼저 될 것으로 생각 했는데 (버블링)

이상하게 Window가 먼저 KeyDown을 받고 있습니다.

제가 뭘 잘 못 생각 한 걸까요?

1개의 좋아요

보여 주신 코드로 이벤트 처리 순서를 알 수 있나요?

TextBox 도 KeyDown 이벤트 핸들러를 등록해야 순서를 확인할 수 있을 것 같은데요.

2개의 좋아요

네 예측할 수는 있습니다.

터널링에 의해서 Preview가 붙은 이벤트들이 먼저 발생한 뒤 Command가 발생하고 버블링에 의해서 Preview라는 접두사가 안붙은 이벤트들이 실행되는게 WPF의 일반적인 이벤트 호출 흐름입니다.

TextBox에 포커싱이 되어있다면 상위 컨트롤인 Window, Stackpanel의 Preview Keydown을 거쳐서 실행된 뒤, TextBox의 Command가 실행되고 (바인딩된게 있으면), 이후 TextBox를 시작으로 StackPanel, Window 까지 버블링으로 실행되기 때문에

제 생각도 code님의 의견처럼 TextBox에 먼저 Text가 입력된 후에 버블링에 의해 Window에 등록된 Keydown이 실행되어야 맞는 것 같은데…

그래서 저도 알아보는 중이네요.

1개의 좋아요

우선 이 글을 참고했습니다.

보면 이런 말이 있는데

키보드 입력의 경우 WPF는 먼저 적절한 KeyDown/KeyUp 이벤트를 보냅니다. 해당 이벤트가 처리되지 않았으며 키가 방향 화살표 키 또는 기능 키와 같은 제어 키가 아니라 텍스트 키이면 TextInput 이벤트가 발생합니다. 여러 키스트로크로 단일 텍스트 입력 문자를 생성할 수 있고 단일 키스트로크로 다중 문자 문자열을 생성할 수 있으므로 KeyDown/KeyUpTextInput 이벤트 간에 항상 단순히 일 대 일 매핑이 구성되는 것은 아닙니다. 이는 특히 각 언어의 알파벳으로 수천 개의 가능한 문자를 생성하는 데 IME(입력기)를 사용하는 한국어, 중국어 및 일본어 등의 언어에서 더욱 그렇습니다.

WPF가 KeyUp/KeyDown 이벤트를 보내면 키스트로크가 TextInput 이벤트의 일부가 될 수 있는 경우(예를 들어 Alt+S를 누르는 경우)KeyKey.System으로 설정됩니다. 이를 통해 KeyDown 이벤트 처리기의 코드는 Key.System을 확인하고, 찾은 경우 이후에 발생한 TextInput 이벤트에 대한 처리기가 처리되도록 둘 수 있습니다. 이러한 경우 TextCompositionEventArgs 인수의 다양한 속성을 사용하여 원래 키스트로크를 확인할 수 있습니다. 마찬가지로 IME가 활성 상태인 경우 KeyKey.ImeProcessed 값을 가지며 ImeProcessedKey가 원래 키스트로크를 제공합니다.

무슨 말인지 감이 잘 안와닿아서 @BigSquare 님 말씀처럼 다쳐봤습니다.

<Window x:Class="WpfApp1.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:local="clr-namespace:WpfApp1"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        Title="MainWindow"
        Width="800"
        Height="450"
        KeyDown="Window_KeyDown"
        PreviewKeyDown="Window_PreviewKeyDown"
        mc:Ignorable="d">
    <StackPanel KeyDown="StackPanel_KeyDown"
                PreviewKeyDown="StackPanel_PreviewKeyDown">
        <TextBox KeyDown="TextBox_KeyDown"
                 PreviewKeyDown="TextBox_PreviewKeyDown"
                 PreviewTextInput="TextBox_PreviewTextInput"
                 TextInput="TextBox_TextInput" />
    </StackPanel>
</Window>
using System.Diagnostics;
using System.Windows;
using System.Windows.Input;

namespace WpfApp1;

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void Window_KeyDown(object sender, KeyEventArgs e)
    {
        Debug.WriteLine("Window_KeyDown");
    }

    private void StackPanel_KeyDown(object sender, KeyEventArgs e)
    {
        Debug.WriteLine("StackPanel_KeyDown");
    }

    private void TextBox_TextInput(object sender, TextCompositionEventArgs e)
    {
        Debug.WriteLine("TextBox_TextInput");
    }

    private void TextBox_KeyDown(object sender, KeyEventArgs e)
    {
        Debug.WriteLine("TextBox_KeyDown");
    }

    private void StackPanel_PreviewKeyDown(object sender, KeyEventArgs e)
    {
        Debug.WriteLine("StackPanel_PreviewKeyDown");
    }

    private void TextBox_PreviewKeyDown(object sender, KeyEventArgs e)
    {
        Debug.WriteLine("TextBox_PreviewKeyDown");
    }

    private void TextBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
    {
        Debug.WriteLine("TextBox_PreviewTextInput");
    }

    private void Window_PreviewKeyDown(object sender, KeyEventArgs e)
    {
        Debug.WriteLine("Window_PreviewKeyDown");
    }
}

그랬더니 아래처럼 나오네요.

  1. Window_PreviewKeyDown
  2. StackPanel_PreviewKeyDown
  3. TextBox_PreviewKeyDown
  4. TextBox_KeyDown
  5. StackPanel_KeyDown
  6. Window_KeyDown
  7. TextBox_PreviewTextInput

결과를 보니 Focus된 TextBox에 KeyDown이 터널링과 버블링이 모두 호출된 다음, 실제로 TextBox의 Text를 변경해주는 TextInput 메서드가 호출이 되고 있네요.
TextInput과 PreivewTextInput을 둘다 등록해서 PreviewTextInput에서 처리가 되어, TextInput이 발생하지 않는 것 같습니다.
사용자가 PreviewTextInput을 등록하지 않았더라도 WPF FCL 자체에 기본 구현된 PreviewTextInput에서 처리가 되었을 꺼라고 추측…중입니다. 어떤 건지 모르겠는데 엄청 많이있네요

결론은 KeyDown이 터널링으로 타고 들어가서, 키보드 매핑에서 적절한 키입력이 발견되면 우선 버블링 KeyDown 까지 다 처리하고 나서 TextInput에 의해 키가 입력되고 렌더링이 되는 것 같네요.

그게 위 문서의 아래 말 같습니다.

입력 이벤트는 이벤트 경로의 위쪽으로 버블링하므로 StackPanel은 어떤 요소에 키보드 포커스가 있는지에 관계없이 입력을 받습니다. TextBox 컨트롤이 먼저 알림을 받으며 OnTextInputKeyDown 처리기는 TextBox가 입력을 처리하지 않은 경우에만 호출됩니다. PreviewKeyDown 이벤트가 KeyDown 이벤트 대신 사용되면 OnTextInputKeyDown 처리기가 먼저 호출됩니다.

2개의 좋아요

링크의 글과 이 질문은 약간 핀트가 다릅니다.

버블링과 터널링은 이벤트 핸들러 호출 체인에 관한 것이고, 링크의 글은 event spawn - 이벤트가 다른 이벤트를 raise 하는 경우를 설명하는 것입니다.

즉 전자는 하나의 이벤트 소스가 어떻게 복수의 핸들러를 호출하는 지에 관한 것이고, 후자는 특정 이벤트 소스 사이의 상관 관계를 설명하는 것입니다.

TextBox.KeyDown 이벤트는 한 번만 일어납니다.

이 이벤트에 직접 등록한 핸들러 뿐만 아니라, 이 요소의 부모 요소로 거슬로 올라가 동일한 핸들러를 연속해서 호출하는 것이 버블링입니다.

WPF 뿐만 아니라, Html 의 이벤트에도 동일한 개념이 있습니다.

WPF는 특이하게 Preview 이벤트 핸들러를 제공합니다.
렌더링 트리의 최 상단으로부터 이벤트 소스 방향으로 이벤트 핸들러가 호출됩니다.(터널링)

그래서, 프리뷰 핸들러가 먼저 호출되고, 그 다음 이벤트 핸들러가 호출됩니다.

2개의 좋아요

우선 위 처리의 목적부터 말씀 드리면

저희 프로그램은 내부적으로 단축키 같은 개념이 있습니다. (옵션키를 안쓰고 그냥 A, S, D 등등)

그래서 설정 된 단축키를 누르면 메인윈도우에서 받아 처리 하고 Handled = true 처리 합니다.

그리고 창에 DataGrid가 있는데 여기의 TextColumn 에 값을 아무리 입력 하려고 해도

단축키가 먼저 먹었습니다.

제가 착각 한것은 TextBox의 입력 처리가 KeyDown 인줄 알았으나

위에 @Vincent 님이 말씀 하신것 처럼 TextBox 는 KeyDown 을 처리 하지 않고

TextInput으로 처리 하는 걸 알게 됐습니다.

그래서 TextBox가 포커스가 있어도 TextBox는 KeyDown을 처리 하지 않아

윈도우까지 가게 된 것이었습니다.

해결 방법은 KeyDown 이벤트에서 OriginalSource 를 체크하여 타입이 TextBox 면

return 으로 처리 했습니다.

두분 모두 감사합니다.

2개의 좋아요

네 말씀하신 것이 맞습니다.
저도 @code 님의 질문을 보고 터널링과 버블링의 관계로만 접근했다가, 예시의 이 글이 TextBox 였기 때문에 TextInput 때문에 결과론 적인 현상이 보여지는 거라서 복합적이게 설명한 것이 맞습니다.

그리고 HTML에도 동일하게 버블링과 터널링이 있는 것은 알고 있었는데, Preview 이벤트 핸들러를 WPF만 제공하는 것은 처음 알았네요.

감사합니다.

2개의 좋아요

Html 에는 터널링이 없고, 버블링만 있습니다.

2개의 좋아요

그랬군요! 체크 감사드립니다.

2개의 좋아요

아래의 요구 사항은 WPF 에서 처리하는 게 쉽지 않을 것입니다.

왜냐하면, WPF 에서는 포커스된 요소만이 이벤트를 전파할 수 있기 때문입니다.

이러한 제한이 있는 경우, 아래와 같이 처리하면, 이벤트 간 간섭은 없어질지 몰라도, Window 가 포커스를 가지고 있지 않으면, 단축키 로직은 실행될 기회 자체가 없을 수 있습니다.

Window 가 Loaded 된 후에는 Window 가 Focus 를 가질 수도 있고 안 가질 수도 있습니다.
가진 경우에는 문제가 없지만, 안 가진 경우에는 단축키는 동작하지 않습니다.

만약 TextBox 에 입력이 완료된 후 포커스가 다시 Window 로 돌아 가지 않으면 단축키 로직은 실행되지 않습니다. (이벤트 핸들러를 호출할 이벤트가 전파되니 않으니까요)

이점을 고려하는 코드가 추가되어야 할 것 같습니다.

저 같은 경우, 전역 이벤트가 설정된 뷰의 하위 요소에서는 이벤트 라우팅(버블링)을 끄는 설정을 하곤 했는데, 하위 요소가 많은 경우 코드가 조금 번잡해지는 단점이 있더군요.

그래서 전역 이벤트 보다는 단축키 전용 입력 콘트롤을 정의하고,

<TextBox x:Name="ShortcutCatcher"
         Focusable="True"
         Opacity="0"
         KeyDown="OnShortcutKeyDown"
         IsHitTestVisible="False"
         IsTabStop="False"/>
void OnShortcutKeyDown(object? s, EventArgs e)
{
   // 단축키 처리
}

// 포커스 되돌리기 로직 
void SetFocusToShortcutCatcher() =>
   Keyboard.Focus(ShortcutCatcher);

Window Load 시 이 요소에 포커스를 강제하여 단축키를 즉시 동작가능한 상태로 만들고,

Loaded += (s, e) => SetFocusToShortcutCatcher();

모든 텍스트 입력 요소의 이벤트 처리 후에는 반드시 포커스를 이 요소에 되돌리는 처리를 하는 것이 좋지 않을까합니다.

void OnTextInput(object? s, EventArgs e)
{
   // 고유 이벤트 처리
   SetFocusToShortcutCatcher();
}