Dela via


BoxPanel, ett exempel på en anpassad panel

Lär dig att skriva kod för en anpassad Panel-klass, implementera ArrangeOverride och MeasureOverride-metoder, och använda egenskapen Children.

Viktiga API:er: Panel, ArrangeOverride,MeasureOverride

Exempelkoden visar en anpassad panelimplementering, men vi ägnar inte mycket tid åt att förklara de layoutkoncept som påverkar hur du kan anpassa en panel för olika layoutscenarier. Om du vill ha mer information om dessa layoutkoncept och hur de kan tillämpas på ditt specifika layoutscenario kan du läsa Översikt över anpassade XAML-paneler.

En panel är ett objekt som tillhandahåller ett layoutbeteende för de underordnade element som den innehåller, när XAML-layoutsystemet körs och appens användargränssnitt återges. Du kan definiera anpassade paneler för XAML-layout genom att härleda en anpassad klass från klassen Panel . Du anger beteende för panelen genom att åsidosätta ArrangeOverride- och MeasureOverride metoder och tillhandahålla logik som mäter och ordnar underordnade element. Det här exemplet härleds från Panel. När du börjar från Panel, har ArrangeOverride och MeasureOverride metoderna inget startbeteende. Din kod tillhandahåller den gateway genom vilken underordnade element blir kända för XAML-layoutsystemet och återges i UI:t. Därför är det verkligen viktigt att din kod hanterar alla underordnade element och följer de mönster som layoutsystemet förväntar sig.

Ditt layoutscenario

När du definierar en anpassad panel definierar du ett layoutscenario.

Ett layoutscenario uttrycks genom:

  • Vad panelen gör när den har barnobjekt
  • När panelen har begränsningar i sitt eget utrymme
  • Hur logiken i panelen avgör alla mått, placering, positioner och storlekar som så småningom resulterar i en renderad användargränssnittslayout för barnobjekt

Med detta i åtanke är den BoxPanel som visas här för ett visst scenario. För att hålla koden främst i det här exemplet kommer vi inte att förklara scenariot i detalj ännu, utan istället koncentrera oss på de steg som behövs och kodningsmönstren. Om du vill veta mer om scenariot först, hoppa fram till "Scenariot för BoxPanel" och kom sedan tillbaka till koden.

Börja med att härleda från Panel

Börja med att härleda en anpassad klass från Panel. Det enklaste sättet att göra detta är förmodligen att definiera en separat kodfil för den här klassen med hjälpav snabbmenyalternativen Lägg | | för ett projekt från Solution Explorer i Microsoft Visual Studio. Namnge klassen (och filen) BoxPanel.

Mallfilen för en klass börjar inte med många -deklarationer med hjälp av-instruktioner eftersom den inte är specifik för Windows-appar. Så lägg först till using uttalanden. Mallfilen börjar också med några med-instruktioner som du förmodligen inte behöver, och dessa kan tas bort. Här är en föreslagen lista över med-instruktioner som kan lösa typer du behöver för standard anpassad panelkod:

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

Nu när du kan lösa Panel gör du den till basklassen för BoxPanel. Gör BoxPanel också offentligt:

public class BoxPanel : Panel
{
}

På klassnivå definierar du några int- och double-värden som ska delas av flera av dina logikfunktioner, men som inte behöver exponeras som offentligt API. I exemplet heter dessa: maxrc, rowcount, , colcount, cellwidth, cellheightmaxcellheightaspectratio, , .

När du har gjort detta ser den fullständiga kodfilen ut så här (tar bort kommentarer om användning, nu när du vet varför vi har dem):

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;
}

Från och med nu kommer vi att visa en medlemsdefinition i taget, antingen en metodåterställning eller något stödjande som en beroendeegenskap. Du kan lägga till dessa i skelettet ovan i valfri ordning.

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);
}

Det nödvändiga mönstret för en MeasureOverride-implementering är att gå igenom varje element i Panel.Children. Anropa alltid metoden Measure för vart och ett av dessa element. Måttet har en parameter av typen Storlek. Det du skickar här är den storlek som panelen åtar sig att ha tillgänglig för det specifika underordnade elementet. Innan du kan göra loopen och börja anropa Måttmåste du veta hur mycket utrymme varje cell kan ägna. Själva metoden MeasureOverride ger dig värdet availableSize. Det är den storlek som panelens överordnade använde när det anropade Measure, vilket var orsaken till att denna MeasureOverride anropades från början. Så en typisk logik är att utforma ett schema där varje underordnat element delar upp utrymmet i panelens övergripande availableSize. Sedan skickar du varje storlekssektion till Mätning för varje element.

