Comment puis-je faire en sorte qu'une zone de liste déroulante WPF ait la largeur de son élément le plus large en XAML?

Je sais comment le faire en code, mais cela peut-il être fait 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;
        }
    }
}
86
demandé sur Dave Clemmer 2009-06-23 23:02:39

14 réponses

Cela ne peut pas être en XAML Sans:

  • Création d'un contrôle caché (réponse D'Alan Hunford)
  • changer radicalement le ControlTemplate. Même dans ce cas, une version cachée D'un ItemsPresenter peut devoir être créée.

La raison en est que le ComboBox ControlTemplates par défaut que j'ai rencontré (Aero, Luna, etc.) tous imbriquent les ItemsPresenter dans une fenêtre contextuelle. Cela signifie que la mise en page de ces éléments est reportée jusqu'à ce qu'ils soient réellement faits visible.

Un moyen facile de tester ceci est de modifier le ControlTemplate par défaut pour lier la MinWidth du conteneur le plus externe (C'est une grille pour Aero et Luna) à la ActualWidth de PART_Popup. Vous serez en mesure d'avoir la liste déroulante synchroniser automatiquement sa largeur lorsque vous cliquez sur le bouton drop, mais pas avant.

Donc, à moins que vous ne puissiez forcer une opération de mesure dans le système de mise en page (que vous pouvez faire en ajoutant un deuxième contrôle), Je ne pense pas que cela puisse être faire.

Comme toujours, je suis ouvert à une solution courte et élégante-mais dans ce cas, un code-behind ou des hacks à double contrôle/ControlTemplate sont les seules solutions que j'ai vues.

29
répondu micahtan 2009-06-24 16:02:24

Vous ne pouvez pas le faire directement en Xaml mais vous pouvez utiliser ce comportement attaché. (La largeur sera visible dans le concepteur)

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

Le Comportement Attaché 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);
    }
}

Ce qu'il fait, c'est qu'il appelle une méthode d'extension pour ComboBox appelée SetWidthFromItems qui (invisiblement) se développe et s'effondre, puis calcule la largeur en fonction des ComboBoxItems générés. (IExpandCollapseProvider nécessite une référence à UIAutomationProvider.dll)

Puis méthode D'extension 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();
    }
}

Cette méthode d'extension fournit également la possibilité d'appeler

comboBox.SetWidthFromItems();

Dans le code derrière (par exemple dans la liste déroulante.Événement chargé)

51
répondu Fredrik Hedblad 2010-12-12 02:19:31

Ouais, celui-ci est un peu méchant.

Ce que j'ai fait dans le passé est d'ajouter dans ControlTemplate une listbox cachée (avec son itemscontainerpanel défini sur une grille) montrant chaque élément en même temps mais avec leur visibilité définie sur hidden.

Je serais heureux d'entendre de meilleures idées qui ne reposent pas sur un code horrible ou votre vue devant comprendre qu'elle doit utiliser un contrôle différent pour fournir la largeur pour supporter les visuels (Beurk!).

10
répondu Alun Harford 2009-06-23 21:47:29

Sur la Base des autres réponses ci-dessus, voici ma version:

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

HorizontalAlignment = "Left" arrête les contrôles en utilisant toute la largeur du contrôle contenant. Height = " 0 " masque le contrôle items.
Margin= "15,0" permet de chrome supplémentaire autour des éléments combo-box (pas chrome agnostique je crains).

6
répondu Gaspode 2011-11-09 16:50:32

Je me suis retrouvé avec une solution "assez bonne" à ce problème étant de faire en sorte que la zone de liste déroulante ne rétrécit jamais en dessous de la plus grande taille qu'elle détenait, similaire à L'ancien WinForms AutoSizeMode=GrowOnly.

La façon dont je l'ai fait était avec un convertisseur de valeur personnalisé:

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

Ensuite, je configure la zone de liste déroulante en XAML comme suit:

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

Notez qu'avec cela, vous avez besoin d'une instance distincte de GrowConverter pour chaque zone de liste déroulante, sauf si vous voulez bien sûr qu'un ensemble d'entre eux soit dimensionné ensemble, similaire à la fonction SharedSizeScope de la grille.

4
répondu Cheetah 2010-07-07 23:27:07

Un suivi de la réponse de Maleak: j'ai tellement aimé cette implémentation, j'ai écrit un comportement réel pour cela. De toute évidence, vous aurez besoin du SDK Blend afin que vous puissiez référencer le système.Windows.Interactivité.

XAML:

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

Code:

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();
        }
    }
}
2
répondu Mike Post 2012-03-23 03:54:52

, Vous pouvez lier la Largeur de n'importe quel conteneur que vous souhaitez.

<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" x:Name="Window1">
<Grid>
    <ComboBox 
       Name="ComboBox1"
       HorizontalAlignment="Left"
       VerticalAlignment="Top">
       <ComboBox.Width>
          <Binding ElementName="Window1" Path="ActualWidth"/>
       </ComboBox.Width>
          <ComboBoxItem>ComboBoxItem1</ComboBoxItem>
          <ComboBoxItem>ComboBoxItem2</ComboBoxItem>
    </ComboBox>
</Grid>

Pour obtenir exactement ce que vous essayez de faire avec le C # que vous avez écrit, je regarderais impmenter un IValueConverter ou IMultiValueConverter.

0
répondu Aaron 2009-06-24 14:49:18

Placez une listbox contenant le même contenu derrière la dropbox. Ensuite, appliquez la hauteur correcte avec une liaison comme ceci:

<Grid>
       <ListBox x:Name="listBox" Height="{Binding ElementName=dropBox, Path=DesiredSize.Height}" /> 
        <ComboBox x:Name="dropBox" />
</Grid>
0
répondu Matze 2010-04-09 08:08:57

Dans mon cas, un moyen beaucoup plus simple semblait faire l'affaire, Je viens d'utiliser un stackPanel supplémentaire pour envelopper la combobox.

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

(travaillé dans visual studio 2008)

0
répondu Entrodus 2011-07-13 13:07:34

Je cherchais moi-même la réponse, Quand je suis tombé sur la méthode UpdateLayout() que chaque UIElement a.

, C'est très simple maintenant, heureusement!

Appelez simplement ComboBox1.Updatelayout(); Après avoir défini ou modifié le ItemSource.

0
répondu Sinker 2014-06-13 04:26:41

Quant à moi, la solution pour développer ComboBox.Largeur de la colonne entière, définissait la largeur ColumnDefinition sur " * "au lieu de "Auto":

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

    <Label Content="List of items"
      Grid.Column="0" Margin="3" />

    <ComboBox
        Grid.Column="1" 
        ItemsSource="{Binding Path=DestinationSubDivisions}"
        SelectedValue="{Binding Path=TransferRequest.DestinationSubDivision}"
        DisplayMemberPath="Name"
        Margin="3" />
</Grid>
0
répondu Fragment 2014-10-13 10:08:40

L'approche D'Alun Harford, en pratique:

<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
répondu Jan Van Overbeke 2017-07-26 09:39:50

Il suffit d'ajouter une largeur à la zone de liste déroulante

<ComboBox Name="ComboBox1" HorizontalAlignment="Left" VerticalAlignment="Top" Width="100">
0
répondu Samidjo 2017-08-02 21:05:14

Cela maintient la largeur à l'élément le plus large, mais seulement après avoir ouvert la zone de liste déroulante une fois.

<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>
0
répondu Wouter 2017-10-26 22:53:45