Jak sprawić, aby pole kombi WPF miało szerokość najszerszego elementu w XAML?

103

Wiem, jak to zrobić w kodzie, ale czy można to zrobić w języku 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ő
źródło
Sprawdź inny post w podobnych wierszach pod adresem stackoverflow.com/questions/826985/ ... Oznacz swoje pytanie jako „z odpowiedzią”, jeśli to odpowiada na Twoje pytanie.
Sudeep
Próbowałem tego podejścia również w kodzie, ale stwierdziłem, że pomiary mogą się różnić między Vista i XP. W systemie Vista DesiredSize zwykle zawiera rozmiar strzałki rozwijanej, ale w XP często szerokość nie obejmuje strzałki rozwijania. Moje wyniki mogą wynikać z tego, że próbuję wykonać pomiar, zanim okno nadrzędne stanie się widoczne. Dodanie UpdateLayout () przed pomiarem może pomóc, ale może spowodować inne skutki uboczne w aplikacji. Byłbym zainteresowany rozwiązaniem, które wymyśliłeś, gdybyś chciał się nim podzielić.
jschroedl
Jak rozwiązałeś swój problem?
Andrew Kałasznikow

Odpowiedzi:

31

To nie może być w XAML bez:

  • Tworzenie ukrytej kontroli (odpowiedź Alana Hunforda)
  • Drastyczna zmiana ControlTemplate. Nawet w takim przypadku może być konieczne utworzenie ukrytej wersji ItemsPresenter.

Powodem tego jest to, że domyślne ComboBox ControlTemplates, z którymi się spotkałem (Aero, Luna itp.), Wszystkie zagnieżdżają ItemsPresenter w Popup. Oznacza to, że układ tych elementów jest odroczony, dopóki nie staną się widoczne.

Łatwym sposobem na przetestowanie tego jest zmodyfikowanie domyślnego ControlTemplate, aby powiązać MinWidth najbardziej zewnętrznego kontenera (jest to Siatka zarówno dla Aero, jak i Luny) z ActualWidth PART_Popup. Będziesz mógł automatycznie synchronizować ComboBox jego szerokość po kliknięciu przycisku upuść, ale nie wcześniej.

Więc chyba można wymusić działanie środka w systemie układ (którego można zrobić poprzez dodanie drugiego Control), nie sądzę, można to zrobić.

Jak zawsze jestem otwarty na krótkie, eleganckie rozwiązanie - ale w tym przypadku hacki związane z kodem lub podwójną kontrolą / ControlTemplate są jedynymi rozwiązaniami, jakie widziałem.

micahtan
źródło
57

Nie możesz tego zrobić bezpośrednio w Xaml, ale możesz użyć tego dołączonego zachowania. (Szerokość będzie widoczna w Projektancie)

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

Dołączone zachowanie 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);
    }
}

To, co robi, to wywołanie metody rozszerzającej dla ComboBox o nazwie SetWidthFromItems, która (niewidocznie) rozwija się i zwija, a następnie oblicza Width na podstawie wygenerowanych ComboBoxItems. (IExpandCollapseProvider wymaga odwołania do UIAutomationProvider.dll)

Następnie metoda rozszerzenia 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();
    }
}

Ta metoda rozszerzenia zapewnia również możliwość wywołania

comboBox.SetWidthFromItems();

w kodzie za (np. w zdarzeniu ComboBox.Loaded)

Fredrik Hedblad
źródło
+1, świetne rozwiązanie! Próbowałem zrobić coś podobnego, ale ostatecznie wykorzystałem twoją implementację (z kilkoma modyfikacjami)
Thomas Levesque
1
Niesamowite dzięki. Powinno to zostać oznaczone jako zaakceptowana odpowiedź. Wygląda na to, że dołączone nieruchomości są zawsze drogą do wszystkiego :)
Ignacio Soler Garcia
O ile mi wiadomo, najlepsze rozwiązanie. Wypróbowałem wiele sztuczek z całego Internetu, a Twoje rozwiązanie jest najlepsze i najłatwiejsze, jakie znalazłem. +1.
paercebal
7
Zwróć uwagę, że jeśli masz wiele komboboksów w tym samym oknie ( zdarzyło się to w przypadku okna tworzącego kombinacje i ich zawartość z kodem ), wyskakujące okienka mogą stać się widoczne przez sekundę. Wydaje mi się, że dzieje się tak dlatego, że wiele wiadomości typu „otwórz wyskakujące okienko” jest publikowanych przed wywołaniem jakiegokolwiek „zamknięcia wyskakującego okienka”. Rozwiązaniem jest SetWidthFromItemsasynchroniczne ustawienie całej metody przy użyciu akcji / delegata i BeginInvoke z priorytetem Idle (tak jak w przypadku Loaded). W ten sposób żaden pomiar nie zostanie wykonany, gdy pompa wiadomości nie jest pusta, a zatem nie nastąpi przeplatanie wiadomości
paercebal
1
Czy magiczna liczba: double comboBoxWidth = 19;w twoim kodzie jest związana z SystemParameters.VerticalScrollBarWidth?
Jf Beaulac
10

