Condividi tramite


"Esercitazione: Creare un 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 è necessario dedicare molto tempo a spiegare i concetti di layout che influiscono su come personalizzare un pannello per diversi scenari di layout. Per altre info su questi concetti di layout e su come possono essere applicati allo scenario di layout specifico, vedi Panoramica dei pannelli personalizzati XAML.

Un pannello è un oggetto che fornisce un comportamento di layout per gli elementi figlio che contiene, quando viene eseguito il sistema di layout XAML e viene eseguito il rendering dell'interfaccia utente dell'app. Puoi definire pannelli personalizzati per il layout XAML derivando una classe personalizzata dalla classe Panel. È possibile definire il comportamento del pannello eseguendo l'override dei metodi ArrangeOverride e MeasureOverride, fornendo la logica necessaria per misurare e disporre gli elementi figlio. Questo esempio deriva da Panel. Quando si inizia da Panel, i metodi ArrangeOverride e MeasureOverride non hanno un comportamento iniziale. Il tuo codice funge da gateway tramite il quale gli elementi figlio vengono resi noti al sistema di layout XAML e vengono sottoposti a rendering nell'interfaccia utente. È quindi molto importante che il tuo codice consideri 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 contiene 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 risultano in un layout dell'interfaccia utente reso degli elementi figli

Tenendo presente questo aspetto, l'oggetto BoxPanel illustrato di seguito è destinato a uno scenario specifico. Al fine di dare priorità al codice in questo esempio, non spiegheremo ancora lo scenario in dettaglio, ma ci concentreremo invece sui passaggi necessari e sugli schemi di codifica. Per altre informazioni sullo scenario, passare prima a "Scenario per BoxPanel" e quindi tornare al codice.

Iniziare derivando da Panel

Iniziare derivando una classe personalizzata da Panel. Probabilmente il modo più semplice per fare questo è definire un file di codice separato per questa classe, utilizzando le opzioni di scelta rapida del menu Add | New Item | Class per un progetto dal Solution Explorer in Microsoft Visual Studio. Assegnare alla classe il nome e al file BoxPanel.

Il file 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 di cui hai bisogno per un codice del pannello personalizzato tipico:

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 pubblici:

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 pubblica. Nell'esempio, questi sono denominati: maxrc, rowcount, colcount, cellwidthcellheight, maxcellheight. aspectratio

