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 Extensible Application Markup Language (XAML) y se representa la interfaz de usuario de la aplicación.

API importantes: Panel, ArrangeOverride,MeasureOverride.

Puedes definir paneles personalizados para el diseño XAML derivando una clase personalizada de la clase Panel. El comportamiento del panel se proporciona reemplazando MeasureOverride y ArrangeOverride, proporcionando lógica que mide y organiza los elementos secundarios.

La clase base Panel

Para definir una clase de panel personalizada, puede derivar directamente de la clase Panel o derivar de una de las clases prácticas de panel que no están selladas, como Grid o StackPanel. Es más fácil derivar del Panel, ya que puede ser difícil solucionar la lógica de diseño existente de un panel que ya tiene comportamiento de diseño. Además, un panel con comportamiento podría tener propiedades existentes que no son relevantes para las características de diseño del panel.

En Panel, el panel personalizado hereda estas API:

  • La propiedad Children.
  • Las propiedades Background, ChildrenTransitions e IsItemsHost y los identificadores de propiedad de dependencia. Ninguna de estas propiedades es virtual, por lo que normalmente no se reemplazan ni se reemplazan. Normalmente no necesita estas propiedades para escenarios de panel personalizados, ni siquiera para leer valores.
  • Los métodos de invalidación de diseño MeasureOverride y ArrangeOverride. Estos se definieron originalmente mediante FrameworkElement. La clase Panel base no invalida estos, pero los paneles prácticos, como Grid, tienen implementaciones de invalidación que se implementan como código nativo y que el sistema ejecuta. Proporcionar nuevas implementaciones (o aditivas) para ArrangeOverride y MeasureOverride es la mayor parte del esfuerzo que necesita para definir un panel personalizado.
  • Todas las demás API de FrameworkElement, UIElement y DependencyObject, como Height, Visibility, etc. A veces se hace referencia a valores de estas propiedades en las invalidaciones de diseño, pero no son virtuales, por lo que normalmente no se reemplazan ni se reemplazan.

Este enfoque aquí es describir los conceptos de diseño XAML, por lo que puedes considerar todas las posibilidades de cómo un panel personalizado puede y debe comportarse en el diseño. Si prefiere saltar hacia la derecha y ver una implementación de panel personalizada de ejemplo, vea BoxPanel, un panel personalizado de ejemplo.

La propiedad Children

La propiedad Children es relevante para un panel personalizado porque todas las clases derivadas del Panel usan la propiedad Children como lugar para almacenar sus elementos secundarios contenidos en una colección. Los elementos secundarios se designan como la propiedad de contenido XAML para la clase Panel y todas las clases derivadas del Panel pueden heredar el comportamiento de la propiedad de contenido XAML. Si una propiedad se designa como la propiedad de contenido XAML, significa que el marcado XAML puede omitir un elemento de propiedad al especificar esa propiedad en el marcado y los valores se establecen como elementos secundarios de marcado inmediatos (el "contenido"). Por ejemplo, si deriva una clase denominada CustomPanel del Panel que no define ningún comportamiento nuevo, puede seguir usando este marcado:

<local:CustomPanel>
  <Button Name="button1"/>
  <Button Name="button2"/>
</local:CustomPanel>

Cuando un analizador XAML lee este marcado, se sabe que Children es la propiedad de contenido XAML para todos los tipos derivados de Panel, por lo que el analizador agregará los dos elementos Button al valor UIElementCollection de la propiedad Children. La propiedad de contenido XAML facilita una relación simplificada de elementos primarios y secundarios en el marcado XAML para una definición de interfaz de usuario. Para obtener más información sobre las propiedades de contenido XAML y cómo se rellenan las propiedades de la colección cuando se analiza XAML, consulta la guía de sintaxis XAML.

