Wie kann ich dafür sorgen, dass ein WPF-Kombinationsfeld die Breite seines breitesten Elements in XAML hat?

102

Ich weiß, wie es in Code gemacht wird, aber kann dies in XAML gemacht werden?

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ő
quelle
Schauen Sie sich einen anderen Beitrag in ähnlichen Zeilen unter stackoverflow.com/questions/826985/ an. Bitte markieren Sie Ihre Frage als "beantwortet", wenn dies Ihre Frage beantwortet.
Sudeep
Ich habe diesen Ansatz auch im Code ausprobiert, aber festgestellt, dass die Messung zwischen Vista und XP variieren kann. Unter Vista enthält DesiredSize normalerweise die Dropdown-Pfeilgröße, unter XP enthält die Breite häufig nicht den Dropdown-Pfeil. Meine Ergebnisse können nun darauf zurückzuführen sein, dass ich versuche, die Messung durchzuführen, bevor das übergeordnete Fenster sichtbar ist. Das Hinzufügen eines UpdateLayout () vor der Messung kann helfen, kann jedoch andere Nebenwirkungen in der App verursachen. Es würde mich interessieren, welche Lösung Sie finden, wenn Sie bereit sind, diese zu teilen.
Jschroedl
Wie haben Sie Ihr Problem gelöst?
Andrew Kalashnikov

Antworten:

30

Dies kann in XAML nicht sein ohne:

  • Erstellen einer versteckten Kontrolle (Alan Hunfords Antwort)
  • Das ControlTemplate drastisch ändern. Auch in diesem Fall muss möglicherweise eine versteckte Version eines ItemsPresenter erstellt werden.

Der Grund dafür ist, dass die Standard-ComboBox-ControlTemplates, auf die ich gestoßen bin (Aero, Luna usw.), den ItemsPresenter in einem Popup verschachteln. Dies bedeutet, dass das Layout dieser Elemente verschoben wird, bis sie tatsächlich sichtbar gemacht werden.

Eine einfache Möglichkeit, dies zu testen, besteht darin, die Standard-ControlTemplate so zu ändern, dass die Mindestbreite des äußersten Containers (es ist ein Raster für Aero und Luna) an die tatsächliche Breite von PART_Popup gebunden wird. Sie können die ComboBox automatisch ihre Breite synchronisieren lassen, wenn Sie auf die Drop-Schaltfläche klicken, jedoch nicht vorher.

Also , wenn Sie einen Measure Betrieb im Layout - System zwingen können (die Sie können durch Hinzufügen einer zweite Steuer tun), glaube ich nicht , es kann getan werden.

Wie immer bin ich offen für eine kurze, elegante Lösung - aber in diesem Fall sind Code-Behind- oder Dual-Control / ControlTemplate-Hacks die einzigen Lösungen, die ich gesehen habe.

Micahtan
quelle
57

Sie können dies nicht direkt in Xaml tun, aber Sie können dieses angehängte Verhalten verwenden. (Die Breite wird im Designer angezeigt.)

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

Das angehängte Verhalten 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);
    }
}

Es ruft eine Erweiterungsmethode für ComboBox namens SetWidthFromItems auf, die sich (unsichtbar) erweitert und reduziert und dann die Breite basierend auf den generierten ComboBoxItems berechnet. (IExpandCollapseProvider erfordert einen Verweis auf UIAutomationProvider.dll)

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

Diese Erweiterungsmethode bietet auch die Möglichkeit zum Aufrufen

comboBox.SetWidthFromItems();

im Code dahinter (zB im ComboBox.Loaded-Ereignis)

Fredrik Hedblad
quelle
+1, tolle Lösung! Ich habe versucht, etwas in die gleiche Richtung zu tun, aber schließlich habe ich Ihre Implementierung verwendet (mit ein paar Änderungen)
Thomas Levesque
1
Erstaunlicher Dank. Dies sollte als akzeptierte Antwort markiert werden. Sieht aus wie angehängte Eigenschaften sind immer der Weg zu allem :)
Ignacio Soler Garcia
Für mich die beste Lösung. Ich habe mehrere Tricks aus dem ganzen Internet ausprobiert, und Ihre Lösung ist die beste und einfachste, die ich gefunden habe. +1.
Paercebal
7
Beachten Sie , dass die Popups für eine Sekunde sichtbar werden können , wenn Sie mehrere Comboboxen im selben Fenster haben ( dies geschah bei einem Fenster, in dem die Comboboxen und deren Inhalt mit Code- Behind erstellt wurden). Ich denke, das liegt daran, dass mehrere "Open Popup" -Nachrichten gepostet werden, bevor ein "Close Popup" aufgerufen wird. Die Lösung hierfür besteht darin, die gesamte Methode SetWidthFromItemsmithilfe einer Aktion / eines Delegaten und eines BeginInvoke mit einer Leerlaufpriorität (wie im Ereignis Loaded) asynchron zu machen . Auf diese Weise wird keine Maßnahme durchgeführt, solange die Messge-Pumpe nicht leer ist, und daher tritt keine Nachrichtenverschachtelung auf
paercebal
1
Ist die magische Zahl: double comboBoxWidth = 19;in Ihrem Code verwandt mit SystemParameters.VerticalScrollBarWidth?
Jf Beaulac
10

