午後わてんのブログ

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

イベントの追加、ユーザーコントロールの依存関係プロパティの変更時にイベント発生させたい

 
ユーザーコントロールのDependencyPropertyが変更された時にイベントを発生させたい
昨日のIntegerUpDownコントロールValueプロパティ、これが変更された時に何かの処理をしたくて、それにはイベントを追加するんだろうなとググって
外観をカスタマイズできるコントロールの作成 | Microsoft Docs
https://docs.microsoft.com/ja-jp/dotnet/framework/wpf/controls/creating-a-control-that-has-a-customizable-appearance
ここみて、コピペでなんとかできた
 
 
 

f:id:gogowaten:20191212141045p:plain

デザイン画面で追加したユーザーコントロールの名前をupDownにしたところが
12行目
 

f:id:gogowaten:20191212141056p:plain

ユーザーコントロールを使う側のコードの方はこんな感じで29行目が
upDown.MyValueChanged += UpDown_MyValueChanged;
MyValueChangedが追加したイベント

upDownのMyValueChangedイベントを使って、イベント発生時にメッセージボックスを表示するようにしているのが
32行目からの
 
private void UpDown_MyValueChanged(object sender, IntegerUpDown.MyValueChangedEventArgs e)
{
string str = $"Valueプロパティが変更されて{e.RoutedEvent.Name}イベント発生" + "\n";
MessageBox.Show(str + $"変更前の値は{e.MyOldValue}、新しい値は{e.MyNewValue}");
}
 
これを実行して
イメージ 1
普通に表示されてValueは70、変更すると
 
イメージ 4
メッセージボックスが表示される、閉じると
 
イメージ 5
変更された値に変化
 
ボタンのクリックイベントみたいに使いたかったわけね、クリックイベントが発生したら何かの処理をするみたいなの


今回はコピペでなんとかできたんだけど、このイベントを追加するってのがいつにも増してわからん
追加したイベントは名前をMyValueChangedにして

f:id:gogowaten:20191212141121p:plain

前回から書き加えたのがこれ
119行目から164行目までと355から386行目
イベントの効果は
DependencyPropertyのValueが変更された時に発生して、変更前後の値を渡してくれるっていうもの
 
前回は今の117行目が
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(nameof(Value), typeof(int), typeof(IntegerUpDown));
 
こうだったのが今回は末尾に付け足して
 
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(nameof(Value), typeof(int), typeof(IntegerUpDown),
new PropertyMetadata(new PropertyChangedCallback(MyValueChangedCallback)));
こう
ここまでの準備が長い…
 
まずは
MyValueChangedEventArgsクラス
(今回も自分で名前をつけるところには、目印になるよう頭にMyをつけた)
イメージ 7
RoutedEventArgsクラスを継承したクラスが必要みたいなので作成
 
 
今見ていてこれはこうでもいいんじゃないかと
イメージ 8
書き換えてみて動かしたら普通に動いた、こっちのが短くて良さそう
 
イメージ 9
RoutedEventArgsってのはイベントのクラスなんだなあと
 
 
 
 
144行目のOnMyValueChanged
イメージ 17
これはさっき作ったクラスが引数になっていて
それでイベントを発生させているだけかな
 
デリゲートの宣言?

f:id:gogowaten:20191212141147p:plain

デリゲートは未だにわからん、Func<>に似ているらしい
MyValueChangedEventHandlerって名前にして、引数はobject型とさっき作ったクラスのMyValueChangedEventArgs
 
 
これが使われているのは2箇所

f:id:gogowaten:20191212141202p:plain

149と157行目
149行目のMyValueChangedがイベント自体?
この中でイベントをつけたり外したりしているみたい

f:id:gogowaten:20191212141217p:plain

ユーザーコントロールを使う側でコードを書く時に表示されるのはこれになる
イメージ 12
こんなふう、イベントを表す雷マーク
 
 
155行目が…なんだろうこれRoutedEventあるからこれもイベントっぽいけど

f:id:gogowaten:20191212141230p:plain

うーん

f:id:gogowaten:20191212141248p:plain

