Press "Enter" to skip to content

4 – MultiClock – Aggiungere la possibilità di configurare i colori di ogni orologio

Una nuova richiesta dagli utenti della nostra applicazione, che hanno riferito che poter avere orologi di colore diverso per ogni diverso fuso orario sarebbe una cosa utile a chi li guarda per focalizzarsi subito sull’orologio necessario.

Pertanto, per poterlo realizzare abbiamo bisogno di fare alcune modifiche in vari punti del nostro progetto MultiClock, in modo da ottenere quanto visibile nell’immagine qui sotto.

colored_multiclock

Per poter permettere di cambiare il colore degli orologi in modo indipendente l’uno dall’altro, dobbiamo effettuare le seguenti modifiche alla nostra applicazione:

  1. Aggiungere le property per ospitare il valore dei colori a ClockInfo.
  2. Per fare in modo che la finestra principale assuma il colore di uno specifico orologio, che chiameremo PrimaryClock aggiungeremo alla classe ClockInfo una property boolean IsPrimaryClock
  3. Modificheremo l’AppContext in modo che possa restituire i due colori del PrimaryClock così che la Main Window possa usarli.
  4. Modificheremo MainWindow in modo che i colori dello sfondo e del bordo della finestra siano in Binding con i colori del Primary Clock forniti dall’AppContext.
  5. Modificheremo il ClockControl in modo che utilizzi le due property da noi definite per inizializzare il colore dei controlli dell’orologio.
  6. Modificheremo la SetClocksWindow in modo che ci permetta di assegnare i colori ad ogni orologio e permetta di decidere quale orologio è il primary clock.

Direi che ci sono molte modifiche per ottenere una variazione che può sembrare così semplice agli occhi dell’utente.

Correggiamo un Bug

Mentre debuggavo le modifiche, ho notato un comportamento anomalo della finestra SetClock, dove sembrava che le modifiche agli orologi non venissero sempre registrate, dopo varie verifiche ho scoperto che mi ero dimenticata una cosa importante nel metodo che ricarica gli orologi dopo il salvataggio delle modifiche nella SetClocksWindow.

private void SetClocksWindow_ClocksChanged(object sender, EventArgs e)
{
    try
    {
        AppContext.Instance.Reset();
        BackColor = new SolidColorBrush(AppContext.Instance.PrimaryBackgroundColor);
        ForeColor = new SolidColorBrush(AppContext.Instance.PrimaryForegroundColor);
        ClocksContainer.Children.Clear();
        LoadClocks();
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
    }
}

La modifica che corregge il problema è la riga che effettua il Reset dell’AppContext. Se non lo eseguiamo, l’AppContext conterrà ancora i valori degli orologi precedenti al salvataggio, pertanto la funzione di comparazione potrebbe dare esiti inconsistenti e quindi l’attivazione del tasto salva e della verifica delle modifiche non sarebbe corretta.

Modifichiamo ClockInfo.cs

public const string FLD_BackgroundColor = "BackgroundColor";
private Color mBackgroundColor;
public Color BackgroundColor
{
    get
    {
        return mBackgroundColor;
    }
    set
    {
        mBackgroundColor = value;
        OnPropertyChanged(FLD_BackgroundColor);
    }
}

Aggiungiamo La property che ospiterà il colore di Background dell’orologio.

public const string FLD_ForegroundColor = "ForegroundColor";
private Color mForegroundColor;
public Color ForegroundColor
{
    get
    {
        return mForegroundColor;
    }
    set
    {
        mForegroundColor = value;
        OnPropertyChanged(FLD_ForegroundColor);
    }
}

Aggiungiamo la property che ospiterà il colore di Foreground dell’orologio.

public const string FLD_IsPrimaryClock = "IsPrimaryClock";
private bool mIsPrimaryClock;
public bool IsPrimaryClock
{
    get
    {
        return mIsPrimaryClock;
    }
    set
    {
        mIsPrimaryClock = value;
        OnPropertyChanged(FLD_IsPrimaryClock);
    }
}

Aggiungiamo la property che ospiterà il boolean che deciderà qual’è l’orologio primario della lista.

public ClockInfo()
{
    TimeZoneName = TimeZoneInfo.Utc.StandardName;
    BackgroundColor = (Color)ColorConverter.ConvertFromString("#FF000000");
    ForegroundColor = (Color)ColorConverter.ConvertFromString("#FFD2DF33");
}

