Freigeben über


BoxPanel, ein beispiel für ein benutzerdefiniertes Panel

Erfahren Sie, wie Sie Code für eine benutzerdefinierte Panel--Klasse schreiben, ArrangeOverride-- und MeasureOverride--Methoden 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 unter Ü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 aus der klasse Panel ableiten. Sie stellen Verhalten für Ihr Panel bereit, indem Sie die ArrangeOverride- und MeasureOverride- Methoden überschreiben und Logik bereitstellen, die die untergeordneten Elemente misst und anordnet. Dieses Beispiel stammt aus Panel. Wenn Sie mit Panel-beginnen, weisen ArrangeOverride und MeasureOverride Methoden 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 Elemente berücksichtigt und den angegebenen Mustern folgt, die das Layoutsystem erwartet.

Ihr Layoutszenario

Wenn Sie ein benutzerdefiniertes Panel definieren, legen Sie ein Layoutszenario fest.

Ein Layoutszenario wird durch Folgendes ausgedrückt:

  • Was das Panel machen wird, wenn es untergeordnete Elemente hat.
  • 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 Benutzeroberflächen-Layout (UI-Layout) untergeordneter Elemente führen

Mit diesen Überlegungen im Hinterkopf ist die hier gezeigte BoxPanel für 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 dem Ableiten von Panel

Beginnen Sie, indem Sie eine benutzerdefinierte Klasse von Panelableiten. Dies ist wahrscheinlich die einfachste Möglichkeit, eine separate Codedatei für diese Klasse mithilfe der Hinzufügen | Neuen Element | Klasse Kontextmenüoptionen 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 -Anweisungen, die verwenden, da sie nicht speziell für Windows-Apps bestimmt ist. Fügen Sie also zunächst mithilfe von Anweisungen hinzu. Die Vorlagendatei enthält auch einige Anweisungen vom Typ mit, die Sie wahrscheinlich nicht benötigen und gelöscht werden können. Hier ist ein Vorschlag für eine Liste von Anweisungen, die mit arbeiten, um die Typen von aufzulösen, 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

Nachdem Sie nun Panel-auflösen können, stellen Sie sie zur Basisklasse BoxPanel. Machen Sie außerdem BoxPanel öffentlich:

public class BoxPanel : Panel
{
}

Definieren Sie auf Klassenebene einige int und doppelte Werte, die von mehreren Ihrer Logikfunktionen gemeinsam verwendet werden, die jedoch nicht als öffentliche API verfügbar gemacht werden müssen. Im Beispiel werden diese benannt: maxrc, rowcount, colcount, cellwidth, cellheight, maxcellheight, aspectratio.

