午後わてんのブログ

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

WPF、要素をリサイズするときのハンドルThumbをAdornerで作ってみた.mp6

前回との違いはハンドルの表示位置

結果

ユーチューブで
youtu.be



GIFアニメーションで確認

結果


結果



ハンドルの表示位置

前回はリサイズ対象の外側だったのを、内側と境界線の上にも表示できるようにした
内側に表示しておくと、ScrollViewer(スクロールバー表示時)のときに便利
ScrollViewerで正しく表示するには、表示したい対象の位置とサイズが必要で、もし、ハンドルを対象の外側に表示していた場合は、ハンドルの位置とサイズも計算する必要があるけど、内側に表示していればリサイズ対象の位置とサイズだけで済むし、ハンドルの表示の有無にも左右されないので楽ちん
良くないところは、極端な条件下ではハンドルが見えなくなることくらい。たとえば、リサイズ対象の位置がハンドルサイズ以下のときに、リサイズ対象をハンドルサイズ以下にしたときとかは、ハンドルがマイナスの位置になるのでスクロールバーの範囲外になって見えなくなる




テストアプリのコード

2025WPF/20250320_ResizeHandleAdorner at main · gogowaten/2025WPF
github.com


環境

ハンドル表示用のカスタムコントロール

Generic.xaml

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:_20250320_ResizeHandleAdorner">


  <Style TargetType="{x:Type local:HandleThumb}">
    <Setter Property="Canvas.Left"
            Value="{Binding RelativeSource={RelativeSource Mode=Self}, Path=MyLeft, Mode=TwoWay}"/>
    <Setter Property="Canvas.Top"
            Value="{Binding RelativeSource={RelativeSource Mode=Self}, Path=MyTop, Mode=TwoWay}"/>
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="{x:Type local:HandleThumb}">
          <Grid>
            <Rectangle
              Stroke="White"
              StrokeThickness="1.0"/>
            <Rectangle
              Stroke="Black"
              StrokeThickness="1.0"
              StrokeDashArray="2"
              Fill="Transparent"
              />
          </Grid>
        </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>

</ResourceDictionary>




CustomControl.cs

using System.Windows;
using System.Windows.Controls.Primitives;

namespace _20250320_ResizeHandleAdorner
{

    /// <summary>
    /// リサイズ用のハンドルThumb
    /// </summary>
    public class HandleThumb : Thumb
    {
        static HandleThumb()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(HandleThumb), new FrameworkPropertyMetadata(typeof(HandleThumb)));
        }
        public HandleThumb()
        {

        }

        //Canvas.Leftとバインドする用
        public double MyLeft
        {
            get { return (double)GetValue(MyLeftProperty); }
            set { SetValue(MyLeftProperty, value); }
        }
        public static readonly DependencyProperty MyLeftProperty =
            DependencyProperty.Register(nameof(MyLeft), typeof(double), typeof(HandleThumb),
                new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

        public double MyTop
        {
            get { return (double)GetValue(MyTopProperty); }
            set { SetValue(MyTopProperty, value); }
        }
        public static readonly DependencyProperty MyTopProperty =
            DependencyProperty.Register(nameof(MyTop), typeof(double), typeof(HandleThumb),
                new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

    }

}




ResizeHandleAdorner.cs

using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Globalization;
using System.ComponentModel;

namespace _20250320_ResizeHandleAdorner
{
    //ハンドルの表示位置、ターゲットの境界線の上、内側、外側
    public enum HandleLayoutType { Online = 0, Inside, Outside }


    /// <summary>
    /// 要素にリサイズ用のハンドルを装飾表示する
    /// </summary>
    public class ResizeHandleAdorner : Adorner
    {


        #region VisualCollectionで必要        
        protected override int VisualChildrenCount => MyVisualChildren.Count;

        protected override Visual GetVisualChild(int index) => MyVisualChildren[index];
        readonly VisualCollection MyVisualChildren;//表示したい要素を管理する用?
        #endregion VisualCollectionで必要