El tipo de colección que mantiene el valor de la propiedad Children es la clase UIElementCollection. UIElementCollection es una colección fuertemente tipada que usa UIElement como tipo de elemento aplicado. UIElement es un tipo base heredado por cientos de tipos prácticos de elementos de interfaz de usuario, por lo que el cumplimiento de tipos aquí está deliberadamente suelto. Sin embargo, exige que no se pueda tener un pincel como elemento secundario directo de un Panel y, por lo general, significa que solo los elementos que se espera que sean visibles en la interfaz de usuario y participar en el diseño se encontrarán como elementos secundarios en un Panel.

Normalmente, un panel personalizado acepta cualquier elemento secundario UIElement por una definición XAML, simplemente usando las características de la propiedad Children tal como está. Como escenario avanzado, podría admitir la comprobación de tipos adicionales de elementos secundarios, al iterar por la colección en las invalidaciones de diseño.

Además de recorrer en bucle la colección Children en las invalidaciones, la lógica del panel también podría verse afectada por Children.Count. Es posible que tenga lógica que asigne espacio al menos en parte en función del número de elementos, en lugar de tamaños deseados y otras características de elementos individuales.

Invalidación de los métodos de diseño

El modelo básico para los métodos de invalidación de diseño (MeasureOverride y ArrangeOverride) es que deben recorrer en iteración todos los elementos secundarios y llamar al método de diseño específico de cada elemento secundario. El primer ciclo de diseño se inicia cuando el sistema de diseño XAML establece el objeto visual para la ventana raíz. Dado que cada elemento primario invoca el diseño en sus elementos secundarios, esto propaga una llamada a los métodos de diseño a todos los posibles elementos de interfaz de usuario que se supone que forman parte de un diseño. En el diseño XAML, hay dos fases: medir y, a continuación, organizar.

No se obtiene ningún comportamiento de método de diseño integrado para MeasureOverride y ArrangeOverride desde la clase Panel base. Los elementos de Children no se representarán automáticamente como parte del árbol visual XAML. Depende de usted hacer que los elementos sean conocidos para el proceso de diseño, invocando métodos de diseño en cada uno de los elementos que se encuentran en Children a través de un pase de diseño dentro de las implementaciones MeasureOverride y ArrangeOverride .

No hay ninguna razón para llamar a implementaciones base en invalidaciones de diseño a menos que tenga su propia herencia. Los métodos nativos para el comportamiento de diseño (si existen) se ejecutan independientemente, y no llamar a la implementación base desde invalidaciones no impedirá que se produzca el comportamiento nativo.

Durante el paso de medida, la lógica de diseño consulta cada elemento secundario para su tamaño deseado llamando al método Measure en ese elemento secundario. Al llamar al método Measure, se establece el valor de la propiedad DesiredSize. El valor devuelto MeasureOverride es el tamaño deseado para el propio panel.

Durante el paso de organización, las posiciones y tamaños de los elementos secundarios se determinan en el espacio x-y y la composición de diseño está preparada para la representación. El código debe llamar a Arrange en cada elemento secundario de Children para que el sistema de diseño detecte que el elemento pertenece al diseño. La llamada Arrange es un precursor de la composición y la representación; informa al sistema de diseño donde va ese elemento, cuando se envía la composición para su representación.

Muchas propiedades y valores contribuyen a cómo funcionará la lógica de diseño en tiempo de ejecución. Una manera de pensar en el proceso de diseño es que los elementos sin elementos secundarios (generalmente el elemento más profundamente anidado de la interfaz de usuario) son los que pueden finalizar primero las medidas. No tienen dependencias en los elementos secundarios que influyen en su tamaño deseado. Es posible que tengan sus propios tamaños deseados, y son sugerencias de tamaño hasta que el diseño tenga lugar realmente. A continuación, el paso de medida continúa caminando por el árbol visual hasta que el elemento raíz tenga sus medidas y se puedan finalizar todas las medidas.

