午後わてんのブログ

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

WPF、マウスドラッグ移動で要素のサイズ変更、サイズ変更ハンドル8個はThumbで作成して対象にバインド

結果

動作の様子

Animation20220606_104457.gif
四角形のRectangleと楕円のEllipseをThumbで作ったサイズ変更ハンドルで動作確認
2つのcheckボタンはRectangleのサイズ変更と位置変更で、ハンドルが追随するかのチェック
対象にできるのはFrameworkElementなので、ボタンやテキストボックスとかもできるはず


コード

2022WPF/20220605_SizeChangeThumb/20220605_SizeChangeThumb at master · gogowaten/2022WPF

github.com

デザイン画面

MainWindow.xaml

<Window x:Class="_20220605_SizeChangeThumb.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:_20220605_SizeChangeThumb"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="600">
  <Grid UseLayoutRounding="True">
    <Canvas Name="MyCanvas">
      <Canvas.Resources>
        <Style TargetType="Thumb">
          <Setter Property="Width" Value="20"/>
          <Setter Property="Height" Value="20"/>
          <Setter Property="Opacity" Value="0.2"/>
          <Setter Property="Background" Value="Black"/>
        </Style>
      </Canvas.Resources>
      <Rectangle Name="MyRectangle" Fill="RoyalBlue" MouseDown="MyRectangle_MouseDown"
                 Width="150" Height="150" Canvas.Left="150" Canvas.Top="150"/>
      <Ellipse x:Name="MyEllipse" Fill="Gold" MouseDown="MyEllipse_MouseDown"
               Width="150" Height="150" Canvas.Left="250" Canvas.Top="200"/>
      
      <Button x:Name="MyButton1" Content="check" Click="MyButton1_Click"/>
      <Button x:Name="MyButton2" Content="check" Click="MyButton2_Click" 
              Canvas.Top="20" Canvas.Left="100" Width="100" Height="20"/>
      
      <Thumb Name="MyThumb0" DragDelta="MyThumb_DragDelta"/>
      <Thumb Name="MyThumb1" DragDelta="MyThumb_DragDeltaOnlyVertical"/>
      <Thumb Name="MyThumb2" DragDelta="MyThumb_DragDelta"/>
      <Thumb Name="MyThumb3" DragDelta="MyThumb_DragDeltaOnlyHorizontal"/>
      <Thumb Name="MyThumb4" DragDelta="MyThumb_DragDeltaOnlyHorizontal"/>
      <Thumb Name="MyThumb5" DragDelta="MyThumb_DragDelta"/>
      <Thumb Name="MyThumb6" DragDelta="MyThumb_DragDeltaOnlyVertical"/>
      <Thumb Name="MyThumb7" DragDelta="MyThumb_DragDelta"/>

    </Canvas>
  </Grid>
</Window>


MainWindow.xaml.cs

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Controls.Primitives;
using System.Globalization;

