Silverlight animation not smooth - silverlight

When trying to animate objects time/frame based in Silverlight (in contrast to using something like DoubleAnimation or Storyboard, which is not suitable e.g. for fast paced games), for example moving a spaceship in a particular direction every frame, the movement is jumpy and not really smooth. The screen even seems to tear.
There seems to be no difference between CompositionTarget and DistpatcherTimer.
I use the following approach (in pseudocode):
Register Handler to Tick-Event of a DispatcherTimer
In each Tick:
Compute the elapsed time from the last frame in milliseconds
Object.X += movementSpeed * ellapsedMilliseconds
This should result in a smooth movement, right? But it doesn't.
Here is an example (Controls: WASD and Mouse): Silverlight Game.
Although the effect I described is not too prevalent in this sample, I can assure you that even moving a single rectangle over a canvas produces a jumpy animation.
Does someone have an idea how to minimize this. Are there other approaches to to frame based animation exept using Storyboards/DoubleAnimations which could solve this?
Edit: Here a quick and dirty approach, animating a rectangle with minimum code (Controls: A and D) Animation Sample
Xaml:
<Grid x:Name="LayoutRoot" Background="Black">
<Canvas Width="1000" Height="400" Background="Blue">
<Rectangle x:Name="rect" Width="48" Height="48"
Fill="White"
Canvas.Top="200"
Canvas.Left="0"/>
</Canvas>
</Grid>
C#:
private bool isLeft = false;
private bool isRight = false;
private DispatcherTimer timer = new DispatcherTimer();
private double lastUpdate;
public Page()
{
InitializeComponent();
timer.Interval = TimeSpan.FromMilliseconds(1);
timer.Tick += OnTick;
lastUpdate = Environment.TickCount;
timer.Start();
}
private void OnTick(object sender, EventArgs e)
{
double diff = Environment.TickCount - lastUpdate;
double x = Canvas.GetLeft(rect);
if (isRight)
x += 1 * diff;
else if (isLeft)
x -= 1 * diff;
Canvas.SetLeft(rect, x);
lastUpdate = Environment.TickCount;
}
private void UserControl_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.D)
isRight = true;
if (e.Key == Key.A)
isLeft = true;
}
private void UserControl_KeyUp(object sender, KeyEventArgs e)
{
if (e.Key == Key.D)
isRight = false;
if (e.Key == Key.A)
isLeft = false;
}
Thanks!
Andrej

Updating the position every 1 millisecond is overkill because silverlight will simply not update the screen that fast. When you modify the position of an object like that (or any other ui update) silverlight marks the framebuffer as dirty and then it will eventually redraw the screen, however it will not redraw the screen more often than the MaxFrameRate.
To set the MaxFrameRate just:
Application.Current.Host.Settings.MaxFrameRate = 60;
This will limit your application to 60 frames per second which should be more than enough.

Don't use a DispatchTimer for the animation. Reasons for this. Use a storyboard, if you can, or the CompositionTarget.Rendering event.

Related

Spurious and Missing mouse events in WPF app

