So I'm working with a system of switches and bulbs, which are attached together by wires. I'm supposed to make the switches draggable, in such a way that the wires also move. Since you cannot update the points directly in a PointCollection (I think they're a struct I think?), I've figured out a way, but this seems VERY verbose, and I figure there must be a better way to do this.
Basically I retrieve the points, do the manipulation, clear the collection, and add the new points:
Point firstPoint = new Point(firstPointCollection[0].X, firstPointCollection[0].Y + offset.Y);
Point secondPoint = new Point(firstPointCollection[1].X + offset.X, firstPointCollection[1].Y + offset.Y);
Point thirdPoint = new Point(firstPointCollection[2].X + offset.X, firstPointCollection[2].Y + offset.Y);
firstPointCollection.Clear();
firstPointCollection.Add(firstPoint);
firstPointCollection.Add(secondPoint);
firstPointCollection.Add(thirdPoint);
In a system with multiple wires, which all consist of multiple points, this very quickly gets very tedious to write. This must all be done in C#, but if there is a better way to do with this with Data Binding of some sort, please let me know.
well the whole thing could be done with data binding so as you update the location of a bulb it auto updates the UI, stay with me here the code example is a bit long..
the result of this demo is just some circles with lines joining them but you could template this up however you want.
The xaml window, this declares an items control that will display the bulbs (and the lines joining them)
<Window x:Class="points.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:points"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
Title="MainWindow" Height="350" Width="525">
<ItemsControl ItemsSource="{Binding Bulbs}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Canvas HorizontalAlignment="Left" VerticalAlignment="Top">
<Ellipse Fill="Blue" Width="10" Height="10">
<Ellipse.RenderTransform>
<TranslateTransform X="{Binding X}" Y="{Binding Y}"/>
</Ellipse.RenderTransform>
</Ellipse>
<Line X1="{Binding X}" Y1="{Binding Y}" X2="{Binding NextBulb.X}" Y2="{Binding NextBulb.Y}" Stroke="Red">
<Line.RenderTransform>
<TranslateTransform X="5" Y="5"/>
</Line.RenderTransform>
</Line>
</Canvas>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Window>
and this is the cs, I've created a bulb structure that holds the X and Y location of a bulb and a reference to the next bulb in the chain, I've placed all bulbs in to a line and then just moved the Y location of element 5 a bit lower to show it updating.
using System.Collections.ObjectModel;
using System.Windows;
namespace points
{
public class Bulb : DependencyObject
{
public double X
{
get { return (double)GetValue(XProperty); }
set { SetValue(XProperty, value); }
}
public static readonly DependencyProperty XProperty =
DependencyProperty.Register("X", typeof(double), typeof(Bulb), new PropertyMetadata(0d));
public double Y
{
get { return (double)GetValue(YProperty); }
set { SetValue(YProperty, value); }
}
public static readonly DependencyProperty YProperty =
DependencyProperty.Register("Y", typeof(double), typeof(Bulb), new PropertyMetadata(0d));
public Bulb NextBulb
{
get { return (Bulb)GetValue(NextBulbProperty); }
set { SetValue(NextBulbProperty, value); }
}
public static readonly DependencyProperty NextBulbProperty =
DependencyProperty.Register("NextBulb", typeof(Bulb), typeof(Bulb), new PropertyMetadata(null));
}
public partial class MainWindow : Window
{
public ObservableCollection<Bulb> Bulbs
{
get { return (ObservableCollection<Bulb>)GetValue(BulbsProperty); }
set { SetValue(BulbsProperty, value); }
}
public static readonly DependencyProperty BulbsProperty =
DependencyProperty.Register("Bulbs", typeof(ObservableCollection<Bulb>), typeof(MainWindow), new PropertyMetadata(null));
public MainWindow()
{
Bulbs = new ObservableCollection<Bulb>();
InitializeComponent();
for (int i = 0; i < 10; i++)
{
double x = i * 50;
double y = 25;
Bulbs.Add(new Bulb()
{
X = x,
Y = y,
NextBulb = i > 0 ? Bulbs[i - 1] : null,
});
}
Bulbs[5].Y = 50;
}
}
}
The outcome of this is that altering the Bulbs structure in any way will update the bulb display on the UI without having to clear and recreate your collection every time, you can even run animations on the properties for some pretty cool effects.
I really like the accepted answer - an upvote well deserved - because it was well written and really works. But, I thought it would be worth noting: if you intend to use it for many quick changing values, updating a PointCollection will perform better. See How to draw a simple (line) graph?.
Regarding the code and it's verbosity, maybe this would have (7 years ago) helped:
firstPointCollection = new PointCollection(firstPointCollection.Select(p=>new Point(p.X + offset.X, p.Y + offset.Y)));
Cheers ;o)
First of all, the Offset() method
As Amnestic pointed out even though Point is a struct you can mutate it through the Offset method. However since he is using a PointCollection and not an array of Points when you use the indexer you will get a copy of the struct, not the original.
If the points were in a Point[] you could just use this:
firstPointCollection[0].Offset(x, y)
However because we are using a PointCollection you need to set the indexer as such:
firstPointCollection[0] = firstPointCollection[0].Offset(x, y)
The discussion over whether structs should be mutable or not is a whole other kettle of fish.
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 Canvas setup inside an ItemsControl, which I am populating with buttons. Each button has an X and Y position property which is used to position the button on the canvas.
What im trying to do is make the item positions relative to the size of the canvas. My problem is when I resize the canvas, the button ViewModels are not getting the NotifyPropertyChanged call, so their positions never update.
<ItemsControl ItemsSource="{Binding Buttons}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Top" Value="{Binding PosY, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<Setter Property="Canvas.Left" Value="{Binding PosX, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:ToggleButton}">
<local:ToggleButton DataContext="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
in the ToggleButtonViewModel:
public double PosY {
get {
return (_Button.PositionVertical * Size[1]);
}
}
public double PosX {
get { return (_Button.PositionHorizontal * Size[0]); }
}
Size is a double[] with the X and Y dimensions of the canvas. I've tried storing it in the CanvasViewModel, in which case I can keep it updated with a SizeChanged event on the canvas, but then I couldnt figure out how to update the PosX and PosY in the ToggleButtonViewModel when the size changes.
I also tried storing the size as a static parameter in all of the ToggleButtonViewModels, but i couldnt use a NotifyPropertyChanged with a static parameter.
Any ideas how I could get the PosX and PosY to update when the size of the canvas changes?
I think the way to go here, would be to implement a custom Panel which overrides your Arrange Logic, just like Clemens suggested, and add some attached Properties for the Positioning.
I made a really quick small solution for it and it seems to work like you want it to.
Its a simple Panel "RelativePositionCanvas", that supplies two attached Properties "RelativeX" and "RelativeY" which are double values which usually range from 0 to 1, and are used for setting the position of the top left corner, relative to the panel's size.
If you want to add more like aligning it to the right side, or making the size relative, you would just have to add some additional Attached Properties for those and use them in the ArrangeOverride method.
public class RelativePositionCanvas : Panel
{
#region Properties
#region RelativeX
public static readonly DependencyProperty RelativeXProperty =
DependencyProperty.RegisterAttached(
"RelativeX",
typeof(double),
typeof(RelativePositionCanvas),
new FrameworkPropertyMetadata(
0.0d,
new PropertyChangedCallback(OnPositioningChanged)));
public static double GetRelativeX(DependencyObject d)
{
return (double)d.GetValue(RelativeXProperty);
}
public static void SetRelativeX(DependencyObject d, double value)
{
d.SetValue(RelativeXProperty, value);
}
#endregion
#region RelativeY
public static readonly DependencyProperty RelativeYProperty =
DependencyProperty.RegisterAttached(
"RelativeY",
typeof(double),
typeof(RelativePositionCanvas),
new FrameworkPropertyMetadata(
0.0d,
new PropertyChangedCallback(OnPositioningChanged)));
public static double GetRelativeY(DependencyObject d)
{
return (double)d.GetValue(RelativeYProperty);
}
public static void SetRelativeY(DependencyObject d, double value)
{
d.SetValue(RelativeYProperty, value);
}
#endregion
private static void OnPositioningChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
UIElement uie = d as UIElement;
if (uie != null)
{
RelativePositionCanvas p = VisualTreeHelper.GetParent(uie) as RelativePositionCanvas;
if (p != null)
p.InvalidateArrange();
}
}
#endregion
protected override Size MeasureOverride(Size availableSize)
{
Size childConstraint = new Size(Double.PositiveInfinity, Double.PositiveInfinity);
foreach (UIElement child in InternalChildren)
{
if (child == null) { continue; }
child.Measure(childConstraint);
}
return new Size();
}
protected override Size ArrangeOverride(Size arrangeSize)
{
var children = this.Children;
foreach(UIElement child in children)
{
if (child == null) { continue; }
var childRelativeX = GetRelativeX(child);
var childRelativeY = GetRelativeY(child);
var posX = childRelativeX * arrangeSize.Width;
var posY = childRelativeY * arrangeSize.Height;
child.Arrange(new Rect(new Point(posX, posY), child.DesiredSize));
}
return arrangeSize;
}
}
To use it you would have to replace the ItemsPanelTemplate by the RelativePositionCanvas, and the Canvas.Top, and Canvas.Left properties by the RelativePositionCanvas.RelativeY and RelativePositionCanvas.RelativeX properties and should be fine to go
Option1) In the ToggleButtonViewModel, change your definitions of PosX and PosY to be "normal" WPF notifying properties (I assume you know what I mean) which notify when changed. When the values in Size change, set the properties on your ViewModel. For example:
_viewModel.PosY = _Button.PositionVertical * Size[1];
_viewModel.PosX = _Button.PositionHorizontal * Size[0];
This gives a nice separation of concerns; but may not be possible, depending on where you are holding what information.
Option2) Add a method to your ViewModel which you can call to indicate that Size has changed, within which you can:
RaiseNotifyPropertyChanged(nameof(PosX));
RaiseNotifyPropertyChanged(nameof(PosY));
(I can give you an implementation of RaiseNotifyPropertyChanged if you need, but it's pretty standard WPF boilerplate code that you're likely to already have in your ViewModel.)
Simple xaml:
<WrapPanel Orientation="Vertical">
<Ellipse Width="100" Height="100" Fill="Red" />
<Ellipse Width="100" Height="100" Fill="Yellow" />
<Ellipse Width="100" Height="100" Fill="Green" />
</WrapPanel>
Resizing window:
How to show vertical and horizontal scrollbars when content doesn't fit?
Note: this should work for any content.
I tried to put it into ScrollViewer:
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<WrapPanel Orientation="Vertical">
...
</WrapPanel>
</ScrollViewer>
but then WrapPanel stops wrapping anything (always one column):
The problem here is that ScrollViewer gives (NaN, NaN) size to it child, so wrapping never occurs.
I tried to fix it by binding scroll viewer available height to max height of panel:
<ScrollViewer ...>
<WrapPanel MaxHeight="{Binding ViewportHeight, RelativeSource={RelativeSource AncestorType=ScrollViewer}}" ...>
This will limit panel height (not NaN anymore), so wrapping now occurs. But because this also adjust the height of panel - the vertical scrollbar will never appears:
How to add vertical scrollbar?
In my case WrapPanel is vertical, means it will fill columns as much as it can and then wrap to a new column from left to right. Scrollbars are needed when children doesn't fit either vertically (when available space is less than children height) or horizontally.
The idea thought can be used for a standard (horizontal) WrapPanel: from left to right, creating new rows when full. Absolutely same problem will arise (just tried it).
That sort of behavior is not possible with a WrapPanel without setting explicitly its Height/MinHeight for a Vertical orientation or Width/MinWidth for a Horizontal orientation. The ScrollViewer will only show the scrollbars when the FrameworkElement this scroll viewer wraps doesn't fit into the viewport.
You can create your own wrap panel that calculates its minimum size based on its children.
Alternatively, you can implement a Behavior<WrapPanel> or an attached property. This won't be as easy as just adding a couple of XAML tags, as you might expect.
We have solved this issue with an attached property. Let me give you an idea of what we did.
static class ScrollableWrapPanel
{
public static readonly DependencyProperty IsEnabledProperty =
DependencyProperty.RegisterAttached("IsEnabled", typeof(bool), typeof(ScrollableWrapPanel), new PropertyMetadata(false, IsEnabledChanged));
// DP Get/Set static methods omitted
static void IsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var panel = (WrapPanel)d;
if (!panel.IsInitialized)
{
panel.Initialized += PanelInitialized;
}
// Add here the IsEnabled == false logic, if you wish
}
static void PanelInitialized(object sender, EventArgs e)
{
var panel = (WrapPanel)sender;
// Monitor the Orientation property.
// There is no OrientationChanged event, so we use the DP tools.
DependencyPropertyDescriptor.FromProperty(
WrapPanel.OrientationProperty,
typeof(WrapPanel))
.AddValueChanged(panel, OrientationChanged);
panel.Unloaded += PanelUnloaded;
// Sets up our custom behavior for the first time
OrientationChanged(panel, EventArgs.Empty);
}
static void OrientationChanged(object sender, EventArgs e)
{
var panel = (WrapPanel)sender;
if (panel.Orientation == Orientation.Vertical)
{
// We might have set it for the Horizontal orientation
BindingOperations.ClearBinding(panel, WrapPanel.MinWidthProperty);
// This multi-binding monitors the heights of the children
// and returns the maximum height.
var converter = new MaxValueConverter();
var minHeightBiding = new MultiBinding { Converter = converter };
foreach (var child in panel.Children.OfType<FrameworkElement>())
{
minHeightBiding.Bindings.Add(new Binding("ActualHeight") { Mode = BindingMode.OneWay, Source = child });
}
BindingOperations.SetBinding(panel, WrapPanel.MinHeightProperty, minHeightBiding);
// We might have set it for the Horizontal orientation
BindingOperations.ClearBinding(panel, WrapPanel.WidthProperty);
// We have to define the wrap panel's height for the vertical orientation
var binding = new Binding("ViewportHeight")
{
RelativeSource = new RelativeSource { Mode = RelativeSourceMode.FindAncestor, AncestorType = typeof(ScrollViewer)}
};
BindingOperations.SetBinding(panel, WrapPanel.HeightProperty, binding);
}
else
{
// The "transposed" case for the horizontal wrap panel
}
}
static void PanelUnloaded(object sender, RoutedEventArgs e)
{
var panel = (WrapPanel)sender;
panel.Unloaded -= PanelUnloaded;
// This is really important to prevent the memory leaks.
DependencyPropertyDescriptor.FromProperty(WrapPanel.OrientationProperty, typeof(WrapPanel))
.RemoveValueChanged(panel, OrientationChanged);
}
private class MaxValueConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return values.Cast<double>().Max();
}
// ConvertBack omitted
}
}
It is maybe not the easiest way, and there are a little bit more lines that just a few XAML tags, but it works flawlessly.
You have to be careful with the error handling though. I've just omitted all the checks and exception handling in the sample code.
The usage is simple:
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<WrapPanel Orientation="Vertical" local:ScrollableWrapPanel.IsEnabled="True">
<!-- Content -->
</WrapPanel>
</ScrollViewer
It seems monitoring for children is one of important task to achieve wanted. So why not creating custom panel:
public class ColumnPanel : Panel
{
public double ViewportHeight
{
get { return (double)GetValue(ViewportHeightProperty); }
set { SetValue(ViewportHeightProperty, value); }
}
public static readonly DependencyProperty ViewportHeightProperty =
DependencyProperty.Register("ViewportHeight", typeof(double), typeof(ColumnPanel),
new FrameworkPropertyMetadata(double.PositiveInfinity, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsArrange));
protected override Size MeasureOverride(Size constraint)
{
var location = new Point(0, 0);
var size = new Size(0, 0);
foreach (UIElement child in Children)
{
child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
if (location.Y != 0 && ViewportHeight < location.Y + child.DesiredSize.Height)
{
location.X = size.Width;
location.Y = 0;
}
if (size.Width < location.X + child.DesiredSize.Width)
size.Width = location.X + child.DesiredSize.Width;
if (size.Height < location.Y + child.DesiredSize.Height)
size.Height = location.Y + child.DesiredSize.Height;
location.Offset(0, child.DesiredSize.Height);
}
return size;
}
protected override Size ArrangeOverride(Size finalSize)
{
var location = new Point(0, 0);
var size = new Size(0, 0);
foreach (UIElement child in Children)
{
if (location.Y != 0 && ViewportHeight < location.Y + child.DesiredSize.Height)
{
location.X = size.Width;
location.Y = 0;
}
child.Arrange(new Rect(location, child.DesiredSize));
if (size.Width < location.X + child.DesiredSize.Width)
size.Width = location.X + child.DesiredSize.Width;
if (size.Height < location.Y + child.DesiredSize.Height)
size.Height = location.Y + child.DesiredSize.Height;
location.Offset(0, child.DesiredSize.Height);
}
return size;
}
}
The usage (instead of WrapPanel) is following:
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<local:ColumnPanel ViewportHeight="{Binding ViewportHeight, RelativeSource={RelativeSource AncestorType=ScrollViewer}}" ... >
...
</local:ColumnPanel>
</ScrollViewer>
The idea is to calculate layout manually, while MeasureOverride and ArrangeOverride will be automatically called whenever children are changed: added, deleted, resized, etc.
Measure logic is simple: start from (0,0) and measure next child size, if it fits into current column - add it, otherwise start and new column by offsetting location. During whole measurement cycle adjust the resulting size.
The only missing part of puzzle is to provide into measure/arrange cycles ViewportHeight from parent ScrollViewer. This is the role of ColumnPanel.ViewportHeight.
Here is the demo (button add purple circle):
You can do this by wrapping your wrappanel in a scrollviewer, but then binding the height and width of the inner panel to the Height and Width of the Viewport of the scrollviewer, so it stretches and contracts with the rest of the screen.
I've also added minimum Height and Width to my sample, which ensures that the scrollbars appear once the wrap panel is pushed smaller than it's min dimensions
<ScrollViewer x:Name="sv" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
<WrapPanel MinWidth="200" Width="{Binding ElementName=sv, Path=ViewportWidth}" MinHeight="200" Height="{Binding ElementName=sv, Path=ViewportHeight}">
<Ellipse Fill="Red" Height="200" Width="200"/>
<Ellipse Fill="Yellow" Height="200" Width="200"/>
<Ellipse Fill="Green" Height="200" Width="200"/>
</WrapPanel>
</ScrollViewer>
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
Background
[Feel free to skip this]
I'm building a program that deals with horses, their owners, and the owner's racing colours (silks). This question is about a UserControl, called SilksControl that acts as a view for the JockeySilks.
To represent the silks I use the following class of enums:
public class JockeySilks
{
public BodyPatterns BodyPattern { get; set; }
public Colour BodyColour1 { get; set; }
public Colour BodyColour2 { get; set; }
public SleevePatterns SleevePattern { get; set; }
public Colour SleeveColour1 { get; set; }
public Colour SleeveColour2 { get; set; }
public CapPatterns CapPattern { get; set; }
public Colour CapColour1 { get; set; }
public Colour CapColour2 { get; set; }
}
As you can see, there's different patterns and colours for each element of the jockey silks. The main portion of each element is the [Item]Colour1, and the pattern is filled with [Item]Colour2.
The basic composition of the SilksControl is a ViewBox that contains a Canvas which in turn contains a number of Paths. I drew each of the patterns as Paths inside a child Canvas.
Here's a picture. In this example, CapPattern and BodyPattern are set to Plain, and ArmPattern is set to Chevrons.
The Problem
I'm trying to figure out the best way to set the pattern based on a WPF data binding. However, there's one issue: each pattern Path has different Canvas.Top and Canvas.Left values and dimensions.
By "best way" I mean something that's simple, readable and easy to implement.
Approaches I've thought about
Switching the path in code - maybe something like pthCapPattern = CapPatterns[SilksModel.CapPattern] where CapPatterns is a Dictionary<CapPattern,Path> or maybe accessing it from the resources
But not a binding, I'd have to implement some events and stuff
Binding the content of some control/panel to the SilksModel.[Item]Pattern with a converter that generates/pulls a Path from resources/dictionary
Which control?
Could have to generate entire new Path
Somewhat resource intensive
Have all the Paths in the XAML and change the visibility of each
This is just awkward and weird
Figure out a way to reconcile the differences in dimensions then create 1 path and bind to its Path.Data property (probably have some StreamGeometry in the resources, using a converter to go from enum to StreamGeometry)
I have no idea how to give them the same dimensions and hence Canvas offsets. :(
So solution 4 is my preferred solution but as I mentioned, I have no idea how I could do it, and my Googling skills can't come up with anything useful. Failing that, solution 2 would be the next best thing but I don't know of any containers that offer the same functionality as a canvas and offer binding to the children/content.
EDIT 1:
<Canvas x:Name="SPatterns" Height="173" Canvas.Left="6.8" Canvas.Top="107" Width="236.6">
<Path x:Name="Chevrons" Fill="{Binding SilksModel.BodyColour2, Converter={StaticResource DBColourToColorConverter}, ElementName=root" Height="134.125" Canvas.Left="1.087" Stretch="Fill" Stroke="Black" Canvas.Top="21.667" Width="234.168" Data="M21.750001,94.749999 L34.000002,117.66218 30.625003,133.62501 17.000006,113.32909 0.5,126.75 3.2500048,108.125 z M212.418,93.416999 L230.918,106.79199 233.668,125.41701 217.168,111.99609 203.543,132.292 200.168,116.32917 z M32.25,48.374999 L44.250004,72.249999 40.625004,90.249999 28.000003,68.581336 7.750001,82.249999 11.665709,64.166339 z M201.918,47.041991 L222.50229,62.833335 226.418,80.916991 206.168,67.248336 193.543,88.916999 189.918,70.916991 z M41,1.8329993 L55.000002,28.166337 51.66667,45.832999 37.333336,23.499837 16.666001,37.417269 21.66571,19.418135 z M193.168,0.5 L212.50229,18.085143 217.502,36.084262 196.83467,22.166836 182.50133,44.499991 179.168,26.833333 z" />
<!-- More SleevePatterns -->
</Canvas>
This might not be the cleanest solution, but would something like this work for you (Obviously you'd move the geometry initialisation out of the constructor)?
You could create your suggested Dictionary<CapPattern,Path> object and populate it with your path information, but also apply a Transform to the Geometry to give it your desired dimensions/offset, relative to the Canvas.
public partial class Horses : UserControl, INotifyPropertyChanged
{
public enum CapPattern { ChevronPattern, SomeOtherPattern };
public Dictionary<CapPattern, Geometry> Patterns { get; set; }
private Geometry currentPath;
public Geometry CurrentPath
{
get { return this.currentPath; }
set
{
this.currentPath = value;
NotifyPropertyChanged();
}
}
public Horses()
{
Patterns = new Dictionary<CapPattern, Geometry>();
Patterns.Add(
CapPattern.ChevronPattern,
Geometry.Combine(
Geometry.Parse("M21.750001,94.749999 L34.000002,117.66218 30.625003,133.62501 17.000006,113.32909 0.5,126.75 3.2500048,108.125 z M212.418,93.416999 L230.918,106.79199 233.668,125.41701 217.168,111.99609 203.543,132.292 200.168,116.32917 z M32.25,48.374999 L44.250004,72.249999 40.625004,90.249999 28.000003,68.581336 7.750001,82.249999 11.665709,64.166339 z M201.918,47.041991 L222.50229,62.833335 226.418,80.916991 206.168,67.248336 193.543,88.916999 189.918,70.916991 z M41,1.8329993 L55.000002,28.166337 51.66667,45.832999 37.333336,23.499837 16.666001,37.417269 21.66571,19.418135 z M193.168,0.5 L212.50229,18.085143 217.502,36.084262 196.83467,22.166836 182.50133,44.499991 179.168,26.833333 z"),
Geometry.Empty,
GeometryCombineMode.Union,
new TranslateTransform(0, 0)));
Patterns.Add(
CapPattern.SomeOtherPattern,
Geometry.Combine(
Geometry.Parse("M21.750001,94.749999 L34.000002,117.66218 30.625003,133.62501 17.000006,113.32909 0.5,126.75 3.2500048,108.125 z M212.418,93.416999 L230.918,106.79199 233.668,125.41701 217.168,111.99609 203.543,132.292 200.168,116.32917 z M32.25,48.374999 L44.250004,72.249999 40.625004,90.249999 28.000003,68.581336 7.750001,82.249999 11.665709,64.166339 z M201.918,47.041991 L222.50229,62.833335 226.418,80.916991 206.168,67.248336 193.543,88.916999 189.918,70.916991 z M41,1.8329993 L55.000002,28.166337 51.66667,45.832999 37.333336,23.499837 16.666001,37.417269 21.66571,19.418135 z M193.168,0.5 L212.50229,18.085143 217.502,36.084262 196.83467,22.166836 182.50133,44.499991 179.168,26.833333 z"),
Geometry.Empty,
GeometryCombineMode.Union,
new TranslateTransform(20, 30)));
InitializeComponent();
}
// INotifyPropertyChanged implementaton.
}
In my mock up, I've populated a ComboBox from that dictionary which sets a property CurrentPath, which is bound to a Path on the Canvas:
<Grid>
<StackPanel>
<ComboBox ItemsSource="{Binding Path=Patterns}"
SelectedValue="{Binding Path=CurrentPath}"
SelectedValuePath="Value"
DisplayMemberPath="Key"/>
<Canvas>
<Path Data="{Binding Path=CurrentPath}" Stroke="Black" StrokeThickness="1" />
</Canvas>
</StackPanel>
</Grid>
You would retain your binding for Fill and other properties.
Another approach could be to make a small Class which held your Path information, alongside the required Top, Left, Transform or any other information required to position the pattern. You could then bind a list of those objects to a ComboBox in a way similar to the above, and bind all the required properties on Canvas and Path to the properties of the currently selected object.
Edit:
You may also be able to configure your transforms in a ResourceDictionary along these lines:
<Path x:Name="Chevrons" Fill="{Binding SilksModel.BodyColour2, Converter={StaticResource DBColourToColorConverter}, ElementName=root" Data="M21.750001,94.749999 L34.000002,117.66218 30.625003,133.62501 17.000006,113.32909 0.5,126.75 3.2500048,108.125 z M212.418,93.416999 L230.918,106.79199 233.668,125.41701 217.168,111.99609 203.543,132.292 200.168,116.32917 z M32.25,48.374999 L44.250004,72.249999 40.625004,90.249999 28.000003,68.581336 7.750001,82.249999 11.665709,64.166339 z M201.918,47.041991 L222.50229,62.833335 226.418,80.916991 206.168,67.248336 193.543,88.916999 189.918,70.916991 z M41,1.8329993 L55.000002,28.166337 51.66667,45.832999 37.333336,23.499837 16.666001,37.417269 21.66571,19.418135 z M193.168,0.5 L212.50229,18.085143 217.502,36.084262 196.83467,22.166836 182.50133,44.499991 179.168,26.833333 z" Stroke="Black" StrokeThickness="1">
<Path.RenderTransform>
<TranslateTransform X="20" Y="120"/>
</Path.RenderTransform>
</Path>