        private readonly List<HandleThumb> MyHandles = [];//ハンドルリスト、必要なかったかも
        private readonly Canvas MyCanvas = new();// ハンドルを表示する用
        private readonly FrameworkElement MyTarget;// 装飾ターゲット
        private readonly HandleThumb Top = new();// 各ハンドル
        private readonly HandleThumb Left = new();
        private readonly HandleThumb Right = new();
        private readonly HandleThumb Bottom = new();
        private readonly HandleThumb TopLeft = new();
        private readonly HandleThumb TopRight = new();
        private readonly HandleThumb BottomLeft = new();
        private readonly HandleThumb BottomRight = new();

        public ResizeHandleAdorner(FrameworkElement adornedElement) : base(adornedElement)
        {
            MyTarget = adornedElement;

            MyVisualChildren = new VisualCollection(this)
            {
                MyCanvas
            };
            MyHandles.Add(Top);
            MyHandles.Add(Left);
            MyHandles.Add(Right);
            MyHandles.Add(Bottom);
            MyHandles.Add(TopLeft);
            MyHandles.Add(TopRight);
            MyHandles.Add(BottomLeft);
            MyHandles.Add(BottomRight);

            //通常ではサイズが不確定な要素(TextBlockとかButton)のサイズを決定しておく
            if (double.IsNaN(adornedElement.Width))
            {
                adornedElement.Width = adornedElement.ActualWidth;
                adornedElement.Height = adornedElement.ActualHeight;
            }

            //ハンドルの設定
            foreach (HandleThumb item in MyHandles)
            {
                item.Cursor = Cursors.Hand;
                item.DragDelta += Handle_DragDelta;
                MyCanvas.Children.Add(item);
                //装飾ターゲットの親要素がCanvasではない場合は、
                //以下の5個のハンドルは表示しない(外す)
                //これらはターゲットの位置変更に関係するからCanvas以外では不具合の元
                if (MyTarget.Parent.GetType() != typeof(Canvas))
                {
                    MyCanvas.Children.Remove(Top);
                    MyCanvas.Children.Remove(Left);
                    MyCanvas.Children.Remove(TopLeft);
                    MyCanvas.Children.Remove(TopRight);
                    MyCanvas.Children.Remove(BottomLeft);
                }

                item.SetBinding(WidthProperty, new Binding() { Source = this, Path = new PropertyPath(MyHandleSizeProperty) });
                item.SetBinding(HeightProperty, new Binding() { Source = this, Path = new PropertyPath(MyHandleSizeProperty) });
            }

            //ハンドルの表示位置、ターゲットの辺の中間
            MyBind(Top, HandleThumb.MyLeftProperty, WidthProperty);
            MyBind(Left, HandleThumb.MyTopProperty, HeightProperty);
            MyBind(Right, HandleThumb.MyTopProperty, HeightProperty);
            MyBind(Bottom, HandleThumb.MyLeftProperty, WidthProperty);

            //ハンドルの位置とターゲットの縦横サイズをバインド、下、右用
            MyBindForBottomRight(Right, HandleThumb.MyLeftProperty, WidthProperty);
            MyBindForBottomRight(Bottom, HandleThumb.MyTopProperty, HeightProperty);
            MyBindForBottomRight(TopRight, HandleThumb.MyLeftProperty, WidthProperty);
            MyBindForBottomRight(BottomLeft, HandleThumb.MyTopProperty, HeightProperty);
            MyBindForBottomRight(BottomRight, HandleThumb.MyLeftProperty, WidthProperty);
            MyBindForBottomRight(BottomRight, HandleThumb.MyTopProperty, HeightProperty);

            //ハンドルの位置とターゲットの縦横サイズをバインド、上、左用
            MyBindForTopLeft(Top, HandleThumb.MyTopProperty);
            MyBindForTopLeft(Left, HandleThumb.MyLeftProperty);
            MyBindForTopLeft(TopLeft, HandleThumb.MyTopProperty);
            MyBindForTopLeft(TopLeft, HandleThumb.MyLeftProperty);
            MyBindForTopLeft(TopRight, HandleThumb.MyTopProperty);
            MyBindForTopLeft(BottomLeft, HandleThumb.MyLeftProperty);

        }

