Freigeben über


BoxPanel, ein beispiel für ein benutzerdefiniertes Panel

Erfahren Sie, wie Sie Code für eine benutzerdefinierte Panel-Klasse schreiben, die Methoden ArrangeOverride und MeasureOverride implementieren und die Children-Eigenschaft verwenden.

Wichtige APIs: Panel, ArrangeOverride,MeasureOverride

Der Beispielcode zeigt eine benutzerdefinierte Panelimplementierung, aber wir widmen uns nicht viel Zeit, um die Layoutkonzepte zu erläutern, die beeinflussen, wie Sie ein Panel für verschiedene Layoutszenarien anpassen können. Weitere Informationen zu diesen Layoutkonzepten und deren Anwendung auf Ihr bestimmtes Layoutszenario finden Sie in der Übersicht über benutzerdefinierte XAML-Panels.

Ein Panel ist ein Objekt, das ein Layoutverhalten für untergeordnete Elemente bereitstellt, die es enthält, wenn das XAML-Layoutsystem ausgeführt wird und die App-UI gerendert wird. Sie können benutzerdefinierte Panels für das XAML-Layout definieren, indem Sie eine benutzerdefinierte Klasse von der Panel-Klasse ableiten. Sie stellen Verhalten für Ihren Bereich bereit, indem Sie die Methoden ArrangeOverride und MeasureOverride außer Kraft setzen und Logik bereitstellen, die die untergeordneten Elemente misst und anordnet. Dieses Beispiel wird von Panel abgeleitet. Wenn Sie mit "Panel" beginnen, weisen die Methoden "ArrangeOverride" und "MeasureOverride" kein Startverhalten auf. Ihr Code stellt das Gateway bereit, mit dem untergeordnete Elemente dem XAML-Layoutsystem bekannt werden und in der Benutzeroberfläche gerendert werden. Daher ist es wirklich wichtig, dass Ihr Code alle untergeordneten Elemente berücksichtigt und den Mustern folgt, die das Layoutsystem erwartet.

Ihr Layoutszenario

Wenn Sie einen benutzerdefinierten Bereich definieren, definieren Sie ein Layoutszenario.

Ein Layoutszenario wird durch Folgendes ausgedrückt:

  • Was das Panel tun wird, wenn es untergeordnete Elemente enthält
  • Wenn das Panel Einschränkungen für seinen eigenen Platz hat
  • Wie die Logik des Panels alle Maße, Platzierungen, Positionen und Größen bestimmt, die letztendlich zu einem gerenderten UI-Layout untergeordneter Elemente führen

Das hier gezeigte szenario ist für BoxPanel ein bestimmtes Szenario vorgesehen. Im Interesse, den Code in diesem Beispiel in erster Linie zu behalten, werden wir das Szenario noch nicht ausführlich erläutern und konzentrieren uns stattdessen auf die erforderlichen Schritte und die Codierungsmuster. Wenn Sie zuerst mehr über das Szenario wissen möchten, fahren Sie mit "Das Szenario für BoxPanel" fort, und kehren Sie dann zum Code zurück.

Beginnen Sie mit der Ableitung von Panel

Beginnen Sie, indem Sie eine benutzerdefinierte Klasse von Panel ableiten. Dies ist wahrscheinlich die einfachste Möglichkeit, eine separate Codedatei für diese Klasse mithilfe der Kontextmenüoptionen "Neue Elementklasse | hinzufügen | " für ein Projekt aus dem Projektmappen-Explorer in Microsoft Visual Studio zu definieren. Benennen Sie die Klasse (und Datei) BoxPanel.

Die Vorlagendatei für eine Klasse beginnt nicht mit vielen using-Anweisungen, weil sie nicht ausschließlich für Windows-Apps bestimmt ist. Fügen Sie also zunächst using-Anweisungen hinzu. Die Vorlagendatei beginnt auch mit einigen using-Anweisungen , die Sie wahrscheinlich nicht benötigen, und kann gelöscht werden. Hier ist eine vorgeschlagene Liste der Verwendungsanweisungen , die Typen auflösen können, die Sie für typischen benutzerdefinierten Panelcode benötigen:

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

Jetzt, da Sie Panel auflösen können, machen Sie es zur Basisklasse von BoxPanel. Machen Sie BoxPanel außerdem folgendes öffentlich:

public class BoxPanel : Panel
{
}

Definieren Sie auf Klassenebene einige Int - und Double-Werte , die von mehreren Ihrer Logikfunktionen gemeinsam verwendet werden, die jedoch nicht als öffentliche API verfügbar gemacht werden müssen. Im Beispiel werden die folgenden Namen benannt: maxrc, , rowcount, colcount, cellwidth, cellheight, . maxcellheightaspectratio.

