Как сделать так, чтобы поле со списком WPF имело ширину самого широкого элемента в XAML?

Я знаю, как это сделать в коде, но можно ли это сделать в 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;
        }
    }
}

person Jeno Csupor    schedule 23.06.2009    source источник
comment
Ознакомьтесь с другим сообщением в аналогичных строках по адресу stackoverflow.com/questions/826985/ Отметьте свой вопрос как ответ, если он отвечает на ваш вопрос.   -  person Sudeep    schedule 23.06.2009
comment
Я также пробовал этот подход в коде, но обнаружил, что измерения могут варьироваться в Vista и XP. В Vista DesiredSize обычно включает размер стрелки раскрывающегося списка, но в XP часто ширина не включает стрелку раскрывающегося списка. Теперь мои результаты могут быть связаны с тем, что я пытаюсь провести измерение до того, как станет видимым родительское окно. Добавление UpdateLayout () перед измерением может помочь, но может вызвать другие побочные эффекты в приложении. Мне было бы интересно увидеть решение, которое вы придумали, если вы готовы поделиться.   -  person jschroedl    schedule 09.08.2009
comment
Как вы решили свою проблему?   -  person Andrew Kalashnikov    schedule 25.10.2010


Ответы (13)


Этого не может быть в XAML без:

  • Создание скрытого элемента управления (ответ Алана Ханфорда)
  • Резкое изменение ControlTemplate. Даже в этом случае может потребоваться создание скрытой версии ItemsPresenter.

Причина этого в том, что стандартные шаблоны ComboBox ControlTemplates, с которыми я сталкивался (Aero, Luna и т. Д.), Все вкладывают ItemsPresenter во всплывающее окно. Это означает, что макет этих элементов откладывается до тех пор, пока они не станут видимыми.

Простой способ проверить это - изменить ControlTemplate по умолчанию, чтобы привязать MinWidth самого внешнего контейнера (это Grid для Aero и Luna) к ActualWidth PART_Popup. Вы сможете настроить автоматическую синхронизацию ширины ComboBox при нажатии кнопки перетаскивания, но не раньше.

Поэтому, если вы не можете принудительно выполнить операцию измерения в системе макета (что вы можете сделать, добавив второй элемент управления), я не думаю, что это можно сделать.

Как всегда, я открыт для короткого, элегантного решения, но в данном случае хаки кода программной части или двойное управление / ControlTemplate - единственные решения, которые я видел.

person micahtan    schedule 24.06.2009

Вы не можете сделать это прямо в Xaml, но можете использовать это Attached Behavior. (Ширина будет видна в Дизайнере)

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

Прикрепленное поведение 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);
    }
}

Что он делает, так это то, что он вызывает метод расширения для ComboBox, называемый SetWidthFromItems, который (невидимо) расширяется и сворачивается, а затем вычисляет ширину на основе сгенерированных ComboBoxItems. (IExpandCollapseProvider требует ссылки на UIAutomationProvider.dll)

Затем метод расширения 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();
    }
}

Этот метод расширения также обеспечивает возможность вызова

comboBox.SetWidthFromItems();

в коде позади (например, в событии ComboBox.Loaded)

person Fredrik Hedblad    schedule 11.12.2010
comment
+1, отличное решение! Я пытался сделать что-то в том же духе, но в итоге я использовал вашу реализацию (с небольшими изменениями) - person Thomas Levesque; 17.05.2011
comment
Прекрасное спасибо. Это должно быть отмечено как принятый ответ. Похоже, что прикрепленные свойства - это всегда путь ко всему :) - person Ignacio Soler Garcia; 13.03.2012
comment
Насколько я понимаю, лучшее решение. Я испробовал несколько уловок со всего Интернета, и ваше решение - лучшее и самое простое, что я нашел. +1. - person paercebal; 14.06.2012
comment
Обратите внимание, что если у вас есть несколько полей со списком в одном окне (это случилось со мной с окном, в котором создаются поля со списком и их содержимое с выделенным кодом), всплывающие окна могут стать видимыми на секунду. Я предполагаю, что это связано с тем, что несколько открытых всплывающих сообщений публикуются до вызова любого закрытого всплывающего окна. Решение для этого - сделать весь метод SetWidthFromItems асинхронным, используя действие / делегат и BeginInvoke с приоритетом Idle (как это сделано в событии Loaded). Таким образом, никакие меры не будут выполняться, пока насос сообщений не пуст, и, следовательно, не будет происходить чередование сообщений. - person paercebal; 18.06.2012
comment
Связано ли магическое число: double comboBoxWidth = 19; в вашем коде с SystemParameters.VerticalScrollBarWidth? - person Jf Beaulac; 30.11.2016
comment
Используя Visual Studio 2015, это решение работает во время выполнения, но не во время разработки. (я имею в виду, что не работает, размер не соответствует содержимому - будь то жестко запрограммированные элементы XAML или элементы с привязкой к данным) и вызывает некоторые небольшие сбои в самом конструкторе - person BCA; 11.01.2017
comment
Это не адаптируется к масштабированию текста на дисплее win10 + 4K. он переходит в цикл foreach (var item в comboBox.Items), но ContainerFromItem возвращает null - person char m; 17.08.2017
comment
Если вы не откроете поле со списком, а затем закроете хост элемента управления, обработчики событий не откажутся от подписки, и это приведет к утечкам памяти. - person d347hm4n; 22.02.2019
comment
Метод provider.Expand() выдает ElementNotEnabledException, когда сам элемент управления или любой родительский элемент управления отключен, как указано в FlyingFoX под ответом Майка Поста. - person nvi9; 02.09.2019

