午後わてんのブログ

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

WPFにもNumericUpDownみたいなのをユーザーコントロールで、その5

昨日の続き

f:id:gogowaten:20200624162452g:plain
下限値と上限値を設定できるようにした
今回でNumericUpDownは完成
userControl/ControlLibraryCore20200620 at 0624_2341_blog · gogowaten/userControl

github.com

UserControl.xaml

<UserControl x:Class="ControlLibraryCore20200620.NumericUpDown"
             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:ControlLibraryCore20200620"
             mc:Ignorable="d" 
             d:DesignHeight="40" d:DesignWidth="200">
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition/>
      <RowDefinition/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
      <ColumnDefinition/>
      <ColumnDefinition Width="16"/>
    </Grid.ColumnDefinitions>

    <RepeatButton Grid.Row="0" Grid.Column="1" IsTabStop="False"
                  Click="RepeatButtonUp_Click"
                  MouseWheel="RepeatButton_MouseWheel">
      <RepeatButton.Content>
        <Viewbox x:Name="ViewBoxUp" Margin="1">
          <Polygon Points="1,0 2,1 0,1 1,0" Fill="Gray"/>
        </Viewbox>
      </RepeatButton.Content>
    </RepeatButton>
    <RepeatButton Grid.Row="1" Grid.Column="1" IsTabStop="False" 
                  Click="RepeatButtonDown_Click"
                  MouseWheel="RepeatButton_MouseWheel">
      <RepeatButton.Content>
        <Viewbox x:Name="ViewBoxDown" Margin="1">
          <Polygon Points="0,0 2,0 1,1 0,0" Fill="Gray"/>
        </Viewbox>
      </RepeatButton.Content>
    </RepeatButton>

    <TextBox x:Name="MyTextBox" Grid.RowSpan="2" Grid.Column="0"
             TextAlignment="Right" VerticalContentAlignment="Center"
             InputMethod.IsInputMethodSuspended="True"
             PreviewKeyDown="MyTextBox_PreviewKeyDown"
             PreviewTextInput="MyTextBox_PreviewTextInput"
             LostFocus="MyTextBox_LostFocus"
             CommandManager.PreviewExecuted="MyTextBox_PreviewExecuted"
             GotFocus="MyTextBox_GotFocus"
             PreviewMouseLeftButtonDown="MyTextBox_PreviewMouseLeftButtonDown"
             MouseWheel="MyTextBox_MouseWheel"
             Text="{Binding Path=MyValue, Mode=TwoWay,
      RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:NumericUpDown}}">

    </TextBox>
  </Grid>
</UserControl>


UserControl.xaml.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;


namespace ControlLibraryCore20200620
{
    /// <summary>
    /// Interaction logic for UserControl1.xaml
    /// </summary>
    public partial class NumericUpDown : UserControl
    {
        public NumericUpDown()
        {
            InitializeComponent();
        }

        #region 入力制限
        //スペースキーが押されたのを無効にする
        private void MyTextBox_PreviewKeyDown(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Space) e.Handled = true;
        }

        //入力の制限、数字とハイフンとピリオドだけ通す
        private void MyTextBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
        {
            var textbox = (TextBox)sender;
            string str = textbox.Text;//文字列
            var inputStr = e.Text;//入力された文字            

            //正規表現で入力文字の判定、数字とピリオド、ハイフンならtrue
            bool neko = new System.Text.RegularExpressions.Regex("[0-9.-]").IsMatch(inputStr);

            //入力文字が数値とピリオド、ハイフン以外だったら無効
            if (neko == false)
            {
                e.Handled = true;//無効
                return;//終了
            }

            //キャレット(カーソル)位置が先頭(0)じゃないときの、ハイフン入力は無効
            if (textbox.CaretIndex != 0 && inputStr == "-") { e.Handled = true; return; }

            //2つ目のハイフン入力は無効(全選択時なら許可)
            if (textbox.SelectedText != str)
            {
                if (str.Contains("-") && inputStr == "-") { e.Handled = true; return; }
            }

            //2つ目のピリオド入力は無効
            if (str.Contains(".") && inputStr == ".") { e.Handled = true; return; }
        }

        //フォーカス消失時、不自然な文字を削除
        private void MyTextBox_LostFocus(object sender, RoutedEventArgs e)
        {
            //ピリオドの削除
            //先頭か末尾にあった場合は削除
            var tb = (TextBox)sender;
            string text = tb.Text;
            if (text.StartsWith('.') || text.EndsWith('.'))
            {
                text = text.Replace(".", "");
            }

            // -. も変なのでピリオドだけ削除
            text = text.Replace("-.", "-");

            //数値がないのにハイフンやピリオドがあった場合は削除
            if (text == "-" || text == ".")
                text = "";

            tb.Text = text;
        }

