Como posso fazer uma caixa de combinação WPF ter a largura de seu elemento mais largo em XAML?

103

Eu sei como fazer isso em código, mas isso pode ser feito em 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ő
fonte
Confira outra postagem nas linhas semelhantes em stackoverflow.com/questions/826985/… Marque sua pergunta como "respondida" se isso responder à sua pergunta.
Sudeep
Eu tentei essa abordagem no código também, mas descobri que a medição pode variar entre o Vista e o XP. No Vista, DesiredSize geralmente inclui o tamanho da seta suspensa, mas no XP, muitas vezes a largura não inclui a seta suspensa. Agora, meus resultados podem ser porque estou tentando fazer a medição antes que a janela principal esteja visível. Adicionar um UpdateLayout () antes de Measure pode ajudar, mas pode causar outros efeitos colaterais no aplicativo. Eu estaria interessado em ver a solução que você apresenta, se você estiver disposto a compartilhar.
jschroedl
Como você resolveu seu problema?
Andrew Kalashnikov,

Respostas:

31

Isso não pode ser em XAML sem:

  • Criando um controle oculto (resposta de Alan Hunford)
  • Mudando o ControlTemplate drasticamente. Mesmo nesse caso, pode ser necessário criar uma versão oculta de um ItemsPresenter.

O motivo para isso é que os ComboBox ControlTemplates padrão que encontrei (Aero, Luna, etc.) aninham o ItemsPresenter em um Popup. Isso significa que o layout desses itens é adiado até que eles se tornem realmente visíveis.

Uma maneira fácil de testar isso é modificar o ControlTemplate padrão para vincular o MinWidth do contêiner mais externo (é um Grid para Aero e Luna) ao ActualWidth de PART_Popup. Você poderá fazer com que o ComboBox sincronize automaticamente sua largura ao clicar no botão de soltar, mas não antes.

Portanto, a menos que você possa forçar uma operação Medir no sistema de layout (o que pode ser feito adicionando um segundo controle), não acho que isso possa ser feito.

Como sempre, estou aberto a uma solução curta e elegante - mas, neste caso, hacks code-behind ou dual-control / ControlTemplate são as únicas soluções que vi.

micahtan
fonte
57

Você não pode fazer isso diretamente no Xaml, mas pode usar este comportamento anexado. (A largura ficará visível no Designer)

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

The Attached Behavior ComboBoxWidthFromItemsProperty

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);
    }
}

O que ele faz é chamar um método de extensão para ComboBox chamado SetWidthFromItems que (invisivelmente) se expande e se retrai e então calcula a largura com base nos ComboBoxItems gerados. (IExpandCollapseProvider requer uma referência a UIAutomationProvider.dll)

Em seguida, o método de extensão 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 extensão também fornece a capacidade de chamar

comboBox.SetWidthFromItems();

no code behind (por exemplo, no evento ComboBox.Loaded)

Fredrik Hedblad
fonte
+1, ótima solução! Eu estava tentando fazer algo na mesma linha, mas acabei usando sua implementação (com algumas modificações)
Thomas Levesque
1
Muito obrigado. Isso deve ser marcado como a resposta aceita. Parece que as propriedades anexas são sempre o caminho para tudo :)
Ignacio Soler Garcia
Melhor solução, tanto quanto eu estou preocupado. Tentei vários truques em toda a Internet, e sua solução é a melhor e mais fácil que encontrei. +1.
paercebal
7
Observe que se você tiver várias caixas de combinação na mesma janela ( isso aconteceu para mim com uma janela criando as caixas de combinação e seu conteúdo com code-behind ), os pop-ups podem se tornar visíveis por um segundo. Acho que isso ocorre porque várias mensagens de "pop-up aberto" são postadas antes de qualquer "pop-up de fechamento" ser chamado. A solução para isso é tornar todo o método SetWidthFromItemsassíncrono my usando uma action / delegate e um BeginInvoke com uma prioridade Idle (como feito no evento Loaded). Dessa forma, nenhuma medida será feita enquanto a bomba de mensagem não estiver vazia e, portanto, nenhuma intercalação de mensagem ocorrerá
paercebal de
1
O número mágico: double comboBoxWidth = 19;em seu código está relacionado a SystemParameters.VerticalScrollBarWidth?
Jf Beaulac
10