El diseño candidato debe ajustarse a la ventana de la aplicación actual o se recortarán partes de la interfaz de usuario. Los paneles suelen ser el lugar donde se determina la lógica de recorte. La lógica del panel puede determinar qué tamaño está disponible desde la implementación de MeasureOverride y puede tener que insertar las restricciones de tamaño en los elementos secundarios y dividir el espacio entre los elementos secundarios para que todo se ajuste a lo mejor que pueda. El resultado del diseño es idealmente algo que usa varias propiedades de todas las partes del diseño, pero que aún se ajusta dentro de la ventana de la aplicación. Esto requiere una buena implementación para la lógica de diseño de los paneles y también un diseño de interfaz de usuario sensato en parte de cualquier código de aplicación que compile una interfaz de usuario con ese panel. Ningún diseño de panel será bueno si el diseño general de la interfaz de usuario incluye más elementos secundarios de los que posiblemente caben en la aplicación.

Una gran parte de lo que hace que el sistema de diseño funcione es que cualquier elemento basado en FrameworkElement ya tiene parte de su propio comportamiento inherente cuando actúa como secundario en un contenedor. Por ejemplo, hay varias API de FrameworkElement que informan al comportamiento del diseño o son necesarias para que el diseño funcione en absoluto. Entre ellas se incluyen las siguientes:

MeasureOverride

El método MeasureOverride tiene un valor devuelto que usa el sistema de diseño como desiredSize inicial para el propio panel, cuando su elemento primario llama al método Measure en el panel en el diseño. Las opciones lógicas dentro del método son tan importantes como lo que devuelve y la lógica suele influir en qué valor se devuelve.

Todas las implementaciones de MeasureOverride deben recorrer children y llamar al método Measure en cada elemento secundario. Al llamar al método Measure, se establece el valor de la propiedad DesiredSize. Esto puede informar sobre cuánto espacio necesita el panel, así como cómo ese espacio se divide entre elementos o tamaño para un elemento secundario determinado.

Este es un esqueleto muy básico de un método MeasureOverride:

protected override Size MeasureOverride(Size availableSize)
{
    Size returnSize; //TODO might return availableSize, might do something else
     
    //loop through each Child, call Measure on each
    foreach (UIElement child in Children)
    {
        child.Measure(new Size()); // TODO determine how much space the panel allots for this child, that's what you pass to Measure
        Size childDesiredSize = child.DesiredSize; //TODO determine how the returned Size is influenced by each child's DesiredSize
        //TODO, logic if passed-in Size and net DesiredSize are different, does that matter?
    }
    return returnSize;
}

Los elementos suelen tener un tamaño natural en el momento en que están listos para el diseño. Después del paso de la medida, DesiredSize podría indicar que el tamaño natural, si el valor availableSize que pasó para Measure era menor. Si el tamaño natural es mayor que availableSize que ha pasado para Measure, DesiredSize está restringido a availableSize. Así es como se comporta la implementación interna de Measure y las invalidaciones de diseño deben tener en cuenta ese comportamiento.

Algunos elementos no tienen un tamaño natural porque tienen valores Auto para Height y Width. Estos elementos usan el tamaño completo availableSize, ya que es lo que representa un valor Automático : ajustar el tamaño del elemento al tamaño máximo disponible, que el elemento primario de diseño inmediato comunica llamando a Measure con availableSize. En la práctica, siempre hay alguna medida en la que se ajusta el tamaño de una interfaz de usuario (incluso si es la ventana de nivel superior). Finalmente, el paso de medida resuelve todos los valores auto de las restricciones primarias y todos los elementos de valor automático obtienen medidas reales (lo que puede obtener comprobando ActualWidth y ActualHeight, una vez completado el diseño).

Es legal pasar un tamaño a Measure que tenga al menos una dimensión infinita, para indicar que el panel puede intentar ajustar su tamaño para ajustar las medidas de su contenido. Cada elemento secundario que se mide establece su valor DesiredSize con su tamaño natural. A continuación, durante el pase de organización, el panel normalmente organiza el uso de ese tamaño.

Los elementos de texto como TextBlock tienen un actualWidth calculado y ActualHeight en función de sus propiedades de cadena de texto y texto, incluso si no se establece ningún valor height o Width, y la lógica del panel debe respetar estas dimensiones. El recorte de texto es una experiencia de interfaz de usuario especialmente incorrecta.