//対象の要素に直接Thumbをバインド
//Thumbの配置番号
//0 1 2
//3   4
//5 6 7
namespace _20220605_SizeChangeThumb
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            Test1(MyRectangle);
            //Test1(MyButton2);
        }
        //対象の要素にThumbをバインド
        private void Test1(FrameworkElement element)
        {
            Binding b0 = MakeBinding(element, LeftProperty);
            Binding b1 = MakeBinding(element, WidthProperty);
            Binding b2 = MakeBinding(element, TopProperty);
            Binding b3 = MakeBinding(element, HeightProperty);

            MultiBinding m0 = MakeMultiBinding(new MMM0(), element, b0, b1);
            MultiBinding m1 = MakeMultiBinding(new MMM1(), element, b2, b3);
            MultiBinding m2 = MakeMultiBinding(new MMM2(), element, b0, b1);
            MultiBinding m3 = MakeMultiBinding(new MMM3(), element, b0, b1);
            MultiBinding m4 = MakeMultiBinding(new MMM2(), element, b2, b3);
            MultiBinding m5 = MakeMultiBinding(new MMM4(), element, b2, b3);

            MyThumb0.SetBinding(LeftProperty, m0);
            MyThumb0.SetBinding(TopProperty, m1);

            MyThumb1.SetBinding(LeftProperty, m2);
            MyThumb1.SetBinding(TopProperty, m1);

            MyThumb2.SetBinding(LeftProperty, m3);
            MyThumb2.SetBinding(TopProperty, m1);

            MyThumb3.SetBinding(LeftProperty, m0);
            MyThumb3.SetBinding(TopProperty, m4);

            MyThumb4.SetBinding(LeftProperty, m3);
            MyThumb4.SetBinding(TopProperty, m4);

            MyThumb5.SetBinding(LeftProperty, m0);
            MyThumb5.SetBinding(TopProperty, m5);

            MyThumb6.SetBinding(LeftProperty, m2);
            MyThumb6.SetBinding(TopProperty, m5);

            MyThumb7.SetBinding(LeftProperty, m3);
            MyThumb7.SetBinding(TopProperty, m5);
        }
        private Binding MakeBinding(FrameworkElement source, DependencyProperty dp)
        {
            Binding b = new()
            {
                Source = source,
                Path = new PropertyPath(dp),
                Mode = BindingMode.TwoWay
            };
            return b;
        }
        private MultiBinding MakeMultiBinding(
            IMultiValueConverter converter, object? param = null, params Binding[] bindings)
        {
            MultiBinding m = new()
            {
                ConverterParameter = param,
                Converter = converter,
                Mode = BindingMode.TwoWay
            };
            foreach (var item in bindings)
            {
                m.Bindings.Add(item);
            }
            return m;
        }


        #region ドラッグ移動        
        private void MyThumb_DragDelta(object sender, DragDeltaEventArgs e)
        {
            if (sender is not Thumb t) { return; }
            Canvas.SetLeft(t, Canvas.GetLeft(t) + e.HorizontalChange);
            Canvas.SetTop(t, Canvas.GetTop(t) + e.VerticalChange);
        }
        private void MyThumb_DragDeltaOnlyVertical(object sender, DragDeltaEventArgs e)
        {
            if (sender is not Thumb t) { return; }
            Canvas.SetTop(t, Canvas.GetTop(t) + e.VerticalChange);
        }
        private void MyThumb_DragDeltaOnlyHorizontal(object sender, DragDeltaEventArgs e)
        {
            if (sender is not Thumb t) { return; }
            Canvas.SetLeft(t, Canvas.GetLeft(t) + e.HorizontalChange);
        }
        #endregion ドラッグ移動
        #region 動作チェック
        private void MyButton1_Click(object sender, RoutedEventArgs e)
        {
            MyRectangle.Width += 10;
        }

        private void MyButton2_Click(object sender, RoutedEventArgs e)
        {
            Canvas.SetLeft(MyRectangle, Canvas.GetLeft(MyRectangle) + 10);
        }


        private void MyRectangle_MouseDown(object sender, MouseButtonEventArgs e)
        {
            Test1((FrameworkElement)sender);
        }

        private void MyEllipse_MouseDown(object sender, MouseButtonEventArgs e)
        {
            Test1((FrameworkElement)sender);
        }
        #endregion 動作チェック

    }

    #region コンバーター
    public class MMM0 : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            return (double)values[0];
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            FrameworkElement element = (FrameworkElement)parameter;
            double total = element.Width + Canvas.GetLeft(element);
            double left = (double)value;
            double width = total - left;

            object[] result = new object[2];
            result[0] = left;
            result[1] = width;
            //サイズが1未満にならないように調整
            if (width < 1.0)
            {
                result[0] = total - 1.0;
                result[1] = 1.0;
            }
            return result;
        }
    }
    public class MMM1 : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            return (double)values[0];
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            FrameworkElement element = (FrameworkElement)parameter;
            double total = element.Height + Canvas.GetTop(element);
            double top = (double)value;
            double height = total - top;

            object[] result = new object[2];
            result[0] = top;
            result[1] = height;
            if (height < 1.0)
            {
                result[0] = total - 1.0;
                result[1] = 1.0;
            }
            return result;
        }
    }

    public class MMM2 : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            return (double)values[0] + (double)values[1] / 2.0;
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
    public class MMM3 : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            return (double)values[0] + (double)values[1];
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            FrameworkElement element = (FrameworkElement)parameter;
            double left = (double)Canvas.GetLeft(element);
            double width = (double)value - left;

            object[] result = new object[2];
            result[0] = left;
            result[1] = width;
            if (width < 1.0) { result[1] = 1.0; }
            return result;
        }
    }
    public class MMM4 : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            return (double)values[0] + (double)values[1];
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            FrameworkElement element = (FrameworkElement)parameter;
            double top = (double)Canvas.GetTop(element);
            double height = (double)value - top;

            object[] result = new object[2];
            result[0] = top;
            result[1] = height;
            if (height < 1.0) { result[1] = 1.0; }
            return result;
        }
    }
    #endregion コンバーター
}


サイズ変更ハンドルはThumbコントロール
MainWindow.xaml
8つのサイズ変更ハンドル
サイズ変更ハンドルはThumbを使った、これはXAMLのほうで予め作成

移動制限
1番と6番は上下移動だけ、3番と4番は左右移動だけ、それ以外は上下左右移動させたい、3種類のドラッグ移動イベントは

ドラッグ移動イベント


対象要素の位置とサイズを、Thumbの位置と双方向バインド

