¿Cómo puedo hacer que un cuadro combinado de WPF tenga el ancho de su elemento más ancho en XAML?

103

Sé cómo hacerlo en código, pero ¿se puede hacer en XAML?

Window1.xaml:

<Window x:Class="WpfApplication1.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300">
    <Grid>
        <ComboBox Name="ComboBox1" HorizontalAlignment="Left" VerticalAlignment="Top">
            <ComboBoxItem>ComboBoxItem1</ComboBoxItem>
            <ComboBoxItem>ComboBoxItem2</ComboBoxItem>
        </ComboBox>
    </Grid>
</Window>

Window1.xaml.cs:

using System.Windows;
using System.Windows.Controls;

namespace WpfApplication1
{
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
            double width = 0;
            foreach (ComboBoxItem item in ComboBox1.Items)
            {
                item.Measure(new Size(
                    double.PositiveInfinity, double.PositiveInfinity));
                if (item.DesiredSize.Width > width)
                    width = item.DesiredSize.Width;
            }
            ComboBox1.Measure(new Size(
                double.PositiveInfinity, double.PositiveInfinity));
            ComboBox1.Width = ComboBox1.DesiredSize.Width + width;
        }
    }
}
Csupor Jenő
fuente
Consulte otra publicación en líneas similares en stackoverflow.com/questions/826985/… Marque su pregunta como "respondida" si esto responde a su pregunta.
Sudeep
También probé este enfoque en el código, pero descubrí que la medición puede variar entre Vista y XP. En Vista, DesiredSize generalmente incluye el tamaño de la flecha desplegable, pero en XP, a menudo el ancho no incluye la flecha desplegable. Ahora, mis resultados pueden deberse a que estoy intentando hacer la medición antes de que la ventana principal sea visible. Agregar un UpdateLayout () antes de la medida puede ayudar, pero puede causar otros efectos secundarios en la aplicación. Me interesaría ver la solución que se te ocurra si estás dispuesto a compartirla.
jschroedl
¿Cómo resolvió su problema?
Andrew Kalashnikov

Respuestas:

31

Esto no puede estar en XAML sin:

  • Creando un control oculto (respuesta de Alan Hunford)
  • Cambiando la ControlTemplate drásticamente. Incluso en este caso, es posible que deba crearse una versión oculta de ItemsPresenter.

La razón de esto es que las plantillas de control de ComboBox predeterminadas con las que me he encontrado (Aero, Luna, etc.) anidan todos los ItemsPresenter en un Popup. Esto significa que el diseño de estos elementos se aplaza hasta que se hagan visibles.

Una manera fácil de probar esto es modificar la ControlTemplate predeterminada para vincular el MinWidth del contenedor más externo (es una Grid para Aero y Luna) al ActualWidth de PART_Popup. Podrá hacer que ComboBox sincronice automáticamente su ancho cuando haga clic en el botón para soltar, pero no antes.

Entonces, a menos que pueda forzar una operación de Medición en el sistema de diseño (lo que puede hacer agregando un segundo control), no creo que se pueda hacer.

Como siempre, estoy abierto a una solución corta y elegante, pero en este caso, las únicas soluciones que he visto son un código subyacente o un control dual / ControlTemplate.

micahtan
fuente
57

No puede hacerlo directamente en Xaml, pero puede usar este comportamiento adjunto. (El ancho será visible en el diseñador)

<ComboBox behaviors:ComboBoxWidthFromItemsBehavior.ComboBoxWidthFromItems="True">
    <ComboBoxItem Content="Short"/>
    <ComboBoxItem Content="Medium Long"/>
    <ComboBoxItem Content="Min"/>
</ComboBox>

ComboBoxWidthFromItemsProperty de comportamiento adjunto

public static class ComboBoxWidthFromItemsBehavior
{
    public static readonly DependencyProperty ComboBoxWidthFromItemsProperty =
        DependencyProperty.RegisterAttached
        (
            "ComboBoxWidthFromItems",
            typeof(bool),
            typeof(ComboBoxWidthFromItemsBehavior),
            new UIPropertyMetadata(false, OnComboBoxWidthFromItemsPropertyChanged)
        );
    public static bool GetComboBoxWidthFromItems(DependencyObject obj)
    {
        return (bool)obj.GetValue(ComboBoxWidthFromItemsProperty);
    }
    public static void SetComboBoxWidthFromItems(DependencyObject obj, bool value)
    {
        obj.SetValue(ComboBoxWidthFromItemsProperty, value);
    }
    private static void OnComboBoxWidthFromItemsPropertyChanged(DependencyObject dpo,
                                                                DependencyPropertyChangedEventArgs e)
    {
        ComboBox comboBox = dpo as ComboBox;
        if (comboBox != null)
        {
            if ((bool)e.NewValue == true)
            {
                comboBox.Loaded += OnComboBoxLoaded;
            }
            else
            {
                comboBox.Loaded -= OnComboBoxLoaded;
            }
        }
    }
    private static void OnComboBoxLoaded(object sender, RoutedEventArgs e)
    {
        ComboBox comboBox = sender as ComboBox;
        Action action = () => { comboBox.SetWidthFromItems(); };
        comboBox.Dispatcher.BeginInvoke(action, DispatcherPriority.ContextIdle);
    }
}

