Monday, March 7, 2011

WPF animation slow when the mouse is in the window

I'm trying create a moving rectangle (rect). This is the relevant code:

let timer = new Threading.DispatcherTimer();
timer.Tick.Add(fun _ -> Canvas.SetLeft(rect, Canvas.GetLeft(rect)+0.01) |> ignore)
timer.Start()

The problem is that the animation is slower if I move the mouse over the window. If I set the timer's interval:

timer.Interval <- TimeSpan.FromMilliSeconds(30.0)

Then the animation doesn't work at all. What am I doing wrong? Thanks.

From stackoverflow
  • I have two suggestions:

    1) if you are using f# interactive check you have installed a WFP event loop. F# interactive sets up a winforms eventloop by default, there is some compatibility between the winforms and WFP event loops, but not everything works to correctly. To install the event loop use the following script:

    #light
    
    // When running inside fsi, this will install a WPF event loop
    #if INTERACTIVE
    
    #I "c:/Program Files/Reference Assemblies/Microsoft/Framework/v3.0";;
    #I "C:/WINDOWS/Microsoft.NET/Framework/v3.0/WPF/";;
    #r "presentationcore.dll";;
    #r "presentationframework.dll";;
    #r "WindowsBase.dll";;
    
    module WPFEventLoop = 
        open System
        open System.Windows
        open System.Windows.Threading
        open Microsoft.FSharp.Compiler.Interactive
        open Microsoft.FSharp.Compiler.Interactive.Settings
        type RunDelegate<'b> = delegate of unit -> 'b 
    
        let Create() = 
            let app  = 
                try 
                    // Ensure the current application exists. This may fail, if it already does.
                    let app = new Application() in 
                    // Create a dummy window to act as the main window for the application.
                    // Because we're in FSI we never want to clean this up.
                    new Window() |> ignore; 
                    app 
                 with :? InvalidOperationException -> Application.Current
            let disp = app.Dispatcher
            let restart = ref false
            { new IEventLoop with
                 member x.Run() =   
                     app.Run() |> ignore
                     !restart
                 member x.Invoke(f) = 
                     try disp.Invoke(DispatcherPriority.Send,new RunDelegate<_>(fun () -> box(f ()))) |> unbox
                     with e -> eprintf "\n\n ERROR: %O\n" e; rethrow()
                 member x.ScheduleRestart() =   ()
                     //restart := true;
                     //app.Shutdown()
            } 
        let Install() = fsi.EventLoop <-  Create()
    
    WPFEventLoop.Install();;
    
    #endif
    

    2) I haven't tested this too well, but I think using an animation story board will give smother more consistent results, rather than using a timer. I think this would look something like (changing the width property haven't worked out how to change the position):

    #light
    
    open System
    open System.Windows
    open System.Windows.Controls
    open System.Windows.Shapes
    open System.Windows.Media
    open System.Windows.Media.Animation
    
    
    let rect = new Rectangle(Fill = new SolidColorBrush(Colors.Red), Height = 20., Width = 20., Name = "myRectangle")
    
    let canvas = 
        let c = new Canvas()
        c.Children.Add(rect) |> ignore
        c
    
    NameScope.SetNameScope(canvas, new NameScope());
    canvas.RegisterName(rect.Name, rect)
    
    let window = new Window(Content = canvas)
    
    let nfloat f = new Nullable<float>(f)
    
    let myDoubleAnimation = new DoubleAnimation(From = nfloat 20.0, To = nfloat 50.0,
                                                Duration = new Duration(TimeSpan.FromSeconds(5.)),
                                                RepeatBehavior = RepeatBehavior.Forever)
    let myStoryboard = 
        let sb = new Storyboard()
        sb.Children.Add(myDoubleAnimation)
        sb
    
    Storyboard.SetTargetName(myDoubleAnimation, rect.Name);
    Storyboard.SetTargetProperty(myDoubleAnimation, new PropertyPath(Rectangle.WidthProperty))
    
    rect.Loaded.Add(fun _ -> myStoryboard.Begin canvas)
    
    let main() =
        let app = new Application()
        app.Run window |> ignore
    
    [<STAThread>]
    do main()
    
    Jules : Thanks for your suggestions. Because you said that it could be a problem with F# interactive I did the same example in C#. The C# code didn't work either. My problem was that the 0.01 movement per tick is far too low. I thought that 1.0 would be the entire window, so 0.01 would be 1% of the window.
    Jules : If I change 0.01 to 1.0 then the animation works! Apparently if you don't set an interval for a timer it runs ticks as fast as possible (so it would still move despite the low 0.01). The WPF event loop is very useful. Until now I reset F# interactive all the time. Thanks for the story board too.
    Jules : Unfortunately I can't use a story board solution because the animation isn't a straight line. I'm trying to create a simulation of planets with gravity to learn more about WPF. Thanks for your help!

0 comments:

Post a Comment