// (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. using System; using System.Windows; using System.Windows.Controls; using System.Windows.Markup; using System.Windows.Media; #if !SILVERLIGHT using System.IO; using System.Text; #endif namespace System.Windows.Controls.DataVisualization { /// /// Control that implements support for transformations as if applied by /// LayoutTransform (which does not exist in Silverlight). /// [ContentProperty("Child")] internal class LayoutTransformControl : Control { #region public FrameworkElement Child /// /// Gets or sets the single child of the LayoutTransformControl. /// /// /// Corresponds to Windows Presentation Foundation's Decorator.Child /// property. /// public FrameworkElement Child { get { return (FrameworkElement)GetValue(ContentProperty); } set { SetValue(ContentProperty, value); } } /// /// Identifies the ContentProperty. /// public static readonly DependencyProperty ContentProperty = DependencyProperty.Register( "Child", typeof(FrameworkElement), typeof(LayoutTransformControl), new PropertyMetadata(ChildChanged)); #endregion public FrameworkElement Child #region public Transform Transform /// /// Gets or sets the Transform of the LayoutTransformControl. /// /// /// Corresponds to UIElement.RenderTransform. /// public Transform Transform { get { return (Transform)GetValue(TransformProperty); } set { SetValue(TransformProperty, value); } } /// /// Identifies the TransformProperty dependency property. /// public static readonly DependencyProperty TransformProperty = DependencyProperty.Register( "Transform", typeof(Transform), typeof(LayoutTransformControl), new PropertyMetadata(TransformChanged)); #endregion /// /// Value used to work around double arithmetic rounding issues in /// Silverlight. /// private const double AcceptableDelta = 0.0001; /// /// Value used to work around double arithmetic rounding issues in /// Silverlight. /// private const int DecimalsAfterRound = 4; /// /// Host panel for Child element. /// private Panel _layoutRoot; /// /// RenderTransform/MatrixTransform applied to layout root. /// private MatrixTransform _matrixTransform; /// /// Transformation matrix corresponding to matrix transform. /// private Matrix _transformation; /// /// Actual DesiredSize of Child element. /// private Size _childActualSize = Size.Empty; /// /// Initializes a new instance of the LayoutTransformControl class. /// public LayoutTransformControl() { // Can't tab to LayoutTransformControl IsTabStop = false; #if SILVERLIGHT // Disable layout rounding because its rounding of values confuses // things. UseLayoutRounding = false; #endif // Hard coded template is never meant to be changed and avoids the // need for generic.xaml. string templateXaml = @"" + "" + "" + "" + "" + "" + ""; #if SILVERLIGHT Template = (ControlTemplate)XamlReader.Load(templateXaml); #else using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(templateXaml))) { Template = (ControlTemplate)XamlReader.Load(stream); } #endif } /// /// Called whenever the control's template changes. /// public override void OnApplyTemplate() { // Save existing content and remove it from the visual tree FrameworkElement savedContent = Child; Child = null; // Apply new template base.OnApplyTemplate(); // Find template parts _layoutRoot = GetTemplateChild("LayoutRoot") as Panel; _matrixTransform = GetTemplateChild("MatrixTransform") as MatrixTransform; // Restore saved content Child = savedContent; // Apply the current transform TransformUpdated(); } /// /// Handle changes to the child dependency property. /// /// The source of the event. /// Information about the event. private static void ChildChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) { // Casts are safe because Silverlight is enforcing the types ((LayoutTransformControl)o).OnChildChanged((FrameworkElement)e.NewValue); } /// /// Updates content when the child property is changed. /// /// The new child. private void OnChildChanged(FrameworkElement newContent) { if (null != _layoutRoot) { // Clear current child _layoutRoot.Children.Clear(); if (null != newContent) { // Add the new child to the tree _layoutRoot.Children.Add(newContent); } // New child means re-layout is necessary InvalidateMeasure(); } } /// /// Handles changes to the Transform DependencyProperty. /// /// The source of the event. /// Information about the event. private static void TransformChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) { // Casts are safe because Silverlight is enforcing the types ((LayoutTransformControl)o).OnTransformChanged((Transform)e.NewValue); } /// /// Processes the transform when the transform is changed. /// /// The transform to process. private void OnTransformChanged(Transform newValue) { ProcessTransform(newValue); } /// /// Notifies the LayoutTransformControl that some aspect of its /// Transform property has changed. /// /// /// Call this to update the LayoutTransform in cases where /// LayoutTransformControl wouldn't otherwise know to do so. /// public void TransformUpdated() { ProcessTransform(Transform); } /// /// Processes the current transform to determine the corresponding /// matrix. /// /// The transform to use to determine the /// matrix. private void ProcessTransform(Transform transform) { // Get the transform matrix and apply it _transformation = RoundMatrix(GetTransformMatrix(transform), DecimalsAfterRound); if (null != _matrixTransform) { _matrixTransform.Matrix = _transformation; } // New transform means re-layout is necessary InvalidateMeasure(); } /// /// Walks the Transform and returns the corresponding matrix. /// /// The transform to create a matrix for. /// /// The matrix calculated from the transform. private Matrix GetTransformMatrix(Transform transform) { if (null != transform) { // WPF equivalent of this entire method: // return transform.Value; // Process the TransformGroup TransformGroup transformGroup = transform as TransformGroup; if (null != transformGroup) { Matrix groupMatrix = Matrix.Identity; foreach (Transform child in transformGroup.Children) { groupMatrix = MatrixMultiply(groupMatrix, GetTransformMatrix(child)); } return groupMatrix; } // Process the RotateTransform RotateTransform rotateTransform = transform as RotateTransform; if (null != rotateTransform) { double angle = rotateTransform.Angle; double angleRadians = (2 * Math.PI * angle) / 360; double sine = Math.Sin(angleRadians); double cosine = Math.Cos(angleRadians); return new Matrix(cosine, sine, -sine, cosine, 0, 0); } // Process the ScaleTransform ScaleTransform scaleTransform = transform as ScaleTransform; if (null != scaleTransform) { double scaleX = scaleTransform.ScaleX; double scaleY = scaleTransform.ScaleY; return new Matrix(scaleX, 0, 0, scaleY, 0, 0); } // Process the SkewTransform SkewTransform skewTransform = transform as SkewTransform; if (null != skewTransform) { double angleX = skewTransform.AngleX; double angleY = skewTransform.AngleY; double angleXRadians = (2 * Math.PI * angleX) / 360; double angleYRadians = (2 * Math.PI * angleY) / 360; return new Matrix(1, angleYRadians, angleXRadians, 1, 0, 0); } // Process the MatrixTransform MatrixTransform matrixTransform = transform as MatrixTransform; if (null != matrixTransform) { return matrixTransform.Matrix; } // TranslateTransform has no effect in LayoutTransform } // Fall back to no-op transformation return Matrix.Identity; } /// /// Provides the behavior for the "Measure" pass of layout. /// /// The available size that this element can /// give to child elements. Infinity can be specified as a value to /// indicate that the element will size to whatever content is /// available. /// The size that this element determines it needs during /// layout, based on its calculations of child element sizes. protected override Size MeasureOverride(Size availableSize) { FrameworkElement child = Child; if ((null == _layoutRoot) || (null == child)) { // No content, no size return Size.Empty; } Size measureSize; if (_childActualSize == Size.Empty) { // Determine the largest size after the transformation measureSize = ComputeLargestTransformedSize(availableSize); } else { // Previous measure/arrange pass determined that // Child.DesiredSize was larger than believed. measureSize = _childActualSize; } // Perform a mesaure on the _layoutRoot (containing Child) _layoutRoot.Measure(measureSize); // WPF equivalent of _childActualSize technique (much simpler, but // doesn't work on Silverlight 2). If the child is going to render // larger than the available size, re-measure according to that size. ////child.Arrange(new Rect()); ////if (child.RenderSize != child.DesiredSize) ////{ //// _layoutRoot.Measure(child.RenderSize); ////} // Transform DesiredSize to find its width/height Rect transformedDesiredRect = RectTransform(new Rect(0, 0, _layoutRoot.DesiredSize.Width, _layoutRoot.DesiredSize.Height), _transformation); Size transformedDesiredSize = new Size(transformedDesiredRect.Width, transformedDesiredRect.Height); // Return result to allocate enough space for the transformation return transformedDesiredSize; } /// /// Provides the behavior for the "Arrange" pass of layout. /// /// The final area within the parent that this /// element should use to arrange itself and its children. /// The actual size used. /// /// Using the WPF paramater name finalSize instead of Silverlight's /// finalSize for clarity. /// protected override Size ArrangeOverride(Size finalSize) { FrameworkElement child = Child; if ((null == _layoutRoot) || (null == child)) { // No child, use whatever was given return finalSize; } // Determine the largest available size after the transformation Size finalSizeTransformed = ComputeLargestTransformedSize(finalSize); if (IsSizeSmaller(finalSizeTransformed, _layoutRoot.DesiredSize)) { // Some elements do not like being given less space than they asked for (ex: TextBlock) // Bump the working size up to do the right thing by them finalSizeTransformed = _layoutRoot.DesiredSize; } // Transform the working size to find its width/height Rect transformedRect = RectTransform(new Rect(0, 0, finalSizeTransformed.Width, finalSizeTransformed.Height), _transformation); // Create the Arrange rect to center the transformed content Rect finalRect = new Rect( -transformedRect.Left + ((finalSize.Width - transformedRect.Width) / 2), -transformedRect.Top + ((finalSize.Height - transformedRect.Height) / 2), finalSizeTransformed.Width, finalSizeTransformed.Height); // Perform an Arrange on _layoutRoot (containing Child) _layoutRoot.Arrange(finalRect); // This is the first opportunity under Silverlight to find out the Child's true DesiredSize if (IsSizeSmaller(finalSizeTransformed, child.RenderSize) && (Size.Empty == _childActualSize)) { // Unfortunately, all the work so far is invalid because the wrong DesiredSize was used // Make a note of the actual DesiredSize _childActualSize = new Size(child.ActualWidth, child.ActualHeight); // Force a new measure/arrange pass InvalidateMeasure(); } else { // Clear the "need to measure/arrange again" flag _childActualSize = Size.Empty; } // Return result to perform the transformation return finalSize; } /// /// Computes the largest usable size after applying the transformation /// to the specified bounds. /// /// The size to arrange within. /// The size required. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Closely corresponds to WPF's FrameworkElement.FindMaximalAreaLocalSpaceRect.")] private Size ComputeLargestTransformedSize(Size arrangeBounds) { // Computed largest transformed size Size computedSize = Size.Empty; // Detect infinite bounds and constrain the scenario bool infiniteWidth = double.IsInfinity(arrangeBounds.Width); if (infiniteWidth) { arrangeBounds.Width = arrangeBounds.Height; } bool infiniteHeight = double.IsInfinity(arrangeBounds.Height); if (infiniteHeight) { arrangeBounds.Height = arrangeBounds.Width; } // Capture the matrix parameters double a = _transformation.M11; double b = _transformation.M12; double c = _transformation.M21; double d = _transformation.M22; // Compute maximum possible transformed width/height based on starting width/height // These constraints define two lines in the positive x/y quadrant double maxWidthFromWidth = Math.Abs(arrangeBounds.Width / a); double maxHeightFromWidth = Math.Abs(arrangeBounds.Width / c); double maxWidthFromHeight = Math.Abs(arrangeBounds.Height / b); double maxHeightFromHeight = Math.Abs(arrangeBounds.Height / d); // The transformed width/height that maximize the area under each segment is its midpoint // At most one of the two midpoints will satisfy both constraints double idealWidthFromWidth = maxWidthFromWidth / 2; double idealHeightFromWidth = maxHeightFromWidth / 2; double idealWidthFromHeight = maxWidthFromHeight / 2; double idealHeightFromHeight = maxHeightFromHeight / 2; // Compute slope of both constraint lines double slopeFromWidth = -(maxHeightFromWidth / maxWidthFromWidth); double slopeFromHeight = -(maxHeightFromHeight / maxWidthFromHeight); if ((0 == arrangeBounds.Width) || (0 == arrangeBounds.Height)) { // Check for empty bounds computedSize = new Size(0, 0); } else if (infiniteWidth && infiniteHeight) { // Check for completely unbound scenario computedSize = new Size(double.PositiveInfinity, double.PositiveInfinity); } else if (!MatrixHasInverse(_transformation)) { // Check for singular matrix computedSize = new Size(0, 0); } else if ((0 == b) || (0 == c)) { // Check for 0/180 degree special cases double maxHeight = (infiniteHeight ? double.PositiveInfinity : maxHeightFromHeight); double maxWidth = (infiniteWidth ? double.PositiveInfinity : maxWidthFromWidth); if ((0 == b) && (0 == c)) { // No constraints computedSize = new Size(maxWidth, maxHeight); } else if (0 == b) { // Constrained by width double computedHeight = Math.Min(idealHeightFromWidth, maxHeight); computedSize = new Size( maxWidth - Math.Abs((c * computedHeight) / a), computedHeight); } else if (0 == c) { // Constrained by height double computedWidth = Math.Min(idealWidthFromHeight, maxWidth); computedSize = new Size( computedWidth, maxHeight - Math.Abs((b * computedWidth) / d)); } } else if ((0 == a) || (0 == d)) { // Check for 90/270 degree special cases double maxWidth = (infiniteHeight ? double.PositiveInfinity : maxWidthFromHeight); double maxHeight = (infiniteWidth ? double.PositiveInfinity : maxHeightFromWidth); if ((0 == a) && (0 == d)) { // No constraints computedSize = new Size(maxWidth, maxHeight); } else if (0 == a) { // Constrained by width double computedHeight = Math.Min(idealHeightFromHeight, maxHeight); computedSize = new Size( maxWidth - Math.Abs((d * computedHeight) / b), computedHeight); } else if (0 == d) { // Constrained by height. double computedWidth = Math.Min(idealWidthFromWidth, maxWidth); computedSize = new Size( computedWidth, maxHeight - Math.Abs((a * computedWidth) / c)); } } else if (idealHeightFromWidth <= ((slopeFromHeight * idealWidthFromWidth) + maxHeightFromHeight)) { // Check the width midpoint for viability (by being below the // height constraint line). computedSize = new Size(idealWidthFromWidth, idealHeightFromWidth); } else if (idealHeightFromHeight <= ((slopeFromWidth * idealWidthFromHeight) + maxHeightFromWidth)) { // Check the height midpoint for viability (by being below the // width constraint line). computedSize = new Size(idealWidthFromHeight, idealHeightFromHeight); } else { // Neither midpoint is viable; use the intersection of the two // constraint lines instead. // Compute width by setting heights equal (m1*x+c1=m2*x+c2). double computedWidth = (maxHeightFromHeight - maxHeightFromWidth) / (slopeFromWidth - slopeFromHeight); // Compute height from width constraint line (y=m*x+c; using // height would give same result). computedSize = new Size( computedWidth, (slopeFromWidth * computedWidth) + maxHeightFromWidth); } return computedSize; } /// /// Return true if Size a is smaller than Size b in either dimension. /// /// The left size. /// The right size. /// A value indicating whether the left size is smaller than /// the right. private static bool IsSizeSmaller(Size a, Size b) { // WPF equivalent of following code: // return ((a.Width < b.Width) || (a.Height < b.Height)); return ((a.Width + AcceptableDelta < b.Width) || (a.Height + AcceptableDelta < b.Height)); } /// /// Rounds the non-offset elements of a matrix to avoid issues due to /// floating point imprecision. /// /// The matrix to round. /// The number of decimals after the /// round. /// The rounded matrix. private static Matrix RoundMatrix(Matrix matrix, int decimalsAfterRound) { return new Matrix( Math.Round(matrix.M11, decimalsAfterRound), Math.Round(matrix.M12, decimalsAfterRound), Math.Round(matrix.M21, decimalsAfterRound), Math.Round(matrix.M22, decimalsAfterRound), matrix.OffsetX, matrix.OffsetY); } /// /// Implement Windows Presentation Foundation's Rect.Transform on /// Silverlight. /// /// The rectangle to transform. /// The matrix to use to transform the rectangle. /// /// The transformed rectangle. private static Rect RectTransform(Rect rectangle, Matrix matrix) { // WPF equivalent of following code: // var rectTransformed = Rect.Transform(rect, matrix); Point leftTop = matrix.Transform(new Point(rectangle.Left, rectangle.Top)); Point rightTop = matrix.Transform(new Point(rectangle.Right, rectangle.Top)); Point leftBottom = matrix.Transform(new Point(rectangle.Left, rectangle.Bottom)); Point rightBottom = matrix.Transform(new Point(rectangle.Right, rectangle.Bottom)); double left = Math.Min(Math.Min(leftTop.X, rightTop.X), Math.Min(leftBottom.X, rightBottom.X)); double top = Math.Min(Math.Min(leftTop.Y, rightTop.Y), Math.Min(leftBottom.Y, rightBottom.Y)); double right = Math.Max(Math.Max(leftTop.X, rightTop.X), Math.Max(leftBottom.X, rightBottom.X)); double bottom = Math.Max(Math.Max(leftTop.Y, rightTop.Y), Math.Max(leftBottom.Y, rightBottom.Y)); Rect rectTransformed = new Rect(left, top, right - left, bottom - top); return rectTransformed; } /// /// Implements Windows Presentation Foundation's Matrix.Multiply on /// Silverlight. /// /// The left matrix. /// The right matrix. /// The product of the two matrices. private static Matrix MatrixMultiply(Matrix matrix1, Matrix matrix2) { // WPF equivalent of following code: // return Matrix.Multiply(matrix1, matrix2); return new Matrix( (matrix1.M11 * matrix2.M11) + (matrix1.M12 * matrix2.M21), (matrix1.M11 * matrix2.M12) + (matrix1.M12 * matrix2.M22), (matrix1.M21 * matrix2.M11) + (matrix1.M22 * matrix2.M21), (matrix1.M21 * matrix2.M12) + (matrix1.M22 * matrix2.M22), ((matrix1.OffsetX * matrix2.M11) + (matrix1.OffsetY * matrix2.M21)) + matrix2.OffsetX, ((matrix1.OffsetX * matrix2.M12) + (matrix1.OffsetY * matrix2.M22)) + matrix2.OffsetY); } /// /// Implements Windows Presentation Foundation's Matrix.HasInverse on /// Silverlight. /// /// The matrix. /// True if matrix has an inverse. private static bool MatrixHasInverse(Matrix matrix) { // WPF equivalent of following code: // return matrix.HasInverse; return (0 != ((matrix.M11 * matrix.M22) - (matrix.M12 * matrix.M21))); } } }