        //ハンドルの位置とターゲットの縦横サイズをバインド
        private void MyBindForBottomRight(HandleThumb handle, DependencyProperty handleDp, DependencyProperty targetDp)
        {
            MultiBinding mb = new() { Converter = new MyConvForBottomRight(), Mode = BindingMode.OneWay };
            mb.Bindings.Add(new Binding() { Source = this, Path = new PropertyPath(MyHandleSizeProperty) });
            mb.Bindings.Add(new Binding() { Source = this, Path = new PropertyPath(MyHandleLayoutProperty) });
            mb.Bindings.Add(new Binding() { Source = MyTarget, Path = new PropertyPath(targetDp) });
            handle.SetBinding(handleDp, mb);
        }


        //ハンドルの表示位置、ターゲットの辺の中間
        private void MyBind(HandleThumb handle, DependencyProperty dp, DependencyProperty targetProperty)
        {
            MultiBinding mb;
            mb = new() { Converter = new MyConvHalf() };
            mb.Bindings.Add(new Binding() { Source = MyTarget, Path = new PropertyPath(targetProperty) });
            mb.Bindings.Add(new Binding() { Source = this, Path = new PropertyPath(MyHandleSizeProperty) });
            handle.SetBinding(dp, mb);
        }

        //ハンドルの位置とハンドルのサイズをバインド       
        private void MyBindForTopLeft(HandleThumb handle, DependencyProperty dp)
        {
            var mb = new MultiBinding() { Converter = new MyConvForTopLeftHandle() };
            mb.Bindings.Add(new Binding() { Source = this, Path = new PropertyPath(MyHandleSizeProperty) });
            mb.Bindings.Add(new Binding() { Source = this, Path = new PropertyPath(MyHandleLayoutProperty) });
            handle.SetBinding(dp, mb);
        }



        #region イベント
        //ハンドルのドラッグ移動により、ターゲットの位置が変更されたときに、その変更量を通知
        public event Action<double>? OnTargetLeftChanged;
        public event Action<double>? OnTargetTopChanged;
        //public event EventHandler<double>? OnTargetLeftChanged;//こっちでもいい
        //public event PropertyChangedEventHandler PropertyChanged;//これは違う感じ
        #endregion イベント

        private bool SetTargetWidth(double horizontalChange)
        {
            if (MyTarget.Width + horizontalChange >= 1.0)
            {
                MyTarget.Width += horizontalChange;
                return true;
            }
            else if (MyTarget.Width > 1.0)
            {
                MyTarget.Width = 1.0;
                return true;
            }
            else { return false; }

        }
        private bool SetTargetHeight(double verticalChange)
        {
            if (MyTarget.Height + verticalChange >= 1.0)
            {
                MyTarget.Height += verticalChange;
                return true;
            }
            else if (MyTarget.Height > 1.0)
            {
                MyTarget.Height = 1.0;
                return true;
            }
            else { return false; }
        }

        //各ハンドルをドラッグ移動したとき
        private void Handle_DragDelta(object sender, DragDeltaEventArgs e)
        {
            if (sender == Left)
            {
                HorizontalChange(MyTarget, e.HorizontalChange);
                e.Handled = true;
            }
            else if (sender == Top)
            {
                VerticalChange(MyTarget, e.VerticalChange);
                e.Handled = true;
            }
            else if (sender == Right)
            {
                SetTargetWidth(e.HorizontalChange);
                e.Handled = true;
            }
            else if (sender == Bottom)
            {
                SetTargetHeight(e.VerticalChange);
                e.Handled = true;
            }
            else if (TopLeft == sender)
            {
                HorizontalChange(MyTarget, e.HorizontalChange);
                VerticalChange(MyTarget, e.VerticalChange);
                e.Handled = true;
            }
            else if (TopRight == sender)
            {
                if (SetTargetWidth(e.HorizontalChange))
                {
                    VerticalChange(MyTarget, e.VerticalChange);
                }
                e.Handled = true;
            }
            else if (BottomLeft == sender)
            {
                if (SetTargetHeight(e.VerticalChange))
                {
                    HorizontalChange(MyTarget, e.HorizontalChange);
                }
                e.Handled = true;
            }
            else if (BottomRight == sender)
            {
                SetTargetWidth(e.HorizontalChange);
                SetTargetHeight(e.VerticalChange);
                e.Handled = true;
            }

        }


