Gianni Giaccaglini

Tricks & mini applics on WPF
posts - 46, comments - 0, trackbacks - 0

Shape con eventi vs. ControlTemplate

Shape con eventi o ControlTemplate: questo il dilemma?

Il mondo Wpf è molto vasto e, quel che è peggio, lo sono i suoi vari sotto-mondi, inoltre i manuali specifici dedicano corposi capitoli a ciascun ambiente non sempre ben correlandoli fra loro. Di conseguenza chi ne studia uno rischia di perdere di vista altri e, peggio, di non rendersi conto delle alternative e/o delle (notevoli) possibilità di farne dei “merge”. Perché in Wpf tutto si tiene.

Venendo al sodo chi inizia, dopo le parti generali, potrebbe percorrere l’iter seguente: 1) Controlli (col tipo Button in primo piano); 2) Shape o forme varie (Rectangle, Ellipse poi Line, Polygone ecc.); 3) Stili; 4) ControlTemplate. I più curiosi fin dal passo 1 si chiedono, viste le promesse “artistiche” di Wpf, come ottenere pulsanti dalla forma meno spartana rispetto al burocratico rettangolo default. Ma non sembra esserci verso di ottenerlo direttamente. Studiando poi le Shape si apprende che anch’esse come tutti gli oggetti Wpf(FrameworkElement in gergo Wpf) sono dotate di eventi, a partire da un MouseDown che di fatto equivale al Click che Wpf riserva al solo Button. Interessante, perché nelle tradizionali Window Form non esiste tale comodità. Così viene la tentazione di surrogare i grigi pulsanti standard perlomeno con rettangoli arrotondati, Ellissi o forme più personali o magari con immagini (cosa anch’essa lecita, ma di cui qui non mi occupo per brevità).

Poi un giorno l’amico Ciccio Satutto ci mette in crisi informandoci fugacemente che esistono dei ControlTemplate che nell’ambito di un dato stile permettono di conferire qualsiasi forma a un controllo. Poiché questo non è un giallo, semmai è resoconto autobiografico (ma come avete fatto a capirlo?) do subito la risposta al dilemma del titolo:

In genere usare le shape con routine d’evento quando non si hanno troppe ripetizioni di controlli consimili. La spiegazione, come sarà più chiaro negli esempi che fornirò, risiede nel fatto che i ControlTemplate utilizzano giustappunto delle shape per dar forma al controllo.

Di conseguenza ritengo che almeno nei casi normali della vita (v. le conclusioni finali) e quando sono in ballo pochi pulsanti consimili non valga la pena di affrontare le complicanze di un ControlTemplate.

Nota. E la performance nei due casi? Non ho autorevolezza per sentenziare ma sono incline a ritenere che non vi sia differenza apprezzabile. Sicuramente i ControlTemplate permettono significativo risparmio di codice (a onta della grande prolissità dello specifico XAML, che viene ben compresso in BAML) ma solo in presenza di molti controlli cui si possano applicare.

Piccoli esempi di Shape dotate di routine d’evento

Uno dei primi problemi che si presentano a chi si prefigge di usare una forma come surrogato di un controllo è quello della scritta esplicativa di cui dotarlo. Infatti per motivi un po’ misteriosi una Shape non possiede l’attributo Content. I due esempi che seguono costituiscono due diverse soluzioni.

Primo esempio

<Grid>

  <Ellipse Name="miaEllisse" Height="80" Width="250"

           Fill="Blue" MouseDown="Insulto" />

  <Label Name="testoEllisse" Height="80" Width="250"

         MouseDown="Insulto"

         Content="Scemo!" FontSize="25"

     HorizontalContentAlignment="Center"

     VerticalContentAlignment="Center" />

</Grid>

Il trucco consiste nell’uso di un contenitore Grid unicellulare per ospitare sia un’Ellisse (o altra Shape) che un’etichetta di pari dimensioni. Le due si sovrappongono ma la seconda – provare per credere - non occulta la prima. Invito poi a ricordarsi di questo utilizzo di Label nei ControlTemplate, dove vedremo che l’analogo mestiere viene svolto da un apposito ContentPresenter-

Ed ecco una routine d’evento di squallido umorismo (ma siamo qui per divertirci, mica per lavorare, no?).

