Partager via


BoxPanel, exemple de panneau personnalisé

Apprenez à écrire du code pour une classe Panel personnalisée, en implémentant des méthodes ArrangeOverride et MeasureOverride et en utilisant la propriété Children.

API importantes : Panel, ArrangeOverride, MeasureOverride

L’exemple de code montre une implémentation de panneau personnalisé, mais nous ne consacrons pas beaucoup de temps à expliquer les concepts de disposition qui influencent la façon dont vous pouvez personnaliser un panneau pour différents scénarios de disposition. Si vous souhaitez plus d’informations sur ces concepts de disposition et la façon dont ils peuvent s’appliquer à votre scénario de disposition particulier, consultez la vue d’ensemble des panneaux personnalisés XAML.

Un panneau est un objet qui fournit un comportement de disposition pour les éléments enfants qu’il contient, lorsque le système de disposition XAML s’exécute et que l’interface utilisateur de votre application est affichée. Vous pouvez définir des panneaux personnalisés pour la disposition XAML en dérivant une classe personnalisée de la classe Panel. Vous fournissez un comportement pour votre panneau en remplaçant les méthodes ArrangeOverride et MeasureOverride, en fournissant une logique qui mesure et organise les éléments enfants. Cet exemple dérive du panneau. Lorsque vous démarrez à partir de Panel, les méthodes ArrangeOverride et MeasureOverride n’ont pas de comportement de démarrage. Votre code fournit la passerelle par laquelle les éléments enfants deviennent connus du système de disposition XAML et sont rendus dans l’interface utilisateur. Par conséquent, il est vraiment important que votre code compte pour tous les éléments enfants et suit les modèles attendus par le système de disposition.

Votre scénario de disposition

Lorsque vous définissez un panneau personnalisé, vous définissez un scénario de disposition.

Un scénario de disposition est exprimé par :

  • Que fera le panneau lorsqu’il possède des éléments enfants
  • Lorsque le panneau a des contraintes sur son propre espace
  • Comment la logique du panneau détermine toutes les mesures, positionnement, positions et dimensionnements qui aboutissent à une disposition d’interface utilisateur rendue des enfants

Dans cet esprit, l’exemple BoxPanel présenté ici concerne un scénario particulier. Dans l’intérêt de conserver le code avant tout dans cet exemple, nous n’expliquerons pas encore le scénario en détail, et nous nous concentrerons plutôt sur les étapes nécessaires et les modèles de codage. Si vous souhaitez d’abord en savoir plus sur le scénario, passez à « Le scénario pour BoxPanel», puis revenez au code.

Commencer par dériver du panneau

Commencez par dériver une classe personnalisée à partir de Panel. Probablement le moyen le plus simple de le faire consiste à définir un fichier de code distinct pour cette classe, à l’aide des options de menu contextuel Ajouter | une nouvelle classe d’élément | pour un projet à partir de l’Explorateur de solutions dans Microsoft Visual Studio. Nommez la classe (et le fichier) BoxPanel.

Le fichier modèle d’une classe ne commence pas par beaucoup d’instructions using, car il n’est pas destiné spécifiquement aux applications Windows. Tout d’abord, ajoutez des instructions using . Le fichier de modèle commence également par quelques instructions using que vous n’avez probablement pas besoin et qui peuvent être supprimées. Voici une liste suggérée d’instructions utilisant qui peuvent résoudre les types dont vous aurez besoin pour le code de panneau personnalisé classique :

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

Maintenant que vous pouvez résoudre le panneau, faites-le la classe de base de BoxPanel. En outre, rendre BoxPanel public :

public class BoxPanel : Panel
{
}

Au niveau de la classe, définissez des valeurs int et double qui seront partagées par plusieurs de vos fonctions logiques, mais qui n’auront pas besoin d’être exposées en tant qu’API publique. Dans l’exemple, ils sont nommés : maxrc, , colcountrowcount, cellwidthcellheight, maxcellheight, , . aspectratio

