WPF: Resizing a circle, keeping the center point instead of TopLeft? - wpf

I'd like to resize a circle on my canvas with the help of a slider. This circle can be moved around on the canvas by some drag&drop stuff I did in code behind, so its position is not fixed.
I have bound the slider's value to an ellipse's height and width. Unfortunately, when I use the slider, the circle gets resized with its top left point (actually the top left point of the rectangle it's sitting in) staying the same during the operation.
I would like to resize it with its center point being constant during the operation. Is there an easy way to do this in XAML? BTW, I already tried ScaleTransform, but it didn't quite do what I wanted.
Thanks a bunch! :-)
Jan
<Canvas x:Name="MyCanvas">
<!-- this is needed for some adorner stuff I do in code behind -->
<AdornerDecorator Canvas.Left="10"
Canvas.Top="10">
<Ellipse x:Name="myEllipse"
Height="{Binding Path=Value, ElementName=mySlider}"
Width="{Binding Path=Value, ElementName=mySlider}"
Stroke="Aquamarine"
Fill="AliceBlue"
RenderTransformOrigin="0.5 0.5">
<Ellipse.RenderTransform>
<RotateTransform Angle="{Binding Path=Value, ElementName=myRotationSlider}" />
</Ellipse.RenderTransform>
</Ellipse>
</AdornerDecorator>
<Slider x:Name="mySlider"
Maximum="100"
Minimum="0"
Width="100"
Value="10"
Canvas.Left="150"
Canvas.Top="10" />
<Slider x:Name="myRotationSlider"
Maximum="360"
Minimum="0"
Width="100"
Value="0"
Canvas.Left="150"
Canvas.Top="50" />
</Canvas>

