Condividi tramite


BoxPanel, un esempio di pannello personalizzato

Informazioni su come scrivere codice per una classe Panel personalizzata, implementare i metodi ArrangeOverride e MeasureOverride e usare la proprietà Children.

API importanti: Panel, ArrangeOverride,MeasureOverride

Il codice di esempio mostra un'implementazione del pannello personalizzata, ma non dedichiamo molto tempo a spiegare i concetti di layout che influiscono su come personalizzare un pannello per diversi scenari di layout. Per ulteriori informazioni su questi concetti di layout e su come possono essere applicati allo scenario di layout specifico, vedere Panoramica dei pannelli personalizzati XAML.

Un pannello è un oggetto che fornisce un comportamento di layout per gli elementi figlio che contiene nel momento in cui viene eseguito il sistema di layout XAML e il rendering dell'interfaccia utente dell'app. È possibile definire pannelli personalizzati per il layout XAML derivando una classe personalizzata dalla classe Panel. È possibile indicare il comportamento del pannello eseguendo l'override dei metodi ArrangeOverride e MeasureOverride e fornendo la logica che determina le dimensioni e dispone gli elementi figlio. Questo esempio deriva da Panel. Quando si inizia da Panel, ai metodi ArrangeOverride e MeasureOverride non è associato un comportamento iniziale. Il codice fornisce il gateway con cui gli elementi figlio diventano noti al sistema di layout XAML e vengono sottoposti a rendering nell'interfaccia utente. È quindi molto importante che il codice tenga conto di tutti gli elementi figlio e segua i modelli previsti dal sistema di layout.

Scenario di layout

Quando si definisce un pannello personalizzato, si definisce uno scenario di layout.

Uno scenario di layout viene espresso tramite:

  • Cosa farà il pannello quando disporrà di elementi figlio
  • Quando il pannello ha vincoli sul proprio spazio
  • Come la logica del pannello determina tutte le misure, il posizionamento, le posizioni e le dimensioni che alla fine si traducono in un layout di interfaccia utente in cui gli elementi figlio sono oggetto di rendering

Tenendo presente questo aspetto, l'oggetto BoxPanel illustrato di seguito è destinato a uno scenario specifico. Nell'interesse di mantenere il codice maggiormente in evidenza in questo esempio, non verrà ancora illustrato lo scenario in dettaglio mentre ci si concentrerà sui passaggi necessari e sui modelli di codifica. Per ulteriori informazioni sullo scenario, andare direttamente a "Scenario per BoxPanel" e quindi tornare indietro al codice.

Iniziare derivando da Panel

Iniziare derivando una classe personalizzata da Panel. Il modo più semplice per eseguire questa operazione consiste probabilmente nel definire un file di codice separato per questa classe, usando le opzioni di menu di scelta rapida Aggiungi | Nuovo elemento | Classe per un progetto all'interno della sezione Esplora soluzioni di Microsoft Visual Studio. Assegnare il nome BoxPanel alla classe (e al file).

Il file del modello per una classe non inizia con molte istruzioni using perché non è specifico per le app di Windows. Prima di tutto, aggiungere le istruzioni using . Il file modello inizia anche con alcune istruzioni using che probabilmente non sono necessarie e possono essere eliminate. Di seguito è riportato un elenco consigliato di istruzioni using che possono risolvere i tipi necessari per il tipico codice del pannello personalizzato:

using System;
using System.Collections.Generic; // if you need to cast IEnumerable for iteration, or define your own collection properties
using Windows.Foundation; // Point, Size, and Rect
using Windows.UI.Xaml; // DependencyObject, UIElement, and FrameworkElement
using Windows.UI.Xaml.Controls; // Panel
using Windows.UI.Xaml.Media; // if you need Brushes or other utilities

Ora che è possibile risolvere Panel, impostarla come classe di base di BoxPanel. Inoltre, rendere BoxPanel pubblico:

public class BoxPanel : Panel
{
}

A livello di classe, definire alcuni valori int e double che verranno condivisi da diverse funzioni logiche, ma che non dovranno essere esposti come API pubbliche. Nell'esempio, questi sono denominati: maxrc, rowcount, colcount, cellwidth, cellheight, maxcellheight, aspectratio.

Dopo aver completato questa operazione, il file di codice completo è simile al seguente (rimuovendo i commenti sulle istruzioni using, ora che si conosce il motivo per cui sono presenti):

using System;
using System.Collections.Generic;
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;

public class BoxPanel : Panel 
{
    int maxrc, rowcount, colcount;
    double cellwidth, cellheight, maxcellheight, aspectratio;
}

Da qui in poi verrà visualizzata una definizione di membro alla volta, ad esempio un override di un metodo o un elemento di supporto come una proprietà di dipendenza. È possibile aggiungerli allo scheletro sopra in qualsiasi ordine.

MeasureOverride

protected override Size MeasureOverride(Size availableSize)
{
    // Determine the square that can contain this number of items.
    maxrc = (int)Math.Ceiling(Math.Sqrt(Children.Count));
    // Get an aspect ratio from availableSize, decides whether to trim row or column.
    aspectratio = availableSize.Width / availableSize.Height;

    // Now trim this square down to a rect, many times an entire row or column can be omitted.
    if (aspectratio > 1)
    {
        rowcount = maxrc;
        colcount = (maxrc > 2 && Children.Count <= maxrc * (maxrc - 1)) ? maxrc - 1 : maxrc;
    } 
    else 
    {
        rowcount = (maxrc > 2 && Children.Count <= maxrc * (maxrc - 1)) ? maxrc - 1 : maxrc;
        colcount = maxrc;
    }

    // Now that we have a column count, divide available horizontal, that's our cell width.
    cellwidth = (int)Math.Floor(availableSize.Width / colcount);
    // Next get a cell height, same logic of dividing available vertical by rowcount.
    cellheight = Double.IsInfinity(availableSize.Height) ? Double.PositiveInfinity : availableSize.Height / rowcount;
           
    foreach (UIElement child in Children)
    {
        child.Measure(new Size(cellwidth, cellheight));
        maxcellheight = (child.DesiredSize.Height > maxcellheight) ? child.DesiredSize.Height : maxcellheight;
    }
    return LimitUnboundedSize(availableSize);
}

Il modello necessario di un'implementazione di MeasureOverride è il ciclo attraverso ogni elemento in Panel.Children. Chiamare sempre il metodo Measure su ognuno di questi elementi. Measure ha un parametro di tipo Size. Quello che si passa qui è la dimensione che il pannello si sta impegnando a rendere disponibile per quel particolare elemento figlio. Quindi, prima di poter eseguire il ciclo e iniziare a chiamare Measure, è necessario sapere quanto spazio può mettere a disposizione ogni cella. Dal metodo MeasureOverride stesso si ottiene il valore availableSize. Questa è la dimensione dell'elemento padre del pannello quando è stato richiamato il metodo Measure, che ha a sua volta attivato il metodo MeasureOverride invocato all'inizio. Quindi, una logica tipica consiste nel progettare uno schema in cui ogni elemento figlio divide lo spazio della proprietà availableSize complessiva del pannello. Si passa quindi ogni divisione delle dimensioni al metodo Measure di ogni elemento figlio.

La suddivisione delle dimensioni da parte di BoxPanel è piuttosto semplice: divide lo spazio in un numero di caselle che è principalmente controllato dal numero di elementi. Le caselle vengono ridimensionate in base al numero di righe e colonne e alle dimensioni disponibili. A volte una riga o una colonna del quadrato originario non sono necessarie, quindi vengono eliminate e il pannello diventa un rettangolo anziché un quadrato in termini di rapporto tra righe e colonne. Per ulteriori informazioni su come si è arrivati a questa logica, passare a "Scenario per BoxPanel".

