I've got a Canvas with a few UIElements on. After I've moved them on the canvas by animating the top and left properties, very occasionally a subsiquent call to Canvas.GetTop results in NaN.
Am I not 'closing' the animations properly?
Here's how I'm doing the move
private void InternalMove(double durationMS, FrameworkElement fElement, Point point, EventHandler callback)
{
_moveElement = fElement;
_destination = point;
Duration duration = new Duration(TimeSpan.FromMilliseconds(durationMS));
DoubleAnimation moveLeftAnimation = new DoubleAnimation(Canvas.GetLeft(fElement), point.X, duration, FillBehavior.Stop);
Storyboard.SetTargetProperty(moveLeftAnimation, new PropertyPath("(Canvas.Left)"));
DoubleAnimation moveTopAnimation = new DoubleAnimation(Canvas.GetTop(fElement), point.Y, duration, FillBehavior.Stop);
Storyboard.SetTargetProperty(moveTopAnimation, new PropertyPath("(Canvas.Top)"));
// Create a storyboard to contain the animation.
_moveStoryboard = new Storyboard();
if (callback != null) _moveStoryboard.Completed += callback;
_moveStoryboard.Completed += new EventHandler(s1_Completed);
_moveStoryboard.Children.Add(moveLeftAnimation);
_moveStoryboard.Children.Add(moveTopAnimation);
_moveStoryboard.FillBehavior = FillBehavior.Stop;
_moveStoryboard.Begin(fElement);
}
private void s1_Completed(object sender, EventArgs e)
{
if (_moveStoryboard != null)
{
_moveStoryboard.BeginAnimation(Canvas.LeftProperty, null, HandoffBehavior.Compose);
_moveStoryboard.BeginAnimation(Canvas.TopProperty, null, HandoffBehavior.Compose);
}
Canvas.SetLeft(_moveElement, _destination.X);
Canvas.SetTop(_moveElement, _destination.Y);
}
thanks
It seems than Canvas.GetTop(x) can return 'Nan' if the value is not explictly set (even tho I do explicitly set it I still sometimes get that result).
An alternative method I'm now using is
Vector offset = VisualTreeHelper.GetOffset(fElement);
which returns the position of fElement within it's container.
I've run into a similar situation (NaN), but in a different context. As I recall, it had something to do with how the element was positioned in the container.
Sorry I couldn't provide more help, but maybe this will provide some guidance.
Check back your xaml file in Properties Layout make sure value of left or top show only number but not mix (auto).
Example: Top 123(auto), this caused Canvas.GetTop will return NaN.
Related
I have a very simple project where I need to animate the RenderTransform of an element, and then further manipulate such transform.
Please find as a reference an MVCE here: https://github.com/cghersi/UWPExamples/tree/master/RenderTransformAnimation.
The scenario is the following: there is a ScrollViewer m_scrollView, with a Canvas content m_zoomView.
For the sake of the example we also have a CompositeTransform m_zoomViewTransform = m_zoomView.RenderTransform.
I use the following method to manipulate the RenderTransform, either with or without an animation:
private void SetEffectiveOffsetOfScrollView(Point newOffset, bool isAnimated)
{
if (isAnimated)
{
TimeSpan dur = TimeSpan.FromSeconds(0.2);
Storyboard sb = new Storyboard { Duration = dur };
DoubleAnimation animationX = new DoubleAnimation
{
To = newOffset.X,
Duration = dur,
AutoReverse = false
};
DoubleAnimation animationY = new DoubleAnimation
{
To = newOffset.Y,
Duration = dur,
AutoReverse = false
};
sb.Children.Add(animationX);
sb.Children.Add(animationY);
Storyboard.SetTarget(animationX, m_zoomViewTransform);
Storyboard.SetTarget(animationY, m_zoomViewTransform);
Storyboard.SetTargetProperty(animationX, "CompositeTransform.TranslateX");
Storyboard.SetTargetProperty(animationY, "CompositeTransform.TranslateY");
sb.Begin();
sb.Completed += (sender, o) =>
{
m_zoomViewTransform.TranslateX = newOffset.X;
m_zoomViewTransform.TranslateY = newOffset.Y;
};
}
else
{
m_zoomViewTransform.TranslateX = newOffset.X;
m_zoomViewTransform.TranslateY = newOffset.Y;
}
}
Now, if I use SetEffectiveOffsetOfScrollView() with isAnimated = true, I am not able to change the RenderTransform anymore, or at least I don't see any update to the UI anymore.
In the MVCE I added a button that invokes the SetEffectiveOffsetOfScrollView() method with isAnimated = true, and I added a Manipulation event to pan the m_zoomView Canvas: as soon as I click on the button, I am no more able to pan the Canvas.
How can I animate the transformation, still being able to see the updates to the UI after this action, using SetEffectiveOffsetOfScrollView() with animate=false?
This is due to dependency property value precedence, as described here:
https://learn.microsoft.com/en-us/windows/uwp/xaml-platform/dependency-properties-overview#dependency-property-value-precedence
In this repro, the Storyboard is still active, due to the default FillBehavior=HoldEnd on the DoubleAnimations. Since those animations are still alive, the animated value gets used, even as new local values get set on the isAnimated=false case.
The easy fix is to call sb.Stop() in the Storyboard's Completed handler, after you've set the new local values to hold. This will stop the animations, removing the Animated values they are holding, and allow the Local values to be used.
I have a WPF window containing a Canvas which is populated with rotated Rectangles in code. The rectangles each have a MouseDown event and their positions will be distributed according to coordinates provided by the user. Often two or more will overlap, partially obstructing the rectangle beneath it.
I need the MouseDown event to fire for each rectangle that is under the mouse when it is pressed, even if that rectangle is obstructed by another rectangle, but I am only getting the MouseDown event for the topmost rectangle.
I have tried setting e.Handled for the clicked rectangle, and routing the events through the Canvas with no luck, and even gone as far as trying to locate the objects beneath the mouse based on their coordinates, but the rotation of the rectangles make that difficult to calculate.
public MainWindow()
{
InitializeComponent();
Rectangle r1 = new Rectangle() {Width = 80, Height = 120, Fill = Brushes.Blue };
r1.MouseDown += r_MouseDown;
RotateTransform rt1 = new RotateTransform(60);
r1.RenderTransform = rt1;
Canvas.SetLeft(r1, 150);
Canvas.SetTop(r1, 50);
canvas1.Children.Add(r1);
Rectangle r2 = new Rectangle() { Width = 150, Height = 50, Fill = Brushes.Green };
r2.MouseDown += r_MouseDown;
RotateTransform rt2 = new RotateTransform(15);
r2.RenderTransform = rt2;
Canvas.SetLeft(r2, 100);
Canvas.SetTop(r2, 100);
canvas1.Children.Add(r2);
}
private void r_MouseDown(object sender, MouseButtonEventArgs e)
{
Console.WriteLine("Rectangle Clicked");
}
}
There is another question that is similar to this, but it has no accepted answer and it is quite unclear as to what the final solution should be to resolve this issue. Let's see if we can be a little more clear.
First off, the solution outlined below will use the VisualTreeHelper.HitTest method in order to identify if the mouse has clicked your rectangles. The VisualTreeHelper allows us to find the rectangles even if they have moved around due to things like Canvas.SetTop and various .RenderTransform operations.
Secondly, we are going to be capturing the click event on your canvas element rather than on the individual rectangles. This allows us to handle things at the canvas level and check all the rectangles at once, as it were.
public MainWindow()
{
InitializeComponent();
//Additional rectangle for testing.
Rectangle r3 = new Rectangle() { Width = 175, Height = 80, Fill = Brushes.Goldenrod };
Canvas.SetLeft(r3, 80);
Canvas.SetTop(r3, 80);
canvas1.Children.Add(r3);
Rectangle r1 = new Rectangle() { Width = 80, Height = 120, Fill = Brushes.Blue };
RotateTransform rt1 = new RotateTransform(60);
r1.RenderTransform = rt1;
Canvas.SetLeft(r1, 100);
Canvas.SetTop(r1, 100);
canvas1.Children.Add(r1);
Rectangle r2 = new Rectangle() { Width = 150, Height = 50, Fill = Brushes.Green };
RotateTransform rt2 = new RotateTransform(15);
r2.LayoutTransform = rt2;
Canvas.SetLeft(r2, 100);
Canvas.SetTop(r2, 100);
canvas1.Children.Add(r2);
//Mouse 'click' event.
canvas1.PreviewMouseDown += canvasMouseDown;
}
//list to store the hit test results
private List<HitTestResult> hitResultsList = new List<HitTestResult>();
The HitTest method being used is the more complicated one, because the simplest version of that method only returns "the topmost" item. And by topmost, they mean the first item drawn, so it's actually visually the one on the bottom of the stack of rectangles. In order to get all of the rectangles, we need to use the complicated version of the HitTest method shown below.
private void canvasMouseDown(object sender, MouseButtonEventArgs e)
{
if (canvas1.Children.Count > 0)
{
// Retrieve the coordinates of the mouse position.
Point pt = e.GetPosition((UIElement)sender);
// Clear the contents of the list used for hit test results.
hitResultsList.Clear();
// Set up a callback to receive the hit test result enumeration.
VisualTreeHelper.HitTest(canvas1,
new HitTestFilterCallback(MyHitTestFilter),
new HitTestResultCallback(MyHitTestResult),
new PointHitTestParameters(pt));
// Perform actions on the hit test results list.
if (hitResultsList.Count > 0)
{
string msg = null;
foreach (HitTestResult htr in hitResultsList)
{
Rectangle r = (Rectangle)htr.VisualHit;
msg += r.Fill.ToString() + "\n";
}
//Message displaying the fill colors of all the rectangles
//under the mouse when it was clicked.
MessageBox.Show(msg);
}
}
}
// Filter the hit test values for each object in the enumeration.
private HitTestFilterBehavior MyHitTestFilter(DependencyObject o)
{
// Test for the object value you want to filter.
if (o.GetType() == typeof(Label))
{
// Visual object and descendants are NOT part of hit test results enumeration.
return HitTestFilterBehavior.ContinueSkipSelfAndChildren;
}
else
{
// Visual object is part of hit test results enumeration.
return HitTestFilterBehavior.Continue;
}
}
// Add the hit test result to the list of results.
private HitTestResultBehavior MyHitTestResult(HitTestResult result)
{
//Filter out the canvas object.
if (!result.VisualHit.ToString().Contains("Canvas"))
{
hitResultsList.Add(result);
}
// Set the behavior to return visuals at all z-order levels.
return HitTestResultBehavior.Continue;
}
The test example above just displays a message box showing the fill colors of all rectangles under the mouse pointer when it was clicked; verifying that VisualTreeHelper did in fact retrieve all the rectangles in the stack.
I have a question about animations in UWP. I want to have a menu on the bottom of my app that, when tapped on the top, slides up (shows) or down (hides almost entirely). I was learning WPF before and there I know I can use ThicknessAnimation to move the margin of my control and have it slide. Unfortunately, in UWP I can't use ThicknessAnimations, so I tried to find another way. I want this to work for an arbitrary FrameworkElement (so as to be able to reuse it). Eventually, I came up with this solution:
/// <summary>
/// Adds a vertical slide animation
/// </summary>
/// <param name="storyboard">The storyboard to add the animation to</param>
/// <param name="seconds">The time the animation will take</param>
/// <param name="offset">The distance the element will cover (nagative is up, positive is down)</param>
public static void AddVerticalSlide(this Storyboard storyboard, FrameworkElement element, float seconds, double offset)
{
var slideAnimation = new ObjectAnimationUsingKeyFrames();
for (int i = 0; i <= 100; ++i)
{
double scalar = (double)i / 100;
slideAnimation.KeyFrames.Add(new DiscreteObjectKeyFrame
{
Value = new Thickness(0, scalar*offset, 0, -scalar*offset),
KeyTime = TimeSpan.FromSeconds(scalar*seconds),
});
}
//slideAnimation.Duration = TimeSpan.FromSeconds(seconds);
// Set the target and target property
Storyboard.SetTarget(slideAnimation, element);
Storyboard.SetTargetProperty(slideAnimation, "(FrameworkElement.Margin)");
// Add the animation to the storyboard
storyboard.Children.Add(slideAnimation);
}
It works, looks nice, but here's the reason I'm asking this question: I don't know if it's proper. My guees is that there's a better way to slide objects than manually define 100 points on the way and move the object to each point using this animation.
This works but is just not the ideal way to offset an element. Why? Because you are creating two many DiscreteObjectKeyFrames when you really just need one DoubleAnimation.
You should almost never animate the Margin of an element. To change its position, a better approach is to animate the translate values (i.e. TranslateX/TranslateY) of its transform (RenderTransform) instead.
Animating anything in transform is efficient. They are off the UI thread. Traditionally, they were running in a special thread called Compositor thread (I think), but ever since the Creators Update, they have become even more performant according to the Windows UI team -
When you use Storyboard and Transition animations in XAML, you’re
using Composition under the hood. Animations run at 60 frames per
second!
The following is an example of using such technique
public static void Slide(this UIElement target, Orientation orientation, double? from, double to, int duration = 400, int startTime = 0, EasingFunctionBase easing = null)
{
if (easing == null)
{
easing = new ExponentialEase();
}
var transform = target.RenderTransform as CompositeTransform;
if (transform == null)
{
transform = new CompositeTransform();
target.RenderTransform = transform;
}
target.RenderTransformOrigin = new Point(0.5, 0.5);
var db = new DoubleAnimation
{
To = to,
From = from,
EasingFunction = easing,
Duration = TimeSpan.FromMilliseconds(duration)
};
Storyboard.SetTarget(db, target);
var axis = orientation == Orientation.Horizontal ? "X" : "Y";
Storyboard.SetTargetProperty(db, $"(UIElement.RenderTransform).(CompositeTransform.Translate{axis})");
var sb = new Storyboard
{
BeginTime = TimeSpan.FromMilliseconds(startTime)
};
sb.Children.Add(db);
sb.Begin();
}
Note as performant as this approach gets, there is even more powerful animation support in UWP, thanks to the new Composition API. But offset animation in Composition can be a bit tricky.
However, the UWP Community Tookit has done a great job wrapping some useful animations like Blur, Offset, etc. Feel free to check them out.
I have a control in WPF (which is a custom control containing a circle).
And I need to move it every 60ms.
I have an array of "Position" (Class with 2 attributes : X and Y) and I do this to move it :
timer_tick()
{
myControl.Margin = new Thickness { Left = MyArray[i].X, Top = MyArray[i].Y};
i++;
}
with a global variable.
But can I do it in a better way ? Using something like :
public static void MoveTo(this Image target, double newX, double newY)
{
var top = Canvas.GetTop(target);
var left = Canvas.GetLeft(target);
TranslateTransform trans = new TranslateTransform();
target.RenderTransform = trans;
DoubleAnimation anim1 = new DoubleAnimation(top, newY - top, TimeSpan.FromMilliseconds(60));
DoubleAnimation anim2 = new DoubleAnimation(left, newX - left, TimeSpan.FromMilliseconds(60));
trans.BeginAnimation(TranslateTransform.XProperty,anim1);
trans.BeginAnimation(TranslateTransform.YProperty,anim2);
}
in every tick ?
Thank you
Very close. But there is no need to call this in a timer. Let the animation do the work; that is what it is for. Just set the animations to go from the origin to the destination in a timespan for the desired speed. You can always interrupt the animation at any point if you need it to end early.
Someone else already posted the best way, which is to use an animation, so here's a different way that's less "good" but probably works ok.
If you're using variables like that indexer "i" (careful about calling it global - be specific about its scope) then why not make another one?
At the class level, create a variable...
Thickness awful_marg_variable;
And in your initializer, initialize it and assign the reference to the control's Margin:
awful_marg_variable = new Thickness { Left = MyArray[0].X, Top = MyArray[0].Y};
myControl.Margin = awful_marg_variable;
And then in your timer tick procedure, you can just manipulate that variable...
awful_marg_variable.Left = MyArray[i].X;
awful_marg_variable.Top = MyArray[i].Y;
Finally, you can revel in the horrible thing you've just done, or whatever. Never do this in large quantities. Some people would say never do this ever. But sometimes you just gotta get work done and this is one more way to do it.
The purpose of the code below is that a thumb follows a horizontal mouse movement. The code is called upon a mouse event, so the target value of the animation gets updated continuously.
In the code, offset is the current mouse horizontal position. The problem is, that the animation of the thumb doesn't fully animate to the specified offset, but always seems to be stopping at a value smaller or higher (depending if the mouse is dragged left or right).
The SeekAlignedToLastTick() influences the behavior of the animation, although I couldn't figure out what this function does by reading the documentation.
How can I animate the thumb, so that it follows smoothly the drag event?
private Storyboard _thumbStoryboard;
private DoubleAnimation _thumbAnimation = new DoubleAnimation();;
private CompositeTransform _thumbTransform = new CompositeTransform();
private void UpdateUserInterface(double offset)
{
var thumbItem = Thumb as FrameworkElement;
if (_thumbStoryboard == null)
{
Storyboard.SetTarget(_thumbAnimation, _thumbTransform);
_thumbStoryboard = new Storyboard();
_thumbStoryboard.Children.Add(_thumbAnimation);
thumbItem.RenderTransform = _thumbTransform;
_thumbStoryboard.Duration = new Duration(TimeSpan.FromMilliseconds(100));
_thumbAnimation.EasingFunction = new ExponentialEase();
}
double from = _thumbTransform.TranslateX;
_thumbStoryboard.Stop();
Storyboard.SetTargetProperty(_thumbAnimation, new PropertyPath("TranslateX"));
_thumbAnimation.From = from;
_thumbAnimation.To = offset;
_thumbStoryboard.Begin();
_thumbStoryboard.SeekAlignedToLastTick(TimeSpan.Zero);
}
I've tried to solve your issue, So I've created a Silverlight application and added a Border element for testing.
<Border x:Name="Thumb" VerticalAlignment="Top" HorizontalAlignment="Left" Width="50" height="25" Background="#ff0000" />
There was no need to set the "From" Property, since the DoubleAnimation object could automatically continue from the current Value to the "To" Property.
And you were setting the Duration to the Storyboard, which causes the DoubleAnimation to Cutoff its animation without reaching the "To" Value, You need to set the Duration Property to the DoubleAnimation itself instead.
Also there was no need to call _thumbStoryboard.Stop(), because it will reset the current animation to the first TranslateX Value.
Here is the updated "UpdateUserInterface" function code with comments:
private void UpdateUserInterface(double offset) {
var thumbItem = Thumb as FrameworkElement;
if ( _thumbStoryboard == null ) {
// UpdateLayout Method is update the ActualWidth Properity of the UI Elements
this.UpdateLayout();
// Applying the CompositeTransform on "thumbItem" UI Element
thumbItem.RenderTransform = _thumbTransform;
// Setting the Render Transform Origin to be the Center of X and Y
thumbItem.RenderTransformOrigin = new Point(0.5d, 0.5d);
// Setting the target of the DoubleAnimation to be the Thumb CompositeTransform
Storyboard.SetTarget(_thumbAnimation, _thumbTransform);
// Setting the Targeted Properity of the DoubleAnimation to be The "TranslateX" Properity
Storyboard.SetTargetProperty(_thumbAnimation, new PropertyPath("TranslateX"));
// Used QuinticEase instead of ExponentialEase
// and Added EaseOut to make the animation be more smoother.
_thumbAnimation.EasingFunction = new QuinticEase(){ EasingMode = EasingMode.EaseOut };
// Initializing the Storyboard
_thumbStoryboard = new Storyboard();
// Specifing the Duration of the DoubleAnimation not the StoryBoard
_thumbAnimation.Duration = new Duration(TimeSpan.FromMilliseconds(500));
// Adding the DoubleAnimation to the Children of the Storyboard
_thumbStoryboard.Children.Add(_thumbAnimation);
}
// Calculate the New Centered Position
double newPos = offset - (thumbItem.ActualWidth / 2);
// Set the New DoubleAnimation "To" Value,
// There is no need to set the "From" Value since it'll automatically continue from the current TranslateX Value
_thumbAnimation.To = newPos;
// Begin the animation.
_thumbStoryboard.Begin();
}
Hope that helps you :)
Regards,
Monir Abu Hilal