Да, это немного неприятно.

Раньше я добавлял в ControlTemplate скрытый список (с его itemscontainerpanel, установленным в сетку), показывающий все элементы одновременно, но с их скрытой видимостью.

Я был бы рад услышать о любых лучших идеях, которые не полагаются на ужасный код программной части или ваше представление, которое должно понимать, что ему нужно использовать другой элемент управления, чтобы обеспечить ширину для поддержки визуальных эффектов (фу!).

person Alun Harford    schedule 23.06.2009
comment
Будет ли это подходить к размеру комбо достаточно широким, чтобы самый широкий элемент был полностью виден, когда это выбранный элемент? Вот где я видел проблемы. - person jschroedl; 09.08.2009

Основываясь на других ответах выше, вот моя версия:

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

HorizontalAlignment = "Left" останавливает элементы управления, используя полную ширину содержащего элемента управления. Высота = "0" скрывает элемент управления элементами.
Margin = "15,0" позволяет добавить дополнительный хром вокруг элементов поля со списком (боюсь, что это не зависит от хрома).

person Gaspode    schedule 09.11.2011
comment
Это, безусловно, самый простой ответ. Это нелепо, но это на порядок менее сложно и нелепо, чем другие обходные пути. Я не понимаю, почему нет свойства SizeToOptions или это не просто поведение элемента управления по умолчанию! WPF иногда бывает совершенно ментальным. Хороший, Гаспод! - person Pseudonymous; 28.06.2021

В итоге я нашел «достаточно хорошее» решение этой проблемы: сделать так, чтобы поле со списком никогда не сжималось ниже максимального размера, который он имел, аналогично старому WinForms AutoSizeMode = GrowOnly.

Я сделал это с помощью специального преобразователя значений:

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

Затем я настраиваю поле со списком в XAML следующим образом:

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

Обратите внимание, что для этого вам понадобится отдельный экземпляр GrowConverter для каждого поля со списком, если, конечно, вы не хотите, чтобы их набор совпадал по размеру, аналогично функции Grid SharedSizeScope.

person Cheetah    schedule 07.07.2010
comment
Красиво, но только «стабильно» после выбора самой длинной записи. - person primfaktor; 29.06.2012
comment
Правильный. Я что-то сделал по этому поводу в WinForms, где я использовал бы текстовые API для измерения всех строк в поле со списком и установил минимальную ширину, чтобы учесть это. В WPF сделать то же самое значительно сложнее, особенно если ваши элементы не являются строками и / или поступают из привязки. - person Cheetah; 21.07.2012

Продолжение ответа Малеака: мне так понравилась эта реализация, я написал для нее настоящее поведение. Очевидно, вам понадобится Blend SDK, чтобы вы могли ссылаться на System.Windows.Interactivity.

XAML:

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

Код:

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();
        }
    }
}
person Mike Post    schedule 23.03.2012
comment
Это не работает, если ComboBox не включен. provider.Expand() бросает ElementNotEnabledException. Когда ComboBox не включен из-за того, что родитель отключен, то невозможно даже временно включить ComboBox до завершения измерения. - person FlyingFoX; 25.10.2018

Альтернативным решением основного ответа является Измерьте само всплывающее окно, а не все элементы. Даем чуть более простую SetWidthFromItems() реализацию:

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

работает и с отключенными ComboBoxes.