Nachdem Sie dies getan haben, sieht die vollständige Codedatei wie folgt aus (Entfernen von Kommentaren zu mithilfe von, 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 methode Measure für jedes dieser Elemente auf. Measure hat einen Parameter vom Typ Größe. Die Größe, die Sie hier angeben, ist die Größe, die Ihr Panel bereitstellt, um für dieses bestimmte untergeordnete Element verfügbar zu sein. Bevor Sie also die Schleife ausführen und mit dem Aufrufen Measurebeginnen können, müssen Sie wissen, wie viel Speicherplatz jede Zelle widmen kann. Aus der MeasureOverride- Methode selbst verfügen Sie über den availableSize Wert. Dies ist die Größe, die das übergeordnete Element des Panels verwendet hat, als es Measureaufgerufen hat, was den Aufruf dieser MeasureOverride überhaupt ausgelöst hat. Eine typische Logik besteht also darin, ein Schema zu entwickeln, bei dem jedes untergeordnete Element den Raum der gesamten verfügbarSizedividiert. Anschließend übergeben Sie jede Größenteilung an Measure jedes untergeordneten Elements.

Wie BoxPanel die Größe aufteilt, ist relativ einfach: Es teilt seinen Raum in eine Anzahl von Feldern auf, die größtenteils durch die Anzahl der Elemente bestimmt wird. Boxen werden basierend auf der Zeilen- und Spaltenanzahl sowie der verfügbaren Größe festgelegt. Manchmal wird eine Zeile oder Spalte aus einem Quadrat nicht benötigt, daher wird sie verworfen, und das Panel wird anstelle eines Quadrats zu einem Rechteck in Bezug auf das Zeilen : Spalten-Verhältnis. Weitere Informationen darüber, wie diese Logik zustande kam, finden Sie weiter bei "Das Szenario für BoxPanel".

Was bewirkt also die Maßnahme? Es legt einen Wert für die schreibgeschützte DesiredSize-Eigenschaft für jedes Element fest, bei dem Measure aufgerufen wurde. Wenn Sie einen DesiredSize Wert haben, kann es möglicherweise wichtig sein, sobald Sie zum Anordnungsdurchlauf gelangen, da die DesiredSize kommuniziert, was die Größe beim Anordnen und im endgültigen Rendering sein kann oder sein 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öhekomponente 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 des Messvorgangs jedes untergeordnete Element, dass es noch keine begrenzte Höhe hat. Eine Size wird an den Measure Aufruf für Kindelemente weitergegeben, bei denen Size.Height unendlich ist. Das ist legal. Wenn Measure- aufgerufen wird, besteht die Logik darin, die DesiredSize- als das Minimum festzulegen: entweder das, was an Measureübergeben wurde, oder die natürliche Größe dieses Elements aus Faktoren wie der explizit festgelegten Height und der Width.

Hinweis

Die interne Logik von StackPanel- weist auch dieses Verhalten auf: StackPanel übergibt einen unendlichen Dimensionswert an Measure für untergeordnete Elemente, was darauf hinweist, dass keine Einschränkung für untergeordnete Elemente in der Ausrichtungsdimension besteht. StackPanel- passt sich in der Regel dynamisch in seiner Größe an, um alle untergeordneten Elemente in einem Stapel aufzunehmen, der in dieser Dimension wächst.

Das Panel selbst kann jedoch keine Size- mit einem unendlichen Wert aus MeasureOverride-zurückgeben, da dies während des Layouts eine Ausnahme 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. Sie verwendet die maximale Zellhöhe, um dem Panel eine bestimmte endliche Höhe zu geben, und stellt sicher, dass cellheight eine endliche Zahl ist, bevor der Anordnungsprozess 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 Methode Arrange für jedes dieser Elemente auf.

Beachten Sie, dass es nicht so viele Berechnungen gibt wie in MeasureOverride; das ist typisch. Die Größe der untergeordneten Elemente ist bereits aus der eigenen MeasureOverride Logik des Panels oder aus der DesiredSize Wert jedes untergeordneten Elements bekannt, das während des Messdurchlaufs festgelegt wurde. Wir müssen jedoch noch entscheiden, an welcher Stelle innerhalb des Panels jedes Kind angezeigt wird. In einem typischen Panel sollte jedes Kind an einer anderen Position gerendert werden. Ein Panel, das überlappende Elemente erstellt, ist für typische Szenarien nicht wünschenswert (obwohl es nicht ausgeschlossen ist, Panels mit gezielten Überschneidungen zu erstellen, wenn das 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 (da dies für die Messung notwendig war). So tragen jetzt die Form der Zeilen und Spalten sowie die bekannten Größen jeder Zelle zur Logik der Definition einer Position für das Rendern (anchorPoint) jedes Elements bei, das dieses Panel enthält. Diese Pointwerden zusammen mit der Größe, die bereits aus der Messung bekannt ist, als zwei Komponenten verwendet, um ein Rectzu konstruieren. Rect ist der Eingabetyp für die Anordnung von .

Panels müssen ihre Inhalte manchmal abschneiden. Wenn dies der Fall ist, ist die beschnittene Größe die Größe, die in DesiredSizevorhanden ist, da die Measure-Logik sie als das Minimum der übergebenen Werte an Measureoder andere natürliche Größenfaktoren festlegt. Daher müssen Sie in der Regel während Anordnennicht gezielt nach Clipping suchen; das Clipping erfolgt einfach durch die Übergabe der DesiredSize an jeden Anordnen Aufruf.

Sie benötigen nicht immer einen Zähler, während Sie die Schleife durchlaufen, wenn alle Informationen, die Sie zur Definition der Renderingposition benötigen, auf andere Weise bekannt sind. In Canvas- Layoutlogik spielt die Position in der Children-Auflistung keine Rolle. Alle Informationen, die erforderlich sind, um jedes Element in einem Canvas- zu positionieren, werden als Teil der Anordnungslogik durch das Lesen der Canvas.Left- und Canvas.Top Werte der untergeordneten Elemente erfasst. Die BoxPanel Logik bedarf zufällig einer Zählung, um mit der Spaltenanzahl verglichen zu werden, damit bekannt ist, wann eine neue Zeile begonnen wird und der y Wert versetzt wird.

Es ist üblich, dass die Eingabe finalSize und die Size, die Sie von einer ArrangeOverride Implementierung zurückgeben, die gleichen sind. Weitere Informationen dazu finden Sie in der Übersicht über benutzerdefinierte XAML-Panels im Abschnitt "ArrangeOverride" .

Eine Verfeinerung: Kontrolle über die Zeilen- vs. Spaltenanzahl

Sie können dieses Panel so wie es jetzt ist kompilieren und verwenden. Wir fügen jedoch eine weitere Verfeinerung 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();
    }
}