        //
        private void MyTextBox_PreviewExecuted(object sender, ExecutedRoutedEventArgs e)
        {
            //貼り付け無効
            if (e.Command == ApplicationCommands.Paste)
            {
                e.Handled = true;
            }
        }

        //focusしたときにテキストを全選択
        private void MyTextBox_GotFocus(object sender, RoutedEventArgs e)
        {
            var tb = sender as TextBox;
            tb.SelectAll();
        }

        //        | オールトの雲
        //http://ooltcloud.sakura.ne.jp/blog/201311/article_30013700.html
        //クリックしたときにテキストを全選択
        private void MyTextBox_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            var tb = sender as TextBox;
            if (tb.IsFocused == false)
            {
                tb.Focus();
                e.Handled = true;
            }
        }
        #endregion 入力制限


        #region 依存関係プロパティ

        //要の値
        public decimal MyValue
        {
            get { return (decimal)GetValue(MyValueProperty); }
            set { SetValue(MyValueProperty, value); }
        }

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

        //MyValueの変更直後の動作
        private static void OnMyValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            //することない
        }

        //MyValueの変更直前の動作、値の検証、矛盾があれば値を書き換えて解消
        //入力された値が下限値より小さい場合は下限値に書き換え
        //入力された値が上限値より大きい場合は上限値に書き換え
        private static object CoerceMyValue(DependencyObject d, object basaValue)
        {
            var ud = (NumericUpDown)d;
            var m = (decimal)basaValue;
            if (m < ud.MyMinValue) m = ud.MyMinValue;
            if (m > ud.MyMaxValue) m = ud.MyMaxValue;
            return m;
        }



        //小変更値
        public decimal MySmallChange
        {
            get { return (decimal)GetValue(MySmallChangeProperty); }
            set { SetValue(MySmallChangeProperty, value); }
        }
        public static readonly DependencyProperty MySmallChangeProperty =
            DependencyProperty.Register(nameof(MySmallChange), typeof(decimal), typeof(NumericUpDown), new PropertyMetadata(1m));


        //大変更値
        public decimal MyLargeChange
        {
            get { return (decimal)GetValue(MyLargeChangeProperty); }
            set { SetValue(MyLargeChangeProperty, value); }
        }
        public static readonly DependencyProperty MyLargeChangeProperty =
            DependencyProperty.Register(nameof(MyLargeChange), typeof(decimal), typeof(NumericUpDown), new PropertyMetadata(10m));



        //下限値
        public decimal MyMinValue
        {
            get { return (decimal)GetValue(MyMinValueProperty); }
            set { SetValue(MyMinValueProperty, value); }
        }
        public static readonly DependencyProperty MyMinValueProperty =
            DependencyProperty.Register(nameof(MyMinValue), typeof(decimal), typeof(NumericUpDown),
                new PropertyMetadata(decimal.MinValue, OnMyMinValuePropertyChanged, CoerceMyMinValue));

        //PropertyChangedコールバック、プロパティ値変更"直後"に実行される
        //変更された下限値と今の値での矛盾を解消
        //変更された新しい下限値と、今の値(MyValue)で矛盾が生じた(下限値 < 今の値)場合は、今の値を下限値に変更する
        private static void OnMyMinValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var ud = (NumericUpDown)d;
            var min = (decimal)e.NewValue;//変更後の新しい下限値
            if (min > ud.MyValue) ud.MyValue = min;
        }

        //値の検証と変更
        //CoerceValueコールバック、プロパティ値変更"直前"に実行される
        //設定された値を強制(Coerce)的に変更できるので、矛盾があれば変更して解消する
        //入力された下限値と、今の上限値で矛盾が生じる(下限値 > 上限値)場合は、下限値を上限値に書き換える
        private static object CoerceMyMinValue(DependencyObject d, object baseValue)
        {
            var ud = (NumericUpDown)d;
            var min = (decimal)baseValue;//入力された下限値
            if (min > ud.MyMaxValue) min = ud.MyMaxValue;
            return min;
        }


        //上限値
        public decimal MyMaxValue
        {
            get { return (decimal)GetValue(MyMaxValueProperty); }
            set { SetValue(MyMaxValueProperty, value); }
        }
        public static readonly DependencyProperty MyMaxValueProperty =
            DependencyProperty.Register(nameof(MyMaxValue), typeof(decimal), typeof(NumericUpDown),
                new PropertyMetadata(decimal.MaxValue, OnMyMaxValuePropertyChanged, CoerceMyMaxValue));

        //上限値の変更直後の動作。上限値より今の値が大きい場合は、今の値を上限値に変更する
        private static void OnMyMaxValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var ud = (NumericUpDown)d;
            var max = (decimal)e.NewValue;
            if (max < ud.MyValue) ud.MyValue = max;
        }

        //上限値変更直前の動作。入力された上限値が今の下限値より小さくなる場合は、上限値を下限値に書き換える
        private static object CoerceMyMaxValue(DependencyObject d, object baseValue)
        {
            var ud = (NumericUpDown)d;
            var max = (decimal)baseValue;
            if (max < ud.MyMinValue) max = ud.MyMinValue;
            return max;
        }



        #endregion 依存関係プロパティ



        private void RepeatButtonUp_Click(object sender, RoutedEventArgs e)
        {
            MyValue += MySmallChange;
        }

        private void RepeatButtonDown_Click(object sender, RoutedEventArgs e)
        {
            MyValue -= MySmallChange;
        }

        private void RepeatButton_MouseWheel(object sender, MouseWheelEventArgs e)
        {
            if (e.Delta < 0) MyValue -= MyLargeChange;
            else MyValue += MyLargeChange;
        }

        private void MyTextBox_MouseWheel(object sender, MouseWheelEventArgs e)
        {
            if (e.Delta < 0) MyValue -= MySmallChange;
            else MyValue += MySmallChange;
        }
    }
}


