Access GUI object method from ViewModel C# - c#

I'm trying to create a "fake" drag animation on a chart. The goal is to allow the user to manually edit points from a graph by dragging them.
The idea is fairly simple: I put a canvas on top of the graph and upon clicking on one of the graph's point, a red circle becomes visible on the clicked mark value, as well as 2 lines coming from the previous and next graph point. I "just" have to set the opacity of each shape from 0 to 1 and specify their position (XAML code):
<Canvas Name="GraphOverlay" Grid.Row="1">
<Ellipse Canvas.Left="{Binding ElipseCanvasLeft}" Canvas.Top="{Binding ElipseCanvasTop}" Stroke="{Binding ShapesColor}" StrokeThickness="6" Width="10" Height="10" Opacity="{Binding ShapesOpacity}"/>
<Line X1="{Binding leftX1}" X2="{Binding leftX2}" Y1="{Binding leftY1}" Y2="{Binding leftY2}" Stroke="{Binding ShapesColor}" StrokeThickness="3" Opacity="{Binding ShapesOpacity}"/>
<Line X1="{Binding rightX1}" X2="{Binding rightX2}" Y1="{Binding rightY1}" Y2="{Binding rightY2}" Stroke="{Binding ShapesColor}" StrokeThickness="3" Opacity="{Binding ShapesOpacity}"/>
</Canvas>
So far I can easily get the graph values from the point I clicked on as well as the previous point and next point on the graph. However, to define the position of each shape on the cavas (1 circle and two lines), I need to convert these graph values into actual pixel values. For the circle, I just have to get the mouse position when the user clics. I am using this:
<i:Interaction.Behaviors>
<utility:MouseBehaviour MouseX="{Binding PanelX, Mode=OneWayToSource}" MouseY="{Binding PanelY, Mode=OneWayToSource}"/>
</i:Interaction.Behaviors>
And the MouseBehaviour class:
public class MouseBehaviour : System.Windows.Interactivity.Behavior<FrameworkElement>
{
public static readonly DependencyProperty MouseYProperty = DependencyProperty.Register(
"MouseY", typeof(double), typeof(MouseBehaviour), new PropertyMetadata(default(double)));
public double MouseY
{
get { return (double)GetValue(MouseYProperty); }
set { SetValue(MouseYProperty, value); }
}
public static readonly DependencyProperty MouseXProperty = DependencyProperty.Register(
"MouseX", typeof(double), typeof(MouseBehaviour), new PropertyMetadata(default(double)));
public double MouseX
{
get { return (double)GetValue(MouseXProperty); }
set { SetValue(MouseXProperty, value); }
}
protected override void OnAttached()
{
AssociatedObject.MouseMove += AssociatedObjectOnMouseMove;
}
private void AssociatedObjectOnMouseMove(object sender, MouseEventArgs mouseEventArgs)
{
var pos = mouseEventArgs.GetPosition(AssociatedObject);
MouseX = pos.X;
MouseY = pos.Y;
}
protected override void OnDetaching()
{
AssociatedObject.MouseMove -= AssociatedObjectOnMouseMove;
}
}
And PanelX and PanelY are two properties from the ViewModel. Source
However I am struggling getting the pixel coordinate of the previous and next point. On livechart's website, there is an example that shows how to use both commands and events to access graph values. However, when using events, they have access to the ConvertToPixels method which is associated to the chart object. I don't have access to this method from the viewmodel.
Is there a way around it, or should I manually code that method ?
Edit: Here is a picture that shows what I am aiming for and what I mean by "next point", "previous point", ... Hopefully it helps!

I found a solution, I don't really know how ugly this is, but it keeps my codebehind nice and blank.
It's (very) ugly and should never pass a code review :)
Your view model now has a dependency not only on the App class but also on a particular control in a particular window that must be present on the screen for your code to work. This will for example never work in a unit test and breaks the MVVM pattern badly.
You should inject the view model with an interface and interact with the view using this interface. There is an example of how to do this available here if you are interested.

I found a solution, I don't really know how ugly this is, but it keeps my codebehind nice and blank.
To get access to that UI element, you have to first name it in the XAML:
<lvc:CartesianChart Name="Chart" >
...
</lvc:CartesianChart>
An then look for it in you app windows:
var MyChart = App.Current.Windows[0].FindName("Chart") as Chart;
I am not really at peace with the Windows[0] being hard coded, but this does the trick.