期待する動作は

  1. Thumbを移動させたとき、対象要素のサイズ変更+場合によっては位置も変更される
  2. 対象要素のサイズが変更されたとき、Thumbの位置が変更される

1番の場合ってのは、左と上要素のThumbを移動させた場合で、Thumb番号だと0 1 2 3 5

Thumb番号
要素の位置は左上が基本になるので、そこが変化する0 1 2 3 5番の移動は、サイズ変更に加えて位置変更も必要になる


左上Thumbを右に1移動
対象要素の位置も(1,1)から(2,1)に変更する必要がある


今回は対象要素をBindingソースにしたので、位置とサイズそれぞれのBindingを作成

Binding作成
位置はLeftPropertyとTopPropertyの2つ
サイズはWidthPropertyとHeightPropertyの2つで
対象要素側は合計4つのBinding作成

対するThumbは位置だけなので2つ

対象要素のLeftとWidth ↔ ThumbのLeft
対象要素のTopとHeight ↔ ThumbのTop
一対一のBindingじゃなくて、多対一なのでMultiBindingを使った

MultiBinding
Thumbの位置変更から、対象要素のサイズと位置を求めるには計算が必要なのでConverterを用意、その計算には今の対象要素の位置とサイズが必要なので、対象要素自体をConverterParameterに指定している

Converter

Converter
0、3、5番のThumbで使うConverter、水平方向の変更専用

Convertは対象要素に変更があったときに、計算(変換)結果をThumbへ送る
valuesには0番にLeft位置、1番に横幅が入っている、その0番だけ返しているのは、横幅が変化してもThumbの0、3、5番には関係ないから

ConvertBackはConvertの逆方向なので、Thumbが移動したときに、計算結果を対象要素へ送る
Thumbの位置を元に対象要素のサイズと位置に変換(Converter)する処理
引数のvalueがThumbのLeftの値、parameterには対象要素自体が入っている

0番Thumb移動
要素の位置はThumbの位置と同じなので、そのままでいい149行目
要素のサイズは横幅の場合は、要素の右端位置 - 要素の左端位置で計算で150行目

この時の場合だと
value=2、これはThumbのLeftの位置
対象要素の横幅element.Widthは3、これはまだ変更前だから
位置も変更前なのでCanvas.GetLeft(element)は1
右端位置 = 横幅 + Left位置は3+1=4

要素のLeftは、そのままThumbのLeftになるので149行目は2
要素のWidthは、要素の右端位置 - 要素の左端位置なので、150行目で計算して4-2=2

MultiBindingに詰め込んたBindingの順番はLeftProperty、WidthPropertyだったので結果も、そのとおりに詰め込む、153と154行目
これで完了

垂直方向のコンバーター

垂直方向のコンバーター
さっきの水平方向が垂直方向へ変わっただけなのでほとんど同じ

水平方向のコンバーターその2

全部同じじゃないですか!?
垂直方向のコンバーターその2
ちがいますよーーっ
一つにまとめたかったけど、なんかできなかった

中間地点用

中間地点用コンバーター
1、3、4、6番Thumb用
ConbertBackには何も書いていないんだけど、これで動いている

これらを8つのThumbに適切にBinding

適切に
作成したMultiBindingをThumbのLeftPropertyとTopPropertyにバインド、43から65行目

要素を指定
起動時に要素を指定

結果
できた
ほんとはAdornerとかいうコントロール?を使うのが良さそうなんだけど、解説記事を見てもわからなかったので、8年前の方法をWPFのBindingを使って再現してみたってところ

対象要素をボタンにした場合
ボタンもサイズ変更できるけど、表示している文字とかはフォントサイズで決まっているので変化しない

対象要素の位置やサイズが指定されていない場合も変化しない
横幅の場合だとPropertyは2種類あって、WidthPropertyとActualWidthProperty
ActualWidthPropertyは必ず値が入っているけど、WidthPropertyは指定されない限りは値が入っていない要素が多くて、ボタンを始めText表示系、Panel系とか
このへんはサイズを指定してからバインドしないとサイズ変化しなかった
ってことはWidthPropertyにバインドするんじゃなくて、ActualWidthPropertyにバインドすればいい?
でも実際期待どおりに動いているからいいや




関連記事

271日後、ついにAdornerを使う
gogowaten.hatenablog.com

次回のWPF記事
gogowaten.hatenablog.com

前回のWPF記事
gogowaten.hatenablog.com

3年前
WPF、マウスドラッグ移動でThumbのサイズ変更 - 午後わてんのブログ gogowaten.hatenablog.com これはWPFなのにBindingなしで、しかも変更は右、下、右下の3方向だけの簡単なもの

8年前
マウスドラッグでコントロールの移動とサイズ変更LabelとPictureBox - 午後わてんのブログ gogowaten.hatenablog.com WindowsForm+VBのときのもので、今回のはこれのWPFC#