WPFのTextBoxのEnterキーの挙動の変更♯3

前回の投稿でTextBoxでEnterキーを入力したときの挙動を変更する添付プロパティをnugetライブラリーとして公開したことについて紹介しました。今回はその機能拡張についてです。

TextBoxのEnterキーの挙動を制御する添付プロパティ

TextBoxのEnterキーの挙動を制御できる添付プロパティを実装したnugetライブラリーを公開しました。

このライブラリーで提供されれる添付プロパティは、TextBoxコントロールに設定して利用します。他のコントロールに設定した場合は、設定は無視されます。

添付プロパティの名前は、TextBox.AcceptsReturnプロパティの名前に合わせて、Enterキー(Return)が入力されたときの挙動という意味で、ReturnBehaviorとしました。

<TextBox 
    nsAttachedProps:TextBoxProperties.ReturnBehavior="UpdateSource"/>
...

このTextBoxProperties.ReturnBehavior添付プロパティを使うとEditBox内でEnterキーを入力したときの挙動を制御できます。

詳細な仕様は、ライブラリーのページを参照してください。

ComboBoxコントロールのEditBox

ところで、EditBoxコントロールと類似した機能を持つComboBoxコントロールがあります。ComboBoxコントロールのプロパティをComboBox.IsEditable=trueに設定したものは、EditBoxコントロールと同じようにテキストが編集可能となります。

このComboBoxコントロール内のEditBoxコントロールにも、TextBoxProperties.ReturnBehavior添付プロパティが使えるようにしたいと思いました。

EditBoxとComboBoxのTextプロパティのUpdateSourceTriggerの既定値

EditBoxコントロールのTextプロパティはデータバインディングしたときのUpdateSourceTriggerの既定値はLostFocusでした。そのため、文字を入力しただけでは、値が反映されず、コントロールがフォーカスを失ったときにはじめて値が反映されました。フォーカスを失う前であってもEnterキーを入力したときには値を反映することができるようにTextBoxProperties.ReturnBehavior添付プロパティを用意しました。

しかし、ComboBoxコントロールのTextプロパティはデータバインディングしたときのUpdateSourceTriggerの既定値はPropertyChangedです。UpdateSourceTriggerがPropertyChangedのため、文字が入力されたらその都度、値が反映されます。このような場合、Enterキーの入力で値を反映するような機能は必要ありません。

ただ、キー入力がその都度反映されるので、処理が重くなったりすることがあります。そのような場合は、EditBoxと同じように、UpdateSourceTriggerLostFocusに設定します。ComboBoxコントロールの場合はUpdateSourceTriggerLostFocusに設定したときに、TextBoxProperties.ReturnBehavior添付プロパティが効果的に働きます。

TextBoxPropertiesクラスの実装変更(ComboBox対応)

今回、TextBoxProperties クラスのTextBoxProperties.ReturnBehavior添付プロパティをComboBoxコントロールにも対応させます。

TextBoxPropertiesクラスの実装コードは、github.comにnishy2000/nsAttachedPropertiesとして公開されています。

添付プロパティの値が変化したときの処理

まず、TextBoxProperties.ReturnBehavior添付プロパティの値が変化したときの処理を変更する必要があります。

今まで(バージョン1.1.0まで)は、EditBoxコントロールのときのみ処理していましたが、ComboBoxコントロールのときも処理するようにします。具体的には下記の水色背景のところのコードのように、ComboBoxコントロールも受け入れるようにします。

static void OnReturnBehaviorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    if (!(e.NewValue is ReturnBehaviorMode mode)) { return; }

    if (!(d is Control ctrl)) { return; }
    if (!(ctrl is TextBox) && !(ctrl is ComboBox)) { return; }

    if (mode != ReturnBehaviorMode.None)
    {
        ctrl.PreviewKeyDown += OnPreviewKeyDown;
    }
    else
    {
        ctrl.PreviewKeyDown -= OnPreviewKeyDown;
    }
}

キー入力があった時の処理

キー入力があった時の処理も変更する必要があります。キー入力イベントの送信元が、EditBoxコントロールだけでなく、ComboBoxコントロールも追加されます。

送信元がComboBoxコントロールの場合は、内包しているEditBoxコントロールを取得する必要があります。前半の水色背景のところで、送信元がComboBoxコントロールの場合に、内包するEditBoxコントロールを取得しています。

また、データバインディングするプロパティ名はTextですが、使用すべきDependencyPropertyは、EditBoxとComboBoxで異なります。送信元により、使用するDependencyPropertyを調整します。後半の水色背景のところで、送信元に合わせて使用するDependencyPropertyを調整しています。

