Confessions of a Dangerous Mind

Brain.FlushBuffer()
posts - 176, comments - 234, trackbacks - 93

WPF Cookbook: personalizzare una ListBox – Part 2

Una delle (molte) cose  che fanno VERAMENTE la differenza nell’impiego di WPF è il concetto di Stile e Template di controllo. Un concetto così raffinato è impossibile da trovare in altri ambienti client (leggi Windows Forms). In pratica il concetto di stile e quello di template di controllo vanno di pari passo, in quanto mentre il primo definisce le caratteristiche di un particolare controllo quali Font, Colori, dimensioni dei bordi, effetti vari etc., il secondo definisce la struttura del controllo stesso.

La caratteristica veramente dirompente consiste però nella possibilità di cambiare completamente la struttura del controllo mantenendone le funzionalità. Nel nostro caso, infatti, vogliamo “rendere presentabile” la listbox, senza però perderne le caratteristiche di base. Definiremo lo stile nel file App.xaml, anche se personalmente consiglio sempre di impiegare un file ResourceDictionary, in modo da renderlo “intercambiabile”.

   1: <Style x:Key="CoolListBoxStyle" TargetType="{x:Type ListBox}">
   2:     <Style.Resources>
   3:         <SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="Transparent"/>
   4:         <SolidColorBrush x:Key="{x:Static SystemColors.ControlBrushKey}" Color="Transparent"/>
   5:     </Style.Resources>
   6:     <Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"/>
   7:     <Setter Property="BorderBrush" Value="{StaticResource ListBorder}"/>
   8:     <Setter Property="BorderThickness" Value="1"/>
   9:     <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
  10:     <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/>
  11:     <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
  12:     <Setter Property="ScrollViewer.CanContentScroll" Value="true"/>
  13:     <Setter Property="VerticalContentAlignment" Value="Center"/>
  14:     <Setter Property="Template">
  15:         <Setter.Value>
  16:             <ControlTemplate TargetType="{x:Type ListBox}">
  17:                 <Border x:Name="Bd" SnapsToDevicePixels="true" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" 
  18:                     BorderThickness="{TemplateBinding BorderThickness}" Padding="1" CornerRadius="10,10,10,10" Height="Auto">
  19:                     <ScrollViewer Padding="{TemplateBinding Padding}" Focusable="false">
  20:                         <ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
  21:                     </ScrollViewer>
  22:                 </Border>                        
  23:                 <ControlTemplate.Triggers>
  24:                     <Trigger Property="IsEnabled" Value="false">
  25:                         <Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
  26:                     </Trigger>
  27:                     <Trigger Property="IsGrouping" Value="true">
  28:                         <Setter Property="ScrollViewer.CanContentScroll" Value="false"/>
  29:                     </Trigger>
  30:                 </ControlTemplate.Triggers>
  31:             </ControlTemplate>
  32:         </Setter.Value>
  33:     </Setter>
  34:     <Setter Property="ListBox.ItemContainerStyle">
  35:         <Setter.Value>
  36:             <Style>
  37:                 <Setter Property="Control.Padding" Value="0"></Setter>
  38:                 <Style.Triggers>
  39:                     <Trigger Property="ListBoxItem.IsSelected" Value="True">
  40:                         <Setter Property="ListBoxItem.Background">
  41:                             <Setter.Value>
  42:                                 <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
  43:                                     <GradientStop Color="#FFFFFFFF"/>
  44:                                     <GradientStop Color="#9968D2FA" Offset="0.536"/>
  45:                                 </LinearGradientBrush>
  46:                             </Setter.Value>
  47:                         </Setter>
  48:                     </Trigger>
  49:                 </Style.Triggers>
  50:             </Style>
  51:         </Setter.Value>
  52:     </Setter>
  53: </Style>

Focalizzandoci sullo stile CoolListBoxStyle, tralasciando il bordo arrotondato e sfumato che “wrappa” lo scroll viewer e l’items presenter, possiamo notare un paio di accorgimenti “particolari”. Nello stile sono definite delle Resources, e nello specifico viene fatto lo shadowing delle proprietà di Highlight e Control Brush. Questo perchè vogliamo per prima cosa eliminare il brutto effetto di “selezione blu”. Questo è una delle cose più fastidiose e, una volta che si sa come fare, più facili da eliminare nel controllo listbox.

Avendo impostato anche uno sfondino ed i colori del data template a White, un background semitrasparente con gradiente sulla listbox i risultato è come quello nella figura a destra; già buono, ma non ancora perfetto. Manca infatti una qualunque variazione di colore/struttura nell’item selezionato. Questo non va bene, perchè di solito la selezione dell’elemento è la chiave per una buona usabilità del controllo. L’utente deve avere ben chiaro che cosa sta selezionando, per cui bisogna scegliere una buona tecnica di evidenziazione della selezione.

Enter ItemViewModel

La selezione è una caratteristica dell’elemento della listbox; quindi perchè non intervenire a livello dell’elemento, ovvero nella view associata al FilmViewModel? Questa sembra una buona idea, ma con WPF si può perseguire solo in parte. Infatti i colori del bordo dell’item si possono controllare dalla view associata all’item stesso, mentre lo sfondo no. Il background deve essere impostato a livello di Listbox e reperito tramite una Binding Expression piuttosto articolata che vedremo nel prossimo snippet.