Modifichiamo il costruttore della classe, dando ai due valori dei colori i valori di default da noi usati, quindi Nero per lo sfondo e Giallo Fastidio per i caratteri.

public int CompareTo(object obj)
{
    int comparator = -1;
 
    if (obj is ClockInfo)
    {
        ClockInfo val = (ClockInfo)obj;
        comparator = 0;
 
        comparator = CompareStrings(this.Name, val.Name);
        if (comparator != 0) return comparator;
        comparator = CompareStrings(this.TimeZoneName, val.TimeZoneName);
        if (comparator != 0) return comparator;
        comparator = this.Order.CompareTo(val.Order);
        if (comparator != 0) return comparator;
        comparator = CompareStrings(this.BackgroundColor.ToString(), val.BackgroundColor.ToString());
        if (comparator != 0) return comparator;
        comparator = CompareStrings(this.ForegroundColor.ToString(), val.ForegroundColor.ToString());
        if (comparator != 0) return comparator;
        comparator = this.IsPrimaryClock.CompareTo(val.IsPrimaryClock);
        return (comparator);
    }
 
    return (comparator);
}

Modifichiamo il metodo CompareTo della classe in modo che anche le nuove property siano utilizzate per decidere se due ClockInfo sono diversi, abbiamo anche compattato il metodo.

public void Copy(ClockInfo item)
{
    item.Name = this.Name;
    item.Order = this.Order;
    item.TimeZone = this.TimeZone;
    item.ForegroundColor = this.ForegroundColor;
    item.BackgroundColor = this.BackgroundColor;
    item.IsPrimaryClock = this.IsPrimaryClock;
}

Aggiungiamo le nuove property alla funzione Copy in modo che gli orologi siano correttamente duplicati quando forniti alla finestra di modifica.

Modifichiamo ClockControl.xaml.cs

public const string FLD_BackColor = "BackColor";
 
public SolidColorBrush BackColor
{
    get
    {
        return new SolidColorBrush( Clock.BackgroundColor);
    }
}
 
public const string FLD_ForeColor = "ForeColor";
 
public SolidColorBrush ForeColor
{
    get
    {
        return new SolidColorBrush(Clock.ForegroundColor);
    }
}

Innanzi tutto, creiamo le 2 property che forniranno il Brush del colore giusto ai controlli dell’orologio.

public ClockInfo Clock
{
    get
    {
        return mClock;
    }
    set
    {
        mClockTimer.Stop();
        mClock = value;
        mClockTimer.Start();
        OnPropertyChanged(FLD_Clock);
        OnPropertyChanged(FLD_BackColor);
        OnPropertyChanged(FLD_ForeColor);
    }
}

Modifichiamo la property Clock, in modo che quando viene modificata, automaticamente aggiorni le due property contenenti i pennelli.

Modifichiamo ClockControl.xaml

In ClockControl.xaml dovremo mettere tutte le property BackGround e Foreground dei controlli della form in Binding con le due nuove property:

        <TextBlock
            Grid.Row="0"
            Margin="0"
            Padding="4"
            HorizontalAlignment="Stretch"
            VerticalAlignment="Center"
            Background="{Binding BackColor, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
            Foreground="{Binding ForeColor, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
            FontFamily="Courier New"
            FontSize="24"
            FontStyle="Normal"
            FontWeight="Bold"
            TextAlignment="Left"
            Text="{Binding Path=Clock.Name}"/>

Tutte le property saranno simili, pertanto ne inseriamo una fra tutte, come vedete, Background e Foreground della TextBlock ora non hanno più un valore fisso, ma sono in Binding con BackColor e ForeColor.

            <TextBlock
                Grid.Column="0"
                Margin="0"
                Padding="4"
                HorizontalAlignment="Stretch"
                VerticalAlignment="Center"
                Background="{Binding ForeColor, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
                Foreground="{Binding BackColor, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
                FontFamily="Courier New"
                FontSize="16"
                FontStyle="Normal"
                FontWeight="Bold"
                TextAlignment="Right"
                Text="{Binding Clock.DST}"/>

La sola eccezione, è la Dailight Saving Time che ha i colori invertiti, pertanto saranno invertiti anche i due Binding.

Modifichiamo AppContext.cs

Per fare in modo che la MainWindow abbia accesso alle property relative al PrimaryClock, definiamo due property calcolate al suo interno:

public Color PrimaryBackgroundColor
{
    get
    {
        return ConfigData.GetPrimaryBackgorundColor();
    }
}
 
