XAML/WPF zoom to a specific area on a canvas - c#

So I have a WPF window with a grid which has 2 columns. In the first column I have a canvas which I am rendering an SVG image to. This canvas is re-sized to be the same size as the image (so there are no transforms going on) and it is assumed that the image is a lot smaller than the screen so there is no need for a scroll viewer - I'll call this the left canvas.
In the second column I have another canvas which is inside a Viewbox, the same SVG is also being rendered to this canvas and it is assumed that the SVG image size is larger than the Viewbox fixed size. The Viewbox re-sizes the canvas to fit inside it - although doesn't appear to apply any transforms to the canvas and doesn't change its width or height, some other magic goes on here - but fine, it works.
The idea is that the user can draw a rectangle on the left canvas, which will represent a zoom area, and then the right canvas will zoom in so that rectangle will fit to the Viewbox containing the canvas - by fit I mean without cropping or stretching/squashing any of the zoom area , so if it is a landscape zoom area in a portrait Viewbox, the sides of the zoom area will meet the sides of the Viewbox leaving space at the top and bottom which is fine.
I though this would be straight forward as there are no transforms applied to either of the canvases, and they both have the same width and height (even though some magic from the Viewbox is making the right one smaller). This is how I'm doing it at the moment:
Find the centre point:
centreX = Canvas.GetLeft(zoomAreaRect) + (zoomAreaRect.Width / 2);
centreY = Canvas.GetTop(zoomAreaRect) + (zoomAreaRect.Height / 2);
Find the scale amount:
double scale;
if(zoomAreaRect.Width > zoomAreaRect.Height)
{
scale = RightCanvas.Width / zoomAreaRect.Width;
}
else
{
scale = RightCanvas.Height / zoomAreaRect.Height;
}
Then use a scale transform using centreX and centreY as the centre of the transform, and scale for both scaleX and scaleY on the transform.
Now this clearly doesn't work because I need to somehow take into account the Viewbox size when working out the scale amount, I'm just not sure how to do this. Can anyone help please?
Update:
I have scrapped the Viewbox as this complicates things.. so the right canvas is just normal size too but is contained inside a border with fixed width and height. The aim is to zoom in on the zoom area until it fits to the border.
Here is the XAML for the right side:
<Border Name="ContainingBorder"
Grid.Column="1"
MaxWidth="295"
MaxHeight="487"
Height="487">
<Border.Clip>
<RectangleGeometry Rect="0.5, 0.5, 295, 487"/>
</Border.Clip>
<Canvas Name="RightCanvas"/>
</Border>
I have managed to zoom in the correct amount, it just doesn't zoom into the right centre. I just use the aspect ratio as the scale amount which appears to work:
double ratioX = ContainingBorder.AcctualWidth / zoomAreaRect.Width;
double ratioY = ContainingBorder.AcctualHeight / zoomAreaRect.Height;
double scale = ratioX < ratioY ? ratioX : ratioY;
Any ideas how I can figure out the centre x and y? The above centreX and centreY calculations don't appear to work properly.

I'm not sure if I fully understood your question. maybe pasting the xaml code would help.
Potentially you can work with the "margin" property of the canvas:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<!--clip to bounds for hide parts of the canvas-->
<Viewbox ClipToBounds="True">
<!--set margin negatively to zoom out of the viewbox size-->
<Canvas Margin="0,0,-100,-100" Width="200" Background="Red" Height="200">
<Rectangle Width="100" Height="100" Fill="Gray"/>
</Canvas>
</Viewbox>
</Grid>