I have a WPF app with two canvases which overlay each other . . .
<Canvas Name="GeometryCnv" Canvas.Top="0" Canvas.Left="0" Margin="10,21,315,251" />
<Canvas Name="ROIcnv" Background ="Transparent" Canvas.Top="0" Canvas.Left="0" Margin="10,21,315,251" MouseDown="ROIcnvMouseDown" MouseUp="ROIcnvMouseUp" MouseMove="ROIcnvMouseMove"/>
I draw some geometry on the first canvas and I draw a rectangle to denote a Region on Interest (ROI) on the second one, using the Mouse-down event to start the drawing, Mouse-move events while drawing (resizing or positioning) the rectangle, and the Mouse-up event to end the drawing.
Except that it's not handling the events reliably. It gets the initial Mouse-down event to start it. It gets Mouse-move events continuously - regardless of whether the mouse is moving - and it does not get the Mouse-up event at all, nor does it get any subsequent mouse down events, say if I double-click the mouse.
The event-handler code looks like this . . .
private void ROIcnvMouseDown(object sender, MouseButtonEventArgs e)
{
MouseLineBegin = Mouse.GetPosition(ROIcnv);
bMouseDown = true;
}
private void ROIcnvMouseUp(object sender, MouseButtonEventArgs e)
{
MouseLineEnd = Mouse.GetPosition(ROIcnv);
bMouseDown = false;
}
private void ROIcnvMouseMove(object sender, MouseEventArgs e)
{
iMM++; // counting mouse move events
ROIcnv.Children.Clear(); // clear the ROI canvas
if (bMouseDown) // if we're drawing now
{
MouseLineEnd = Mouse.GetPosition(ROIcnv);
// get the upper left and lower right = coords from the beginning and end points . . .
int ulx = 0;
int uly = 0;
int lrx = 0;
int lry = 0;
if (MouseLineEnd.X >= MouseLineBegin.X)
{
ulx = (int) MouseLineBegin.X;
lrx = (int) MouseLineEnd.X;
}
else
{
lrx = (int)MouseLineBegin.X;
ulx = (int)MouseLineEnd.X;
}
if (MouseLineEnd.Y >= MouseLineBegin.Y)
{
uly = (int)MouseLineBegin.Y;
lry = (int)MouseLineEnd.Y;
}
else
{
lry = (int)MouseLineBegin.Y;
uly = (int)MouseLineEnd.Y;
}
int h = Math.Abs(lry-uly);
int w = Math.Abs(lrx-ulx);
var rect = new Path
{
Data = new RectangleGeometry(new Rect(ulx, uly, w, h)),
Stroke = Brushes.Black,
StrokeThickness = 2
};
ROIcnv.Children.Add(rect);
}
}
... I've tried suspending the mouse in mid-air and resting it on towels to eliminate any vibrations that might cause spurious move events with no benefit, any anyway that wouldn't account for not getting subsequent up and down events.
Note: I tried this on another computer with exactly the same results.
You'll have much better responses if you provide a minimal, working example of your problem (specifically both your ROIcnvMouseDown and ROIcnvMouseUp methods are missing as are all of your property declarations). The problem is possibly due to your newly-created Path object interfering with the mouse messages, if so then it can be fixed by setting it's IsHitTestVisible property to false. Need a minimal example to determine this for sure though.
UPDATE: Sorry, my bad, I must have stuffed up the cut-n-paste into my test app. Try capturing the mouse in response to the mouse down event:
private void ROIcnvMouseDown(object sender, MouseButtonEventArgs e)
{
MouseLineBegin = Mouse.GetPosition(ROIcnv);
bMouseDown = true;
Mouse.Capture(sender as IInputElement);
}
And of course you need to release it in response to MouseUp:
private void ROIcnvMouseUp(object sender, MouseButtonEventArgs e)
{
MouseLineEnd = Mouse.GetPosition(ROIcnv);
bMouseDown = false;
Mouse.Capture(sender as IInputElement, CaptureMode.None);
ROIcnv.Children.Clear();
}
The other thing I've done is call ROIcnv.Children.Clear(); in response to MouseUp as I assume you no longer want the selection rectangle to be visible. On my machine this doesn't result in any spurious mouse move events.
Does that answer the question?

Move a WPF Shape

I cannot figure out how to move a WPF shape at runtime. Specifically, I want to move a ellipse.
Here is my current code:
private void Tick(object sender, EventArgs e)
{
Point ballLocation = ball.TransformToAncestor(Application.Current.MainWindow).Transform(new Point(0, 0));
//MessageBox.Show(ballLocation.ToString());
Canvas.SetLeft(ball, ballLocation.X + 5);
InvalidateVisual();
}
Every time the timer ticks (1 second) the ball should move 5 pixels in the x direction, correct? If this is wrong, how do I get the current location of the Ellipse and how do I set it to a new location. Maybe there is a problem with the InvalidateVisual? I believe that basically repaints the control. If that is wrong, how do I repaint the ellipse to show its change in location. I am also tried ball.InvalidateVisual(), it did not work.
This is how I create and start the timer:
var timer = new DispatcherTimer {IsEnabled = true};
timer.Tick += Tick;
timer.Interval = new TimeSpan(0, 0, 1);
timer.Start();
If Canvas.Left is initialized with some value, either in XAML
<Ellipse Name="ball" Canvas.Left="0" ... />
or in code
Canvas.SetLeft(ball, 0);
the following works:
void Tick(object sender, EventArgs e)
{
Canvas.SetLeft(ball, Canvas.GetLeft(ball) + 5);
}
The initialization is necessary because the default value is Double.NaN.
You may however consider to animate the Canvas.Left property to get a smooth movement. Start reading here: Animation Overview

