3

I have a control sitting somewhere in my Window. At the root of this Window is a grid named MainGrid. I have a ScaleTransform applied on MainGrid's LayoutTransform that I use to zoom in on all the contents of my Window when the window's size grows. However, one of my controls usesBitmapCache on their canvases to optimize drawing performance. BitmapCache does not take into consideration any ScaleTransforms that might be applied to the control, so if I'm zoomed in, the control appears blurry.

BitmapCache does have a RenderAtScale property I can use to increase the scale of the cached image. However, the problem I have is that I don't know of an elegant way to find out what that Scale value needs to be. For now, I have a property on my control so that my Window can pass its scale value to the control. However, I would like it if I didn't need to rely on some external source passing in a scale value.

Is there any way a Control can get a summary of all the ScaleTransforms applied to it?

Dave Clemmer
  • 3,741
  • 12
  • 49
  • 72
JacobJ
  • 3,677
  • 3
  • 28
  • 32

5 Answers5

3

You can use the PresentationSource's RootVisual to calculate the total transform of an element. The general pattern to find the scale applied between a parent and a child is

Visual child = /* some visual */ this;
Visual parent = PresentationSource.FromVisual(child).RootVisual;

// calculate the transform needed to go from the parent to the child coordinate spaces
var childTransform = parent.TransformToDescendant(child);

// use the transform to scale a 1x1 rectangle
// use Inverse as the transform is from Parent coordinates to child, we want 
// child to parent
var unitRect = new Rectangle(0, 0, 1, 1);
var transformedUnit = childTransform.Inverse.TransformBounds(unitRect);

// transformedUnit.Width and transformedUnit.Height are now the scale factors (X and Y)
Debug.Assert(transformedUnit.Width == 0.25 && transformedUnit.Height == 0.25);
Mitch
  • 21,223
  • 6
  • 63
  • 86
  • This will however ignore any scale transforms on the way. For instance: I have a window with a slider to control scale. It is not useful to apply that scale transform rhght away to the whole "Window" (which is the RootVisual). Instead a "MainWindowControl" is taking this place and gets the ScaleTransform. Therefore it is important to sum up. (Multiply matrixes) – Robetto Oct 20 '14 at 10:33
  • @Robetto, I don't understand. The `TransformToDescendant` call will take into account all of the `.Transform`s applied in the visual tree between the nodes you specify - regardless of the transform type. "This while loop will walk up the visual tree until we encounter the ancestor. As it does so, it will accumulate the descendent->ancestor transform." (http://referencesource.microsoft.com/#PresentationCore/src/Core/CSharp/System/Windows/Media/Visual.cs#46e3a62ee137ecab#references) – Mitch Oct 22 '14 at 17:44
  • I don't see any recursion or loops in the code above. But the track is the right one. Go up by multiplying inverse on a unit square or go from the root by multiplying the transforms (but all). That was my point – Robetto Oct 23 '14 at 20:45
2

As Elad Katz said, I don't think there's any direct way to find out if a ScaleTransform is applied to any parent without going through them.

However, if you know that the ScaleTransform is applied to the MainGrid, then you can bind RenderAtScale to the ScaleX or ScaleY of the MainGrid's ScaleTransform. Maybe this is what you're already doing but I throw it in as a tip anyway

Probably the easiest way to reference the ScaleTransform in the Binding is by naming it

<Grid Name="MainGrid">
    <Grid.LayoutTransform>
        <TransformGroup>
            <ScaleTransform x:Name="MainGridScaleTransform" .../>
        </TransformGroup>            
    </Grid.LayoutTransform>

The Binding should then look similar to this

<BitmapCache RenderAtScale="{Binding ElementName=MainGridScaleTransform,
                                     Path=ScaleX}"/>

Otherwise, you can get the ScaleTransform in the Path with TransformGroup

<BitmapCache RenderAtScale="{Binding ElementName=MainGrid,
 Path=(LayoutTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleX)}"/>

Or without TransformGroup

<BitmapCache RenderAtScale="{Binding ElementName=MainGrid,
                              Path=(LayoutTransform).(ScaleTransform.ScaleX)}"/>
Fredrik Hedblad
  • 83,499
  • 23
  • 264
  • 266
  • I get what you're saying. For me, the BitmapCache isn't accessible by my Window. It's stored in the control itself (and said control is actually defined in a separate control library binary), but I understand what you're driving at with the binding and could use that for the "AppliedScale" dependency property I added to the control. – JacobJ Feb 27 '11 at 19:26
2

Based on Elad Katz's recommendations, I have created some code to scan up the VisualTree to attempt to total any uniform ScaleTransforms. If any one has some optimizations for this algorithms or can think of a few things I may not be considering, let me know. Again, my goal is to get a RenderAtScale that is actually apropriate given the currently applied ScaleTransforms. OnRenderSizeChanged seems to be an OK place to do this, as it happens AFTER all the layout stuff has run. But, perhaps there is a better place to trigger GetTotalTransformScale()?

protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
    base.OnRenderSizeChanged(sizeInfo);
    double totalTransformScale = GetTotalTransformScale();
    BitmapCache bitmapCache = (BitmapCache)MyCachedCanvas.CacheMode;
    if (bitmapCache.RenderAtScale != totalTransformScale)
        bitmapCache.RenderAtScale = totalTransformScale;
}

private double GetTotalTransformScale()
{
    double totalTransform = 1.0d;    
    DependencyObject currentVisualTreeElement = this;            
    do
    {
        Visual visual = currentVisualTreeElement as Visual;
        if (visual != null)
        {
            Transform transform = VisualTreeHelper.GetTransform(visual);

            // This condition is a way of determining if it
            // was a uniform scale transform. Is there some better way?
            if ((transform != null) &&
                (transform.Value.M12 == 0) &&
                (transform.Value.M21 == 0) &&
                (transform.Value.OffsetX == 0) &&
                (transform.Value.OffsetY == 0) &&
                (transform.Value.M11 == transform.Value.M22))
            {                        
                totalTransform *= transform.Value.M11;
            }
        }
        currentVisualTreeElement = VisualTreeHelper.GetParent(currentVisualTreeElement);
    }
    while (currentVisualTreeElement != null);

    return totalTransform;
}
Dave Clemmer
  • 3,741
  • 12
  • 49
  • 72
JacobJ
  • 3,677
  • 3
  • 28
  • 32
  • 2
    I just want to mention that this works only for the special case where we have Scale tranforms only and X and Y scale (M11, M22) are the same. This is most of the thime the case, but does except special cases. – Robetto Oct 23 '14 at 20:47
1

You can always recursively go through all of the Control's parents and sum their ScaleTransform.

I don't think you can do it any other way.

Dave Clemmer
  • 3,741
  • 12
  • 49
  • 72
Elad Katz
  • 7,483
  • 5
  • 35
  • 66
  • Is there a concise/elegant way to get change notification for all of the ScaleTransform changes in my control's VisualTree? If so, I may just add a helper method to all of my controls that does exactly what you suggested - recursively runs through the visual tree to get the sum of ScaleTransforms I need to apply. – JacobJ Feb 27 '11 at 19:28
  • you can recursively register for the changes in the ScaleTransorms of the parents also. same helper class. – Elad Katz Feb 27 '11 at 19:44
0

I know this is an old question, but I had a similar problem myself: I needed to make one type of control to ignore the ScaleTransform on the top level.

UIElement has a RenderTransform property which is basically the ScaleTransform effecting it.

Dave Clemmer
  • 3,741
  • 12
  • 49
  • 72
teemu
  • 343
  • 2
  • 7