1
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

時刻入力用TextBoxBehavior作成 (MaskedTextProviderを利用)

Last updated at Posted at 2021-03-20

はじめに

時刻入力用のTextBoxが必要なことがあり、MaskedTextProviderを使えばいいらしいことは分かったのですが、実装がうまくできませんでした。

欲しかった画面はこんな感じでした。
image.png
日付を入力して、そのあと、時間を入力するというもの。
時刻はText(string)で使えるようにしたいです。

MaterialDesignのExsampleにMaskedTextBoxがあったので使えるかなと思ったのですが、これはXceed SoftwareのExtended.Wpf.Toolkitを利用していました。
Extended.Wpf.Toolkitはライセンスが以前はMS-PLライセンスだったみたいですが、ライセンスが変わって、非商用でも10人以上のユーザーがいれば商用ライセンスが必要となったようで、採用するのは難しいと思いました。

そこで、いろいろと調べていたところ、Blindmeis's BlogにWPF – Masked Textbox Behaviorがとても参考になりました。
ブログの記事だと、汎用的だったので、参考にしながら、時刻用の設定を追加したりして完成できました。

詳細は後で記事にするかもしれませんが、まずは参考にしていただければと思い、全コードを記載しておきます。
ちなみに、ビヘイビアとして作成しています。

MaskedTextBoxBehavior : Behavior