In pratica, nel nostro caso, si tratta di andare a valorizzare lo sfondo ed il bordo dell’elemento Border. Lo faremo prendendo il valore dello sfondo dichiarato a livello di ListBox.ItemContainerStyle nello stile CoolListBoxStyle. Nello snippet seguente possiamo vedere come l’utilizzo di una Binding Expression con impostata la proprietà RelativeSource = FindAncestor ed AncestorType=ListBoxItem indica al motore di rendering di WPF di “risalire” l’albero dei controlli e trovare il controllo contenitore di tipo ListBoxItem. Essendo questa istruzione collocata in un DataTemplate contenuto a sua volta in un ListBoxItem creato dinamicamente dal databinding della listbox, il risultato è piuttosto scontato:

  • Se l’elemento (ListBoxItem) è selezionato, viene applicato un colore al bordo dell’elemento, definito nel trigger dello style del border stesso
  • Lo sfondo viene reperito dallo stile impostato (CoolListBoxStyle) ed è un gradiente che fa variare di fatto la proprietà background del ListBoxItem
  • La proprietà Background del ListBoxItem viene reperita dalla binding expression ed assegnata allo sfondo del Border.
   1: <UserControl x:Class="FilmList.Views.FilmView"
   2:     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   3:     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   4:     xmlns:Converters="clr-namespace:FilmList.Converters">
   5:     <UserControl.Resources>
   6:         <Converters:ImagePathConverter x:Key="ImagePathConverter"/>
   7:     </UserControl.Resources>
   8:     <Border BorderThickness="2" CornerRadius="10" Padding="4" Margin="2" 
   9:         Background="{Binding Path=Background, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem}}}">
  10:         <Grid>
  11:             <Grid.RowDefinitions>
  12:                 <RowDefinition/>
  13:                 <RowDefinition/>
  14:             </Grid.RowDefinitions>
  15:             <Grid.ColumnDefinitions>
  16:                 <ColumnDefinition Width="70"></ColumnDefinition>
  17:                 <ColumnDefinition Width="300"></ColumnDefinition>
  18:                 <ColumnDefinition Width="*"></ColumnDefinition>
  19:             </Grid.ColumnDefinitions>
  20:             <Border Grid.Column="0" Grid.RowSpan="2" VerticalAlignment="Stretch" Background="White" Margin="0,0,10,0" CornerRadius="4">
  21:                 <Image  Source="{Binding Path=Cover, Converter={StaticResource ImagePathConverter}, ConverterParameter=../Images}" 
  22:                     Margin="2,2,2,2" VerticalAlignment="Center" Height="64"/>
  23:             </Border>
  24:             <TextBlock Grid.Row="0" Grid.Column="1" Style="{StaticResource SubTitleStyle}" Text="{Binding Path=Title}" MinWidth="50" Margin="0,0,5,0" />
  25:             <TextBlock Grid.Row="1" Grid.Column="2" Style="{StaticResource SubTitleStyle}" Text="{Binding Path=Genre}" FontSize="20" MinWidth="50" Margin="0,0,5,0" />
  26:             <TextBlock Grid.Row="1" Grid.Column="1" Style="{StaticResource SubTitleStyle}" Text="{Binding Path=OriginalTitle}" FontSize="20" MinWidth="50" Margin="0,0,5,0" />            
  27:         </Grid>
  28:         <Border.Style>
  29:             <Style>
  30:                 <Style.Triggers>
  31:                     <DataTrigger Binding="{Binding Path=IsSelected, 
  32:                         RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem}}}" Value="True">
  33:                         <Setter Property="Border.BorderBrush" Value="#FF0561CE"/>
  34:                     </DataTrigger>
  35:                     <DataTrigger Binding="{Binding Path=IsSelected, 
  36:                         RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem}}}" Value="False">
  37:                         <Setter Property="Border.BorderBrush" Value="#FF9B9B9B"/>
  38:                     </DataTrigger>
  39:                 </Style.Triggers>
  40:             </Style>
  41:         </Border.Style>
  42:     </Border>
  43: </UserControl>

 

Mi preme ribadire un paio di cose:

  • La binding Expression {Binding Path=Background, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem}}} significa:
    • Rispetto a “dove” ti trovi, cerca il primo “ancestor” (antenato) di tipo ListBoxItem, ovvero il tuo contenitore di tipo ListBoxItem. Di ciò che trovi, prendi la proprietà Background, ed usala come sfondo dell’elemento border. Ovviamente il valore di Background varierà a seconda che l’elemento sia selezionato o meno.
  • Il DataTrigger impostato a livello di border ha lo scopo di selezionare un colore diverso anche per il bordo a seconda che l’elemento sia quello con selezione attiva.

Il risultato lo potete vedere nella immagine a destra. E’ stato completamente stravolto il comportamento della ListBox, annullando di fatto anche i colori di selezione blu standard e sostituendoli con dei piacevoli effetti di gradiente. Ovviamente è possibile aggiungere a tutto questo anche animazioni ed effetti sonori… ma questo è materiale per un altro articolo!

 

Se qualcuno di voi vuole fare altri esperimenti, il codice d’esempio lo può trovare qui.

Print | posted on giovedì 15 ottobre 2009 17:16 |

Feedback

Gravatar

# re: WPF Cookbook: personalizzare una ListBox – Part 2

Per quanto riguarda il riuso puoi ottenerlo anche con un semplice datatemplate, visto che usi RelativeBinding e cerchi un ListBoxItem dubito che tu possa utilizzarlo al di fuori di una listbox.
Per quanto riguarda l'overhead direi che se virtualizzi il container non dovrebbe essere granchè, sicuramente un datatemplate è un oggetto più 'light'.
16/10/2009 11:38 | Corrado Cavalli
Comments have been closed on this topic.

Powered by:
Powered By Subtext Powered By ASP.NET