        #region 依存関係プロパティ


        public HandleLayoutType MyHandleLayout
        {
            get { return (HandleLayoutType)GetValue(MyHandleLayoutProperty); }
            set { SetValue(MyHandleLayoutProperty, value); }
        }
        public static readonly DependencyProperty MyHandleLayoutProperty =
            DependencyProperty.Register(nameof(MyHandleLayout), typeof(HandleLayoutType), typeof(ResizeHandleAdorner),
                new FrameworkPropertyMetadata(HandleLayoutType.Online,
                    FrameworkPropertyMetadataOptions.AffectsRender |
                    FrameworkPropertyMetadataOptions.AffectsMeasure |
                    FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

        public double MyHandleSize
        {
            get { return (double)GetValue(MyHandleSizeProperty); }
            set { SetValue(MyHandleSizeProperty, value); }
        }
        public static readonly DependencyProperty MyHandleSizeProperty =
            DependencyProperty.Register(nameof(MyHandleSize), typeof(double), typeof(ResizeHandleAdorner), new FrameworkPropertyMetadata(20.0));
        #endregion 依存関係プロパティ

        //これがないとCanvasのサイズが0のままになって何も表示されない
        protected override Size ArrangeOverride(Size finalSize)
        {
            MyCanvas.Arrange(new Rect(0, 0, finalSize.Width, finalSize.Height));
            return base.ArrangeOverride(finalSize);
        }


        /// <summary>
        /// 移動量で水平移動とWidthを変更
        /// もしwidthが1未満になるような移動量のときは、Widthが1になるように移動量を変換して処理
        /// </summary>
        /// <param name="target">ターゲット</param>
        /// <param name="horizontalChange">水平移動量</param>
        /// <returns></returns>
        private void HorizontalChange(FrameworkElement target, double horizontalChange)
        {
            if (horizontalChange == 0) { return; }
            if (target.Width - horizontalChange > 0)
            {
                target.Width -= horizontalChange;
            }
            else
            {
                horizontalChange = target.Width - 1.0;
                target.Width = 1.0;
            }
            OffsetLeft(target, horizontalChange);
            //リサイズハンドルによりターゲットの位置が変更されたことを知らせる
            OnTargetLeftChanged?.Invoke(horizontalChange);
        }

        private void VerticalChange(FrameworkElement target, double verticalChange)
        {
            if (verticalChange == 0) { return; }
            if (target.Height - verticalChange > 0)
            {
                target.Height -= verticalChange;
            }
            else
            {
                verticalChange = target.Height - 1.0;
                target.Height = 1.0;
            }
            OffsetTop(target, verticalChange);
            OnTargetTopChanged?.Invoke(verticalChange);
        }


        /// <summary>
        /// 要素をオフセット移動する
        /// </summary>
        /// <param name="elem">ターゲット</param>
        /// <param name="offset">移動量</param>
        public static void OffsetTop(FrameworkElement elem, double offset)
        {
            Canvas.SetTop(elem, Canvas.GetTop(elem) + offset);
        }
        public static void OffsetLeft(FrameworkElement elem, double offset)
        {
            Canvas.SetLeft(elem, Canvas.GetLeft(elem) + offset);
        }

        public void GetHandlesRenderBounds()
        {
            var r = this.RenderSize;
        }

    }



    #region コンバーター
    /// <summary>
    /// ハンドルサイズと位置指定によって位置を計算
    /// 左要素か上要素があるハンドル用
    /// </summary>
    public class MyConvForTopLeftHandle : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            var handleSize = (double)values[0];
            var layout = (HandleLayoutType)values[1];
            return layout switch
            {
                HandleLayoutType.Inside => 0.0,
                HandleLayoutType.Outside => -handleSize,
                HandleLayoutType.Online => -handleSize / 2.0,
                _ => (object)0.0,
            };
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

    /// <summary>
    /// ハンドルサイズと位置指定とリサイズ対象のサイズからハンドルの位置を計算
    /// 右要素か下要素があるハンドル用
    /// </summary>
    public class MyConvForBottomRight : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            var handleSize = (double)values[0];
            var layout = (HandleLayoutType)values[1];
            var targetSize = (double)values[2];
            if (targetSize <= 1.0) { targetSize = 1.0; }
            return layout switch
            {
                HandleLayoutType.Inside => targetSize - handleSize,
                HandleLayoutType.Outside => targetSize,
                HandleLayoutType.Online => targetSize - handleSize / 2.0,
                _ => targetSize
            };
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

    /// <summary>
    /// ハンドルの位置を装飾ターゲットの辺の中間にするのに使う
    /// </summary>
    public class MyConvHalf : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            var target = (double)values[0];// 装飾ターゲットの辺の長さ
            var handle = (double)values[1];// ハンドルの大きさ
            return (target - handle) / 2.0;
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
    #endregion コンバーター

}

ハンドルの表示位置指定用のenumを追加

表示位置指定用enum

コンバーターの変更

TopLeft用
前回はハンドルサイズ分だけマイナスにしていた

BottomRight用


マウスドラッグ移動でのサイズ変更時、前回は(サイズ - 移動距離)が1未満になるときはサイズ変更しないだったので、サイズが10のとき移動距離が15だった場合にサイズが10のままだった、ほんとはサイズを1にしたいので、今回はそうなるように変更

リサイズ処理の変更


イベント追加
リサイズに伴って位置も変更されたときに発行する用、移動量を送る
リサイズには直接関係ないけど、別のところで使うので追加した

追加イベント
イベントは作る方法がよくわからん

invork
イベントの使い方がこれであっているのかわからんけど、期待通りの動きにはなっている
invork
英語「invoke」の意味・使い方・読み方 | Weblio英和辞書
ejje.weblio.jp




MainWindow.xaml

<Window x:Class="_20250320_ResizeHandleAdorner.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:_20250320_ResizeHandleAdorner"
        mc:Ignorable="d"
        Title="MainWindow" Height="367" Width="602">
  <Grid UseLayoutRounding="True">
    <Grid.ColumnDefinitions>
      <ColumnDefinition/>
      <ColumnDefinition Width="200"/>
    </Grid.ColumnDefinitions>
    <Canvas x:Name="MyCanvas">
      <Rectangle x:Name="MyElement" Fill="Red" Width="100" Height="100" Canvas.Left="150" Canvas.Top="50"/>
    </Canvas>
    