Une fois que vous avez effectué cette opération, le fichier de code complet ressemble à ceci (en supprimant les commentaires sur l’utilisation, maintenant que vous savez pourquoi nous les avons) :

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

À partir de là, nous allons vous montrer une définition de membre à la fois, soit qu’une méthode substitue ou quelque chose de support tel qu’une propriété de dépendance. Vous pouvez les ajouter au squelette ci-dessus dans n’importe quel ordre.

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

Le modèle nécessaire d’une implémentation MeasureOverride est la boucle de chaque élément dans Panel.Children. Appelez toujours la méthode Measure sur chacun de ces éléments. La mesure a un paramètre de type Size. Ce que vous passez ici est la taille que votre panneau s’engage à avoir disponible pour cet élément enfant particulier. Ainsi, avant de pouvoir effectuer la boucle et commencer à appeler Measure, vous devez savoir combien d’espace chaque cellule peut consacrer. À partir de la méthode MeasureOverride elle-même, vous disposez de la valeur availableSize . Il s’agit de la taille utilisée par le parent du panneau lorsqu’il a appelé Measure, qui était le déclencheur de cette MeasureOverride appelée en premier lieu. Par conséquent, une logique classique consiste à concevoir un schéma dans lequel chaque élément enfant divise l’espace de la taille globale disponible du panneau. Vous passez ensuite chaque division de taille à Mesure de chaque élément enfant.

La façon dont BoxPanel la taille divise est assez simple : elle divise son espace en un certain nombre de boîtes qui sont largement contrôlées par le nombre d’éléments. Les zones sont dimensionnées en fonction du nombre de lignes et de colonnes et de la taille disponible. Parfois, une ligne ou une colonne d’un carré n’est pas nécessaire. Il est donc supprimé et le panneau devient un rectangle plutôt que carré en termes de ligne : ratio de colonne. Pour plus d’informations sur l’arrivée de cette logique, passez à « Le scénario pour BoxPanel ».

Qu’est-ce que la passe de mesure fait ? Elle définit une valeur pour la propriété DesiredSize en lecture seule sur chaque élément appelé Measure. Avoir une valeur DesiredSize est peut-être importante une fois que vous accédez à la passe d’organisation, car le DesiredSize communique ce que la taille peut ou doit être lors de l’organisation et dans le rendu final. Même si vous n’utilisez pas DesiredSize dans votre propre logique, le système en a toujours besoin.

Il est possible que ce panneau soit utilisé lorsque le composant hauteur de availableSize n’est pas lié. Si c’est vrai, le panneau n’a pas de hauteur connue à diviser. Dans ce cas, la logique de la passe de mesure informe chaque enfant qu’il n’a pas encore de hauteur limitée. Pour ce faire, transmettez une taille à l’appel de mesure pour les enfants où Size.Height est infini. C’est légal. Lorsque la mesure est appelée, la logique est que le DesiredSize est défini comme le minimum suivant : ce qui a été passé à Measure, ou la taille naturelle de cet élément à partir de facteurs tels que la hauteur et la largeur définies explicitement.

Remarque

La logique interne de StackPanel a également ce comportement : StackPanel transmet une valeur de dimension infinie à Mesurer sur les enfants, indiquant qu’il n’existe aucune contrainte sur les enfants dans la dimension d’orientation. StackPanel se dimensionne généralement dynamiquement pour prendre en charge tous les enfants d’une pile qui augmente dans cette dimension.

Toutefois, le panneau lui-même ne peut pas retourner une taille avec une valeur infinie de MeasureOverride ; qui lève une exception pendant la disposition. Par conséquent, une partie de la logique consiste à déterminer la hauteur maximale que toutes les demandes enfants et à utiliser cette hauteur comme hauteur de cellule dans le cas où les contraintes de taille du panneau ne proviennent pas déjà des propres contraintes de taille du panneau. Voici la fonction LimitUnboundedSize d’assistance qui a été référencée dans le code précédent, qui prend ensuite cette hauteur de cellule maximale et l’utilise pour donner au panneau une hauteur finie à retourner, ainsi que l’assurance qu’il cellheight s’agit d’un nombre fini avant que la passe d’organisation soit lancée :

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