Sub Insulto(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs)

  Static sw As Boolean

  Dim Msg As String = IIf(sw, "Scemo!", "Scemo sarai tu")

  MessageBox.Show(Msg)

  testoEllisse.Content = Msg

  sw = Not sw

End Sub

Nota. Al di là dell’infelice imitazione di Renzo Arbore, si noti che avremmo potuto assegnare Insulto alla sola Label. È stata affibbiata pure all’Ellisse per mettersi al riparo da futuri cambiamenti delle dimensioni.

Secondo esempio

Stavolta i due FrameworkElement sovrapposti nella cella di una Grid (sottintesa) sono un Button sottostante e un Rectangle che sta sopra, dotato di angoli arrotondati grazie alle proprietà RadiusX e RadiusY (v. Guida):

<Button Name="cmdPerTxtBinding" FontFamily="Tahoma" FontSize="12

        Content="Cliccami" Width="200" Height="75" Click="miaRout" />

<Rectangle Name="Rectangle1" Margin="3"  Width="200" Height="100"

           Stroke="Blue" RadiusX="25"  RadiusY="25" MouseDown="miaSub">

  <Rectangle.Fill>

    <VisualBrush Visual="{Binding ElementName=cmdPerTxtBinding}" />

  </Rectangle.Fill>

</Rectangle>

 

Il segreto risiede nella particolare proprietà VisualBrush del riempimento (Rectangle.Fill) che a sua volta con argomento Visual richiama per binding l’elemento il cui nome è, guarda caso, quello del sottostante pulsante cmdPerTxtBinding.

Tale magia non solo fa comparire nel nostro rettangolo la fatidica frase “Cliccami” ma pesca l’intero mondo”visivo” del Button, inclusa un’eventuale immagine in esso mostrata! Troppa grazia? Ovviamente dipende dalle concrete esigenze. Qualora l’obiettivo fosse solo attingere un’etichetta si potrebbe sostituire il Button con una più umile Label, facile esercizio lasciato a chi legge.

Nota. Dei due eventi indicati solo il MouseDown del rettangolo è basilare, visto che stiamo trattando shape con evento per cui il Button avrebbe un ruolo servile. Ma in esso ho lasciato ugualmente Click=”miaRout” per uno sfizio: avvicendare i due controlli, con il rettangolo che richiama il pulsante e viceversa. I più curiosi provino a crearsi tale codice, riportato in fondo all’articolo.

Uso di un ControlTemplate (mini applicazione Gioco del 15)

Questo esempio contiene un pot pourri di varie funzionalità del ControlTemplate e si applica al Gioco del 15 illustrato nel mio articolo didattico pubblicato nella sezione principale di questo sito e qui rivisitato per ottenere il risultato visibile nella figura qui sotto:

Persino chi ha poca memoria (ma è meglio leggerlo, l’articolo testé citato) si rende subito conto che i 15 pulsanti contenuti in una griglia(Grid)  4 x 4 si sono ben prestati a uno stile e a un ControlTemplate ad hoc data la loro forte somiglianza a parte il numeretto, e l’essere inoltre accomunati dalla routine che, sul Click, sposta ciascuno nel “buco” accanto. Non riporto tale procedura, ponendo qui l’accento sugli stili e, alquanto potenziati rispetto alla versione originaria, soprattutto introducendo un ControlTemplate.

Codice XAML

La parte alta, relativa a risorse e stili è la seguente:

<Window x:Class="Window1"

  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

  Title="Gioco del 15" Height="300" Width="300"

        FontSize="20" FontFamily="Verdana" VerticalAlignment="Center">

  <Window.Resources>

    <Style TargetType= "Button">

      <Setter Property="FontSize" Value="15" />

      <Setter Property="FontWeight" Value="Bold" />

      <Setter Property="Foreground" Value="Chocolate" />

      <EventSetter Event="Click" Handler="SpostaPulsante"/>

      <Setter Property="Template">

        <Setter.Value>

          <ControlTemplate TargetType="Button">

            <Grid>

              <Rectangle Name="questoRett" RadiusX="5" RadiusY="5"

                         Stroke="DarkRed"

                         Fill="{TemplateBinding Control.Background}"/>

              <ContentPresenter HorizontalAlignment="Center"

                                VerticalAlignment="Center" />

            </Grid>

            <ControlTemplate.Triggers>

              <Trigger Property="Button.IsPressed" Value="True">

                <Setter TargetName="questoRett"

                        Property="Fill" Value="Yellow"/>

              </Trigger>

            </ControlTemplate.Triggers>

          </ControlTemplate>

        </Setter.Value>

      </Setter>

    </Style>

  </Window.Resources>

 

