I'm working on my first WPF application and I'm testing out a custom control that is essential a circle with a play button drawn into the middle of it. I seem to have hit a bit of a hitch though. When I draw my play button I can't seem to get it to resize alongside the circle. Specifically, when I resize the circle to be wider or taller, the play button polygon remains the same size and in the same absolute position. Any pointer on setting up my XAML or code to correct this?
Existing XAML:
<Window x:Class="WPFTest.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" xmlns:my="clr-namespace:WPFTest">
<StackPanel>
<StackPanel.Resources>
<Style TargetType="my:GradientButton">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type my:GradientButton}">
<Grid>
<Ellipse Width="{TemplateBinding Width}" Height="{TemplateBinding Height}" Stroke="{TemplateBinding Foreground}" VerticalAlignment="Top" HorizontalAlignment="Left">
<Ellipse.Fill>
<LinearGradientBrush>
<GradientStop Color="{TemplateBinding GradientStart}" Offset="0"></GradientStop>
<GradientStop Color="{TemplateBinding GradientEnd}" Offset="1"></GradientStop>
</LinearGradientBrush>
</Ellipse.Fill>
</Ellipse>
<Polygon Points="{TemplateBinding PlayPoints}" Fill="{TemplateBinding Foreground}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</StackPanel.Resources>
<my:GradientButton Content="Button" Height="50" x:Name="gradientButton1" Width="50" GradientStart="#FFCCCCCC" GradientEnd="#FFAAAAAA" PlayPoints="18,12 35,25 18,38" />
</StackPanel>
</Window>
Code:
public class GradientButton : Button
{
internal static DependencyProperty GradientStartProperty;
internal static DependencyProperty GradientEndProperty;
internal static DependencyProperty PlayPointsProperty;
static GradientButton()
{
GradientStartProperty = DependencyProperty.Register("GradientStart", typeof(Color), typeof(GradientButton));
GradientEndProperty = DependencyProperty.Register("GradientEnd", typeof(Color), typeof(GradientButton));
PlayPointsProperty = DependencyProperty.Register("PlayPoints", typeof(PointCollection), typeof(GradientButton));
}
public Color GradientStart
{
get { return (Color)base.GetValue(GradientStartProperty); }
set { SetValue(GradientStartProperty, value); }
}
public Color GradientEnd
{
get { return (Color)base.GetValue(GradientEndProperty); }
set { SetValue(GradientEndProperty, value); }
}
public PointCollection PlayPoints
{
get
{
//this is where I'm trying to make the resizing dynamic, but this property never seems to get hit when I put in a breakpoint?
PointCollection result = new PointCollection();
double top = this.Width / 2.778;
double left = this.Width / 4.167;
double middle = this.Height / 2.00;
double right = this.Width / 1.429;
double bottom = this.Height / 1.316;
result.Add(new Point(left, top));
result.Add(new Point(right, middle));
result.Add(new Point(left, bottom));
return result;
}
set { SetValue(PlayPointsProperty, value); }
}
}
You need to set the Stretch property of the Polygon to Uniform
Related
I'm trying to draw Microsoft.UI.Xaml.Shapes.Path objects to a Microsoft.UI.Xaml.Controls.Canvas control. These objects are configured in their own coordinate system, and transformed to the pixel-space coordinate system using the Transform property of the Geometry objects.
However, I'm getting some odd behaviour that I'm struggling to make sense of. Here is code that replicates the issue:
XAML
<Window
x:Class="TransformRenderBug.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:TransformRenderBug"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
SizeChanged="Window_SizeChanged">
<TabView >
<TabViewItem Header="Blank tab"/>
<TabViewItem Header="Drawing tab">
<local:NumberSpace x:Name="NumberSpace" >
<Path Stroke="Red" StrokeThickness="1">
<Path.Data>
<EllipseGeometry RadiusX="1" RadiusY="1" Transform="{x:Bind Transform}"/>
</Path.Data>
</Path>
<Path Stroke="Green" StrokeThickness="1">
<Path.Data>
<EllipseGeometry RadiusX="0.9" RadiusY="0.9" Transform="{x:Bind NumberSpace.Transform}"/>
</Path.Data>
</Path>
<local:NumberSpacePath Stroke="Blue" StrokeThickness="1">
<local:NumberSpacePath.Data>
<EllipseGeometry RadiusX="0.8" RadiusY="0.8" Transform="{x:Bind Transform}"/>
</local:NumberSpacePath.Data>
</local:NumberSpacePath>
<local:NumberSpacePath Stroke="Black" StrokeThickness="1">
<local:NumberSpacePath.Data>
<EllipseGeometry RadiusX="0.7" RadiusY="0.7" Transform="{x:Bind NumberSpace.Transform}"/>
</local:NumberSpacePath.Data>
</local:NumberSpacePath>
</local:NumberSpace>
</TabViewItem>
</TabView>
</Window>
C#
public sealed partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private CompositeTransform Transform { get; } = new();
private void Window_SizeChanged(object sender, WindowSizeChangedEventArgs args)
{
NumberSpace.SetTransform(Transform, args.Size.Width, args.Size.Height - 40);
NumberSpace.Width = args.Size.Width;
NumberSpace.Height = args.Size.Height - 40;
}
}
public class NumberSpace : Canvas
{
public CompositeTransform Transform { get; } = new();
public NumberSpace()
{
SizeChanged += (o, e) => SetTransform(Transform, ActualWidth, ActualHeight);
}
public static void SetTransform(CompositeTransform transform, double width, double height)
{
// Figure out the transform required for the shapes to fill the space
double pixelScaleX = width / 2;
double pixelScaleY = height / 2;
transform.ScaleX = pixelScaleX;
transform.ScaleY = pixelScaleY;
transform.TranslateX = pixelScaleX;
transform.TranslateY = pixelScaleY;
}
}
public class NumberSpacePath : Path
{
}
This code produces a window with two tabs. The first is blank. The second has four concentric ellipses drawn on it which take their transforms from either the Window or the Canvas. The ellipses are drawn as either Path objects or NumberSpacePath objects. NumberSpacePath inherits from Path but adds and changes nothing.
The two outer rings draw and resize without issue. However, when the second tab is first switched to, the innermost black ring is not rendered. Switching back to the first tab and again to the second tab causes it to be rendered. Additionally, enlarging the window causes the innermost two rings to be clipped, like on the image below. Again, switching and returning tabs causes them to be rendered correctly.
What is going on? Why does subclassing Path make a difference?
I have a struggle, I don't know how to set the background and size of a canvas with respect to MVVM, currently, I handle this only in the View part, and work ok, over this image (canvas) I want to draw some rectangle, but I want to be able to do everything according to MVVM pattern.
var bmp = new BitmapImage(new Uri(filename, UriKind.Relative));
ImageBrush brush = new ImageBrush();
brush.ImageSource = bmp;
canvas.Width = bmp.PixelWidth;
canvas.Height = bmp.PixelHeight;
canvas.Background = brush;
canvas.SnapsToDevicePixels = true;
The view model could expose a property with e.g. the image file path
public string ImagePath { get; set; }
to which you would bind like this
<Canvas Width="{Binding Background.ImageSource.PixelWidth,
RelativeSource={RelativeSource Self}}"
Height="{Binding Background.ImageSource.PixelHeight,
RelativeSource={RelativeSource Self}}"
SnapsToDevicePixels="True">
<Canvas.Background>
<ImageBrush ImageSource="{Binding ImagePath}"/>
</Canvas.Background>
</Canvas>
The conversion from string to ImageSource would automatically be performed by an ImageSourceConverter instance in WPF.
The Bindings would be simpler when the view model exposes a property of type BitmapSource:
public BitmapSource Image { get; set; }
XAML:
<Canvas Width="{Binding Image.PixelWidth}"
Height="{Binding Image.PixelHeight}"
SnapsToDevicePixels="True">
<Canvas.Background>
<ImageBrush ImageSource="{Binding Image}"/>
</Canvas.Background>
</Canvas>
I'm using a rectangle drawn on an adorner to mark a region of interest on an image. The issue is that if I resize the window, the rectangle doesn't change size.
I'm new to WPF, so I've done a bunch of research, googling what I can with multiple different search terms. I actually just learned adorners that way, and I've gotten this far on that, but I've hit a wall on how to finish this last piece. I know that my problem is based in the size of the rectangle, but I don't know what to capture/look for to adjust it, since wpf resizes the actual image object on window resize, so there's no scale factor to look at.
Here's the XAML for the application I'm testing things in.
<Window x:Class="TestingAdorners.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:TestingAdorners"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid ClipToBounds="True">
<AdornerDecorator>
<Image Name="Btn" Source="nyan.png" Stretch="Uniform"/>
</AdornerDecorator>
</Grid>
</Window>
The adorner class:
class RoiAdorner : Adorner
{
public Rect rectangle = new Rect();
public RoiAdorner(UIElement adornedElement) : base(adornedElement)
{
rectangle.Height = 30;
rectangle.Width = 100;
IsHitTestVisible = false;
}
protected override void OnRender(DrawingContext drawingContext)
{
Pen pen = new Pen(Brushes.Green, 5);
drawingContext.DrawRectangle(null, pen, rectangle);
}
}
And the Xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
AdornerLayer.GetAdornerLayer(Btn).Add(new RoiAdorner(Btn));
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
}
}
The desired result is that the rectangle scales with the image object so that it always covers the same region of the image. The problem is I don't know how to capture a scale factor to scale it up and down as the window resizes.
Update: After thinking through Frenchy's suggestion I realized the answer is simply: "Normalize your coordinates"
you just adapt your render method like this:
class RoiAdorner : Adorner
{
public double factorX = 0d;
public double factorY = 0d;
public Rect rectangle = new Rect();
public RoiAdorner(UIElement adornedElement) : base(adornedElement)
{
rectangle.Height = 30;
rectangle.Width = 100;
IsHitTestVisible = false;
}
protected override void OnRender(DrawingContext drawingContext)
{
if (factorY == 0)
factorY = rectangle.Height / AdornedElement.DesiredSize.Height;
if (factorX == 0)
factorX = rectangle.Width / AdornedElement.DesiredSize.Width;
var r = new Rect(new Size(AdornedElement.DesiredSize.Width * factorX, AdornedElement.DesiredSize.Height * factorY));
//Rect adornedElementRect = new Rect(this.AdornedElement.DesiredSize);
drawingContext.DrawRectangle(null, new Pen(Brushes.Red, 5), r);
}
this.AdornedElement.DesiredSize gives you the size of image.
The approach I would use is to render the picture and rectangle to the same thing. Then that one thing is stretched, scaled or whatever.
One way to do this would be to use a DrawingImage. Drawing methods are extremely efficient if rather low level.
<Grid ClipToBounds="True">
<AdornerDecorator>
<Image Name="img" Stretch="Uniform">
<Image.Source>
<DrawingImage PresentationOptions:Freeze="True">
<DrawingImage.Drawing>
<DrawingGroup>
<ImageDrawing Rect="0,0,595,446" ImageSource="DSC00025.jpg"/>
<GeometryDrawing Brush="Green">
<GeometryDrawing.Geometry>
<RectangleGeometry Rect="0,0,100,30" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
</AdornerDecorator>
</Grid>
Another is with a visualbrush. Controls inherit from visual - this is somewhat higher level coding.
<Grid ClipToBounds="True">
<AdornerDecorator>
<Rectangle Name="rec">
<Rectangle.Fill>
<VisualBrush Stretch="Uniform">
<VisualBrush.Visual>
<Grid Height="446" Width="595">
<Image Source="DSC00025.jpg" Stretch="Fill"/>
<Rectangle Height="30" Width="100" Fill="Green"
VerticalAlignment="Top" HorizontalAlignment="Left"/>
</Grid>
</VisualBrush.Visual>
</VisualBrush>
</Rectangle.Fill>
</Rectangle>
</AdornerDecorator>
</Grid>
Note that both of these are quick and dirty illustrations to give you the idea. The image I picked at random off my hard drive is sized 446 * 595. You could calculate sizes or bind or stretch as suits your requirement best.
I have read various solutions to the problem and I now wonder which one is to be preferred in which situation (and mine especially)
I have created a Custom Control that Renders a Color wheel and a circle (Ellipse) on a canvas in the middle. I now want to be able to click and drag that circle as a selector on the color wheel.
Possible solutions include: Overriding the OnClick and / or OnMouseMove events and update the circles position by either dependency properties or using TemplateParts or even generating the circle in the Controls Code Behind.
I wonder if it would also be possible to use triggers in XAML to achieve this effect and which solution would deliver the "smoothest" motion.
Update 1: To address comments, here is some code:
ColorPicker.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TrigDebugUtil.Controls">
<Style x:Key="ColorPicker" TargetType="{x:Type local:ColorPicker}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ColorPicker}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<Image Source="{TemplateBinding ColorWheelImage}" Width="500" Height="500"/>
<Canvas Width="10" Height="10" HorizontalAlignment="Center" VerticalAlignment="Center">
<Ellipse Fill="{TemplateBinding Property=SelectedColor}" Width="10" Height="10" Stroke="Black" StrokeThickness=".5" />
</Canvas>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
ColorPicker.cs
namespace TrigDebugUtil.Controls
{
public class ColorPicker : Control
{
#region Private Fields
#endregion //Private Fields
#region Dependency Properties
public static readonly DependencyProperty ColorWheelImageProperty = DependencyProperty.Register("ColorWheelImage", typeof(WriteableBitmap), typeof(ColorPicker));
public static readonly DependencyProperty SelectedColorProperty = DependencyProperty.Register("SelectedColor", typeof(SolidColorBrush), typeof(ColorPicker));
#endregion //Dependency Properties
#region Properties
public WriteableBitmap ColorWheelImage
{
get { return (WriteableBitmap)GetValue(ColorWheelImageProperty); }
private set { SetValue(ColorWheelImageProperty, value); }
}
public SolidColorBrush SelectedColor
{
get { return (SolidColorBrush)GetValue(SelectedColorProperty); }
private set { SetValue(SelectedColorProperty, value); }
}
#endregion //Properties
static ColorPicker()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ColorPicker), new FrameworkPropertyMetadata(typeof(ColorPicker)));
}
public ColorPicker()
{
ColorWheelImage = new WriteableBitmap(500, 500, 96, 96, PixelFormats.Rgb24, null);
SelectedColor = Brushes.White;
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
Byte[] pixels = new Byte[1500 * 500];
// Update algo here
ColorWheelImage.WritePixels(new Int32Rect(0, 0, 500, 500), pixels, 1500, 0);
}
}
}
I want to be able to click on the ellipse in the canvas and move it to another location in the control itself (i.e. on the image)
For people having the same issue, one possible solution is to implement the draggable object as a Thumb and overriding its DragStarted DragDelta and DragCompleted events.
Here's an example:
private void OnThumbDragDelta(object sender, DragDeltaEventArgs e)
{
Thumb thumb = sender as Thumb;
double newX = Canvas.GetLeft(thumb) + e.HorizontalChange;
double newY = Canvas.GetTop(thumb) + e.VerticalChange;
Canvas.SetLeft(thumb, newX);
Canvas.SetTop(thumb, newY);
// This is optional for routed events.
e.RoutedEvent = ThumbDragDeltaEvent;
RaiseEvent(e);
}
without using the DragStarted and DragCompleted Events.
I'd like to see comments on wether this is a preferred solution or if it has any disadvantages. From what I can see it's not the most efficient, on fast mouse movements, the thumb movement is delayed and not always "right under the mouse"
I have a Line and Textblock in my xaml as follow:
<Line X1="{Binding StartPoint.X}" Y1="{Binding StartPoint.Y}" X2="{Binding EndPoint.X}"
Y2="{Binding EndPoint.Y}" Stroke="{Binding Color}" StrokeThickness="{Binding Thickness}" />
<TextBlock Text="{Binding Title}" />
The TextBlock shows the title of line (for example "Line 1").
The above XAML draw the lines on a canvas and work correctly, but it doesn't show the TextBlock next to line and in parallel to it.
How can I change this XAML code so the text be in the centre of line and parallel to it.
I would do that by using a TextBlock and nest inside of it the Line and the TextBlock you want to use. For example:
<TextBlock Canvas.Left="147" Canvas.Top="132" Height="45">
<Line X1="10" Y1="20" X2="100" Y2="0" Stroke="Black" StrokeThickness="4"/>
<TextBlock Text="Line1" VerticalAlignment="Center" HorizontalAlignment="Center"/>
</TextBlock>
The result of the above will be:
I attempted to solve your problem with a different approach
example below will render the line for given coordinates and will place a label next to it
I start by writing a class for LineData
class LineData : DependencyObject
{
public Point P1
{
get { return (Point)GetValue(P1Property); }
set { SetValue(P1Property, value); }
}
// Using a DependencyProperty as the backing store for P1. This enables animation, styling, binding, etc...
public static readonly DependencyProperty P1Property =
DependencyProperty.Register("P1", typeof(Point), typeof(LineData), new PropertyMetadata(new Point(), (d, e) => (d as LineData).Update()));
public Point P2
{
get { return (Point)GetValue(P2Property); }
set { SetValue(P2Property, value); }
}
// Using a DependencyProperty as the backing store for P1. This enables animation, styling, binding, etc...
public static readonly DependencyProperty P2Property =
DependencyProperty.Register("P2", typeof(Point), typeof(LineData), new PropertyMetadata(new Point(), (d, e) => (d as LineData).Update()));
public string Label
{
get { return (string)GetValue(LabelProperty); }
set { SetValue(LabelProperty, value); }
}
// Using a DependencyProperty as the backing store for Label. This enables animation, styling, binding, etc...
public static readonly DependencyProperty LabelProperty =
DependencyProperty.Register("Label", typeof(string), typeof(LineData), new PropertyMetadata(string.Empty));
public double Length
{
get { return (double)GetValue(LengthProperty); }
set { SetValue(LengthProperty, value); }
}
// Using a DependencyProperty as the backing store for Length. This enables animation, styling, binding, etc...
public static readonly DependencyProperty LengthProperty =
DependencyProperty.Register("Length", typeof(double), typeof(LineData), new PropertyMetadata(0.0));
public double TransformAngle
{
get { return (double)GetValue(TransformAngleProperty); }
set { SetValue(TransformAngleProperty, value); }
}
// Using a DependencyProperty as the backing store for TransformAngle. This enables animation, styling, binding, etc...
public static readonly DependencyProperty TransformAngleProperty =
DependencyProperty.Register("TransformAngle", typeof(double), typeof(LineData), new PropertyMetadata(0.0));
public void Update()
{
//calculate angle
double dy = P2.Y - P1.Y;
double dx = P2.X - P1.X;
double theta = Math.Atan2(dy, dx);
theta *= 180 / Math.PI;
TransformAngle = theta - 90;
//calculate length
double aSq = Math.Pow(P1.X - P2.X, 2);
double bSq = Math.Pow(P1.Y - P2.Y, 2);
Length = Math.Sqrt(aSq + bSq);
}
}
in this class i wrote logic to convert given point into angle and length which will be used in data template to render accordingly, Update method is called when any point is changed
here comes the xaml, here l: is namespace to the line data class
<ItemsControl>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding Path=P1.X}" />
<Setter Property="Canvas.Top" Value="{Binding Path=P1.Y}" />
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.RenderTransform>
<RotateTransform Angle="{Binding TransformAngle}"/>
</Grid.RenderTransform>
<TextBlock Text="{Binding Label}" VerticalAlignment="Center" Margin="5,0,0,0"/>
<Line Y2="{Binding Length}" StrokeThickness="1" Stroke="Black"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
<l:LineData Label="Line 1" P1="100,100" P2="135,150"/>
<l:LineData Label="Line 2" P1="150,150" P2="235,50"/>
</ItemsControl>
i used the canvas as panel of the items control
placed the item at the correct place by binding canvas.left and canvas.top to point one of the line data in style of ContentPresenter
created a data template with a grid, label and line
set the y2 of line as length of line data leaving all other points to be 0
set the label in textbox
rotated the grid based on the angle
it's done, result of the sample
since I was not sure what is actually meant by parallel to you I assumed a label which is horizontal when line is vertical (so perpendicular to line)
if you want both horizontally parallel then apply a render transform to your textblock too
<TextBlock Text="{Binding Label}" VerticalAlignment="Center" Margin="16,0,0,10">
<TextBlock.RenderTransform>
<RotateTransform Angle="90"/>
</TextBlock.RenderTransform>
</TextBlock>
you may also bind the items control to a observable collection of line data and binding will do the rest.
Alternative horizontally parallel approach
public void Update()
{
double dy = P2.Y - P1.Y;
double dx = P2.X - P1.X;
double theta = Math.Atan2(dy, dx);
theta *= 180 / Math.PI;
TransformAngle = theta; //remove the 90 degree vertial adjustment
double aSq = Math.Pow(P1.X - P2.X, 2);
double bSq = Math.Pow(P1.Y - P2.Y, 2);
Length = Math.Sqrt(aSq + bSq);
}
<DataTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<Grid.RenderTransform>
<RotateTransform Angle="{Binding TransformAngle}"/>
</Grid.RenderTransform>
<TextBlock Text="{Binding Label}" HorizontalAlignment="Center" ></TextBlock>
<Line X2="{Binding Length}" Grid.Row="1" StrokeThickness="1" Stroke="Black"/>
</Grid>
</DataTemplate>
removed vertical adjustment in calculation
using Length property for line's X2
removed margin adjustments from textblock
result is bit cleaner approach without adjustments