わからん
けどこれでだいたい必要なのが揃って、これらを使うのが
159行目

f:id:gogowaten:20191212141302p:plain

DependencyProperty変更時に呼んで使うMyValueChangedCallback
引数からコントロール自体と変更前後の値を得られるみたいで
それを使ってさっき作ったクラスのMyValueChangedEventArgsをnewで作って
それを使ってOnMyValueChangedに渡してイベントを発生させている、のかな
 
最後に

f:id:gogowaten:20191212141313p:plain

DependencyPropertyのValueプロパティ
これの変更時にイベント発生させるのが目的だったのでここに
MyValueChangedCallbackを実行させるためにこうなった
119行目、最後の引数のPropertyMetadataの引数の
PropertyChangedCallbackの引数に
MyValueChangedCallbackを渡している
 
これで完成、ボタンコントロールのクリックイベントみたいに使えるようになった!
 
 
 
ユーザーコントロールを使う側

f:id:gogowaten:20191212141328p:plain

upDownがユーザーコントロール
29行目でMyValueChangedイベント発生時に
32行目からのupDown_MyValueChanged(メッセージボックス表示)を実行
するように書いておいた場合
アプリを起動すると
29行目でupDownにイベントが登録されるので
 
 
ユーザーコントロール側の
イメージ 20
149行目のMyValueChangedに行って151行目のaddで登録処理される
 
これでアプリ起動完了で
イメージ 21
表示されて
値を変更すると

f:id:gogowaten:20191212141350p:plain

159行目のMyValueChangedCallbackが呼ばれて
順番に処理されて

f:id:gogowaten:20191212141404p:plain

newValue、oldValue取得したところ
続いて
164行目でMyValueChangedEventArgsクラスを作成するので355行目
イメージ 24
順番に処理されて
イメージ 25
作成されたら
164行目に戻って
イメージ 26
次はOnMyValueChanged、144行目
イメージ 27
146行目でイベント発生!
 
 
ユーザーコントロールを使う側
イベント発生したので

f:id:gogowaten:20191212141431p:plain

イベント発生時の処理が実行される、34行目
 
イメージ 29
メッセージボックスが表示された!
これでDependencyPropertyの変更時にイベント発生させるってのはできたけど
難しすぎる、理解できる気がしない
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;
using System.Text.RegularExpressions;
using System.Windows.Controls.Primitives;
using System.Globalization;

namespace IntegerUpDown
{
    /// <summary>
    /// IntegerUpDown.xaml の相互作用ロジック
    /// fasdf
    /// </summary>
    public partial class IntegerUpDown : UserControl
    {

        public int Max
        {
            get { return (int)GetValue(MaxProperty); }
            set { SetValue(MaxProperty, value); }
        }
        public static readonly DependencyProperty MaxProperty =
            DependencyProperty.Register(nameof(Max), typeof(int), typeof(IntegerUpDown));
        public int Min
        {
            get { return (int)GetValue(MinProperty); }
            set { SetValue(MinProperty, value); }
        }
        public static readonly DependencyProperty MinProperty =
            DependencyProperty.Register(nameof(Min), typeof(int), typeof(IntegerUpDown));
        public int SmallChange
        {
            get { return (int)GetValue(SmallChangeProperty); }
            set { SetValue(SmallChangeProperty, value); }
        }
        public static readonly DependencyProperty SmallChangeProperty =
            DependencyProperty.Register(nameof(SmallChange), typeof(int), typeof(IntegerUpDown));

        public int LargeChange
        {
            get { return (int)GetValue(LargeChangeProperty); }
            set { SetValue(LargeChangeProperty, value); }
        }
        public static readonly DependencyProperty LargeChangeProperty =
            DependencyProperty.Register(nameof(LargeChange), typeof(int), typeof(IntegerUpDown));

        public double IntegerFontSize
        {
            get { return (double)GetValue(IntegerFontSizeProperty); }
            set { SetValue(IntegerFontSizeProperty, value); }
        }
        public static readonly DependencyProperty IntegerFontSizeProperty =
            DependencyProperty.Register(nameof(IntegerFontSize), typeof(double), typeof(IntegerUpDown));