Nachfolgend sehen Sie, wie sich die Verwendung von Orientation auf die Messlogik in MeasureOverrideauswirkt. Tatsächlich ändert es nur, wie rowcount und colcount aus maxrc und dem tatsächlichen Seitenverhältnis abgeleitet werden, und daher gibt es entsprechende Größenunterschiede für jede Zelle. Wenn Orientationvertikalen (Standard) ist, wird der Wert des tatsächlichen Seitenverhältnisses invertiert, bevor er für die Anzahl der Zeilen und Spalten im "Hochformat"-Rechteck-Layout 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 für BoxPanel ist, dass es sich um ein Panel handelt, in dem einer der bestimmenden Faktoren für die Aufteilung des Raums darin besteht, die Anzahl der untergeordneten Elemente zu kennen und den bekannten verfügbaren Platz für das Panel aufzuteilen. Panels sind von Natur aus rechteckige Formen. Viele Panels funktionieren, indem sie diese Rechtecksfläche weiter in Rechtecke unterteilen; das ist, was Grid für seine Zellen macht. In Fall von Gridwird die Größe der Zellen durch ColumnDefinition- und RowDefinition--Werte festgelegt, und Elemente deklarieren die genaue Zelle, in die sie mit Grid.Row- und "Grid.Column" angefügten Eigenschaften gehen. Ein gutes Layout aus einem Grid zu erhalten, erfordert gewöhnlich, die Anzahl der untergeordneten Elemente im Voraus zu kennen, damit ausreichend Zellen bereitgestellt werden können und jedes untergeordnete Element seine angefügten Eigenschaften so einstellt, dass es in seine jeweilige Zelle passt.

Aber was geschieht, wenn die Anzahl der Kinder 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 für zugrunde liegende Sammlungen/Geschäftsobjekte verwenden, wird das Abrufen solcher Updates und die Benutzeroberfläche zu aktualisieren automatisch gehandhabt, sodass dies oft die bevorzugte Technik darstellt (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 gilt für dieses Szenario. Eine Veränderung der Anzahl untergeordneter Elemente ist kein Problem für BoxPanel, da es die Anzahl der untergeordneten Elemente in Berechnungen einbezieht und sowohl vorhandene als auch neue untergeordnete Elemente in einem neuen Layout so anpasst, dass sie alle passen.

Ein erweitertes Szenario zur weiteren Erweiterung von BoxPanel (nicht hier gezeigt) könnte sowohl dynamische Kindelemente berücksichtigen als auch die DesiredSize eines Kindelements als bedeutenderen Faktor für die Größenanpassung einzelner Zellen verwenden. In diesem Szenario können unterschiedliche Zeilen- oder Spaltengrößen sowie nicht-gitterförmige Strukturen verwendet werden, damit weniger Platz *verschwendet* wird. Dies erfordert eine Strategie, wie mehrere Rechtecke verschiedener Größen und Seitenverhältnisse in ein umschließendes Rechteck passen können, sowohl aus ästhetischen Gründen als auch um die kleinstmögliche Größe zu erreichen. BoxPanel tut das nicht; es verwendet eine einfachere Technik zum Teilen des Raums. Die Technik von BoxPanelbesteht darin, die kleinste quadratische Zahl zu bestimmen, die größer als die Anzahl der Kinder 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 jedoch häufig Elemente anpassen, während Sie weiterhin eine Zeile oder eine Spalte des Ausgangsquadrats entfernen, um Platz zu sparen. Im Beispiel mit count=10 passt das in ein 4x3- oder 3x4-Rechteck.

Vielleicht fragen Sie sich, warum das Panel nicht stattdessen 5x2 für 10 Elemente wählen würde, da dies gut zur Anzahl der Elemente passt. In der Praxis werden Panels jedoch als Rechtecke bemessen, die selten ein stark ausgeprägtes Seitenverhältnis aufweisen. Die Methode der kleinsten Quadrate ist eine Möglichkeit, die Größenanpassungslogik so zu beeinflussen, dass sie gut mit typischen Layoutformen funktioniert und nicht die Größenanpassung dort fördert, wo die Zellformen ungerade Seitenverhältnisse erhalten.

Referenz

Konzepte