Nachdem Sie dies getan haben, sieht die vollständige Codedatei wie folgt aus (Entfernen von Kommentaren zur Verwendung, jetzt wissen Sie, warum wir sie haben):

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

Von hier aus zeigen wir Ihnen jeweils eine Elementdefinition an, sei es, dass eine Methode außer Kraft setzen oder etwas unterstützt, z. B. eine Abhängigkeitseigenschaft. Sie können diese dem obigen Skelett in beliebiger Reihenfolge hinzufügen.

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

Das erforderliche Muster einer MeasureOverride-Implementierung ist die Schleife durch jedes Element in Panel.Children. Rufen Sie immer die Measure-Methode für jedes dieser Elemente auf. Measure hat einen Parameter vom Typ Size. Was Sie hier übergeben, ist die Größe, die Ihr Panel verpflichtet, für dieses bestimmte untergeordnete Element verfügbar zu sein. Bevor Sie also die Schleife ausführen und mit dem Aufrufen von Measure beginnen können, müssen Sie wissen, wie viel Platz jede Zelle aufnehmen kann. Aus der MeasureOverride-Methode selbst haben Sie den availableSize-Wert . Dies ist die Größe, die das übergeordnete Element des Panels beim Aufrufen von Measure verwendet hat, was der Trigger für diese MeasureOverride war, die zuerst aufgerufen wird. Eine typische Logik besteht also darin, ein Schema zu entwickeln, bei dem jedes untergeordnete Element den Raum der gesamten verfügbaren Größe des Panels teilt. Anschließend übergeben Sie jede Größenteilung an Measure jedes untergeordneten Elements.

Die BoxPanel Aufteilung der Größe ist relativ einfach: Sie teilt ihren Platz in eine Reihe von Feldern auf, die weitgehend durch die Anzahl der Elemente gesteuert werden. Felder werden basierend auf der Zeilen- und Spaltenanzahl und der verfügbaren Größe angepasst. Manchmal wird eine Zeile oder Spalte aus einem Quadrat nicht benötigt, daher wird sie verworfen, und das Panel wird zu einem Rechteck anstelle eines Quadrats in Bezug auf das Zeilenverhältnis : Spaltenverhältnis. Weitere Informationen dazu, wie diese Logik erreicht wurde, fahren Sie mit "Das Szenario für BoxPanel" fort.

Was macht der Messdurchlauf also? Es legt einen Wert für die schreibgeschützte DesiredSize-Eigenschaft für jedes Element fest, in dem Measure aufgerufen wurde. Ein DesiredSize-Wert ist möglicherweise wichtig, sobald Sie zum Anordnungsdurchlauf gelangen, da die DesiredSize kommuniziert, was die Größe beim Anordnen und im endgültigen Rendering sein kann oder sollte. Auch wenn Sie DesiredSize nicht in Ihrer eigenen Logik verwenden, benötigt das System es weiterhin.

Es ist möglich, dass dieser Bereich verwendet werden kann, wenn die Höhenkomponente von availableSize ungebunden ist. Wenn dies der Fall ist, verfügt das Panel nicht über eine bekannte Höhe zum Dividieren. In diesem Fall informiert die Logik für den Messdurchlauf jedes untergeordnete Element, dass es noch keine begrenzungsgebundene Höhe hat. Dies geschieht durch Übergeben einer Größe an den Measure-Aufruf für untergeordnete Elemente, bei denen Size.Height unendlich ist. Das ist legal. Wenn "Measure " aufgerufen wird, ist die Logik, dass die DesiredSize-Eigenschaft als Mindestmaß festgelegt wird: was an Measure übergeben wurde, oder die natürliche Größe dieses Elements aus Faktoren wie explizit festgelegter Höhe und Breite.

Hinweis

Die interne Logik von StackPanel weist auch dieses Verhalten auf: StackPanel übergibt einen unendlichen Dimensionswert an Measure für untergeordnete Elemente, was angibt, dass es keine Einschränkung für untergeordnete Elemente in der Ausrichtungsdimension gibt. StackPanel passt sich in der Regel dynamisch an, um alle untergeordneten Elemente in einem Stapel aufzunehmen, der in dieser Dimension wächst.

Das Panel selbst kann jedoch keine Größe mit einem unendlichen Wert aus MeasureOverride zurückgeben, der eine Ausnahme während des Layouts auslöst. Ein Teil der Logik besteht also darin, die maximale Höhe zu ermitteln, die von allen untergeordneten Elementen angefordert wird, und diese Höhe als Zellenhöhe zu verwenden, falls diese nicht bereits aus den eigenen Größenbeschränkungen des Panels stammt. Hier ist die Hilfsfunktion LimitUnboundedSize , auf die im vorherigen Code verwiesen wurde, die dann die maximale Zellhöhe verwendet und verwendet, um dem Panel eine endliche Höhe zurückzugeben, sowie sicherzustellen, dass es cellheight sich um eine endliche Zahl handelt, bevor der Anordnungsdurchlauf initiiert wird:

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