        // 外観をカスタマイズできるコントロールの作成 | Microsoft Docs
        //https://docs.microsoft.com/ja-jp/dotnet/framework/wpf/controls/creating-a-control-that-has-a-customizable-appearance

        public int Value
        {
            get { return (int)GetValue(ValueProperty); }
            set { SetValue(ValueProperty, value); }
        }
        
        public static readonly DependencyProperty ValueProperty =
            DependencyProperty.Register(nameof(Value), typeof(int), typeof(IntegerUpDown),
                new PropertyMetadata(new PropertyChangedCallback(MyValueChangedCallback)));

        public delegate void MyValueChangedEventHndler(object sender, MyValueChangedEventArgs e);

        protected virtual void OnMyValueChanged(MyValueChangedEventArgs e)
        {
            RaiseEvent(e);
        }

        public event MyValueChangedEventHndler MyValueChanged
        {
            add { AddHandler(MyValueChangedEvent, value); }
            remove { RemoveHandler(MyValueChangedEvent, value); }
        }

        public static readonly RoutedEvent MyValueChangedEvent =
            EventManager.RegisterRoutedEvent(nameof(MyValueChanged),
                RoutingStrategy.Bubble, typeof(MyValueChangedEventHndler), typeof(IntegerUpDown));

        private static void MyValueChangedCallback(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            var ctl = (IntegerUpDown)obj;
            int newValue = (int)args.NewValue;
            int oldValue = (int)args.OldValue;
            ctl.OnMyValueChanged(new MyValueChangedEventArgs(MyValueChangedEvent, newValue, oldValue));
        }



        public IntegerUpDown()
        {
            InitializeComponent();
            SmallChange = 1;//ここで指定するとデフォルトの数値にできる、ユーザーコントロールを使う側のXAMLから指定されていた場合はそちらが適用される
            LargeChange = 1;
            //Height = 30;
            Width = 50;
            Max = 10;
            Min = 0;
            Value = 0;
            IntegerFontSize = 14f;

            MyInitialize();
            this.Loaded += IntegerUpDown_Loaded;//レイアウト用、起動しないと初期サイズがわからないので
        }

        private void IntegerUpDown_Loaded(object sender, RoutedEventArgs e)
        {//テキストボックスの横幅は全体の横幅-スクロールバーの横幅
            nTextBox.Width = Width - nScrollBar.Width;
        }

        private void MyInitialize()
        {
            nScrollBar.RenderTransform = new RotateTransform(180);
            nScrollBar.RenderTransformOrigin = new Point(0.5, 0.5);

            nTextBox.TextAlignment = TextAlignment.Right;
            nTextBox.VerticalContentAlignment = VerticalAlignment.Center;

            nStackPanel.Height = Height;

            MySetBinding();
            MySetEvents();

        }



       #region バインディング関連

        private void MySetBinding()
        {
            var b = new Binding();
            b.Source = this;
            b.Path = new PropertyPath(IntegerUpDown.ValueProperty);
            b.Converter = new ConverterInteger2Double();
            nScrollBar.SetBinding(ScrollBar.ValueProperty, b);

            b = new Binding();
            b.Source = nScrollBar;
            b.Path = new PropertyPath(ScrollBar.ValueProperty);
            b.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
            nTextBox.SetBinding(TextBox.TextProperty, b);

            b = new Binding();
            b.Source = this;
            b.Path = new PropertyPath(MaxProperty);
            b.Converter = new ConverterInteger2Double();
            nScrollBar.SetBinding(ScrollBar.MaximumProperty, b);

            b = new Binding();
            b.Source = this;
            b.Path = new PropertyPath(MinProperty);
            b.Converter = new ConverterInteger2Double();
            nScrollBar.SetBinding(ScrollBar.MinimumProperty, b);

            b = new Binding();
            b.Source = this;
            b.Path = new PropertyPath(SmallChangeProperty);
            b.Mode = BindingMode.OneWay;
            nScrollBar.SetBinding(ScrollBar.SmallChangeProperty, b);

            b = new Binding();
            b.Source = this;
            b.Path = new PropertyPath(LargeChangeProperty);
            b.Mode = BindingMode.OneWay;
            nScrollBar.SetBinding(ScrollBar.LargeChangeProperty, b);

            b = new Binding();
            b.Source = this;
            b.Path = new PropertyPath(IntegerFontSizeProperty);
            b.Mode = BindingMode.OneWay;
            nTextBox.SetBinding(TextBox.FontSizeProperty, b);

        }
       #endregion


