ダブルクリックで編集可能になるTextBoxをつくる2

大昔にダブルクリックで編集可能になるTextBoxを実装した(ダブルクリックで編集可能になるTextBoxをつくる - 陰間茶屋)のだが、Drag操作で難があったので違う実装をしてみることにした。

 

今度はUserControlにLabelとTextBoxを置いて、編集操作で表示を切り替える作戦。
Labelって書いたけど"_"の表示ができなかったので実際にはTextBlockになった。
コードはこんな感じになった。
もうちょっとうまい書き方があるような気はする。

EditableLabel.xaml.cs

<UserControl x:Class="Railgun.Views.EditableLabel"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:Railgun.Views"
             mc:Ignorable="d" 
             d:DesignHeight="100" d:DesignWidth="200">
    <Grid>
        <!-- ラベルだと_表記が省略される。TextBlockだとダブルクリックがとれない。そのため構造を挟む -->
        <ContentControl x:Name="PART_Label"
                        Visibility="Visible">
            <TextBlock x:Name="PART_LabelText"
                       Text="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:EditableLabel}},
                       Path=Text}"
                       Padding="5,3,5,3"/>
        </ContentControl>

        <!-- 最初はHiddenにしておかないと領域計算ができず、ダブルクリック後のカーソル位置の計算が上手くいかない -->
        <TextBox x:Name="PART_TextBox" 
                 Visibility="Hidden"
                 AcceptsReturn="True"
                 Background="{x:Null}"
                 Text="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:EditableLabel}},
                        Path=Text}"/>
    </Grid>
</UserControl>


EditableLabel.xaml