You can bind your Canvas.Left and Canvas.Top to your Height and Width via a ValueConverter.
Specifically (edit):
Create a property each for the Canvas.Left and Canvas.Top and bind to these.
Store the old values for Width and Heigth or the old slider value.
Whenever the slider is changed, get the incremental change "dx" by subtracting the stored value.
(Don't forget to update the stored value...)
Add dx to Width and Height property.
And, as Will said, add dx/2*-1 to Canvas.Left and Canvas.Top properties.
Does that make sense?

The problem is that you are using the SLIDER to adjust the width and height. Width and height are not calculated around RenderTransformOrigin; only RenderTransforms use that value.
Here's a corrected version (brb, kaxaml):
<Canvas x:Name="MyCanvas">
<!-- this is needed for some adorner stuff I do in code behind -->
<AdornerDecorator Canvas.Left="50" Canvas.Top="50">
<Ellipse
x:Name="myEllipse"
Width="10"
Height="10"
Fill="AliceBlue"
RenderTransformOrigin="0.5 0.5"
Stroke="Aquamarine">
<Ellipse.RenderTransform>
<TransformGroup>
<RotateTransform Angle="{Binding Path=Value, ElementName=myRotationSlider}"/>
<ScaleTransform
CenterX=".5"
CenterY=".5"
ScaleX="{Binding Path=Value, ElementName=mySlider}"
ScaleY="{Binding Path=Value, ElementName=mySlider}"/>
</TransformGroup>
</Ellipse.RenderTransform>
</Ellipse>
</AdornerDecorator>
<Slider
x:Name="mySlider"
Width="100"
Canvas.Left="150"
Canvas.Top="10"
Maximum="10"
Minimum="0"
SmallChange=".01"
Value="1"/>
<Slider
x:Name="myRotationSlider"
Width="100"
Canvas.Left="150"
Canvas.Top="50"
Maximum="360"
Minimum="0"
Value="0"/>
</Canvas>
Of course, this will probably not work for you. Why? Well, the ScaleTransform I used zooms not only the circle but also the border; as the circle gets bigger the border does as well. Hopefully you won't care about this.
Also, realize when combining transforms (scale then rotate in this case) that they are applied in order, and one may affect how another is done. In your case, you would not notice this. But if, say, you were doing a rotate and translate, the order would be relevant.
Ah, what was I thinking? Just stick the ellipse in a Grid (simplest solution but other containers would work). The grid automatically takes care of centering the ellipse as it is resized. No need for any value converters! Here's the code:
<Canvas x:Name="MyCanvas">
<!-- this is needed for some adorner stuff I do in code behind -->
<Grid Width="100" Height="100">
<AdornerDecorator>
<Ellipse
x:Name="myEllipse"
Width="{Binding Path=Value, ElementName=mySlider}"
Height="{Binding Path=Value, ElementName=mySlider}"
Fill="AliceBlue"
RenderTransformOrigin="0.5 0.5"
Stroke="Aquamarine">
<Ellipse.RenderTransform>
<RotateTransform Angle="{Binding Path=Value, ElementName=myRotationSlider}"/>
</Ellipse.RenderTransform>
</Ellipse>
</AdornerDecorator>
</Grid>
<Slider
x:Name="mySlider"
Width="100"
Canvas.Left="150"
Canvas.Top="10"
Maximum="100"
Minimum="0"
Value="10"/>
<Slider
x:Name="myRotationSlider"
Width="100"
Canvas.Left="150"
Canvas.Top="50"
Maximum="360"
Minimum="0"
Value="0"/>
</Canvas>

Since you're using a Canvas, the location an element has is the location. If you want the Top,Left position to change you need to do it yourself. If you were using another Panel type, like a Grid, you could change the alignment of your Ellipse to place it in the same relative location no matter what the size. You could get that effect by adding a Grid inside your AdornerDecorator and centering the Ellipse but you'd also need to set the AdornerDecorator or Grid to a fixed size because they won't stretch in a Canvas.
The best solution you could use would be a ScaleTransform applied to the RenderTransform property with a RenderTransformOrigin of 0.5,0.5. You said you had problems with ScaleTransform but not what the problem was.

Wrap your Ellipse in a Grid of the maximum size. As long as it is smaller, the Ellipse will be centered in the Grid:
<Grid
Canvas.Left="10"
Canvas.Top="10"
Width="100"
Height="100">
<AdornerDecorator>
<Ellipse x:Name="myEllipse"
Height="{Binding Path=Value, ElementName=mySlider}"
Width="{Binding Path=Value, ElementName=mySlider}"
Stroke="Aquamarine"
Fill="AliceBlue"
RenderTransformOrigin="0.5 0.5">
<Ellipse.RenderTransform>
<RotateTransform Angle="{Binding Path=Value, ElementName=myRotationSlider}" />
</Ellipse.RenderTransform>
</Ellipse>
</AdornerDecorator>
</Grid>
You may need to adjust your dragging logic to handle dragging the Grid instead of the Ellipse itself.

I've found a very easy way to do this in plain XAML: set Margin="-1000000". Read more here: Positioning an element inside the Canvas by its center (instead of the top left corner) using only XAML in WPF

Related

Bind the scale of a Button to an image

I have a simple window with an image and a button. When the user resizes the window, I would like the button to be resized with the image in the exact same proportions. Below is an example of the desired behaviour I would like to have. The button around the head of the dog just extends and translates with the image.
I managed to have the above behaviour with a "Path" (above in blue), but I didn't manage to get the same behaviour with a button. I would like to perform some actions when the user clicks on the button.
Here is the xaml code I used for the working "Path" senario:
<Grid>
<Canvas x:Name="polylineCanvas" Grid.Row="1">
<Image x:Name="imgTraining"
Width="{Binding ActualWidth, ElementName=polylineCanvas, Mode=OneWay}"
Height="{Binding ActualHeight, ElementName=polylineCanvas, Mode=OneWay}"
Stretch="Uniform"
HorizontalAlignment="Left"
VerticalAlignment="Top">
</Image>
<Path Stroke="Blue" StrokeThickness="3">
<Path.Data>
<PathGeometry x:Name="polyline">
<PathGeometry.Transform>
<ScaleTransform
ScaleX="{Binding ActualWidth, ElementName=imgTraining}"
ScaleY="{Binding ActualHeight, ElementName=imgTraining}"/>
</PathGeometry.Transform>
</PathGeometry>
</Path.Data>
</Path>
</Canvas>
</Grid>
I tried to apply the same logic by adding a button instead of a Path. In this case, the button streches to the size of the window (but not to the size of the image):
Here is the xaml code I added for the button:
<Button x:Name="buttSelection" Opacity="0.5" Background="#FF000CFF">
<Button.LayoutTransform>
<ScaleTransform
ScaleX="{Binding ActualWidth, ElementName=imgTraining}"
ScaleY="{Binding ActualHeight, ElementName=imgTraining}"/>
</Button.LayoutTransform>
</Button>
Questions:
How could I scale my button in the same proportion as the image?
How could I set the size and position of this button to only be a part of the image (and not to be as big as the image itself)?
Thanks a lot for the support!
PS: I am very new to wpf/xaml, so there is a big chance that I am not doing things in the "intended way" or that there is a completely different solution.
You should put both the Image and the Button in a common Viewbox, which would automatically scale them together. There is usually no need to bind the size of an element to the actual size of another. This is done by using appropriate layout panels.
Inside the Viewbox, there would be a Grid as common parent, and a Canvas to position the Button. The Button Template would contain a Border around a ContentPresenter that could optionally show the Button's Content.
<Grid>
<Viewbox>
<Grid>
<Image x:Name="imageTraining"/>
<Canvas>
<Button x:Name="buttonSelection" Width="200" Height="200"
Canvas.Left="200" Canvas.Top="200">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border BorderThickness="10"
BorderBrush="Blue"
Background="Transparent">
<ContentPresenter
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
</Border>
</ControlTemplate>
</Button.Template>
</Button>
</Canvas>
</Grid>
</Viewbox>
</Grid>

Canvas is clipped when size is too big

I am using a canvas with the an ImageBrush to dispaly an image. I am setting the size of the canvas to the original size of the image so I can get the coordinates when I move the mouse etc.
The problem is that when I put the canvas in a control (Grid for example) with a smaller size the Canvas is clipped.
<Grid>
<Canvas Width="{Binding ImageWidth}" Height="{Binding ImageHeight}" >
<Canvas.Background>
<ImageBrush ImageSource="{Binding Image, Converter={StaticResource imgConverter}}"/>
</Canvas.Background>
</Canvas>
</Grid>
Is there a way to keep the canvas size without being clipped?
I've been meaning to dig deeper in to the source to work out where the clipping occurs for a while now, but never get around to doing it. I've been using a not-so-nice trick of inserting a Canvas into the visual tree when this happens as a workaround.
There are a number of controls that clip the child visuals; Grid, StackPanel, etc. As I mentioned the usual quick fix is to use a Canvas after the container that causes the clip.
In your snippet are there more containers higher up the visual tree?
If the depth was actually something like this:-
<StackPanel>
<Grid>
<Canvas Width="{Binding ImageWidth}" Height="{Binding ImageHeight}" >
<Canvas.Background>
<ImageBrush ImageSource="{Binding Image, Converter={StaticResource imgConverter}}"/>
</Canvas.Background>
</Canvas>
</Grid>
</StackPanel>
Then this might cause clipping. If you insert another Canvas further up the visual tree then this clipping is removed.
<StackPanel>
<Canvas>
<Grid>
<Canvas Width="{Binding ImageWidth}" Height="{Binding ImageHeight}" >
<Canvas.Background>
<ImageBrush ImageSource="{Binding Image, Converter={StaticResource imgConverter}}"/>
</Canvas.Background>
</Canvas>
</Grid>
</Canvas>
</StackPanel>
This workaround can then become problematic if it alters other layout needs for other controls.

Silverlight: How to apply ScaleTransform to a Ellipse but not the StrokeThickness

I have to show some circles with scale function, but I need the strokethickness stay the same. How can I achieve that? Thanks.
<Grid x:Name="SelectedPanel"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
RenderTransformOrigin="0.5,0.5" IsHitTestVisible="False">
<Grid.RenderTransform>
<ScaleTransform
ScaleX="{Binding some binding}"
ScaleY="{Binding some binding}"/>
</Grid.RenderTransform>
<Ellipse
Stroke="#09C900"
StrokeThickness="3"
StrokeDashArray="5 2"
StrokeDashOffset="6"
RenderTransformOrigin="0.5,0.5"/>
</Grid>
Well, if you apply the ScaleTransform to the Ellipse the StrokeThickness will grow with the rest of the element.
I suggest you manipulate Width and Height of the Ellipse instead.
Something like this...
<Ellipse
Stroke="#09C900"
StrokeThickness="3"
StrokeDashArray="5 2"
StrokeDashOffset="6"
Width="{some binding}"
Height="{some binding}"/>

ScaleTransform and CenterX

I have the following code
<Canvas Width="800" Height="600">
...
<local:UpgradeLandDialog x:Name="upgradeDialog" Canvas.Left="250" Canvas.Top="200" HorizontalAlignment="Center" VerticalAlignment="Center" Opacity="0">
<local:UpgradeLandDialog.LayoutTransform>
<ScaleTransform ScaleX="0" ScaleY="0" CenterX="400" CenterY="300"/>
</local:UpgradeLandDialog.LayoutTransform>
</local:UpgradeLandDialog>
</Canvas>
In the UserControl I animate the ScaleTranform to 1. I want UserControl to "grow" from its center, but it "grows" from the upper left corner of it. The values in CenterX and CenterY do nothing. How can I make it Scale as I want?
You can use RenderTransformOrigin="0.5,0.5" on the control you want to animate.
You can change your code like this:
<Canvas Width="800" Height="600" RenderTransformOrigin="0.5,0.5">
<local:UpgradeLandDialog x:Name="upgradeDialog" Canvas.Left="250" Canvas.Top="200" HorizontalAlignment="Center" VerticalAlignment="Center" Opacity="0">
<local:UpgradeLandDialog.LayoutTransform>
<ScaleTransform ScaleX="0" ScaleY="0"/>
</local:UpgradeLandDialog.LayoutTransform>
</local:UpgradeLandDialog>
</Canvas>
Remove (CenterX="400" CenterY="300") and add (RenderTransformOrigin="0.5,0.5") to the Canvas. This way if you have a container with dynamic width and height, it can scale from the center without problem.
To make it grow from its center, you'll have to animate its margins as well (at half the rate at which you animate the width and height).
I ran into this problem not too long ago as well. I ended up repositioning the user control at every layout update to simulate a custom point based growth.
This does work for me. Did I miss something?
<Rectangle StrokeThickness="1" Stroke="Black" Width="200" Height="200">
<Rectangle.RenderTransform>
<ScaleTransform
ScaleX="{Binding ElementName=slider, Path=Value}"
ScaleY="{Binding ElementName=slider, Path=Value}"
CenterX ="100" CenterY="100"/>
</Rectangle.RenderTransform>
</Rectangle>
Even though this is an old post, I thought I'd share my findings, since it took me way too long to figure out this fairly simple solution.
Flipping the y-axis was easy, but I couldn't get CenterX and CenterY working. I really needed to be able to set the origin at any position I wanted.
Solution: nested canvasses.
<Canvas HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Canvas Canvas.Left="{Binding MyOriginLeft}" Canvas.Bottom="{Binding MyOriginBottom}">
<Canvas.LayoutTransform>
<ScaleTransform ScaleX="1" ScaleY="-1"/>
</Canvas.LayoutTransform>
<!-- This now does what you expect it to do, independent of position of origin -->
<Line X1="10" Y1="20" X2="30" Y2="40" Stroke="Black" StrokeThickness="1"/>
</Canvas>
</Canvas>

How to prevent the Visual Brush from stretching its content

In my project I want to display a small logo on the side of a custom control. Since I have no canvas I thought maybe a Visual Brush would be a good Idea to place the logo in the background.
<VisualBrush>
<VisualBrush.Visual>
<Rectangle Width="200" Height="200" Fill="Red" />
</VisualBrush.Visual>
</VisualBrush>
But the Rectangle I am using right now is not 200x200. It takes the complete available space. Thats not what I want. I also tried a Viewbox and set the stretch property but the result is the same because in the end I don't need a simple Rectangle but a canvas with many path objects as children. A Viewbox supports only one child.
This there any way to get around this problem?
You need to set TileMode, Stretch, AlignmentX and AlignmentY properties on your VisualBrush:
<VisualBrush TileMode="None" Stretch="None" AlignmentX="Left" AlignmentY="Top">
<VisualBrush.Visual>
<Rectangle Height="200" Width="200" Fill="Red"></Rectangle>
</VisualBrush.Visual>
</VisualBrush>
Add Grid and this Set Vertical alligment to Top and Horizontal alignment to Right
Sample code
<VisualBrush x:Key="myVisual">
<VisualBrush.Visual>
<Grid>
<Rectangle Height="200" Width="200" Fill="Red" HorizontalAlignment="Right" VerticalAlignment="Top" ></Rectangle>
</Grid>
</VisualBrush.Visual>
</VisualBrush>
For me, I set the following attribute on the VisualBrush, and the VisualBrush now looks exactly like a MediaElement:
Stretch="Uniform"

Resources