Commenti tacitiani.

Circa lo Style va notata l’assenza dell’argomento x:Key. Si ha così uno stile anonimo (cosiddetto) che si applica automaticamente a tutti i controlli – Button nella fattispecie – senza che questi debbano citare la chiave. Sui vari Setter dello stile in alto non dico null’altro che le varie proprietà definite non si applicano a quei pulsanti ove le stesse siano diversamente fissate (legge del predominio delle impostazioni locali su quelle di livello superiore), idem per l’EventSetter che definisce la routine comunitaria.

Venendo al CentrolTemplate, definito con la tag <ControlTemplate TargetType=”Button”>  di limpida semantica esso prevede come Shape che dà forma al Button un Rettangolo dagli angoli arrotondati (con RadiusX=”5” e RadiusY=”5”. Tale Shape è racchiusa in una Grid mono cellula che ospita sotto al rettangolo un ContentPresenter, speciale oggetto(annunciato descrivendo l’Ellisse “etichettata”, ricordate?) che garantisce che i Content dei vari pulsanti vengano visualizzati sulla Shape, allineati centralmente sia in orizzontale che verticale.

Un’altra particolarità – un po’ contorta e di non troppo immediata reperibilità (segnarsela a futura memoria!) – è data dall’argomento Fill della nostra forma rettangolare. Tale direttiva attinge il riempimento (Fill) del rettangolo dalla concettualmente analoga ma diversamente nomata proprietà Control.Background, mediante lo speciale TemplateBinding tipico dei ControlTemplate. Più facile a vedersi che a dirsi, ne consegue che la Shape assume precisamente il riempimento del pulsante cui essa dà forma.

Molto peculiare e interessante è infine il capitolo dei TemPlateTriggers, grilletti per far scattare eventi vari. Il Trigger del nostro caso si applica con l’argomento Property=”ButtonIsPressed “ con valore Value=”True”. È una sottigliezza: si badi bene che non viene richiamato l’evento Click, bensì la proprietà ButtonIsPressed = “True”. Bizantinismi a parte (cui è giocoforza inchinarsi, ahimè) tutto ciò fa sì che il Setter del Trigger, caratterizzato da tre argomenti:

·         il nome del bersaglio, ossia il nostro rettangolo (TargetName=”questoRett”) e si tenga a mente che la shape va tassativamente denominata se si vuole che il trigger funzioni);

·         la proprietà da “bersagliare” ossia Property=”Fill  nel nostro caso, con un certo valore, nel nostro caso Value=”Yellow”.

Chi non l’avesse ancora capito, tale marchingegno dipinge di giallo il controllo a forma di rettangolo (o altra Shape scelta a raffigurarlo) quando lo si schiaccia, dando un feeling visivo dell’evento. Va sottolineato che un risultato del genere – complicato da conseguire tramite codice VB/C#- si ottiene in XAML soltanto ricorrendo a un ControlTemplate (a parte segreti improbabili, ignoti a chi scrive).

