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

103

Я знаю, как это сделать в коде, но можно ли это сделать в 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;
        }
    }
}
Джено Чупор
источник
Ознакомьтесь с другим сообщением в аналогичных строках на stackoverflow.com/questions/826985/… Пожалуйста, отметьте свой вопрос как «отвеченный», если он отвечает на ваш вопрос.
Sudeep
Я также пробовал этот подход в коде, но обнаружил, что измерения могут различаться в Vista и XP. В Vista DesiredSize обычно включает размер стрелки раскрывающегося списка, но в XP часто ширина не включает стрелку раскрывающегося списка. Теперь мои результаты могут быть связаны с тем, что я пытаюсь провести измерение до того, как будет видно родительское окно. Добавление UpdateLayout () перед измерением может помочь, но может вызвать другие побочные эффекты в приложении. Мне было бы интересно увидеть решение, которое вы придумаете, если вы готовы поделиться.
jschroedl
Как вы решили свою проблему?
Андрей Калашников

Ответы:

31

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

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

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

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

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

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

Micahtan
источник
57

Вы не можете сделать это напрямую в 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)

Фредрик Хедблад
источник
+1, отличное решение! Я пытался сделать что-то в том же духе, но в итоге я использовал вашу реализацию (с небольшими изменениями)
Томас Левеск
1
Прекрасное спасибо. Это должно быть отмечено как принятый ответ. Похоже, что присоединенная собственность - всегда путь ко всему :)
Игнасио Солер Гарсия,
Насколько я понимаю, лучшее решение. Я испробовал несколько уловок со всего Интернета, и ваше решение - лучшее и самое простое, что я нашел. +1.
paercebal
7
Обратите внимание, что если у вас есть несколько полей со списком в одном окне ( это случилось со мной с окном, в котором создаются поля со списком и их содержимое с выделенным кодом ), всплывающие окна могут стать видимыми на секунду. Я предполагаю, что это связано с тем, что несколько сообщений «открыть всплывающее окно» отправляются до вызова любого «всплывающего окна закрытия». Решение для этого - сделать весь метод SetWidthFromItemsасинхронным, используя действие / делегат и BeginInvoke с приоритетом Idle (как это сделано в событии Loaded). Таким образом, никакие измерения не будут выполняться, пока насос сообщений не пуст, и, следовательно, не будет чередования сообщений
paercebal
1
Связано ли магическое число: double comboBoxWidth = 19;в вашем коде с SystemParameters.VerticalScrollBarWidth?
Jf Beaulac
10

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

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

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

Алан Харфорд
источник
1
Будет ли это подходить к размеру комбо достаточно широким, чтобы самый широкий элемент был полностью виден, когда это выбранный элемент? Вот где я видел проблемы.
jschroedl 09
8

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

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

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

Гаспод
источник
4

В итоге я нашел «достаточно хорошее» решение этой проблемы: сделать так, чтобы поле со списком никогда не сжималось ниже максимального размера, который он имел, аналогично старому 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 для каждого поля со списком, если, конечно, вы не хотите, чтобы их набор совпадал по размеру, аналогично функции SharedSizeScope Grid.

Гепард
источник
1
Красиво, но только «стабильно» после выбора самой длинной записи.
primfaktor
1
Верный. Я что-то сделал по этому поводу в WinForms, где я бы использовал текстовые API для измерения всех строк в поле со списком и установил минимальную ширину, чтобы учесть это. В WPF сделать то же самое значительно сложнее, особенно если ваши элементы не являются строками и / или поступают из привязки.
Cheetah
3

Продолжение ответа Малеака: мне так понравилась эта реализация, я написал для нее настоящее поведение. Очевидно, вам понадобится 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();
        }
    }
}
Майк Пост
источник
Это не работает, если ComboBox не включен. provider.Expand()бросает ElementNotEnabledException. Когда ComboBox не включен из-за того, что родитель отключен, тогда невозможно даже временно включить ComboBox до завершения измерения.
FlyingFoX
1

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

<Grid>
       <ListBox x:Name="listBox" Height="{Binding ElementName=dropBox, Path=DesiredSize.Height}" /> 
        <ComboBox x:Name="dropBox" />
</Grid>
Маце
источник
1

В моем случае, казалось, более простой способ сработал, я просто использовал дополнительный 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)

Никос Цокос
источник
1

Альтернативным решением основного ответа является измерение самого всплывающего окна , а не измерения всех элементов. Даем немного более простую 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;
    }
}

работает и с инвалидами ComboBox.

Wondra
источник
0

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

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

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

Грузило
источник
0

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

<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

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

<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>
Воутер
источник