using Railgun.Helper;
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace Railgun.Views
{
    // 参考: https://stackoverflow.com/questions/32368931/universally-usable-editable-label

    /// <summary>
    /// EditableLabel.xaml の相互作用ロジック
    /// </summary>
    public partial class EditableLabel : UserControl
    {
        public EditableLabel()
        {
            EditLabelCommand = new EditLabelCommand(this);
            InitializeComponent();

            // InputBindingのためにはKeyboardFocusが必要になる
            Focusable = true;
        }

        // bool IsEditing   編集可能か?
        public static readonly DependencyProperty IsEditingProperty =
            DependencyProperty.Register("IsEditing",
                                        typeof(bool),
                                        typeof(EditableLabel),
                                        new FrameworkPropertyMetadata(
                                            false,
                                            FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                                            OnIsEditingChanged));
        public bool IsEditing {
            get { return (bool)GetValue(IsEditingProperty); }
            set { SetValue(IsEditingProperty, value); }
        }

        static void OnIsEditingChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            EditableLabel self = obj as EditableLabel;
            if (self.IsEditing) {
                self.PART_Label.Visibility = Visibility.Collapsed;
                self.PART_TextBox.Visibility = Visibility.Visible;
            }
            else {
                self.PART_Label.Visibility = Visibility.Visible;
                self.PART_TextBox.Visibility = Visibility.Collapsed;
            }
        }

        // string Text      文字列
        public static readonly DependencyProperty TextProperty =
            DependencyProperty.Register("Text", typeof(string), typeof(EditableLabel),
                new FrameworkPropertyMetadata("",
                    FrameworkPropertyMetadataOptions.AffectsRender |
                    FrameworkPropertyMetadataOptions.AffectsParentMeasure |
                    FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                    OnTextChanged));

        public string Text {
            get { return (string)GetValue(TextProperty); }
            set { SetValue(TextProperty, value); }
        }

        static void OnTextChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {

        }

        // ICommand EditLabelCommand    編集コマンド
        public static readonly DependencyProperty EditLabelCommandProperty =
            DependencyProperty.Register("EditLabelCommand", typeof(ICommand), typeof(EditableLabel),
                new PropertyMetadata(null, OnEditLabelCommandChanged));

        public ICommand EditLabelCommand {
            get { return (ICommand)GetValue(EditLabelCommandProperty); }
            set { SetValue(EditLabelCommandProperty, value); }
        }

        static void OnEditLabelCommandChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            EditableLabel self = obj as EditableLabel;

            // 古いコマンドを削除
            ICommand old = e.OldValue as ICommand;
            self.RemoveInputBinding(old, Key.F2, ModifierKeys.None);

            // 新しいコマンドを登録
            ICommand command = e.NewValue as ICommand;
            if (command != null) {
                self.InputBindings.Add(new KeyBinding(command, Key.F2, ModifierKeys.None));
            }
        }

        // コマンド削除
        void RemoveInputBinding(ICommand command, Key key, ModifierKeys modifier)
        {
            foreach (InputBinding binding in InputBindings) {
                KeyBinding kb = binding as KeyBinding;
                if (kb != null) {
                    if (kb.Command == command
                        && kb.Key == key
                        && kb.Modifiers == modifier) {

                        InputBindings.Remove(binding);
                        return;
                    }
                }
            }
        }

        /*-----------------------------------------------*/

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            PART_Label.MouseDoubleClick += Label_MouseDoubleClick;
            PART_TextBox.LostKeyboardFocus += TextBox_LostKeyboardFocus;
            PART_TextBox.PreviewKeyDown += TextBox_PreviewKeyDown;

            // 他の要素をクリックしたときに編集モードを解除したいが難しい・・・
        }

        // マウスクリック
        protected override void OnMouseUp(MouseButtonEventArgs e)
        {
            base.OnMouseUp(e);

            // InputBindingのためにはKeyboardFocusが必要になる
            Focus();
            e.Handled = true;
        }


        // ラベルのダブルクリック
        void Label_MouseDoubleClick(object sender, MouseButtonEventArgs e)
        {
            IsEditing = true;

            // ダブルクリック後のカーソル位置を決める
            Point pos = Mouse.GetPosition(PART_TextBox);
            int textIndex = PART_TextBox.GetCharacterIndexFromPoint(pos, false);
            if (textIndex < 0) {
                textIndex = PART_TextBox.Text.Length;
            }
            PART_TextBox.Select(textIndex, 0);

            // BeginInvokeで実行しないとフォーカスが取れなかった
            Dispatcher.BeginInvoke(new Action(() => {
                PART_TextBox.Focus();
            }));
        }

        // テキストボックスのキーボードフォーカスがなくなった
        void TextBox_LostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
        {
            // 表示タイミングが合わないので明示的に入れる
            Text = PART_TextBox.Text;
            IsEditing = false;
        }

        // テキストボックスのキーが押されたときの前処理
        void TextBox_PreviewKeyDown(object sender, KeyEventArgs e)
        {
            // Enter, Escapeが押されたら編集終了
            bool isShift = ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift);
            if (e.Key == Key.Enter && !isShift) {
                // 表示タイミングが合わないので明示的に入れる
                Text = PART_TextBox.Text;
                IsEditing = false;
                e.Handled = true;
            }
            else if (e.Key == Key.Escape) {
                // この時点ではTextは編集前の値なので、ここで戻せばキャンセルが間に合う
                PART_TextBox.Text = Text;
                IsEditing = false;
                e.Handled = true;
            }
        }
    }

    // 編集可能にするコマンド
    class EditLabelCommand : DelegateCommand
    {
        public EditLabelCommand(EditableLabel editableLabel) : base(
            () => {
                editableLabel.IsEditing = true;
                editableLabel.PART_TextBox.Focus();
            }
        )
        { }
    }
}

ダブルクリックで編集モードになる。
F2キーを押したときも編集モードになる。
Enterを押すと編集完了。
Escapeを押したときも編集完了、だけど、編集前の値に戻る。

他の要素をクリックしたときに編集モードを解除させたかったんだけどやり方わからなくてできなかった。
フォーカスが違うところに行く場合なら編集モードが解除されるんだけど。
誰か教えて・・・。

WPF勉強中なんだけど分からんことが多い。
俺は雰囲気と勘と勢いでxamlを書いている。
デバッグは泣きながら行う。

参考にしたのはこのあたり
c# - Bind Command to KeyBinding in Code Behind - Stack Overflow
c# - UserControl InputBindings Only working after pressing a button first - Stack Overflow
wpf - Do mouse clicks bring keyboard focus to focusable controls by default? - Stack Overflow