Press "Enter" to skip to content

8 – MultiClock – Aggiungere la TimeZone ed il controllo esterno all’orologio Analogico

In questa terza parte della costruzione dell’orologio analogico, vogliamo predisporre l’orologio per essere compatibile con l’applicazione MultiClock, per farlo dobbiamo fare in modo che sia pilotabile dall’esterno, infatti non sarebbe molto utile da parte nostra pilotare i due orologi, Analogico e Digitale con due diversi timer, anche se possibile non è a mio avviso il modo migliore di procedere. Pertanto, aggiungeremo le seguenti funzionalità:

  • Una property per poter attivare o disattivare il timer del controllo orologio analogico.
  • Un metodo per aggiornare dall’esterno l’orologio.
  • Aggiungeremo anche la TimeZone all’orologio.
  • Aggiungeremo inoltre la possibilità di modificare i numeri delle ore dell’orologio.

Qual’è lo scopo di tutte queste modifiche? Essendo una serie destinata ai principianti, serve a darvi delle idee, quando create un applicazione, o un componente, è importante non fermarsi al semplice “Basta che funzioni”, ma adottare alcuni accorgimenti nella progettazione ci permetterà di rendere più facili ulteriori implementazioni nel futuro. Per accontentare le richieste dei nostri clienti/utenti senza dover rifare l’applicazione ad ogni richiesta.

Prima di iniziare cambiamo un comportamento

Attualmente, il nostro orologio, ha un comportamento che potremo definire “A scatti” infatti, la property che rappresenta la data e aggiorna le lancette ha il seguente codice:

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

Visto che Hour, Minute e Second sono degli interi, questo significa che la lancetta dei secondi, dei minuti e delle ore, si spostano a scatto pertanto ad esempio alle 9 di sera, dalle 9:00 alle 9:59 la lancetta delle ore sarà posta in perpendicolare al numero 9, alle 10:00 si sposterà di 5 tacche fino ad essere perpendicolare alla linea delle 10 dove resterà fino alle 11 e così via. Mentre per i minuti e i secondi il comportamento potrebbe essere anche accettabile, per le ore il colpo d’occhio non è naturale, pertanto, lo correggiamo in modo molto semplice:

public DateTime DateAndTime
{
    get
    {
        return mDateAndTime;
    }
    set
    {
        mDateAndTime = value;
        HourHand.HandTime = mDateAndTime.Hour+((double)mDateAndTime.Minute)/60;
        MinuteHand.HandTime = mDateAndTime.Minute;
        SecondsHand.HandTime = mDateAndTime.Second;
    }
}

Per far spostare correttamente l’ora anche dei “decimali” se così possiamo definirli, aggiungiamo al suo valore, il valore dei minuti diviso per sessanta dopo averlo convertito in un numero a virgola mobile, altrimenti dividendo per sessanta il valore sarebbe sempre zero.

Se vi può piacere, potete fare lo stesso per i minuti e i secondi e aumentare la frequenza del timer per vedere come si nuove la lancetta dei secondi.

Correggiamo un Bug e miglioriamo la forma del quadrante del nostro orologio

Come qualcuno mi ha fatto notare, ho fatto un errore piuttosto grossolano nella creazione del quadrante numerato dell’orologio, infatti se osservate l’immagine qui sotto, vi accorgerete che i numeri delle ore sono spostati indietro di un ora, pertanto a mezzogiorno (o mezzanotte) abbiamo messo il numero 1.

analog_clock_whours_02[4]

E’ un errore semplice da correggere, ma visto che lo facciamo ne approfittiamo per migliorare il nostro quadrante, permettendo a chi usa il nostro componente di avere 5 diversi tipi di quadrante.

  1. Numerico con tutte le ore
  2. Numerico con le sole 4 ore primarie (12, 3, 6, 9)
  3. A numeri romani con tutte le ore
  4. A numeri romani con le sole 4 ore primarie.
  5. Senza numeri