Lo que hace es que llama a un método de extensión para ComboBox llamado SetWidthFromItems que (de manera invisible) se expande y colapsa y luego calcula el Ancho en función de los ComboBoxItems generados. (IExpandCollapseProvider requiere una referencia a UIAutomationProvider.dll)

Luego, método de extensión SetWidthFromItems

public static class ComboBoxExtensionMethods
{
    public static void SetWidthFromItems(this ComboBox comboBox)
    {
        double comboBoxWidth = 19;// comboBox.DesiredSize.Width;

        // Create the peer and provider to expand the comboBox in code behind. 
        ComboBoxAutomationPeer peer = new ComboBoxAutomationPeer(comboBox);
        IExpandCollapseProvider provider = (IExpandCollapseProvider)peer.GetPattern(PatternInterface.ExpandCollapse);
        EventHandler eventHandler = null;
        eventHandler = new EventHandler(delegate
        {
            if (comboBox.IsDropDownOpen &&
                comboBox.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
            {
                double width = 0;
                foreach (var item in comboBox.Items)
                {
                    ComboBoxItem comboBoxItem = comboBox.ItemContainerGenerator.ContainerFromItem(item) as ComboBoxItem;
                    comboBoxItem.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
                    if (comboBoxItem.DesiredSize.Width > width)
                    {
                        width = comboBoxItem.DesiredSize.Width;
                    }
                }
                comboBox.Width = comboBoxWidth + width;
                // Remove the event handler. 
                comboBox.ItemContainerGenerator.StatusChanged -= eventHandler;
                comboBox.DropDownOpened -= eventHandler;
                provider.Collapse();
            }
        });
        comboBox.ItemContainerGenerator.StatusChanged += eventHandler;
        comboBox.DropDownOpened += eventHandler;
        // Expand the comboBox to generate all its ComboBoxItem's. 
        provider.Expand();
    }
}

Este método de extensión también proporciona la capacidad de llamar

comboBox.SetWidthFromItems();

en el código subyacente (por ejemplo, en el evento ComboBox.Loaded)

Fredrik Hedblad
fuente
+1, ¡gran solución! Estaba tratando de hacer algo en la misma línea, pero finalmente utilicé su implementación (con algunas modificaciones)
Thomas Levesque
1
Increíble gracias. Esto debe marcarse como la respuesta aceptada. Parece que las propiedades adjuntas son siempre el camino a todo :)
Ignacio Soler García
La mejor solución en lo que a mí respecta. Probé varios trucos de todo Internet y su solución es la mejor y más fácil que encontré. +1.
paercebal
7
Tenga en cuenta que si tiene varios cuadros combinados en la misma ventana ( a mí me sucedió con una ventana que creaba los cuadros combinados y su contenido con código subyacente ), las ventanas emergentes pueden volverse visibles por un segundo. Supongo que esto se debe a que se publican varios mensajes de "ventana emergente abierta" antes de que se llame a cualquier "ventana emergente cerrada". La solución para eso es hacer que todo el método sea SetWidthFromItemsasincrónico usando una acción / delegado y un BeginInvoke con una prioridad de inactividad (como se hizo en el evento Loaded). De esta manera, no se tomará ninguna medida mientras la bomba de mensajes no esté vacía y, por lo tanto, no se producirá ningún intercalado de mensajes
paercebal
1
¿Está relacionado el número mágico: double comboBoxWidth = 19;en su código SystemParameters.VerticalScrollBarWidth?
Jf Beaulac
10

Sí, este es un poco desagradable.

Lo que he hecho en el pasado es agregar a ControlTemplate un cuadro de lista oculto (con su panel de contenido de elementos configurado en una cuadrícula) que muestra todos los elementos al mismo tiempo pero con su visibilidad configurada como oculta.