Maybe you can solve your problem with a visual brush:
<Window x:Class="ZoomViewBoxTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="300" Width="700">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Viewbox Width="700" Height="300">
<Canvas x:Name="LeftCanvas" Width="600" Background="Red" Height="600">
<!--drawing Rectangle simulates your zoom area-->
<Rectangle x:Name="DrawingRectange" Width="100" Height="150" Fill="Green" Canvas.Left="200" Canvas.Top="300"></Rectangle>
<Rectangle Width="200" Height="200" Fill="Gray" Canvas.Left="250" Canvas.Top="350"/>
</Canvas>
</Viewbox>
<Viewbox Grid.Column="1">
<Rectangle Width="{Binding ElementName=DrawingRectange,Path=Width}" Height="{Binding ElementName=DrawingRectange,Path=Height}">
<Rectangle.Fill>
<VisualBrush Visual="{Binding ElementName=LeftCanvas}" Stretch="None" >
<VisualBrush.Viewbox>
<!-- X = DrawingRectangle.X / LeftCanvas.Width
Y = DrawingRectangle.X / LeftCanvas.Height
Width = DrawingRectangle.Width / LeftCanvas.Width
Height = DrawingRectangle.Height / LeftCanvas.Height-->
<Rect X="0.333" Y="0.5" Width="0.1666" Height="0.25"></Rect>
</VisualBrush.Viewbox>
</VisualBrush>
</Rectangle.Fill>
</Rectangle>
</Viewbox>
</Grid>
Of coarse the Viewbox of the VisualBrush must be binded to the actual values from the DrawingRectangle (in code behind or via converters).

Related

WPF - Effects of "Center" vertical alignment on negative margins

Attempting to animate a control by modifying its margins, I realized that I might not fully understand the effects of the different alignment options on negative margins. To better explain my question, I created an example containing two TextBlock controls each surrounded by a Border.
As shown below, I attempt to give the first TextBlock _TextBlock1 (blue) - which has a vertical alignment of Top - a top margin of -20 so that its bottom edge will sit immediately on top of its border _Border1. This produces the desired result. I then attempt to achieve the same effect on TextBlock _TextBlock2 (orange), which is identical to _TextBlock1 except for its vertical alignment of Center. Since this TextBlock is centered vertically, I apply a top margin of -40, which from my understanding should produce the same result (20 pixels to move its top edge to the top edge of the border _Border2 and another 20 pixels to bring it fully above the border).
As shown on the image below - taken from the designer view in Visual Studio - I seem to be missing something about how the margins affect the placement of these controls given their vertical alignment type. Could somebody explain to me how I should be interpreting the interaction between margins and alignment types (as related to this example)? Also, how can I modify the margins on _TextBlock2 to produce the same results as _TextBlock1?
<Window x:Class="Test.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Test"
Height="350" Width="525">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="60"/>
<ColumnDefinition Width="60"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="60"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Border
x:Name="_Border1"
Grid.Row="1"
Grid.Column="1"
BorderBrush="Black"
BorderThickness="1">
<TextBlock
Margin="0 -20 0 0"
x:Name="_TextBlock1"
Background="DodgerBlue"
VerticalAlignment="Top"
Height="20"/>
</Border>
<Border
x:Name="_Border2"
Grid.Row="1"
Grid.Column="2"
BorderBrush="Black"
BorderThickness="1">
<TextBlock
Margin="0 -40 0 0"
x:Name="_TextBlock2"
Background="Orange"
VerticalAlignment="Center"
Height="20"/>
</Border>
</Grid>
</Window>
Margins do not ‘move’ an element. Margins effectively grow or shrink the size of an element’s layout rectangle, which is provided by its parent. Alignment controls how an element positions itself within its layout rectangle.
Initially, the orange block has its parent’s entire area available for positioning, so its layout rectangle starts with a height of 60. Normally, adding (positive) margins shrinks the portion of the layout rectangle available to position an element. But a top margin of -40 effectively grows the orange block’s layout rectangle so it has a height of 60 - (-40) = 100. Let’s define the top-left corner of our effective layout rectangle as being (0, 0). Relative to this, the parent border’s top-left corner is (0, 40).
The orange block has a height of 20, and it has 100 units of vertical space in which to center itself. (100 - 20) / 2 = 40, so the block gets 40 units of vertical space above it and below it. This puts the orange block’s top-left corner at (0, 40), right along with its parent.
You can modify your _TextBlock2 as below to obtain required behaviour:
<TextBlock
Margin="0 -80 0 0"
x:Name="_TextBlock2"
Background="Orange"
VerticalAlignment="Center"
Height="20"/>
To have a better understanding regarding margins and padding go here.