person wondra    schedule 11.12.2019
comment
Это решение не должно вкратце отображать поле со списком, чтобы измерить размер его содержимого, что является хорошим улучшением по сравнению с исходным верхним ответом. - person Mort; 02.03.2021
comment
Он работает, но поле со списком занимает больше места, чем требуется для содержимого. По сравнению с исходным ответом, где emptySize является статическим 19, здесь emptySize вычисленное значение 27. - person Grigoriy; 03.06.2021

Поместите список, содержащий то же содержимое, за Dropbox. Затем установите правильную высоту с помощью такой привязки:

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

В моем случае более простой способ сработал, я просто использовал дополнительный stackPanel, чтобы обернуть поле со списком.

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

(работал в visual studio 2008)

person Nikos Tsokos    schedule 13.07.2011

Я хотел, чтобы он изменял размер только до максимального элемента, пока раскрывающийся список открыт, и в противном случае соответствовал выбранному значению. Вот код для этого:

Частично на основе ответа Фредерика (который на самом деле не сработал для меня)

public static class ComboBoxAutoWidthBehavior {
    public static readonly DependencyProperty ComboBoxAutoWidthProperty =
            DependencyProperty.RegisterAttached(
                "ComboBoxAutoWidth",
                typeof(bool),
                typeof(ComboBoxAutoWidthBehavior),
                new UIPropertyMetadata(false, OnComboBoxAutoWidthPropertyChanged)
            );

    public static bool GetComboBoxAutoWidth(DependencyObject obj) {
        return (bool) obj.GetValue(ComboBoxAutoWidthProperty);
    }

    public static void SetComboBoxAutoWidth(DependencyObject obj, bool value) {
        obj.SetValue(ComboBoxAutoWidthProperty, value);
    }

    private static void OnComboBoxAutoWidthPropertyChanged(DependencyObject dpo, DependencyPropertyChangedEventArgs e) {
        if(dpo is ComboBox comboBox) {
            if((bool) e.NewValue) {
                comboBox.Loaded += OnComboBoxLoaded;
                comboBox.DropDownOpened += OnComboBoxOpened;
                comboBox.DropDownClosed += OnComboBoxClosed;
            } else {
                comboBox.Loaded -= OnComboBoxLoaded;
                comboBox.DropDownOpened -= OnComboBoxOpened;
                comboBox.DropDownClosed -= OnComboBoxClosed;
            }
        }
    }

    private static void OnComboBoxLoaded(object sender, EventArgs eventArgs) {
        ComboBox comboBox = (ComboBox) sender;
        comboBox.SetMaxWidthFromItems();
    }

    private static void OnComboBoxOpened(object sender, EventArgs eventArgs) {
        ComboBox comboBox = (ComboBox) sender;
        comboBox.Width = comboBox.MaxWidth;
    }

    private static void OnComboBoxClosed(object sender, EventArgs eventArgs) => ((ComboBox) sender).Width = double.NaN;
}

public static class ComboBoxExtensionMethods {
    public static void SetMaxWidthFromItems(this ComboBox combo) {
        double idealWidth = combo.MinWidth;
        string longestItem = combo.Items.Cast<object>().Select(x => x.ToString()).Max(x => (x?.Length, x)).x;
        if(longestItem != null && longestItem.Length >= 0) {
            string tmpTxt = combo.Text;
            combo.Text = longestItem;
            Thickness tmpMarg = combo.Margin;
            combo.Margin = new Thickness(0);
            combo.UpdateLayout();

            combo.Width = double.NaN;
            combo.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));

            idealWidth = Math.Max(idealWidth, combo.DesiredSize.Width);

            combo.Text = tmpTxt;
            combo.Margin = tmpMarg;
        }

        combo.MaxWidth = idealWidth;
    }
}

И вы включаете это так:

<ComboBox behaviours:ComboBoxAutoWidthBehavior.ComboBoxAutoWidth="True" />

Вы также можете просто установить ширину напрямую вместо MaxWidth, а затем удалить части DropDownOpened и Closed, если вы хотите, чтобы они вели себя как другие anwsers.

person Ali Rahman    schedule 29.11.2020

Я сам искал ответ, когда наткнулся на метод UpdateLayout(), который есть в каждом UIElement.

К счастью, теперь это очень просто!

Просто вызовите ComboBox1.Updatelayout(); после установки или изменения ItemSource.

person Sinker    schedule 13.06.2014

Подход Алуна Харфорда на практике:

<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>
person Jan Van Overbeke    schedule 26.07.2017

Это сохраняет ширину самого широкого элемента, но только после открытия поля со списком один раз.

<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>
person Wouter    schedule 26.10.2017