static void OnPreviewKeyDown(object sender, KeyEventArgs e)
{
    if (e.Key != System.Windows.Input.Key.Enter
        || !(sender is DependencyObject owner))
    {
        return;
    }

    var tb = sender as TextBox;
    ComboBox cb = null;
    if (tb == null)
    {
        cb = sender as ComboBox;
        if (cb != null
            && cb.IsEditable && !cb.IsReadOnly)
        {
            tb = GetTemplateChild(cb, "PART_EditableTextBox") as TextBox;
        }
    }

    if (tb == null
        || tb.AcceptsReturn)
    {
        return;
    }

    var mode = GetReturnBehavior(owner);

    if (mode == ReturnBehaviorMode.None) { return; }

    var modifiers = Keyboard.Modifiers;

    if (modifiers == ModifierKeys.None)
    {
        if (mode.HasFlag(ReturnBehaviorMode.UpdateSource))
        {
            var dp = cb != null ? ComboBox.TextProperty : TextBox.TextProperty;
            var bindingExpression = BindingOperations.GetBindingExpression(owner, dp);
            bindingExpression?.UpdateSource();
            e.Handled = true;
        }

        if (mode.HasFlag(ReturnBehaviorMode.SelectAll))
        {
            tb.SelectAll();
            e.Handled = true;
        }
    }

    if (modifiers == ModifierKeys.None || modifiers == ModifierKeys.Shift)
    {
        if (mode.HasFlag(ReturnBehaviorMode.MoveFocus))
        {
            var direction = modifiers == ModifierKeys.Shift ? FocusNavigationDirection.Previous : FocusNavigationDirection.Next;
            var focused = FocusManager.GetFocusedElement(tb) as FrameworkElement;
            if (focused == null && tb.IsFocused)
            {
                focused = tb;
            }
            focused?.MoveFocus(new TraversalRequest(direction));

            e.Handled = true;
        }
    }
}

テンプレートの中のコントロールを取得する処理

ComboBoxコントロールの場合は、内包するEditBoxコントロールを取得する必要があります。

内包するEditBoxコントロールを取得方法は、ComboBoxの実装で使われている方法を使います。具体的には、以下のコードです。

private const string EditableTextBoxTemplateName = "PART_EditableTextBox";

...

/// <summary>
/// Called when the Template's tree has been generated
/// </summary>
public override void OnApplyTemplate()
{
    base.OnApplyTemplate();
 
    if (_dropDownPopup != null)
    {
        _dropDownPopup.Closed -= OnPopupClosed;
    }
 
    EditableTextBoxSite = GetTemplateChild(EditableTextBoxTemplateName) as TextBox;
    _dropDownPopup = GetTemplateChild(PopupTemplateName) as Popup;
...

GetTemplateChild()を使って、PART_EditableTextBoxの名前のEditBoxコントロールを取得しています。

TextBoxProperties クラスでも同様の方法で取得します。ただし、FrameworkElement.GetTemplateChild()は、protectedのスコープのメソッドであるため、直接は呼び出せません。そのため、リフレクションを使って呼び出します。

具体的には以下のコードを使ってEditBoxコントロールを取得します。

static MethodInfo _getTemplateChildMethodInfo;
static DependencyObject GetTemplateChild(FrameworkElement fe, string childName)
{
    if (fe == null)
    {
        return null;
    }

    if (_getTemplateChildMethodInfo == null)
    {
        // Get PropInfo of internal property using reflection
        var feType = typeof(FrameworkElement);
        _getTemplateChildMethodInfo = feType.GetMethod("GetTemplateChild", BindingFlags.NonPublic | BindingFlags.Instance);
    }

    return _getTemplateChildMethodInfo?.Invoke(fe, new object [] { childName }) as DependencyObject;
}

これらの実装をすることにより、EditBoxコントロールのみに対応していたTextBoxProperties.ReturnBehavior添付プロパティがComboBoxコントロールにも対応できます。

nugetライブラリー化

今回、この添付プロパティの拡張機能を容易に利用できるようにするため、nugetライブラリーを更新しました。

nuget.orgに、NishySoftware.Wpf.AttachedProperties (nsAttachedProperties) 1.2.0として公開しました。パッケージを参照したら、すぐに利用できるようになります。

ComboBoxへの設定

xaml上では、ComboBoxでEnterキーが押されたときの挙動を制御するために、以下のように利用します。今までのEditBoxコントロールと同じ使い方です。

<Window
  ...
  xmlns:nsAttachedProps="http://schemas.nishy-software.com/xaml/attached-properties">
    <Grid>
...
      <ComboBox
          Text="{Binding TextValue,UpdateSourceTrigger=LostFocus}"
          IsEditable="True"
          nsAttachedProps:TextBoxProperties.ReturnBehavior="UpdateSource"/>
...
</Window>

以上、今回の投稿では、

  • ComboBoxでEnterキーを押したときの挙動を拡張するための添付プロパティの設計
  • それをライブラリー化したnugetパッケージ NishySoftware.Wpf.AttachedProperties (nsAttachedProperties) 1.2.0の公開

について説明しました。

コメントを残す