動作確認用アプリの
MainWindow.xaml

<Window x:Class="UserControlDll動作確認用.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:UserControlDll動作確認用"
        xmlns:myControl="clr-namespace:ControlLibraryCore20200620;assembly=ControlLibraryCore20200620"
        mc:Ignorable="d"
        Title="動作確認用MainWindow" Height="327" Width="414">
  <Grid>
    <Viewbox>
      <StackPanel Margin="10">
        <TextBlock Text="WPFにもNumericUpDownをユーザーコントロールで、その5"/>
        <myControl:NumericUpDown x:Name="nume" Margin="10"
                                 MyValue="5" MyMinValue="-10" MyMaxValue="20"/>
        <StackPanel Orientation="Horizontal">
          <TextBlock Text="今の下限値 = "/>
          <TextBlock Text="{Binding ElementName=nume, Path=MyMinValue}"/>
        </StackPanel>
        <StackPanel Orientation="Horizontal">
          <TextBlock Text="今の上限値 = "/>
          <TextBlock Text="{Binding ElementName=nume, Path=MyMaxValue}"/>
        </StackPanel>
        <Button Content="リセット(値:5、下限値:-10、上限値:20)" Click="Button_Click"/>
        <Button Content="Set 下限値 = 10" Tag="10" Click="ButtonMin_Click"/>
        <Button Content="Set 上限値 = 2" Tag="2" Click="ButtonMax_Click"/>
        <Button Content="Set 下限値 = 30" Tag="30" Click="ButtonMin_Click"/>
        <Button Content="Set 上限値 = -20" Tag="-20" Click="ButtonMax_Click"/>
        <DockPanel LastChildFill="True" Background="Lavender">
          <TextBlock Text="{Binding ElementName=nume, Path=MyValue}" Margin="10" Width="30"
                     TextAlignment="Right" Background="White"/>
          <Slider Value="{Binding ElementName=nume, Path=MyValue, Mode=TwoWay}" Margin="10"
                Minimum="-50" Maximum="50"/>
        </DockPanel>
      </StackPanel>
    </Viewbox>
  </Grid>
</Window>


MainWindow.xaml.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace UserControlDll動作確認用
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            nume.MyMinValue = -10;
            nume.MyMaxValue = 20;
            nume.MyValue = 5;
        }


        private void ButtonMin_Click(object sender, RoutedEventArgs e)
        {
            var b = (Button)sender;
            nume.MyMinValue = decimal.Parse(b.Tag.ToString());
        }

        private void ButtonMax_Click(object sender, RoutedEventArgs e)
        {
            var b = (Button)sender;
            nume.MyMaxValue = decimal.Parse(b.Tag.ToString());
        }
    }
}

できた!

f:id:gogowaten:20200624163016p:plain
下限値、上限値を設定できるようにすると、いろいろ矛盾が出てくる場合があるので、矛盾解消のルールを決める必要がある
ルールは

  • 限度値と今の値に矛盾が生じる場合は、今の値を変更
  • 限度値どうしの矛盾は、変更されようとしている方を変更

ってことにした

下限値の依存関係プロパティ
UserControl.xaml.cs
f:id:gogowaten:20200624164542p:plain
完成形がこれ、175~182行目は、昨日まで書いていたのと同じような依存関係プロパティだけど、182行目のPropertyMetadataの引数が増えている

f:id:gogowaten:20200624165356p:plain
第1引数は既定値にする値、これにはdecimal型の最小値を指定した。昨日までも第1引数はあった

f:id:gogowaten:20200624165505p:plain
第2引数はPropertyChangedCallbackの指定、プロパティ値が変更された直後になにかの処理をしたいとき使う