Why does a too small container cause a child to clip to its own bounds?

For alignment purposes, I have a control which renders a button outside of it's own bounds. This seems to work perfectly in most cases, but it produces undesired output in cases where the size of the control plus its margins exceeds the outline of its container.
Below is an example where the red grid represents the control and the blue rectangle is the button. The blue rectangle renders correctly when the outer grid is sufficiently big (Width="300"), but not when the outer grid is too small to contain the red grid and its margin. The blue rectangle will then get clipped to the bounds for the red grid.
This seems to be a bug. I would expect the blue rectangle to be clipped according to the bounds of the outer grid and the right margin of the red grid, but being clipped to the bounds of the red rectangle makes no sense to me. What is the best approach for working around this? I could set the right margin of the red grid to a sufficiently large negative number and set ClipToBounds="True" on the outer grid, but I was hoping that there would be a better solution.
<Grid Width="300">
<Grid Margin="50" Background="Red" Width="200" HorizontalAlignment="Left">
<Rectangle Fill="Blue" Margin="-20" HorizontalAlignment="Left" Width="50" Height="50" />
</Grid>
</Grid>
<Grid Width="299">
<Grid Margin="50" Background="Red" Width="200" HorizontalAlignment="Left">
<Rectangle Fill="Blue" Margin="-20" HorizontalAlignment="Left" Width="50" Height="50" />
</Grid>
</Grid>

Zooming WPF canvas relative to the center rather than top left

I am writing a label-making program. In this program, I have a canvas with a white rectangle positioned in the center, with multiple objects that the user can resize, drag, etc on top of it. I also have an option for the user to zoom the canvas, which I accomplish using ScaleTransform via LayoutTransform. I want to have it so that, when the user zooms in, the canvas zooms in on the center, rather than relative to the top right.
Here's a demonstration:
Currently, the canvas zooms like this:
I need it to zoom like this:
How can I accomplish this task, without re-positioning the elements in the canvas when it is zoomed?
Note: I'm using LayoutTransform, since I must embed this in a ScrollViewer. RenderTransform accomplishes this, but won't let the user scroll when canvas elements exceed the visible canvas bounds.
You should be able to accomplish the same thing as RenderTransformOrigin, by using a transform that combines two translations around the scaling. E.g. using TransformGroup, or combining matrices for a MatrixTransform.
Specifically: translate the center point (X,Y) to (0,0) by using translation offsets of -X and -Y, perform the scale transform, and then translate (0,0) back to the original point (X, Y) using translation offsets of X and Y.
It's hard for me to know for sure without having a specific code example, but I have a vague sense that it ought to be possible to use RenderTransform, perhaps applied differently than you'd prefer (e.g. adding a new container to the hierarchy and applying it to that). But assuming RenderTransform simply won't work, the above should.
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="600" Width="600">
<Grid>
<ScrollViewer>
<Border>
<Border.LayoutTransform>
<TransformGroup>
<ScaleTransform ScaleX="2.0" ScaleY="2.0"/>
</TransformGroup>
</Border.LayoutTransform>
<Canvas Background="Red" Width="500" Height="500" VerticalAlignment="Center" HorizontalAlignment="Center">
<Canvas.RenderTransform>
<TransformGroup>
<TranslateTransform X="0" Y="-100"/>
</TransformGroup>
</Canvas.RenderTransform>
<Rectangle Canvas.Left="200" Canvas.Top="200" Width="100" Height="100" Fill="Yellow"/>
</Canvas>
</Border>
</ScrollViewer>
</Grid>
</Window>

Polygon not being drawn on Canvas

