Search code examples
wpfxamlmvvmscaleviewbox

Removing or setting an absolute transform of XAML Element


I have a need to have a XAML object always be scaled 1:1, or atleast the imagebrush content, even though it's parent is in a viewbox and the content is compressed in the X direction.

An example: Viewbox contains Label & ImageBrush. I'd like the label text to scale, but only the ImageBrush size - when zoomed out it would only display the top corner of the content.

The viewmodel for the object does not have access to the scale factor. I've been looking for a way to remove or reset the viewbox transform, but I've been unable to find one. Is there one or will I have to propagate the current scale factor from the parent to the end viewmodel? I'd rather not mix presentation logic there unless I absolutely must.

Here's the current XAML I've got so far:

           <ImageBrush ImageSource="{Binding Paint, Mode=OneWay, Converter={StaticResource BitmapToImageSourceConverter}}">
                <ImageBrush.RelativeTransform>
                    <MatrixTransform>
                        <MatrixTransform.Matrix>
                            <MultiBinding Converter="{StaticResource TimelineMatrixConverter}">
                                <Binding />
                                <Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type MatrixTransform}}" Path="Matrix" />
                            </MultiBinding>
                        </MatrixTransform.Matrix>
                    </MatrixTransform>
                </ImageBrush.RelativeTransform>
            </ImageBrush>

Solution

  • Here is a basic exemple of what you can do. First extend the Viewbox class:

    public class ViewboxEx : Viewbox
    {
    
        private FrameworkElement _child;
    
        #region InvertScaleH
    
        /// <summary>
        /// InvertScaleH Read-Only Dependency Property
        /// </summary>
        private static readonly DependencyPropertyKey InvertScaleHPropertyKey
            = DependencyProperty.RegisterReadOnly("InvertScaleH", typeof(double), typeof(ViewboxEx),
                new FrameworkPropertyMetadata((double)1));
    
        public static readonly DependencyProperty InvertScaleHProperty
            = InvertScaleHPropertyKey.DependencyProperty;
    
        /// <summary>
        /// Gets the InvertScaleH property. This dependency property 
        /// indicates invert scale factor to compensate for horizontal scale fo the Viewbox.
        /// </summary>
        public double InvertScaleH
        {
            get { return (double)GetValue(InvertScaleHProperty); }
        }
    
        /// <summary>
        /// Provides a secure method for setting the InvertScaleH property.  
        /// This dependency property indicates invert scale factor to compensate for horizontal scale fo the Viewbox.
        /// </summary>
        /// <param name="value">The new value for the property.</param>
        protected void SetInvertScaleH(double value)
        {
            SetValue(InvertScaleHPropertyKey, value);
        }
    
        #endregion
    
        #region InvertScaleV
    
        /// <summary>
        /// InvertScaleV Read-Only Dependency Property
        /// </summary>
        private static readonly DependencyPropertyKey InvertScaleVPropertyKey
            = DependencyProperty.RegisterReadOnly("InvertScaleV", typeof(double), typeof(ViewboxEx),
                new FrameworkPropertyMetadata((double)1));
    
        public static readonly DependencyProperty InvertScaleVProperty
            = InvertScaleVPropertyKey.DependencyProperty;
    
        /// <summary>
        /// Gets the InvertScaleV property. This dependency property 
        /// indicates invert scale factor to compensate for vertical scale fo the Viewbox.
        /// </summary>
        public double InvertScaleV
        {
            get { return (double)GetValue(InvertScaleVProperty); }
        }
    
        /// <summary>
        /// Provides a secure method for setting the InvertScaleV property.  
        /// This dependency property indicates invert scale factor to compensate for vertical scale fo the Viewbox.
        /// </summary>
        /// <param name="value">The new value for the property.</param>
        protected void SetInvertScaleV(double value)
        {
            SetValue(InvertScaleVPropertyKey, value);
        }
    
        #endregion
    
        public ViewboxEx()
        {
            Loaded += OnLoaded;
            SizeChanged += (_,__) => UpdateScale();
        }
    
        private void OnLoaded(object sender, RoutedEventArgs e)
        {
            UpdateChild();
        }
    
        protected override void OnVisualChildrenChanged(DependencyObject visualAdded, DependencyObject visualRemoved)
        {
            base.OnVisualChildrenChanged(visualAdded, visualRemoved);
    
            UpdateChild();
        }
    
        private void UpdateChild()
        {
            if (_child != null)
            {
                _child.SizeChanged -= OnChild_SizeChanged;
            }
            _child = Child as FrameworkElement;
            if (_child != null)
            {
                _child.SizeChanged += OnChild_SizeChanged;
            }
    
            UpdateScale();
        }
    
        private void OnChild_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            UpdateScale();
        }
    
        private void UpdateScale()
        {
            if (_child == null)
                return;
            SetInvertScaleH(_child.ActualWidth / ActualWidth);
            SetInvertScaleV(_child.ActualHeight / ActualHeight);
        }
    
    }
    

    Basically what it does is to listen for the changes of the size of the Viewbox and its content and then calculate the invert scale factor. Now to use this scale factor:

    <Window x:Class="YourNamespace.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:local="clr-namespace:YourNamespace"
            Title="MainWindow" Height="450" Width="800">
        <Grid>
            <local:ViewboxEx>
                <Grid Width="100" Height="100">
                    <Ellipse Fill="Green"/>
                    <TextBlock Text="123" Margin="10,10,0,0">
                        <TextBlock.RenderTransform>
                            <ScaleTransform 
                                ScaleX="{Binding InvertScaleH, RelativeSource={RelativeSource AncestorType={x:Type local:ViewboxEx}}}"
                                ScaleY="{Binding InvertScaleV, RelativeSource={RelativeSource AncestorType={x:Type local:ViewboxEx}}}"/>
                        </TextBlock.RenderTransform>
                    </TextBlock>
                </Grid>
            </local:ViewboxEx>
        </Grid>
    </Window>
    

    In this exemple the Ellipse follows the Viewbox size change as usual but the text keeps its size.

    This is a basic implementation and may not work in all cases.