Horizontal image scaling with ScaleTransform in WPF - c#

I have an image in a WPF window with the default Stretch setting, Uniform, and am making an attempt to make it fill the screen horizontally. I do not wish to use a different Stretch setting as this is supposed to a learning experience. The image dimensions being loaded are 420x800. This is the XAML for the window..
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Background="Red" Height="1200" Width="840">
<Image Name="Image" Source="{Binding SourceUri}">
<Image.RenderTransform>
<TransformGroup>
<ScaleTransform x:Name="Scale" />
<TranslateTransform x:Name="Translate" />
</TransformGroup>
</Image.RenderTransform>
</Image>
</Window>
On the code-behind, I am attempting to calculate the scaling to zoom the image to fill the horizontal screen and I am using translate transform the move it to the center of the screen. The following bit of code is obviously wrong...
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace WpfApplication1 {
public partial class MainWindow : Window {
public MainWindow() {
InitializeComponent();
DataContext = this;
double ImageWidth = 420;
Scale.ScaleX = Width / ImageWidth;
Translate.X = -(ImageWidth / 2);
}
public string SourceUri {
get {
return #"C:\Users\Roel\Desktop\test.png";
}
}
}
}
I am attempting to understand how stretching and transformations are working together but I am having difficulty with this. I would appreciate all insights, even references to detailed explanations, as I have trouble finding any informational source explaining clearly and concisely how the transformations are applied.

You would usually just do this:
<Image Name="Image" Source="{Binding SourceUri}" Stretch="Fill"/>
In case you really need to calculate the stretch transformation manually, you would only need a ScaleTransform, no TranslateTransform, and you would put that into the LayoutTransform of the Image control. Moreover, the Image control would have to be placed into a Grid, which provides the size of the Windows's "client area". You can't calculate anything based on the Window's Width (or ActualWidth) as that includes the width of the Window's borders.
<Grid SizeChanged="Grid_SizeChanged">
<Image Name="image" Source="{Binding SourceUri}">
<Image.LayoutTransform>
<ScaleTransform x:Name="scale"/>
</Image.LayoutTransform>
</Image>
</Grid>
In the Grid's SizeChanged handler you would calculate the scaling as shown below.
private void Grid_SizeChanged(object sender, SizeChangedEventArgs e)
{
scale.ScaleX = e.NewSize.Width / image.Source.Width;
scale.ScaleY = e.NewSize.Height / image.Source.Height;
}

Related

Irregular behavior - XAML / UWP Community Toolkit Scale animation