Sim, este é um pouco desagradável.

O que fiz no passado foi adicionar ao ControlTemplate uma caixa de listagem oculta (com seu painel de contêiner de itens definido como uma grade) mostrando cada item ao mesmo tempo, mas com sua visibilidade definida como oculta.

Eu ficaria satisfeito em saber de ideias melhores que não dependem do horrível code-behind ou de sua visualização ter que entender que ele precisa usar um controle diferente para fornecer a largura para suportar os visuais (eca!).

Alun Harford
fonte
1
Essa abordagem dimensionará o combo o suficiente para que o item mais largo fique totalmente visível quando for o item selecionado? É aqui que tenho visto problemas.
jschroedl
8

Com base nas outras respostas acima, aqui está minha versão:

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

HorizontalAlignment = "Left" interrompe os controles usando a largura total do controle que o contém. Height = "0" oculta o controle de itens.
Margin = "15,0" permite cromo adicional em torno de itens da caixa de combinação (não é agnóstico de cromo, infelizmente).

Gaspode
fonte
4

Acabei com uma solução "boa o suficiente" para esse problema: fazer com que a caixa de combinação nunca diminua abaixo do maior tamanho que ela continha, semelhante ao antigo WinForms AutoSizeMode = GrowOnly.

A maneira como fiz isso foi com um conversor 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();
    }
}

Em seguida, configuro a caixa de combinação em XAML assim:

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

Observe que, com isso, você precisa de uma instância separada do GrowConverter para cada caixa de combinação, a menos, é claro, que você queira que um conjunto deles seja dimensionado juntos, semelhante ao recurso SharedSizeScope do Grid.

guepardo
fonte
1
Bom, mas apenas "estável" após ter selecionado a entrada mais longa.
primfaktor
1
Corrigir. Eu tinha feito algo sobre isso no WinForms, onde usaria as APIs de texto para medir todas as strings na caixa de combinação e definiria a largura mínima para isso. Fazer o mesmo é consideravelmente mais difícil no WPF, especialmente quando seus itens não são strings e / ou vêm de uma ligação.
Cheetah de
3

Seguindo a resposta de Maleak: Gostei tanto dessa implementação que escrevi um Behavior real para ela. Obviamente, você precisará do Blend SDK para que possa fazer referência 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
fonte
Isso não funciona quando o ComboBox não está habilitado. provider.Expand()joga um ElementNotEnabledException. Quando o ComboBox não está habilitado, devido a um dos pais estar desabilitado, então não é nem mesmo possível habilitar temporariamente o ComboBox até que a medição seja concluída.
FlyingFoX
1

Coloque uma caixa de listagem com o mesmo conteúdo atrás da caixa de depósito. Em seguida, reforce a altura correta com alguma amarração como esta:

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

No meu caso, uma maneira muito mais simples parecia funcionar, apenas usei um stackPanel extra para embrulhar a caixa de combinação.

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

(trabalhou no visual studio 2008)

Nikos Tsokos
fonte
1

Uma solução alternativa para a primeira resposta é Medir o próprio Popup em vez de medir todos os itens. Oferecendo uma SetWidthFromItems()implementação um pouco mais simples :

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;
    }
}

funciona em aparelhos com deficiência ComboBoxtambém.

Wonderra
fonte
0

Eu estava procurando a resposta, quando me deparei com o UpdateLayout()método que todo mundo UIElementtem.

É muito simples agora, felizmente!

Basta ligar ComboBox1.Updatelayout();depois de definir ou modificar o ItemSource.

Sinker
fonte
0

Abordagem de Alun Harford, na prática:

<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
fonte
0

Isso mantém a largura até o elemento mais largo, mas somente após abrir a caixa de combinação uma 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
fonte