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

106

我知道如何在代码中完成它,但是否可以在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;
        }
    }
}

请查看类似主题的另一篇帖子,网址为https://dev59.com/g3RA5IYBdhLWcg3wyRF6。如果这个回答解决了你的问题,请将你的问题标记为“已解答”。 - Sudeep
我在代码中也尝试了这种方法,但发现在Vista和XP之间测量可能会有所不同。在Vista上,DesiredSize通常包括下拉箭头的大小,但在XP上,宽度通常不包括下拉箭头。现在,我的结果可能是因为我试图在父窗口可见之前进行测量。在测量之前添加UpdateLayout()可以帮助解决问题,但可能会导致应用程序出现其他副作用。如果您愿意分享,我很想看看您提出的解决方案。 - jschroedl
你是如何解决你的问题的? - Andrew Kalashnikov
13个回答

62

你无法直接在Xaml中完成,但是可以使用这个附加行为。(宽度将在设计器中可见)

<ComboBox behaviors:ComboBoxWidthFromItemsBehavior.ComboBoxWidthFromItems="True">
    <ComboBoxItem Content="Short"/>
    <ComboBoxItem Content="Medium Long"/>
    <ComboBoxItem Content="Min"/>
</ComboBox>
Attached Behavior 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);
    }
}

它所做的是调用名为SetWidthFromItems的ComboBox扩展方法,该方法(在不可见的情况下)展开和折叠自身,然后根据生成的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
太棒了,谢谢。这应该标记为被接受的答案。看起来附加属性总是万能的解决方案 :) - Ignacio Soler Garcia
7
请注意,如果您在同一窗口中有多个组合框(我使用代码后端创建了这些组合框及其内容的窗口),则弹出窗口可能会在短暂时间内变为可见。我想这是因为在调用任何“关闭弹出窗口”之前,多个“打开弹出窗口”消息被发布。解决方法是将整个方法SetWidthFromItems改为异步方法,使用一个动作/委托和带有空闲优先级的BeginInvoke(如在Loaded事件中所做)。这样,在消息队列不为空时不会执行任何度量操作,因此不会发生消息交错。 - paercebal
1
你的代码中的神奇数字 double comboBoxWidth = 19; 是否与 SystemParameters.VerticalScrollBarWidth 相关? - Jf Beaulac
1
如果您不打开下拉框,然后关闭控件的主机,事件处理程序将不会取消订阅,并导致内存泄漏。 - aydjay
1
provider.Expand() 方法会在控件本身或任何父控件被禁用时抛出 ElementNotEnabledException 异常,正如 FlyingFoX 在 Mike Post 的回答下所指出的。 - nvi9
显示剩余5条评论

34

如果不是通过以下两种方式之一,这将无法在XAML中实现:

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

原因是我遇到的所有默认ComboBox ControlTemplates(Aero、Luna等)都将ItemsPresenter嵌套在Popup中。这意味着这些项的布局要等到它们实际上可见时才进行。

测试的简单方法是修改默认的ControlTemplate,将最外层容器(对于Aero和Luna都是Grid)的MinWidth绑定到PART_Popup的ActualWidth上。当单击下拉按钮时,您将能够使ComboBox自动同步其宽度,但在此之前是不行的。

因此,除非您可以强制在布局系统中执行Measure操作(通过添加第二个控件您确实可以这样做),否则我认为不可能实现它。

总之,我始终欢迎短小精悍的解决方案——但在这种情况下,Code-behind或双控件/ControlTemplate的Hack是我看到的唯一解决方案。


13

基于上面的其他答案,这是我的版本:

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

HorizontalAlignment="Left"会使控件不使用包含控件的全部宽度。 Height="0"会隐藏项控件。
Margin="15,0"可以为组合框项目周围提供额外的装饰(遗憾的是,这并不是与chrome无关的)。


4
这绝对是最简单的答案。不得不这样做很荒谬,但它比其他解决方法少了一个数量级的复杂性和荒谬。为什么没有SizeToOptions属性,或者这不是控件的默认行为,我无法理解!WPF有时候完全疯狂。干得好,Gaspode! - Pseudonymous

11

嗯,这个有点棘手。

我过去做的是将一个隐藏的ListBox(其ItemsContainerPanel设置为Grid)添加到ControlTemplate中,同时显示所有项,但它们的可见性设置为隐藏。

如果有任何更好的想法,不需要依赖于可怕的Code-Behind或您的View需要理解它需要使用不同的控件来支持视觉效果的宽度(呕吐!),我会很高兴听到的。


1
这种方法会使组合框的宽度足够大,以便在选择项时最宽的项目完全可见吗?这就是我遇到问题的地方。 - jschroedl

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功能。

1
不错,但只有在选择最长的条目后才会“稳定”。 - primfaktor
1
正确。我在WinForms中已经做了一些关于这个问题的工作,我会使用文本API来测量组合框中的所有字符串,并设置最小宽度以解决此问题。在WPF中做同样的事情要困难得多,特别是当您的项目不是字符串和/或来自绑定时。 - Cheetah

3
Maleak的回答很好,我非常喜欢他的实现方式,因此我为此编写了一个行为。显然,您需要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

3
另一种解决顶部答案的方法是测量弹出窗口本身而不是测量所有项。以下是稍微简单一些的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;
    }
}

可以处理禁用的 ComboBox

这个解决方案无需简要显示Combobox,就可以测量其内容的大小,这是对原始顶部答案的良好改进。 - Mort
它可以工作,但是组合框占用的空间比内容需要的空间更多。与原始答案相比,其中emptySize是静态19,这里的emptySize计算值为27。 - Grigoriy
我相信是SystemParameters.VerticalScrollBarWidth添加了额外的宽度。有趣的是,无论垂直滚动条是否显示,都会添加这个宽度。 - Mike

2

在我的情况下,似乎有一种更简单的方法可以解决问题,我只是使用了一个额外的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中工作)


1
阿伦·哈福德的实践方法:

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

1
将一个包含相同内容的列表框放在下拉框后面。然后使用类似以下绑定的方式强制正确的高度:
<Grid>
       <ListBox x:Name="listBox" Height="{Binding ElementName=dropBox, Path=DesiredSize.Height}" /> 
        <ComboBox x:Name="dropBox" />
</Grid>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接