public Color PrimaryForegroundColor
{
    get
    {
        return ConfigData.GetPrimaryForegorundColor();
    }
}

Queste property andranno a chiedere ai dati di configurazione degli orologi il colore relativo al Background e al Foreground.

Modifichiamo MultiClocksConfig.cs

public ClockInfo PrimaryClock
{
    get
    {
        if (Clocks == null || Clocks.Count == 0) return null;
        ClockInfo pc = Clocks.FirstOrDefault(x => x.IsPrimaryClock);
        if (pc == null)
        {
            pc = Clocks[0]; Clocks[0].IsPrimaryClock = true;
        }
        return pc;
    }
 
}

Aggiungiamo una property calcolata che restituisce i dati di quello che è stato indicato come PrimaryClock. In questo caso, la tecnica che utilizzo è un po’ diversa da quelle usate in altri punti dell’applicazione. E’ qualcosa che ritengo opportuno fare essendo questo un progetto didattico, per mostrare che vi sono molti modi diversi per ottenere le stesse cose in una applicazione C# applicare una tecnica o una diversa, dipende dal contesto. In questo caso, l’esecuzione di un metodo ogni volta che la property è richiesta è un metodo piuttosto lento, visto quello che deve eseguire. Può essere usata solo perché sarà difficile che la collezione contenga molti elementi, visto che in totale i fusi orari sono una trentina. (sono più di 24 perché vengono chiamati in modo diverso in posti diversi e ci sono dei fusi locali con differenze di mezz’ora rispetto a quelli vicini.)

internal Color GetPrimaryForegorundColor()
{
    if( PrimaryClock == null ) return (Color)ColorConverter.ConvertFromString("#FFD2DF33");
    return (PrimaryClock.ForegroundColor);
 
 
}
 
internal Color GetPrimaryBackgorundColor()
{
    if (PrimaryClock == null) return (Color)ColorConverter.ConvertFromString("#FF000000");
    return PrimaryClock.BackgroundColor;
}

Aggiungiamo i due metodi che restituiscono il valore dei colori del Primary Clock.

Modifichiamo MainWindow.xaml.cs

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

Aggiungiamo la property che conterrà il colore di sfondo.

public const string FLD_ForeColor = "ForeColor";
private SolidColorBrush mForeColor;
public SolidColorBrush ForeColor
{
    get
    {
        return mForeColor;
    }
    set
    {
        mForeColor = value;
        OnPropertyChanged(FLD_ForeColor);
    }
}

Aggiungiamo la property che conterrà il colore dei caratteri.

private void Window_Loaded(object sender, RoutedEventArgs e)
{
    BackColor = new SolidColorBrush(AppContext.Instance.PrimaryBackgroundColor);
    ForeColor = new SolidColorBrush(AppContext.Instance.PrimaryForegroundColor);
    if (AppContext.Instance.ConfigData.Clocks.Count == 0)
    {
        var timeZones = TimeZoneInfo.GetSystemTimeZones();
        ClockInfo cli = new ClockInfo();
        cli.Name = "Local Time";
        cli.TimeZoneName = TimeZoneInfo.Local.StandardName;
        cli.Order = 0;
        Clocks.Add(cli);
 
        cli = new ClockInfo();
        cli.Name = "UTC";
        cli.TimeZoneName = TimeZoneInfo.Utc.StandardName;
        cli.Order = 1;
        Clocks.Add(cli);
 
        cli = new ClockInfo();
        cli.Name = "New York";
        TimeZoneInfo tzi = timeZones.FirstOrDefault(x => x.DisplayName.Contains("Eastern Time"));
        if (tzi != null)
        {
            cli.Order = 2;
            cli.TimeZoneName = tzi.StandardName;
            Clocks.Add(cli);
        }
 
        AppContext.Instance.ConfigData.SaveConfig();
    }
 
    LoadClocks();
}

Modifichiamo il metodo che inizializza gli orologi al load della finestra inizializzando i due colori.

private void SetClocksWindow_ClocksChanged(object sender, EventArgs e)
{
    try
    {
        AppContext.Instance.Reset();
        BackColor = new SolidColorBrush(AppContext.Instance.PrimaryBackgroundColor);
        ForeColor = new SolidColorBrush(AppContext.Instance.PrimaryForegroundColor);
        ClocksContainer.Children.Clear();
        LoadClocks();
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
    }
}

