Compartir a través de


BoxPanel, un panel personalizado de ejemplo

Aprenda a escribir código para una clase de panel personalizada , implementando los métodos ArrangeOverride y MeasureOverride, y utilizando la propiedad Children.

APIs importantes: Panel, ArrangeOverride,MeasureOverride

El código de ejemplo muestra una implementación de panel personalizada, pero no dedicamos mucho tiempo a explicar los conceptos de diseño que influyen en cómo puede personalizar un panel para distintos escenarios de diseño. Si quieres obtener más información sobre estos conceptos de diseño y cómo podrían aplicarse a tu escenario de diseño concreto, consulta Información general sobre los paneles personalizados XAML.

Un panel es un objeto que proporciona un comportamiento de diseño para los elementos secundarios que contiene, cuando se ejecuta el sistema de diseño XAML y se representa la interfaz de usuario de la aplicación. Puedes definir paneles personalizados para el diseño XAML derivando una clase personalizada de la clase Panel . Para proporcionar un comportamiento para el panel, reemplace los métodos ArrangeOverride y MeasureOverride, proporcionando lógica que mide y organiza los elementos secundarios. Este ejemplo proviene de Panel. Al empezar desde Panel, los métodos ArrangeOverride y MeasureOverride no presentan un comportamiento inicial. El código proporciona la puerta de enlace por la que los elementos secundarios se conocen en el sistema de diseño XAML y se representan en la interfaz de usuario. Por lo tanto, es muy importante que el código tenga en cuenta todos los elementos secundarios y siga los patrones que espera el sistema de diseño.

Escenario de disposición

Al definir un panel personalizado, va a definir un escenario de diseño.

Un escenario de diseño se expresa mediante:

  • Qué hará el panel cuando tenga elementos secundarios
  • Cuando el panel tiene restricciones en su propio espacio
  • Cómo determina la lógica del panel todas las medidas, la colocación, las posiciones y los tamaños que finalmente dan lugar a un diseño de interfaz de usuario representado de elementos secundarios

Teniendo esto en cuenta, el BoxPanel que se muestra aquí es para un escenario específico. En el interés de mantener el código más importante en este ejemplo, aún no explicaremos el escenario en detalle y, en su lugar, nos centraremos en los pasos necesarios y los patrones de codificación. Si quiere obtener más información sobre el escenario en primer lugar, vaya directamente a "El escenario de BoxPanel"y vuelva al código.

Comience derivando del Panel

Empiece derivando una clase personalizada de Panel. Probablemente la manera más fácil de hacerlo es definir un archivo de código independiente para esta clase, usando el Agregar | Nuevo elemento | Opciones de menú contextual clase para un proyecto desde el Explorador de soluciones de en Microsoft Visual Studio. Asigne un nombre a la clase (y al archivo). BoxPanel

El archivo de plantilla de una clase no comienza con muchos mediante instrucciones porque no es específicamente para aplicaciones de Windows. Por lo tanto, primero, agregue mediante instrucciones. El archivo de plantilla también comienza con algunas declaraciones mediante que probablemente no necesite y que pueden eliminarse. Esta es una lista sugerida de con declaraciones que pueden resolver los tipos que necesitarás para el código de panel personalizado típico.

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

Ahora que puede resolver Panel, conviértalo en la clase base de BoxPanel. Además, haga BoxPanel público:

public class BoxPanel : Panel
{
}

En el nivel de clase, defina algunos valores int y double que serán compartidos por varias de sus funciones lógicas, pero que no necesitarán exponerse como API pública. En el ejemplo, se denominan : maxrc, rowcount, colcountcellwidth, cellheight, , maxcellheight, . aspectratio

Una vez hecho esto, el archivo de código completo tiene este aspecto (quitar comentarios en using, ahora que sabes por qué los tenemos):

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

A partir de ahora, le mostraremos una definición de elemento a la vez, ya sea una anulación de método o algo que lo respalde, como una propiedad de dependencia. Puede agregarlos al esqueleto anterior en cualquier orden.

MedidaSobrecarga

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

El patrón necesario de una implementación de MeasureOverride es el bucle a través de cada elemento de Panel.Children. Siempre llame al método Measure en cada uno de estos elementos. Measure tiene un parámetro de tipo Tamaño. Lo que estás pasando aquí es el tamaño que tu panel está comprometiendo tener disponible para ese elemento secundario específico. Por lo tanto, antes de poder realizar el bucle y empezar a llamar a Measure, es necesario saber cuánto espacio puede dedicar cada celda. Desde el propio método MeasureOverride, dispone del valor availableSize. Ese es el tamaño que usó el elemento primario del panel cuando llamó a Measure, que fue el desencadenante de que se llamara a este MeasureOverride en primer lugar. Por lo tanto, una lógica típica consiste en diseñar un esquema mediante el cual cada elemento secundario divide el espacio disponible general del panel . A continuación, pasa cada división de tamaño al Measure de cada elemento secundario.