Problem : I am using UWP Community Toolkit Scale animation and it works as expected for most of the images in the GridView, but for some the image goes out of bounds . (Please see the image below)
I have detected that the issue happens when the image width is more than 2x (2 times) the height of the image. That is when the image is very wide.
Code
I am using a user control as data template
Xaml :
<!-- Grid View -->
<GridView x:Name="gridView" SelectionChanged="gridView_SelectionChanged">
<GridView.ItemTemplate>
<DataTemplate>
<local:GridViewMenu/>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
<!-- GridViewMenu User Control markup -->
<Grid>
<StackPanel>
<Image Source="{Binding webformatURL}" Stretch="UniformToFill" PointerEntered="image_PointerEntered" PointerExited="image_PointerExited"/>
</StackPanel>
</Grid>
C# Code :
private void image_PointerEntered(object sender, PointerRoutedEventArgs e)
{
Image img = sender as Image;
img.Scale(centerX: (float)(grid.ActualWidth / 2),
centerY: 100,
scaleX: 1.2f,
scaleY: 1.2f,
duration: 500, delay: 0).StartAsync();
}
private void image_PointerExited(object sender, PointerRoutedEventArgs e)
{
Image img = sender as Image;
img.Scale(centerX: (float)(grid.ActualWidth / 2),
centerY: 100,
scaleX: 1f,
scaleY: 1f,
duration: 500, delay: 0).StartAsync();
}
Result (Top left image is not scaling as expected, that is, it is going out of bounds)
How can I solve this issue ?
The scale animation of UWP Community Toolkit package actually use the CompositeTransform class for scaling. According to the description of Transforms and layout section:
Because layout comes first, you'll sometimes get unexpected results if you transform elements that are in a Grid cell or similar layout container that allocates space during layout. The transformed element may appear truncated or obscured because it's trying to draw into an area that didn't calculate the post-transform dimensions when dividing space within its parent container.
So that the parts overflow the bound that being truncated are unexpected. In another words, the image goes out is the transform expected. The current way you are using to meet your requirements is not reliable. If you change width-height ratio of GridViewMenu to 1.0 , you may find more images that width-height ratio larger than 1.0 will go out.
For a solution inside GridView, you could consider to use the ScrollViewer to zoom in the image instead, which can ensure the image is limited in a fixed area. For example:
<Grid x:Name="grid">
<ScrollViewer
x:Name="currentscroll"
HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Hidden">
<Image
x:Name="myImage"
Width="300"
Height="180"
PointerEntered="image_PointerEntered"
PointerExited="image_PointerExited"
Source="{Binding webformatURL}"
Stretch="UniformToFill">
</Image>
</ScrollViewer>
</Grid>
Code behind:
private void image_PointerEntered(object sender, PointerRoutedEventArgs e)
{
currentscroll.ChangeView(0, 0, 1.2f );
}
private void image_PointerExited(object sender, PointerRoutedEventArgs e)
{
currentscroll.ChangeView(0, 0, 1.0f);
}
You can try to use clipping:
<Grid>
<StackPanel>
<Image Source="{Binding webformatURL}" Stretch="UniformToFill"
PointerEntered="image_PointerEntered"
PointerExited="image_PointerExited">
<Image.Clip>
<RectangleGeometry Rect="0,0,300,150" />
</Image.Clip>
</Image>
</StackPanel>
</Grid>
300 and 150 would be the width and height of the grid item.
Otherwise it looks like a bug in the UWP Community Toolkit, it would be best to report it as an issue on GitHub.

How can I get the new position and size of a UIElement after performing a RenderTransform (Scale & Translate)?

I am performing scaling, translation and rotation on an image using RenderTransform. Transformations are not done using mouse events, rather, the user will click a button and then the image will scale/translate/rotate on a fixed value.
My problem is I want to determine the new position/size of the image each time a scaling, translation or rotation is performed. So I added Changed events on the code-behind. The question is how do you get the new position/size?
Please take a look at what I've done so far:
XAML:
<Border x:Name="mainImageBorderCtrl" ClipToBounds="True">
<Grid x:Name="imageGridCtrl">
<Grid.RenderTransform>
<TranslateTransform Changed="TranslateTransform_Changed"/>
</Grid.RenderTransform>
<Image x:Name="mainImageCtrl" RenderTransformOrigin="0.5, 0.5" Source="{Binding Image}">
<Image.RenderTransform>
<TransformGroup>
<ScaleTransform Changed="ScaleTransform_Changed"/>
<RotateTransform />
</TransformGroup>
</Image.RenderTransform>
</Image>
</Grid>
</Border>
Please note that I separated TranslateTransform from ScaleTransform and RotateTransform so that the orientation won't be affected.
The following are the 2 changed events. In here, I expect to get the new position/size every time a scale or a translate has occurred. But the bounds values do not change.
Code-behind
private Rect bounds;
private void TranslateTransform_Changed(object sender, EventArgs e)
{
bounds = imageGridCtrl.TransformToAncestor(mainImageBorderCtrl).TransformBounds(new Rect(imageGridCtrl.RenderSize));
}
private void ScaleTransform_Changed(object sender, EventArgs e)
{
bounds = imageGridCtrl.TransformToAncestor(mainImageBorderCtrl).TransformBounds(new Rect(imageGridCtrl.RenderSize));
}
EDIT: My goal here is to restrict the translation(panning) of the Image inside the Border control, that's why I need to get the bounds of the Image after each transformation, whatever the transformation is, so that I can check whether the bounds of the Image exceeds the Border.
TranslateTransform is generating correct bounds. However, for ScaleTransform you are getting bounds on control imageGridCtrl but the transform is added as child of control mainImageCtrl.
So you should use mainImageCtrl in place of imageGridCtrl to get correct bounds:
bounds = mainImageCtrl.TransformToAncestor(mainImageBorderCtrl)
.TransformBounds(new Rect(mainImageCtrl.RenderSize));
The transform change events fire too early. You should hook up to LayoutUpdated event of the image control. Then try:
mainImageCtrl.TransformToAncestor(mainImageBorderCtrl)
.TransformBounds(new Rect(mainImageCtrl.RenderSize))

