Come posso fare in modo che una casella combinata WPF abbia la larghezza del suo elemento più largo in XAML?


103

So come farlo nel codice, ma può essere fatto in 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;
        }
    }
}

Controlla un altro post sulle righe simili su stackoverflow.com/questions/826985/… . Contrassegna la tua domanda come "risposta" se questo risponde alla tua domanda.
Sudeep

Ho provato questo approccio anche nel codice, ma ho scoperto che la misurazione può variare tra Vista e XP. Su Vista, DesiredSize di solito include la dimensione della freccia a discesa, ma su XP, spesso la larghezza non include la freccia a discesa. Ora, i miei risultati potrebbero essere dovuti al fatto che sto tentando di eseguire la misurazione prima che la finestra principale sia visibile. L'aggiunta di un UpdateLayout () prima della misura può aiutare ma può causare altri effetti collaterali nell'app. Sarei interessato a vedere la soluzione che trovi se sei disposto a condividere.
jschroedl

Come hai risolto il tuo problema?
Andrew Kalashnikov

Risposte:


31

Non può essere in XAML senza:

  • Creare un controllo nascosto (risposta di Alan Hunford)
  • Cambiare drasticamente il ControlTemplate. Anche in questo caso, potrebbe essere necessario creare una versione nascosta di un ItemsPresenter.

La ragione di ciò è che i ControlTemplates ComboBox predefiniti che ho incontrato (Aero, Luna, ecc.) Nidificano tutti gli ItemsPresenter in un popup. Ciò significa che il layout di questi elementi viene differito fino a quando non vengono effettivamente resi visibili.

Un modo semplice per verificarlo consiste nel modificare il ControlTemplate predefinito per associare il MinWidth del contenitore più esterno (è una griglia sia per Aero che per Luna) alla ActualWidth di PART_Popup. Sarai in grado di fare in modo che il ComboBox sincronizzi automaticamente la sua larghezza quando fai clic sul pulsante di rilascio, ma non prima.

Quindi, a meno che tu non possa forzare un'operazione di misura nel sistema di layout (cosa che puoi fare aggiungendo un secondo controllo), non penso che possa essere fatto.

Come sempre, sono aperto a una soluzione breve ed elegante, ma in questo caso un hack code-behind o dual-control / ControlTemplate sono le uniche soluzioni che ho visto.


57

Non puoi farlo direttamente in Xaml ma puoi usare questo comportamento allegato. (La larghezza sarà visibile nel Designer)

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

ComboBoxWidthFromItemsProperty del comportamento allegato

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

Quello che fa è che chiama un metodo di estensione per ComboBox chiamato SetWidthFromItems che (invisibilmente) si espande e si comprime e quindi calcola la larghezza in base ai ComboBoxItem generati. (IExpandCollapseProvider richiede un riferimento a UIAutomationProvider.dll)

Quindi il metodo di estensione 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();
    }
}

Questo metodo di estensione fornisce anche la possibilità di chiamare

comboBox.SetWidthFromItems();

nel codice sottostante (ad esempio nell'evento ComboBox.Loaded)


+1, ottima soluzione! Stavo cercando di fare qualcosa sulla stessa linea, ma alla fine ho usato la tua implementazione (con alcune modifiche)
Thomas Levesque

1
Grazie fantastico. Questo dovrebbe essere contrassegnato come risposta accettata. Sembra che le proprietà annesse siano sempre la via per tutto :)
Ignacio Soler Garcia

La migliore soluzione per quanto mi riguarda. Ho provato diversi trucchi da tutto Internet e la tua soluzione è la migliore e più semplice che ho trovato. +1.
paercebal