La forma BoxPanel en que divide el tamaño es bastante simple: divide su espacio en una serie de cuadros que se controlan en gran medida por el número de elementos. Los cuadros se dimensionan en función del recuento de filas y columnas y del tamaño disponible. A veces no se necesita una fila o columna de un cuadrado, por lo que se quita y el panel se convierte en un rectángulo en lugar de cuadrado en términos de su relación de fila : columna. Para obtener más información sobre cómo se llegó a esta lógica, vaya directamente a "El escenario para BoxPanel".

Entonces, ¿qué implica la aprobación de la medida? Establece un valor para la propiedad de solo lectura DesiredSize en cada elemento donde se llamó a Measure. Tener un valor de DesiredSize es posiblemente importante una vez que llegue a la etapa de disposición, ya que el DesiredSize comunica lo que el tamaño puede o debe ser al organizarse y en la representación final. Incluso si no usa DesiredSize en su propia lógica, el sistema todavía lo necesita.

Es posible utilizar este panel cuando el componente de altura de tamañoDisponible sea ilimitado. Si es así, el panel no tiene un alto conocido para dividir. En este caso, la lógica del paso de medida informa a cada elemento secundario de que aún no tiene una altura delimitada. Para ello, pasa un Size a la llamada Measure para los elementos secundarios donde Size.Height es infinito. Eso es legal. Cuando se llama a Measure, la lógica es que el DesiredSize se establece como el mínimo entre: lo que se pasó a Measure, o el tamaño natural de ese elemento de factores como Height y Width.

Nota:

La lógica interna de StackPanel también tiene este comportamiento: StackPanel pasa un valor de dimensión infinito a Measure en los hijos, lo que indica que no hay ninguna restricción en los hijos en la dirección de orientación. StackPanel normalmente ajusta su tamaño de manera dinámica para acomodar a todos los elementos secundarios en una pila que crece en esa dimensión.

Sin embargo, el propio panel no puede devolver un Size con un valor infinito de MeasureOverride, lo que resulta en una excepción durante el diseño. Por lo tanto, parte de la lógica es averiguar la altura máxima que solicita cualquier niño y usar esa altura como altura de celda en caso de que no provenga ya de las restricciones de tamaño del propio panel. Esta es la función auxiliar LimitUnboundedSize mencionada en el código anterior, que luego toma la altura máxima de celda y la utiliza para asignar al panel una altura finita para devolver, además de asegurarse de que cellheight sea un número finito antes de iniciar la fase de disposición.

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

DisposiciónSobrescribir

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

El patrón necesario de una implementación de ArrangeOverride es el recorrido por cada elemento de Panel.Children. Llame siempre al método Arrange en cada uno de estos elementos.

Observe cómo no hay tantos cálculos como en MeasureOverride; eso es típico. El tamaño de los niños ya se conoce desde la propia lógica de MeasureOverride del panel o del valor de DesiredSize de cada niño establecido durante el paso de medida. Sin embargo, todavía es necesario decidir la ubicación dentro del panel donde aparecerá cada elemento secundario. En un panel típico, cada hijo debe renderizarse en una posición diferente. Un panel que crea elementos superpuestos no es deseable en escenarios típicos (aunque no es descabellado crear paneles con superposiciones deliberadas, si realmente ese es su escenario previsto).

Este panel se organiza según el concepto de filas y columnas. El número de filas y columnas ya se calculó (era necesario para la medición). Por lo tanto, la forma de las filas y columnas más los tamaños conocidos de cada celda contribuyen a la lógica de definir una posición de representación (la anchorPoint) para cada elemento que contiene este panel. Ese Point, junto con el Size de ya conocido a partir de la medida, se usan como los dos componentes que crean un Rect. Rect es el tipo de entrada para Arrange.

A veces, los paneles necesitan recortar su contenido. Si lo hacen, el tamaño recortado es el tamaño que está presente en DesiredSize, ya que la lógica de Measure lo establece como el mínimo de lo que se pasó a Measureu otros factores de tamaño natural. Por lo tanto, normalmente no es necesario comprobar específicamente el recorte durante Organizar; el recorte simplemente se produce en función de pasar el tamañoDeseado a cada llamada de Organizar.

No siempre necesita un recuento mientras recorre el bucle si toda la información que necesita para definir la posición de representación se obtiene por otros medios. Por ejemplo, en la lógica de diseño de Canvas, la posición en la colección de Children no importa. Toda la información necesaria para colocar cada elemento en un Canvas se conoce leyendo los valores de Canvas.Left y Canvas.Top de los hijos como parte de la lógica de organización. La lógica de necesita un recuento para comparar con el de colcount, por lo que se sabe cuándo comenzar una nueva fila y desplazar el valor de y.