I am learning WPF and trying to dynamically draw a collection of polygons to a canvas.
I am configuring the polygons as follows:
mHexagon = new Polygon();
mHexagon.Width = Diameter * 2;
mHexagon.Height = Diameter * 2;
mHexagon.StrokeThickness = 1;
mHexagon.Stroke = Brushes.AliceBlue;
mHexagon.Points = new PointCollection(Vertices.Select(v => new Point(v.X, v.Y)));
Sample point collection:
Vertices
:128,64
:96,8.57437415779593
:32,8.57437415779592
:0,64
:32,119.425625842204
:96,119.425625842204
I am adding the polygon as follows (encapsulated in Hexagon class):
PlayerMapWindow.Children.Add(hex.Polygon);
And my XAML looks like:
<Border BorderThickness="1"
BorderBrush="DarkRed"
Background="Black"
Padding="2">
<Viewbox Name="PlayerMapViewbox"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch">
<Canvas Name="PlayerMapWindow"/>
</Viewbox>
</Border>
After adding the polygon (hex) to the canvas children, nothing is drawn.
Remove the ViewBox and your code works fine.
<Border BorderThickness="1"
BorderBrush="DarkRed"
Background="Black"
Padding="2">
<Canvas Name="PlayerMapWindow"/>
</Border>
Canvas has default height and width set to 0. If you want scaling feature of ViewBox, you have to constraint width and height of canvas.
<Border BorderThickness="1"
BorderBrush="DarkRed"
Background="Black"
Padding="2">
<Viewbox Name="PlayerMapViewbox"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch">
<Canvas Name="PlayerMapWindow" Width="300" Height="300"/>
</Viewbox>
</Border>
From MSDN:
Canvas is the only panel element that has no inherent layout
characteristics. A Canvas has default Height and Width properties of
zero, unless it is the child of an element that automatically sizes
its child elements. Child elements of a Canvas are never resized, they
are just positioned at their designated coordinates. This provides
flexibility for situations in which inherent sizing constraints or
alignment are not needed or wanted. For cases in which you want child
content to be automatically resized and aligned, it is usually best to
use a Grid element.

Border around a rotating image

Basically I'm trying to do something whereby a WPF image is inside a WPF border, and periodically I rotate the image by changing the RotateTransform Angle property.
The problem is, when I rotate the image, the border doesn't rotate, or attempt to change to fit the new shape of the picture. I've tried setting it's Alignment properties to stretch, and even binding the height/width of the border to that of the image, but no luck. I suspect the problem is that, when I rotate the image, it doesn't actually change the height or width of the Image object, so of course the border doesn't know what to do.
Is there a better way to rotate the image that would allow the border to resize, or if not, how do I get the border to resize correctly, given that I'm changing the RotateTransform Angle.
Thanks!
You can use the LayoutTransform instead of RenderTransform for this. If you try changing the angle of rotation you'll see the border changes size to accommodate it. (Think this is what you're asking? If you actually want the border to rotate then you can just rotate that instead of the image)
<Window x:Class="rotate.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">
<Grid>
<Border VerticalAlignment="Center" HorizontalAlignment="Center" BorderBrush="Black" BorderThickness="1">
<Grid Background="Blue" Width="80" Height="80">
<Grid.LayoutTransform>
<RotateTransform Angle="10"/>
</Grid.LayoutTransform>
</Grid>
</Border>
</Grid>
</Window>
Use LayoutTransform instead of RenderTranform.
RenderTransform only does a visual transformation of the control and is applied after measuring and arranging Controls. Therefore it doesn't affect the size seen by other controls.
LayoutTransform really affects the layout of the object. It's applied before measuring and arranging control, so the other control see a change in the size.
Caution: LayoutTransform is much slower and won't usually give a smooth animation.
<Border BorderThickness="5" BorderBrush="Red" HorizontalAlignment="Center" VerticalAlignment="Center">
<Image Width="50" Height="50">
<Image.LayoutTransform>
<RotateTransform Angle="45" />
</Image.LayoutTransform>
</Image>
</Border>

Categories