Press "Enter" to skip to content

6 – MultiClock – Visualizzare un orologio Analogico

Nonostante quello che abbiamo già fatto sui nostri orologi, i nostri utenti/clienti sono sempre pronti a fare ulteriori richieste. La prima che ci hanno fatto oggi è stata: ma non potreste visualizzare un orologio analogico? sai le lancette sono più facili da leggere.

Davvero è più facile leggere le lancette dei numeri? Lo trovo strano visto che Windows 10 ha (finalmente) dismesso l’orologio analogico nel suo calendario per quello numerico. Però questa richiesta ci da modo di fare un esercizio di matematica, geometria, e di imparare un po’ di cose, pertanto sono andata a cercare su Internet un articolo che spiegasse come disegnare un orologio e ne ho trovato uno fatto piuttosto bene, anche se il codice postato contiene quattro o cinque errori che devono essere corretti per far funzionare l’orologio. Trovate l’articolo originale a questo indirizzo non farò una traduzione diretta, e il mio codice sarà un po’ più teso verso MVVM che verso la semplice visualizzazione di ore minuti e secondi, spero possa rivelarsi interessante.

Ma come si disegna un orologio in WPF?

Cos’è un orologio analogico se guardiamo alla sua geometria?

  • Un cerchio
  • Tre linee rette che rappresentano ore, minuti e secondi
  • Una serie di linee disposte lungo la circonferenza per marcare i 60 punti che rappresentano minuti o secondi, e di questi 60 12 sono quelli che rappresentano le ore.

Quello che dobbiamo capire come fare quindi è:

  • Come disegnare delle linee, e questo è facile perché in WPF esiste un oggetto che si chiama Line.
  • Quale oggetto ci permette di disegnare le linee in modo arbitrario al suo interno, ed è ancora facile perché in WPF esiste un oggetto che si chiama Canvas, fatto proprio per questo.

La cosa più complicata, sfortunatamente è sapere come fare a decidere l’angolo esatto da dare alla linea retta rispetto al centro di una circonferenza virtuale, per rappresentare il valore di ore, minuti, secondi.
Tutto questo, deriva da qualcosa che è sepolto nella mia memoria da più di 30 anni, ovvero la geometria piana pertanto prenderò un escavatore, per andare a cercare con l’aiuto di Wikipedia, quello che ci serve.

Geometria spicciola

Prima operazione da fare, trasformare il numero che rappresenta una delle nostre lancette in un angolo. E’ un operazione facile, una volta che qualcuno ci mostra come si fa:

Una circonferenza è formata da 360° trecentosessanta gradi. Un giorno è formato da 24 ore, ma noi vogliamo un orologio classico, quindi le ore sono 12 x 2;  la formula  per stabilire l’angolo della lancetta rispetto al quadrante di 360 gradi è: x = valore/12 * 360, tutto qui? Si, tutto qui, ecco perché gli orologi sono fatti così. Ovviamente per i minuti e i secondi, visto che ce ne sono 60, il valore del divisore diviene 60, quindi x=valore/60 * 360.

  • Gradi Ora: ore/12 * 360 – ci sono 24 ore in un giorno, ma noi le rappresentiamo con i numeri da 1 a 12 pertanto, dividiamo le ore per 12 e le moltiplichiamo per 360 che sono i gradi di una circonferenza.
  • Gradi Minuto: minuti/60 * 360 – ci sono 60 minuti in un ora, quindi dividiamo il numero di minuti per 60 e li moltiplichiamo per 360.
  • Gradi Secondo: secondi /60 * 360 – ci sono 60 secondi in un minuto, quindi dividiamo il numero di secondi per 60 e li moltiplichiamo per 360.

Sapere l’angolo con cui disegnare la lancetta di un orologio, è bello ma come disegnamo la lancetta?

Il valore dell’angolo è quello che abbiamo calcolato, ma con l’angolo non possiamo disegnare una retta, infatti, per disegnare una retta ci serve quanto segue:

Quello che dobbiamo calcolare, sono x1, y1, x2 e y2. Perché abbiamo usato questi due valori che ci ricordano qualcosa riguardo la geometria?

Perché le posizioni dei due punti di inizio e fine della nostra lancetta, sono due serie di coordinate cartesiane, ed il nostro orologio può essere trasformato in un sistema di assi cartesiani in cui il punto 0 del sistema è il centro del cerchio.
Questo significa che x1 e y1 ci vengono forniti gratis, e valgono 0 e 0.

Dobbiamo calcolare x2 e y2 però e l’unica informazione che abbiamo è l’angolo della lancetta. In realtà no, non è esattamente così, non sappiamo solo l’angolo della lancetta, ma, considerato che siamo noi a decidere quanto è grande l’orologio, sappiamo anche quanto è lunga la lancetta, perché la sua misura la possiamo decidere noi in base al raggio del cerchio.

Qui sopra abbiamo le informazioni note, che sono la lunghezza della lancetta, l’origine della retta e quello che non sappiamo, ovvero le coordinate della fine della nostra lancetta. Guardando l’immagine possiamo aggiungere un ulteriore particolare:

Le coordinate x2 e y2 sono i cateti di un triangolo di cui la lancetta è l’ipotenusa. Quindi, quello che dobbiamo calcolare è la lunghezza dei cateti, conoscendo l’ipotenusa e l’angolo della stessa.
Per nostra fortuna, la geometria ci permette di ottenere quel che ci serve grazie alle seguenti proprietà:

  • il cateto adiacente ad un angolo (x2), è uguale alla lunghezza dell’ipotenusa moltiplicata per il Coseno dell’angolo espresso in radianti.
  • il cateto opposto ad un angolo (y2), è uguale alla lunghezza dell’ipotenusa moltiplicata per il Seno dell’angolo espresso in radianti.

Pertanto, dobbiamo trasformare il nostro angolo in radianti. Cos’è il radiante? (non c’entra con il riscaldamento, quello è il radiatore). L’articolo che lo spiega in modo dettagliato, lo trovate ovviamente su wikipedia.
A noi interessa sapere, che per convertire un angolo in radianti la formula è:  rad = gradi * PI / 180, dove PI è il valore del pigreco.  (3.141592654).

  • Ora in radianti: Gradi Ora * PI /180
  • Minuti in radianti: Gradi Minuto * PI / 180
  • Secondi in radianti: Gradi Secondo * PI/180

Dove il valore dei gradi di ciascun valore lo abbiamo calcolato con la formula espressa più in alto. Pertanto applicando le 2 formule citate, e se volete sapere cosa sono il Seno ed il Coseno del nostro angolo wikipedia vi da una mano come sempre, le formule per trovare x2 e y2 sono le seguenti.

  • x2 = LunghezzaLancetta * cos(Angolo in radianti)
  • y2 = LunghezzaLancetta * sin(Angolo in radianti)

A questo punto abbiamo il necessario per generare il nostro orologio analogico, quindi possiamo procedere con il C# e lo XAML. In realtà poco XAML e molto codice, visto che costruiremo quasi tutto l’orologio da codice.

La soluzione

Per creare questo orologio analogico, che in seguito verrà incorporato nel progetto MultiClock, ho deciso di fare un progetto a se stante che conterrà il progetto eseguibile per testare l’orologio analogico e una libreria che conterrà lo user control del nostro orologio, in modo tale che possiamo poi aggiungerlo alla libreria del Multiclock per utilizzarlo assieme agli orologi numerici. Ho quindi creato le seguenti cose:

  • Una nuova soluzione utilizzando il progetto WPF Application che ho chiamato TheClock.
  • A questa soluzione ho aggiunto un secondo progetto, utilizzando il progetto WPF User Control Library, che ho chiamato AnalogClock.

Qui sopra l’aspetto base dei due progetti, in AnalogClock, ho creato 2 cartelle, Controls ed Entities, che ospiteranno le classi necessarie a costruire l’orologio analogico. ho inoltre modificato le proprietà del progetto nel seguente modo:

 