f:id:gogowaten:20200624170215p:plain
第3引数はCoerceValueCallbackの指定、プロパティ値が変更される直前に実行される。変更されようとしている値を強制的に変更したいときに使うみたい。Coerce:強制

依存関係プロパティの値変更時になにかの処理をしたいときは、set{}のところに書くんじゃなくて、DependencyProperty.RegisterのPropertyMetadataの引数にコールバックを渡して行うみたいねえ
といってもコールバックが何なのかよくわからん、見た感じだとメソッドや関数みたいなのもの

2つのコールバック

  • PropertyChangedCallback
  • CoerceValueCallback

どちらもプロパティ値の変更(set)時に実行されるけど

  • 実行される順番はCoerceValueCallbackが先
  • CoerceValueCallback
    • 必ず実行される
    • 実行タイミングは変更直前
    • 入力された値を変更できる
  • PropertyChangedCallback
    • 新しい値が古い値と同じだった場合は実行されない
    • 実行タイミングは変更直後

動きを見ていたら、こんな感じだった

CoerceValueCallbackでの処理

  • 限度値どうしの矛盾は、変更されようとしている方を変更

ってルールを決めておいたので、これは値が変更される前、入力されたときに実行したいのでCoerceValueCallbackに書いた、198行目~
f:id:gogowaten:20200624174158p:plain
CoerceValueCallbackの引数は決まっているみたい?で、DependencyObjectとobjectの2つ。
DependencyObjectには元の依存関係プロパティのownerTypeが入っていて、ここではNumericUpDown、もう一つのobjectには、入力された値が入っている。この2つを使って色々処理することができて、最後に値を返す。この返された値がプロパティ値に適用されるみたい
ここでは入力された下限値が上限値より大きな値だった場合は、入力された値を変更して、その値を返している。
上限値20のときに、下限値として30が入力されたときは、30を20にして返す

PropertyChangedCallbackでの処理

  • 限度値と今の値に矛盾が生じる場合は、今の値を変更

ってルールでは決めておいた。下限値(限度値)を変更する必要はないのでので、これはPropertyChangedCallbackで処理するとこにした

f:id:gogowaten:20200624180122p:plain
PropertyChangedCallbackも引数は決まっているみたいで、2つ
DependencyはCoerceValueCallbackと同じように、ここではNumericUpDownで
DependencyPropertyChangedEventArgsは変更イベント時のいろいろな値が入ったもの
これらを使って新しい下限値が今の値より大きい場合は、今の値を変更

これで2つのコールバックができた、さっきの依存関係プロパティをもう一度見てみると
f:id:gogowaten:20200624181924p:plain
182行目のPropertyMetadataに渡して使っている、これでプロパティ値変更時にそれぞれ実行される
わからんのが、コールバックにはどちらにも引数が2つあるのに、PropertyMetadataで指定するときには、それを書いていないこと。staticってのが関係しているのかなあ

上限値の依存関係プロパティは
f:id:gogowaten:20200625002035p:plain
ほとんど下限値のときと同じ、ナニモイウコトハナイ

今の値を保持している依存関係プロパティのMyValueにもコールバックを追加、完成形はこれ
f:id:gogowaten:20200624221820p:plain
追加したのは133~149行目、伴って変更したのが131行目
目的はMyValueが下限値と上限値を超えないように、もし超える値が入力されたら範囲内に変更すること、なので処理するタイミングは変更直前、ってことでCoerceValueCallbackに書くことになる、これが142行目~で、あとは131行目のPropertyMetadataの引数に、既定値とCoerceValueCallbackの2つだけを渡したいんだけど、この組み合わせのオーバーロードがない!ので、134行目になにもしないPropertyChangedCallbackを用意して渡している
なんか変な感じするけど、これで動いてる

UserControl.xamlの方は昨日から変更なし

次は動作確認用アプリのほう
MainWindow.xaml
f:id:gogowaten:20200624231306p:plain
色々書き換えた、NumericUpDownの値をリセットするボタンと、下限値、上限値を指定するボタン4つ

MainWindow.xaml.cs
f:id:gogowaten:20200624231606p:plain
ボタンクリックイベント時の処理



参照したところ
WPF4.5入門 その42 「WPFのプロパティシステム」 - かずきのBlog@hatena

blog.okazuki.jp
依存関係プロパティのコールバックと検証 - WPF | Microsoft Docs

docs.microsoft.com

DepdencyProperty の CoerceValueCallback が Binding ソースに反映されない

var.blog.jp



関連記事
3週間後、その7で数値の書式設定できるようになって完成?
gogowaten.hatenablog.com

次のWPF記事は明後日
gogowaten.hatenablog.com

前回は昨日
gogowaten.hatenablog.com

その1は4日前
gogowaten.hatenablog.com