Me complacería escuchar alguna idea mejor que no se base en un código subyacente horrible o que su vista tenga que entender que necesita usar un control diferente para proporcionar el ancho para soportar las imágenes (¡puaj!).

Alun Harford
fuente
1
¿Este enfoque dimensionará el combo lo suficientemente ancho para que el elemento más ancho sea completamente visible cuando sea el elemento seleccionado? Aquí es donde he visto problemas.
jschroedl
8

Según las otras respuestas anteriores, aquí está mi versión:

<Grid HorizontalAlignment="Left">
    <ItemsControl ItemsSource="{Binding EnumValues}" Height="0" Margin="15,0"/>
    <ComboBox ItemsSource="{Binding EnumValues}" />
</Grid>

HorizontalAlignment = "Left" detiene los controles usando el ancho completo del control contenedor. Height = "0" oculta el control de los elementos.
Margin = "15,0" permite cromo adicional alrededor de los elementos del cuadro combinado (me temo que no es independiente del cromo).

Gaspode
fuente
4

Terminé con una solución "suficientemente buena" para este problema que era hacer que el cuadro combinado nunca se redujera por debajo del tamaño más grande que tenía, similar al antiguo WinForms AutoSizeMode = GrowOnly.

La forma en que hice esto fue con un convertidor de valor personalizado:

public class GrowConverter : IValueConverter
{
    public double Minimum
    {
        get;
        set;
    }

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var dvalue = (double)value;
        if (dvalue > Minimum)
            Minimum = dvalue;
        else if (dvalue < Minimum)
            dvalue = Minimum;
        return dvalue;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotSupportedException();
    }
}

Luego configuro el cuadro combinado en XAML así:

 <Whatever>
        <Whatever.Resources>
            <my:GrowConverter x:Key="grow" />
        </Whatever.Resources>
        ...
        <ComboBox MinWidth="{Binding ActualWidth,RelativeSource={RelativeSource Self},Converter={StaticResource grow}}" />
    </Whatever>

Tenga en cuenta que con esto necesita una instancia separada de GrowConverter para cada cuadro combinado, a menos que, por supuesto, desee un conjunto de ellos para medir juntos, similar a la función SharedSizeScope de Grid.

leopardo
fuente
1
Agradable, pero solo “estable” después de haber seleccionado la entrada más larga.
primfaktor
1
Correcto. Había hecho algo al respecto en WinForms, donde usaría las API de texto para medir todas las cadenas en el cuadro combinado y establecer el ancho mínimo para tener en cuenta eso. Hacer lo mismo es considerablemente más difícil en WPF, especialmente cuando sus elementos no son cadenas y / o provienen de un enlace.
Cheetah
3

Un seguimiento de la respuesta de Maleak: me gustó tanto esa implementación que escribí un comportamiento real para ella. Obviamente, necesitará el SDK de Blend para poder hacer referencia a System.Windows.Interactivity.

XAML:

    <ComboBox ItemsSource="{Binding ListOfStuff}">
        <i:Interaction.Behaviors>
            <local:ComboBoxWidthBehavior />
        </i:Interaction.Behaviors>
    </ComboBox>

Código:

using System;
using System.Windows;
using System.Windows.Automation.Peers;
using System.Windows.Automation.Provider;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Interactivity;

namespace MyLibrary
{
    public class ComboBoxWidthBehavior : Behavior<ComboBox>
    {
        protected override void OnAttached()
        {
            base.OnAttached();
            AssociatedObject.Loaded += OnLoaded;
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();
            AssociatedObject.Loaded -= OnLoaded;
        }

        private void OnLoaded(object sender, RoutedEventArgs e)
        {
            var desiredWidth = AssociatedObject.DesiredSize.Width;

            // Create the peer and provider to expand the comboBox in code behind. 
            var peer = new ComboBoxAutomationPeer(AssociatedObject);
            var provider = peer.GetPattern(PatternInterface.ExpandCollapse) as IExpandCollapseProvider;
            if (provider == null)
                return;

            EventHandler[] handler = {null};    // array usage prevents access to modified closure
            handler[0] = new EventHandler(delegate
            {
                if (!AssociatedObject.IsDropDownOpen || AssociatedObject.ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated)
                    return;

                double largestWidth = 0;
                foreach (var item in AssociatedObject.Items)
                {
                    var comboBoxItem = AssociatedObject.ItemContainerGenerator.ContainerFromItem(item) as ComboBoxItem;
                    if (comboBoxItem == null)
                        continue;

                    comboBoxItem.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
                    if (comboBoxItem.DesiredSize.Width > largestWidth)
                        largestWidth = comboBoxItem.DesiredSize.Width;
                }

                AssociatedObject.Width = desiredWidth + largestWidth;

                // Remove the event handler.
                AssociatedObject.ItemContainerGenerator.StatusChanged -= handler[0];
                AssociatedObject.DropDownOpened -= handler[0];
                provider.Collapse();
            });

            AssociatedObject.ItemContainerGenerator.StatusChanged += handler[0];
            AssociatedObject.DropDownOpened += handler[0];

            // Expand the comboBox to generate all its ComboBoxItem's. 
            provider.Expand();
        }
    }
}
Mike Post
fuente
Esto no funciona cuando ComboBox no está habilitado. provider.Expand()lanza un ElementNotEnabledException. Cuando el ComboBox no está habilitado, debido a que un padre está deshabilitado, entonces ni siquiera es posible habilitar temporalmente el ComboBox hasta que la medición haya finalizado.
FlyingFoX
1