Dopo aver completato questa operazione, il file di codice completo è simile al seguente (rimuovendo i commenti sull'uso, ora che si conosce il motivo per cui sono disponibili):

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, ad esempio 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 MeasureOverride è il loop attraverso ogni elemento in Panel.Children. Chiamare sempre il metodo Measure su ognuno di questi elementi. Measure ha un parametro di tipo Size. Ciò che stai passando qui è la dimensione che il pannello dedica ad avere a disposizione per quel particolare elemento figlio. Quindi, prima di poter eseguire il loop e iniziare a chiamare Measure, è necessario sapere quanto spazio ogni cella può dedicare. Dal metodo MeasureOverride stesso, si ha il valore availableSize . Ovvero, le dimensioni che l'elemento padre del pannello aveva usato quando ha chiamato Measure, il che è stato il trigger per cui MeasureOverride è stato chiamato in primo luogo. Quindi, una tipica logica consiste nel progettare uno schema in cui ogni elemento figlio divide lo spazio complessivo del availableSize del pannello. Si passa quindi ogni divisione delle dimensioni a Measure di ogni elemento figlio.

La BoxPanel suddivisione delle dimensioni è piuttosto semplice: divide lo spazio in un numero di caselle che è in gran parte controllato dal numero di elementi. Le caselle vengono ridimensionate in base al numero di righe e colonne e alle dimensioni disponibili. A volte non è necessaria una riga o una colonna di un quadrato, quindi viene eliminata e il pannello diventa un rettangolo anziché un quadrato, in termini di rapporto tra righe e colonne. Per altre info su come è arrivata questa logica, passare a "Scenario per BoxPanel".

Quindi, cosa comporta l'approvazione della misura? Imposta un valore per la proprietà DesiredSize di sola lettura in ogni elemento in cui è stato chiamato Measure . Avere un valore DesiredSize è probabilmente importante quando si arriva alla fase di disposizione, perché DesiredSize comunica quali dimensioni possono o devono avere durante la disposizione e nel rendering finale. Anche se non si usa DesiredSize nella propria logica, il sistema ne ha ancora bisogno.

È possibile usare questo pannello quando il componente altezza di availableSize è senza limiti. Se questo è vero, il pannello non ha un'altezza conosciuta da dividere. In questo caso, la logica per il passaggio della misura informa ogni figlio che non ha ancora un'altezza limitata. A tale scopo, si passa un valore Size alla chiamata Measure per gli elementi figli dove Size.Height è infinito. È legale. Quando Measure viene chiamato, la logica consiste nel fatto che DesiredSize è impostato come il valore minimo tra ciò che viene passato a Measure o la dimensione naturale dell'elemento determinata da fattori come l'impostazione esplicita di Height e Width.

Annotazioni

La logica interna di StackPanel ha anche questo comportamento: StackPanel passa un valore di dimensione infinito a Measure sugli elementi figlio, a indicare che non esiste alcun vincolo per gli elementi figlio nella dimensione di orientamento. StackPanel si ridimensiona di solito in modo dinamico, per contenere tutti gli elementi figlio in uno stack che cresce in quella direzione.

Tuttavia, il pannello stesso non può restituire un valore Size con un valore infinito da MeasureOverride; che genera un'eccezione durante il layout. Quindi, parte della logica consiste nell’individuare l'altezza massima richiesta da qualsiasi bambino e usare tale altezza come altezza della cella nel caso in cui non provenga già dai vincoli dimensionali del pannello. Ecco la funzione LimitUnboundedSize helper 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 ArrangeOverride è il loop attraverso ogni elemento in Panel.Children. Chiamare sempre il metodo Arrange su ognuno di questi elementi.

Si noti come non ci siano tanti calcoli come in MeasureOverride; è tipico. Le dimensioni degli elementi figlio sono già note grazie alla logica MeasureOverride del pannello o al valore DesiredSize di ciascun elemento figlio impostato durante il ciclo di misurazione. Tuttavia, è comunque necessario decidere la posizione all'interno del pannello in cui verrà visualizzato ogni figlio. In un pannello tipico, ogni bambino dovrebbe rendere in una posizione diversa. Un pannello che genera elementi sovrapposti non è consigliabile per gli scenari tipici (anche se non è da escludere creare pannelli con sovrapposizioni intenzionali, se questo è davvero ciò che si desidera).

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 punto, insieme alle dimensioni già note dalla misura, vengono usati come due componenti che costruiscono un rect. Rect è il tipo di input per Arrange.

I pannelli a volte devono tagliare il contenuto. In caso affermativo, la dimensione ritagliata è la dimensione presente in DesiredSize, perché la logica Measure lo imposta al minimo tra ciò che è stato passato a Measure o ad altri fattori di dimensione naturale. Quindi, di solito non è necessario verificare specificamente la presenza di ritagli durante Arrange; il ritaglio si verifica automaticamente quando il DesiredSize viene passato a ciascuna chiamata Arrange.

Non è sempre necessario un conteggio durante l'esecuzione del loop se tutte le informazioni necessarie per definire la posizione di rendering sono note con altri mezzi. Ad esempio, nella logica di layout canvas la posizione nella raccolta Children non è rilevante. Tutte le informazioni necessarie per posizionare ogni elemento in un Canvas sono note leggendo i valori Canvas.Left e Canvas.Top dei figli come parte della logica di disposizione. La BoxPanel logica 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 le dimensioni restituite da un'implementazione ArrangeOverride siano le stesse. Per altre info sul motivo, vedi 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 altro perfezionamento. Nel codice appena mostrato, la logica inserisce la riga o la colonna aggiuntiva sul lato più lungo in proporzioni. Tuttavia, per un maggiore controllo sulle forme delle celle, potrebbe essere preferibile scegliere un set di celle 4x3 anziché 3x4 anche se il rapporto d'aspetto del pannello è "verticale". Si aggiungerà quindi 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 Orientation influisce sulla logica della misura in MeasureOverride. In realtà, tutto ciò che sta facendo è cambiare come rowcount e colcount sono derivati da maxrc e il rapporto di aspetto reale, e ci sono differenze di dimensioni corrispondenti per ogni cella di conseguenza. Quando Orientation è Verticale (impostazione predefinita), inverte il valore del vero rapporto d'aspetto prima di usarlo per il conteggio delle righe e delle colonne nel layout del rettangolo "ritratto".

// 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 di BoxPanel

Lo scenario specifico per BoxPanel è che si tratta di un pannello in cui uno dei principali fattori determinanti di come dividere lo spazio è conoscendo il numero di elementi figlio e dividendo lo spazio disponibile noto per il pannello. I pannelli sono forme innatemente rettangole. Molti pannelli operano dividendo lo spazio rettangolo in ulteriori rettangoli; questo è ciò che Grid fa 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 griglia richiede in genere conoscere il numero di elementi figlio in anticipo, in modo che ci siano celle sufficienti e ogni elemento figlio imposta le relative proprietà associate per adattarsi alla propria cella.

Ma cosa succede se il numero di figli è dinamico? Questo è certamente possibile; il codice dell'app può aggiungere elementi alle raccolte, in risposta a qualsiasi condizione di runtime dinamica che 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 viene gestito automaticamente, in modo che sia spesso 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 è pensato 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 siano tutti adatti.

Uno scenario avanzato per estendere BoxPanel ulteriormente (non illustrato qui) potrebbe sia contenere elementi figlio dinamici sia utilizzare DesiredSize di un figlio come fattore più determinante per il ridimensionamento delle singole celle. Questo scenario può utilizzare dimensioni variabili di righe o colonne oppure forme non a griglia, in modo che lo spazio "sprecato" sia minore. Questo richiede una strategia per il modo in cui più rettangoli di varie dimensioni e proporzioni possono tutti adattarsi a un rettangolo contenitore sia per l'estetica che per le dimensioni più piccole. BoxPanel non lo fa; usa una tecnica più semplice per dividere lo spazio. BoxPanelLa tecnica è determinare il numero quadrato minore maggiore del numero dei figli. Ad esempio, 9 elementi si adattano a un quadrato 3x3. 10 elementi richiedono un quadrato 4x4. Tuttavia, è spesso possibile adattare gli elementi rimuovendo comunque una riga o una colonna del quadrato iniziale, per risparmiare spazio. Nell'esempio count=10, questo si adatta a un rettangolo di 4x3 o 3x4.

Ci si potrebbe chiedere perché il pannello non sceglierebbe invece 5x2 per 10 elementi, perché si adatta perfettamente al numero di articolo. In realtà però, i pannelli vengono dimensionati a forma di rettangolo, che raramente hanno un rapporto d'aspetto fortemente orientato. La tecnica dei minimi quadrati è un modo per orientare la logica di ridimensionamento per funzionare bene con forme di layout tipiche e non incoraggiare il ridimensionamento in cui le forme delle celle ottengono proporzioni insolite.

Riferimento

Concetti