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.
Related
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.
I'm trying to render a simple image by binding a PathFigureCollection defined in Code-Behind to the Figures property of a corresponding UI element. PropertyChanged is appearing as null in the debugger and the figures I'm trying to render are not appearing.
This is my first time implementing data binding so I'm guessing the issue lies with my understanding of it. Most similar issues I've found were solved by setting the DataContext variable or setting Source instead of Path in the XAML. I implemented those solutions and they don't solve my problem.
<Window x:Class="DrawingSandBox.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:DrawingSandBox"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="754">
<Grid Margin="0,0,0,0">
<Image HorizontalAlignment="Left" Height="400" Margin="10,10,10,10" VerticalAlignment="Bottom" Width="700" RenderTransformOrigin="0.5,0.5">
<mage.RenderTransform>
<TransformGroup>
<ScaleTransform/>
<SkewTransform/>
<RotateTransform Angle="0"/>
<TranslateTransform/>
</TransformGroup>
</Image.RenderTransform>
<Image.Source>
<DrawingImage>
<DrawingImage.Drawing>
<GeometryDrawing Brush="Black">
<GeometryDrawing.Pen>
<Pen Thickness="11" Brush="Black"/>
</GeometryDrawing.Pen>
<GeometryDrawing.Geometry>
<PathGeometry Figures="{Binding Path=Frame, UpdateSourceTrigger=PropertyChanged}" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
</Grid>
namespace DrawingSandBox
{
public partial class MainWindow : Window
{
private static readonly CurveBuilder curve = new CurveBuilder();
public MainWindow()
{
InitializeComponent();
DataContext = curve;
}
}
public class CurveBuilder : INotifyPropertyChanged
{
private PointCollection points;
private PolyBezierSegment seg;
private PathFigureCollection frame;
public event PropertyChangedEventHandler PropertyChanged;
public PathFigure Figure;
public PathFigureCollection Frame
{
get
{
return frame;
}
set
{
if (value != frame)
{
frame = value;
NotifyPropertyChanged("Frame");
}
}
}
public CurveBuilder()
{
points = new PointCollection { new Point(20, 20), new Point(40, 40) };
seg = new PolyBezierSegment(points, true);
Figure = new PathFigure(new Point(50, 50), new PathSegmentCollection { seg }, false);
Frame = new PathFigureCollection { Figure };
}
public void NotifyPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
}
As it stands, this code merely displays a blank page.
You need at least 3 points for the PolyBezierSegment to show up.
According to the documentation:
A cubic Bezier curve is defined by four points: a start point, an end point and two
control points. A PolyBezierSegment specifies one or more cubic Bezier curves by
setting the Points property to a collection of points. For every three points in the
collection, the first and second points specify the two control points of the curve
and the third point specifies the end point.
The binding is correct. Add one more Point to your PointCollection or use a PolyQuadraticBezierSegment or QuadraticBezierSegment.
I am building an application like a noteBook in Landscape.
I have two canvas next to each other and I'm changing the Background inside them.
Every time I write on the two canvas and move to another 2 backgrounds, I need to save what I wrote on the two canvas and reload it once I move to these backgrounds once again, and clear them. And do the same for all the backgrounds.
<Grid x:Name="RenderedGrid" Width="1140" Height="770">
<ScrollViewer x:Name="DScrollViewer" MaxZoomFactor="2.0" MinZoomFactor="1.0" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden" ZoomMode="Disabled">
<StackPanel Orientation="Horizontal">
<Canvas x:Name="inkCanvas0" HorizontalAlignment="Left" Width="570">
<Canvas.Background>
<ImageBrush x:Name="DImage0" ImageSource="{Binding}"/>
</Canvas.Background>
</Canvas>
<Canvas x:Name="inkCanvas1" HorizontalAlignment="Right" Width="570">
<Canvas.Background>
<ImageBrush x:Name="DImage1" ImageSource="{Binding}"/>
</Canvas.Background>
</Canvas>
</StackPanel>
</ScrollViewer>
</Grid>
I need to bind the Canvas in a way to this class instead of inkCanvas0 & inkCanvas1:
public class MyCanvas
{
public Canvas inkCanvas { get; set; }
}
List<MyCanvas> CanvasList = new List<MyCanvas>();
This is how I am creating Empty Canvas and storing them in CanvasList:
for (int i = 0; i < 24; i++)
{
Canvas canvas = new Canvas();
canvas.Name = "inkCanvas" + i;
MyCanvas mc = new MyCanvas();
mc.inkCanvas = canvas;
CanvasList.Add(mc);
}
I need to Bind these Canvas to the its corresponding page (inkCanvas0 to page0)
but I'm getting an exception in Xaml when I try to bind the Name of the Canvas.
<Canvas x:Name="{Binding}" HorizontalAlignment="Left" Width="570">
Exception: Catastrophic failure The name already exists in the tree.
How can I solve this? Is my logic of creating and loading the Canvas correct?
I'm making a ploting application. Because I want same behaviour as math plots I apply following transformation to canvas with data points:
<UserControl.Resources>
<TransformGroup x:Key="CanvasTransform">
<TranslateTransform X="30" Y="30"/>
<ScaleTransform ScaleX="1" ScaleY="-1" CenterX=".5" CenterY=".5" />
</TransformGroup>
</UserControl.Resources>
Here is the transformation used:
<ListBox>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<Canvas>
<Canvas.RenderTransformOrigin>
<Point X="0.5" Y="0.5"/>
</Canvas.RenderTransformOrigin>
<Canvas.RenderTransform>
<Binding Source="{StaticResource CanvasTransform}"/>
</Canvas.RenderTransform>
</Canvas>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
So far so good. The problem is with adding points to plot. Because the mouse click event is returning position in window coordinates it's no use. The point is added at wrong location, because it's transformed after adding.
e.g. Canvas is 400 units high. I click in top left corner mouse location is [X=10, Y=10] this point is added to the plot and rendered. The render transform then use the [10,10] point and calculates it's new position: [X=40,Y=360] (window coordinates).
It means that I click in top corner and the point appears in bottom corner. Which is in fact correct behavior.
My question is, how to apply the render transform manually before storing the point, so the point will appear under mouse.
So far I have tried following:
var trans = Resources["CanvasTransform"] as TransformGroup;
var mouse = e.GetPosition(this); // mouse position relative to canvas
var newPoint = trans.Transform(mouse);
But after this transformation newPoint has following coordinates [40,-39]. Again I know why is the result as it is. The origin of the transform is [0,0] and the translation is 29 probably due to rounding error.
Now I can take this new point and manualy change the values - subtract 30 from X coord and then add Canvas.ActualHeight to Y coordinate, which will fix the position.
But then what's the point?
My question is: is it possible to apply the RenderTransform in same fashion as the rendere is doing it, to avoid fiddling with coordinates manualy?
CenterX=".5" CenterY=".5" in ScaleTransform is unnecessary. All it does is adds a tiny translate transform (half-pixel).
To get source position from transformed position, you need to use inverse transformation (Inverse property of Transform). This is where X-30 error comes from.
To change transformation origin, you need to, first, subtract half canvas size, then transform, then add half canvas size.
var origin = new Point(lstItems.ActualWidth / 2, lstItems.ActualHeight / 2);
var transform = ((TransformGroup)Resources["CanvasTransform"]).Clone();
transform.Children.Insert(0, new TranslateTransform(-origin.X, -origin.Y));
transform.Children.Add(new TranslateTransform(origin.X, origin.Y));
_transform = transform.Inverse;
Complete sample:
MainWindow.xaml
<Window x:Class="So21501609WpfMouseRenderTransform.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" SizeToContent="WidthAndHeight">
<Control.Resources>
<TransformGroup x:Key="CanvasTransform">
<TranslateTransform X="30" Y="30"/>
<ScaleTransform ScaleX="1" ScaleY="-1" CenterX=".5" CenterY=".5"/>
</TransformGroup>
<Style TargetType="TextBlock">
<Setter Property="Background" Value="SkyBlue"/>
</Style>
</Control.Resources>
<ItemsControl x:Name="lstItems" MouseDown="LstItems_OnMouseDown" Width="400" Height="400" Background="Transparent">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas>
<Canvas.RenderTransformOrigin>
<Point X="0.5" Y="0.5"/>
</Canvas.RenderTransformOrigin>
<Canvas.RenderTransform>
<Binding Source="{StaticResource CanvasTransform}"/>
</Canvas.RenderTransform>
</Canvas>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.Items>
<TextBlock Canvas.Left="10" Canvas.Top="10" Text="10 10"/>
<TextBlock Canvas.Left="10" Canvas.Top="300" Text="10 300"/>
<TextBlock Canvas.Left="300" Canvas.Top="300" Text="300 300"/>
<TextBlock Canvas.Left="300" Canvas.Top="10" Text="300 10"/>
</ItemsControl.Items>
</ItemsControl>
</Window>
MainWindow.xaml.cs
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace So21501609WpfMouseRenderTransform
{
public partial class MainWindow
{
private GeneralTransform _transform;
public MainWindow ()
{
InitializeComponent();
Loaded += OnLoaded;
}
private void OnLoaded (object sender, RoutedEventArgs routedEventArgs)
{
var origin = new Point(lstItems.ActualWidth / 2, lstItems.ActualHeight / 2);
var transform = ((TransformGroup)Resources["CanvasTransform"]).Clone();
transform.Children.Insert(0, new TranslateTransform(-origin.X, -origin.Y));
transform.Children.Add(new TranslateTransform(origin.X, origin.Y));
_transform = transform.Inverse;
}
private void LstItems_OnMouseDown (object sender, MouseButtonEventArgs e)
{
Point pos = _transform.Transform(e.GetPosition(lstItems));
var item = new TextBlock { Text = pos.ToString() };
Canvas.SetLeft(item, pos.X);
Canvas.SetTop(item, pos.Y);
lstItems.Items.Add(item);
}
}
}
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;
}
}
}