Es habitual que la entrada finalSize y el tamaño de que devuelva de una implementación de ArrangeOverride sean los mismos. Para obtener más información sobre por qué, consulta la sección "ArrangeOverride" de información general sobre paneles personalizados XAML.

Un refinamiento: controlar el recuento de filas en comparación con columnas

Puede compilar y usar este panel tal y como está ahora. Sin embargo, agregaremos un refinamiento más. En el código que acaba de mostrar, la lógica coloca la fila o columna adicional en el lado más largo en relación de aspecto. Pero para un mayor control sobre las formas de las celdas, podría ser conveniente elegir un conjunto de celdas 4x3 en lugar de 3x4 incluso si la relación de aspecto del panel es en "modo retrato". Por lo tanto, agregaremos una propiedad de dependencia opcional que el usuario del panel puede establecer para gestionar ese comportamiento. Esta es la definición de la propiedad de dependencia, que es muy básica:

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

Y a continuación se muestra cómo afecta el uso de Orientation a la lógica de medición en MeasureOverride. En realidad, lo único que hace es cambiar cómo rowcount y colcount se derivan de maxrc y la verdadera proporción de aspecto, y hay diferencias de tamaño correspondientes para cada celda debido a ello. Cuando Orientation es vertical (valor predeterminado), invierte el valor de la relación de aspecto verdadera antes de usarlo para recuentos de filas y columnas para el diseño de rectángulo "retrato".

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

El escenario de BoxPanel

El escenario concreto de BoxPanel es un panel donde uno de los factores principales para dividir el espacio es conocer el número de elementos secundarios y dividir el espacio disponible conocido para el panel. Los paneles tienen forma rectangular por naturaleza. Muchos paneles operan dividiendo ese espacio rectángulo en rectángulos adicionales; eso es lo que hace Grid para sus celdas. En caso de Grid, el tamaño de las celdas se establece mediante ColumnDefinition y valores de RowDefinition, y los elementos declaran la celda exacta en la que entran con Grid.Row y Grid.Column propiedades adjuntas. Obtener un buen diseño de una cuadrícula normalmente requiere conocer el número de elementos secundarios de antemano, de modo que haya suficientes celdas y cada elemento secundario establece sus propiedades adjuntas para ajustarse a su propia celda.

Pero, ¿qué ocurre si el número de hijos es dinámico? Eso es ciertamente posible; El código de la aplicación puede agregar elementos a colecciones, en respuesta a cualquier condición dinámica en tiempo de ejecución que considere lo suficientemente importante como para que valga la pena actualizar la interfaz de usuario. Si usa el enlace de datos para realizar copias de seguridad de colecciones o objetos empresariales, la obtención de estas actualizaciones y la actualización de la interfaz de usuario se controla automáticamente, por lo que suele ser la técnica preferida (consulte Enlace de datos en profundidad).

Pero no todos los escenarios de aplicación se prestan al enlace de datos. A veces, debe crear nuevos elementos de interfaz de usuario en tiempo de ejecución y hacer que sean visibles. BoxPanel corresponde a este escenario. Un número variable de elementos secundarios no supone ningún problema para BoxPanel porque utiliza el conteo de elementos secundarios en los cálculos y ajusta tanto los elementos secundarios existentes como los nuevos en un nuevo diseño para que todos encajen.

Un escenario avanzado para extender BoxPanel aún más (no se muestra aquí) podría acomodar elementos secundarios dinámicos y usar el DesiredSize de un elemento secundario como un factor más determinante para dimensionar las celdas individuales. Este escenario podría usar diferentes tamaños de fila o columna o formas que no sean de cuadrícula para que haya menos espacio "desperdiciado". Esto requiere una estrategia de cómo rectángulos de diferentes tamaños y proporciones pueden caber en un rectángulo contenedor, manteniendo tanto la estética como el menor tamaño posible. BoxPanel no hace eso; usa una técnica más sencilla para dividir el espacio. BoxPaneltécnica es determinar el número cuadrado más pequeño que es mayor que la cantidad de hijos. Por ejemplo, 9 elementos caben en un cuadrado de 3x3. 10 elementos requieren un cuadrado de 4x4. Sin embargo, a menudo puede encajar los elementos mientras quita una fila o columna del cuadrado de inicio para ahorrar espacio. En el ejemplo "count=10", que cabe en un rectángulo de 4x3 o 3x4.

Es posible que se pregunte por qué el panel no elegiría 5x2 para 10 elementos, porque eso se ajusta perfectamente al número de elemento. Sin embargo, en la práctica, los paneles están dimensionados como rectángulos que rara vez tienen una relación de aspecto pronunciada. La técnica de mínimos cuadrados es una manera de inclinar la lógica de ajuste de tamaño para funcionar bien con formas de diseño típicas y no fomentar el ajuste de tamaño donde las formas de celda obtienen relaciones de aspecto impares.

Referencia

Conceptos