WPF DragEnter between two Canvas not firing

I am trying to drag an item from one Canvas to another. I want an event to fire when the object enters the other Canvas. None of the Drag events seem to fire.
I have tried following the solution for this question, but it does not work for me:
Drag and Drop not responding as expected
My canvas is this:
<Window x:Class="DragEnterTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="DragEnterMainWindow" Height="460" Width="1000">
<Grid>
<Canvas Name="Toolbox" Background="Beige" Height="400" Width="200" Margin="12,12,800,35">
<Rectangle Name="dragRectangle" Canvas.Left="0" Canvas.Right="0" Width="50" Height="50" Fill="Red"
MouseLeftButtonDown="dragRectangle_MouseLeftButtonDown"
MouseLeftButtonUp="dragRectangle_MouseLeftButtonUp"
MouseMove="dragRectangle_MouseMove"
/>
</Canvas>
<Canvas Background="Azure" Height="400" Margin="218,12,0,35" Name="mainCanvas" Panel.ZIndex="-1"
DragEnter="mainCanvas_DragEnter"
DragLeave="mainCanvas_DragLeave"
PreviewDragEnter="mainCanvas_PreviewDragEnter"
PreviewDragLeave="mainCanvas_PreviewDragLeave"
AllowDrop="True"
DragDrop.Drop="mainCanvas_Drop"
/>
</Grid>
</Window>
If I do not have the Panel.ZIndex="-1" then the rectangle is dragged underneath the mainCanvas. This is true even if I set the ZIndex for the rectangle to some positive value.
My code is the following, modified by examples I have found:
namespace DragEnterTest
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private bool _isRectDragInProg;
public MainWindow()
{
InitializeComponent();
}
private void dragRectangle_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
_isRectDragInProg = true;
dragRectangle.CaptureMouse();
}
private void dragRectangle_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
_isRectDragInProg = false;
dragRectangle.ReleaseMouseCapture();
}
private void dragRectangle_MouseMove(object sender, MouseEventArgs e)
{
if (!_isRectDragInProg) return;
// get the position of the mouse relative to the Canvas
var mousePos = e.GetPosition(Toolbox);
// center the rect on the mouse
double left = mousePos.X - (dragRectangle.ActualWidth / 2);
double top = mousePos.Y - (dragRectangle.ActualHeight / 2);
Canvas.SetLeft(dragRectangle, left);
Canvas.SetTop(dragRectangle, top);
}
private void mainCanvas_DragEnter(object sender, DragEventArgs e)
{
string t = "Test"; // Never enters this event
}
private void mainCanvas_DragLeave(object sender, DragEventArgs e)
{
string t = "Test"; // Never enters this event
}
private void mainCanvas_PreviewDragEnter(object sender, DragEventArgs e)
{
string t = "Test"; // Never enters this event
}
private void mainCanvas_PreviewDragLeave(object sender, DragEventArgs e)
{
string t = "Test"; // Never enters this event
}
private void mainCanvas_Drop(object sender, DragEventArgs e)
{
string t = "Test"; // Never enters this event
}
}
}
You're not dragging anything actually, just moving rectangle here and there in canvas.
You'll need to call DragDrop.DoDragDrop function when your rectangle leave the source, also detach it from source so that you can add it to target later.
// Drag - In mousemove event when mouse has gone out of toolbox
DragDrop.DoDragDrop(
Toolbox,
new DataObject("MyWPFObject", rectangle),
DragDropEffects.Move
);
// Drop - In Drop event of target
if (e.Data.GetDataPresent("MyWPFObject"))
{
var rectangle = e.Data.GetData("MyWPFObject") as Rectangle
....
Tutorial ...
I believe it's because you're capturing the mouse, so all mouse events are being handled by your dragged Rectangle, and they don't get passed on to your Canvas
I personally had all kinds of problems with using WPF's built-in drag/drop functionality, so I ended up using MouseEvents instead.
The solution I used was from this answer, and went like this:
On MouseDown with left button down, record the position (on MouseLeave erase the position)
On MouseMove, if left button down, position is recorded, and current mouse position differs by more than delta, set a flag saying drag operation is in progress & have your application (not the dragged object) capture the mouse
On MouseMove with drag operation in progress, use hit testing to determine where your rectangle should be (ignoring the rectangle itself) and adjust its parenting and position accordingly.
On MouseUp with drag operation in progress, release the mouse capture and clear the "drag operation is in progress" flag

Manipulating SurfaceScrollViewer content

I'm diving into WPF here and I can't figure some things with multitouch.
I've got two questions about SurfaceScrollViewer.
Easier one first: I've got a large photo I'm displaying with SurfaceScrollViewer, so I can pan around, but I can't figure out how to get the content to start out centered in the screen. I can't find any native alignment properties in SScrollViewer. If I give the content margins, it crops it. Same if I do a RenderTransform. If I do a LayoutTransform, it doesn't seem to do change. Any ideas?
I also want to give this content Zoom functionality while inside SurfaceScrollViewer. Really I'm trying to zoom and pan with the elastic effects of SSV. Should I write the manipulations out by hand or can I patch the functions in SSV to be able to zoom? It seems like SSV absorbs 2nd touches into its panning function. I'd have to write a Manipulation handler to send multi touches to the content, right?
My code looks something like this right now:
<Grid x:Name="LayoutGrid" Width="1950" Height="1118" HorizontalAlignment="Center" >
<s:SurfaceScrollViewer x:Name="scrollViewer" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden" >
<local:FloorView x:Name="floorViewer" Width="4209" Height="1442" >
<local:FloorView.LayoutTransform>
<TranslateTransform X="1000" />
</local:FloorView.LayoutTransform>
</local:FloorView>
</s:SurfaceScrollViewer>
</Grid>
Any help is much appreciated. Thanks!
figured out the first part:
scrollViewer.ScrollToHorizontalOffset(x);
scrollViewer.ScrollToVerticalOffset(y);
looks like i'll have to control manipulation events on the SSV to add zoom.
figured out the second part to zoom inside scrollviewer
handle touchdown events on the scrollviewer
send one touches to surfacescrollviewer and
send two touches to the content
enable manipulations on content
handle manipulations with the scrollviewer as the container
then use the delta manipulations to add a ScaleTransform to the content
don't forget to handle touchup events
private void floorViewer_TouchDown(object sender, TouchEventArgs e) //catch touch events on floorviewer
{
Touch1ID = e.TouchDevice.Id - 16777216; ;
if (Touch1ID == 0) //if one touch present, TouchDevice.Id is 2^24, two then 2^24+1 (this might just be my machine)
{
floorViewer.IsManipulationEnabled = false;
floorViewer.ReleaseTouchCapture(e.TouchDevice);
scrollViewer.CaptureTouch(e.TouchDevice);
}
else {
floorViewer.IsManipulationEnabled = true;
foreach(TouchDevice device in scrollViewer.TouchesOver){
scrollViewer.ReleaseTouchCapture(device);
floorViewer.CaptureTouch(device);
}
}
StartTimeout();
e.Handled = true;
}
void scrollViewer_TouchUp(object sender,TouchEventArgs e)
{
clearID();
e.Handled = true;
}
private void clearID()
{
Touch1ID = 0;
}
private void floorview_TouchUp(object sender, TouchEventArgs e)
{
clearID();
e.Handled = true;
}
//manipulators on floorviewer when it gets touches passed to it
private void scrollViewer_ManipulationStarting(object sender, ManipulationStartingEventArgs e)
{
e.ManipulationContainer = scrollViewer;
e.Handled = true;
}
private void scrollViewer_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
{
double oldScale = flrScale;
flrScale *= e.DeltaManipulation.Scale.X;
if (flrScale < .95 | flrScale > 2) flrScale = oldScale;
floorViewer.RenderTransform = new ScaleTransform(flrScale, flrScale, e.ManipulationOrigin.X + flrInitX, e.ManipulationOrigin.Y + flrInitY);
e.Handled = true;
}
boom!

How can I make WPF ScrollViewer middle-click-scroll?

Clicking the middle mouse button (aka: mouse wheel) and then moving the mouse down slightly lets users scroll in IE, and most Windows apps. This behavior appears to be missing in WPF controls by default? Is there a setting, a workaround, or something obvious that I'm missing?
I have found how to achieve this using 3 mouse events (MouseDown, MouseUp, MouseMove). Their handlers are attached to the ScrollViewer element in the xaml below:
<Grid>
<ScrollViewer MouseDown="ScrollViewer_MouseDown" MouseUp="ScrollViewer_MouseUp" MouseMove="ScrollViewer_MouseMove">
<StackPanel x:Name="dynamicLongStackPanel">
</StackPanel>
</ScrollViewer>
<Canvas x:Name="topLayer" IsHitTestVisible="False" />
</Grid>
It would be better to write a behaviour instead of events in code-behind, but not everyone has the necessary library, and also I don't know how to connect it with the Canvas.
The event handlers:
private bool isMoving = false; //False - ignore mouse movements and don't scroll
private bool isDeferredMovingStarted = false; //True - Mouse down -> Mouse up without moving -> Move; False - Mouse down -> Move
private Point? startPosition = null;
private double slowdown = 200; //The number 200 is found from experiments, it should be corrected
private void ScrollViewer_MouseDown(object sender, MouseButtonEventArgs e)
{
if (this.isMoving == true) //Moving with a released wheel and pressing a button
this.CancelScrolling();
else if (e.ChangedButton == MouseButton.Middle && e.ButtonState == MouseButtonState.Pressed)
{
if (this.isMoving == false) //Pressing a wheel the first time
{
this.isMoving = true;
this.startPosition = e.GetPosition(sender as IInputElement);
this.isDeferredMovingStarted = true; //the default value is true until the opposite value is set
this.AddScrollSign(e.GetPosition(this.topLayer).X, e.GetPosition(this.topLayer).Y);
}
}
}
private void ScrollViewer_MouseUp(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Middle && e.ButtonState == MouseButtonState.Released && this.isDeferredMovingStarted != true)
this.CancelScrolling();
}
private void CancelScrolling()
{
this.isMoving = false;
this.startPosition = null;
this.isDeferredMovingStarted = false;
this.RemoveScrollSign();
}
private void ScrollViewer_MouseMove(object sender, MouseEventArgs e)
{
var sv = sender as ScrollViewer;
if (this.isMoving && sv != null)
{
this.isDeferredMovingStarted = false; //standard scrolling (Mouse down -> Move)
var currentPosition = e.GetPosition(sv);
var offset = currentPosition - startPosition.Value;
offset.Y /= slowdown;
offset.X /= slowdown;
//if(Math.Abs(offset.Y) > 25.0/slowdown) //Some kind of a dead space, uncomment if it is neccessary
sv.ScrollToVerticalOffset(sv.VerticalOffset + offset.Y);
sv.ScrollToHorizontalOffset(sv.HorizontalOffset + offset.X);
}
}
If to remove the method calls AddScrollSign and RemoveScrollSign this example will work. But I have extended it with 2 methods which set scroll icon:
private void AddScrollSign(double x, double y)
{
int size = 50;
var img = new BitmapImage(new Uri(#"d:\middle_button_scroll.png"));
var adorner = new Image() { Source = img, Width = size, Height = size };
//var adorner = new Ellipse { Stroke = Brushes.Red, StrokeThickness = 2.0, Width = 20, Height = 20 };
this.topLayer.Children.Add(adorner);
Canvas.SetLeft(adorner, x - size / 2);
Canvas.SetTop(adorner, y - size / 2);
}
private void RemoveScrollSign()
{
this.topLayer.Children.Clear();
}
Example of icons:
And one last remark: there are some problems with the way Press -> Immediately Release -> Move. It is supposed to cancel scrolling if a user clicks the mouse left button, or any key of keyboard, or the application looses focus. There are many events and I don't have time to handle them all.
But standard way Press -> Move -> Release works without problems.
vorrtex posted a nice solution, please upvote him!
I do have some suggestions for his solution though, that are too lengthy to fit them all in comments, that's why I post a separate answer and direct it to him!
You mention problems with Press->Release->Move. You should use MouseCapturing to get the MouseEvents even when the Mouse is not over the ScrollViewer anymore. I have not tested it, but I guess your solution also fails in Press->Move->Move outside of ScrollViewer->Release, Mousecapturing will take care of that too.
Also you mention using a Behavior. I'd rather suggest an attached behavior that doesn't need extra dependencies.
You should definately not use an extra Canvas but do this in an Adorner.
The ScrollViewer itsself hosts a ScrollContentPresenter that defines an AdornerLayer. You should insert the Adorner there. This removes the need for any further dependency and also keeps the attached behavior as simple as IsMiddleScrollable="true".

Resources