Quindi, che effetto ha il passaggio della misura? Imposta un valore per la proprietà di sola lettura DesiredSize in ogni elemento in cui è stato chiamato il metodo Measure. Disporre di un valore DesiredSize diventa importante quando si arriva al passaggio di disposizione, perché DesiredSize comunica quali possono o devono essere le dimensioni quando avvengono la disposizione e il rendering finale. Anche se non si usa DesiredSize nella propria logica, il sistema ne ha comunque bisogno.

È possibile usare questo pannello quando il componente height di availableSize non è associato. Se questo è vero, il pannello non dispone di un'altezza nota da dividere. In questo caso, la logica del passaggio della misura informa ogni figlio che non ha ancora un'altezza limitata. A tale scopo, passare un valore Size alla chiamata Measure per gli elementi figlio in cui Size.Height è infinito. Si tratta di un'operazione legale. Quando viene chiamato Measure, la logica consiste nel fatto che DesiredSize sia impostato al valore minimo tra ciò che è stato passato a Measure e la dimensione naturale dell'elemento derivante da fattori come l'impostazione esplicita di Height e Width.

Nota

Anche la logica interna di StackPanel segue questo comportamento: StackPanel passa un valore di dimensione infinito a Measure sugli elementi figlio, indicando che non esiste alcun vincolo sugli elementi figlio nella dimensione di orientamento. StackPanel si ridimensiona in genere in modo dinamico per ospitare tutti gli elementi figlio in uno stack che cresce in tale dimensione.

Tuttavia, il pannello stesso non può restituire un valore Size infinito dalla chiamata MeasureOverride. Tale operazione genera un'eccezione durante la disposizione. Quindi, parte della logica consiste nell'individuare l'altezza massima richiesta da qualsiasi elemento figlio e usare tale altezza come altezza della cella nel caso in cui questa non fosse già disponibile a partire dai vincoli di dimensioni del pannello. Ecco la funzione helper LimitUnboundedSize a cui si fa riferimento nel codice precedente, che quindi accetta l'altezza massima della cella e la usa per assegnare al pannello un'altezza finita da restituire, oltre a garantire che cellheight sia un numero finito prima dell'avvio del passaggio di disposizione:

// This method limits the panel height when no limit is imposed by the panel's parent.
// That can happen to height if the panel is close to the root of main app window.
// In this case, base the height of a cell on the max height from desired size
// and base the height of the panel on that number times the #rows.
Size LimitUnboundedSize(Size input)
{
    if (Double.IsInfinity(input.Height))
    {
        input.Height = maxcellheight * colcount;
        cellheight = maxcellheight;
    }
    return input;
}

ArrangeOverride

protected override Size ArrangeOverride(Size finalSize)
{
     int count = 1;
     double x, y;
     foreach (UIElement child in Children)
     {
          x = (count - 1) % colcount * cellwidth;
          y = ((int)(count - 1) / colcount) * cellheight;
          Point anchorPoint = new Point(x, y);
          child.Arrange(new Rect(anchorPoint, child.DesiredSize));
          count++;
     }
     return finalSize;
}

Il modello necessario di un'implementazione di ArrangeOverride è il ciclo attraverso ogni elemento in Panel.Children. Chiamare sempre il metodo Arrange su ognuno di questi elementi.

Si noti che non sono presenti tanti calcoli come in MeasureOverride. Questo è un comportamento tipico. Le dimensioni degli elementi figlio sono già note dalla logica di MeasureOverride del pannello o dal valore DesiredSize di ogni elemento figlio impostato durante il passaggio della misura. Tuttavia, è comunque necessario decidere la posizione all'interno del pannello in cui verrà visualizzato ogni figlio. In un pannello tipico, ogni elemento figlio deve eseguire il rendering in una posizione diversa. Un pannello che crea elementi sovrapposti non è consigliabile per gli scenari tipici (anche se non è vietata a priori la creazione di pannelli con sovrapposizioni intenzionali, se questo è davvero lo scenario desiderato).