Oltre a ciò, aggiungiamo le property e modifichiamo i metodi per ottenere le seguenti funzionalità.

  • Attivare o disattivare il timer del controllo
  • Esporre un campo ove indicare il fuso orario
  • Aggiornare l’orologio dall’esterno

Modifiche a AClockControl.cs

Per effettuare le modifiche e gli aggiornamenti che abbiamo deciso, non dobbiamo modificare nulla nella parte XAML del controllo orologio, modifichiamo solo il codice.

public void DrawTicks()
{
    if (Clock == null) return;
    string[] hours = null;
    switch (Clock.NumberType)
    {
        case ClockNumberTypes.Arab12Numbers:
            hours = new string[] { "12", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11" };
            break;
 
        case ClockNumberTypes.Arab4Numbers:
            hours = new string[] { "12", "", "", "3", "", "", "6", "", "", "9", "", "" };
            break;
 
        case ClockNumberTypes.Roman12Numbers:
            hours = new string[] { "XII", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI" };
            break;
 
        case ClockNumberTypes.Roman4Numbers:
            hours = new string[] { "XII", "", "", "III", "", "", "VI", "", "", "IX", "", "" };
            break;
        default:
            hours = new string[] { "", "", "", "", "", "", "", "", "", ", "", "" };
            break;
    }
    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);
            if (hours[hourIdx].Length > 0)
            {
                //If the numbering has only the 4 primary hours or has no hours we don't draw empty strings
                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);
        }
    }
}

La correzione alla serie dei numeri per creare il quadrante con i numeri giusti è semplice, basta spostare il 12 dall’ultimo al primo posto nell’array delle cifre.

Per permettere di selezionare il tipo di numerazione per l’orologio abbiamo creato una enumerazione, che trovate nel file ClockNumberTypes.cs.

public enum ClockNumberTypes
{
    NoNumbers,
    Arab12Numbers,
    Roman12Numbers,
    Arab4Numbers,
    Roman4Numbers
}

Questa enumerazione è usata nella generazione del controllo orologio analogico per stabilire come disegnare le ore. Se osservate il codice, abbiamo anche modificato il metodo che disegna i numeri in modo che se la stringa è vuota non disegni nulla (miglioriamo le prestazioni ed evitiamo di inserire controlli inutili nella window).

Per permettere di pilotare dall’esterno il valore dell’orologio analogico, in modo da permettere di agganciare l’orologio analogico a una gestione effettuata in altro luogo, modifichiamo il metodo UpdateClock in modo tale che possa essere usata sia dal timer interno che chiamata dall’esterno.

public void UpdateClock(DateTime? clockTime = null)
{
    CheckClockDimension();
    if (clockTime.HasValue)
    {
        Clock.DateAndTime = clockTime.Value;
    }
    else
    {
        Clock.DateAndTime = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, Clock.TimeZone);
    }
}

In questo caso, se chiamato senza passare alcun dato, il metodo aggiorna l’orologio prendendo il valore dall’orologio di sistema e applicando l’eventuale fuso orario se diverso da quello locale. Se invece è chiamato passando un valore per l’orologio, visualizza l’ora richiesta.

Modifiche a AClockData.cs

La struttura dati che costruisce l’orologio invece viene modificata per implementare le nuove funzionalità, ecco come.

public const string FLD_ActivateTimer = "ActivateTimer";
private bool mActivateTimer;
public bool ActivateTimer
{
    get
    {
        return mActivateTimer;
    }
    set
    {
        mActivateTimer = value;
        OnPropertyChanged(FLD_ActivateTimer);
    }
}

Aggiungiamo la property per gestire se utilizzare o meno il timer del controllo.