Tak, ten jest trochę paskudny.

W przeszłości dodałem do ControlTemplate ukrytą listę (z itemcontainerpanel ustawioną na siatkę) pokazującą każdy element w tym samym czasie, ale z ich widocznością ustawioną na ukrytą.

Z przyjemnością usłyszę o jakichkolwiek lepszych pomysłach, które nie opierają się na okropnym kodzie lub twoim spojrzeniu, które musi zrozumieć, że musi użyć innej kontrolki, aby zapewnić szerokość do obsługi wizualizacji (fuj!).

Alun Harford
źródło
1
Czy to podejście sprawi, że kombinacja będzie wystarczająco szeroka, aby najszerszy przedmiot był w pełni widoczny, gdy jest to wybrany przedmiot? Tutaj widziałem problemy.
jschroedl
8

Na podstawie innych odpowiedzi powyżej, oto moja wersja:

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

HorizontalAlignment = "Left" zatrzymuje kontrolki przy użyciu pełnej szerokości kontrolki zawierającej. Wysokość = „0” ukrywa kontrolę elementów.
Margines = "15,0" pozwala na dodanie dodatkowego chromu dookoła elementów z listy rozwijanej (niestety nie jest on niezależny od chromu).

Gaspode
źródło
4

Skończyło się na „wystarczająco dobrym” rozwiązaniu tego problemu polegającym na tym, że pole kombi nigdy nie zmniejszyło się poniżej największego posiadanego rozmiaru, podobnie jak w starym WinForms AutoSizeMode = GrowOnly.

Sposób, w jaki to zrobiłem, był z niestandardowym konwerterem wartości:

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

Następnie konfiguruję pole kombi w XAML w następujący sposób:

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

Zauważ, że w tym przypadku potrzebujesz oddzielnego wystąpienia GrowConvertera dla każdego pola kombi, chyba że chcesz, aby zestaw ich rozmiaru był razem, podobnie jak funkcja SharedSizeScope sieci Grid.

Gepard
źródło
1
Ładny, ale tylko „stabilny” po wybraniu najdłuższego wpisu.
primfaktor
1
Poprawny. Zrobiłem coś z tym w WinForms, gdzie użyłem tekstowych API do zmierzenia wszystkich ciągów w polu kombi i ustawiłem minimalną szerokość, aby to uwzględnić. To samo jest znacznie trudniejsze w WPF, zwłaszcza gdy elementy nie są ciągami znaków i / lub pochodzą z powiązania.
Cheetah,
3

Kontynuacja odpowiedzi Maleaka: tak bardzo podobało mi się to wdrożenie, napisałem dla niego rzeczywiste zachowanie. Oczywiście będziesz potrzebować Blend SDK, aby móc odwoływać się do System.Windows.Interactivity.

XAML:

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

Kod:

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
źródło
To nie działa, gdy ComboBox nie jest włączony. provider.Expand()rzuca ElementNotEnabledException. Gdy ComboBox nie jest włączony z powodu wyłączenia rodzica, nie jest nawet możliwe tymczasowe włączenie ComboBox do czasu zakończenia pomiaru.
FlyingFoX
1

Umieść listę zawierającą tę samą zawartość za skrzynką referencyjną. Następnie wymuś poprawną wysokość za pomocą takiego wiązania:

<Grid>
       <ListBox x:Name="listBox" Height="{Binding ElementName=dropBox, Path=DesiredSize.Height}" /> 
        <ComboBox x:Name="dropBox" />
</Grid>
Matze
źródło
1

W moim przypadku o wiele prostszy sposób wydawał się załatwić sprawę, po prostu użyłem dodatkowego panelu stosu do owinięcia combobox.

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

(pracował w Visual Studio 2008)

Nikos Tsokos
źródło
1

Alternatywnym rozwiązaniem dla pierwszej odpowiedzi jest zmierzenie samego wyskakującego okienka zamiast mierzenia wszystkich elementów. Dając nieco prostszą SetWidthFromItems()implementację:

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

działa również na niepełnosprawnych ComboBox.

Wondra
źródło
0

Sam szukałem odpowiedzi, kiedy natrafiłem na UpdateLayout()metodę, którą każdy UIElementma.

Na szczęście jest to teraz bardzo proste!

Po prostu zadzwoń ComboBox1.Updatelayout();po ustawieniu lub zmodyfikowaniu ItemSource.

Ciężarek u wędki
źródło
0

Podejście Aluna Harforda w praktyce:

<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
źródło
0

Zachowuje to szerokość do najszerszego elementu, ale tylko po jednokrotnym otwarciu pola kombi.

<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
źródło