Questo pannello è disposto in base al concetto di righe e colonne. Il numero di righe e colonne è già stato calcolato (è stato necessario per la misurazione). Ora la forma delle righe e delle colonne più le dimensioni note di ogni cella contribuiscono alla logica di definizione di una posizione di rendering (il anchorPoint) per ogni elemento contenuto in questo pannello. Tale oggetto Point, insieme all'oggetto Size già noto dalla misura, vengono usati come due componenti che compongono un oggetto Rect. Rect è il tipo di input per Arrange.

I pannelli a volte devono ritagliare il contenuto. In tal caso, la dimensione ritagliata è la dimensione presente in DesiredSize, perché la logica di Measure lo imposta come minimo del valore passato a Measure o di altri fattori che determinano la dimensione naturale. Pertanto, in genere non è necessario controllare in modo specifico la presenza di ritagli durante l'operazione Arrange. Il ritaglio avviene solo sulla base del passaggio del valore DesiredSize a ogni chiamata Arrange.

Se tutte le informazioni necessarie per definire la posizione di rendering sono note per altre vie, non è sempre necessario un conteggio durante l'esecuzione del ciclo. Ad esempio, nella logica di disposizione Canvas la posizione nella raccolta Children non è rilevante. Tutte le informazioni necessarie per posizionare ogni elemento in un oggetto Canvas sono note leggendo i valori Canvas.Left e Canvas.Top degli elementi figlio come parte della logica di disposizione. La logica BoxPanel richiede un conteggio da confrontare con il colcount in modo che sia noto quando iniziare una nuova riga ed eseguire l'offset del valore y.

È tipico che l'input finalSize e il valore Size restituito da un'implementazione di ArrangeOverride siano uguali. Per ulteriori informazioni sul perché, vedere la sezione "ArrangeOverride" di Panoramica dei pannelli personalizzati XAML.

Perfezionamento: controllo del conteggio delle righe e delle colonne

È possibile compilare e usare questo pannello così com'è ora. Tuttavia, aggiungeremo un ulteriore perfezionamento. Nel codice appena mostrato, la logica inserisce la riga o la colonna aggiuntive sul lato in proporzione più lungo. Per un maggiore controllo sulle forme delle celle, potrebbe essere preferibile scegliere un set di celle 4x3 anziché 3x4 anche se l'orientamento del pannello è "verticale". Verrà quindi aggiunta una proprietà di dipendenza facoltativa che l'utilizzatore del pannello può impostare per controllare tale comportamento. Ecco la definizione della proprietà di dipendenza, che è molto semplice:

// Property
public Orientation Orientation
{
    get { return (Orientation)GetValue(OrientationProperty); }
    set { SetValue(OrientationProperty, value); }
}

// Dependency Property Registration
public static readonly DependencyProperty OrientationProperty =
        DependencyProperty.Register(nameof(Orientation), typeof(Orientation), typeof(BoxPanel), new PropertyMetadata(null, OnOrientationChanged));

// Changed callback so we invalidate our layout when the property changes.
private static void OnOrientationChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args)
{
    if (dependencyObject is BoxPanel panel)
    {
        panel.InvalidateMeasure();
    }
}

Di seguito viene illustrato come l'uso di Orientation influisce sulla logica della misura in MeasureOverride. In realtà tutto ciò che sta facendo è cambiare il modo in cui rowcount e colcount sono derivati da maxrc e dalle proporzioni reali e ciò genera differenze di dimensioni corrispondenti per ogni cella. Quando Orientation è impostato a Vertical (impostazione predefinita), inverte il valore delle proporzioni effettive prima di usarlo per il conteggio delle righe e delle colonne per il layout del rettangolo "verticale".

// Get an aspect ratio from availableSize, decides whether to trim row or column.
aspectratio = availableSize.Width / availableSize.Height;