Ho inoltre firmato sia la libreria che l’exe con la chiave dotnetwork usata anche per l’applicazione Multiclock vi rimando al primo articolo della serie Multiclock per una breve spiegazione ed il link che spiega il motivo per cui firmare una libreria.

Per creare il nostro orologio ho creato le seguenti classi:

  • AClockControl.xaml – AClockControl.xaml.cs – Lo user control che disegna l’orologio.
  • AClockData.cs – La classe che rappresenta l’intero orologio e tutti i dati che servono per disegnarlo.
  • HandData.cs – La classe che rappresenta una singola lancetta dell’orologio e i dati che servono per disegnarla.

AClockControl.xaml – .xaml.cs

Questa classe è quello che fisicamente genera e fa funzionare il nostro orologio, la parte Xaml ci fornisce il supporto di visualizzazione, il codice genera dinamicamente l’orologio.

<UserControl x:Class="Dnw.AnalogClock.Controls.AClockControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid Background="{Binding Clock.BackColor}">
 
        <Canvas
            Name="TickCanvas"  HorizontalAlignment="Center" VerticalAlignment="Center"  ></Canvas>
 
        <Canvas
            Name="HandsCanvas"  HorizontalAlignment="Center" VerticalAlignment="Center"  ></Canvas>
 
    </Grid>
</UserControl>

Come possiamo vedere, lo xaml è molto semplice, abbiamo una Grid, la cui sola peculiarità è il fatto che il suo colore di background è in Binding ad una property, ovvero Clock.BackColor, questo ci permetterà di pilotare il colore di sfondo dell’orologio analogico, quando lo utilizzeremo nel multiclock.

Abbiamo poi due diversi controlli Canvas che sono posti uno sopra all’altro all’interno della grid, i loro nomi dovrebbero darci un idea del loro uso, infatti TickCanvas (Tick è il nome inglese dei trattini che indicano ore e minuti sull’orologio) conterrà il disegno dei marcatori di ore e minuti. HandsCanvas (Hand vuol dire lancetta) contiene le lancette dell’orologio.

Perché usare 2 Canvas? Perché la struttura dell’orologio, quindi i marcatori possono essere disegnati solo una volta, e le lancette le aggiorneremo ad ogni secondo.

namespace Dnw.AnalogClock.Controls
{
    public partial class AClockControl : UserControl, INotifyPropertyChanged
    {
....
    }
}

la parte di codice relativa alla classe, è molto semplice, si tratta di uno User Control, ed implementiamo anche INotifyPropertyChanged per le funzioni relative al binding dei controlli all’interno del codice.

private const double PI = 3.141592654F;

La costante Pigreco, che usiamo per i calcoli geometrici.

private DispatcherTimer mClockTimer;

Il timer che ci permetterà di disegnare l’orologio ad ogni secondo.

public AClockControl()
{
    InitializeComponent();
    this.DataContext = this;
 
}

Il costruttore della classe, che indica al controllo che il suo data context è se stesso quindi il binding viene fatto sulle property della classe User Control.

public const string FLD_Clock = "Clock";
private AClockData mClock;
public AClockData Clock
{
    get
    {
        return mClock;
    }
    set
    {
        mClock = value;
        OnPropertyChanged(FLD_Clock);
    }
}

La property Clock, che contiene tutti i dati necessari a visualizzare un orologio.

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

La funzione StartClock, deve essere chiamata dal controllo contenitore del nostro orologio, e disegna l’orologio e fa partire il timer che lo aggiornerà ad ogni secondo che passa.

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

Il metodo che allo scoccare del timer disegna l’orologio.