Related

How to simulate a sensor

So I am working on a project which requires me to do some hardware simulations. Let's say I have a belt filled with sensors. Just like this one.
When the obj reaches the sensor it should trigger the sensor which in this case the state of the sensor becomes False (otherwise True). I am working with PLC so when it's false or true I write a bit to the PLC. I have this part covered though. Just need help triggering the sensor(s).
Just like so
Notice the color of the first sensor from the right side turns gray which means it detected an object so now the sensor is false (turned off in other words). Just as the obj passes by the sensor returns to its default state which is true
To do this I had the following in mind.
Something with collision when the sensor detects an object closing by
Instead of a straight beam I could have a region and when the object lands in this region the sensor triggers.
I could work with timers (I have tried this) but it gives me the feeling that I am actually cheating. So It's not very preferable.
I am working with WPF and C#. Would someone please show me some working sample code I could built upon that.
Thanks for your time!
Update #1 Based on the comment below
I have classes of the following with proberties.
Sensor (position, size)
The object which travels along the belt. (position, size)
The simulator I am working on should view both the behavior and visual representation. Just like the diagrams. For testing purposes I have now a simple eclipse. As soon as the object closes by the color of the eclipse changes. It changes back as soon as the object passses by. Doing this in timer.
The objects traveling are always in a shape of a rectangular, but may differ in sizes. The user is able to choose between 2 sort of objects. One is as big as what the diagram now shows, and the other is little bigger which might cause to trigger sensor earlier.
You need HitTesting.
And then you need to set a DependencyProperty something like CollisionDetected to True.
Then apply a DataTrigger to your Beam xaml to change its Style/BackgroundColor etc.
<uc:BeamControl>
<uc:BeamControl.Style>
<Style TargetType="BeamControl">
<Style.Triggers>
<DataTrigger Binding="{Binding CollisionDetected}" Value="True">
<Setter Property="Background" Value="Red" />
</DataTrigger>
</Style.Triggers>
</Style>
</uc:BeamControl.Style>
</uc:BeamControl>
*********************** New Addition with code************************
In this working solution I have used an Attached Property. I have kept everything simple to focus on core issue.
Window2.xaml
<Window x:Class="WpfAnimation.Window2"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfAnimation"
Title="Window2" Height="300" Width="900">
<Canvas>
<Path local:Window2.MovingObjectPos="{Binding (Canvas.Left), ElementName=ElpObj}" Fill="Red" Stretch="Fill" Stroke="Black" HorizontalAlignment="Left" Canvas.Left="100" Canvas.Top="50" Width="39" Data="M19.5,0.5 L19.5,147.5 M0.5,148.5 L38.5,148.5 L38.5,169.5 L0.5,169.5 z">
</Path>
<Path local:Window2.MovingObjectPos="{Binding (Canvas.Left), ElementName=ElpObj}" Fill="Red" Stretch="Fill" Stroke="Black" HorizontalAlignment="Left" Canvas.Left="200" Canvas.Top="50" Width="39" Data="M19.5,0.5 L19.5,147.5 M0.5,148.5 L38.5,148.5 L38.5,169.5 L0.5,169.5 z">
</Path>
<Path local:Window2.MovingObjectPos="{Binding (Canvas.Left), ElementName=ElpObj}" Fill="Red" Stretch="Fill" Stroke="Black" HorizontalAlignment="Left" Canvas.Left="300" Canvas.Top="50" Width="39" Data="M19.5,0.5 L19.5,147.5 M0.5,148.5 L38.5,148.5 L38.5,169.5 L0.5,169.5 z">
</Path>
<Path local:Window2.MovingObjectPos="{Binding (Canvas.Left), ElementName=ElpObj}" Fill="Red" Stretch="Fill" Stroke="Black" HorizontalAlignment="Left" Canvas.Left="400" Canvas.Top="50" Width="39" Data="M19.5,0.5 L19.5,147.5 M0.5,148.5 L38.5,148.5 L38.5,169.5 L0.5,169.5 z">
</Path>
<Ellipse x:Name="ElpObj" Fill="Pink" Stroke="Black" HorizontalAlignment="Right" VerticalAlignment="Top" Width="57" Height="51" Canvas.Left="701" Canvas.Top="115">
<Ellipse.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Canvas.Left)" To="50" By="-2.0"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Ellipse.Triggers>
</Ellipse>
</Canvas>
</Window>
Window2.xaml.cs
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;
namespace WpfAnimation
{
/// <summary>
/// Interaction logic for Window2.xaml
/// </summary>
public partial class Window2 : Window
{
public Window2()
{
InitializeComponent();
}
public static double GetMovingObjectPos(DependencyObject obj)
{
return (double)obj.GetValue(MovingObjectPosProperty);
}
public static void SetMovingObjectPos(DependencyObject obj, double value)
{
obj.SetValue(MovingObjectPosProperty, value);
}
// Using a DependencyProperty as the backing store for MovingObject. This enables animation, styling, binding, etc...
public static readonly DependencyProperty MovingObjectPosProperty =
DependencyProperty.RegisterAttached("MovingObjectPos", typeof(double), typeof(Window2), new PropertyMetadata(0.0, new PropertyChangedCallback(MovingObjectPosChanged)));
private static void MovingObjectPosChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
double leftOfMovingObject = (double) e.NewValue ;
Path beam = (Path) d;
System.Diagnostics.Debug.WriteLine("Left = " + e.NewValue.ToString());
double leftOfBeam = Canvas.GetLeft(beam);
double widthOfBeam = 20.0;
if (leftOfMovingObject > leftOfBeam && leftOfMovingObject < leftOfBeam + widthOfBeam)
{
System.Diagnostics.Debug.WriteLine("Hit >>>>> = " + e.NewValue.ToString());
beam.Fill = Brushes.Gray;
}
}
}
}
I have refined my old answer using Task, and considering unlimited moving objects.
It uses actual HitTesting.
Create a UserControl for Beam object. This is needed to initiate Task immediately after Loading. It contains a DP for the containing Panel.
BeamControl.xaml
<UserControl x:Class="WpfAnimation.BeamControl" ...
Height="300" Width="39">
<Path Fill="Red" Stretch="Fill" Stroke="Black" HorizontalAlignment="Left" Width="39" Data="M19.5,0.5 L19.5,147.5 M0.5,148.5 L38.5,148.5 L38.5,169.5 L0.5,169.5 z"/>
</UserControl>
BeamControl.xaml.cs
using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;
namespace WpfAnimation
{
/// <summary>
/// Interaction logic for BeamControl.xaml
/// </summary>
public partial class BeamControl : UserControl
{
Path _beamPath;
public Panel Sensor
{
get { return (Panel)GetValue(SensorProperty); }
set { SetValue(SensorProperty, value); }
}
// Using a DependencyProperty as the backing store for Sensor. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SensorProperty =
DependencyProperty.Register("Sensor", typeof(Panel), typeof(BeamControl), new PropertyMetadata(null));
private double left;
private double top;
public BeamControl()
{
InitializeComponent();
this.Loaded += BeamControl_Loaded;
}
void BeamControl_Loaded(object sender, RoutedEventArgs e)
{
_beamPath = this.Content as Path;
left = Canvas.GetLeft(this);
top = Canvas.GetTop(this);
Task.Factory.StartNew(() =>
{
while (true)
{
_doDetection();
}
});
}
void _doDetection()
{
// 200 is the height of the beam, you can change it
for (double i = top; i < 200; i = i + 1)
{
System.Diagnostics.Debug.WriteLine(i.ToString());
Point pt = new Point(left + 20.0, i);
this.Dispatcher.Invoke(() =>
{
VisualTreeHelper.HitTest(Sensor,
new HitTestFilterCallback((o) =>
{
if (o is Ellipse)
{
_beamPath.Fill = Brushes.DarkKhaki;
System.Diagnostics.Debug.WriteLine("Detected ! " + o.GetType().ToString());
System.Diagnostics.Debug.WriteLine(pt.ToString());
return HitTestFilterBehavior.Stop;
}
else
System.Diagnostics.Debug.WriteLine(o.GetType().ToString());
return HitTestFilterBehavior.Continue;
}),
new HitTestResultCallback((result) => { return HitTestResultBehavior.Continue; }),
new PointHitTestParameters(pt));
});
}
}
}
}
MainWindow.xaml
<Window x:Class="WpfAnimation.MainWindow"
...
Title="Window5" Height="303.383" Width="622.556">
<Canvas>
<local:BeamControl Canvas.Left="200" Canvas.Top="50" Sensor="{Binding RelativeSource={RelativeSource AncestorType=Canvas, Mode=FindAncestor}}"/>
<local:BeamControl Canvas.Left="300" Canvas.Top="50" Sensor="{Binding RelativeSource={RelativeSource AncestorType=Canvas, Mode=FindAncestor}}"/>
<Ellipse x:Name="ElpObj" Fill="Pink" Stroke="Black" HorizontalAlignment="Right" VerticalAlignment="Top" Width="57" Height="51" Canvas.Left="548" Canvas.Top="114">
<Ellipse.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Canvas.Left)" To="50" By="-0.5" Duration="0:0:20"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Ellipse.Triggers>
</Ellipse>
</Canvas>
</Window>
Run the application Without Debugging.
I'm not sure if you intend to follow MVVM, but either case you can probably try doing this way:
For the visual part, you can use a Canvas, but any other Panels would probably work. Canvas would allow you to use Canvas.Left and stuff like that, and all your positioning would be probably based on System.Windows.Point values, instead of relative positioning, anyway.
The rest of the things can be drawn with one of the System.Window.Shapes.Shape subclasses. The two bars probably Rectangle, for beam would be Line, and for sensor probably an Ellipse or a Rectangle.
The beam objects should have Visibility bound to some property in your ViewModel (or simply code behind if not using MVVM). The Stroke property of the beams and Fill property of the sensors should be bound as well.
I am not sure how sensor size would affect anything, probably it could mean the beam could be very wide? If it does affect, bind the Width and Height as well. Also remember to bind beam's StrokeThickness if sensor size matters.
For the object, you would use Rectangle again. Again, do the necessary binding. The new binding would be Canvas.Left and Canvas.Top, so that you know the position.
Now we need to animate the object. Since your have the position through binding, you can change the position in the source property - the binding engine will do the render changes for you. You can use a timer for the movement of the object.
Now we need to find collision. One way is to calculate it code behind, which would be done every time the object position changes. There is another way to do it, because you are using Canvas. Read this SO post. All you need to do is to convert the underlying Geometry instance for the object and beams (through myobject.RenderedGeometry.FillContainsWithDetail(beam)).
There are certainly many other ways to do this. I just hope I managed to provide a direction to start your project.

