午後わてんのブログ

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

WPF、図形の回転、PathGeometryで描画した図形の「中央」を中心に回転させるには

RotateTransformのCenterXとCenterYの調整が必要で、
RenderedGeometry.BoundsのWidthとHeightの半分の値をそれぞれに指定する
2025/01/30追記
正確には中央じゃなかったけど、エクセルの図形の回転と同じ結果だったので、もうこれでいいことにする
エクセルと和解せよ、古事記にもそう書かれている

結果、比較

ユーチューブで
youtu.be

GIFアニメーション

Animation20250129_184735.gif

線の太さ50のLine要素3つをそれぞれの設定で回転
下の2つはPolygon要素での三角形図形の回転

赤:何も設定していない
緑:RenderTransformOrigin="0.5,0.5"を設定
青:RotateTransformのCenterXとCenterYを調整

赤は図形の左上を中心に回転している

緑はRenderTransformOriginで図形の中央を指定しているはずなんだけど、見た目上では中央からややズレている。
これはRenderTransformOriginは線の太さや塗りつぶしの範囲を考えないで、純粋な点の座標のみをみたときの中央だからずれるみたい
最初はなんでずれるのかわからなかったし、線が細いと気づかない

青は期待通り見た目的に図形中央で回転している
見た目上の中央座標の取得は
対象要素のGeometryのRenderedGeometry.Boundsで得られるRectのWidthの半分の値とHeightの半分の値を、RotateTransformのCenterXとCenterYに設定
設定のタイミングは今回は対象要素のSizeChangedイベントのときにした
もっといいタイミングがある?




テストアプリのコード

2025WPF/20250129_CenterRotateShape at main · gogowaten/2025WPF
github.com


MainWindow.xaml

<Window x:Class="_20250129_CenterRotateShape.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:_20250129_CenterRotateShape"
        mc:Ignorable="d"
        Title="Rotate Shapes" Height="450" Width="600">
  <Grid>
    <Grid.ColumnDefinitions>
      <ColumnDefinition/>
      <ColumnDefinition Width="200"/>
    </Grid.ColumnDefinitions>
    <Canvas>
      <Line Canvas.Left="50" Canvas.Top="50" X1="0" Y1="0" X2="100" Y2="0"
            Stroke="Tomato" StrokeThickness="{Binding ElementName=MySlider2, Path=Value}"
            Opacity="0.7"/>

      <Line x:Name="MyShape1"
            Canvas.Left="50" Canvas.Top="50" X1="0" Y1="0" X2="100" Y2="0"
            Stroke="Tomato" StrokeThickness="{Binding ElementName=MySlider2, Path=Value}"
            Opacity="0.7">
        <Line.RenderTransform>
          <RotateTransform Angle="{Binding ElementName=MySlider, Path=Value}"/>
        </Line.RenderTransform>
      </Line>

      <Line Canvas.Left="150" Canvas.Top="50" X1="0" Y1="0" X2="100" Y2="0"
            Stroke="SeaGreen" StrokeThickness="{Binding ElementName=MySlider2, Path=Value}"
            RenderTransformOrigin="0.5,0.5" Opacity="0.7"/>

      <Line x:Name="MyShape2"
            Canvas.Left="150" Canvas.Top="50" X1="0" Y1="0" X2="100" Y2="0"
            Stroke="SeaGreen" StrokeThickness="{Binding ElementName=MySlider2, Path=Value}"
            RenderTransformOrigin="0.5,0.5" Opacity="0.7">
        <Line.RenderTransform>
          <RotateTransform Angle="{Binding ElementName=MySlider, Path=Value}"/>
        </Line.RenderTransform>
      </Line>
      
      <Line Canvas.Left="250" Canvas.Top="50" X1="0" Y1="0" X2="100" Y2="0"
            Stroke="Blue" StrokeThickness="{Binding ElementName=MySlider2, Path=Value}" Opacity="0.7">
      </Line>
      <Line x:Name="MyShape3" Canvas.Left="250" Canvas.Top="50" X1="0" Y1="0" X2="100" Y2="0"
            Stroke="Blue" StrokeThickness="{Binding ElementName=MySlider2, Path=Value}" Opacity="0.7">
      </Line>

      <Polygon Canvas.Left="50" Canvas.Top="200" Points="0,0,100,100,200,20" Opacity="0.7"
               StrokeThickness="{Binding ElementName=MySlider2, Path=Value}"
               Stroke="SeaGreen" Fill="Gold" RenderTransformOrigin="0.5,0.5">        
      </Polygon>
      <Polygon Canvas.Left="50" Canvas.Top="200" Points="0,0,100,100,200,20" Opacity="0.7"
               StrokeThickness="{Binding ElementName=MySlider2, Path=Value}"
               Stroke="SeaGreen" Fill="Gold" RenderTransformOrigin="0.5,0.5">
        <Polygon.RenderTransform>
          <RotateTransform Angle="{Binding ElementName=MySlider, Path=Value}"/>
        </Polygon.RenderTransform>
      </Polygon>
      
      <Polygon Canvas.Left="250" Canvas.Top="200" Points="0,0,100,100,200,20" Opacity="0.7"
               StrokeThickness="{Binding ElementName=MySlider2, Path=Value}"
               Stroke="DodgerBlue" Fill="Gold">        
      </Polygon>
      <Polygon x:Name="MyShape5"
               Canvas.Left="250" Canvas.Top="200" Points="0,0,100,100,200,20" Opacity="0.7"
               StrokeThickness="{Binding ElementName=MySlider2, Path=Value}"
               Stroke="DodgerBlue" Fill="Gold">        
      </Polygon>
      
    </Canvas>
    
    <StackPanel Grid.Column="1">
      <TextBlock Text="DefaultRotateTransform" Foreground="Tomato"/>
      <TextBlock Text="RenderTransfomOrigin 0.5, 0.5" Foreground="SeaGreen"/>
      <TextBlock Text="違和感のない中心軸に変更" Foreground="Blue"/>
      <Separator Margin="10"/>
      <TextBlock Text="{Binding ElementName=MySlider, Path=Value, StringFormat={}{0:0} angle}"/>
      <Slider x:Name="MySlider" Minimum="0" Maximum="180" Value="{Binding MyAngle}"/>
      <Separator Margin="10"/>
      <TextBlock Text="{Binding ElementName=MySlider2, Path=Value, StringFormat={}{0:0} StrokeThickness}"/>
      <Slider x:Name="MySlider2" Minimum="0" Maximum="50" Value="50"/>
    </StackPanel>
    
  </Grid>