    <StackPanel Grid.Column="1">
      <Button Content="test" Click="Button_Click"/>
      <ComboBox x:Name="MyComboBox"/>
      <Slider x:Name="MySlider" Minimum="1" Maximum="50" Value="20"/>
      
    </StackPanel>
  </Grid>
</Window>




MainWindow.xaml.cs

using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;

namespace _20250320_ResizeHandleAdorner
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            MyComboBox.ItemsSource = Enum.GetValues(typeof(HandleLayoutType));
            MyComboBox.SelectedIndex = 0;
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            if(AdornerLayer.GetAdornerLayer(MyElement) is AdornerLayer layer)
            {
                var adorner = new ResizeHandleAdorner(MyElement);
                adorner.UseLayoutRounding = true;
                adorner.SetBinding(ResizeHandleAdorner.MyHandleLayoutProperty, new Binding() { Source = MyComboBox, Path = new PropertyPath(ComboBox.SelectedItemProperty) });
                adorner.SetBinding(ResizeHandleAdorner.MyHandleSizeProperty, new Binding() { Source = MySlider, Path = new PropertyPath(Slider.ValueProperty) });

                layer.Add(adorner);

            }
        }
    }
}




感想

外側表示だとスクロールバーとの関係で難しかったので、内側表示もできるようにした




関連記事

次のWPF記事は
WPF、ここ2ヶ月間で行ってきたことのまとめ、矢印図形の移動と編集 - 午後わてんのブログ
gogowaten.hatenablog.com


前回のWPF記事は3日前
WPF、矢印図形のアンカーハンドルポイントの表示と操作、Pointの追加と削除テスト - 午後わてんのブログ
gogowaten.hatenablog.com




前回のリサイズは6日前
WPF、要素をリサイズするときのハンドルThumbをAdornerで作ってみた.mp5 - 午後わてんのブログ
gogowaten.hatenablog.com