public const string FLD_TimeZone = "TimeZone";
private TimeZoneInfo mTimeZone;
public TimeZoneInfo TimeZone
{
    get
    {
        return mTimeZone;
    }
    set
    {
        mTimeZone = value;
        if (value != null)
        {
            mTimeZoneName = mTimeZone.StandardName;
        }
        OnPropertyChanged(FLD_TimeZone);
        OnPropertyChanged(FLD_TimeZoneName);
    }
}

public const string FLD_TimeZoneName = "TimeZoneName";
private string mTimeZoneName;
public string TimeZoneName
{
    get
    {
        return mTimeZoneName;
    }
    set
    {
        mTimeZoneName = value;
        TimeZone = TimeZoneInfo.GetSystemTimeZones().FirstOrDefault(x => x.StandardName == value);
        if (TimeZone == null)
        {
            TimeZone = TimeZoneInfo.Utc;
            mTimeZoneName = TimeZone.StandardName;
        }
        OnPropertyChanged(FLD_TimeZoneName);
    }
}

Aggiungiamo inoltre le due variabili che ci permettono di gestire il fuso orario del nostro orologio copiandole ed incollandole dalla struttura dati dell’orologio digitale usato negli articoli precedenti.

Modifiche a MainWindow.xaml – MainWindow.xaml.cs

Per testare le modifiche al nostro componente aggiornato, dobbiamo aggiungere alcuni componenti alla main window, esattamente sono 3

  1. Una Combobox con la lista delle Time Zones (i fusi orari) che permetta di regolare l’orologio sull’ora di qualsiasi parte del mondo.
  2. Una Combobox con la lista dei tipi di numerazione dell’orologio che possiamo assegnare.
  3. Una Checkbox, per poter attivare l’assegnazione del valore dell’orologio dall’esterno del controllo.

<Grid.RowDefinitions>
     <RowDefinition Height="*"/>
     <RowDefinition Height="Auto"/>
     <RowDefinition Height="Auto"/>
     <RowDefinition Height="Auto"/>
</Grid.RowDefinitions>

Per prima cosa, aggiungiamo la definizione delle righe alla Grid che contiene il nostro orologio analogico.

<gctl:AClockControl 
    Grid.Row="0"
    Name="ClockControl" 
    HorizontalAlignment="Stretch"
    VerticalAlignment="Stretch"/>

Inseriamo lo user control nella prima riga, in modo che occupi tutto lo spazio libero.

 <ComboBox
     Grid.Row="1"
     Margin="4,2,4,2"
     HorizontalAlignment="Stretch"
     VerticalAlignment="Center"
     SelectedItem="{Binding SelectedTimeZone, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
     ItemsSource="{Binding TimeZones}">
     <ComboBox.ItemTemplate>
          <DataTemplate>
              <Grid>
                  <Grid.ColumnDefinitions>
                      <ColumnDefinition Width="Auto"/>
                      <ColumnDefinition Width="Auto"/>
                      <ColumnDefinition Width="Auto"/>
                   </Grid.ColumnDefinitions>
                   <TextBlock
                        Grid.Column="0"
                        Margin="4,2,4,2"
                        HorizontalAlignment="Left"
                        VerticalAlignment="Center"
                        TextAlignment="Left"
                        MinWidth="120"
                        Text="{Binding StandardName}"/>
                   <TextBlock
                        Grid.Column="1"
                        Margin="4,2,4,2"
                        HorizontalAlignment="Left"
                        VerticalAlignment="Center"
                        TextAlignment="Left"
                        MinWidth="120"
                        Text="{Binding DisplayName}"/>
                   <TextBlock
                        Grid.Column="1"
                        Margin="4,2,4,2"
                        HorizontalAlignment="Right"
                        VerticalAlignment="Center"
                        TextAlignment="Left"
                        MinWidth="30"
                        Text="{Binding DST}"/>
                   </Grid>
         </DataTemplate>
     </ComboBox.ItemTemplate>
</ComboBox>

