[WPF] Desnudando el TabControl: ajustar las pestañas en una fila

Después de bastante tiempo sin ponerme ante esta página, hoy he encontrado algo de tiempo y una excusa adecuada en las opciones de personalización que nos ofrece el control TabControl de WPF.

El hecho es que si añadimos más pestañas de las que caben en el control, se crea una segunda línea de pestañas para alojarlas:

image

Este comportamiento que en general es suficiente, en ocasiones puede no ser lo que deseamos para nuestra aplicación. En mi caso, necesitaba que al reducir el ancho de la ventana no se cree una segunda fila, sino que se estrechen las pestañas para ajustarse al ancho disponible.

Desnudando el TabControl

El modelo de extensibilidad de WPF permite atacar este problema con sencillez. Para esto, debemos conocer la composición del TabControl: internamente, utiliza un control TabPanel para mostrar las pestañas, y un ContentPresenter para el contenido de la pestaña seleccionada. El mencionado control TabPanel proporciona un diseño muy específico: es similar a un WrapPanel, ya que cuando los controles no caben saltan a la siguiente línea, pero con la particularidad de ajustar el contenido para abarcar toda la fila. Podemos ver el TabControl por dentro sustituyendo su Template por esta sencilla plantilla:

        <TabControl Margin="10" Height="80">
            <TabControl.Template>
                <ControlTemplate TargetType="TabControl">
                    <DockPanel>
                        <TabPanel DockPanel.Dock="Top"
                                  IsItemsHost="True"/>
                        <Border Background="{TemplateBinding Background}" 
                                BorderThickness="{TemplateBinding BorderThickness}" 
                                BorderBrush="{TemplateBinding BorderBrush}">
                            <ContentPresenter Name="PART_SelectedContentHost" 
                                              Margin="{TemplateBinding Padding}" 
                                              ContentSource="SelectedContent" />
                        </Border>
                    </DockPanel>
                </ControlTemplate>
            </TabControl.Template>
            <TabItem Header="Tab 1">

Si lo probamos, ahora el TabControl se dibujará de forma ligeramente distinta:

image

Aunque a grandes rasgos es el mismo diseño, por la simplicidad de la plantilla utilizada se ha descuadrado ligeramente el diseño. No es esto lo que me interesa, sino identificar que el responsable de dibujar las pestañas es el control TabPanel, lo que nos va a permitir sustituirlo por otro y conseguir otros comportamientos.

Usando un WrapPanel

Simplemente con sustituir la etiqueta TabPanel por WrapPanel (el resto de propiedades se mantiene igual), el diseño del control cambia automáticamente:

image

Evidentemente el diseño no ha mejorado, pero vemos lo sencillo que es realizar cambios una vez que nos hemos hecho con la plantilla del control.

Usando un StackPanel

Vamos a probar a usar un StackPanel horizontal. Cambiamos el WrapPanel por:

<StackPanel DockPanel.Dock="Top" Orientation="Horizontal"
            IsItemsHost="True"/>

Y obtenemos:

image

La última pestaña, Tab 7, no cabe y no hay forma de hacerla visible si no es ampliando el tamaño de la ventana. Pero veamos cómo podemos añadir la posibilidad de desplazarnos a esas pestañas ocultas.

Usando un StackPanel y un ScrollViewer

Vamos a incluir el StackPanel dentro de un ScrollViewer:

<ScrollViewer DockPanel.Dock="Top" 
                HorizontalScrollBarVisibility="Auto" 
                VerticalScrollBarVisibility="Disabled">
    <StackPanel Orientation="Horizontal"
                IsItemsHost="True"/>
</ScrollViewer>

Aunque el diseño no es muy atractivo, ya tenemos la posibilidad de desplazarnos a los elementos no visibles:

image

Este diseño puede potenciarse:

  • Para pantallas táctiles, ocultando la barra horizontal (Hidden) y activando el PanningMode="HorizontalOnly" que permitirá desplazarnos arrastrando las pestañas con el dedo.
  • Incluyendo botones de desplazamiento incrustados junto a las pestañas, lo que puede conseguirse cambiando el Template del ScrollViewer. Más información en este buen post de Olaf Rabbachin.

Pero no era esto lo que yo pretendía: mi objetivo era ajustar al ancho disponible. Lo primero que pienso es un Grid.

Por qué no uso un Grid

Evidentemente, para conseguir ese ajuste podemos crear un Grid con 7 columnas de tamaño proporcional (Star – *) y situar cada pestaña en una de ellas. El problema es que para esto necesitamos definir la propiedad adjunta Grid.Column en cada cabecera de pestaña, lo que no es sencillo. Bueno, el concepto de sencillez es diferente para cada uno, pero los buenos informáticos somos vagos (eso me digo para consolarme 😉 e intentamos conseguir más con menos.

Si recopilamos, hasta ahora no hemos tenido que establecer ninguna propiedad en los ítems para conseguir los diseños, por lo que vamos a intentar otra aproximación con un control de uso poco frecuente.

Usando un UniformGrid

El control UniformGrid es un Grid simplificado donde definimos sólo el número de filas y columnas, y se creará una cuadrícula con filas y columnas del mismo tamaño. Además, los elementos no hay que asignarlos a cada fila o columna, sino que se asignan por orden: el primer control a la primera celda, el siguiente a la segunda, así hasta completar la fila, luego la segunda fila y así hasta que no queden más ítems. Por lo cual podemos usar como contenedor sólo esto:

<UniformGrid DockPanel.Dock="Top" Columns="7" Rows="1" IsItemsHost="True"/>

Con el UniformGrid tenemos el ajuste que buscábamos:

image

image

El ajuste y los bordes puede mejorarse si se sigue más fielmente la Template original del TabControl. Yo he procurado simplificar las plantillas para que sea más fácil seguir los ejemplos, sacrificando este nivel de detalle.

Mi versión final

Aunque la solución que hemos introducido con UniformGrid tiene un defecto: necesita conocer el número de pestañas para asignar Columns=”7”. Pero vamos a solucionar esto usando un sencillo truco: si definimos Rows=”1” pero no definimos el número de columnas, se crearán tantas columnas como pestañas existan. Así que sólo tenemos que eliminar la propiedad Columns, y finalmente nos queda:

        <TabControl Margin="10" Height="80">
            <TabControl.Template>
                <ControlTemplate TargetType="TabControl">
                    <DockPanel SnapsToDevicePixels="true" ClipToBounds="true">
                        <UniformGrid DockPanel.Dock="Top" Rows="1" IsItemsHost="True"/>
                        <Border Background="{TemplateBinding Background}" 
                                BorderThickness="{TemplateBinding BorderThickness}" 
                                BorderBrush="{TemplateBinding BorderBrush}">
                            <ContentPresenter Name="PART_SelectedContentHost" 
                                              SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" 
                                              Margin="{TemplateBinding Padding}" 
                                              ContentSource="SelectedContent" />
                        </Border>
                    </DockPanel>
                </ControlTemplate>
            </TabControl.Template>
            <TabItem Header="Tab 1">
                <TextBlock Text="With UniformGrid"/>
            </TabItem>
            <TabItem Header="Tab 2"/>
            <TabItem Header="Tab 3"/>
            <TabItem Header="Tab 4"/>
            <TabItem Header="Tab 5"/>
            <TabItem Header="Tab 6"/>
            <TabItem Header="Tab 7"/>
        </TabControl>

El control UniformGrid está sólo disponible en WPF, pero es muy sencillo de implementar en Silverlight y otras plataformas XAML, como muestra Jeff Wilcox en este post.

Un placer.