</Window>




MainWindow.xaml.cs

using System.Globalization;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media;

namespace _20250129_CenterRotateShape
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            
            DataContext = this;
            MyShape3.SizeChanged += MyShape3_SizeChanged;
            MyShape5.SizeChanged += MyShape3_SizeChanged;

            MultiBinding mb = new() { Converter = new MyConverterRenderTF() };
            mb.Bindings.Add(new Binding() { Source = this, Path = new PropertyPath(MyAngleProperty), Mode = BindingMode.OneWay });
            mb.Bindings.Add(new Binding() { Source = this, Path = new PropertyPath(MyBoundsProperty), Mode = BindingMode.OneWay });
            MyShape3.SetBinding(RenderTransformProperty, mb);

            mb = new() { Converter = new MyConverterRenderTF() };
            mb.Bindings.Add(new Binding() { Source = this, Path = new PropertyPath(MyAngleProperty), Mode = BindingMode.OneWay });
            mb.Bindings.Add(new Binding() { Source = this, Path = new PropertyPath(MyBounds2Property), Mode = BindingMode.OneWay });
            MyShape5.SetBinding(RenderTransformProperty, mb);
        }

        private void MyShape3_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            MyBounds = MyShape3.RenderedGeometry.Bounds;
            MyBounds2 = MyShape5.RenderedGeometry.Bounds;
        }


        public double MyAngle
        {
            get { return (double)GetValue(MyAngleProperty); }
            set { SetValue(MyAngleProperty, value); }
        }
        public static readonly DependencyProperty MyAngleProperty =
            DependencyProperty.Register(nameof(MyAngle), typeof(double), typeof(MainWindow), new PropertyMetadata(0.0));

        public Rect MyBounds
        {
            get { return (Rect)GetValue(MyBoundsProperty); }
            set { SetValue(MyBoundsProperty, value); }
        }
        public static readonly DependencyProperty MyBoundsProperty =
            DependencyProperty.Register(nameof(MyBounds), typeof(Rect), typeof(MainWindow), new PropertyMetadata(new Rect()));

        public Rect MyBounds2
        {
            get { return (Rect)GetValue(MyBounds2Property); }
            set { SetValue(MyBounds2Property, value); }
        }
        public static readonly DependencyProperty MyBounds2Property =
            DependencyProperty.Register(nameof(MyBounds2), typeof(Rect), typeof(MainWindow), new PropertyMetadata(new Rect()));

    }



    //角度とRenderBoundsからRotateTransformに変換
    //見た目場違和感のない回転中心点を計算する
    public class MyConverterRenderTF : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            double angle = (double)values[0];
            Rect r = (Rect)values[1];
            return new RotateTransform(angle, r.Width / 2.0, r.Height / 2.0);
        }

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

中央座標の取得と設定では表示後の点座標の変更にも対応するために、マルチバインディングとかコンバーターとか割と面倒なことになっているけど、今回のように図形の変更がないなら決め打ちで良かったかも…
どうせならカスタムコントロールにしたほうが楽だったかも?




感想

テストアプリ

思ってたよりかなりめんどくさかった
単純な四角形を表示するRectangleや、その他の要素、ボタンとかテキストボックスとかはRenderTransformOrigin 0.5するだけで中央中心回転になるけど、PathGeometryとかのGeometry系は違うとかね、気づかないよ、特に線が細い場合は気のせいかなくらいで思っていた




関連記事

次のWPF記事
WPF、図形の回転、PathGeometryで描画した図形の中心は全頂点の平均座標もいいね - 午後わてんのブログ
gogowaten.hatenablog.com




前回のWPF記事
WPF、折れ線図形を描画するクラスをControlを継承したカスタムコントロールで作ってみた、線の太さを考慮してサイズ計算 - 午後わてんのブログ
gogowaten.hatenablog.com