MeasureOverride and ArrangeOverride are not called

I'm using ZoomControl from WPFExtensions. Source code is here.
I've added adorner with OnRender method:
protected override void OnRender(DrawingContext drawingContext)
{
drawingContext.DrawEllipse(
new SolidColorBrush(Colors.CornflowerBlue),
null,
new Point(1292, 100),
100, 100);
}
using ZoomControl xaml:
<Controls:ZoomControl x:Name="zoomControl1" Background="AliceBlue">
<Canvas Width="{Binding Path=SModel.Width}" Height="{Binding Path=SModel.Height}">
<Image HorizontalAlignment="Center"
Name="image1" Stretch="Fill"
VerticalAlignment="Center" Source="/Z;component/Images/00000006.jpg"
Margin="0,0,0,0" Canvas.Left="1"
Width="{Binding Path=SModel.Width}" Height="{Binding Path=SModel.Height}">
<Image.LayoutTransform>
<RotateTransform Angle="{Binding Path=SModel.Angle}" />
</Image.LayoutTransform>
</Image>
</Canvas>
</Controls:ZoomControl>
Applying adorner:
public Window1()
{
InitializeComponent();
image1.Loaded += loaded;
}
private void loaded(object sender, RoutedEventArgs e)
{
var adorner = new SplitAdorner(image1);
AdornerLayer.GetAdornerLayer(image1).Add(adorner);
}
When I drag Image control, my adorner is not moving.
I've tried overriding ArrangeOverride and MeasureOverride:
protected override Size MeasureOverride(Size constraint)
{
Debug.WriteLine("measureoverride");
InvalidateVisual();
return base.MeasureOverride(constraint);
}
protected override Size ArrangeOverride(Size finalSize)
{
Debug.WriteLine("arrangeoverride");
InvalidateVisual();
return base.ArrangeOverride(finalSize);
}
But no effect. There is not anything in Output window and adorner is not moving.
When I'm zooming - everything is OK though. Adorner changes it's position accordingly to Image control changes.
The problem is in my code or in ZoomControl?
Example app is here.
SOLUTION:
I had to put my canvas into AdornerDecorator:
<Controls:ZoomControl x:Name="zoomControl1" Background="AliceBlue">
<AdornerDecorator>
<Canvas Width="{Binding Path=SModel.Width}" Height="{Binding Path=SModel.Height}">
<Image HorizontalAlignment="Center"
Name="image1" Stretch="Fill"
VerticalAlignment="Center" Source="/Z;component/Images/00000006.jpg"
Margin="0,0,0,0" Canvas.Left="1"
Width="{Binding Path=SModel.Width}" Height="{Binding Path=SModel.Height}">
<Image.LayoutTransform>
<RotateTransform Angle="{Binding Path=SModel.Angle}" />
</Image.LayoutTransform>
</Image>
</Canvas>
</AdornerDecorator>
</Controls:ZoomControl>
Description is here.
Ok, i looked into your example app and checked it with snoop. So it seems that the transformation is somehow applied to your adorner aswell, but the visual is not correctly updated. Panning works in fact, its just that your adorner is not properly invalidated.
One quick idea is to give your adorner dependency property for zoom, translate x, translate y, and set the property flag "AffectsRender" and bind them from the zoom control to your canvas. Now everytime your zoom control modifies one of these properties, the adorners properties are updated via binding and directly invalidated via the "AffectsRender" flag.
[First answer, but not valid]
Are you applying the transform of the zoom control to your adorner? Because an adorner is not placed on the control, in fact it is registered in the next AdornerDecorator, found upwards beginning on the provided control. My guess is that the only decorator(or layer) found is the default one on the window. Apply the transform of the control to your adorner and it should work. Or you could place an AdornerDecorator inside your Zoomcontrol, but to be honest i'm not sure if this has the desired effect.

