午後わてんのブログ

ベランダ菜園とWindows用アプリ作成(WPFとC#)

WPF、数値とBindingしたテキストボックスに「0.」とか「-0」とか「-0.」を入力したい

f:id:gogowaten:20200626122011g:plain
数値とBindingしたテキストボックス

2020WPF/20200625_decimalTextBox at master · gogowaten/2020WPF
github.com

目的
テキストボックスと数値型依存関係プロパティのリアルタイム連動

  • 数値型の依存関係プロパティとTextBoxのTextPropertyをBinding
  • TextPropertyの既定の更新タイミングはロストフォーカス時だけど、入力中リアルタイムで更新したいのでUpdateSourceTrigger=PropertyChangedに変更
  • 書式の設定もしたい。入力中は書式を外して、ロストフォーカス時に書式を適用

問題なのは
普通にBindingした状態で「-0.2」と入力しようとする場合。順番にキーを打つと、「-0」の時点で強制的に「0」にされてしまう
また、0.2の場合も「0.」まで打つと強制的に「0」にされてしまう
このような問題はUpdateSourceTrigger=PropertyChangedじゃなければ起きない

解決?
最初に考えていたのは
テキストボックスのTextプロパティ ←Binding→ 数値型依存関係プロパティ
こうだったけど
テキストボックスのTextプロパティ ←Binding→ 文字列型依存関係プロパティ ←連携→ 数値型依存関係プロパティ
こう、あいだに文字列型依存関係プロパティを挟んだら期待する動作にできた

依存関係プロパティ同士の連携(連動)
f:id:gogowaten:20200626130402p:plain
String型とdecimal型の依存関係プロパティを用意、名前は

  • MyText(74と80行目)
  • MyValue(49と55行目)

にした

それぞれに値変更時のコールバック、PropertyChangedCallbackを用意

  • MyTextのCallback(84行目)

    • 入直値が「-0」or「-0.」のときはMyValueを変更しない
    • 入力値をdecimal.TryParseで数値に変換できたら、それをMyValueに入れる
  • MyValueのCallback(59行目)

    • 入力値をToStringで文字列に変換してMyTextに入れる

これは無限ループしそうなんだけどねえ、ならないのは古い値と新しい値が同じ場合は、PropertyChangedCallbackは実行されないからかな
もしこの連携をPropertyChangedCallbackを使わずに、77行目や52行目のsetのところに書くと無限ループになるのかも

書式設定
数値を文字列に変換するときのToString()には、いろいろ書式を指定できる

書式を指定して数値を文字列に変換する - .NET Tips(VB.NET, C#...)

dobon.net ここがわかりやすい

f:id:gogowaten:20200626131800p:plain
書式指定用のプロパティも依存関係プロパティにした(96~128行目)
PropertyChangedCallback(106行目)にはMyValueに書式を適用して文字列に変換したのをMyTextに適用
エラー対策で、入力された書式で変換できなかった場合は、CoerceValueCallback(112行目)を使って元の古い書式に戻すようにした

MainWindow.xaml

<Window x:Class="_20200625_decimalTextBox.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:_20200625_decimalTextBox"
        mc:Ignorable="d"
        Title="MainWindow" Height="400" Width="400">
  <Grid>
    <Viewbox>
      <StackPanel Width="200">
        <TextBox x:Name="MyTextBox"
                 GotFocus="MyTextBox_GotFocus"
                 LostFocus="MyTextBox_LostFocus"
                 PreviewMouseLeftButtonDown="MyTextBox_PreviewMouseLeftButtonDown"
                 Text="{Binding Path=MyText, UpdateSourceTrigger=PropertyChanged,
          RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:MainWindow}}"/>
        <Separator Background="MediumSlateBlue"/>
        <StackPanel Orientation="Horizontal">
          <TextBlock Text="MyText = "/>
          <TextBlock Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:MainWindow},
          Path=MyText}"/>
        </StackPanel>
        <StackPanel Orientation="Horizontal">
          <TextBlock Text="MyValue = "/>
          <TextBlock Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:MainWindow}, 
          Path=MyValue}"/>
        </StackPanel>
        <Slider Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:MainWindow}, 
          Path=MyValue, Mode=TwoWay}"
                Minimum="-100" Maximum="2000"/>
        <Button Content="" Click="Button_Click"/>
        <Button Content="0.000" Click="Button_Click"/>
        <Button Content="C" Click="Button_Click"/>
        <Button Content="#,0" Click="Button_Click"/>
        <Button Content="N" Click="Button_Click"/>
        <Button Content="P" Click="Button_Click"/>
        <Button Content="だいたい0個" Click="Button_Click"/>
        <Button Content="だいたい0個;だいたい-0個;だいたい0個" Click="Button_Click"/>
        <Button Content="気温0.0度;気温-0.0度;気温0.0度" Click="Button_Click"/>
        <Button Content="D4" Click="Button_Click"/>
        <Button Content="E4" Click="Button_Click"/>
      </StackPanel>
    </Viewbox>
  </Grid>