Hur BoxPanel delar upp storleken är ganska enkelt: det delar upp sitt utrymme i ett antal lådor som till stor del styrs av antalet föremål. Boxarna är storleksanpassade baserat på antal rader och kolumner och den tillgängliga storleken. Ibland behövs inte en rad eller kolumn från en kvadrat, så den tas bort och panelen blir en rektangel i stället för kvadrat när det gäller förhållandet rad: kolumn. Om du vill ha mer information om hur du kom fram till den här logiken går du vidare till "Scenariot för BoxPanel".

Vad händer när åtgärden godkänns? Det anger ett värde för den skrivskyddade egenskapen DesiredSize för varje element där Measure-metoden anropades. Att ha ett DesiredSize- värde är potentiellt viktigt när du kommer till ordningspasset, då DesiredSize kommunicerar vad storleken kan eller bör vara vid arrangering och i den slutliga renderingen. Även om du inte använder DesiredSize i din egen logik behöver systemet det fortfarande.

Det är möjligt att använda den här panelen när höjdkomponenten i availableSize är obunden. Om det stämmer har panelen ingen känd höjd att dela. I det här fallet meddelar logiken för mätningspasset varje barnelement att det inte har en begränsad höjd än. Det gör det genom att skicka en Storlek till Mått anrop för barn där Size.Height är oändlig. Det är lagligt. När Measure anropas är logiken att DesiredSize anges som det minsta av följande: vad som skickades till Measureeller det elementets naturliga storlek från faktorer såsom explicit angiven Height och Width.

Anmärkning

Den interna logiken i StackPanel har också detta beteende: StackPanel skickar ett oändligt dimensionsvärde till Measure för barnen, vilket indikerar att inga begränsningar finns för barnen i orienteringsriktningen. StackPanel anpassar sin storlek vanligtvis dynamiskt för att få plats med alla underordnade i en stack som växer i den dimensionen.

Själva panelen kan dock inte returnera en Storlek med ett oändligt värde från MeasureOverride, vilket orsakar ett undantag under layouten. En del av logiken är därför att ta reda på den maximala höjden som något barn kräver, och använda den som höjd för cellen om den inte redan bestäms av panelens egna storleksbegränsningar. Här är hjälpfunktionen LimitUnboundedSize som refererades i föregående kod, som sedan tar den maximala cellhöjden och använder den för att ge panelen en ändlig höjd att returnera, samt att se till att cellheight är ett ändligt nummer innan ordningspasset initieras.

// 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;
}

ArrangeraOverride

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;
}

Det nödvändiga mönstret för en ArrangeOverride implementation är loopen genom varje element i Panel.Children. Anropa alltid Arrange metoden för vart och ett av dessa element.

Observera att det inte finns lika många beräkningar som i MeasureOverride. Det är typiskt. Storleken på barnen är redan känd från panelens logik för MeasureOverride, eller från värdet av DesiredSize för varje barn som ställs in under mätprocessen. Vi måste dock fortfarande bestämma platsen i panelen där varje element ska placeras. I en typisk panel bör varje barnelement renderas på olika positioner. En panel som skapar överlappande element är inte önskvärd för typiska scenarier (även om det inte är uteslutet att skapa paneler som har avsiktliga överlappningar, om det verkligen är ditt avsedda scenario).

Den här panelen ordnas efter begreppen rader och kolumner. Antalet rader och kolumner var redan beräknat (det var nödvändigt för mätning). Så nu bidrar formen på raderna och kolumnerna plus de kända storlekarna för varje cell till logiken för att definiera en renderingsposition () anchorPointför varje element som den här panelen innehåller. Den Pointanvänds tillsammans med den Size som redan är känd från mätningen, som de två komponenter som används för att konstruera en Rect. Rect är indatatypen för Arrange.

** Paneler behöver ibland beskära sitt innehåll. Om de gör så är den klippta storleken den storlek som finns i DesiredSize, eftersom logiken för Measure anger det som minimum av vad som skickades till Measureeller andra naturliga storleksfaktorer. Så du behöver vanligtvis inte specifikt söka efter urklipp under Ordna; urklippet sker bara baserat på att skicka DesiredSize till varje Ordna samtal.

Du behöver inte alltid ett antal när du går igenom loopen om all information du behöver för att definiera renderingspositionen är känd på annat sätt. Inom Canvas-layoutlogik spelar till exempel positionen i barnsamlingen ingen roll. All information som behövs för att placera varje element i en Canvas fastställs genom att läsa av Canvas.Left och Canvas.Top värdena för de underordnade, som en del av placeringslogiken. Den BoxPanel logiken råkar behöva ett antal för att jämföra med colcount så det är känt när du ska börja en ny rad och förskjuta värdet y.

