content wpf layout stackpanel

wpf - stackpanel content alignment



¿Cómo puedo hacer que los elementos organizados en un StackPanel horizontal compartan una línea de base común para su contenido de texto? (7)

Aquí hay un ejemplo trivial del problema que estoy teniendo:

<StackPanel Orientation="Horizontal"> <Label>Foo</Label> <TextBox>Bar</TextBox> <ComboBox> <TextBlock>Baz</TextBlock> <TextBlock>Bat</TextBlock> </ComboBox> <TextBlock>Plugh</TextBlock> <TextBlock VerticalAlignment="Bottom">XYZZY</TextBlock> </StackPanel>

Cada uno de esos elementos, excepto el TextBox y el ComboBox posicionan verticalmente el texto que contienen de manera diferente, y se ve claramente feo.

Puedo alinear el texto en estos elementos especificando un Margin para cada uno. Eso funciona, excepto que el margen está en píxeles y no en relación con la resolución de la pantalla, el tamaño de la fuente o cualquiera de las otras cosas que serán variables.

Ni siquiera estoy seguro de cómo calculo el margen inferior correcto para un control en tiempo de ejecución.

¿Cuál es la mejor manera de hacer esto?


Eso funciona, excepto que el margen está en píxeles y no en relación con la resolución de la pantalla, el tamaño de la fuente o cualquiera de las otras cosas que serán variables.

Sus suposiciones son incorrectas. (Lo sé, porque solía tener las mismas suposiciones y las mismas preocupaciones).

En realidad no píxeles

En primer lugar, el margen no está en píxeles. (Ya crees que estoy loco, ¿verdad?) De los documentos para FrameworkElement.Margin :

La unidad predeterminada para una medida de Espesor es una unidad independiente del dispositivo (1 / 96th inch).

Creo que las versiones anteriores de la documentación tienden a llamar a esto un "píxel" o, más tarde, un "píxel independiente del dispositivo". Con el tiempo, se dieron cuenta de que esta terminología era un gran error, porque WPF en realidad no hace nada en términos de píxeles físicos; estaban usando el término para significar algo nuevo, pero su audiencia asumía que significaba lo que significaba. siempre tuvo Así que ahora los documentos tienden a evitar la confusión evitando cualquier referencia a "píxeles"; ahora usan "unidad independiente del dispositivo" en su lugar.

Si la configuración de pantalla de su computadora se establece en 96 ppp (la configuración predeterminada de Windows), estas unidades independientes del dispositivo se corresponderán de una a una con píxeles. Pero si ha establecido la configuración de pantalla en 120 ppp (llamadas "fuentes grandes" en versiones anteriores de Windows), su elemento WPF con Altura = "96" tendrá 120 píxeles físicos.

Por lo tanto, su suposición de que el margen "no será relativo a la resolución de la pantalla" es incorrecta. Puede verificar esto usted mismo escribiendo su aplicación WPF, luego cambiando a 120 ppp o 144 ppp y ejecutando su aplicación, y observando que todo sigue en línea. Su preocupación de que el margen "no es relativo a la resolución de la pantalla" no es un problema.

(En Windows Vista, cambia a 120 ppp haciendo clic con el botón derecho en el escritorio> Personalizar y haciendo clic en el enlace "Ajustar tamaño de fuente (DPI)" en la barra lateral. Creo que es algo similar en Windows 7. Tenga en cuenta que esto requiere un reinicio cada vez que lo cambies.)

El tamaño de letra no importa

En cuanto al tamaño de la fuente, eso también es un problema. Así es como puedes probarlo. Pegue el siguiente XAML en Kaxaml o en cualquier otro editor de WPF:

<StackPanel Orientation="Horizontal" VerticalAlignment="Top"> <ComboBox SelectedIndex="0"> <TextBlock Background="Blue">Foo</TextBlock> </ComboBox> <ComboBox SelectedIndex="0" FontSize="100pt"> <TextBlock Background="Blue">Foo</TextBlock> </ComboBox> </StackPanel>

Observe que el grosor del cromo ComboBox no se ve afectado por el tamaño de la fuente. La distancia desde la parte superior del ComboBox a la parte superior del TextBlock es exactamente la misma, ya sea que esté utilizando el tamaño de fuente predeterminado o un tamaño de fuente totalmente extremo. El margen incorporado del combobox es constante.