Le modèle nécessaire d’une implémentation ArrangeOverride est la boucle de chaque élément dans Panel.Children. Appelez toujours la méthode Arrange sur chacun de ces éléments.

Notez comment il n’y a pas autant de calculs que dans MeasureOverride ; c’est typique. La taille des enfants est déjà connue à partir de la propre logique MeasureOverride du panneau ou de la valeur DesiredSize de chaque enfant définie pendant la passe de mesure. Toutefois, nous devons toujours décider de l’emplacement dans le panneau où chaque enfant apparaîtra. Dans un panneau classique, chaque enfant doit s’afficher à une position différente. Un panneau qui crée des éléments qui se chevauchent n’est pas souhaitable pour les scénarios classiques (bien qu’il ne soit pas hors de question de créer des panneaux qui ont des chevauchements intentionux, si c’est vraiment votre scénario prévu).

Ce panneau organise le concept de lignes et de colonnes. Le nombre de lignes et de colonnes a déjà été calculé (il était nécessaire pour la mesure). Ainsi, la forme des lignes et des colonnes ainsi que les tailles connues de chaque cellule contribuent à la logique de définition d’une position de rendu (le anchorPoint) pour chaque élément que ce panneau contient. Ce point, ainsi que la taille déjà connue à partir de la mesure, sont utilisés comme deux composants qui construisent un Rect. Rect est le type d’entrée pour Arrange.

Les panneaux doivent parfois découper leur contenu. Si c’est le cas, la taille clippée est la taille présente dans DesiredSize, car la logique Mesure la définit comme la valeur minimale de ce qui a été passé à Measure ou à d’autres facteurs de taille naturelle. Par conséquent, vous n’avez généralement pas besoin de vérifier spécifiquement la capture lors de l’organisation. La capture se produit simplement en fonction du passage de LairedSize à chaque appel Arrange.

Vous n’avez pas toujours besoin d’un nombre tout en parcourant la boucle si toutes les informations dont vous avez besoin pour définir la position de rendu sont connues par d’autres moyens. Par exemple, dans la logique de disposition de canevas, la position de la collection Children n’a pas d’importance. Toutes les informations nécessaires pour positionner chaque élément d’un canevas sont connues en lisant Canvas.Left et en Canvas.Top valeurs d’enfants dans le cadre de la logique d’organisation. La BoxPanel logique a besoin d’un nombre à comparer au colcount afin qu’il soit connu quand commencer une nouvelle ligne et décaler la valeur y .

Il est courant que l’entrée finalSize et la taille que vous retournez à partir d’une implémentation ArrangeOverride sont identiques. Pour plus d’informations sur la raison pour laquelle, consultez la section « ArrangeOverride » de la vue d’ensemble des panneaux personnalisés XAML.

Affinement : contrôle du nombre de lignes et de colonnes

Vous pouvez compiler et utiliser ce panneau comme il l’est maintenant. Toutefois, nous allons ajouter un autre affinement. Dans le code affiché, la logique place la ligne ou la colonne supplémentaire du côté qui est la plus longue en proportion. Mais pour un meilleur contrôle sur les formes de cellules, il peut être souhaitable de choisir un ensemble de cellules 4x3 au lieu de 3x4 même si le rapport d’aspect du panneau est « portrait ». Nous allons donc ajouter une propriété de dépendance facultative que le consommateur du panneau peut définir pour contrôler ce comportement. Voici la définition de propriété de dépendance, qui est très simple :

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

Vous trouverez ci-dessous comment l’utilisation a Orientation un impact sur la logique de mesure dans MeasureOverride. Tout ce qu’il fait est de changer comment rowcount et colcount sont dérivés maxrc et le vrai rapport d’aspect, et il existe des différences de taille correspondantes pour chaque cellule en raison de cela. Lorsqu’il Orientation s’agit de Vertical (valeur par défaut), il inverse la valeur du rapport d’aspect vrai avant de l’utiliser pour le nombre de lignes et de colonnes pour notre disposition de rectangle « portrait ».

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