private void DrawClock()
{
 
    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();
    }
    Clock.DateAndTime = DateTime.Now;
 
    HandsCanvas.Children.Clear();
 
    //Hour
    DrawLine(Clock.CenterX, Clock.CenterY, Clock.HourHand.EndPointX,
        Clock.HourHand.EndPointY, Clock.HourHand.HandColor,
        Clock.HourHand.HandThickness, HandsCanvas);
 
    //minute
    DrawLine(Clock.CenterX, Clock.CenterY, Clock.MinuteHand.EndPointX,
        Clock.MinuteHand.EndPointY, Clock.MinuteHand.HandColor,
        Clock.MinuteHand.HandThickness, HandsCanvas);
 
    //Second
    DrawLine(Clock.CenterX, Clock.CenterY, Clock.SecondsHand.EndPointX,
        Clock.SecondsHand.EndPointY, Clock.SecondsHand.HandColor,
        Clock.SecondsHand.HandThickness, HandsCanvas);
}

Il metodo che disegna fisicamente le lancette dell’orologio, come vedete, utilizziamo una serie di Properties che abbiamo definito all’interno della struttura dati dell’orologio. Per prima cosa, aggiorniamo l’altezza e la larghezza dell’orologio in base alle dimensioni della finestra, in modo che se c’è un resize, l’orologio viene ridimensionato al primo aggiornamento. Poi aggiorniamo il valore dell’orologio con quello dell’orologio di sistema. Infine, eliminiamo le lancette attuali e creiamo tre nuove lancette con la nuova posizione. Potreste obiettare che la cosa non è ottimizzata, vero, ma come sempre, prima facciamolo funzionare, poi possiamo sempre vedere se c’è modo di renderlo più performante, più bello ecc. ecc.

private void DrawLine(double x1, double y1, double x2, double y2,
            SolidColorBrush color, double thickness, Canvas destinationCanvas)
{
    Line line = new Line()
    {
        X1 = x1, Y1 = y1, X2 = x2, Y2 = y2,
        StrokeThickness = thickness,
        Stroke = color
    };
 
    destinationCanvas.Children.Add(line);
}

Il metodo che disegna le lancette, ovvero le linee che le rappresentano, come potete vedere, disegna una linea con la posizione, la dimensione, il colore che abbiamo deciso di assegnargli.

private void DrawTicks()
{
    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.25 * System.Math.Sin(i * 6 * PI / 180)),
            Clock.CenterY - (Clock.ClockRadius / 1.25 * System.Math.Cos(i * 6 * PI / 180)),
            Clock.TickColor, Clock.TickThickness, TickCanvas);
        }
        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);
        }
    }
}

Questo è il metodo che disegna i trattini che indicano ore e minuti sul nostro quadrante, sono 60 linee di cui, una ogni 5 è più lunga a rappresentare l’ora. le linee utilizzano il Raggio del cerchio dell’orologio e il punto centrale (CenterX e CenterY) per calcolare il punto ove inizia e finisce la linea. I valori utilizzati per il calcolo sono del tutto arbitrari, pertanto per allungare le linee potete provare a giocare con il valore decimale indicato. potete provare a variare il divisore per allungare o accorciare le lunghezze dei trattini dei dati.

Abbiamo creato lo User control, ora vediamo come sono fatte le classi che forniscono allo User control tutti i dati necessari a gestire l’orologio.

HandData.cs – la lancetta

Visto che l’orologio ha 3 lancette e che per disegnare una lancetta ci servono parecchie property, ho deciso di creare una classe in modo tale che poi nell’orologio instanzierò tutte quelle che mi servono.

namespace Dnw.AnalogClock.Entities
{
    public class HandData : INotifyPropertyChanged    
    {
....
    }
}

La classe per le lancette, è un’entity, ovvero una classe che contiene dei dati, implementiamo INotifyPropertyChanged, anche se in realtà al momento non la utilizziamo in Binding, perché in futuro, potremo modificare l’orologio in modo da poter utilizzare al meglio MVVM.

public HandData(AClockData parent)
{
    Parent = parent;
}

Il costruttore, molto semplice, che però richiede un parametro specifico, ovvero la classe Orologio che contiene la lancetta. L’ho inserita perché molte delle property che compongono una lancetta dipendono dai dati impostati nell’intero orologio vedremo subito quali.

public AClockData Parent
{
    get
    {
        return mParent;
    }
    private set
    {
        mParent = value;
        OnPropertyChanged(FLD_Parent);
    }
}