Ni siquiera importa si usa fuentes diferentes, siempre que use la misma fuente tanto para la etiqueta como para el contenido del ComboBox, y el mismo tamaño de fuente, estilo de fuente, etc. Las partes superiores de las etiquetas se alinearán, y Si las partes superiores se alinean, las líneas de base también lo harán.

Así que sí, usa márgenes.

Lo sé, suena descuidado. Pero WPF no tiene una línea de base incorporada, y los márgenes son el mecanismo que nos dieron para enfrentar este tipo de problema. Y lo hicieron para que los márgenes funcionaran.

Aquí tienes un consejo. Cuando probé esto por primera vez, no estaba convencido de que el cromo del combobox se correspondería exactamente con un margen superior de 3 píxeles. Después de todo, muchas cosas en WPF, incluyendo y especialmente los tamaños de fuente, se miden de manera exacta, no integral. tamaños y luego se ajustaron a los píxeles del dispositivo: ¿cómo podría saber que las cosas no se desalinearían en las configuraciones de pantalla de 120 ppp o 144 ppp debido al redondeo?

La respuesta resulta ser fácil: pegas una maqueta de tu código en Kaxaml y luego te acercas (hay una barra deslizante de zoom en la parte inferior izquierda de la ventana). Si todo se alinea, incluso cuando te acercas, estás bien.

Pegue el siguiente código en Kaxaml y luego comience a utilizar el zoom para demostrar que los márgenes realmente son el camino a seguir. Si la superposición roja se alinea con la parte superior de las etiquetas azules con un 100% de zoom, y también con un 125% de zoom (120 ppp) y un 150% de zoom (144 ppp), entonces puede estar bastante seguro de que funcionará con cualquier cosa. Lo probé y, en el caso de ComboBox, puedo decirles que usaron un tamaño integral para el cromo. Un margen superior de 3 hará que su etiqueta se alinee con el texto de ComboBox cada vez.

(Si no quieres usar Kaxaml, puedes agregar un ScaleTransform temporal a tu XAML para escalarlo a 1.25 o 1.5, y asegurarte de que las cosas sigan alineadas. Eso funcionará incluso si tu editor de XAML preferido no tiene una característica de zoom.)

<Grid> <StackPanel Orientation="Horizontal" VerticalAlignment="Top"> <TextBlock Background="Blue" VerticalAlignment="Top" Margin="0 3 0 0">Label:</TextBlock> <ComboBox SelectedIndex="0"> <TextBlock Background="Blue">Combobox</TextBlock> </ComboBox> </StackPanel> <Rectangle Fill="#6F00" Height="3" VerticalAlignment="Top"/> </Grid>

  • Al 100%:
  • Al 125%:
  • Al 150%:

Siempre se alinean. Los márgenes son el camino a seguir.


El problema

Entonces, como lo entiendo, el problema es que usted desea colocar los controles horizontalmente en un StackPanel y alinearlos con la parte superior, pero tener el texto en cada control alineado. Además, no desea tener que establecer algo para cada control: un Style o un Margin .

El enfoque basico

La raíz del problema es que los diferentes controles tienen diferentes cantidades de "sobrecarga" entre el límite del control y el texto dentro. Cuando estos controles están alineados en la parte superior, el texto dentro aparece en diferentes ubicaciones.

Entonces, lo que queremos hacer es aplicar un desplazamiento vertical que se personalice para cada control. Esto debería funcionar para todos los tamaños de fuente y todos los DPI: WPF funciona en medidas de longitud independientes del dispositivo.

Automatizando el proceso

Ahora podemos aplicar un Margin para obtener nuestra compensación, pero eso significa que debemos mantener esto en cada control en el StackPanel .

¿Cómo automatizamos esto? Desafortunadamente sería muy difícil conseguir una solución a prueba de balas; es posible anular la plantilla de un control, lo que cambiaría la cantidad de gastos generales de diseño en el control. Pero es posible preparar un control que puede ahorrar una gran cantidad de trabajo de alineación manual, siempre que podamos asociar un tipo de control (TextBox, Label, etc.) con un desplazamiento dado.

La solución