Modifichiamo il metodo che ricarica gli orologi al salvataggio degli orologi nella finestra di modifica aggiungendo il ricalcolo dei colori per la finestra principale.

Modifichiamo MainWindow.xaml

<Window x:Class="DNW.MultiClock.Windows.MainWindow"
...
        BorderBrush="{Binding ForeColor, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
        BorderThickness="4"
        Background="{Binding BackColor, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
...>

Modifichiamo il Tag Window per aggiungere il Binding alle property con i colori del primary clock.

<Grid
    Background="{Binding BackColor, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}">

Modifichiamo il controllo Grid principale della window, mettendo in Binding il Background con la property corretta.

<Button
    ToolTip="Close Clock"
    Command="{x:Static ApplicationCommands.Close}"
    Background="{Binding BackColor, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
    >
...

Facciamo lo stesso per i due Button della finestra.

<ScrollViewer
    Grid.Row="1"
    HorizontalScrollBarVisibility="Disabled"
    VerticalScrollBarVisibility="Auto"
    Background="{Binding BackColor, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}">
...

Modifichiamo anche lo Scroll Viewer che contiene gli orologi.

Modifichiamo SetClocksWindow.xaml

Per modificare la finestra di modifica degli orologi, partiamo dallo Xaml perché è quello che riceverà le modifiche maggiori, mentre il codice in realtà ha poche modifiche.

setclock_colors_01

Quello che dobbiamo ottenere è quello che vediamo qui sopra, pertanto abbiamo bisogno di due controlli ColorPicker per poter fare selezionare i colori agli utenti. Sfortunatamente, nei controlli standard di WPF non c’è il ColorPicker, che è stato aggiunto in seguito nella libreria che si chiama WPFToolkitExtended. Questa libreria, presente in Codeplex e in origine scritta da Microsoft, è stata poi acquisita da un azienda che ne rilascia ancora la versione Community Gratuita, e che ne ha anche fatto una versione a pagamento con molte feature aggiunte.

Per utilizzarla, useremo il package nuget che ci viene messo a disposizione. Vediamo come si fa a referenziare un componente distribuito con nuget, un repository di librerie che viene usato da Microsoft stessa e da molti produttori di componenti, siano questi, open source, gratuiti o a pagamento.

Vediamo come referenziare la libreria nel nostro Multiclock.

nuget_reference_01

Per prima cosa, facciamo tasto destro sulle reference di progetto e selezioniamo Manage NuGet Packages, come indicato nell’immagine precedente.

nuget_reference_02

Apparirà questa finestra, digitando nella stringa di ricerca le parole da noi inserite, quindi WPF Toolkit Extended, il primo link che ci apparirà è esattamente quello che ci serve, la versione Community di Extended Wpf Toolkit di Exceed.

nuget_reference_03

Selezioniamo e clicchiamo Install. Al termine dell’installazione verrà aperta una finestra browser sulla pagina codeplex dei componenti.

nuget_reference_04

Se osserviamo le reference ora, possiamo vedere che ci sono state aggiunte una serie di dll. Probabilmente non ci servono ne AvalonDock ne la Datagrid, quindi potremmo eliminare le references, perché il ColorPicker si trova nella libreria Toolkit. Ma per ora lasciamo tutto com’è.

<Window x:Class="DNW.MultiClock.Windows.SetClocksWindow"
        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:wr="clr-namespace:DNW.MultiClock.Windows"
        xmlns:lcmd="clr-namespace:DNW.MultiClock.Commands"
        xmlns:wpfx="http://schemas.xceed.com/wpf/xaml/toolkit"
        mc:Ignorable="d"
        Title="Set Clocks" Height="300" Width="300" Loaded="SetClocksWindow_Loaded">

La prima modifica che facciamo allo xaml della window, è aggiungere il Namespace relativo alla libreria così che possiamo utilizzare il ColorPicker.

        <Grid 
            Grid.Row="1">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>