       #region イベント関連
        private void MySetEvents()
        {
            nTextBox.TextChanged += NTextBox_TextChanged;
            nTextBox.LostFocus += NTextBox_LostFocus;
            nTextBox.GotFocus += NTextBox_GotFocus;
            nTextBox.MouseWheel += NTextBox_MouseWheel;

            nScrollBar.MouseWheel += NScrollBar_MouseWheel;
        }

        private void NTextBox_MouseWheel(object sender, MouseWheelEventArgs e)
        {
            if (e.Delta > 0) { nScrollBar.Value += SmallChange; }
            else { nScrollBar.Value -= SmallChange; }
        }

        //スクロールバーの上でマウスホイール回転でLargeChange分の数値を上下
        private void NScrollBar_MouseWheel(object sender, MouseWheelEventArgs e)
        {
            if (e.Delta > 0) { nScrollBar.Value += LargeChange; }
            else { nScrollBar.Value -= LargeChange; }
        }

        //テキストボックスクリック時に文字全体を選択状態にする
        private void NTextBox_GotFocus(object sender, RoutedEventArgs e)
        {
            TextBox box = (TextBox)sender;
            this.Dispatcher.InvokeAsync(() =>
            {
                Task.Delay(10);
                box.SelectAll();
            });
        }

        //テキストボックスのロストフォーカス時
        //にテキストをValueに入れる
        //これがないとValueに範囲外の数値が入ったりする
        private void NTextBox_LostFocus(object sender, RoutedEventArgs e)
        {
            TextBox box = (TextBox)sender;
            int i;
            if (int.TryParse(box.Text, out i) == true)
            {
                Value = i;
            }
            else
            {
                Value = Min;
                box.Text = Min.ToString();
            }
            //Value = int.Parse(box.Text);
        }

        //数値以外は入力できないように
        private void NTextBox_TextChanged(object sender, TextChangedEventArgs e)
        {
            TextBox box = (TextBox)sender;
            double d;
            if (!double.TryParse(box.Text, out d))
            {
                box.Text = Regex.Replace(box.Text, "[^0-9-]", "");//数値以外を""に置換
            }
        }
       #endregion
    }

    internal class ConverterInteger2Double : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            int i = (int)value;
            return (double)i;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            double d = (double)value;
            return (int)d;
        }
    }


    // 外観をカスタマイズできるコントロールの作成 | Microsoft Docs
    //https://docs.microsoft.com/ja-jp/dotnet/framework/wpf/controls/creating-a-control-that-has-a-customizable-appearance
    //Valueが変更された時にイベント発生に必要
    public delegate void ValueChangedEventHandler(object sender, ValueChangedEventArgs e);
    public class ValueChangedEventArgs : RoutedEventArgs
    {
        private int _value;
        public ValueChangedEventArgs(RoutedEvent id, int num)
        {
            _value = num;
            RoutedEvent = id;
        }
        public int Value { get { return _value; } }
    }
    //↑を改変して、変更前後2つの数値を渡せるようにしたもの
    public class MyValueChangedEventArgs : RoutedEventArgs
    {
        public MyValueChangedEventArgs(RoutedEvent id, int newValue, int oldValue)
        {
            MyNewValue = newValue;
            MyOldValue = oldValue;
            RoutedEvent = id;
        }
        public int MyNewValue { get; }
        public int MyOldValue { get; }
    }
}
 
 
 
太字のところが昨日から追加変更したところ
デザイン画面のXAMLは変更なし
 
 
 
関連記事
2年後 
 
 
2018/04/03は昨日
WindowsFormのNumericUpDownみたいなのをWPFのユーザーコントロールでDLLで作ってみた ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15442773.html