Generiamo la combobox per le Time Zones, scopiazziamo dalla Window SetClocksWindow.xaml del MultiClock, una combobox per selezionare i fusi orari visualizzando tutti i dati significativi. Questa combobox è posizionata a riga 1 della grid principale.

 <ComboBox
        Grid.Row="2"             
        Margin="4,2,4,2"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Center"
        SelectedItem="{Binding SelectedNumberingType, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"                
        ItemsSource="{Binding NumberingTypes}">
</ComboBox>

Generiamo la combobox per selezionare il tipo di numerazione che vogliamo vedere sull’orologio, vedremo poi nel codice come abbiamo popolato la variabile NumberingTypes nel codice della finestra.

<CheckBox
    Grid.Row="3"
    Margin="4,2,4,2"
    HorizontalAlignment="Left"
    VerticalAlignment="Center"
    IsChecked="{Binding ActivateClockTimer, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}">
        <TextBlock 
        Margin="4,2,4,2"
        Text="Activate User control timer"/>
</CheckBox>

Infine, aggiungiamo la checkbox che potremo usare per attivare o disattivare il timer del controllo, verificandone il funzionamento quando pilotato dall’esterno.

Vediamo ora le modifiche al codice della nostra finestra principale.

private bool mActivateClockTimer;
public bool ActivateClockTimer
{
    get
    {
        return mActivateClockTimer;
    }
    set
    {
        mActivateClockTimer = value;
        UpdateClockTimerManagement();
        OnPropertyChanged(FLD_ActivateClockTimer);
    }
}

Aggiungiamo la property per attivare o disattivare il timer.

public const string FLD_NumberingTypes = "NumberingTypes";
private ObservableCollection<ClockNumberTypes> mNumberingTypes;
public ObservableCollection<ClockNumberTypes> NumberingTypes
{
    get
    {
        return mNumberingTypes;
    }
    set
    {
        mNumberingTypes = value;
        OnPropertyChanged(FLD_NumberingTypes);
    }
}

Aggiungiamo la property per contenere la lista dei tipi di numerazione dell’orologio.

public ClockNumberTypes SelectedNumberingType
{
    get
    {
        return mClock.NumberType;
    }
    set
    {
        mClock.NumberType = value;
        ClockControl.DrawTicks();
        OnPropertyChanged(FLD_SelectedNumberingType);
    }
}

Aggiungiamo la property che mappa, rendendola visibile in modo semplice, la property del tipo di numerazione selezionata.

public const string FLD_TimeZones = "TimeZones";
private ObservableCollection<TimeZoneInfo> mTimeZones;
public ObservableCollection<TimeZoneInfo> TimeZones
{
    get
    {
        return mTimeZones;
    }
    set
    {
        mTimeZones = value;
        OnPropertyChanged(FLD_TimeZones);
    }
}

Aggiungiamo la property in cui sarà memorizzata la lista di tutti i possibili fusi orari del sistema.

public TimeZoneInfo SelectedTimeZone
{
    get
    {
        return mClock.TimeZone;
    }
    set
    {
        mClock.TimeZone = value;
        OnPropertyChanged(FLD_SelectedTimeZone);
    }
}

Aggiungiamo la property che mappa in modo semplice il fuso orario dei dati dell’orologio.

private DispatcherTimer mClockTimer;
Aggiungiamo il timer che permetterà di pilotare l'orologio dall'esterno.
public MainWindow()
{
    InitializeComponent();
    this.Icon = BitmapFrame.Create(new Uri("pack://application:,,,/btn884.ico", UriKind.RelativeOrAbsolute));
    WindowStartupLocation = WindowStartupLocation.CenterScreen;
    DataContext = this;
    TimeZones = new ObservableCollection<TimeZoneInfo>();
    NumberingTypes = new ObservableCollection<ClockNumberTypes>();
    mClock = new AClockData();
    this.mClockTimer = new DispatcherTimer();
    this.mClockTimer.Interval = TimeSpan.FromMilliseconds(1000);
    this.mClockTimer.Tick += ClockTimer_Tick;
    ActivateClockTimer = true;
 
    SelectedTimeZone = TimeZoneInfo.Utc;
    SelectedNumberingType = ClockNumberTypes.Arab12Numbers;
}