Ja, dieser ist ein bisschen böse.

Was ich in der Vergangenheit getan habe, ist, der ControlTemplate ein verstecktes Listenfeld hinzuzufügen (dessen Elementcontainerpanel auf ein Raster eingestellt ist), in dem jedes Element gleichzeitig angezeigt wird, dessen Sichtbarkeit jedoch auf ausgeblendet eingestellt ist.

Ich würde mich freuen, von besseren Ideen zu hören, die nicht auf schrecklichem Code-Behind beruhen oder von Ihrer Ansicht, dass sie verstehen müssen, dass ein anderes Steuerelement verwendet werden muss, um die Breite zur Unterstützung der Grafik bereitzustellen (igitt!).

Alun Harford
quelle
1
Wird bei diesem Ansatz die Kombination so groß sein, dass das breiteste Element vollständig sichtbar ist, wenn es das ausgewählte Element ist? Hier habe ich Probleme gesehen.
Jschroedl
8

Basierend auf den anderen Antworten oben, hier ist meine Version:

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

HorizontalAlignment = "Left" stoppt die Steuerelemente mit der vollen Breite des enthaltenen Steuerelements. Höhe = "0" verbirgt die Gegenstandskontrolle.
Margin = "15,0" ermöglicht zusätzliches Chrom um Combo-Box-Elemente (leider nicht chromunabhängig).

Gaspode
quelle
4

Am Ende hatte ich eine "gute" Lösung für dieses Problem, bei der das Kombinationsfeld niemals unter die größte Größe schrumpfte, die es enthielt, ähnlich wie beim alten WinForms AutoSizeMode = GrowOnly.

Ich habe dies mit einem benutzerdefinierten Wertekonverter getan:

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

Dann konfiguriere ich das Kombinationsfeld in XAML wie folgt:

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

Beachten Sie, dass Sie für jedes Kombinationsfeld eine separate Instanz des GrowConverter benötigen, es sei denn, Sie möchten natürlich, dass eine Reihe von Instanzen zusammen dimensioniert wird, ähnlich wie bei der SharedSizeScope-Funktion des Grids.

Gepard
quelle
1
Schön, aber erst "stabil", nachdem der längste Eintrag ausgewählt wurde.
Primfaktor
1
Richtig. Ich hatte in WinForms etwas dagegen unternommen, wo ich die Text-APIs verwendete, um alle Zeichenfolgen im Kombinationsfeld zu messen und die minimale Breite festzulegen, um dies zu berücksichtigen. Dasselbe zu tun ist in WPF erheblich schwieriger, insbesondere wenn Ihre Artikel keine Zeichenfolgen sind und / oder aus einer Bindung stammen.
Cheetah
3

Eine Fortsetzung von Maleaks Antwort: Diese Implementierung hat mir so gut gefallen, dass ich ein tatsächliches Verhalten dafür geschrieben habe. Natürlich benötigen Sie das Blend SDK, damit Sie auf System.Windows.Interactivity verweisen können.

XAML:

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

Code:

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
quelle
Dies funktioniert nicht, wenn die ComboBox nicht aktiviert ist. provider.Expand()wirft eine ElementNotEnabledException. Wenn die ComboBox nicht aktiviert ist, weil ein Elternteil deaktiviert ist, kann die ComboBox erst nach Abschluss der Messung vorübergehend aktiviert werden.
FlyingFoX
1

Stellen Sie eine Listbox mit demselben Inhalt hinter die Dropbox. Erzwingen Sie dann die korrekte Höhe mit einer Bindung wie dieser:

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

In meinem Fall schien ein viel einfacherer Weg den Trick zu tun, ich habe nur ein zusätzliches stackPanel verwendet, um die Combobox zu verpacken.

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

(arbeitete im Visual Studio 2008)

Nikos Tsokos
quelle
1

Eine alternative Lösung zur Top-Antwort besteht darin, das Popup selbst zu messen, anstatt alle Elemente zu messen . Etwas einfachere SetWidthFromItems()Implementierung:

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

funktioniert auch bei Behinderten ComboBox.

wondra
quelle
0

Ich habe selbst nach der Antwort gesucht, als ich auf die UpdateLayout()Methode gestoßen bin, die jeder UIElementhat.

Zum Glück ist es jetzt sehr einfach!

Rufen ComboBox1.Updatelayout();Sie einfach an, nachdem Sie das eingestellt oder geändert haben ItemSource.

Sinker
quelle
0

Alun Harfords Ansatz in der Praxis:

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

Dadurch bleibt die Breite auf dem breitesten Element, jedoch nur nach einmaligem Öffnen des Kombinationsfelds.

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