I draw chart with DynamicDataDisplay.ChartPlotter and each time I redraw the chart memory increase. So when I have redraw 200 times my chart memory of my app increase to 200mo in memory.
I tried to GC.Collect() manually but nothing change.
private ChartPlotter _courbeChartPlotter;
public ChartPlotter CourbeChartPlotter
{
get { return _courbeChartPlotter; }
set
{
_courbeChartPlotter = value;
RaisePropertyChanged();
}
}
private static ListPointD3Cs _listeX = new ListPointD3Cs { new Point(0, 0) };
private static EnumerableDataSource<Point> dsX = new EnumerableDataSource<Point>(_listeX);
private int nbPoint = 0;
public void TakeMeasureTest()
{
nbPoint++;
_listeX.Add(new Point(nbPoint, GetRandomNumber(-1, 1)));
InitDotsCourbe(out dsX);
TraceCourbe(dsX);
}
public void InitDotsCourbe(out EnumerableDataSource<Point> dsX)
{
dsX = new EnumerableDataSource<Point>(_listeX);
dsX.SetXMapping(x => x.X);
dsX.SetYMapping(y => y.Y);
}
public ChartPlotter TraceCourbe(EnumerableDataSource<Point> dsX)
{
CourbeChartPlotter = new ChartPlotter();
ViewPortAxesRangeRestriction restr = new ViewPortAxesRangeRestriction
{
XRange = new DisplayRange(0, nbPoint + 1),
YRange = new DisplayRange(-2, 2)
};
CourbeChartPlotter.Viewport.Restrictions.Add(restr);
CourbeChartPlotter.AddLineGraph(dsX, Colors.Red, 2, "X");
return CourbeChartPlotter;
}
I want memory not increase each time I redraw the chart or at least when I GC.Collect() after all Measure retrieve a normal memory usage.
I'm assuming you're redrawing the chart by repeatedly calling TraceCourbe method, and reassigning CourbeChartPlotter property, then displaying that. In that case, you probably have a memory leak because something (for example another property, control or an event subscriber) is holding a reference to your ChartPlotter control even after it is reassigned and theoretically removed (your case may be similar to this one).
You can reassign the chart's data source instead, as is described here.
Related
I'd like to display coverarts for each album of an MP3 library, a bit like Itunes does (at a later stage, i'd like to click one any of these coverarts to display the list of songs).
I have a form with a panel panel1 and here is the loop i'm using :
int i = 0;
int perCol = 4;
int disBetWeen = 15;
int width = 250;
int height = 250;
foreach(var alb in mp2)
{
myPicBox.Add(new PictureBox());
myPicBox[i].SizeMode = System.Windows.Forms.PictureBoxSizeMode.StretchImage;
myPicBox[i].Location = new System.Drawing.Point(disBetWeen + (disBetWeen * (i % perCol) +(width * (i % perCol))),
disBetWeen + (disBetWeen * (i / perCol))+ (height * (i / perCol)));
myPicBox[i].Name = "pictureBox" + i;
myPicBox[i].Size = new System.Drawing.Size(width, height);
myPicBox[i].ImageLocation = #"C:/Users/Utilisateur/Music/label.jpg";
panel1.Controls.Add(myPicBox[i]);
i++;
}
I'm using the same picture per picturebox for convenience, but i'll use the coverart embedded in each mp3 file eventually.
It's working fine with an abstract of the library (around 50), but i have several thousands of albums. I tried and as expected, it takes a long time to load and i cannot really scroll afterward.
Is there any way to load only what's displayed ? and then how to assess what is displayed with the scrollbars.
Thanks
Winforms really isn't suited to this sort of thing... Using standard controls, you'd probably need to either provision all the image boxes up front and load images in as they become visible, or manage some overflow placeholder for the appropriate length so the scrollbars work.
Assuming Winforms is your only option, I'd suggest you look into creating a custom control with a scroll bar and manually driving the OnPaint event.
That would allow you to keep a cache of images in memory to draw the current view [and a few either side], while giving you total control over when they're loaded/unloaded [well, as "total" as you can get in a managed language - you may still need tune garbage collection]
To get into some details....
Create a new control
namespace SO61574511 {
// Let's inherit from Panel so we can take advantage of scrolling for free
public class ImageScroller : Panel {
// Some numbers to allow us to calculate layout
private const int BitmapWidth = 100;
private const int BitmapSpacing = 10;
// imageCache will keep the images in memory. Ideally we should unload images we're not using, but that's a problem for the reader
private Bitmap[] imageCache;
public ImageScroller() {
//How many images to put in the cache? If you don't know up-front, use a list instead of an array
imageCache = new Bitmap[100];
//Take advantage of Winforms scrolling
this.AutoScroll = true;
this.AutoScrollMinSize = new Size((BitmapWidth + BitmapSpacing) * imageCache.Length, this.Height);
}
protected override void OnPaint(PaintEventArgs e) {
// Let Winforms paint its bits (like the scroll bar)
base.OnPaint(e);
// Translate whatever _we_ paint by the position of the scrollbar
e.Graphics.TranslateTransform(this.AutoScrollPosition.X,
this.AutoScrollPosition.Y);
// Use this to decide which images are out of sight and can be unloaded
var current_scroll_position = this.HorizontalScroll.Value;
// Loop through the images you want to show (probably not all of them, just those close to the view area)
for (int i = 0; i < imageCache.Length; i++) {
e.Graphics.DrawImage(GetImage(i), new PointF(i * (BitmapSpacing + BitmapWidth), 0));
}
}
//You won't need a random, just for my demo colours below
private Random rnd = new Random();
private Bitmap GetImage(int id) {
// This method is responsible for getting an image.
// If it's already in the cache, use it, otherwise load it
if (imageCache[id] == null) {
//Do something here to load an image into the cache
imageCache[id] = new Bitmap(100, 100);
// For demo purposes, I'll flood fill a random colour
using (var gfx = Graphics.FromImage(imageCache[id])) {
gfx.Clear(Color.FromArgb(255, rnd.Next(0, 255), rnd.Next(0, 255), rnd.Next(0, 255)));
}
}
return imageCache[id];
}
}
}
And Load it into your form, docking to fill the screen....
public Form1() {
InitializeComponent();
this.Controls.Add(new ImageScroller {
Dock = DockStyle.Fill
});
}
You can see it in action here: https://www.youtube.com/watch?v=ftr3v6pLnqA (excuse the mouse trails, I captured area outside the window)
I'm using SFML for C#. I want to create a BackgroundImage Sprite and then start drawing it with an Agent, represented as a Circle, on top of it like that:
static void Main(string[] args)
{
Window = new RenderWindow(new VideoMode((uint)map.Size.X * 30, (uint)map.Size.Y * 30), map.Name + " - MAZE", Styles.Default);
while (Window.IsOpen)
{
Update();
}
}
static public RenderWindow Window { get; private set; }
static Map map = new Map(string.Format(#"C:\Users\{0}\Desktop\Maze.png", Environment.UserName));
static public void Update()
{
Window.Clear(Color.Blue);
DrawBackground();
DrawAgent();
Window.Display();
}
static void DrawAgent()
{
using (CircleShape tempCircle = new CircleShape
{
FillColor = Color.Cyan,
Radius = 15,
Position = new Vector2f(30, 30),
Origin = new Vector2f(30, 30),
Scale = new Vector2f(.5f, .5f)
})
{
Window.Draw(tempCircle);
}
}
static private Sprite BackgroundImage { get; set; }
static void DrawBackground()
{
if (BackgroundImage == null)
BackgroundImage = GetBackground();
Window.Draw(BackgroundImage);
}
static Sprite GetBackground()
{
RenderTexture render = new RenderTexture((uint)map.Size.X * 30, (uint)map.Size.Y * 30);
foreach (var point in map.Grid.Points)
{
RectangleShape pointShape = new RectangleShape(new Vector2f(30, 30));
switch (point.PointType)
{
case PointType.Walkable:
pointShape.FillColor = Color.White;
break;
case PointType.NotWalkable:
pointShape.FillColor = Color.Black;
break;
case PointType.Start:
pointShape.FillColor = Color.Red;
break;
case PointType.Exit:
pointShape.FillColor = Color.Blue;
break;
}
pointShape.Position = new Vector2f(point.Position.X * 30, point.Position.Y * 30);
render.Draw(pointShape);
}
Sprite result = new Sprite(render.Texture);
result.Origin = new Vector2f(0, result.GetLocalBounds().Height);
result.Scale = new Vector2f(1, -1);
return result;
}
Everything works as intended when I start it, but after a few seconds, around the time when process memory reaches 70MB, BackgroundImage turns into completely white sprite. If I change the type of BackgroundImage and GetBackground() to RenderTexture, return "render" object and then change DrawBackground() function like this
void RenderBackground()
{
if (BackgroundImage == null)
BackgroundImage = GetBackground();
using (Sprite result = new Sprite(BackgroundImage.Texture))
{
result.Origin = new Vector2f(0, result.GetLocalBounds().Height);
result.Scale = new Vector2f(1, -1);
Window.Draw(result);
}
}
then the background sprite doesn't turn white, but storing entire RenderTexture, instead of Sprite and then constantly creating new Sprite objects every time we call RenderBackground() function seems like a bad idea.
Is there any way for GetBackground() function to return a Sprite which won't turn white once the function's local "render" variable is destroyed?
You're not completely off with your assumptions. Simplified, SFML knows two types of resources:
Light resources are small objects that are quick to create and destroy. It's not that bad to just drop them and recreate them later. Typical examples would be Sprite, Sound, Text, and basically most SFML classes.
Heavy resourcces are often big objects or objects requiring file access to create or use. Typical examples would be Image, Texture, SoundBuffer, and Font. You shouldn't recreate these and instead keep them alive while you need them. If they're disposed too early, light resources using them will fail in some way or another.
A sprite's texture turning white is – as you've discovered – a typical sign of the assigned texture being freed/disposed.
There are many different approaches to this, but I'd suggest you create some kind of simple resource manager that will load resources just in time or just return them, if they're loaded already.
I haven't used SFML with C# and I haven't really touched C# for quite a while, but for a simple implementation you'd just have a Dictionary<string, Texture>. When you want to load a texture file like texture.png, you look whether there's a dictionary entry with that key name. If there is, just return it. If there isn't, create the new entry and load the texture, then return it.
I'm out of practice, so please consider this pseudo code!
private Dictionary<string, Texture> mTextureCache; // initialized in constructor
public Texture getTexture(file) {
Texture tex;
if (mTextureCache.TryGetValue(file, out tex))
return tex;
tex = new Texture(file);
mTextureCache.add(file, tex);
return tex;
}
// Somewhere else in your code:
Sprite character = new Sprite(getTexture("myCharacter.png"));
If your heavy resource is a RenderTexture, you just have to ensure that it stays alive as long as it's used (e.g. as a separate member).
It turned out the answer was simpler that I expected. All I had to to was create new Texture object and then make a Sprite out of it. So instead of
Sprite result = new Sprite(render.Texture);
I wrote
Sprite result = new Sprite(new Texture(render.Texture));
Now garbage collector doesn't dispose Sprite's texture
I am working with a WPF control derived from the Canvas that I am drawing a selection of geometry to, through a process that works along these lines, but is far more complex so although this illustrates the process, it is closer to pseudo-code:
public class LineView: Canvas
{
private List<GeometryLine> lines;
public LineView()
{
lines = new List<GeometryLine>();
}
private PathFigure GetPathFigure(List<Point> line, bool close)
{
var size = line.Count;
var points = new PathSegmentCollection(size);
var first = line.First();
for (int i = 1; i < size; i++)
{
points.Add(new LineSegment(line[i], true));
}
var figure = new PathFigure(first, points, close);
return new PathGeometry(new List<PathFigure>(figure));
}
public DrawingGroup DrawLine(GeometryLine line)
{
var geometry = new GeometryGroup();
geometry.Children.Add(GetPathGeometry(line.Points, false));
var d = new GeometryDrawing();
d.Pen = GetPen();
d.Geometry = geometry;
DrawingGroup group = new DrawingGroup();
group.Append();
group.Children.Add(d);
return group;
}
public override void OnRender(DrawingContext drw)
{
if ( 0 < lines.Count )
{
foreach ( var line in lines )
{
drw.DrawDrawing(DrawLine(line));
}
}
}
public void SetLines(List<GeometryLine> newLines)
{
lines = newLines;
this.InvalidateVisual();
}
}
This all works fine to render geometry content on the canvas. However, when the data changes and SetLines is called with a new set of data, it doesn't always clear the canvas - sometimes it will draw the new set of lines over the old set, other times it will clear the canvas. I can't see any pattern as to when it draws over as opposed to when it clears the canvas.
If I call InvalidateVisual from the Render method it will empty the canvas reliably, but it also forces the context to render again. Past questions on this topic indicate that either this.Children.Clear() or InvalidateVisual are the recommended strategies, but neither of them prevents this overdrawing problem. When I look at the Children collection it is always empty.
What do I need to do in order to ensure it will clear the previous geometry and then draw the updated geometry every time it changes?
I've finished a game of life implementation but I'm running into a issue when rendering the grid after applying the game rules. I have a game loop that looks like this:
while (gameIsRunning)
{
//Needed for accessing UIControls from the background
//thread.
if (InvokeRequired)
{
//Process the array.
MainBoard.Cells = engine.ApplyGameRules(MainBoard.Cells, MainBoard.Size.Height, MainBoard.Size.Width, BOARD_DIMENSIONS);
//Check if there is a state such as
//all states being dead, or all states being
//alive.
//Update the grid with the updated cells.
this.Invoke(new MethodInvoker(delegate
{
timeCounter++;
lblTimeState.Text = timeCounter.ToString();
pictureBox1.Invalidate();
pictureBox1.Update();
Thread.Sleep(100);
}));
}
}
and a draw function that looks like this:
for (int x = 0; x < MainBoard.Size.Height; x++)
{
for (int y = 0; y < MainBoard.Size.Width; y++)
{
Cell individualCell = MainBoard.Cells[y, x];
if (individualCell.IsAlive() == false)
{
e.Graphics.FillRectangle(Brushes.Red, MainBoard.Cells[y, x].Bounds);
}
//White indicates that cells are alive
else if (individualCell.IsAlive() == true)
{
e.Graphics.FillRectangle(Brushes.White, MainBoard.Cells[y, x].Bounds);
}
else if (individualCell.IsInfected() == true)
{
e.Graphics.FillRectangle(Brushes.Green, MainBoard.Cells[y, x].Bounds);
}
//Draws the grid background itself.
e.Graphics.DrawRectangle(Pens.Black, MainBoard.Cells[y, x].Bounds);
}
}
The problem that I'm running into is that I'm applying all of the game rules to every cell in the grid and then drawing that grid and then applying all the rules again so I never get the life form blobs that I should be seeing. Should the game rules be applied on a cell by cell basis so that its something along the lines of: Apply game rule to cell, draw grid, apply game rule to another cell, draw grid...?
It looks like the current intent of the program is correct.
What you should be doing is (pseudocode):
Board oldBoard = new Board(start cell definitions);
while(not finished) {
Board newBoard = calculate(oldBoard);
display(newBoard);
oldBoard = newBoard();
}
If you're not seeing the forms you expect, then either your display code is wrong, or your rule code is wrong.
In the pseudocode I'm throwing away the previous generation's board once it's no longer needed, and making a new board for each generation. calculate() contains a new Board() statement.
Of course if it's expensive to make a new board you could re-use one instead, and just flip back and forth between a "current" and "other" board. Just bear in mind that each time you write to a board, its new state must be 100% a function of the previous generation's state, and in no way affected by its own starting state. i.e. you must write to every cell.
An alternative method would be for each cell to hold two values. So instead of two boards with one value per cell, you have one board with each cell containing a "current" and "previous" value.
Board board = new Board(initial state);
while(not finished) {
board.calculate(); // fills "current" cells based on "previous" cells.
display(board);
board.tick(); // "current" becomes "previous".
// "previous" becomes current, but is "dirty" until calculated.
}
There are lots of ways you could do it. One way is:
public class Cell {
private boolean[] state = new boolean[2];
private int generation = 0;
public void setCurrentState(boolean state) {
state[generation] = state;
}
public void getCurrentState() {
return state[generation];
}
public void getLastState() {
return state[ (generation + 1) % 2 ];
}
public void tick() {
generation = (generation + 1) % 2;
}
}
Good afternoon,
over the last few weeks I have been working on a project to create an advanced metronome. the metronome is made up of the following things
a swinging arm
a light flash
a collection of dynamically created user controls that represent beats (4 of them that are either on, accented or off).
a usercontrol that displays an LCD numeric display and calculates the number of milliseconds between beats for the selected BPM (60000/BPM=milliseconds)
the user selects a BPM and presses start and the following happens
the arm swings between two angles at a rate of n milliseconds per sweep
the light flashes at the end of each arm sweep
the indicators are created and they flash in sequence (one at the end of each sweep).
now the problem
the Arm and light flash animation are created in code and added to a story board with repeat forever and auto reverse.
the indicators are created in code and need to fire an event at the end of each Arm sweep animation.
So, what I did after much messing around was create a timer that runs at the same pace as the storyboard.
the problem, over 30 seconds the timer and the storyboard go out of sync and therefore the indicators and the arm sweep are not in time (not good for a metronome!!).
I was trying to catch the completed event of the animations and use that as a trigger to stop and restart the timer, this was all I could come up with to keep the two in perfect sync.
the moving out of sync is caused by the storyboard slipping and the fact that the storyboard is invoked with begin on the line before the timer is invoked with .start, this although microseconds I think means that they start impossibly close but not at exactly the same time.
my question,
when I try to bind to the completed event of the animation it never fires. I was under the impression that completed even fires regardless of autoreverse (i.e in between each iteration). is this not the case?
can anyone think of another (more cunning) way to keep the two things in sync.
lastly, I did look to see if I could fire a method from a storyboard (which would of made my life really easy, however it would appear that this cannot be done).
if there are any suggestions I am not precious, I just want to get this finished!!
final point of interest,
the bpm can be adjusted whilst the metronome is running, this is achieved by calculating the millisecond duration on the fly (mouse down of a button) and scale the storyboard by the difference between the current speed and the new speed. obviously the timer running the indicators has to be changed at the same time (using interval).
code below is from my project so far (not the XAML just the C#)
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media.Animation;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Controls;
using System.Windows.Threading;
namespace MetronomeLibrary
{
public partial class MetronomeLarge
{
private bool Running;
//Speed and time signature
private int _bpm = 60;
private int _beats = 4;
private int _beatUnit = 4;
private int _currentBeat = 1;
private readonly int _baseSpeed = 60000 / 60;
private readonly DispatcherTimer BeatTimer = new DispatcherTimer();
private Storyboard storyboard = new Storyboard();
public MetronomeLarge()
{
InitializeComponent();
NumericDisplay.Value = BPM;
BeatTimer.Tick += new EventHandler(TimerTick);
SetUpAnimation();
SetUpIndicators();
}
public int Beats
{
get
{
return _beats;
}
set
{
_beats = value;
SetUpIndicators();
}
}
public int BPM
{
get
{
return _bpm;
}
set
{
_bpm = value;
//Scale the story board here
SetSpeedRatio();
}
}
public int BeatUnit
{
get
{
return _beatUnit;
}
set
{
_beatUnit = value;
}
}
private void SetSpeedRatio()
{
//divide the new speed (bpm by the old speed to get the new ratio)
float newMilliseconds = (60000 / BPM);
float newRatio = _baseSpeed / newMilliseconds;
storyboard.SetSpeedRatio(newRatio);
//Set the beat timer according to the beattype (standard is quarter beats for one sweep of the metronome
BeatTimer.Interval = TimeSpan.FromMilliseconds(newMilliseconds);
}
private void TimerTick(object sender, EventArgs e)
{
MetronomeBeat(_currentBeat);
_currentBeat++;
if (_currentBeat > Beats)
{
_currentBeat = 1;
}
}
private void MetronomeBeat(int Beat)
{
//turnoff all indicators
TurnOffAllIndicators();
//Find a control by name
MetronomeLargeIndicator theIndicator = (MetronomeLargeIndicator)gridContainer.Children[Beat-1];
//illuminate the control
theIndicator.TurnOn();
theIndicator.PlaySound();
}
private void TurnOffAllIndicators()
{
for (int i = 0; i <= gridContainer.Children.Count-1; i++)
{
MetronomeLargeIndicator theIndicator = (MetronomeLargeIndicator)gridContainer.Children[i];
theIndicator.TurnOff();
}
}
private void SetUpIndicators()
{
gridContainer.Children.Clear();
gridContainer.ColumnDefinitions.Clear();
for (int i = 1; i <= _beats; i++)
{
MetronomeLargeIndicator theNewIndicator = new MetronomeLargeIndicator();
ColumnDefinition newCol = new ColumnDefinition() { Width = GridLength.Auto };
gridContainer.ColumnDefinitions.Add(newCol);
gridContainer.Children.Add(theNewIndicator);
theNewIndicator.Name = "Indicator" + i.ToString();
Grid.SetColumn(theNewIndicator, i - 1);
}
}
private void DisplayOverlay_MouseDown(object sender, MouseButtonEventArgs e)
{
ToggleAnimation();
}
private void ToggleAnimation()
{
if (Running)
{
//stop the animation
((Storyboard)Resources["Storyboard"]).Stop() ;
BeatTimer.Stop();
}
else
{
//start the animation
BeatTimer.Start();
((Storyboard)Resources["Storyboard"]).Begin();
SetSpeedRatio();
}
Running = !Running;
}
private void ButtonIncrement_Click(object sender, RoutedEventArgs e)
{
NumericDisplay.Value++;
BPM = NumericDisplay.Value;
}
private void ButtonDecrement_Click(object sender, RoutedEventArgs e)
{
NumericDisplay.Value--;
BPM = NumericDisplay.Value;
}
private void ButtonIncrement_MouseEnter(object sender, MouseEventArgs e)
{
ImageBrush theBrush = new ImageBrush()
{
ImageSource = new BitmapImage(new
Uri(#"pack://application:,,,/MetronomeLibrary;component/Images/pad-metronome-increment-button-over.png"))
};
ButtonIncrement.Background = theBrush;
}
private void ButtonIncrement_MouseLeave(object sender, MouseEventArgs e)
{
ImageBrush theBrush = new ImageBrush()
{
ImageSource = new BitmapImage(new
Uri(#"pack://application:,,,/MetronomeLibrary;component/Images/pad-metronome-increment-button.png"))
};
ButtonIncrement.Background = theBrush;
}
private void ButtonDecrement_MouseEnter(object sender, MouseEventArgs e)
{
ImageBrush theBrush = new ImageBrush()
{
ImageSource = new BitmapImage(new
Uri(#"pack://application:,,,/MetronomeLibrary;component/Images/pad-metronome-decrement-button-over.png"))
};
ButtonDecrement.Background = theBrush;
}
private void ButtonDecrement_MouseLeave(object sender, MouseEventArgs e)
{
ImageBrush theBrush = new ImageBrush()
{
ImageSource = new BitmapImage(new
Uri(#"pack://application:,,,/MetronomeLibrary;component/Images/pad-metronome-decrement-button.png"))
};
ButtonDecrement.Background = theBrush;
}
private void SweepComplete(object sender, EventArgs e)
{
BeatTimer.Stop();
BeatTimer.Start();
}
private void SetUpAnimation()
{
NameScope.SetNameScope(this, new NameScope());
RegisterName(Arm.Name, Arm);
DoubleAnimation animationRotation = new DoubleAnimation()
{
From = -17,
To = 17,
Duration = new Duration(TimeSpan.FromMilliseconds(NumericDisplay.Milliseconds)),
RepeatBehavior = RepeatBehavior.Forever,
AccelerationRatio = 0.3,
DecelerationRatio = 0.3,
AutoReverse = true,
};
Timeline.SetDesiredFrameRate(animationRotation, 90);
MetronomeFlash.Opacity = 0;
DoubleAnimation opacityAnimation = new DoubleAnimation()
{
From = 1.0,
To = 0.0,
AccelerationRatio = 1,
BeginTime = TimeSpan.FromMilliseconds(NumericDisplay.Milliseconds - 0.5),
Duration = new Duration(TimeSpan.FromMilliseconds(100)),
};
Timeline.SetDesiredFrameRate(opacityAnimation, 10);
storyboard.Duration = new Duration(TimeSpan.FromMilliseconds(NumericDisplay.Milliseconds * 2));
storyboard.RepeatBehavior = RepeatBehavior.Forever;
Storyboard.SetTarget(animationRotation, Arm);
Storyboard.SetTargetProperty(animationRotation, new PropertyPath("(UIElement.RenderTransform).(RotateTransform.Angle)"));
Storyboard.SetTarget(opacityAnimation, MetronomeFlash);
Storyboard.SetTargetProperty(opacityAnimation, new PropertyPath("Opacity"));
storyboard.Children.Add(animationRotation);
storyboard.Children.Add(opacityAnimation);
Resources.Add("Storyboard", storyboard);
}
}
}
This might not be easily implemented with WPF animations. Instead, a good method would be a game loop. A little research should turn up lots of resources about this. The first one that jumped out at me was http://www.nuclex.org/articles/3-basics/5-how-a-game-loop-works.
In your game loop, you would follow one or the other of these basic procedures:
Calculate how much time has elapsed since the last frame.
Move your displays appropriately.
or
Calculate the current time.
Position your displays appropriately.
The advantage of a game loop is that although the timing may drift slightly (depending on what sort of timing you use), all displays will drift by the same amount.
You can prevent clock drift by calculating time by the system clock, which for practical purposes does not drift. Timers do drift, because they do not run by the system clock.
Time sync is a vaster field than you'd think.
I suggest you take a look at Quartz.NET which is renowned for scheduling/timers issues.
Syncing a WPF animation is tricky because Storyboards are not part of the logical tree, therefore you can't bind anything in them.
That's why you can't define dynamic/variable Storyboards in XAML, you have to do it in C# as you did.
I suggest you make 2 Storyboards: one for the tick to the left, the other one to the right.
In between each animation, fire a method to do your calculations/update another part of the UI, but do it in a separate Task so that the timings aren't messed up (a few µs for the calculations make up for quite some time after 30s already!)
Keep in mind that you will need to use Application.Current.Dispatcher from your Task to update the UI.
And lastly, at least set the Task flag TaskCreationOptions.PreferFairness so that Tasks run in the order they were started.
Now since that just gives a hint to the TaskScheduler and doesn't guarantees them to run in order, you may want to use a queueing system instead for full guarantee.
HTH,
Bab.
You could try 2 animations , one for the right swing and one for the left. In the animation complete on each, start the other animation (checking for cancellation flags) and update your indicators (possibly via BeginInvoke on the Dispatcher so you don't interfere with the next animation start.)
I think getting the timer to sync with an animation is difficult - it is a dispatcher based timer which is based on messages - sometimes it can skip a bit of time, ie if you click fast with the mouse a lot I think the animation timer also is dispatcher based, so they will easily get out of sync.
I would suggest to abandon the syncing and let the timer handle it. Can't you let it update a property with notification and let your metronome arm position bind to that?
To get the accelaration/deceleration you just have to use a Sine or Cosine function.