Das erforderliche Muster einer ArrangeOverride-Implementierung ist die Schleife durch jedes Element in Panel.Children. Rufen Sie immer die Arrange-Methode für jedes dieser Elemente auf.

Beachten Sie, dass es nicht so viele Berechnungen wie in MeasureOverride gibt. Das ist typisch. Die Größe von untergeordneten Elementen ist bereits aus der eigenen MeasureOverride-Logik des Panels oder aus dem DesiredSize-Wert der einzelnen untergeordneten Elemente bekannt, die während des Measuredurchlaufs festgelegt werden. Wir müssen jedoch trotzdem die Position innerhalb des Panels festlegen, an der jedes untergeordnete Element angezeigt wird. In einem typischen Bereich sollte jedes untergeordnete Element an einer anderen Position gerendert werden. Ein Panel, das überlappende Elemente erstellt, ist für typische Szenarien nicht wünschenswert (obwohl es nicht aus der Frage ist, Panels mit gezielten Überschneidungen zu erstellen, wenn dies wirklich Ihr beabsichtigtes Szenario ist).

Dieses Panel wird nach dem Konzept von Zeilen und Spalten angeordnet. Die Anzahl der Zeilen und Spalten wurde bereits berechnet (es war für die Messung erforderlich). Jetzt tragen also die Form der Zeilen und Spalten sowie die bekannten Größen jeder Zelle zur Logik der Definition einer Renderingposition (der anchorPoint) für jedes Element bei, das dieses Panel enthält. Dieser Punkt wird zusammen mit der größe, die bereits aus der Messung bekannt ist, als die beiden Komponenten verwendet, die ein Rect erstellen. Rect ist der Eingabetyp für "Anordnen".

Panels müssen ihre Inhalte manchmal ausschneiden. Wenn dies der Fall ist, ist die beschnittene Größe die Größe, die in DesiredSize vorhanden ist, da die Measure-Logik sie als Mindestmaß an Measure oder andere natürliche Größenfaktoren festlegt. Daher müssen Sie in der Regel während der Anordnung nicht speziell nach Clipping suchen. Der Clipping erfolgt nur basierend auf der Übergabe der DesiredSize-Option an jeden Arrange-Aufruf.

Sie benötigen nicht immer eine Zählung, während Sie die Schleife durchlaufen, wenn alle Informationen, die Sie zum Definieren der Renderingposition benötigen, auf andere Wege bekannt sind. In der Canvas-Layoutlogik spielt die Position in der Children-Auflistung z. B. keine Rolle. Alle Informationen, die zum Positionieren der einzelnen Elemente in einer Canvas erforderlich sind, sind durch Lesen von Canvas.Left und Canvas.Top Werte untergeordneter Elemente als Teil der Anordnungslogik bekannt. Die BoxPanel Logik benötigt eine Zählung, um mit der Anzahl zu vergleichen, damit bekannt ist, wann eine neue Zeile begonnen und der Y-Wert versetzt wird.

Es ist typisch, dass die Eingabe finalSize und die Größe, die Sie aus einer ArrangeOverride-Implementierung zurückgeben, identisch sind. Weitere Informationen dazu finden Sie im Abschnitt "ArrangeOverride" der Übersicht über benutzerdefinierte XAML-Panels.

Eine Einschränkung: Steuern der Zeilen- und Spaltenanzahl

Sie können diesen Bereich wie jetzt kompilieren und verwenden. Wir fügen jedoch eine weitere Einschränkung hinzu. Im gerade gezeigten Code platziert die Logik die zusätzliche Zeile oder Spalte auf der Seite, die am längsten im Seitenverhältnis ist. Für eine größere Kontrolle über die Formen von Zellen kann es jedoch wünschenswert sein, einen 4x3-Satz von Zellen anstelle von 3x4 auszuwählen, auch wenn das eigene Seitenverhältnis des Panels "Hochformat" lautet. Daher fügen wir eine optionale Abhängigkeitseigenschaft hinzu, die der Panel-Consumer festlegen kann, um dieses Verhalten zu steuern. Hier ist die Definition der Abhängigkeitseigenschaft, die sehr einfach ist:

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

