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?

103

Je sais comment le faire dans le 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;
        }
    }
}
Csupor Jenő
la source
Consultez un autre article sur les lignes similaires sur stackoverflow.com/questions/826985/ ... Veuillez marquer votre question comme "répondu" si cela répond à votre question.
Sudeep
J'ai également essayé cette approche dans le code, mais j'ai constaté que la mesure pouvait varier entre Vista et XP. Sur Vista, DesiredSize inclut généralement la taille de la flèche déroulante mais sur XP, souvent la largeur n'inclut pas la flèche déroulante. Maintenant, mes résultats peuvent être parce que j'essaie de faire la mesure avant que la fenêtre parent ne soit visible. L'ajout d'un UpdateLayout () avant la mesure peut aider, mais peut provoquer d'autres effets secondaires dans l'application. Je serais intéressé de voir la solution que vous proposez si vous êtes prêt à partager.
jschroedl
Comment avez-vous résolu votre problème?
Andrew Kalashnikov

Réponses:

31

Cela ne peut pas être en XAML sans:

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

La raison en est que les modèles de contrôle ComboBox par défaut que j'ai rencontrés (Aero, Luna, etc.) imbriquent tous le ItemsPresenter dans une fenêtre contextuelle. 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 ceci est de modifier le ControlTemplate par défaut pour lier la MinWidth du conteneur le plus extérieur (c'est une Grid pour Aero et Luna) à la ActualWidth de PART_Popup. Vous pourrez faire en sorte que la zone de liste déroulante 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 Mesure dans le système de mise en page (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, les hacks code-behind ou dual-control / ControlTemplate sont les seules solutions que j'ai vues.

micahtan
la source
57

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 de 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 (de manière invisible) se développe et se réduit, 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 permet également d'appeler

comboBox.SetWidthFromItems();

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

Fredrik Hedblad
la source
+1, excellente solution! J'essayais de faire quelque chose dans le même sens, mais j'ai finalement utilisé votre implémentation (avec quelques modifications)
Thomas Levesque
1
Un grand merci. Cela devrait être marqué comme la réponse acceptée. On dirait que les propriétés attachées sont toujours le chemin de tout :)
Ignacio Soler Garcia
Meilleure solution en ce qui me concerne. J'ai essayé plusieurs astuces de partout sur Internet, et votre solution est la meilleure et la plus simple que j'ai trouvée. +1.
paercebal
7
Notez que si vous avez plusieurs comboboxes dans la même fenêtre ( cela m'est arrivé avec une fenêtre créant les comboboxes et leur contenu avec code-behind ), les popups peuvent devenir visibles pendant une seconde. Je suppose que c'est parce que plusieurs messages "open popup" sont postés avant qu'un "close popup" ne soit appelé. La solution pour cela est de rendre toute la méthode SetWidthFromItemsasynchrone en utilisant une action / un délégué et un BeginInvoke avec une priorité Idle (comme cela est fait dans l'événement Loaded). De cette façon, aucune mesure ne sera effectuée tant que la pompe de messagerie n'est pas vide, et par conséquent, aucun entrelacement de message ne se produira
paercebal
1
Le nombre magique: double comboBoxWidth = 19;dans votre code est-il lié à SystemParameters.VerticalScrollBarWidth?
Jf Beaulac
10

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

Ce que j'ai fait dans le passé, c'est d'ajouter dans le ControlTemplate une zone de liste masquée (avec ses itemscontainerpanel définis sur une grille) montrant chaque élément en même temps mais avec leur visibilité définie sur hidden.

Je serais ravi d'entendre parler de meilleures idées qui ne reposent pas sur un horrible code-behind ou que votre vue doit comprendre qu'elle doit utiliser un contrôle différent pour fournir la largeur nécessaire pour prendre en charge les visuels (beurk!).

Alun Harford
la source
1
Cette approche dimensionnera-t-elle le combo suffisamment large pour que l'élément le plus large soit entièrement visible lorsqu'il s'agit de l'élément sélectionné? C'est là que j'ai vu des problèmes.
jschroedl
8

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 conteneur. Height = "0" masque le contrôle des éléments.
Margin = "15,0" permet d'ajouter du chrome autour des éléments de la boîte combo (pas agnostique du chrome, j'en ai peur).

Gaspode
la source
4

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écisse jamais en dessous de la plus grande taille qu'elle contenait, 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 du GrowConverter pour chaque zone de liste déroulante, à moins bien sûr que vous ne souhaitiez qu'un ensemble d'entre eux soit dimensionné ensemble, similaire à la fonctionnalité SharedSizeScope de la grille.

guépard
la source
1
Sympa, mais seulement «stable» après avoir sélectionné l'entrée la plus longue.
primfaktor
1
Correct. J'avais fait quelque chose à ce sujet dans WinForms, où j'utilisais les API de texte pour mesurer toutes les chaînes de la zone de liste déroulante et définirais la largeur minimale pour en tenir compte. Faire de même est considérablement plus difficile dans WPF, en particulier lorsque vos éléments ne sont pas des chaînes et / ou proviennent d'une liaison.
Cheetah
3

Une suite à la réponse de Maleak: j'ai tellement aimé cette implémentation que j'ai écrit un véritable Behavior pour cela. Évidemment, 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();
        }
    }
}
Mike Post
la source
Cela ne fonctionne pas lorsque le ComboBox n'est pas activé. provider.Expand()jette un ElementNotEnabledException. Lorsque le ComboBox n'est pas activé, en raison d'un parent étant désactivé, il n'est même pas possible d'activer temporairement le ComboBox jusqu'à ce que la mesure soit terminée.
FlyingFoX
1

Placez une liste contenant le même contenu derrière la boîte de dépôt. Ensuite, appliquez une hauteur correcte avec une liaison comme celle-ci:

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

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é en studio visuel 2008)

Nikos Tsokos
la source
1

Une solution alternative à la réponse principale consiste à mesurer le Popup lui-même plutôt que de mesurer tous les éléments. Donnant une SetWidthFromItems()mise en œuvre un peu plus 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;
    }
}

fonctionne également sur les handicapés ComboBox.

Wonderra
la source
0

Je cherchais moi-même la réponse, quand je suis tombé sur la UpdateLayout()méthode que chacun UIElementa.

C'est très simple maintenant, heureusement!

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

Plomb
la source
0

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>
Jan Van Overbeke
la source
0

Cela permet de conserver la largeur de 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>
Wouter
la source