sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

WPF DataGridCell の TextBox だと KeyDown などのイベントが発生しない

問題点

WPF の DataGrid で DataGridTemplateColumn のテンプレートで TextBox(カスタムしたものなど)を設定しているとします。

f:id:shikaku_sh:20200520141350g:plain:w600
数字のキーを押して、フォーカスを与えています

わかりにくいけど、キーボードの「1」を押下してフォーカスを TextBox に与えていますが、TextBox は GotFocus イベントは発生していますが、KeyDown イベントは発生していません。

データグリッドを操作して、(キーボードで)数字やテキストを入力すると選択中のセル (DataGridCell) に数字やテキストを入力……となるのですが、このとき TextBox は KeyDown や PreviewKeyDown などのイベントを実行することなく、数値やテキストを受け取っていることがあります。

なにが不味いかというと、例えば、KeyDown イベントなどで入力された内容を訂正するようなチェックメソッドを入れていると、入力内容のチェックをせずに内容を変更できてしまいます。

マウスで DataGridCell を直接選択したときは、気にすることないです。

備考

この現象は Windows Forms のころからあるみたいです。問題が発生する理由としては、TextBox をホストしている親コントロールが KeyDown イベントを受信してしまって、子にあたる TextBox にイベントが伝播しないようです。

データグリッドは、セルの表示モードや編集モードなどがあるため、イベントの伝播がややこしい設計になっているのだと思います。

イベントを発生させるための対策

まず、DataGrid でキーボードのイベントを受け取るようにします。

<DataGrid ItemsSource="{Binding Samples}"
          PreviewKeyDown="DataGrid_PreviewKeyDown">

つぎに、受け取ったイベントを子要素に伝播させるコードを追加します。

private void DataGrid_PreviewKeyDown(object sender, KeyEventArgs e)
{
    var datagrid = sender as DataGrid;
    var pressedCell = e.OriginalSource as DataGridCell;
    var key = e.Key;

    if (datagrid == null || pressedCell == null) return;
    if (key == Key.Up || key == Key.Down || key == Key.Left || key == Key.Right) return;

    var nextFocus = pressedCell.PredictFocus(FocusNavigationDirection.Left);
    var inputElement = nextFocus as IInputElement;

    if (inputElement != null && nextFocus != null)
    {
        if (pressedCell.IsAncestorOf(nextFocus))
        {
            inputElement.Focus();

            // 子要素に発生したイベントを渡す
            if (inputElement is TextBox)
            {
                var target = inputElement as TextBox;
                var ev = new KeyEventArgs(Keyboard.PrimaryDevice, PresentationSource.FromVisual(target), e.Timestamp, e.Key);
                
                ev.RoutedEvent = e.RoutedEvent;
                (inputElement as TextBox).RaiseEvent(ev);
                e.Handled = true;
            }
        }

        // Enter キーの入力によるフォーカス設定はキー入力のみ無効化する(入力で再度フォーカスが外れるため)
        if (key == Key.Enter)
        {
            e.Handled = true;
        }
    }
}

MVVM でイベントをコマンド実行にしていても似たような対応が可能だと思います。

サンプル

テストプログラムは GitHub の「Samples」に公開しています。今回のプログラムは「FixedDecimalPointTextBoxSample」です。

参考

WPF 4.5入門

WPF 4.5入門