Und unten sehen Sie, wie sich die Verwendung Orientation der Messlogik in MeasureOverride. Wirklich alles, was es tut, ändert, wie rowcount und colcount von dem tatsächlichen Seitenverhältnis abgeleitet maxrc wird, und es gibt entsprechende Größenunterschiede für jede Zelle. Ist Orientation "Vertikal" (Standard), wird der Wert des tatsächlichen Seitenverhältnisses invertiert, bevor er für zeilen- und spaltenzählungen für das Rechtecklayout "Hochformat" verwendet wird.

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

Das Szenario für BoxPanel

Das besondere Szenario BoxPanel ist, dass es sich um ein Panel handelt, in dem einer der wichtigsten Determinanten der Aufteilung des Raums darin besteht, die Anzahl der untergeordneten Elemente zu kennen und den bekannten verfügbaren Platz für das Panel zu dividieren. Panels sind innate rechteckigen Formen. Viele Panels funktionieren, indem sie diesen Rechteckraum in weitere Rechtecke unterteilen; Dies ist die Funktion von Grid für seine Zellen. In Grids Fall wird die Größe der Zellen durch ColumnDefinition- und RowDefinition-Werte festgelegt, und Elemente deklarieren die genaue Zelle, in die sie mit den angefügten Eigenschaften "Grid.Row" und "Grid.Column" eingefügt werden. Das Abrufen eines guten Layouts aus einem Raster erfordert in der Regel, die Anzahl der untergeordneten Elemente vorher zu kennen, sodass genügend Zellen vorhanden sind und jedes untergeordnete Element seine angefügten Eigenschaften so festlegt, dass es in seine eigene Zelle passt.

Aber was geschieht, wenn die Anzahl der untergeordneten Elemente dynamisch ist? Das ist sicher möglich; Ihr App-Code kann Sammlungen Elemente hinzufügen, als Reaktion auf jede dynamische Laufzeitbedingung, die Sie als wichtig genug erachten, um die Benutzeroberfläche zu aktualisieren. Wenn Sie Datenbindung zum Sichern von Sammlungen/Geschäftsobjekten verwenden, wird das Abrufen solcher Aktualisierungen und das Aktualisieren der Benutzeroberfläche automatisch behandelt, sodass dies häufig die bevorzugte Technik ist (siehe Datenbindung im Detail).

Aber nicht alle App-Szenarien eignen sich für die Datenbindung. Manchmal müssen Sie zur Laufzeit neue UI-Elemente erstellen und sichtbar machen. BoxPanel ist für dieses Szenario vorgesehen. Eine änderung der Anzahl untergeordneter Elemente ist kein Problem, BoxPanel da sie die untergeordnete Anzahl in Berechnungen verwendet, und passt sowohl die vorhandenen als auch die neuen untergeordneten Elemente in ein neues Layout an, sodass sie alle passen.

Ein erweitertes Szenario zum Erweitern BoxPanel weiterer (nicht hier gezeigt) könnte sowohl dynamische untergeordnete Elemente berücksichtigen als auch die DesiredSize eines untergeordneten Elements als stärkerer Faktor für die Größenanpassung einzelner Zellen verwenden. In diesem Szenario können unterschiedliche Zeilen- oder Spaltengrößen oder Nicht-Raster-Shapes verwendet werden, sodass weniger Platz "verschwendet" wird. Dies erfordert eine Strategie, wie mehrere Rechtecke verschiedener Größen und Seitenverhältnisse in ein enthaltenes Rechteck passen können, sowohl für Ästhetik als auch für kleinste Größe. BoxPanel tut das nicht; Es wird eine einfachere Technik zum Teilen des Raums verwendet. BoxPanelDie Technik besteht darin, die kleinste quadratische Zahl zu bestimmen, die größer als die untergeordnete Anzahl ist. Beispielsweise würden 9 Elemente in ein Quadrat mit 3 x 3 passen. Für 10 Elemente ist ein Quadrat von 4 x 4 erforderlich. Sie können elemente jedoch häufig anpassen, während Sie weiterhin eine Zeile oder Spalte des Anfangsquadecks entfernen, um Platz zu sparen. In the count=10 example, that fits in a 4x3 or 3x4 rectangle.

Vielleicht fragen Sie sich, warum das Panel nicht stattdessen 5x2 für 10 Elemente auswählen würde, da dies ordnungsgemäß zur Elementnummer passt. In der Praxis werden Panels jedoch als Rechtecke angepasst, die selten ein stark ausgerichtetes Seitenverhältnis aufweisen. Die Technik der kleinsten Quadrate ist eine Möglichkeit, die Größenanpassungslogik so zu verzerren, dass sie gut mit typischen Layoutformen funktioniert und nicht die Größenanpassung animiert, wo die Zellen-Shapes ungerade Seitenverhältnisse erhalten.

Referenz

Konzepte