Modifichiamo ora la grid che contiene i dettagli dell’orologio correntemente selezionato, aggiungendo 3 righe.

            <TextBlock
                Grid.Row="2"
                Grid.Column="0"
                Margin="4,2,4,2"
                HorizontalAlignment="Right"
                VerticalAlignment="Center"
                Text="Clocks Background color"/>
            <wpfx:ColorPicker 
                Grid.Row="2"
                    Grid.Column="1"
                    VerticalAlignment="Center"
                    HorizontalAlignment="Stretch"
                    Height="30" DisplayColorAndName="True" 
                    SelectedColor="{Binding SelectedClock.BackgroundColor, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                    ShowAdvancedButton="True"
                   Margin="4,2,4,2" />
            <TextBlock
                Grid.Row="3"
                Grid.Column="0"
                Margin="4,2,4,2"
                HorizontalAlignment="Right"
                VerticalAlignment="Center"
                Text="Clocks Foreground color"/>
            <wpfx:ColorPicker 
                Grid.Row="3"
                    Grid.Column="1"
                    VerticalAlignment="Center"
                    HorizontalAlignment="Stretch"
                     Height="30" DisplayColorAndName="True" 
                    SelectedColor="{Binding SelectedClock.ForegroundColor, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                    ShowAdvancedButton="True"
                   Margin="4,2,4,2" />
            <CheckBox
                Grid.Row="4"
                Margin="4,2,4,2"
                VerticalAlignment="Center"
                HorizontalAlignment="Left"
                IsChecked="{Binding SelectedClock.IsPrimaryClock, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                Checked="IsPrimaryClock_Checked"
                Unchecked="IsPrimaryClock_Unchecked"
                >
                <TextBlock
                Grid.Row="3"
                Grid.Column="0"
                Margin="4,2,4,2"
                HorizontalAlignment="Right"
                VerticalAlignment="Center"
                Text="Is primary clock"/>
            </CheckBox>

Sotto alla combobox della TimeZone, aggiungiamo i controlli per i Color Picker e la Checkbox effettuando il Binding con le 3 nuove property introdotte nel SelectedClock. Inoltre, sulla checkbox andiamo a generare un event handler sia per l’evento Checked che per l’Unchecked perché dobbiamo fare in modo che ci sia un solo orologio che è configurato come PrimaryClock per volta.

Lo Xaml ora è a posto, vediamo come modificare il codice di gestione della finestra:

Modifichiamo SetClocksWindow.xaml.cs

private void IsPrimaryClock_Checked(object sender, RoutedEventArgs e)
{
    try
    {
        if (Clocks != null && Clocks.Count > 0)
        {
            //Uncheck any clock checked as primary except the selected item
            foreach (ClockInfo cInfo in Clocks.Where(x => x.IsPrimaryClock && x != SelectedClock))
            {
                cInfo.IsPrimaryClock = false;
            }
        }
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message, "Error (4)", MessageBoxButton.OK, MessageBoxImage.Error);
    }
}
 
private void IsPrimaryClock_Unchecked(object sender, RoutedEventArgs e)
{
    try
    {
        if (Clocks != null && Clocks.Count > 0)
        {
            //If no clock is primary clock force first clock to become one
            if (Clocks.Count(x => x.IsPrimaryClock) == 0)
            {
                Clocks[0].IsPrimaryClock = true;
            }
        }
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message, "Error (5)", MessageBoxButton.OK, MessageBoxImage.Error);
    }
}

La sola modifica alla classe che supporta la nostra finestra di modifica orologi, è l’implementazione degli event handler della checkbox del PrimaryClock.
Quando un orologio viene indicato come essere l’orologio Primario della lista, se un altro orologio fosse stato ugualmente selezionato, verrà automaticamente deselezionato. Se nessun’orologio è selezionato come orologio Primario, il primo della lista viene automaticamente selezionato come tale.

Riepilogo

In questo post abbiamo spiegato i seguenti argomenti:

  • Aggiungere due property di tipo System.Windows.Media.Color ad una classe serializzabile, salvandola e rileggendola dal file salvato in JSON.
  • Aggiungere due property SolidColorBrush che convertono i Color della classe dati in Brush per colorare i controlli sullo User Control e sulla Main Window.
  • Aggiungere il Binding alle property che dalla property Color Creano un SolidColorBrush sullo User Control e la Main Window dell’orologio.
  • Aggiungere al progetto una libreria NuGet da Visual Studio.
  • Utilizzare il componente ColorPicker della libreria Extended.Wpf.Toolkit community edition.
  • Modificare i dati delle classi degli Orologi al variare della Checkbox sulla User Interface.

Potete scaricare il progetto esempio dal link qui indicato:

Se volete invece installare l’orologio, questo è il link del setup

Per installare l’orologio scaricare lo zip, unzipparlo, contiene un file di setup al suo interno ed eseguirlo sul vostro computer.

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