La property Parent definisce la classe contenente i dati dell’orologio di cui la lancetta fa parte.

public SolidColorBrush HandColor
{
    get
    {
        return mHandColor;
    }
    set
    {
        mHandColor = value;
        OnPropertyChanged(FLD_HandColor);
    }
}

Questa property ospita il pennello che disegna la lancetta, che verrà creato del colore da noi desiderato al momento della generazione dell’orologio.

public double LengthMultiplier
{
    get
    {
        return mLengthMultiplier;
    }
    set
    {
        mLengthMultiplier = value;
        OnPropertyChanged(FLD_LengthMultiplier);
    }
}

Questa property conterrà un valore arbitrario che indicato quando l’orologio viene generato indica il valore che sarà utilizzato come moltiplicatore per stabilire la lunghezza della lancetta.

public double ThicknessDivisor
{
    get
    {
        return mThicknessDivisor;
    }
    set
    {
        mThicknessDivisor = value;
        OnPropertyChanged(FLD_ThicknessDivisor);
    }
}

Questa property conterrà un valore arbitrario, che indicato quando l’orologio viene generato indica il valore che sarà utilizzato come divisore nella formula che stabilisce lo spessore della lancetta.

Se vi chiedete il motivo di moltiplicatori e divisori invece che una semplice lunghezza e spessore, li abbiamo utilizzati per essere in grado di ridimensionare le lancette in modo proporzionale alla dimensione della finestra o del contenitore in generale che contiene lo user control.

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

Le property che rappresentano il punto finale corrente della lancetta quindi (x2, y2) sul disegno da noi mostrato in precedenza. Sono valori calcolati sulla base della posizione del centro dell’orologio, sulla lunghezza della lancetta utilizzando il valore dell’angolo della lancetta in radianti e le funzioni matematiche di Seno e Coseno.

public double HandLength
{
    get
    {
        if (Parent == null) return 0;
        return Parent.ClockDimension / 2 * LengthMultiplier;
    }
}

La lunghezza della lancetta, che dipende dalla dimensione dell’orologio e dal moltiplicatore arbitrario definito alla creazione dell’orologio stesso.

public double HandThickness
{
    get
    {
        if (Parent == null) return 0.0;
        return Parent.ClockDimension / ThicknessDivisor;
    }
}

Lo spessore della lancetta, calcolato in base alla dimensione dell’orologio e al divisore impostato arbitrariamente alla creazione dell’orologio.

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

Il valore del tempo segnato dalla lancetta, quindi ora, minuto o secondo in base alla lancetta rappresentata.

public bool IsHour
{
    get
    {
        return mIsHour;
    }
    set
    {
        mIsHour = value;
        OnPropertyChanged(FLD_IsHour);
    }
}

Valore che va impostato a true per la lancetta delle ore, perché la sua posizione è ovviamente calcolata in modo diverso rispetto alle altre.

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

Il valore dell’angolo espresso in radianti, che serve a calcolare la posizione della lancetta, è privato perché non ci serve all’esterno della classe, e come vediamo è diverso in base al fatto che sia una lancetta delle ore o di minuti o secondi.

AClockData.cs – L’orologio

La classe che rappresenta l’orologio, sarà generata e configurata in modo arbitrario per essere visualizzata come parte di una finestra.

namespace Dnw.AnalogClock.Entities
{
    public class AClockData : INotifyPropertyChanged
    {
....
    }
}

Come la lancetta anche la classe orologio analogico è una classe entity, ovvero contiene tutte le informazioni necessarie a disegnare un orologio analogico. Anche in questo caso abbiamo implementato l’interfaccia INotifyPropertyChanged perchè l’orologio è in Binding ai controlli dello User Control e perché in seguito potremo migliorare l’applicazione e utilizzare molto più di ora, le caratteristiche di MVVM.

public AClockData()
{
    HourHand = new HandData(this);
    HourHand.IsHour = true;
    MinuteHand = new HandData(this);
    SecondsHand = new HandData(this);
    CenterX = 0;
    CenterY = 0;
}