</Window>


MainWindow.xaml.cs

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


//目的
//テキストボックスのTextプロパティに数値型の依存関係プロパティをBindingしておいて
//テキストボックスに入力中(UpdateSourceTrigger=PropertyChanged)にも更新したい
//要はテキストボックスと数値型依存関係プロパティが連動していればいい
//追加で、書式の設定もしたい。入力中は書式を外して、ロストフォーカス時に書式を適用

namespace _20200625_decimalTextBox
{
    public partial class MainWindow : Window
    {   
        public MainWindow()
        {
            InitializeComponent();

        }

        #region 依存関係プロパティ
        //数値型依存関係プロパティ、今回はdecimal型にした
        public decimal MyValue
        {
            get { return (decimal)GetValue(MyValueProperty); }
            set { SetValue(MyValueProperty, value); }
        }

        public static readonly DependencyProperty MyValueProperty =
            DependencyProperty.Register(nameof(MyValue), typeof(decimal), typeof(MainWindow),
                new PropertyMetadata(0m, OnMyValuePropertyChanged));

        private static void OnMyValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var mw = d as MainWindow;
            var m = (decimal)e.NewValue;
            if (mw.MyTextBox.IsFocused)
            {
                mw.MyText = m.ToString();
            }
            else
            {
                mw.MyText = m.ToString(mw.MyStringFormat);
            }
        }

        //文字列型依存関係プロパティ
        public string MyText
        {
            get { return (string)GetValue(MyTextProperty); }
            set { SetValue(MyTextProperty, value); }
        }

        public static readonly DependencyProperty MyTextProperty =
            DependencyProperty.Register(nameof(MyText), typeof(string), typeof(MainWindow),
                new PropertyMetadata("", OnMyTextPropertyChanged));

        private static void OnMyTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var mw = d as MainWindow;
            var s = (string)e.NewValue;
            if (s == "-0" || s == "-0.") return;
            if (decimal.TryParse(s, out decimal m))
            {
                mw.MyValue = m;
            }
        }

        //書式指定用の文字列型依存関係プロパティ
        public string MyStringFormat
        {
            get { return (string)GetValue(MyStringFormatProperty); }
            set { SetValue(MyStringFormatProperty, value); }
        }

        public static readonly DependencyProperty MyStringFormatProperty =
            DependencyProperty.Register(nameof(MyStringFormat), typeof(string), typeof(MainWindow),
                new PropertyMetadata("", OnMyStrinfFormatChanged, CoerceMyStrinfFormatValue));

        private static void OnMyStrinfFormatChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var mw = d as MainWindow;
            var sf = (string)e.NewValue;
            mw.MyText = mw.MyValue.ToString(sf);
        }
        private static object CoerceMyStrinfFormatValue(DependencyObject d, object baseValue)
        {
            //新しい書式を適用するとエラーになる場合は、元の書式に書き換える
            var mw = d as MainWindow;
            var s = (string)baseValue;//新しい書式
            try
            {
                //新しい書式適用
                mw.MyValue.ToString(s);
            }
            catch (Exception)
            {
                //エラーなら元の書式に書き換え
                s = mw.MyStringFormat;
            }
            return s;
        }
        #endregion 依存関係プロパティ


        #region textboxのイベント時の動作
        //フォーカス時に文字列全部を選択
        private void MyTextBox_GotFocus(object sender, RoutedEventArgs e)
        {
            var tb = sender as TextBox;
            tb.Text = MyValue.ToString();
            tb.SelectAll();
        }

        //ロストフォーカス時に書式適用
        private void MyTextBox_LostFocus(object sender, RoutedEventArgs e)
        {
            var tb = sender as TextBox;
            tb.Text = MyValue.ToString(MyStringFormat);
        }

        //左クリック時にも文字列全部を選択
        private void MyTextBox_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            var tb = sender as TextBox;
            if (tb.IsFocused == false)
            {
                tb.Focus();
                e.Handled = true;
            }
        }
        #endregion textboxのイベント時の動作


        //        書式を指定して数値を文字列に変換する - .NET Tips(VB.NET, C#...)
        //https://dobon.net/vb/dotnet/string/inttostring.html
        //書式変更確認用ボタンクリック時の動作
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            var tb = sender as Button;
            MyStringFormat = tb.Content.ToString();
        }


    }

}




関連記事
次回は1週間後
WPFにもNumericUpDownみたいなのをユーザーコントロールで、その6 - 午後わてんのブログ
gogowaten.hatenablog.com

一昨日、PropertyChangedCallbackとCoerceValueCallbackはこっち
WPFにもNumericUpDownみたいなのをユーザーコントロールで、その5
gogowaten.hatenablog.com これに今回のを組み入れたい

1週間前
WPFで数字とハイフンとピリオドだけ入力できるテキストボックス、-0.0に意味はある?
gogowaten.hatenablog.com