Incluso si la implementación no usa las medidas de tamaño deseadas, es mejor llamar al método Measure en cada elemento secundario, ya que hay comportamientos internos y nativos que se desencadenan mediante la llamada a Measure. Para que un elemento participe en el diseño, cada elemento secundario debe haber llamado a Measure durante el paso de medida y el método Arrange al que se llama durante el paso de organización. Al llamar a estos métodos, se establecen marcas internas en el objeto y se rellenan valores (como la propiedad DesiredSize ) que la lógica de diseño del sistema necesita cuando compila el árbol visual y representa la interfaz de usuario.

El valor devuelto MeasureOverride se basa en la lógica del panel interpretando DesiredSize u otras consideraciones de tamaño para cada uno de los elementos secundarios de Children cuando se llama a Measure en ellos. Qué hacer con los valores DesiredSize de los elementos secundarios y cómo debe usarlos el valor devuelto measureOverride es hasta la interpretación de su propia lógica. Normalmente no agrega los valores sin modificaciones, ya que la entrada de MeasureOverride suele ser un tamaño disponible fijo sugerido por el elemento primario del panel. Si supera ese tamaño, el propio panel podría recortarse. Normalmente, compararía el tamaño total de los elementos secundarios con el tamaño disponible del panel y realizar ajustes si es necesario.

Sugerencias e instrucciones

  • Idealmente, un panel personalizado debe ser adecuado para ser el primer objeto visual verdadero en una composición de la interfaz de usuario, quizás en un nivel inmediatamente bajo Page, UserControl u otro elemento que sea la raíz de la página XAML. En las implementaciones de MeasureOverride, no devuelva rutinariamente el tamaño de entrada sin examinar los valores. Si el valor devuelto Size tiene un valor Infinito , esto puede producir excepciones en la lógica de diseño en tiempo de ejecución. Un valor Infinity puede provenir de la ventana principal de la aplicación, que se puede desplazar y, por lo tanto, no tiene un alto máximo. Otro contenido desplazable puede tener el mismo comportamiento.
  • Otro error común en las implementaciones de MeasureOverride es devolver un nuevo tamaño predeterminado (los valores de alto y ancho son 0). Puede comenzar con ese valor e incluso podría ser el valor correcto si el panel determina que no se debe representar ninguno de los elementos secundarios. Sin embargo, un tamaño predeterminado da como resultado que su host no se ajuste correctamente al tamaño del panel. No solicita espacio en la interfaz de usuario y, por tanto, no obtiene espacio y no se representa. Es posible que todo el código del panel funcione correctamente, pero todavía no verá el panel o el contenido del mismo si se está redactando con un alto cero, un ancho cero.
  • Dentro de las invalidaciones, evite la tentación de convertir elementos secundarios en FrameworkElement y use propiedades que se calculan como resultado del diseño, especialmente ActualWidth y ActualHeight. En los escenarios más comunes, puede basar la lógica en el valor DesiredSize del elemento secundario y no necesitará ninguna de las propiedades relacionadas Height o Width de un elemento secundario. En los casos especializados, donde conoce el tipo de elemento y tiene información adicional, por ejemplo, el tamaño natural de un archivo de imagen, puede usar la información especializada del elemento porque no es un valor que los sistemas de diseño modifican activamente. La inclusión de propiedades calculadas por el diseño como parte de la lógica de diseño aumenta considerablemente el riesgo de definir un bucle de diseño no intencional. Estos bucles provocan una condición en la que no se puede crear un diseño válido y el sistema puede producir una excepción LayoutCycleException si el bucle no se puede recuperar.
  • Los paneles normalmente dividen su espacio disponible entre varios elementos secundarios, aunque exactamente cómo varía el espacio. Por ejemplo, Grid implementa la lógica de diseño que usa sus valores RowDefinition y ColumnDefinition para dividir el espacio en las celdas Grid, lo que admite valores de tamaño de estrella y píxeles. Si son valores de píxeles, el tamaño disponible para cada elemento secundario ya se conoce, por lo que es lo que se pasa como tamaño de entrada para una medida de estilo de cuadrícula.
  • Los propios paneles pueden introducir espacio reservado para el relleno entre elementos. Si lo hace, asegúrese de exponer las medidas como una propiedad distinta de Margin o cualquier propiedad Padding .
  • Los elementos pueden tener valores para sus propiedades ActualWidth y ActualHeight en función de un pase de diseño anterior. Si los valores cambian, el código de la interfaz de usuario de la aplicación puede colocar controladores para LayoutUpdated en elementos si hay lógica especial para ejecutarse, pero la lógica del panel normalmente no necesita comprobar si hay cambios con el control de eventos. El sistema de diseño ya está realizando las determinaciones de cuándo volver a ejecutar el diseño porque se ha cambiado un valor de propiedad relevante para el diseño y se llama automáticamente a MeasureOverride o ArrangeOverride de un panel en las circunstancias adecuadas.