Scénario pour BoxPanel

Le scénario particulier est BoxPanel qu’il s’agit d’un panneau où l’un des principaux déterminants de la façon de diviser l’espace consiste à connaître le nombre d’éléments enfants et à diviser l’espace disponible connu pour le panneau. Les panneaux sont des formes de rectangle innée. De nombreux panneaux fonctionnent en divisant cet espace rectangle en rectangles supplémentaires ; c’est ce que Grid fait pour ses cellules. Dans le cas de Grid, la taille des cellules est définie par les valeurs ColumnDefinition et RowDefinition, et les éléments déclarent la cellule exacte dans laquelle ils entrent avec les propriétés jointes Grid.Row et Grid.Column. L’obtention d’une bonne disposition à partir d’une grille nécessite généralement de connaître le nombre d’éléments enfants au préalable, afin qu’il y ait suffisamment de cellules et que chaque élément enfant définit ses propriétés jointes pour s’adapter à sa propre cellule.

Mais que se passe-t-il si le nombre d’enfants est dynamique ? C’est certainement possible ; votre code d’application peut ajouter des éléments à des regroupements, en réponse à toute condition d’exécution dynamique que vous envisagez d’être suffisamment importante pour être en mesure de mettre à jour votre interface utilisateur. Si vous utilisez la liaison de données pour sauvegarder des collections/objets métier, obtenir ces mises à jour et mettre à jour l’interface utilisateur est gérée automatiquement, de sorte que c’est souvent la technique préférée (voir Liaison de données en profondeur).

Mais tous les scénarios d’application ne se prêtent pas à la liaison de données. Parfois, vous devez créer de nouveaux éléments d’interface utilisateur au moment de l’exécution et les rendre visibles. BoxPanel convient dans ce scénario. Un nombre changeant d’éléments enfants n’est pas un problème car BoxPanel il utilise le nombre d’enfants dans les calculs et ajuste à la fois les éléments existants et nouveaux enfants dans une nouvelle disposition afin qu’ils s’adaptent tous.

Un scénario avancé d’extension BoxPanel supplémentaire (non illustré ici) peut prendre en charge les enfants dynamiques et utiliser la dimensionnement souhaité d’un enfant comme facteur plus fort pour le dimensionnement des cellules individuelles. Ce scénario peut utiliser des tailles de ligne ou de colonne variables ou des formes non grilles afin qu’il y ait moins d’espace « gaspiller ». Cela nécessite une stratégie pour la façon dont plusieurs rectangles de différentes tailles et ratios d’aspect peuvent tous s’adapter à un rectangle contenant à la fois pour l’esthétique et la plus petite taille. BoxPanel n’offre pas cette fonctionnalité. Il utilise une technique plus simple pour diviser l’espace. La technique employée par BoxPanel consiste à déterminer le plus petit nombre de carré supérieur au nombre d’enfants. Par exemple, 9 éléments s’adapteraient à un carré de 3x3. 10 éléments nécessitent un carré 4x4. Toutefois, vous pouvez souvent ajuster les éléments tout en supprimant une ligne ou une colonne du carré de départ, pour économiser de l’espace. Dans l’exemple count=10, qui correspond à un rectangle 4x3 ou 3x4.

Vous pouvez vous demander pourquoi le panneau ne choisirait pas plutôt 5x2 pour 10 éléments, car cela correspond au numéro d’élément correctement. Toutefois, dans la pratique, les panneaux sont dimensionnés sous forme de rectangles qui ont rarement un rapport d’aspect fortement orienté. La technique des moindres carrés est un moyen de biaiser la logique de dimensionnement pour fonctionner correctement avec les formes de disposition classiques et ne pas encourager le dimensionnement où les formes de cellule obtiennent des proportions impaires.

Référence

Concepts