Search code examples
c#wpf

How to use relative coordinates in specific transforms that are part of a WPF TransformGroup in C#


According to MS knowledge base, it is possible to specify a RenderTransformOrigin to each transform operation to be applied to an object. See this article

It says: "When you apply a Transform to the RenderTransform property of a UIElement, you can use the RenderTransformOrigin property to specify an origin for every Transform that you apply to the element". With this information I expected to be able to use the TransformGroup with multiple rotations, each of them around their own center point, one of them being the center of the element which is given by the relative coordinate (0.5, 0.5).

I haven't really found any way or any sample on how to define RenderTransformOrigin for a transform, as said is possible (though not demonstrated) in the article, so I gave a try to see if setting RenderTransformOrigin for the object takes it into consideration when not specifying CenterX and CenterY. That is, I was assuming that for any RotateTransform without explicit center x/y, it would use the relative coordinate in RenderTransformOrigin. My assumption, however, is wrong. See this simple test:

MainWindow.xaml:

<Window x:Class="RenderTransformOrigin.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:RenderTransformOrigin"
        mc:Ignorable="d"
        Title="MainWindow" Height="540" Width="500">
    <StackPanel>
        <Canvas x:Name="cv" Height="500" Width="500"/>
    </StackPanel>
</Window>

MainWindow.xaml.cs:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace RenderTransformOrigin
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            TextBlock tblock = new TextBlock() { Text = "A", FontSize = 30, FontWeight = FontWeights.Bold };
            tblock.Measure(new Size());
            tblock.Arrange(new Rect());
            cv.Measure(new Size());
            cv.Arrange(new Rect());

            tblock.RenderTransformOrigin = new Point(0.5, 0.5);

            TransformGroup tgroup = new TransformGroup();
            tgroup.Children.Add(new TranslateTransform() { X = (cv.ActualWidth - tblock.ActualWidth) / 2, Y = (cv.ActualHeight - tblock.ActualHeight) / 2 - 100 });
            tgroup.Children.Add(new RotateTransform() { Angle = 90, CenterX = (cv.ActualWidth - tblock.ActualWidth) / 2 , CenterY = (cv.ActualHeight - tblock.ActualHeight) / 2 });
            //tgroup.Children.Add(new RotateTransform() { Angle = -90 });
            
            tblock.RenderTransform = tgroup;
            cv.Children.Add(tblock);
        }
    }
}

The program places a TextBlock in a Canvas slightly off center in Y and centered in X. First it rotates the text by 90 degrees, and then the idea is to rotate the text around its center to make it face the reader again. See that I assigned RenderTransformOrigin before any transform. With the second rotation commented out, it doesn't affect the first rotation with its explicit center x/y. If you uncomment the line, the text is certainly rotated around the canvas origin (0, 0) because it goes off screen. Adding { CenterX = 0.5, CenterY = 0.5 } makes no difference, though I would think it is redundant after setting RenderTransformOrigin.

So my question is, how can I use the relative coordinates for a transform given the fact that the transform group has other transforms with different centers that have to be preserved? You can see similar questions being asked in S.O. before, without successful answers:

C# WPF How to change RenderTransformOrigin but keep location?

Multiple RotateTransforms with different origins

====== UPDATE 21/07 AFTER CLEMENS POST =======

In the C# part I find the element's center (in this case a TextBlock but could be anything else) and assign the CenterX and CenterY of the first rotation, while keeping the angle at 0 for the moment. Then I apply the translation and the rotation around the canvas center. After the transform group is assigned to the element and adding it to the canvas, I change the first rotation to the desired angle. This seems to take advantage of the CenterX and CenterY of previous transforms being transformed along the way, so I don't have to keep track of the element's center.

public MainWindow()
{
    InitializeComponent();

    TextBlock tblock = new TextBlock() { Text = "A", FontSize = 30, FontWeight = FontWeights.Bold };
    tblock.Measure(new Size());
    tblock.Arrange(new Rect());
    cv.Measure(new Size());
    cv.Arrange(new Rect());

    double cx = (cv.ActualWidth - tblock.ActualWidth) / 2,
           cy = (cv.ActualHeight - tblock.ActualHeight) / 2;

    TransformGroup tgroup = new TransformGroup();
    tgroup.Children.Add(new RotateTransform() { Angle = 0, CenterX = tblock.ActualWidth / 2, CenterY = tblock.ActualHeight / 2 });
    tgroup.Children.Add(new TranslateTransform() { X = cx, Y = cy - 100 });
    tgroup.Children.Add(new RotateTransform() { Angle = 90, CenterX = cx , CenterY = cy });
    tblock.RenderTransform = tgroup;

    cv.Children.Add(tblock);

    ((RotateTransform)((TransformGroup)(tblock).RenderTransform).Children[0]).Angle = -90;
}

Then the text faces the reader after the first rotation:

Text rotated -90 around its center, after being rotated 90 around canvas center


Solution

  • ... how can I use the relative coordinates for a transform given the fact that the transform group has other transforms with different centers that have to be preserved?

    You would prepend the back-rotation - or any other transform that should act first, without being affected by the other transforms - by adding it as first child element of the TransformGroup. The rotation is around the TextBlock's center point because its RenderTransformOrigin is 0.5,0.5.

    var tblock = new TextBlock
    {
        Text = "A",
        FontSize = 30,
        FontWeight = FontWeights.Bold,
        RenderTransformOrigin = new Point(0.5, 0.5)
    };
    
    cv.Children.Add(tblock);
    cv.Measure(new Size());
    cv.Arrange(new Rect());
    
    var cx = (cv.ActualWidth - tblock.ActualWidth) / 2;
    var cy = (cv.ActualHeight - tblock.ActualHeight) / 2;
    
    var tgroup = new TransformGroup();
    tgroup.Children.Add(new RotateTransform());
    tgroup.Children.Add(new TranslateTransform(cx, cy - 100));
    tgroup.Children.Add(new RotateTransform(90, cx, cy));
    
    tblock.RenderTransform = tgroup;
    

    You may now (or any time later) modify the Angle of the first RotateTransform like this:

    ((RotateTransform)((TransformGroup)tblock.RenderTransform).Children[0]).Angle = -90;
    

    Instead of a TransformGroup you may perhaps also use a MatrixTransform like shown below. In order to prepend a rotation around the TextBlock center, use the RotatePrepend method.

    var matrix = new Matrix();
    matrix.Translate(cx, cy - 100);
    matrix.RotateAt(90, cx, cy);
    
    // prepend here
    matrix.RotatePrepend(-90);
    
    tblock.RenderTransform = new MatrixTransform(matrix);
    

    If necessary, you can keep the initial TransformGroup and still prepend the rotation by matrix manipulation. Get the current transform matrix from the Value property of the TransformGroup.

    var matrix = tgroup.Value;
    matrix.RotatePrepend(-90);
    
    tblock.RenderTransform = new MatrixTransform(matrix);