Hay varios enfoques diferentes que podría tomar, pero creo que este es un problema de diseño y necesita un poco de lógica personalizada de Medir y Organizar:

public class AlignStackPanel : StackPanel { public bool AlignTop { get; set; } protected override Size MeasureOverride(Size constraint) { Size stackDesiredSize = new Size(); UIElementCollection children = InternalChildren; Size layoutSlotSize = constraint; bool fHorizontal = (Orientation == Orientation.Horizontal); if (fHorizontal) { layoutSlotSize.Width = Double.PositiveInfinity; } else { layoutSlotSize.Height = Double.PositiveInfinity; } for (int i = 0, count = children.Count; i < count; ++i) { // Get next child. UIElement child = children[i]; if (child == null) { continue; } // Accumulate child size. if (fHorizontal) { // Find the offset needed to line up the text and give the child a little less room. double offset = GetStackElementOffset(child); child.Measure(new Size(Double.PositiveInfinity, constraint.Height - offset)); Size childDesiredSize = child.DesiredSize; stackDesiredSize.Width += childDesiredSize.Width; stackDesiredSize.Height = Math.Max(stackDesiredSize.Height, childDesiredSize.Height + GetStackElementOffset(child)); } else { child.Measure(layoutSlotSize); Size childDesiredSize = child.DesiredSize; stackDesiredSize.Width = Math.Max(stackDesiredSize.Width, childDesiredSize.Width); stackDesiredSize.Height += childDesiredSize.Height; } } return stackDesiredSize; } protected override Size ArrangeOverride(Size arrangeSize) { UIElementCollection children = this.Children; bool fHorizontal = (Orientation == Orientation.Horizontal); Rect rcChild = new Rect(arrangeSize); double previousChildSize = 0.0; for (int i = 0, count = children.Count; i < count; ++i) { UIElement child = children[i]; if (child == null) { continue; } if (fHorizontal) { double offset = GetStackElementOffset(child); if (this.AlignTop) { rcChild.Y = offset; } rcChild.X += previousChildSize; previousChildSize = child.DesiredSize.Width; rcChild.Width = previousChildSize; rcChild.Height = Math.Max(arrangeSize.Height - offset, child.DesiredSize.Height); } else { rcChild.Y += previousChildSize; previousChildSize = child.DesiredSize.Height; rcChild.Height = previousChildSize; rcChild.Width = Math.Max(arrangeSize.Width, child.DesiredSize.Width); } child.Arrange(rcChild); } return arrangeSize; } private static double GetStackElementOffset(UIElement stackElement) { if (stackElement is TextBlock) { return 5; } if (stackElement is Label) { return 0; } if (stackElement is TextBox) { return 2; } if (stackElement is ComboBox) { return 2; } return 0; } }

Comencé con los métodos de Medir y Organizar de StackPanel, luego eliminé las referencias a eventos de desplazamiento y ETW y añadí el búfer de espaciado necesario en función del tipo de elemento presente. La lógica solo afecta a los paneles de pila horizontales.

La propiedad AlignTop controla si el espaciado alineará el texto con la parte superior o inferior.

Los números necesarios para alinear el texto pueden cambiar si los controles obtienen una plantilla personalizada, pero no es necesario que coloque un Margin o Style diferente en cada elemento de la colección. Otra ventaja es que ahora puede especificar Margin en los controles secundarios sin interferir con la alineación.

Resultados:

<local:AlignStackPanel Orientation="Horizontal" AlignTop="True" > <Label>Foo</Label> <TextBox>Bar</TextBox> <ComboBox SelectedIndex="0"> <TextBlock>Baz</TextBlock> <TextBlock>Bat</TextBlock> </ComboBox> <TextBlock>Plugh</TextBlock> </local:AlignStackPanel>

AlignTop="False" :


Cómo terminé resolviendo esto fue usar márgenes de tamaño fijo y relleno.

El problema real que estaba teniendo era que estaba permitiendo a los usuarios cambiar el tamaño de fuente dentro de la aplicación. Esto parecía una buena idea para alguien que estaba llegando a este problema desde la perspectiva de Windows Forms. Pero arruinó todo el diseño; los márgenes y el relleno que se veían bien con texto de 12 puntos lucían terribles con texto de 36 puntos.

Sin embargo, desde la perspectiva de WPF, una forma mucho más fácil (y mejor) de lograr lo que realmente estaba tratando de lograr, una interfaz de usuario cuyo tamaño el usuario podría ajustar a su gusto, fue simplemente poner un ScaleTransform sobre la vista, y vincule su ScaleX y ScaleY al valor de un control deslizante.

Esto no solo brinda a los usuarios un control mucho más preciso sobre el tamaño de su interfaz de usuario, sino que también significa que toda la alineación y los ajustes realizados para que las cosas se alineen correctamente siguen funcionando, independientemente del tamaño de la interfaz de usuario.


Cada UIElement tiene algún relleno interno adjunto que es diferente para la etiqueta, el bloque de texto y cualquier otro control. Creo que la configuración de relleno para cada control hará por ti. **

El margen especifica el espacio relativo a otro UIElement en píxeles que puede no ser consistente en el cambio de tamaño o en cualquier otra operación, mientras que el relleno es interno para cada UIElement que no se verá afectado en el cambio de tamaño de la ventana.

**

<StackPanel Orientation="Horizontal"> <Label Padding="10">Foo</Label> <TextBox Padding="10">Bar</TextBox> <ComboBox Padding="10"> <TextBlock>Baz</TextBlock> <TextBlock>Bat</TextBlock> </ComboBox> <TextBlock Padding="10">Plugh</TextBlock> <TextBlock Padding="10" VerticalAlignment="Bottom">XYZZY</TextBlock> </StackPanel>

Aquí, proporciono un relleno interno uniforme de tamaño 10 para cada control, siempre puedes jugar con él para cambiarlo con respecto a los tamaños de relleno izquierdo, superior, derecho, inferior.

Vea las capturas de pantalla adjuntas arriba para referencia (1) Sin relleno y (2) Con relleno Espero que esto pueda ser de alguna ayuda ...


Esto es complicado ya que ComboBox y TextBlock tienen diferentes márgenes internos. En tales circunstancias, siempre dejé todo para tener VerticalAlignment como Centro que no se ve muy bien, pero sí bastante aceptable.

Como alternativa, puede crear su propio CustomControl derivado de ComboBox, inicializar su margen en el constructor y reutilizarlo en todas partes.


Tal vez esto ayude:

<Window x:Class="Wpfcrm.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:Wpfcrm" mc:Ignorable="d" Title="Business" Height="600" Width="1000" WindowStartupLocation="CenterScreen" ResizeMode="NoResize"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="434*"/> <ColumnDefinition Width="51*"/> <ColumnDefinition Width="510*"/> </Grid.ColumnDefinitions> <StackPanel x:Name="mainPanel" Orientation="Vertical" Grid.ColumnSpan="3"> <StackPanel.Background> <RadialGradientBrush> <GradientStop Color="Black" Offset="0"/> <GradientStop Color="White"/> <GradientStop Color="White"/> </RadialGradientBrush> </StackPanel.Background> <DataGrid Name="grdUsers" ColumnWidth="*" Margin="0,-20,0,273" Height="272"> </DataGrid> </StackPanel> <StackPanel Orientation="Horizontal" Grid.ColumnSpan="3"> <TextBox Name="txtName" Text="Name" Width="203" Margin="70,262,0,277"/> <TextBox x:Name="txtPass" Text="Pass" Width="205" Margin="70,262,0,277"/> <TextBox x:Name="txtPosition" Text="Position" Width="205" Margin="70,262,0,277"/> </StackPanel> <StackPanel Orientation="Vertical" VerticalAlignment="Bottom" Height="217" Grid.ColumnSpan="3" Margin="263,0,297,0"> <Button Name="btnUpdate" Content="Update" Height="46" FontSize="24" FontWeight="Bold" FontFamily="Comic Sans MS" Margin="82,0,140,0" BorderThickness="1"> <Button.Background> <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0"> <GradientStop Color="Black"/> <GradientStop Color="#FF19A0AE" Offset="0.551"/> </LinearGradientBrush> </Button.Background> </Button> </StackPanel> </Grid> </Window>


VerticalContentAlignment & HorizontalContentAlignment, luego especifique el relleno y el margen de 0 para cada control secundario.