Nunca encuentras lo que necesitas (parte 2 de 2)

En la primera parte vimos algo de teoría necesaria sobre la forma de construir un panel y de algunas operaciones que al respecto debemos adelantar. Esta vez finalizaremos “echando” código para crear nuestro panel circular… lindo nombre. Comencemos con el método MeasureOverride.

  1. protected override Size MeasureOverride(Size availableSize)
  2. {
  3.   // Determinar si se me ha otorgado espacio infinito…
  4.   // en cuyo caso lo restrinjo a un máximo.
  5.   Size WorkSize = new Size(
  6.     double.IsPositiveInfinity(availableSize.Width) ?
  7.       300d : availableSize.Width,
  8.     double.IsPositiveInfinity(availableSize.Height) ?
  9.       300d : availableSize.Height);
  10.  
  11.   // Informarle a cada hijo de cuanto espacio dispone.
  12.   foreach (FrameworkElement item in Children)
  13.     item.Measure(WorkSize);
  14.  
  15.   // Dado que no me interesa reportar que necesito un tamaño diferente
  16.   // retorno el mismo tamaño que me fué otorgado.
  17.   return WorkSize;

Reiterando lo afirmado en la primera parte, a este método le llega como parámetro la cantidad de espacio disponible que el control padre le otorgaría a nuestro pánel. Esto es un estimado pero igual debemos trabajar asumiendo que dicho espacio es el final.

Puede suceder que se nos informe que disponemos de espacio ilimitado. En este caso availableSize.Width y/o availableSize.Height vienen en infinito positivo. Esto sucede en dos ocasiones: en tiempo de diseño y cuando nuestro pánel se hospeda dentro de otro que no restringe el tamaño de sus controles hijos, como el Canvas. Es imprescindible que manejemos correctamente estas áreas pues, aunque nos digan que disponemos de todo ese espacio, no podemos informar (el momento del retorno en el método) que necesitamos espacio ilimitado pues se generaría una excepción ya que informar algo así está prohibido. De modo que nuestra primera tarea es asumir que el espacio es limitado sin importar el que nos concedan, y es lo que hacemos al trabajar con la variable adicional WorkSize. En este ejemplo, si nos dicen que el espacio es ilimitado, lo limitamos a 300 (300 es un valor arbitrario que se me vino a mi calva cabeza).

En seguida le informamos a cada uno de los controles hijos que pueda contener nuestro pánel (recorriendo la colección Children) de cuanto espacio dispone para “pintarse” en pantalla. Para nuestro ejemplo no nos interesa constreñir (linda palabra) dicho espacio de modo que le informo a cada hijo que dispone de tanto espacio como el que calculé para WorkSize. Si vamos a diseñar otro pánel que requiera alterar el tamaño de los hijos de alguna forma este es el momento de indicar ese cambio de tamaño.

Finalizamos retornando el tamaño que, estimamos, requiere nuestro pánel para “pintar” su contenido. Continuemos con el método ArrangeOverride.

  1. protected override Size ArrangeOverride(Size finalSize)
  2. {
  3.   int CountHijos = Children.Count;
  4.   if (CountHijos == 0) return finalSize;
  5.  
  6.   // Calcular el tamaño máximo para todos los hijos.
  7.   Double MaxChildWidth = Children.OfType<UIElement>()
  8.     .Max(x => x.DesiredSize.Width);
  9.   Double MaxChildHeight = Children.OfType<UIElement>()
  10.     .Max(x => x.DesiredSize.Height);
  11.  
  12.   // Calcular el radio contra el centro.
  13.   double Radio;
  14.   if (finalSize.Width < finalSize.Height)
  15.     Radio = finalSize.Width / 2d – MaxChildWidth / 2d;
  16.   else
  17.     Radio = finalSize.Height / 2d – MaxChildHeight / 2d;
  18.  
  19.   // Calcular el ángulo entre cada hijo.
  20.   double AngleStep = Math.PI * 2d / CountHijos;
  21.   double AngleCount = (Angulo * Math.PI / 180d) + Math.PI * 1.5d;
  22.  
  23.   double CenterX = finalSize.Width / 2d;
  24.   double CenterY = finalSize.Height / 2d;
  25.  
  26.   for (int i = 0; i < CountHijos; i++)
  27.   {
  28.     Children[i].Arrange(
  29.       new Rect(
  30.         CenterX + Math.Cos(AngleCount) * Radio –
  31.           Children[i].DesiredSize.Width / 2d,
  32.         CenterY + Math.Sin(AngleCount) * Radio –
  33.           Children[i].DesiredSize.Height / 2d,
  34.         Children[i].DesiredSize.Width,
  35.         Children[i].DesiredSize.Height));
  36.  
  37.     AngleCount += AngleStep;
  38.   }
  39.  
  40.   return finalSize;
  41. }

Aquí nuevamente viene como parámetro el espacio otorgado por nuestro padre (el del pánel). Aquí podemos asumir con tranquilidad que esta vez no nos va a llegar un espacio ilimitado y que dicho espacio es del que finalmente dispondremos.

Comencemos contando el número de hijos que tiene nuestro control, dato que tomamos de la colección Children. Esto es cortesía del hecho que heredamos de la clase Panel. Si nuestro pánel no tiene hijos podemos tranquilamente terminar el procesamiento y retornar como espacio necesario el mismo que nos otorgaron.

Antes de continuar recordemos cómo se verá nuestro pánel una vez “pinte” su contenido:

Preliminar1

Aclaro, creo que por tercera vez, que todo el código siguiente y anterior se encarga de las necesidades particulares de nuestro ejemplo: organizar los hijos del pánel en el borde de una circunferencia.

Preliminar2

Primero determinamos cual es el control más ancho, el más alto y recordamos esos máximos (adoro LINQ). Con esto trataremos que los hijos no se salgan de los límites de nuestro pánel.

Luego calcularemos cual debería ser el radio de la circunferencia que vamos a utilizar para organizar los hijos. Obsérvese que en el cuadrante que vamos a trabajar los valores X se incrementan a la derecha y los valores Y a hacia abajo, y con esto quiero aclarar un detalle importante: la posición final de los hijos deberá ser especificada en coordenadas X,Y. Debemos tener en cuenta esto al momento de calcular el radio, representado como la línea en rojo. Obsérvese que el radio calculado depende si la altura es menor que el ancho concedido o viceversa, esto para que el radio no sea tan largo que se salga de los confines de nuestro pánel. A propósito, no me voy a extender explicando cómo se calcula el punto de una circunferencia ni para qué sirve el radio… asumiré con cierto grado de tranquilidad que todos lo sabemos o podemos averiguarlo fácilmente.

En seguida dividimos una circunferencia de 360° en tantas partes (ángulos) como hijos tiene nuestro pánel e inicializamos una variable “AngleCount” donde vamos “contando” el ángulo en que debemos pintar cada hijo. A esta “AngleCount” le vamos a restar un cuarto de circunferencia para que el primer hijo se pinte en la parte superior de la circunferencia (que el primer hijo se pinte en esa posición fue un decisión arbitraria mía). Nótese que para toda la matemática que utilizamos aquí los grados están expresados en radianes y no en centígrados.

A continuación calculamos el centro de nuestra “circunferencia virtual”. Muy sencillo: la mitad del ancho y la mitad del alto del espacio que disponemos.

Ahora viene la parte crucial. Hay que ubicar cada uno de los hijos y al mismo tiempo decirles de cuanto espacio dispone cada uno. Esto lo hacemos pasándole al método Arrange de cada hijo un objeto del tipo Rect (rectángulo). Observen como calculamos la coordenada X,Y (los dos primeros parámetros del Rect) con ayuda del radio (la línea en rojo): a partir del centro ya calculado usamos un poco de trigo(nometría) y restamos la mitad del tamaño deseado por cada hijo (DesiredSize). En seguida, para los dos parámetros restantes, simplemente le consultamos a cada hijo cuanto espacio desea. Tomaremos esos valores directamente ya que para este ejemplo no nos interesa (o no me interesa) restringir (ya no me gustó “constreñir”)el tamaño de los hijos. Luego incrementamos la variable en la que llevamos el conteo del ángulo y la dejamos lista para el cálculo de la posición del siguiente hijo. Finalmente retornamos el mismo espacio que se nos concedió indicando así que dicho espacio fue suficiente.

Y eso es todo.

Bonus Track

Habrán notado que las imágenes en este artículo y el código mismo incluyen un control Slider que sirve para rotar 360° la posición de todos los hijos del pánel y que no hemos comentado algo al respecto… pues aquí viene el “Cómo se hizo”.

La implementación de esta funcionalidad no tiene ningún misterio pues todo lo que se necesita es incrementar el ángulo con el que calculamos el radio, cambiamos:

  1. double AngleCount = (Math.PI / 180d) + Math.PI * 1.5d;

por:

  1. double AngleCount = (Angulo * Math.PI / 180d) + Math.PI * 1.5d;

 

Agregamos una Dependeny Property de tipo Double para incrementar el ángulo:

  1. public double Angulo
  2. {
  3.   get { return (double)GetValue(AnguloProperty); }
  4.   set { SetValue(AnguloProperty, value); }
  5. }
  6.  
  7. public static readonly DependencyProperty AnguloProperty =
  8.     DependencyProperty.Register(“Angulo”, typeof(double),
  9.     typeof(CircularPanel),
  10.     new PropertyMetadata(0d, IsAnguloPropertyChanged));
  11.  
  12. private static void IsAnguloPropertyChanged(
  13.   DependencyObject d,
  14.   DependencyPropertyChangedEventArgs e)
  15. {
  16.   if (e.OldValue != e.NewValue)
  17.     (d as Panel).InvalidateArrange();
  18.  
  19. }

Elegí una Dependency Property ya que no sólo permite Bindings también permite aplicar animaciones sobre el valor del ángulo.

Ah, un dato muy importante: Cambiar el Angulo no invoca automáticamente el método ArrangeOverride (donde hacemos un par de cálculos que dependen de dicho dato), razón por la cual estoy invocando un callback (el método IsAnguloPropertyChanged) donde le informo a Silverlight que debe llamar el método ArrangeOverride ((d as Panel).InvalidateArrange()).

Si sucediese que requiriéramos un recálculo del método MeasureOverride entonces debemos invocar MiPanel.InvalidateMeasure(). Invocar este último método implica a su vez un llamado automático a ArrangeOverride. Lo contrario no se da.

Finalmente ajustamos nuestro XAML para hacer un Binding entre el Slider y nuestro pánel:

  1. <my:CircularPanel x:Name=”cp01″
  2.                  Grid.Row=”0″
  3.                  Angulo=”{Binding ElementName=MiSlider01, Path=Value}”>
  4.   <Button Content=”12″ />
  5.   <Button Content=”1″ />
  6.   <Button Content=”2″ />
  7.   <Button Content=”3″ />
  8.   <Button Content=”4″ />
  9.   <Button Content=”5″ />
  10.   <Button Content=”6″ />
  11.   <Button Content=”7″ />
  12.   <Button Content=”8″ />
  13.   <Button Content=”9″ />
  14.   <Button Content=”10″ />
  15.   <Button Content=”11″ />
  16. </my:CircularPanel>
  17. <Slider x:Name=”MiSlider01″
  18.        Grid.Row=”1″
  19.        Margin=”10″
  20.        Maximum=”360″
  21.        Minimum=”0″ />

El demo de este ejercicio lo encuentran aquí.

Como podrán notar la construcción de un pánel no es nada difícil, es simplemente diferente comparado con la construcción de un control. Solo tener en cuenta que hay dos pasos: uno para estimar el espacio necesario y un segundo para ubicar los hijos del pánel y restringir su tamaño.

Y no olviden… nunca invocamos directamente MeasureOverride ni ArrangeOverride, solo el runtime de Silverlight debe llamarlos. Cuando se nos ofrezca que Silverlight los llame porque creemos que ha cambiado el contenido del pánel o la organización de sus hijos y no vemos ese cambio reflejado en pantalla, debemos invocar InvalidateMeasure o InvalidateArrange. Generalmente cuando modifiquen la colección de hijos de su pánel, bien sea en tiempo de diseño o de ejecución, no necesitarán invocar estos métodos ya que el runtime es lo suficientemente listo (según creo) para saber que dicho cambio puede implicar una reorganización del contenido del pánel, por lo que el mismo runtime los invocará.

Anuncios

Acerca de SilverIdeas

Instructor y entusiasta en el uso de Silverlight y otras tecnologías XAML.
Esta entrada fue publicada en Silverlight. Guarda el enlace permanente.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s