如何在XAML中使WPF组合框具有其最宽元素的宽度?


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上,宽度通常不包括下拉箭头。现在,我的结果可能是因为我正在尝试在可见父窗口之前进行测量。在Measure之前添加UpdateLayout()可能会有所帮助,但可能会导致应用程序产生其他副作用。如果您愿意分享,我很想知道您提出的解决方案。
jschroedl

您如何解决您的问题?
安德鲁·卡拉什尼科夫

Answers:


31

不能在XAML中使用:

  • 创建一个隐藏的控件(Alan Hunford的答案)
  • 彻底更改ControlTemplate。即使在这种情况下,也可能需要创建ItemsPresenter的隐藏版本。

原因是我遇到的默认ComboBox ControlTemplates(Aero,Luna等)都将ItemsPresenter嵌套在弹出窗口中。这意味着将这些项目的布局推迟到实际可见之前。

一种简单的测试方法是修改默认的ControlTemplate以将最外面的容器(对于Aero和Luna都是网格)的MinWidth绑定到PART_Popup的ActualWidth。单击下拉按钮时,您将能够让ComboBox自动同步其宽度,但之前没有。

因此,除非您可以在布局系统中强制执行“测量”操作(您可以通过添加第二个控件执行此操作),否则我认为这是不可能完成的。

和往常一样,我欢迎一个简短而优雅的解决方案-但在这种情况下,我看到的仅有代码隐藏或双重控制/ ControlTemplate hacks的方法。


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计算Width。(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
太感谢了 这应该标记为已接受的答案。看起来附属物业始终是通往一切的道路:)
伊格纳西奥·索勒·加西亚

就我而言,最好的解决方案。我从Internet上尝试了多种技巧,而您的解决方案是我找到的最好,最简单的解决方案。+1。
paercebal 2012年

7
请注意,如果您在同一个窗口中有多个组合框(对于我来说,这是在一个窗口中创建了组合框及其后带有代码的内容),则弹出窗口可以显示一秒钟。我猜这是因为在调用任何“关闭弹出窗口”之前发布了多个“打开弹出窗口”消息。解决方案是SetWidthFromItems使用动作/委托和具有空闲优先级的BeginInvoke 使整个方法异步(如在Loaded事件中所做的那样)。这样,当消息泵不为空时将不会执行任何措施,因此不会发生消息交织
paercebal 2012年

1
魔术数字是否与double comboBoxWidth = 19;您的代码相关SystemParameters.VerticalScrollBarWidth
Jf Beaulac '16

10

是的,这有点令人讨厌。

我过去所做的就是将一个隐藏的列表框(其itemscontainerpanel设置为网格)添加到ControlTemplate中,该列表框同时显示每个项目,但其可见性设置为隐藏。

我很高兴听到任何不依赖于可怕代码背后的更好主意,或者您必须了解必须使用其他控件来提供宽度以支持视觉效果的观点(糟糕!)。


1
此方法的组合大小是否足够宽,以使最宽的项目在被选中时完全可见?这是我看到问题的地方。
jschroedl

8

根据上述其他答案,这是我的版本:

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

Horizo​​ntalAlignment =“ Left”使用包含控件的整个宽度停止控件。Height =“ 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实例,除非您当然希望将它们的集合一起调整大小,类似于Grid的SharedSizeScope功能。


1
不错,但是选择了最长的条目后才“稳定”。
primfaktor 2012年

1
正确。我在WinForms中做了一些有关此操作的事情,我将使用文本API来测量组合框中的所有字符串,并设置最小宽度以解决此问题。在WPF中执行此操作要困难得多,尤其是当您的项目不是字符串和/或来自绑定时。
猎豹

3

Maleak的答案的后续行动:我非常喜欢该实现,为此我写了一个实际的Behavior。显然,您将需要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

将包含相同内容的列表框放在下拉框后面。然后使用一些绑定强制正确的高度,如下所示:

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

也适用于禁用的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>
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.