Modifichiamo il costruttore della window, in modo da inizializzare le nuove property ed il timer.

private void ClockTimer_Tick(object sender, EventArgs e)
{
    ClockControl.UpdateClock(TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, SelectedTimeZone));
}

Aggiungiamo l’aggiornamento del timer dell’orologio se è stato scelto di non usare il timer interno.

private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
    var timeZones = TimeZoneInfo.GetSystemTimeZones();
    foreach (TimeZoneInfo tzi in timeZones)
    {
        TimeZones.Add(tzi);
    }
 
    var clocknumbers = Enum.GetValues(typeof(ClockNumberTypes));
    foreach (ClockNumberTypes cnt in clocknumbers)
    {
        NumberingTypes.Add(cnt);
    }
 
    mClock.BackColor = new SolidColorBrush(Colors.Orange);
    mClock.TickColor = new SolidColorBrush(Colors.Navy);
    mClock.TickThicknessDivisor = 130;
    mClock.NumbersColor = new SolidColorBrush(Colors.SteelBlue);
    mClock.NumbersFontFamily = new FontFamily("Calibri");
    mClock.NumbersSize = 24;
 
    mClock.HourHand.HandColor = new SolidColorBrush(Colors.Black);
    mClock.HourHand.LengthMultiplier = 0.48F;
    mClock.HourHand.ThicknessDivisor = 100;
 
    mClock.MinuteHand.HandColor = new SolidColorBrush(Colors.DarkGray);
    mClock.MinuteHand.LengthMultiplier = 0.58F;
    mClock.MinuteHand.ThicknessDivisor = 150;
 
    mClock.SecondsHand.HandColor = new SolidColorBrush(Colors.White);
    mClock.SecondsHand.LengthMultiplier = 0.68F;
    mClock.SecondsHand.ThicknessDivisor = 200;
 
    ClockControl.StartClock(mClock);
}

Modifichiamo il metodo di caricamento della window in modo da aggiornare le combobox con le liste necessarie e predisporre i dati delle lancette per far partire l’orologio.

private void UpdateClockTimerManagement()
{
    mClock.ActivateTimer = ActivateClockTimer;
    ClockControl.CheckActivateTimer();
    if (ActivateClockTimer)
    {
        this.mClockTimer.Stop();
    }
    else
    {
        mClock.ActivateTimer = false;
    }
}

Implementiamo il metodo che ci permetterà di modificare il comportamento dell’orologio quando variano i parametri di istruzione.

Abbiamo implementato tutte le modifiche necessarie, e adesso possiamo testare il funzionamento del nostro orologio.

analogClockDemo_01

analogClockDemo_02

analogClockDemo_03

analogClockDemo_04

Abbiamo preso quattro screenshot che mostrano quattro possibili configurazioni selezionate grazie ai controlli inseriti nell’interfaccia. Un breve video demo può essere scaricato dal link a piè pagina.

Riepilogo

Vediamo cosa abbiamo spiegato in questo nuovo articolo:

  • Abbiamo corretto il bug nella visualizzazione delle ore sul quadrante
  • Abbiamo aggiunto la possibilità di configurare come le ore sono visualizzate
  • Abbiamo aggiunto la possibilità di controllare l’orologio dall’esterno
  • Abbiamo aggiunto la possibilità di visualizzare un fuso orario diverso dall’ora locale

Adesso potremo utilizzare il nostro componente per modificare il MultiClock.

Potete scaricare il progetto esempio dal link qui indicato:

Potete scaricare un mini video che mostra una demo del progetto dell’orologio e come può essere configurato a runtime.

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