Det är typiskt att indata finalSize och den Size som du returnerar från en ArrangeOverride-implementering är desamma. För mer information om anledningarna, se avsnittet "ArrangeOverride" i översikten över XAML-anpassade paneler.

En förfining: kontrollera antalet rader jämfört med kolumner

Du kan kompilera och använda den här panelen precis som den är nu. Vi lägger dock till ytterligare en förfining. I koden som just visas placerar logiken den extra raden eller kolumnen på den sida som har längst proportioner. Men för större kontroll över cellernas former kan det vara önskvärt att välja en 4x3-uppsättning celler istället för 3x4 även om panelens egen proportion är "stående". Därför lägger vi till en valfri beroendeegenskap som panelkonsumenten kan ange för att styra det beteendet. Här är definitionen av beroendeegenskapen, som är mycket grundläggande:

// 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();
    }
}

Och nedan visas hur användningen Orientation påverkar måttlogiken i MeasureOverride. Det enda det faktiskt gör är att ändra hur rowcount och colcount härleds från maxrc och den sanna proportionen, vilket medför motsvarande storleksskillnader för varje cell. När Orientation är vertikal (standard) inverterar det värdet av det sanna aspektförhållandet innan det används för att beräkna antalet rader och kolumner i vår "stående" rektangellayout.

// 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; }

Scenariot för BoxPanel

Det specifika scenariot för BoxPanel är att det är en panel där en av de viktigaste faktorerna för utrymmets fördelning är att känna till antalet barnobjekt och dela upp det tillgängliga utrymmet för panelen. Paneler är naturligt rektangulära former. Många paneler fungerar genom att dela upp rektangelutrymmet i ytterligare rektanglar; det är vad Grid gör för sina celler. I Grids fall bestäms storleken på cellerna av värdena ColumnDefinition och RowDefinition , och elementen deklarerar den exakta cellen de går in i med bifogade egenskaper Grid.Row och Grid.Column . Att få en bra layout från ett rutnät kräver vanligtvis att man känner till antalet underordnade element i förväg, så att det finns tillräckligt med celler och varje underordnat element ställer in sina bifogade egenskaper så att de passar in i sin egen cell.

Men vad händer om antalet barn är dynamiskt? Det är verkligen möjligt; Din appkod kan lägga till objekt i samlingar, som svar på alla dynamiska körningsvillkor som du anser vara tillräckligt viktiga för att vara värda att uppdatera användargränssnittet. Om du använder databindning för att säkerhetskopiera samlingar/affärsobjekt hanteras sådana uppdateringar och uppdatering av användargränssnittet automatiskt, så det är ofta den bästa tekniken (se Databindning på djupet).

Men det är inte alla appscenarier som lämpar sig för databindning. Ibland måste du skapa nya gränssnittselement under körning och göra dem synliga. BoxPanel är för det här scenariot. Ett föränderligt antal underordnade objekt är inga problem för BoxPanel eftersom det använder antalet underordnade objekt i beräkningar och justerar både befintliga och nya underordnade objekt till en ny layout så att alla passar.

Ett avancerat scenario för att utöka BoxPanel ytterligare (visas inte här) kan både ta emot dynamiska barn och använda ett barns DesiredSize som en starkare faktor för storleksändring av enskilda celler. I det här scenariot kan du använda olika rad- eller kolumnstorlekar eller former som inte är rutnät, så att det blir mindre "slöseri" med utrymme. Detta kräver en strategi för hur flera rektanglar av olika storlekar och bildförhållanden kan passa in i en innehållande rektangel, både för estetik och minsta storlek. BoxPanel gör inte det; Den använder en enklare teknik för att dela upp utrymmet. BoxPanel:s teknik är att fastställa det minsta kvadrattalet som är större än antalet barn. Till exempel skulle 9 objekt få plats i en 3x3-ruta. 10 objekt kräver en fyrkant. Du kan dock ofta passa in objekt samtidigt som du tar bort en rad eller kolumn i startrutan för att spara utrymme. I exemplet count=10 passar det in i en 4x3- eller 3x4-rektangel.

Du kanske undrar varför panelen inte istället skulle välja 5x2 för 10 artiklar, eftersom det passar artikelnumret snyggt. Men i praktiken är paneler storleksanpassade som rektanglar som sällan har ett starkt orienterat bildförhållande. Minsta-kvadrat-tekniken är ett sätt att snedvrida storlekslogiken så att den fungerar bra med typiska layoutformer och inte uppmuntrar till storleksändring där cellformerna får udda bildförhållanden.

Referens

Begrepp