ArrangeOverride

El método ArrangeOverride tiene un valor devuelto Size que usa el sistema de diseño al representar el propio panel cuando se llama al método Arrange en el panel por su elemento primario en el diseño. Es habitual que la entrada finalSize y el tamaño devuelto ArrangeOverride sean los mismos. Si no lo son, significa que el panel intenta establecerse en un tamaño diferente al que están disponibles los demás participantes en la notificación de diseño. El tamaño final se basaba en haber ejecutado previamente el paso de medida del diseño a través del código del panel, por lo que no es habitual devolver un tamaño diferente: significa que se ignora deliberadamente la lógica de medida.

No devuelva un tamaño con un componente Infinity . Al intentar usar este tipo de tamaño , se produce una excepción del diseño interno.

Todas las implementaciones arrangeOverride deben recorrer los elementos secundarios y llamar al método Arrange en cada elemento secundario. Al igual que Measure, Arrange no tiene un valor devuelto. A diferencia de Measure, no se establece ninguna propiedad calculada como resultado (sin embargo, el elemento en cuestión normalmente desencadena un evento LayoutUpdated).

Este es un esqueleto muy básico de un método ArrangeOverride:

protected override Size ArrangeOverride(Size finalSize)
{
    //loop through each Child, call Arrange on each
    foreach (UIElement child in Children)
    {
        Point anchorPoint = new Point(); //TODO more logic for topleft corner placement in your panel
       // for this child, and based on finalSize or other internal state of your panel
        child.Arrange(new Rect(anchorPoint, child.DesiredSize)); //OR, set a different Size 
    }
    return finalSize; //OR, return a different Size, but that's rare
}

El paso de organización del diseño puede ocurrir sin ir precedido por un pase de medida. Sin embargo, esto solo ocurre cuando el sistema de diseño ha determinado que no han cambiado propiedades que habrían afectado a las medidas anteriores. Por ejemplo, si cambia una alineación, no es necesario volver a medir ese elemento determinado porque su DesiredSize no cambiaría cuando cambie su elección de alineación. Por otro lado, si ActualHeight cambia en cualquier elemento de un diseño, se necesita un nuevo paso de medida. El sistema de diseño detecta automáticamente los cambios de medida verdaderos e invoca de nuevo el pase de medida y, a continuación, ejecuta otro pase de organización.

La entrada de Arrange toma un valor rect . La forma más común de construir este rect es usar el constructor que tiene una entrada Point y una entrada Size. El punto es el punto donde se debe colocar la esquina superior izquierda del cuadro de límite del elemento. Size es las dimensiones que se usan para representar ese elemento en particular. A menudo se usa DesiredSize para ese elemento como este valor size, ya que establecer DesiredSize para todos los elementos implicados en el diseño era el propósito del paso de medida del diseño. (El paso de medida determina todo el tamaño de los elementos de forma iterativa para que el sistema de diseño pueda optimizar cómo se colocan los elementos una vez que llegue al paso de organización).