Riporto qui sotto senza nessun commento il resto delle dichiarazioni XAML che definiscono (e creano) la griglia coi 15 pulsanti del Gioco del 15.

  <Grid Name="Grid1">

    <Grid.ColumnDefinitions>

      <ColumnDefinition Width="100*" />

      <ColumnDefinition Width="100*" />

      <ColumnDefinition Width="100*" />

      <ColumnDefinition Width="100*" />

    </Grid.ColumnDefinitions>

    <Grid.RowDefinitions>

      <RowDefinition Height="40*" />

      <RowDefinition Height="40*" />

      <RowDefinition Height="40*" />

      <RowDefinition Height="40*" />

    </Grid.RowDefinitions>

    <Button Margin="10" Name="Button1" Grid.Row="0" Grid.Column="0">

        10

    </Button>

    <Button  Margin="10" Name="Button2" Grid.Row="0" Grid.Column="1"

             Background="Blue" Foreground="White">

        6

    </Button>

    <Button  Margin="10" Name="Button3" Grid.Row="0" Grid.Column="2"

             Background="Blue" Foreground="White">

        14

      </Button>

    <Button  Margin="10" Name="Button4" Grid.Row="0" Grid.Column="3">

        13

    </Button>

    <Button  Margin="10" Name="Button5" Grid.Row="1" Grid.Column="0">

        12

    </Button>

    <Button  Margin="10" Name="Button6" Grid.Row="1" Grid.Column="1">

        15

    </Button>

    <Button  Margin="10" Name="Button7" Grid.Row="1" Grid.Column="2">

        5

    </Button>

    <Button  Margin="10" Name="Button8" Grid.Row="1" Grid.Column="3">

        7

    </Button>

    <Button  Margin="10" Name="Button9" Grid.Row="2" Grid.Column="0">

        4

    </Button>

    <Button  Margin="10" Name="Button10" Grid.Row="2" Grid.Column="1"

             Background="Blue" Foreground="White">

        9

    </Button>

    <Button  Margin="10" Name="Button11" Grid.Row="2" Grid.Column="2"

             Background="Blue Foreground="White">

        11

    </Button>

    <Button  Margin="10" Name="Button12" Grid.Row="2" Grid.Column="3"

             Background="Blue" Foreground="White">

        1

    </Button>

    <Button  Margin="10" Name="Button13" Grid.Row="3" Grid.Column="0"

             Background="Blue" Foreground="White">

        3

    </Button>

    <Button  Margin="10" Name="Button14" Grid.Row="3" Grid.Column="1"

             Background="Blue" Foreground="White">

        8

    </Button>

    <Button  Margin="10" Name="Button15" Grid.Row="3" Grid.Column="2">

        2

    </Button>

  </Grid>

</Window>

Conclusioni

Molto sinteticamente:

·         La riserva posta prudentemente in apertura ha fin qui scoperto almeno una qualità dei ControlTemplate che non ha riscontro nelle Shape con evento. Parlo dei Triggers tra cui quello visto sopra innescato da una Property="Button.IsPressed". Gl’interessati ne troveranno altre? Le segnalino, prego…

·         L’impiego di una cella singola Grid  che serve a esibire una scritta sia nelle Shape con evento sia nei ControlTemplate (mediante un ContentPresenter) si direbbe escluso in SilverLight ove tale container è assente. ALLA FINE HO SCOPERTO CHE PURE IN TALE MONDO SI USA <Grid>... <Grid> NEI CONTROL TEMPLATE COME QUI DESCRITTO. Come non detto.

Dulcis in fundo…

Ecco le due procedure sopra citate , relative al caso del rettangolo che “dipende” da un sostante Button cmdPerTxtBinding:

Class MainWindow

  Dim oldContent = "Cliccami Ciccio"

  Sub MiaSub(ByVal sender As Object, ByVal e As RoutedEventArgs)

    MessageBox.Show("Hai cliccato sul rettangolo. Bravo Ciccio!")

    sender.Visibility = Windows.Visibility.Hidden

    cmdPerTxtBinding.Content = "Secondo lavoro"

  End Sub

 

  Sub miaRout(ByVal sender As Object, ByVal e As RoutedEventArgs)

    MessageBox.Show("Hai cliccato sul Button sottostante. Bravo Ciccio!" & vbLf _

               & "Ora faccio ricomparire il rettangolo...")

    sender.Content = oldContent

    Rectangle1.Visibility = Windows.Visibility.Visible

  End Sub

End Class

 

Umorismo discutibile? Forse. Comunque non solo giova a ricrear lo spirito fra un enigma e l’altro ma può essere un esempio su come presentare lavori successivi mediante controlli sovrapposti, in modo che quello in testa, compiuto il primo lavoro, si auto-occulti – con sender.Visibility = Windows.VisibilityHidden – mettendo in mostra il controllo dedicato al secondo lavoro… Un’idea non inedita ma che Wpf in qualche modo facilita.

Print | posted on martedì 12 ottobre 2010 19:30 |

Feedback

No comments posted yet.

Post Comment

Title  
Name  
Email
Url
Comment   
Please add 8 and 7 and type the answer here:

Powered by:
Powered By Subtext Powered By ASP.NET