Working around lack of LayoutTransform in Silverlight

August 5, 2009

So I’m working on a ChartingLibrary at the moment that has to compile for both Silverlight and WPF.  I can across a particularly nasty problem with axis tick labels.  So the requirement is that the tick labels can be rotated.  In WPF this is a doddle – you just use a RotateTransform and set it as the LayoutTransform in a TextBlock.  In Silverlight, however, you only have the RenderTransform to play with.  This is the solution I came up with:

    [TemplatePart(Name = PART_TEXT_BLOCK, Type = typeof(TextBlock))]
    public class TickLabel : Control
    {
        #region Private Fields
        private TextBlock _textBlock;
        private RotateTransform _rotateTransform;
#if SILVERLIGHT
        private TranslateTransform _translateTransform;
#endif
        #endregion

        #region Ctor
        public TickLabel()
        {
            base.DefaultStyleKey = typeof(TickLabel);
        }
        #endregion

        #region Dependency Properties

        #region Text
        public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
            "Text", typeof(string), typeof(TickLabel), null);

        public string Text
        {
            get { return base.GetValue(TextProperty) as string; }
            set { base.SetValue(TextProperty, value); }
        }
        #endregion

        #region Angle
        public static readonly DependencyProperty AngleProperty = DependencyProperty.Register(
            "Angle", typeof(double), typeof(TickLabel),
            new PropertyMetadata(0.0, OnAngleChanged));

        public double Angle
        {
            get { return (double)base.GetValue(AngleProperty); }
            set { base.SetValue(AngleProperty, value); }
        }

        private static void OnAngleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var instance = (TickLabel) d;
            var angle = (double) e.NewValue;
            instance.OnAngleChanged(angle);
        }

        private void OnAngleChanged(double angle)
        {
            _rotateTransform.Angle = angle;
#if SILVERLIGHT
            // Have to manually translate for Silverlight
            var width = _textBlock.ActualWidth;
            var height = _textBlock.ActualHeight;
            var sourceCenter = new Point(width / 2, height / 2);
            var destinationCenter = _rotateTransform.Transform(sourceCenter);
            var sourceRect = new Rect { Width = width, Height = height };
            var destinationRect = _rotateTransform.TransformBounds(sourceRect);
            _translateTransform.X = destinationRect.Width/2 - destinationCenter.X;
            _translateTransform.Y = destinationRect.Height/2 - destinationCenter.Y;
            base.InvalidateMeasure();
#endif
        }
        #endregion

        #endregion

        #region FrameworkElement Members
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            _textBlock = base.GetTemplateChild(PART_TEXT_BLOCK) as TextBlock;
            if (_textBlock == null)
            {
                throw new Exception(string.Format("TemplatePart: {0} missing.", PART_TEXT_BLOCK));
            }

            _rotateTransform = new RotateTransform { Angle = this.Angle };
#if SILVERLIGHT
            // No LayoutTransform in Silverlight so we have to use RenderTransform
            var transformGroup = new TransformGroup();
            transformGroup.Children.Add(_rotateTransform);
            _translateTransform = new TranslateTransform();
            transformGroup.Children.Add(_translateTransform);
            _textBlock.RenderTransform = transformGroup;
#else
            _textBlock.LayoutTransform = _rotateTransform;
#endif
        }

#if SILVERLIGHT
        protected override Size MeasureOverride(Size constraint)
        {
            var width = _textBlock.ActualWidth;
            var height = _textBlock.ActualHeight;
            var sourceRect = new Rect { Width = width, Height = height };
            var destinationRect = _rotateTransform.TransformBounds(sourceRect);
            return new Size(destinationRect.Width, destinationRect.Height);
        }
#endif
        #endregion

        #region Constants
        private const string PART_TEXT_BLOCK = "PART_TextBlock";
        #endregion
    }

And the default control template looks like this:

<Style TargetType="local:TickLabel>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:TickLabel">
  <Canvas>
    <TextBlock Name="PART_TextBlock" Text="{TemplateBinding Text}" />
  </Canvas>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

Note that you have to include the TextBlock in a Canvas otherwise it will clip.

Works like a treat!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: