Press "Enter" to skip to content

7 – MultiClock – Migliorare l’orologio analogico

Nel precedente articolo, abbiamo generato l’orologio analogico, e lo abbiamo disegnato e fatto funzionare. Adesso, vediamo come migliorare la sua funzionalità e come utilizzare meglio MVVM e le peculiarità di WPF. Inoltre, ne miglioriamo anche la parte visuale, aggiungendo oltre ai “Tick” per ore, minuti/secondi, anche il valore numerico delle ore.

Nella prima versione del nostro orologio analogico, abbiamo implementato l’aggiornamento delle lancette disegnandole da codice. Non solo, ad ogni modifica dell’orologio (quindi una volta al secondo), cancellavamo e ridisegnavamo le tre lancette. E’ un metodo poco efficiente, anche se comunque WPF ricicla le classi da noi generate ed eliminate quindi funziona comunque bene.

In realtà, per disegnare correttamente le lancette, possiamo generarle una sola volta e poi spostarne semplicemente la posizione. Vediamo quindi come fare.

Modifichiamo HandData.cs

Per fare in modo che la posizione della linea di una lancetta sia modificata automaticamente effettuando il Binding dell’oggetto Line alle property della nostra classe, dobbiamo fare in modo che le Property che variano al variare dell’ora scatenino l’opportuno evento PropertyChanged, in modo tale che WPF automaticamente aggiorni la User Interface.

public double HandTime
{
    get
    {
        return mHandTime;
    }
    set
    {
        mHandTime = value;
        OnPropertyChanged(FLD_HandTime);
        OnPropertyChanged(FLD_EndPointX);
        OnPropertyChanged(FLD_EndPointY);
    }
}

La prima modifica che facciamo è quella che fa in modo che alla modifica del valore del tempo segnato dalla lancetta, vengano scatenati gli eventi PropertyChanged anche per i due punti  che identificano l’estremo esterno della linea. infatti, le property EndPointX ed EndPointY sono a sola lettura e sono prodotte con la formula da noi individuata nel precedente articolo.

public double EndPointX
{
    get
    {
        return Parent.CenterX + (HandLength * System.Math.Sin(RadianValue));
    }
}

private double RadianValue
{
    get
    {
        if (IsHour)
        {
            return HandTime * 360 / 12 * PI / 180;
        }
        else
        {
            return HandTime * 360 / 60 * PI / 180;
        }
    }
}

Qui sopra vediamo come è calcolata EndPointX, e come è calcolata RadianValue, siccome RadianValue dipende dal valore di HandTime, quando questo varia, deve essere ricalcolata e deve scatenare l’aggiornamento di EndPointX e Y.

public void UpdateHandProperties()
{
    OnPropertyChanged(FLD_HandThickness);
    OnPropertyChanged(FLD_EndPointX);
    OnPropertyChanged(FLD_EndPointY);
}

Oltre alla modifica precedente, creiamo un metodo nella classe, che scatena in modo arbitrario l’aggiornamento di 3 property, la dimensione della linea, e la sua posizione. Questo metodo, dovrà essere chiamato sia quando inizializziamo l’orologio, per disegnare la lancetta la prima volta, sia quando viene modificata la dimensione dell’orologio, affinché dimensione e posizione siano ricalcolate.

Modifiche a AClockData.cs

Nei dati dell’orologio, aggiorniamo quanto serve a supportare le lancette in MVVM.

public double ClockDimension
{
    get
    {
        return mClockDimension;
    }
    set
    {
        mClockDimension = value;
        OnPropertyChanged(FLD_ClockDimension);
        OnPropertyChanged(FLD_TickThickness);
        HourHand.UpdateHandProperties();
        MinuteHand.UpdateHandProperties();
        SecondsHand.UpdateHandProperties();
    }
}

La sola modifica in questo caso, è la chiamata esplicita all’aggiornamento delle property che disegnano le lancette alla modifica della dimensione dell’orologio.

Modifiche a AClockControl.xaml e AClockControl.xaml.cs

Visto che le lancette dell’orologio non saranno più disegnate da codice, andiamo a inserirle dentro allo XAML dello user control.

        <Canvas
            Name="HandsCanvas"  HorizontalAlignment="Center" VerticalAlignment="Center"  >
            <Line 
                X1="{Binding Path=Clock.CenterX, UpdateSourceTrigger=PropertyChanged}"
                Y1="{Binding Path=Clock.CenterY, UpdateSourceTrigger=PropertyChanged}"
                X2="{Binding Path=Clock.HourHand.EndPointX, UpdateSourceTrigger=PropertyChanged}"
                Y2="{Binding Path=Clock.HourHand.EndPointY, UpdateSourceTrigger=PropertyChanged}"
                Stroke="{Binding Path=Clock.HourHand.HandColor, UpdateSourceTrigger=PropertyChanged}"
                StrokeThickness="{Binding Path=Clock.HourHand.HandThickness, UpdateSourceTrigger=PropertyChanged}"
                
                />
            <Line 
                X1="{Binding Path=Clock.CenterX, UpdateSourceTrigger=PropertyChanged}"
                Y1="{Binding Path=Clock.CenterY, UpdateSourceTrigger=PropertyChanged}"
                X2="{Binding Path=Clock.MinuteHand.EndPointX, UpdateSourceTrigger=PropertyChanged}"
                Y2="{Binding Path=Clock.MinuteHand.EndPointY, UpdateSourceTrigger=PropertyChanged}"
                Stroke="{Binding Path=Clock.MinuteHand.HandColor, UpdateSourceTrigger=PropertyChanged}"
                StrokeThickness="{Binding Path=Clock.MinuteHand.HandThickness, UpdateSourceTrigger=PropertyChanged}"
                
                />
            <Line 
                X1="{Binding Path=Clock.CenterX, UpdateSourceTrigger=PropertyChanged}"
                Y1="{Binding Path=Clock.CenterY, UpdateSourceTrigger=PropertyChanged}"
                X2="{Binding Path=Clock.SecondsHand.EndPointX, UpdateSourceTrigger=PropertyChanged}"
                Y2="{Binding Path=Clock.SecondsHand.EndPointY, UpdateSourceTrigger=PropertyChanged}"
                Stroke="{Binding Path=Clock.SecondsHand.HandColor, UpdateSourceTrigger=PropertyChanged}"
                StrokeThickness="{Binding Path=Clock.SecondsHand.HandThickness, UpdateSourceTrigger=PropertyChanged}"
                
                />
        </Canvas>

Modifichiamo “HandsCanvas” e vi inseriamo all’interno i tre oggetti Line, come vedete, abbiamo messo in Binding gli estremi delle linee, il colore (Stroke) e la grossezza della linea. Abbiamo utilizzato il Path per indicare la property dell’oggetto Hand, dentro all’oggetto Clock a cui ogni attributo della linea è agganciato.

public void StartClock(AClockData clockData)
{
    Clock = clockData;
 
    CheckClockDimension();
 
    this.mClockTimer = new DispatcherTimer();
    this.mClockTimer.Interval = TimeSpan.FromMilliseconds(1000);
    this.mClockTimer.Tick += ClockTimer_Tick;
    this.mClockTimer.Start();
}

Nel codice di gestione del controllo, modifichiamo StartClock, chiamando solo il controllo della dimensione dell’orologio e togliendo il disegno delle lancette.

private void CheckClockDimension()
{
    double dimension = ActualHeight < ActualWidth ? ActualHeight : ActualWidth;
 
    //If the clock control dimension is different due to a resize of the container
    //changes the clock dimension and redraws the ticks.
    if (dimension != Clock.ClockDimension)
    {
        Clock.ClockDimension = dimension;
        DrawTicks();
    }
}

Modifichiamo la funzione di controllo delle dimensioni, facendo solo la modifica alla dimensione dell’orologio e ridisegnando i “Ticks”.

private void ClockTimer_Tick(object sender, object e)
{
    UpdateClock();
}

Modifichiamo l’event handler scatenato dal Timer di aggiornamento, chiamando un nuovo metodo UpdateClock, perché non disegnamo più le lancette.

private void UpdateClock()
{
    CheckClockDimension();
 
    Clock.DateAndTime = DateTime.Now;
}

Il metodo UpdateClock adesso ricontrolla sempre la dimensione dell’orologio, e poi, semplicemente, aggiorna l’ora nella classe orologio. Da questo possiamo notare come usare MVVM ci fa scrivere molto meno codice.

Miglioriamo ora il disegno del quadrante dell’orologio, aggiungendo i numeri che indicano le ore.

private void DrawTicks()
{
    string[] hours = new string[] { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12" };
    int hourIdx = 0;
    TickCanvas.Children.Clear();
    for (int i = 0; i < 60; i++)
    {
        if (i % 5 == 0)
        {
            DrawLine(
            Clock.CenterX + (Clock.ClockRadius / 1.10 * System.Math.Sin(i * 6 * PI / 180)),
            Clock.CenterY - (Clock.ClockRadius / 1.10 * System.Math.Cos(i * 6 * PI / 180)),
            Clock.CenterX + (Clock.ClockRadius / 1.35 * System.Math.Sin(i * 6 * PI / 180)),
            Clock.CenterY - (Clock.ClockRadius / 1.35 * System.Math.Cos(i * 6 * PI / 180)),
            Clock.TickColor, Clock.TickThickness, TickCanvas);
            DrawText(hours[hourIdx],
                Clock.CenterX + (Clock.ClockRadius / 1.50 * System.Math.Sin(i * 6 * PI / 180)),
                Clock.CenterY - (Clock.ClockRadius / 1.50 * System.Math.Cos(i * 6 * PI / 180)),
                Clock.CenterX + (Clock.ClockRadius / 1.55 * System.Math.Sin(i * 6 * PI / 180)),
                Clock.CenterY - (Clock.ClockRadius / 1.55 * System.Math.Cos(i * 6 * PI / 180)),
            Clock.TickColor, TickCanvas);
            hourIdx++;
        }
        else
        {
            DrawLine(
            Clock.CenterX + (Clock.ClockRadius / 1.10 * System.Math.Sin(i * 6 * PI / 180)),
            Clock.CenterY - (Clock.ClockRadius / 1.10 * System.Math.Cos(i * 6 * PI / 180)),
            Clock.CenterX + (Clock.ClockRadius / 1.15 * System.Math.Sin(i * 6 * PI / 180)),
            Clock.CenterY - (Clock.ClockRadius / 1.15 * System.Math.Cos(i * 6 * PI / 180)),
            Clock.TickColor, Clock.TickThickness, TickCanvas);
        }
    }
}

Per aggiungere i numeri che identificano le ore al nostro orologio, per prima cosa creiamo un array che li contiene, dopo di ciò, aggiungiamo un metodo che chiamiamo DrawText che andrà a disegnare ogni numero all’interno del quadrante. abbiamo usato un metodo di posizionamento simile a quello dei Ticks, ma li abbiamo sistemati in un punto più interno, aumentando il valore del divisore del raggio (per renderlo più corto). Questo sistema, ci permette volendo di indicare le ore sia con i numeri arabi, che con quelli romani, basta variare le stringhe.

Vediamo ora, come abbiamo costruito il metodo per disegnare le ore, ma prima, facciamo una ulteriore modifica a AClockData.cs

public const string FLD_NumbersColor = "NumbersColor";
private SolidColorBrush mNumbersColor;
public FontFamily NumbersFontFamily
{
    get
    {
        return mNumbersFontFamily;
    }
    set
    {
        mNumbersFontFamily = value;
        OnPropertyChanged(FLD_NumbersFontFamily);
    }
}

public const string FLD_NumbersFontFamily = "NumbersFontFamily";
private FontFamily mNumbersFontFamily;
public FontFamily NumbersFontFamily
{
    get
    {
        return mNumbersFontFamily;
    }
    set
    {
        mNumbersFontFamily = value;
        OnPropertyChanged(FLD_NumbersFontFamily);
    }
}

public const string FLD_NumbersSize = "NumbersSize";
private double mNumbersSize;
public double NumbersSize
{
    get
    {
        return mNumbersSize;
    }
    set
    {
        mNumbersSize = value;
        OnPropertyChanged(FLD_NumbersSize);
    }
}

Aggiungiamo tre dati, per poter indicare all’orologio analogico il colore, la forma e la dimensione dei caratteri per scrivere i numeri che rappresentano le ore.

private void DrawText(string text, double x1, double y1, double x2, double y2,
    SolidColorBrush color, Canvas destinationCanvas)
{
    TextBlock txt = new TextBlock()
    {
        Foreground = Clock.NumbersColor,
        FontSize = Clock.NumbersSize,
        FontFamily = Clock.NumbersFontFamily,
        Text = text
    };
 
    Size sz = MeasureString(text, txt);
    txt.Margin = new System.Windows.Thickness(x1 - sz.Width / 2, y1 - sz.Height / 2, x2, y2);
 
    destinationCanvas.Children.Add(txt);
}

Ed ora, il metodo dello UserControl che disegna il numero, questo metodo, per prima cosa genera un TextBlock, che è l’oggetto WPF deputato a scrivere del testo formattato, assegna al controllo colore, font e dimensione, e il testo da scrivere. Poi, va a calcolare la dimensione del campo di testo, utilizzando un metodo helper che abbiamo chiamato MeasureString (e vedremo fra poco), per misurare le dimensioni del testo (o meglio del rettangolo che lo contiene), fatto questo, la posizione in cui il testo è disegnato viene calcolata utilizzando la property Margin dell’oggetto TextBlock, questo perché quando inseriamo un oggetto di questo tipo in un Canvas, la sua posizione è stabilita dal margine dell’oggetto stesso rispetto alle dimensioni del Canvas. In questo caso, usiamo la dimensione del testo per centrarlo rispetto alla linea dell’ora, spostandolo di Width/Mezzi e Height/Mezzi rispetto al valore calcolato sulla circonferenza.

private Size MeasureString(string hour, TextBlock txt)
{
    var formattedText = new FormattedText(
        hour,
        CultureInfo.CurrentUICulture,
        FlowDirection.LeftToRight,
        new Typeface(txt.FontFamily, txt.FontStyle, txt.FontWeight, txt.FontStretch),
        txt.FontSize, Brushes.Black);
 
    return new Size(formattedText.Width, formattedText.Height);
}

MeasureString è un metodo helper che permette di ottenere le dimensioni di un testo all’interno di un Textblock, basate sulla font, sulla culture, sulla forma e la dimensione di quanto scritto. Un metodo artigianale, ma efficacie.

Modifiche a MainWindow.cs

private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
    AClockData clock = new AClockData();
 
    clock.BackColor = new SolidColorBrush(Colors.Black);
    clock.TickColor = new SolidColorBrush(Colors.White);
    clock.TickThicknessDivisor = 130;
    clock.NumbersColor = new SolidColorBrush(Colors.Yellow);
    clock.NumbersFontFamily = new FontFamily("Calibri");
    clock.NumbersSize = 24;
 
    clock.HourHand.HandColor = new SolidColorBrush(Colors.LightGray);
    clock.HourHand.LengthMultiplier = 0.48F;
    clock.HourHand.ThicknessDivisor = 100;
 
    clock.MinuteHand.HandColor = new SolidColorBrush(Colors.DarkGray);
    clock.MinuteHand.LengthMultiplier = 0.58F;
    clock.MinuteHand.ThicknessDivisor = 150;
 
    clock.SecondsHand.HandColor = new SolidColorBrush(Colors.LightSteelBlue);
    clock.SecondsHand.LengthMultiplier = 0.68F;
    clock.SecondsHand.ThicknessDivisor = 200;
 
    ClockControl.StartClock(clock);
}

In MainWindow, modifichiamo la configurazione dell’orologio, aggiungendo il necessario a colorare e disegnare i numeri delle ore.

L’orologio risultante è il seguente:

analog_clock_whours_01[12]

analog_clock_whours_02[12]

Rimpicciolendolo, vediamo che i numeri rimangono della stessa dimensione, al momento ci accontentiamo, in seguito proveremo a legarli alla dimensione dell’orologio.

Riepilogo

Vediamo cosa abbiamo spiegato in questo nuovo articolo:

  • Come usare MVVM per pilotare gli oggetti Line che rappresentano le lancette invece di disegnarle da codice.
  • Come aggiungere il numero che rappresenta le ore all’orologio analogico.

 

Potete scaricare il progetto esempio dal link qui indicato:

Per qualsiasi domanda, osservazione, commento, approfondimento, o per segnalare un errore, potete usare il link alla form di contatto in cima alla pagina.