// Transpose aspect ratio based on Orientation property.
if (Orientation == Orientation.Vertical) { aspectratio = 1 / aspectratio; }

Scenario per BoxPanel

Lo scenario specifico per BoxPanel è che si tratta di un pannello in cui uno dei principali fattori determinanti di come dividere lo spazio è la conoscenza del numero di elementi figlio. Lo spazio disponibile del pannello viene suddiviso tra essi. I pannelli assumono per loro natura forme rettangolari. Molti pannelli operano dividendo lo spazio rettangolare in ulteriori rettangoli. Questo è l'approccio adottato da Grid per le sue celle. Nel caso di Grid, le dimensioni delle celle vengono impostate dai valori ColumnDefinition e RowDefinition e gli elementi dichiarano la cella esatta in cui vengono inseriti con le proprietà associate Grid.Row e Grid.Column. Ottenere un layout ottimale da una disposizione Grid richiede in genere la conoscenza anticipata del numero di elementi figlio, in modo che ci siano celle sufficienti e ogni elemento figlio imposti le relative proprietà associate per adattarsi alla propria cella.

Ma cosa succede se il numero di elementi figli è dinamico? Questo è certamente un caso possibile. Il codice dell'app può aggiungere elementi alle raccolte, in risposta a qualsiasi condizione di runtime dinamica che si consideri abbastanza importante da poter aggiornare l'interfaccia utente. Se si usa il data binding per eseguire il backup di raccolte/oggetti business, ottenere tali aggiornamenti e aggiornare l'interfaccia utente è un processo che viene gestito automaticamente, tanto che spesso questa è la tecnica preferita (vedere Informazioni approfondite sul data binding).

Ma non tutti gli scenari di app si prestano al data binding. In alcuni casi, è necessario creare nuovi elementi dell'interfaccia utente in fase di esecuzione e renderli visibili. BoxPanel è per questo scenario. Un numero variabile di elementi figlio non è un problema perché BoxPanel usa il conteggio figlio nei calcoli e regola sia gli elementi figlio esistenti che i nuovi elementi figlio in un nuovo layout in modo che tutti trovino posto.

Uno scenario avanzato per estendere ulteriormente BoxPanel (non illustrato qui) può prevedere la presenza di elementi figlio dinamici e usare il valore DesiredSize di un elemento figlio come fattore preponderante per il ridimensionamento delle singole celle. Questo scenario può usare dimensioni variabili di riga o colonna o forme non a griglia, in modo che lo spazio "sprecato" sia minore. Tale approccio richiede una strategia per gestire il modo in cui più rettangoli di diverse dimensioni e proporzioni possono rientrare tutti insieme in un rettangolo contenitore, sia dal punto di vista estetico che per la visualizzazione che utilizza dimensioni ridotte. BoxPanel non esegue questa operazione, ma usa una tecnica più semplice per dividere lo spazio. BoxPanel usa una tecnica che determina il numero minimo di quadrati che è maggiore del numero di elementi figlio. Ad esempio, 9 elementi si adattano a un quadrato 3x3. 10 elementi richiedono un quadrato 4x4. Tuttavia, per risparmiare spazio, è spesso possibile adattare gli elementi rimuovendo comunque una riga o una colonna del quadrato iniziale. Nell'esempio count=10, che si adatta a un rettangolo 4x3 o 3x4.

Ci si potrebbe chiedere perché per il pannello non si scelga invece una disposizione 5x2 che si adatterebbe perfettamente ai 10 elementi. Tuttavia, in pratica, i pannelli assumono dimensioni rettangolari che raramente presentano proporzioni fortemente orientate. La tecnica dei minimi quadrati è un modo per indirizzare la logica di ridimensionamento affinché funzioni bene con le forme tipiche dei layout e non incoraggi un ridimensionamento che dia origine a celle dalle proporzioni strane.

Riferimento

Concetti