web-dev-qa-db-fra.com

Comment créer une zone de liste déroulante WPF avec la largeur de l'é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;
        }
    }
}
87
Csupor Jenő

Cela ne peut pas être en XAML sans soit:

  • Créer un contrôle caché (réponse d'Alan Hunford)
  • Changer le ControlTemplate radicalement. Même dans ce cas, il peut être nécessaire de créer une version cachée d'un ItemsPresenter.

La raison en est que les ControlTemplates par défaut de ComboBox que j'ai rencontrés (Aero, Luna, etc.) imbriquent tous le ItemsPresenter dans un Popup. Cela signifie que la mise en page de ces éléments est différée jusqu'à ce qu'ils soient réellement rendus visibles.

Un moyen simple de tester cela consiste à modifier le ControlTemplate par défaut pour lier la MinWidth du conteneur le plus externe (c'est une grille pour Aero et Luna) à la valeur ActualWidth de PART_Popup. Vous pourrez faire en sorte que la ComboBox synchronise automatiquement sa largeur lorsque vous cliquez sur le bouton de dépôt, mais pas avant.

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

Comme toujours, je suis ouvert à une solution courte et élégante - mais dans ce cas, un hack code-behind ou dual-control/ControlTemplate est la seule solution que j'ai vue.

29
micahtan

Vous ne pouvez pas le faire directement dans 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>

ComboBoxWidthFromItemsProperty du comportement attaché

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 se développe et se rétracte (de manière invisible), puis calcule la largeur en fonction des objets ComboBoxItems générés. (IExpandCollapseProvider requiert 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 permet également d’appeler

comboBox.SetWidthFromItems();

dans le code derrière (par exemple, dans l'événement ComboBox.Loaded)

54
Fredrik Hedblad

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

Ce que j’ai fait par le passé est d’ajouter dans le ControlTemplate une zone de liste masquée (avec ses éléments contenant un conteneur défini sur une grille) affichant tous les éléments en même temps, mais leur visibilité étant définie sur masqué.

Je serais heureux d'entendre parler de meilleures idées qui ne s'appuient pas sur un code-behind horrible ou sur votre vision qui doit comprendre qu'il faut utiliser un contrôle différent pour fournir la largeur nécessaire à la prise en charge des images (beurk!).

10
Alun Harford

Basé sur les 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 qui le contient . Height = "0" masque le contrôle items.
Margin = "15,0" autorise l’ajout de chrome autour des éléments de la liste déroulante (je ne crains pas l’agnostie du chrome).

7
Gaspode

Je me suis retrouvé avec une "assez bonne" solution à ce problème: faire en sorte que la liste déroulante ne diminue jamais en dessous de la taille la plus grande qu'il contenait, comme dans l'ancien WinForms AutoSizeMode = GrowOnly.

La façon dont j'ai fait cela é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 liste déroulante en XAML comme ceci:

 <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 liste déroulante, à moins bien sûr que vous souhaitiez en redimensionner un ensemble, de la même manière que la fonctionnalité SharedSizeScope de la grille.

4
Cheetah

Suite de la réponse de Maleak: J'ai tellement aimé cette implémentation que j'ai écrit un comportement réel pour elle. De toute évidence, vous aurez besoin du SDK Blend pour pouvoir référencer System.Windows.Interactivity.

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

Placez une zone de liste contenant le même contenu derrière la liste déroulante. Ensuite, appliquez une hauteur correcte avec des liaisons comme celle-ci:

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

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

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

(a travaillé dans visual studio 2008)

1
Entrodus

Cela permet de conserver la largeur à l'élément le plus large, mais uniquement après avoir ouvert la 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
Wouter

Je cherchais moi-même la réponse lorsque 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é la ItemSource.

0
Sinker

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
Jan Van Overbeke