Il costruttore dell’orologio genera le lancette indicando qual’è quella delle ore e inizializza il punto centrale dell’orologio.

public const string FLD_BackColor = "BackColor";
private SolidColorBrush mBackColor;
public SolidColorBrush BackColor
{
    get
    {
        return mBackColor;
    }
    set
    {
        mBackColor = value;
        OnPropertyChanged(FLD_BackColor);
    }
}

La property che rappresenta il pennello con cui sarà disegnato lo sfondo dell’orologio.

public const string FLD_CenterX = "CenterX";
private double mCenterX;
public double CenterX
{
    get
    {
        return mCenterX;
    }
    private set
    {
        mCenterX = value;
        OnPropertyChanged(FLD_CenterX);
    }
}

public const string FLD_CenterY = "CenterY";
private double mCenterY;
public double CenterY
{
    get
    {
        return mCenterY;
    }
    private set
    {
        mCenterY = value;
        OnPropertyChanged(FLD_CenterY);
    }
}

Le property che rappresentano il centro dell’orologio, considerato che valgono sempre zero, potremmo usare due costanti, ma se in futuro ci servisse poterle modificare, da qui possiamo farlo.

public const string FLD_ClockDimension = "ClockDimension";
private double mClockDimension;
public double ClockDimension
{
    get
    {
        return mClockDimension;
    }
    set
    {
        mClockDimension = value;
        OnPropertyChanged(FLD_ClockDimension);
        OnPropertyChanged(FLD_TickThickness);
    }
}

Una property fondamentale, in realtà non rappresenta proprio la dimensione dell’orologio, ma la dimensione più ampia presa fra larghezza e altezza dello User Control dell’orologio. Da questa dimensione verranno derivate molte misure relative a come l’orologio è costruito.

public double ClockRadius
{
    get
    {
        return ClockDimension * 0.5;
    }
}

Il raggio del cerchio dell’orologio, in questo caso la metà della dimensione massima dell’orologio.

private DateTime mDateAndTime;
public DateTime DateAndTime
{
    get
    {
        return mDateAndTime;
    }
    set
    {
        mDateAndTime = value;
        HourHand.HandTime = mDateAndTime.Hour;
        MinuteHand.HandTime = mDateAndTime.Minute;
        SecondsHand.HandTime = mDateAndTime.Second;
    }
}

La property ove verrà aggiornata la data e l’ora da visualizzare nell’orologio analogico.

public const string FLD_HourHand = "HourHand";
private HandData mHourHand;
public HandData HourHand
{
    get
    {
        return mHourHand;
    }
    set
    {
        mHourHand = value;
        OnPropertyChanged(FLD_HourHand);
    }
}
public const string FLD_MinuteHand = "MinuteHand";
private HandData mMinuteHand;
public HandData MinuteHand
{
    get
    {
        return mMinuteHand;
    }
    set
    {
        mMinuteHand = value;
        OnPropertyChanged(FLD_MinuteHand);
    }
}
public const string FLD_SecondsHand = "SecondsHand";
private HandData mSecondsHand;
public HandData SecondsHand
{
    get
    {
        return mSecondsHand;
    }
    set
    {
        mSecondsHand = value;
        OnPropertyChanged(FLD_SecondsHand);
    }
}

Le tre lancette di ora, minuti e secondi

public SolidColorBrush TickColor
{
    get
    {
        return mTickColor;
    }
    set
    {
        mTickColor = value;
        OnPropertyChanged(FLD_TickColor);
    }
}

Il colore della rappresentazione di ore e minuti dell’orologio.

public double TickThicknessDivisor
{
    get
    {
        return mTickThicknessDivisor;
    }
    set
    {
        mTickThicknessDivisor = value;
        OnPropertyChanged(FLD_TickThicknessDivisor);
        OnPropertyChanged(FLD_TickThickness);
    }
}

Un numero arbitrario che viene utilizzato per decidere quanto sono spesse le linee che disegnano l’orologio.

public double TickThickness
{
    get
    {
        return ClockDimension / TickThicknessDivisor;
    }
}

