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

WPFで、ダブルクリックで編集可能になるTextBoxをつくりたいなーと思ったので作ってみた。

Behaviorで、TextBoxのIsReadOnlyとFocusableを制御する作戦。

 

コードはこんな感じ。

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interactivity;

namespace Railgun.Helper
{
    /**
     * ダブルクリックで編集モードに移行するTextBoxになる
     * フォーカスが外れるか、Enterキーで編集終了となる
     * 
     *  <TextBox Text="hogehoge">
     *      <i:Interaction.Behaviors>
     *          <local:EditableLabelBehavior/>
     *      </i:Interaction.Behaviors>
     *  </Button>
     */
    public class EditableLabelBehavior : Behavior<TextBox>
    {
        // bool IsEditable          編集可能か?
        public static readonly DependencyProperty IsEditableProperty =
            DependencyProperty.Register("IsEditable",
                                        typeof(bool),
                                        typeof(EditableLabelBehavior),
                                        new PropertyMetadata(false, OnIsEditableChanged));

        public bool IsEditable {
            get { return (bool)GetValue(IsEditableProperty); }
            set { SetValue(IsEditableProperty, value); }
        }

        static void OnIsEditableChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            EditableLabelBehavior behavior = obj as EditableLabelBehavior;
            behavior.ApplyEditable();
        }


        // Thickness EditingBorderThickness     編集中の枠線
        public static readonly DependencyProperty EditingBorderThicknessProperty =
            DependencyProperty.Register("EditingBorderThickness",
                                typeof(Thickness),
                                typeof(EditableLabelBehavior),
                                new PropertyMetadata(new Thickness(3), OnEditingBorderThicknessChanged));

        public Thickness EditingBorderThickness {
            get { return (Thickness)GetValue(EditingBorderThicknessProperty); }
            set { SetValue(EditingBorderThicknessProperty, value); }
        }

        static void OnEditingBorderThicknessChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
        }


        // textBoxの状態を編集可・不可に切り替える
        void ApplyEditable()
        {
            if (IsEditable) {
                // 編集可
                AssociatedObject.IsReadOnly = false;
                AssociatedObject.BorderThickness = EditingBorderThickness;
                AssociatedObject.Focusable = true;
            }
            else {
                // 編集不可
                AssociatedObject.IsReadOnly = true;
                AssociatedObject.BorderThickness = _borderThickness;    // 元に戻す
                AssociatedObject.Focusable = false;
            }
        }

        Thickness _borderThickness;

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

        // 初期化
        protected override void OnAttached()
        {
            _borderThickness = AssociatedObject.BorderThickness;
            ApplyEditable();

            AssociatedObject.MouseDoubleClick += AssociatedObject_MouseDoubleClick;
            AssociatedObject.LostFocus += AssociatedObject_LostFocus;
            AssociatedObject.KeyDown += AssociatedObject_KeyDown;
        }

        protected override void OnDetaching()
        {
            AssociatedObject.MouseDoubleClick -= AssociatedObject_MouseDoubleClick;
            AssociatedObject.LostFocus -= AssociatedObject_LostFocus;
            AssociatedObject.KeyDown -= AssociatedObject_KeyDown;
        }


        void AssociatedObject_MouseDoubleClick(object sender, MouseButtonEventArgs e)
        {
            IsEditable = true;      // 編集可

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

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

        void AssociatedObject_LostFocus(object sender, RoutedEventArgs e)
        {
            IsEditable = false;
            //Console.WriteLine("AssociatedObject_LostFocus");
        }

        void AssociatedObject_KeyDown(object sender, KeyEventArgs e)
        {
            // Enterキーが押されたら編集終了
            if (e.Key == Key.Enter) {
                IsEditable = false;
            }
        }
    }
}

使い方

    <TextBox Text="hogehoge">
        <i:Interaction.Behaviors>
            <local:EditableLabelBehavior EditingBorderThickness="3"/>
        </i:Interaction.Behaviors>
    </Button>

ゲームエディタ作ってる中で実験してたので、実はシンプルな例で試してない。
うまく動かんかったらすまぬ。

ダブルクリックで編集可能になって、フォーカスが外れると編集不可になる。
編集時はBorderThicknessの太さが変わるようにして、Enterキーで編集終了。
という動きになってる。


実装上のポイントは、
編集可能:TextBox.IsReadOnly = false; TextBox.Focusable = true;
編集不可:TextBox.IsReadOnly = true; TextBox.Focusable = false;
としているところ。これでだいたい困らないかなーと。

あと、ダブルクリックしたときはカーソル位置を決めてからフォーカスをとっている。
ただ、普通にAssociatedObject.Focus();としてもうまく動かなかったので、Dispatcher.BeginInvoke経由で実行してる。
(なお、このやり方見つけるまではmouse_eventを駆使して無理やり再クリックさせてフォーカスをとるという実装だった・・・)
その時の実装もメモ程度に載せておく。一応これでも動く

    // マウスクリック
    [DllImport("USER32.dll", CallingConvention = CallingConvention.StdCall)]
    private static extern void mouse_event(int dwFlags, int dx, int dy, int cButtons, int dwExtraInfo);

    private const int MOUSEEVENTF_LEFTDOWN = 0x2;
    private const int MOUSEEVENTF_LEFTUP = 0x4;

    // ...(略)...

        // AssociatedObject.Focus()だとうまくいかなかったので、無理やりクリックさせてみる
        mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0);
        mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);

    // ...(略)...


参考にしたのはこのあたり:
stackoverflow.com
stackoverflow.com
www.nuits.jp