BoxPanel, un panel personalizado de ejemplo
Aprenda a escribir código para una clase Panel personalizada, implementar métodos ArrangeOverride y MeasureOverride y usar la propiedad Children.
API 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 más información sobre estos conceptos de diseño y cómo podrían aplicarse a tu escenario de diseño concreto, consulta Introducción a 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. El comportamiento del panel se proporciona reemplazando los métodos ArrangeOverride y MeasureOverride, proporcionando lógica que mide y organiza los elementos secundarios. En este ejemplo se deriva del Panel. Cuando se inicia desde Panel, los métodos ArrangeOverride y MeasureOverride no tienen 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 diseño
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 determinado. 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 del Panel. Probablemente la manera más fácil de hacerlo es definir un archivo de código independiente para esta clase, mediante las opciones de menú contextual Agregar | nueva clase de elemento | para un proyecto desde el Explorador de soluciones en Microsoft Visual Studio. Asigne un nombre a la clase (y al archivo). BoxPanel
El archivo de plantilla de una clase no comienza con una gran cantidad de instrucciones using, ya que no está destinado específicamente a aplicaciones de Windows. Por lo tanto, primero, agregue instrucciones using . El archivo de plantilla también comienza con algunas instrucciones using que probablemente no necesite y que se puedan eliminar. Esta es una lista sugerida de instrucciones using que pueden resolver los tipos que necesitará 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, consítelo como 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 compartidos por varias de las funciones lógicas, pero que no tendrán que exponerse como API pública. En el ejemplo, se denominan : maxrc
, rowcount
, colcount
cellwidth
, cellheight
, , maxcellheight
, . aspectratio
Una vez hecho esto, el archivo de código completo tiene este aspecto (quitar comentarios sobre el uso, ahora que sabe 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;
}
Desde aquí, le mostraremos una definición de miembro a la vez, ya sea que un método invalide o algo que admita, como una propiedad de dependencia. Puede agregarlos al esqueleto anterior en cualquier orden.
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);
}
El patrón necesario de una implementación de MeasureOverride es el bucle a través de cada elemento de Panel.Children. Llame siempre al método Measure en cada uno de estos elementos. Measure tiene un parámetro de tipo Size. Lo que está pasando aquí es el tamaño que el panel está confirmando tener disponible para ese elemento secundario determinado. Por lo tanto, antes de poder hacer el bucle y empezar a llamar a Measure, debe saber cuánto espacio puede dedicar cada celda. Desde el propio método MeasureOverride , tiene el valor availableSize . Ese es el tamaño que usó el elemento primario del panel cuando llamó a Measure, que era el desencadenador para que se llamara a measureOverride en primer lugar. Por lo tanto, una lógica típica consiste en diseñar un esquema en el que cada elemento secundario divide el espacio del tamaño global del panel disponible. A continuación, se pasa cada división de tamaño a Medida 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 tienen un tamaño basado en el recuento de filas y columnas y en el 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é hace la medida? Establece un valor para la propiedad DesiredSize de solo lectura en cada elemento al que se llamó a Measure. Tener un valor DesiredSize es posiblemente importante una vez que llegue al pase de organización, ya que DesiredSize comunica lo que el tamaño puede o debe ser al organizar y en la representación final. Incluso si no usa DesiredSize en su propia lógica, el sistema todavía lo necesita.
Es posible que este panel se use cuando el componente de alto de availableSize esté sin enlazar. Si es así, el panel no tiene un alto conocido para dividirse. En este caso, la lógica del paso de medida informa a cada elemento secundario de que aún no tiene una altura limitada. Para ello, pasa un tamaño 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 DesiredSize se establece como mínimo: lo que se pasó a Measure o el tamaño natural de ese elemento a partir de factores como Height y Width establecidos explícitamente.
Nota:
La lógica interna de StackPanel también tiene este comportamiento: StackPanel pasa un valor de dimensión infinito a Measure en elementos secundarios, lo que indica que no hay ninguna restricción en los elementos secundarios de la dimensión de orientación. StackPanel normalmente se ajusta dinámicamente a todos los elementos secundarios de una pila que crece en esa dimensión.
Sin embargo, el propio panel no puede devolver un tamaño con un valor infinito de MeasureOverride; esto produce una excepción durante el diseño. Por lo tanto, parte de la lógica es averiguar el alto máximo que solicita cualquier elemento secundario y usar ese alto como alto de celda en caso de que no proceda de las propias restricciones de tamaño del panel. Esta es la función LimitUnboundedSize
auxiliar a la que se hizo referencia en el código anterior, que luego toma ese alto máximo de celda y lo usa para dar al panel una altura finita para devolver, así como asegurarse de que cellheight
es un número finito antes de que se inicie el paso de organizació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;
}
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;
}
El patrón necesario de una implementación arrangeOverride es el bucle a través de cada elemento de Panel.Children. Llame siempre al método Arrange en cada uno de estos elementos.
Tenga en cuenta cómo no hay tantos cálculos como en MeasureOverride; es habitual. El tamaño de los elementos secundarios ya se conoce desde la propia lógica measureOverride del panel o desde el valor DesiredSize de cada conjunto secundario 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 elemento secundario debe representarse en una posición diferente. Un panel que crea elementos superpuestos no es deseable para escenarios típicos (aunque no está fuera de la pregunta para crear paneles que tienen superposiciones intencionadas, si es realmente su escenario previsto).
Este panel organiza por 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 punto, junto con el tamaño ya conocido de la medida, se usan como los dos componentes que construyen un rect. Rect es el tipo de entrada de 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 Measure la establece como mínimo de lo que se pasó a Measure u otros factores de tamaño natural. Por lo tanto, normalmente no es necesario comprobar específicamente el recorte durante Arrange; el recorte se produce en función de pasar DesiredSize a cada llamada Arrange.
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 conoce por otros medios. Por ejemplo, en la lógica de diseño de Canvas, la posición de la colección Children no importa. Toda la información necesaria para colocar cada elemento en un canvas se conoce leyendo Canvas.Left y Canvas.Top valores de elementos secundarios como parte de la lógica de organización. La BoxPanel
lógica necesita un recuento para compararlo con el recuento , por lo que se sabe cuándo comenzar una nueva fila y desplazar el valor y .
Es habitual que la entrada finalSize y el tamaño que devuelve de una implementación 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 frente a 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 "vertical". Por lo tanto, agregaremos una propiedad de dependencia opcional que el consumidor del panel puede establecer para controlar 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 el uso Orientation
de afecta a la lógica de medida en MeasureOverride
. En realidad, todo lo que está haciendo es cambiar cómo rowcount
y colcount
se derivan de maxrc
y la verdadera relación de aspecto, y hay diferencias de tamaño correspondientes para cada celda debido a eso. Cuando Orientation
es Vertical (valor predeterminado), invierte el valor de la relación de aspecto true antes de usarlo para recuentos de filas y columnas para nuestro diseño de rectángulo "vertical".
// 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 que es un panel donde uno de los principales determinantes de cómo dividir el espacio es sabiendo el número de elementos secundarios y dividiendo el espacio disponible conocido para el panel. Los paneles son formas rectángulos innatamente. Muchos paneles operan dividiendo ese espacio rectángulo en rectángulos adicionales; eso es lo que hace Grid para sus celdas. En el caso de Grid, el tamaño de las celdas se establece mediante los valores ColumnDefinition y RowDefinition, y los elementos declaran la celda exacta en la que entran con las propiedades adjuntas Grid.Row y Grid.Column. 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 elementos secundarios 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
es para este escenario. Un número cambiante de elementos secundarios no es un problema para BoxPanel
porque usa el recuento secundario en cálculos y ajusta los elementos secundarios existentes y nuevos en un nuevo diseño para que todos se ajusten.
Un escenario avanzado para ampliar BoxPanel
aún más (no se muestra aquí) podría acomodar elementos secundarios dinámicos y usar DesiredSize de un elemento secundario como factor más fuerte para el ajuste de tamaño de 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 para la forma en que varios rectángulos de varios tamaños y relaciones de aspecto pueden caber en un rectángulo contenedor tanto para la estética como para el tamaño más pequeño. BoxPanel
no hace eso; usa una técnica más sencilla para dividir el espacio. BoxPanel
La técnica es determinar el número mínimo cuadrado mayor que el número secundario. Por ejemplo, 9 elementos caben en un cuadrado de 3x3. 10 elementos requieren un cuadrado de 4x4. Sin embargo, a menudo puede ajustarse a los elementos mientras sigue quitando una fila o columna del cuadrado inicial, para ahorrar espacio. En el ejemplo count=10, que se ajusta a un rectángulo 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 tienen un tamaño de rectángulos que rara vez tienen una relación de aspecto fuertemente orientada. 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.
Temas relacionados
Referencia
Conceptos