Zum Inhalt
Kuonen.UI

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.

Vorführung: DispatcherTimerVorführung: DispatcherTimer

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;
    }
}