WPF databinding problem

I have a grid inside a canvas on a tab.
The grid contains a large bitmap image,
I have(tried to) bound the size of the grid to the size of the tab and also have a five pixel margin around the grid.
imageTab.cs
public ImageTab(SendInfo sendInfo, int numImge, int numAccs)
{
imageDisplay = new ImageDisplay(sendInfo, numImge, numAccs);
imageDisplay.ClipToBounds = true;
CreateCanvas();
}
private void CreateCanvas()
{
Canvas canvas = new Canvas();
canvas.Children.Add(imageDisplay);
this.AddChild(canvas);
}
ImageDisplay.xaml
<UserControl x:Class="MyProj.ImageDisplay">
<Grid Margin="5,5,5,5" Height="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=TabControl, AncestorLevel=1}, Path=ActualHeight}">
<Image/>
</Grid>
</UserControl>
The grid comes off the bottom of the tab area slightly causing the bottom of the image to be cut off.
Is there a problem with my databinding, do I need to apply some sort of offset to it? (size of tab - 10pixels for the margin?)
You don't need to set the Height property at all (also realize that it is incorrect to do so as you have it when you consider the 5 pixel margin, i.e., it would be off by 10 pixels).
Just leave VerticalAlignment and HorizontalAlignment at their default values (which is Stretch) to get the effect you are after here.
Try this on a new Window to see what I mean:
<Window x:Class="WpfApplication9.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="438" Width="587" Background="Pink">
<Grid Background="Black" Margin="5">
</Grid>
</Window>
The grid here will be black and will always stretch to the size of the window, using a 5 pixel margin which you will see because the Window's back color is pink.

WPF element positioning on a Canvas

I have a point on a canvas that I want to place an ellipse. I want the centre of the ellipse to be over this point. At the moment the top left most edge of the ellipse is over this point.
I know I can shift the ellipse programmatically on the canvas but I was wondering if there is a way to tell WPF to centre the element over the point instead of sizing it from the top left???
I do not know of any in built in feature in Ellipse to set its center on a point, but you can extend the Ellipse class to do it.
Add this class to project
public static class EllipseX
{
public static void SetCenter(this Ellipse ellipse, double X, double Y)
{
Canvas.SetTop(ellipse, Y - ellipse.Height/2);
Canvas.SetLeft(ellipse, X - ellipse.Width/2);
}
}
Then in xaml create the Ellipse
<Window x:Class="WpfApplication2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Canvas Background="LightGray">
<Ellipse
Name="myEllipse"
Fill="Red"
Height="75"
Width="75"
/>
</Canvas>
</Window>
Then write int following code in code behind:
myEllipse.SetCenter(200,200);
The advantage of this is that you do not have to repeat the logic of finding center in every ellipse you create.
Hope this helps.
No there is no such way. Top Left is a top left, because it's a top left :). Alternatively instead of shifting ellipse you can shift point, if you know ellipse dimensions.
You could apply a TranslateTransform to the Ellipse, but that requires you to know its width and height.
I had a similar problem setting the Center of a ScaleTransform in a style of different sized controls.
Ended up using a converter to bind to the ActualWidth/ActualHeight divided by 2. THe same should work for you with a TranslateTransform as Rune Grimstad mentioned.
<ScaleTransform CenterX="{Binding ActualWidth, Converter={StaticResource divisionConverter}}"
CenterY="{Binding ActualHeight, Converter={StaticResource divisionConverter}}"
ScaleX="1.2"
ScaleY="1.2" />

Categories

Resources