Lo spessore delle linee che disegnano l’orologio.

Abbiamo così terminato di generare le classi che servono per generare e gestire il nostro orologio analogico, quindi vediamo come utilizzarlo.

MainWindow.xaml – MainWindow.xaml.cs

Ci spostiamo sul progetto TheClock, e per poter utilizzare la libreria contenente lo User Control per prima cosa referenziamo il progetto.

Tasto destro su References nel progetto TheClock e selezioniamo Projects, apparirà un solo progetto, ovvero la libreria AnalogClock.

Selezioniamo la libreria e diamo OK.

Il riferimento alla libreria sarà aggiunto alle references del progetto, così potremo utilizzare il suo contenuto.

<Window x:Class="TheClock.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:gctl="clr-namespace:Dnw.AnalogClock.Controls;assembly=Dnw.AnalogClock"
        mc:Ignorable="d"
        Title="MainWindow" Height="500" Width="500" Loaded="MainWindow_Loaded">
    <Grid >
        <gctl:AClockControl Name="ClockControl" 
            HorizontalAlignment="Stretch"
            VerticalAlignment="Stretch"/>
    </Grid>
</Window>

Il codice XAML della nostra finestra è molto semplice, ci sono 3 cose soltanto che abbiamo aggiunto al codice standard:

  1. Un riferimento alla libreria del controllo AnalogClock, ricordo a tutti se non lo avessimo specificato, che queste linee di codice sono identiche agli using nel file Csharp. Questa linea di codice xaml ci permette di inserire il tag che definisce il nostro user control.
            xmlns:gctl="clr-namespace:Dnw.AnalogClock.Controls;assembly=Dnw.AnalogClock"
    

  2. Un event handler per l’evento Loaded della window, in cui andremo a inizializzare ed avviare il nostro orologio.
  3. Lo User control all’interno della grid standard della finestra, con l’indicazione esplicita di occupare tutto lo spazio disponibile.

public MainWindow()
{
    InitializeComponent();
    this.Icon = BitmapFrame.Create(new Uri("pack://application:,,,/btn884.ico", UriKind.RelativeOrAbsolute));
    WindowStartupLocation = WindowStartupLocation.CenterScreen;
 
}

Nel costruttore della nostra MainWindow facciamo due cose importanti, la prima è assegnare un icona a noi gradita per la finestra, la seconda è indicare alla MainWindow di aprirsi al centro dello schermo corrente, cosa significa questo, che se lavoriamo con 2 diversi schermi, l’orologio si aprirà al centro dello schermo su cui è posizionato il mouse, così lo vedremo subito.

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

L’ultima parte del progetto, è l’event handler del caricamento della window, dove andiamo a creare l’orologio, e andiamo a configurare tutti i dati arbitrari per la rappresentazione del nostro orologio. questo è il punto dove potrete giocare un poco per trovare la forma più adatta al vostro gusto. Questi, saranno anche i parametri che metteremo a disposizione degli utenti nel multiclock per far loro configurare gli orologi analogici come preferiscono.

Riepilogo

Creando questa nuova applicazione, abbiamo spiegato le seguenti cose:

  • Come generare una libreria di User Control WPF
  • Come utilizzare i controlli Line e Canvas per disegnare un orologio analogico.
  • Un po’ di geometria piana.
  • Come collegare la dimensione di uno user control e del suo contenuto alla dimensione della finestra che lo contiene.

Una Nota:

Se osservate il vostro orologio mentre funziona, vi accorgerete che ogni tanto, la lancetta salta avanti di 2 secondi, qual’è il motivo per cui accade? Semplicemente perché non siamo in grado di far scattare il timer esattamente quando l’orologio fa scattare un nuovo secondo, pertanto, visto che visualizziamo sempre l’ora del computer e non semplicemente il valore arbitrario dell’ora basato sull’ora di partenza del timer, a volte nell’intervallo in cui il timer scatta, sono passati 2 secondi. Potete modificare il comportamento a vostro piacimento, nel mio caso, io manterrò questa modalità di funzionamento.

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.