DispatcherTimer
Ein Timer am Dispatcher: Tick läuft auf dem UI-Thread, Start, Stopp und Interval gehen von jedem Thread. Verpasste Ticks werden nicht nachgeholt, eine Ausnahme im Tick beendet den Timer nicht. Ersetzt WinForms-Timer und Handler.postDelayed.


KXAML
DispatcherTimerSeite.kxaml
<StackPanel xmlns="https://ui.kuonen.org/kxaml"
xmlns:x="https://ui.kuonen.org/kxaml/x"
x:Class="Kuonen.UI.Galerie.Seiten.DispatcherTimerVorfuehrung"
Spacing="12">
<!-- Der Timer entsteht im Code-Behind beim ersten Start: ein DispatcherTimer braucht beim Anlegen einen Dispatcher. -->
<TextBlock x:Name="Zeit" Text="Zeit: 0.0 s" FontSize="20" />
<TextBlock x:Name="Zaehler" Text="Ticks: 0" FontSize="20" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Content="Start" Click="Start_Click" />
<Button Content="Stopp" Click="Stopp_Click" />
<Button Content="Zurück" Click="Zurueck_Click" />
</StackPanel>
<TextBlock x:Name="IntervallText" Text="Intervall: 100 ms" />
<Slider Minimum="10" Maximum="1000" Value="100" SmallChange="10" LargeChange="100" Width="320" HorizontalAlignment="Left" ValueChanged="Intervall_ValueChanged" />
<Button Content="In 2 s melden" Click="Einmal_Click" />
<TextBlock x:Name="Status" Text="Noch keine Meldung." TextWrapping="Wrap" />
</StackPanel>DispatcherTimerSeite.kxaml.cs
using System.Globalization;
using Kuonen.UI;
using Kuonen.UI.Controls;
using Kuonen.UI.Ereignisse;
using Kuonen.UI.Threading;
namespace Kuonen.UI.Galerie.Seiten;
public sealed partial class DispatcherTimerSeite
{
protected override Func<UIElement>? KxamlFabrik => () => new DispatcherTimerVorfuehrung();
}
/// <summary>KXAML-Fassung der DispatcherTimer-Seite: der Baum steht in DispatcherTimerSeite.kxaml, hier nur das Verhalten.</summary>
public sealed partial class DispatcherTimerVorfuehrung
{
private DispatcherTimer? _timer;
private IDisposable? _meldung;
private int _ticks, _meldungen, _intervall = 100;
private long _summe, _seit = -1; // gemessene Zeit in ms: Summe der beendeten Läufe und Beginn des laufenden
public DispatcherTimerVorfuehrung() => InitializeComponent();
private long Jetzt() => _timer!.Dispatcher.Uhr.Jetzt;
private void Zeige()
{
long ms = _summe + (_seit >= 0 ? Jetzt() - _seit : 0), zehntel = (ms + 50) / 100;
Zaehler.Text = "Ticks: " + _ticks.ToString(CultureInfo.InvariantCulture);
Zeit.Text = "Zeit: " + (zehntel / 10).ToString(CultureInfo.InvariantCulture) + "." + (zehntel % 10).ToString(CultureInfo.InvariantCulture) + " s";
}
private void Halte()
{
if (_seit < 0) return;
_summe += Jetzt() - _seit;
_seit = -1;
}
// Ohne Entladen-Ereignis: ein Tick merkt, dass die Seite nicht mehr im Fenster steht, und stoppt den Timer.
private bool ImFenster()
{
for (Visual? v = this; v is not null; v = v.VisualParent)
if (v is Window) return true;
return false;
}
private void Start_Click(object sender, RoutedEventArgs e)
{
if (_timer is null)
{
if (Dispatcher.Aktuell is null)
{
Status.Text = "Kein Dispatcher auf diesem Thread: der Timer startet nicht.";
return;
}
_timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(_intervall) };
_timer.Tick += (_, _) =>
{
if (!ImFenster())
{
Halte();
_timer.Stop();
return;
}
_ticks++;
Zeige();
};
}
_timer.Start();
if (_seit < 0) _seit = Jetzt();
}
private void Stopp_Click(object sender, RoutedEventArgs e)
{
if (_timer is null) return;
Halte();
_timer.Stop();
}
private void Zurueck_Click(object sender, RoutedEventArgs e)
{
if (_timer is not null)
{
Halte();
_timer.Stop();
}
_summe = 0;
_ticks = 0;
Zeige();
}
private void Intervall_ValueChanged(object? sender, WertGeaendertArgs e)
{
_intervall = (int)Math.Round(e.Neu);
IntervallText.Text = "Intervall: " + _intervall.ToString(CultureInfo.InvariantCulture) + " ms";
if (_timer is not null) _timer.Interval = TimeSpan.FromMilliseconds(_intervall); // wirkt ab jetzt, auch bei laufendem Timer
}
private void Einmal_Click(object sender, RoutedEventArgs e)
{
if (Dispatcher.Aktuell is null)
{
Status.Text = "Kein Dispatcher auf diesem Thread: nichts wird gemeldet.";
return;
}
_meldung?.Dispose(); // ein erneuter Druck ersetzt die ausstehende Meldung
_meldung = DispatcherTimer.RunOnce(() => Status.Text = "Gemeldet nach 2 s (" + (++_meldungen).ToString(CultureInfo.InvariantCulture) + ")", TimeSpan.FromSeconds(2));
}
}C#
DispatcherTimerSeite.cs
using System.Globalization;
using Kuonen.UI;
using Kuonen.UI.Controls;
using Kuonen.UI.Layout;
using Kuonen.UI.Threading;
namespace Kuonen.UI.Galerie.Seiten;
/// <summary>Galerie-Seite: <see cref="DispatcherTimer"/>.</summary>
public sealed partial class DispatcherTimerSeite : GalerieSeite
{
public override string Titel => "DispatcherTimer";
public override string Beschreibung =>
"Ein Timer am Dispatcher: Tick läuft auf dem UI-Thread, Start, Stopp und Interval gehen von jedem Thread. Verpasste "
+ "Ticks werden nicht nachgeholt, eine Ausnahme im Tick beendet den Timer nicht. Ersetzt WinForms-Timer und Handler.postDelayed.";
public override string Stichworte => "Timer Zeitgeber Takt Stoppuhr Tick Interval RunOnce Dispatcher";
public override string Datei => nameof(DispatcherTimerSeite);
public override UIElement Baue()
{
DispatcherTimer? timer = null;
IDisposable? meldung = null;
int ticks = 0, meldungen = 0, intervall = 100;
long summe = 0, seit = -1; // gemessene Zeit in ms: Summe der beendeten Läufe und Beginn des laufenden
StackPanel? wurzel = null;
var zaehler = new TextBlock { Text = "Ticks: 0", FontSize = 20 };
var zeit = new TextBlock { Text = "Zeit: 0.0 s", FontSize = 20 };
var intervallText = new TextBlock { Text = "Intervall: 100 ms" };
var status = new TextBlock { Text = "Noch keine Meldung.", TextWrapping = TextWrapping.Wrap };
long Jetzt() => timer!.Dispatcher.Uhr.Jetzt;
void Zeige()
{
long ms = summe + (seit >= 0 ? Jetzt() - seit : 0), zehntel = (ms + 50) / 100;
zaehler.Text = "Ticks: " + ticks.ToString(CultureInfo.InvariantCulture);
zeit.Text = "Zeit: " + (zehntel / 10).ToString(CultureInfo.InvariantCulture) + "." + (zehntel % 10).ToString(CultureInfo.InvariantCulture) + " s";
}
void Halte()
{
if (seit < 0) return;
summe += Jetzt() - seit;
seit = -1;
}
// Ohne Entladen-Ereignis: ein Tick merkt, dass die Seite nicht mehr im Fenster steht, und stoppt den Timer.
bool ImFenster()
{
for (Visual? v = wurzel; v is not null; v = v.VisualParent)
if (v is Window) return true;
return false;
}
// Der Timer entsteht erst beim ersten Start: bis dahin läuft nichts, und die Seite braucht keinen Dispatcher.
var start = new Button { Content = "Start" };
start.Click += (_, _) =>
{
if (timer is null)
{
if (Dispatcher.Aktuell is null)
{
status.Text = "Kein Dispatcher auf diesem Thread: der Timer startet nicht.";
return;
}
timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(intervall) };
timer.Tick += (_, _) =>
{
if (!ImFenster())
{
Halte();
timer.Stop();
return;
}
ticks++;
Zeige();
};
}
timer.Start();
if (seit < 0) seit = Jetzt();
};
var stopp = new Button { Content = "Stopp" };
stopp.Click += (_, _) =>
{
if (timer is null) return;
Halte();
timer.Stop();
};
var zurueck = new Button { Content = "Zurück" };
zurueck.Click += (_, _) =>
{
if (timer is not null)
{
Halte();
timer.Stop();
}
summe = 0;
ticks = 0;
Zeige();
};
var regler = new Slider { Minimum = 10, Maximum = 1000, Value = intervall, SmallChange = 10, LargeChange = 100, Width = 320, HorizontalAlignment = HorizontalAlignment.Left };
regler.ValueChanged += (_, e) =>
{
intervall = (int)Math.Round(e.Neu);
intervallText.Text = "Intervall: " + intervall.ToString(CultureInfo.InvariantCulture) + " ms";
if (timer is not null) timer.Interval = TimeSpan.FromMilliseconds(intervall); // wirkt ab jetzt, auch bei laufendem Timer
};
var einmal = new Button { Content = "In 2 s melden" };
einmal.Click += (_, _) =>
{
if (Dispatcher.Aktuell is null)
{
status.Text = "Kein Dispatcher auf diesem Thread: nichts wird gemeldet.";
return;
}
meldung?.Dispose(); // ein erneuter Druck ersetzt die ausstehende Meldung
meldung = DispatcherTimer.RunOnce(() => status.Text = "Gemeldet nach 2 s (" + (++meldungen).ToString(CultureInfo.InvariantCulture) + ")", TimeSpan.FromSeconds(2));
};
var knoepfe = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 };
knoepfe.Children.Add(start);
knoepfe.Children.Add(stopp);
knoepfe.Children.Add(zurueck);
var seite = wurzel = new StackPanel { Spacing = 12 };
seite.Children.Add(zeit);
seite.Children.Add(zaehler);
seite.Children.Add(knoepfe);
seite.Children.Add(intervallText);
seite.Children.Add(regler);
seite.Children.Add(einmal);
seite.Children.Add(status);
return seite;
}
}