using Microsoft.Xaml.Behaviors;
using System;
using System.ComponentModel;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace Sample.WPF.Behaviors
{
     public class MaskedTextBoxBehavior : Behavior<TextBox> 
    {
        #region DependencyProperties
        public string InputMask
        {
            get { return (string)GetValue(InputMaskProperty); }
            set { SetValue(InputMaskProperty, value); }
        }

        public static readonly DependencyProperty InputMaskProperty =
          DependencyProperty.Register("InputMask", typeof(string), 
                                        typeof(MaskedTextBoxBehavior),  
                                        null);


        public char PromptChar
        {
            get { return (char)GetValue(PromptCharProperty); }
            set { SetValue(PromptCharProperty, value); }
        }

        public static readonly DependencyProperty PromptCharProperty =
           DependencyProperty.Register("PromptChar", typeof(char), 
                                        typeof(MaskedTextBoxBehavior),
                                        new PropertyMetadata('_'));


        public bool IsTime
        {
            get { return (bool)GetValue(IsTimeProperty); }
            set { SetValue(IsTimeProperty, value); }
        }

        public static readonly DependencyProperty IsTimeProperty =
            DependencyProperty.Register("MyProperty", typeof(bool), 
                                        typeof(MaskedTextBoxBehavior), 
                                        new PropertyMetadata(false));

        #endregion

        public MaskedTextProvider Provider { get; private set; }

        protected override void OnAttached()
        {
            base.OnAttached();
            AssociatedObject.Loaded += AssociatedObjectLoaded;
            AssociatedObject.PreviewTextInput += AssociatedObjectPreviewTextInput;
            AssociatedObject.PreviewKeyDown += AssociatedObjectPreviewKeyDown;
            AssociatedObject.LostFocus += AccociateObjectLostFocus;
            AssociatedObject.GotFocus += AssociatedObjectGotFocus;

            DataObject.AddPastingHandler(AssociatedObject, Pasting);
        }


        protected override void OnDetaching()
        {
            base.OnDetaching();
            AssociatedObject.Loaded -= AssociatedObjectLoaded;
            AssociatedObject.PreviewTextInput -= AssociatedObjectPreviewTextInput;
            AssociatedObject.PreviewKeyDown -= AssociatedObjectPreviewKeyDown;
            AssociatedObject.LostFocus -= AccociateObjectLostFocus;
            AssociatedObject.GotFocus -= AssociatedObjectGotFocus;

            DataObject.RemovePastingHandler(AssociatedObject, Pasting);
        }


        /// <summary>
        /// カーソルを最初の位置にする
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void AssociatedObjectGotFocus(object sender, RoutedEventArgs e)
        {
            AssociatedObject.Select(0, 0);

            //選択時にSelectAllにする場合はこちら
            //AssociatedObject.Select(0, AssociatedObject.Text.Length);
        }

    
        void AssociatedObjectLoaded(object sender, System.Windows.RoutedEventArgs e)
        {
            Provider = new MaskedTextProvider(InputMask, CultureInfo.CurrentCulture);
            Provider.PromptChar = PromptChar;
            
            Provider.Set(AssociatedObject.Text);

            //ToDisplayString:表示できる形式で書式設定された文字列
            AssociatedObject.Text = Provider.ToDisplayString();

            var textProp = DependencyPropertyDescriptor.FromProperty(TextBox.TextProperty, typeof(TextBox));
            if (textProp != null)
            {
                textProp.AddValueChanged(AssociatedObject, (s, args) => UpdateText());
            }
        }

        void AssociatedObjectPreviewTextInput(object sender, System.Windows.Input.TextCompositionEventArgs e)
        {
            TreatSelectedText();

            var position = GetNextCharacterPosition(AssociatedObject.SelectionStart);

            if (Keyboard.IsKeyToggled(Key.Insert))
            {
                //上書きモード
                if (Provider.Replace(e.Text, position))
                    position++;
            }
            else
            {
                //挿入モード
                if (Provider.InsertAt(e.Text, position))
                    position++;
            }

            position = GetNextCharacterPosition(position);

            RefreshText(position);

            e.Handled = true;
        }

        void AssociatedObjectPreviewKeyDown(object sender, KeyEventArgs e)
        {

            if (e.Key == Key.Space)
            {
                TreatSelectedText();

                var position = GetNextCharacterPosition(AssociatedObject.SelectionStart);

                if (Provider.InsertAt(" ", position))
                    RefreshText(position);

                e.Handled = true;
            }

            if (e.Key == Key.Back)
            {
                if (TreatSelectedText())
                {
                    RefreshText(AssociatedObject.SelectionStart);
                }
                else
                {
                    if (AssociatedObject.SelectionStart != 0)
                    {
                        if (Provider.RemoveAt(AssociatedObject.SelectionStart - 1))
                            RefreshText(AssociatedObject.SelectionStart - 1);
                    }
                }

                e.Handled = true;
            }

            if (e.Key == Key.Delete)
            {
                if (TreatSelectedText())
                {
                    RefreshText(AssociatedObject.SelectionStart);
                }
                else
                {

                    if (Provider.RemoveAt(AssociatedObject.SelectionStart))
                        RefreshText(AssociatedObject.SelectionStart);

                }

                e.Handled = true;
            }

        }

        private void AccociateObjectLostFocus(object sender, RoutedEventArgs e)
        {
            if (IsTime)
            {
                if(CheckTime(AssociatedObject.Text) != null)
                {
                    // Time is correct!
                }
                else
                {
                    AssociatedObject.Text = null;
                }
            }
        }

        /// <summary>
        /// 00:00形式のテキスト入力が正しくできているかどうか、違う場合はnullにする
        /// </summary>
        /// <param name="time"></param>
        /// <returns></returns>
        private string CheckTime(string time)
        {
            if (time == null) return null;

            if (string.IsNullOrWhiteSpace(time)) return null;

            string[] HHmm = time.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries);
            if (HHmm.Length != 2) return null;

            int hour;
            int minute;
            if (!int.TryParse(HHmm[0], out hour)) return null;
            if (!int.TryParse(HHmm[1], out minute)) return null;

            if (hour < 0 || hour > 24) return null;
            if (minute < 0 || minute > 59) return null;

            return time;
        }

        /// <summary>
        /// ペースト対応
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Pasting(object sender, DataObjectPastingEventArgs e)
        {
            if (e.DataObject.GetDataPresent(typeof(string)))
            {
                var pastedText = (string)e.DataObject.GetData(typeof(string));

                TreatSelectedText();

                var position = GetNextCharacterPosition(AssociatedObject.SelectionStart);

                if (Provider.InsertAt(pastedText, position))
                {
                    RefreshText(position);
                }
            }

            e.CancelCommand();
        }

        /// <summary>
        /// Textの更新
        /// </summary>
        private void UpdateText()
        {
            //同じときは更新なし
            if (Provider.ToDisplayString().Equals(AssociatedObject.Text))
                return;

            var isSet = Provider.Set(AssociatedObject.Text);

            SetText(isSet ? Provider.ToDisplayString() : AssociatedObject.Text);
        }

        /// <summary>
        /// Provider.Textの整理
        /// </summary>
        /// <returns></returns>
        private bool TreatSelectedText()
        {
            if (AssociatedObject.SelectionLength > 0)
            {
                return Provider.RemoveAt(AssociatedObject.SelectionStart,
                                        AssociatedObject.SelectionStart + AssociatedObject.SelectionLength - 1);
            }
            return false;
        }

        private void RefreshText(int position)
        {
            SetText(Provider.ToDisplayString());

            AssociatedObject.SelectionStart = position;
        }

        /// <summary>
        /// Textをセットします
        /// </summary>
        /// <param name="text"></param>
        private void SetText(string text)
        {
            AssociatedObject.Text = string.IsNullOrWhiteSpace(text) ? string.Empty : text;
        }

        private int GetNextCharacterPosition(int startPosition)
        {
            var position = Provider.FindEditPositionFrom(startPosition, true);

            if (position == -1)
                return startPosition;
            else
                return position;
        }
    }
}

参考)
Blindmeis's Blog WPF – Masked Textbox Behavior  (ドイツ語のブログでした💦)
Microsoft Document MaskedTextProvider クラス

1
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?