Як я можу зробити так, щоб комбінований блок 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 кардинально. Навіть у цьому випадку може знадобитися створити приховану версію ItemPresenter.

Причиною цього є те, що шаблони ComboBox ControlTemplates за замовчуванням, на які я натрапив (Aero, Luna, тощо), все гніздяться ItemsPresenter у спливаючому вікні. Це означає, що макет цих елементів відкладається, поки вони фактично не стануть видимими.

Простий спосіб перевірити це - змінити шаблон ControlTemplate за замовчуванням, щоб прив’язати MinWidth найвіддаленішого контейнера (це сітка для Aero і Luna) до фактичної ширини PART_Popup. Ви зможете автоматично синхронізувати ширину ComboBox, натиснувши кнопку випадання, але не раніше.

Тому, якщо ви не можете змусити операцію Measure в системі компонування (що можна зробити, додавши другий елемент управління), я не думаю, що це можна зробити.

Як завжди, я відкритий до короткого, елегантного рішення - але в цьому випадку хаки з кодом або з подвійним керуванням / ControlTemplate є єдиними рішеннями, які я бачив.


57

Ви не можете зробити це безпосередньо в Xaml, але ви можете використовувати цю додану поведінку. (Ширина буде видима в Дизайнері)

<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, чудове рішення! Я намагався зробити щось за тією ж схемою, але врешті-решт я застосував вашу реалізацію (з кількома модифікаціями)
Thomas Levesque

1
Дивовижна подяка Це слід позначити як прийняту відповідь. Схоже, що приєднані властивості завжди є способом до всього :)
Ігнасіо Солер Гарсія

Що стосується мене, найкраще рішення. Я спробував декілька хитрощів з усього Інтернету, і ваше рішення - найкращий і найпростіший, який я знайшов. +1.
paercebal

7
Зауважте, що якщо у вас є декілька комбінових скриньок в одному вікні ( це сталося зі мною з вікном, яке створює комбобокси та їх вміст із кодом ), спливаючі вікна можуть стати видимими на секунду. Я думаю, це тому, що кілька "відкритих спливаючих" повідомлень публікуються перед тим, як викликається будь-яке "закрите спливаюче вікно". Рішення для цього полягає в тому, щоб зробити весь метод SetWidthFromItemsасинхронним моїм, використовуючи дію / делегат та BeginInvoke з пріоритетом простою (як це робиться у випадку Loaded). Таким чином, жодних заходів не буде зроблено, поки насос безладу не буде порожнім, і, таким чином, не відбудеться
жодне перемежування

1
Чи пов'язане магічне число: double comboBoxWidth = 19;у вашому коді SystemParameters.VerticalScrollBarWidth?
Jf Beaulac

10

Так, це трохи неприємно.

Що я робив у минулому, це додати до ControlTemplate прихований список списку (з його елементами контейнерних панелей, встановлених у сітку), що показують кожен елемент одночасно, але їх видимість встановлена ​​на приховану.

Мені буде приємно почути будь-які кращі ідеї, які не покладаються на жахливий код, або ваш погляд, маючи на увазі, що йому потрібно використовувати інший елемент керування, щоб забезпечити ширину для підтримки візуальних зображень (yuck!).


1
Чи буде цей підхід розміром комбінації достатньо широким, щоб найширший елемент був повністю видимий, коли це вибраний елемент? Тут я бачив проблеми.
jschroedl

8

На основі інших відповідей вище, ось моя версія:

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

HorizontalAlignment = "Зліва" зупиняє елементи керування, використовуючи повну ширину елемента, що містить керування. Висота = "0" приховує контроль елементів.
Маржа = "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, особливо коли ваші предмети не є рядками та / або виходять із прив'язки.
Гепард

3

Слідом за відповіддю Маляка: мені так сподобалась реалізація, я написав для неї фактичну поведінку. Очевидно, що вам знадобиться SDK Blend, щоб ви могли посилатися на 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

Поставте список списку, що містить той самий вміст за списком. Потім застосуйте правильну висоту з таким прив’язкою, як ця:

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

(працював у візуальній студії 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;
    }
}

працює і для інвалідів ComboBoxes.


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>
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.