7
Nota che se hai più combobox nella stessa finestra ( è successo per me con una finestra che creava le combobox e il loro contenuto con code-behind ), i popup possono diventare visibili per un secondo. Immagino che ciò sia dovuto al fatto che più messaggi "apri popup" vengono inviati prima che venga chiamato qualsiasi "popup di chiusura". La soluzione è rendere SetWidthFromItemsasincrono l'intero metodo utilizzando un'azione / delegato e un BeginInvoke con una priorità di inattività (come fatto nell'evento Loaded). In questo modo, nessuna misura verrà eseguita finché la pompa del messaggio non è vuota, e quindi, non si verificherà alcun interleaving dei messaggi
paercebal

1
Il numero magico: double comboBoxWidth = 19;nel tuo codice è correlato a SystemParameters.VerticalScrollBarWidth?
Jf Beaulac

10

Sì, questo è un po 'brutto.

Quello che ho fatto in passato è stato aggiungere al ControlTemplate una casella di riepilogo nascosta (con il pannello del contenitore di elementi impostato su una griglia) che mostra tutti gli elementi contemporaneamente ma con la loro visibilità impostata su nascosta.

Sarei lieto di sapere di idee migliori che non si basano su un orribile code-behind o sulla tua vista che deve capire che è necessario utilizzare un controllo diverso per fornire la larghezza per supportare le immagini (bleah!).


1
Questo approccio ridimensionerà la combinazione abbastanza ampia in modo che l'elemento più largo sia completamente visibile quando è l'elemento selezionato? Qui è dove ho riscontrato problemi.
jschroedl

8

Sulla base delle altre risposte sopra, ecco la mia versione:

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

HorizontalAlignment = "Left" interrompe i controlli utilizzando l'intera larghezza del controllo contenitore. Altezza = "0" nasconde il controllo degli elementi.
Margin = "15,0" consente l'aggiunta di chrome intorno agli elementi della casella combinata (non è indipendente da chrome, temo).


4

Ho trovato una soluzione "abbastanza buona" a questo problema che consisteva nel fare in modo che la casella combinata non si riducesse mai al di sotto della dimensione massima che conteneva, simile al vecchio WinForms AutoSizeMode = GrowOnly.

Il modo in cui l'ho fatto è stato con un convertitore di valori personalizzato:

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

Quindi configuro la casella combinata in XAML in questo modo:

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

Nota che con questo hai bisogno di un'istanza separata del GrowConverter per ogni casella combinata, a meno che, ovviamente, non desideri che un set di essi si ridimensiona insieme, in modo simile alla funzione SharedSizeScope di Grid.


1
Bello, ma solo “stabile” dopo aver selezionato l'ingresso più lungo.
primfaktor

1
Corretta. Avevo fatto qualcosa al riguardo in WinForms, dove avrei usato le API di testo per misurare tutte le stringhe nella casella combinata e impostare la larghezza minima per tenerne conto. Fare lo stesso è notevolmente più difficile in WPF, soprattutto quando i tuoi elementi non sono stringhe e / o provengono da un'associazione.
Cheetah

3

Un seguito alla risposta di Maleak: mi è piaciuta così tanto l'implementazione, ho scritto un vero comportamento per essa. Ovviamente avrai bisogno di Blend SDK in modo da poter fare riferimento a System.Windows.Interactivity.

XAML:

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

Codice:

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

Questo non funziona quando il ComboBox non è abilitato. provider.Expand()lancia un ElementNotEnabledException. Quando il ComboBox non è abilitato, a causa della disabilitazione di un genitore, non è nemmeno possibile abilitare temporaneamente il ComboBox fino al termine della misurazione.
FlyingFoX

1

Metti una casella di riepilogo contenente lo stesso contenuto dietro la casella personale. Quindi applica l'altezza corretta con alcuni legami come questo:

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

1

Nel mio caso un modo molto più semplice sembrava fare il trucco, ho appena usato uno stackPanel extra per avvolgere la casella combinata.

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

(lavorato in visual studio 2008)


1

Una soluzione alternativa alla risposta principale è misurare il popup stesso piuttosto che misurare tutti gli elementi. Dare SetWidthFromItems()un'implementazione leggermente più semplice :

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

funziona anche sui disabili ComboBox.


0

Stavo cercando la risposta io stesso, quando mi sono imbattuto nel UpdateLayout()metodo che ogniUIElement ha.

È molto semplice ora, per fortuna!

Basta chiamare ComboBox1.Updatelayout();dopo aver impostato o modificato il file ItemSource.


0

L'approccio di Alun Harford, in pratica:

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

0

Ciò mantiene la larghezza dell'elemento più largo ma solo dopo aver aperto la casella combinata una volta.

<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>
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.