Lo que suele variar entre las implementaciones de ArrangeOverride es la lógica por la que el panel determina el componente Point de cómo organiza cada elemento secundario. Un panel de posicionamiento absoluto, como Canvas, usa la información de ubicación explícita que obtiene de cada elemento a través de Canvas.Left y Canvas.Top valores. Un panel de división de espacio, como Grid , tendría operaciones matemáticas que dividieron el espacio disponible en celdas y cada celda tendría un valor x-y para donde se debe colocar y organizar su contenido. Un panel adaptable como StackPanel podría expandirse para ajustarse al contenido en su dimensión de orientación.

Todavía hay influencias de posicionamiento adicionales en los elementos en el diseño, más allá de lo que controlas y pasas directamente a Arrange. Proceden de la implementación nativa interna de Arrange que es común a todos los tipos derivados de FrameworkElement y que aumentan algunos otros tipos, como elementos de texto. Por ejemplo, los elementos pueden tener margen y alineación, y algunos pueden tener relleno. Estas propiedades suelen interactuar. Para obtener más información, consulta Alineación, margen y relleno.

Paneles y controles

Evite colocar la funcionalidad en un panel personalizado que en su lugar se debe compilar como un control personalizado. El rol de un panel es presentar cualquier contenido de elemento secundario que exista dentro de él, como una función de diseño que se produce automáticamente. El panel puede agregar decoraciones al contenido (similar a cómo un borde agrega el borde alrededor del elemento que presenta) o realizar otros ajustes relacionados con el diseño, como relleno. Pero eso es lo que debe ir al extender la salida del árbol visual más allá de la generación de informes y el uso de información de los elementos secundarios.

Si hay alguna interacción accesible para el usuario, debe escribir un control personalizado, no un panel. Por ejemplo, un panel no debe agregar ventanillas de desplazamiento al contenido que presenta, incluso si el objetivo es evitar el recorte, ya que las barras de desplazamiento, los pulgares, etc. son elementos de control interactivos. (Es posible que el contenido tenga barras de desplazamiento después de todo, pero debe dejarlo hasta la lógica del elemento secundario. No lo obligue agregando desplazamiento como una operación de diseño). Puede crear un control y escribir también un panel personalizado que desempeña un papel importante en el árbol visual de ese control, cuando se trata de presentar contenido en ese control. Pero el control y el panel deben ser objetos de código distintos.

Una razón por la que la distinción entre el control y el panel es importante es debido a microsoft Automatización de la interfaz de usuario y accesibilidad. Los paneles proporcionan un comportamiento de diseño visual, no un comportamiento lógico. La apariencia visual de un elemento de interfaz de usuario no es un aspecto de la interfaz de usuario que suele ser importante para los escenarios de accesibilidad. La accesibilidad consiste en exponer las partes de una aplicación que son lógicamente importantes para comprender una interfaz de usuario. Cuando se requiere interacción, los controles deben exponer las posibilidades de interacción a la infraestructura de Automatización de la interfaz de usuario. Para obtener más información, consulte Automatización personalizada del mismo nivel.

Otra API de diseño

Hay otras API que forman parte del sistema de diseño, pero no se declaran en Panel. Puede usarlos en una implementación de panel o en un control personalizado que use paneles.

  • UpdateLayout, InvalidateMeasure e InvalidateArrange son métodos que inician un pase de diseño. InvalidateArrange podría no desencadenar un pase de medida, pero los otros dos sí. Nunca llame a estos métodos desde una invalidación de método de diseño, ya que casi están seguros de provocar un bucle de diseño. Normalmente, el código de control no necesita llamarlos. La mayoría de los aspectos del diseño se desencadenan automáticamente mediante la detección de cambios en las propiedades de diseño definidas por el marco, como Width , etc.
  • LayoutUpdated es un evento que se activa cuando cambia algún aspecto del elemento. Esto no es específico de los paneles; FrameworkElement define el evento.
  • SizeChanged es un evento que se activa solo cuando finalizan los pases de diseño e indica que ActualHeight o ActualWidth han cambiado como resultado. Este es otro evento FrameworkElement. Hay casos en los que LayoutUpdated se activa, pero SizeChanged no. Por ejemplo, el contenido interno podría reorganizarse, pero el tamaño del elemento no cambió.

Referencia

Conceptos