WPF、要素をリサイズするときのハンドルThumbをAdornerで作ってみた.mp5
もう何回目なのかわからん、5回?
結果
ユーチューブで
youtu.be
GIFアニメーション

対象要素がFrameworkElementなので、ほとんどの種類の要素に使える
ハンドルの表示位置。XAMLでのデザイン画面を真似して、要素の外側になるようにした

比較 ハンドルの表示数。対象要素がCanvasパネルに配置されているときだけMAXの8個、それ以外は3個
Canvas以外のパネルでは要素の表示位置の指定は不具合のもとになるので、表示位置に関わるハンドルは表示しないようにした
ハンドル個数 Bindingでもハンドルのサイズ変更できる

ハンドルサイズ
テストアプリのコード
2025WPF/20250315_ResizeAdorner at main · gogowaten/2025WPF
github.com
環境
- Windows 10 Home バージョン 22H2
- Visual Studio Community 2022 Version 17.13.2
- WPF
- C#
- .NET 8.0
CustomControl1.sc
ハンドル表示用のコントロール
Thumbを継承させたカスタムコントロール
using System.Windows; using System.Windows.Controls.Primitives; namespace _20250315_ResizeAdorner { 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)); } }
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:_20250315_ResizeAdorner"> <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>
Templateを変更
Gridの中に、2つ重ねたRectangleを入れている
下に白枠線のRectangle、上は黒の破線
ResizeAdorner.sc
今回の要
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; namespace _20250315_ResizeAdorner { /// <summary> /// 要素にリサイズ用のハンドルを装飾表示する /// </summary> public class ResizeAdorner : 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 ResizeAdorner(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); //ハンドルの位置とターゲットの縦横サイズをバインド MyBind2(Right, HandleThumb.MyLeftProperty, WidthProperty); MyBind2(Bottom, HandleThumb.MyTopProperty, HeightProperty); MyBind2(TopRight, HandleThumb.MyLeftProperty, WidthProperty); MyBind2(BottomLeft, HandleThumb.MyTopProperty, HeightProperty); MyBind2(BottomRight, HandleThumb.MyLeftProperty, WidthProperty); MyBind2(BottomRight, HandleThumb.MyTopProperty, HeightProperty); //ハンドルの位置とハンドルのサイズをバインド MyBindHandleSize(Top, HandleThumb.MyTopProperty); MyBindHandleSize(Left, HandleThumb.MyLeftProperty); MyBindHandleSize(TopLeft, HandleThumb.MyTopProperty); MyBindHandleSize(TopLeft, HandleThumb.MyLeftProperty); MyBindHandleSize(TopRight, HandleThumb.MyTopProperty); MyBindHandleSize(BottomLeft, HandleThumb.MyLeftProperty); } //ハンドルの位置とターゲットの縦横サイズをバインド private void MyBind2(HandleThumb handle, DependencyProperty handleDp, DependencyProperty targetDp) { handle.SetBinding(handleDp, new Binding() { Source = MyTarget, Path = new PropertyPath(targetDp), Mode = BindingMode.TwoWay, Converter = new MyConvNonZero() }); } //ハンドルの表示位置、ターゲットの辺の中間 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 MyBindHandleSize(HandleThumb target, DependencyProperty dp) { target.SetBinding(dp, new Binding() { Source = this, Path = new PropertyPath(MyHandleSizeProperty), Converter = new MyConvReverseSign(), Mode = BindingMode.TwoWay }); } //各ハンドルをドラッグ移動したとき private void Handle_DragDelta(object sender, DragDeltaEventArgs e) { if (sender == Left) { e.Handled = HorizontalChange(MyTarget, e.HorizontalChange); } else if (sender == Top) { e.Handled = VerticalChange(MyTarget, e.VerticalChange); } else if (sender == Right) { Right.MyLeft += e.HorizontalChange; e.Handled = true; } else if (sender == Bottom) { Bottom.MyTop += e.VerticalChange; e.Handled = true; } else if (TopLeft == sender) { e.Handled = HorizontalChange(MyTarget, e.HorizontalChange); e.Handled = VerticalChange(MyTarget, e.VerticalChange); } else if (TopRight == sender) { e.Handled = VerticalChange(MyTarget, e.VerticalChange); TopRight.MyLeft += e.HorizontalChange; } else if (BottomLeft == sender) { e.Handled = HorizontalChange(MyTarget, e.HorizontalChange); BottomLeft.MyTop += e.VerticalChange; } else if (BottomRight == sender) { BottomRight.MyLeft += e.HorizontalChange; BottomRight.MyTop += e.VerticalChange; e.Handled = true; } } public double MyHandleSize { get { return (double)GetValue(MyHandleSizeProperty); } set { SetValue(MyHandleSizeProperty, value); } } public static readonly DependencyProperty MyHandleSizeProperty = DependencyProperty.Register(nameof(MyHandleSize), typeof(double), typeof(ResizeAdorner), new FrameworkPropertyMetadata(20.0)); //これがないと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が0未満にならないときだけ /// </summary> /// <param name="target">ターゲット</param> /// <param name="horizontalChange">水平移動量</param> /// <returns></returns> private bool HorizontalChange(FrameworkElement target, double horizontalChange) { if (target.Width - horizontalChange > 0) { OffsetLeft(target, horizontalChange); target.Width -= horizontalChange; return true; } return false; } private bool VerticalChange(FrameworkElement target, double verticalChange) { if (target.Height - verticalChange > 0) { OffsetTop(target, verticalChange); target.Height -= verticalChange; return true; } return false; } /// <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); } } #region コンバーター /// <summary> /// +-を逆にする /// </summary> public class MyConvReverseSign : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { return -(double)value; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } } /// <summary> /// 0未満のときだけ1に変換 /// </summary> public class MyConvNonZero : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { double v = (double)value; return v < 1 ? 1 : v; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { double v = (double)value; return v < 1 ? 1 : v; } } /// <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 コンバーター }
MainWindow.xaml
<Window x:Class="_20250315_ResizeAdorner.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:_20250315_ResizeAdorner" mc:Ignorable="d" Title="MainWindow" Height="367" Width="602"> <Grid x:Name="MyGrid"> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition Width="250"/> </Grid.ColumnDefinitions> <Canvas> <Rectangle x:Name="MyTarget" Canvas.Left="50" Canvas.Top="50" Width="100" Height="100" Fill="MistyRose" Stroke="Tomato" StrokeThickness="1.0"/> <Ellipse x:Name="MyEllipse" Canvas.Left="100" Canvas.Top="200" Width="80" Height="80" Fill="MistyRose" Stroke="Tomato" StrokeThickness="1.0"/> <Button x:Name="MyButton" Canvas.Left="200" Canvas.Top="50" Content="Canvasの中のボタン"/> </Canvas> <StackPanel Grid.Column="1" x:Name="MyStackPanel"> <Button x:Name="MyButtonInStackPanel" Content="StackPanelの中のボタン" Margin="30"/> <Slider Value="{Binding MyHandleSize}" Minimum="0" Maximum="50"/> <TextBlock Text="{Binding MyHandleSize, StringFormat=handleSize 0}"/> <Button Content="ハンドル表示切替" Click="MyButtonChangeHandleVisible_Click"/> <Button Content="背景色切り替え" Click="MyButtonChangeBackground_Click"/> </StackPanel> </Grid> </Window>
MainWindow.xaml.cs
using System.Windows; using System.Windows.Documents; using System.Windows.Media; namespace _20250315_ResizeAdorner; public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void MyButtonChangeHandleVisible_Click(object sender, RoutedEventArgs e) { ResizeAdorner? adorner = SwitchAdorner(MyTarget); MyStackPanel.DataContext = adorner; _ = SwitchAdorner(MyButton); _ = SwitchAdorner(MyButtonInStackPanel); _ = SwitchAdorner(MyEllipse); } /// <summary> /// 対象にリサイズ用のハンドル(装飾)を付け外しする /// </summary> /// <param name="elem">対象要素</param> /// <returns>装飾</returns> private static ResizeAdorner? SwitchAdorner(FrameworkElement elem) { if (AdornerLayer.GetAdornerLayer(elem) is AdornerLayer layer) { var items = layer.GetAdorners(elem); if (items != null) { foreach (var item in items.OfType<ResizeAdorner>()) { layer.Remove(item); } return null; } else { var adorner = new ResizeAdorner(elem); layer.Add(adorner); return adorner; } } return null; } private void MyButtonChangeBackground_Click(object sender, RoutedEventArgs e) { if (MyGrid.Background == null || MyGrid.Background == Brushes.White) { MyGrid.Background = Brushes.Black; } else { MyGrid.Background = Brushes.White; } } }
感想
流石にリサイズ系はこれで最後だと思う
あとは見た目を変えるプロパティの追加くらいで、動作や配置はマンゾク…
関連記事
続きは6日後
WPF、要素をリサイズするときのハンドルThumbをAdornerで作ってみた.mp6 - 午後わてんのブログ
gogowaten.hatenablog.com
次のWPF記事
WPF、矢印図形のアンカーハンドルポイントの表示と操作、Pointの追加と削除テスト - 午後わてんのブログ
gogowaten.hatenablog.com
前回のWPF記事
WPF、要素をファイルに保存と復元テスト - 午後わてんのブログ
gogowaten.hatenablog.com
2ヶ月前
WPF、「8点ハンドル」でサイズ可変+マウスドラッグ移動可能なCanvasを「できるだけ簡易」にカスタムコントロールで作ってみた - 午後わてんのブログ
gogowaten.hatenablog.com
2年前
WPF、Rectangleとかに2色の破線(点線)枠表示 - 午後わてんのブログ
gogowaten.hatenablog.com