Binding double property to AngleProperty

Is there a way to control rotation of rectangle via Binding ? I tried it this way but it does not work any idea ?
// class Unit
private double _rotation;
public double rotation
{
get
{
return _rotation;
}
set
{
_rotation = value;
OnPropertyChanged("rotation");
}
}
public Binding rotationBinding { get; set; }
// Controller class generating UI
private Rectangle GenerateUnit(Unit u)
{
Rectangle c = new Rectangle() { Width = u.size, Height = u.size };
c.Fill = new ImageBrush(new BitmapImage(new Uri(#"..\..\Images\tank\up.png", UriKind.Relative)));
c.SetBinding(Canvas.LeftProperty, u.xBinding);
c.SetBinding(Canvas.TopProperty, u.yBinding);
RotateTransform rt = new RotateTransform();
BindingOperations.SetBinding(rt, RotateTransform.AngleProperty, u.rotationBinding);
c.LayoutTransform = rt;
return c;
}
X and Y bindings work fine so I guess that is implemented correctly.
I am just looking for a way to bind the angle property so when I change the rotation property it will rotate the rectangle in UI. (I dont need animation, switching the angle instantly is fine).
Thanks
Problem seems to be in your rotationBinding. You should create binding in your Unit class:
rotationBinding = new Binding("rotation");
rotationBinding.Source = this;// or instance of o your Unit class if you create rotationBinding outside Unit class
It works for me...
I would advise against creating the binding in code when you can do it all through XAML:
<Window x:Class="WpfApplication3.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid Margin="10">
<Slider x:Name="AngleProvider"
VerticalAlignment="Bottom"
Minimum="0"
Maximum="360" />
<Rectangle Fill="Blue"
Margin="100"
RenderTransformOrigin="0.5,0.5">
<Rectangle.RenderTransform>
<RotateTransform Angle="{Binding Value, ElementName=AngleProvider}" />
</Rectangle.RenderTransform>
</Rectangle>
</Grid>
</Window>
This shows a Rectangle element in the centre of the window, and a Slider control at the bottom. Drag the slider to change the rotation angle of the rectangle.
What you should bind to is the angle of rotation. Here I am using the slider to provide that angle, but it could obviously come from a property in the code-behind of the window or a ViewModel or whatever.

How Remove the Stroke from Arabic character connection position on WPF?

My project use some font like Arabic character, I also need use stroke on the font.
I already try some way to display the Stroke, like :
https://stackoverflow.com/a/9887123/1900498 (OuterTextBlock)
https://stackoverflow.com/a/97728/1900498 (OutlineText)
The Stroke can display now, but the problem is the Arabic character connection position still display some stroke, So I need to remove it.
the result like this:
So Is there any way I can remove the stroke from the connection position? I mean If the character is connection, just stroke on the full connection outsize, not all character stroke 1 time.
I need the result like this(I'm using PHOTOSHOP to edit the second picture and remove the stroke from character connection position, not WPF, that is just for you understand the correct result must handle by WPF)
UPDATE:
please download the 2 class from the 2 link , then use This code :
<Window x:Class="test.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:test"
Title="MainWindow" Height="200" Width="400">
<Grid Background="Black">
<local:OutlinedTextBlock HorizontalAlignment="Left" Height="200" VerticalAlignment="Bottom" FontSize="150" StrokeThickness="1" Fill="White" Stroke="Red" TextDecorations="">ئالما</local:OutlinedTextBlock>
<local:OutlinedText HorizontalAlignment="Right" Height="200" VerticalAlignment="Top" FontSize="150" StrokeThickness="1" Fill="White" Stroke="Red" Text="ئالما"></local:OutlinedText>
</Grid>
Initial observations: The artifacts you are seeing seem to be the actual edges of the single characters. The characters touch and overlap slightly, while you would like to perceive several characters as one consecutive shape.
I have tried my suggestion from the comments by extending the OutlinedTextBlock class from the first linked answer by Kent Boogaart.
The Geometry instance obtained by OutlinedTextBlock from the BuildGeometry method consists of nested GeometryGroup instances (at least, separate such groups are created when incorporating text with several reading directions). After diving through those groups, you will find one PathGeometry instance per character.
N.B.: This is what I figured out when looking at the data. It is probably undocumented (?), and if it ever changes, this solution may not work any more.
By using the Geometry.Combine method, all of these PathGeometry instances can be combined with GeometryCombineMode.Union, which means that overlapping areas will be merged.
First, I have defined a method for finding all the PathGeometry objects. It recursively dives into the hierarchy of GeometryGroup objects and is rather not very efficient, but it serves to demonstrate the point - feel free to optimize this performance-wise:
private IEnumerable<PathGeometry> FindAllPathGeometries(Geometry geometry)
{
var pathGeometry = geometry as PathGeometry;
if (pathGeometry != null) {
yield return pathGeometry;
} else {
var geoGroup = geometry as GeometryGroup;
if (geoGroup != null) {
foreach (var geo in geoGroup.Children) {
foreach (var pg in FindAllPathGeometries(geo)) {
yield return pg;
}
}
}
}
}
Then, I have modified the OutlinedTextBox.EnsureGeometry method. Originally, the geometry retrieved from BuildGeometry was directly displayed:
private void EnsureGeometry()
{
if (this.textGeometry != null) {
return;
}
this.EnsureFormattedText();
this.textGeometry = this.formattedText.BuildGeometry(new Point(0, 0));
}
Instead, I now process that geometry by iterating over all of the contained PathGeometry instances and incrementally combining them with the Union mode. For the sake of convenience (and so you can actually observe the difference), I have made that behaviour optional by adding a MergeShapes property:
private void EnsureGeometry()
{
if (this.textGeometry != null) {
return;
}
this.EnsureFormattedText();
var originalGeometry = this.formattedText.BuildGeometry(new Point(0, 0));
if (MergeShapes) {
PathGeometry newGeo = new PathGeometry();
foreach (var pg in FindAllPathGeometries(originalGeometry)) {
newGeo = Geometry.Combine(newGeo, pg, GeometryCombineMode.Union, null);
}
this.textGeometry = newGeo;
} else {
this.textGeometry = originalGeometry;
}
}
public static readonly DependencyProperty MergeShapesProperty = DependencyProperty.Register("MergeShapes",
typeof(bool),
typeof(OutlinedTextBlock),
new FrameworkPropertyMetadata(OnFormattedTextUpdated));
public bool MergeShapes {
get {
return (bool)GetValue(MergeShapesProperty);
}
set {
SetValue(MergeShapesProperty, value);
}
}

Scale Two Canvases Proportionally

I am trying to think of the best way to scale two panels proportionally.
If I have a grid that contains two canvases, both stacked horizontally next to each other, I want canvas (A) to scale to the size of canvas (B), proportionally, though.
So, essentially, if canvas (B) increases in size, canvas (A) decreases, and if canvas (A) increases, canvas (B) decreases.
I'm thinking of using a converter to do this, but wanted to know if anyone had any good ideas.
Below is a link that demonstrates the desired behavior. Refer to the pan/zoom control in the lower right corner of the screen. That control represents a preview of the main screen. If you press on the zoom button within the pan/zoom control, the main screen zooms in, and the rectangular "pan" area in the pan/zoom control decreases in size.
http://quince.infragistics.com/#/Search/ViewPattern$pattern=Button+Groups/PatternExamples$guid=289a497a-6632-455a-87b6-74ee70c2d3be
Thanks!
Chris
A converter is probably be the best way to go. You could also use RenderTransform.ScaleX/ScaleY instead of adjusting the Height/Width of the canvas.
Here's an example of binding to a property. Not sure if it'd be better then a converter.
<Canvas Background="Blue">
<Canvas x:Name="canvas1" ClipToBounds="True" Background="Red" Width="100" Height="100">
<Canvas.RenderTransform>
<ScaleTransform ScaleX="{Binding ElementName=slider, Path=Value}" ScaleY="{Binding ElementName=slider, Path=Value}"/>
</Canvas.RenderTransform>
</Canvas>
<Canvas x:Name="canvas2" ClipToBounds="True" Background="Green" Grid.Column="2" Height="100" Width="100" Canvas.Left="200">
<Canvas.RenderTransform>
<ScaleTransform ScaleX="{Binding ScaleValue2}" ScaleY="{Binding ScaleValue2}"/>
</Canvas.RenderTransform>
</Canvas>
<Slider x:Name="slider" Canvas.Top="200" Width="200" Value="{Binding Path=ScaleValue, Mode=TwoWay}" Maximum="2"></Slider>
</Canvas>
Code:
public partial class Window1 : Window, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public Window1()
{
InitializeComponent();
this.DataContext = this;
}
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
private double scaleValue = 1;
public double ScaleValue
{
get
{
return scaleValue;
}
set
{
scaleValue = value;
NotifyPropertyChanged("ScaleValue");
NotifyPropertyChanged("ScaleValue2");
}
}
public double ScaleValue2
{
get
{
return slider.Maximum - ScaleValue;
}
}
}

Change the coordinate system of a Canvas in WPF

I'm writing a mapping app that uses a Canvas for positioning elements. For each element I have to programatically convert element's Lat/Long to the canvas' coordinate, then set the Canvas.Top and Canvas.Left properties.
If I had a 360x180 Canvas, can I convert the coordinates on the canvas to go from -180 to 180 rather than 0 to 360 on the X axis and 90 to -90 rather than 0 to 180 on the Y axis?
Scaling requirements:
The canvas can be any size, so should still work if it's 360x180 or 5000x100.
The Lat/Long area may not always be (-90,-180)x(90,180), it could be anything (ie (5,-175)x(89,-174)).
Elements such as PathGeometry which are point base, rather than Canvas.Top/Left based need to work.
Here's an all-XAML solution. Well, mostly XAML, because you have to have the IValueConverter in code. So: Create a new WPF project and add a class to it. The class is MultiplyConverter:
namespace YourProject
{
public class MultiplyConverter : System.Windows.Data.IValueConverter
{
public object Convert(object value, System.Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return AsDouble(value)* AsDouble(parameter);
}
double AsDouble(object value)
{
var valueText = value as string;
if (valueText != null)
return double.Parse(valueText);
else
return (double)value;
}
public object ConvertBack(object value, System.Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new System.NotSupportedException();
}
}
}
Then use this XAML for your Window. Now you should see the results right in your XAML preview window.
EDIT: You can fix the Background problem by putting your Canvas inside another Canvas. Kind of weird, but it works. In addition, I've added a ScaleTransform which flips the Y-axis so that positive Y is up and negative is down. Note carefully which Names go where:
<Canvas Name="canvas" Background="Moccasin">
<Canvas Name="innerCanvas">
<Canvas.RenderTransform>
<TransformGroup>
<TranslateTransform x:Name="translate">
<TranslateTransform.X>
<Binding ElementName="canvas" Path="ActualWidth"
Converter="{StaticResource multiplyConverter}" ConverterParameter="0.5" />
</TranslateTransform.X>
<TranslateTransform.Y>
<Binding ElementName="canvas" Path="ActualHeight"
Converter="{StaticResource multiplyConverter}" ConverterParameter="0.5" />
</TranslateTransform.Y>
</TranslateTransform>
<ScaleTransform ScaleX="1" ScaleY="-1" CenterX="{Binding ElementName=translate,Path=X}"
CenterY="{Binding ElementName=translate,Path=Y}" />
</TransformGroup>
</Canvas.RenderTransform>
<Rectangle Canvas.Top="-50" Canvas.Left="-50" Height="100" Width="200" Fill="Blue" />
<Rectangle Canvas.Top="0" Canvas.Left="0" Height="200" Width="100" Fill="Green" />
<Rectangle Canvas.Top="-25" Canvas.Left="-25" Height="50" Width="50" Fill="HotPink" />
</Canvas>
</Canvas>
As for your new requirements that you need varying ranges, a more complex ValueConverter would probably do the trick.
I was able to get it to by creating my own custom canvas and overriding the ArrangeOverride function like so:
public class CustomCanvas : Canvas
{
protected override Size ArrangeOverride(Size arrangeSize)
{
foreach (UIElement child in InternalChildren)
{
double left = Canvas.GetLeft(child);
double top = Canvas.GetTop(child);
Point canvasPoint = ToCanvas(top, left);
child.Arrange(new Rect(canvasPoint, child.DesiredSize));
}
return arrangeSize;
}
Point ToCanvas(double lat, double lon)
{
double x = this.Width / 360;
x *= (lon - -180);
double y = this.Height / 180;
y *= -(lat + -90);
return new Point(x, y);
}
}
Which works for my described problem, but it probably would not work for another need I have, which is a PathGeometry. It wouldn't work because the points are not defined as Top and Left, but as actual points.
I'm pretty sure you can't do that exactly, but it would be pretty trivial to have a method which translated from lat/long to Canvas coordinates.
Point ToCanvas(double lat, double lon) {
double x = ((lon * myCanvas.ActualWidth) / 360.0) - 180.0;
double y = ((lat * myCanvas.ActualHeight) / 180.0) - 90.0;
return new Point(x,y);
}
(Or something along those lines)
I guess another option would be to extend canvas and override the measure / arrange to make it behave the way you want.
You can use transform to translate between the coordinate systems, maybe a TransformGroup with a TranslateTranform to move (0,0) to the center of the canvas and a ScaleTransform to get the coordinates to the right range.
With data binding and maybe a value converter or two you can get the transforms to update automatically based on the canvas size.
The advantage of this is that it will work for any element (including a PathGeometry), a possible disadvantage is that it will scale everything and not just points - so it will change the size of icons and text on the map.
Another possible solution:
Embed a custom canvas (the draw-to canvas) in another canvas (the background canvas) and set the draw-to canvas so that it is transparent and does not clip to bounds. Transform the draw-to canvas with a matrix that makes y flip (M22 = -1) and translates/scales the canvas inside the parent canvas to view the extend of the world you're looking at.
In effect, if you draw at -115, 42 in the draw-to canvas, the item you are drawing is "off" the canvas, but shows up anyway because the canvas is not clipping to bounds. You then transform the draw-to canvas so that the point shows up in the right spot on the background canvas.
This is something I'll be trying myself soon. Hope it helps.
Here's an answer which describes a Canvas extension method that allows you to apply a Cartesian coordinate system. I.e.:
canvas.SetCoordinateSystem(-10, 10, -10, 10)
will set the coordinate system of canvas so that x goes from -10 to 10 and y goes from -10 to 10.
i have nearly the same problem. so i went online. and this guy uses matrix to transform from 'device pixel' to what he calls 'world coordinates' and by that he means real world numbers instead of 'device pixels' see the link
http://csharphelper.com/blog/2014/09/use-transformations-draw-graph-wpf-c/

Categories

Resources