Coloque un cuadro de lista que contenga el mismo contenido detrás del cuadro desplegable. Luego aplique la altura correcta con un enlace como este:

<Grid>
       <ListBox x:Name="listBox" Height="{Binding ElementName=dropBox, Path=DesiredSize.Height}" /> 
        <ComboBox x:Name="dropBox" />
</Grid>
Matze
fuente
1

En mi caso, una forma mucho más simple pareció funcionar, solo usé un stackPanel adicional para envolver el cuadro combinado.

<StackPanel Grid.Row="1" Orientation="Horizontal">
    <ComboBox ItemsSource="{Binding ExecutionTimesModeList}" Width="Auto"
        SelectedValuePath="Item" DisplayMemberPath="FriendlyName"
        SelectedValue="{Binding Model.SelectedExecutionTimesMode}" />    
</StackPanel>

(trabajó en visual studio 2008)

Nikos Tsokos
fuente
1

Una solución alternativa a la respuesta principal es medir la ventana emergente en sí en lugar de medir todos los elementos. Dando una SetWidthFromItems()implementación un poco más simple :

private static void SetWidthFromItems(this ComboBox comboBox)
{
    if (comboBox.Template.FindName("PART_Popup", comboBox) is Popup popup 
        && popup.Child is FrameworkElement popupContent)
    {
        popupContent.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
        // suggested in comments, original answer has a static value 19.0
        var emptySize = SystemParameters.VerticalScrollBarWidth + comboBox.Padding.Left + comboBox.Padding.Right;
        comboBox.Width = emptySize + popupContent.DesiredSize.Width;
    }
}

también funciona en discapacitados ComboBox.

wondra
fuente
0

Yo mismo buscaba la respuesta cuando encontré el UpdateLayout()método que todos UIElementtienen.

¡Es muy simple ahora, afortunadamente!

Simplemente llame ComboBox1.Updatelayout();después de configurar o modificar el ItemSource.

Plomo
fuente
0

El enfoque de Alun Harford, en la práctica:

<Grid>

  <Grid.ColumnDefinitions>
    <ColumnDefinition Width="Auto"/>
    <ColumnDefinition Width="*"/>
  </Grid.ColumnDefinitions>

  <!-- hidden listbox that has all the items in one grid -->
  <ListBox ItemsSource="{Binding Items, ElementName=uiComboBox, Mode=OneWay}" Height="10" VerticalAlignment="Top" Visibility="Hidden">
    <ListBox.ItemsPanel><ItemsPanelTemplate><Grid/></ItemsPanelTemplate></ListBox.ItemsPanel>
  </ListBox>

  <ComboBox VerticalAlignment="Top" SelectedIndex="0" x:Name="uiComboBox">
    <ComboBoxItem>foo</ComboBoxItem>
    <ComboBoxItem>bar</ComboBoxItem>
    <ComboBoxItem>fiuafiouhoiruhslkfhalsjfhalhflasdkf</ComboBoxItem>
  </ComboBox>

</Grid>
Jan Van Overbeke
fuente
0

Esto mantiene el ancho al elemento más ancho pero solo después de abrir el cuadro combinado una vez.

<ComboBox ItemsSource="{Binding ComboBoxItems}" Grid.IsSharedSizeScope="True" HorizontalAlignment="Left">
    <ComboBox.ItemTemplate>
        <DataTemplate>
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition SharedSizeGroup="sharedSizeGroup"/>
                </Grid.ColumnDefinitions>
                <TextBlock Text="{Binding}"/>
            </Grid>
        </DataTemplate>
    </ComboBox.ItemTemplate>
</ComboBox>
Wouter
fuente