Как я могу создать поле со списком 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;
        }
    }
}

Ответ 1

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

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

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

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

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

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

Ответ 2

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

Ответ 3

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

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

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

Ответ 4

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

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

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

Ответ 5

Я закончил с "достаточно хорошим" решением этой проблемы, заключающейся в том, чтобы сделать поле со списком никогда не уменьшаться ниже самого большого размера, который он держал, подобно старым 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.

Ответ 6

Следуйте за ответом Малака: мне так понравилась эта реализация, я написал для нее фактическое поведение. Очевидно, вам понадобится 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();
        }
    }
}

Ответ 7

Вы можете привязать Width к любому контейнеру, который вы хотите.

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

Чтобы получить именно то, что вы пытаетесь сделать с С#, который вы написали, я бы посмотрел на включение IValueConverter или IMultiValueConverter.

Ответ 8

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

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

Ответ 9

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

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

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

Ответ 10

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

Это очень просто, к счастью!

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

Ответ 11

Как и для меня, решение для расширения ComboBox.Width для всей ширины столбца устанавливало ширину ColumnDefinition в "*" вместо "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>

Ответ 12

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

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

Ответ 13

Просто добавьте ширину в поле со списком

<ComboBox Name="ComboBox1" HorizontalAlignment="Left" VerticalAlignment="Top" Width="100">

Ответ 14

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

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