【问题标题】:How can I make a WPF combo box have the width of its widest element in XAML?如何使 WPF 组合框在 XAML 中具有其最宽元素的宽度?
【发布时间】:2009-06-23 19:02:39
【问题描述】:

我知道如何在代码中做到这一点,但这可以在 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/… 上类似行的另一篇帖子如果回答了您的问题,请将您的问题标记为“已回答”。
  • 我也在代码中尝试过这种方法,但发现在 Vista 和 XP 之间测量值可能会有所不同。在 Vista 上,DesiredSize 通常包括下拉箭头的大小,但在 XP 上,宽度通常不包括下拉箭头。现在,我的结果可能是因为我试图在父窗口可见之前进行测量。在 Measure 之前添加 UpdateLayout() 会有所帮助,但可能会导致应用程序中的其他副作用。如果您愿意分享,我很想看看您提出的解决方案。
  • 您是如何解决问题的?

标签: c# wpf combobox


【解决方案1】:

您不能直接在 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);
    }
}

它的作用是调用一个名为 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,很好的解决方案!我试图做同样的事情,但最终我使用了你的实现(做了一些修改)
  • 非常感谢。这应该被标记为接受的答案。看起来附加属性总是通往一切的道路:)
  • 请注意,如果您在同一个窗口中有多个组合框(它发生在我的窗口中,使用代码隐藏创建组合框及其内容),弹出窗口可能变成可见一秒钟。我想这是因为在调用任何“关闭弹出窗口”之前发布了多个“打开弹出窗口”消息。解决方案是使整个方法 SetWidthFromItems 异步我使用操作/委托和具有空闲优先级的 BeginInvoke(如在 Loaded 事件中所做的那样)。这样在messge pump不为空的时候不做任何措施,也就不会发生message interleaving
  • 您的代码中的幻数:double comboBoxWidth = 19; 是否与SystemParameters.VerticalScrollBarWidth 相关?
  • 如果你不打开组合框然后关闭控件的宿主,事件处理程序不会取消订阅,它会导致内存泄漏。
【解决方案2】:

这不能在 XAML 中没有:

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

原因是我遇到的默认 ComboBox ControlTemplates(Aero、Luna 等)都将 ItemsPresenter 嵌套在 Popup 中。这意味着这些项目的布局会延迟到它们真正可见。

一个简单的测试方法是修改默认 ControlTemplate 以将最外面的容器(它是 Aero 和 Luna 的 Grid)的 MinWidth 绑定到 PART_Popup 的 ActualWidth。当您单击下拉按钮时,您将能够让 ComboBox 自动同步其宽度,但之前不会。

因此,除非您可以在布局系统中强制执行 Measure 操作(您可以通过添加第二个控件来做到这一点),否则我认为它无法做到。

与往常一样,我愿意接受一个简短而优雅的解决方案——但在这种情况下,代码隐藏或双重控制/ControlTemplate hack 是我见过的唯一解决方案。

【讨论】:

    【解决方案3】:

    是啊,这个有点恶心。

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

    我很高兴听到任何更好的想法,它们不依赖于可怕的代码隐藏或您的视图必须了解它需要使用不同的控件来提供宽度来支持视觉效果(糟糕!) .

    【讨论】:

    • 这种方法是否会将组合的大小调整到足够宽,以便最宽的项目在被选中时完全可见?这是我发现问题的地方。
    【解决方案4】:

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

    <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" 允许在组合框项目周围添加额外的 chrome(恐怕不是 chrome 不可知论者)。

    【讨论】:

    • 这是迄今为止最简单的答案。不得不这样做很荒谬,但它比其他解决方法复杂和荒谬一个数量级。为什么没有 SizeToOptions 属性,或者这不仅仅是控件的默认行为,我无法理解! WPF 有时完全是精神上的。不错,加斯波德!
    【解决方案5】:

    我最终为这个问题找到了一个“足够好”的解决方案,即让组合框永远不会缩小到它所容纳的最大尺寸以下,类似于旧的 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 功能。

    【讨论】:

    • 不错,但只有在选择了最长的条目后才“稳定”。
    • 正确。我在 WinForms 中对此做了一些处理,我将使用文本 API 来测量组合框中的所有字符串,并设置最小宽度来解决这个问题。在 WPF 中做同样的事情要困难得多,尤其是当您的项目不是字符串和/或来自绑定时。
    【解决方案6】:

    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。
    【解决方案7】:

    top answer 的替代解决方案是Measure the Popup 本身,而不是测量所有项目。给出稍微简单的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。

    【讨论】:

    • 此解决方案不必简单地显示 Combobox 即可测量其内容的大小,这比原来的最佳答案有了很好的改进。
    • 它可以工作,但组合框占用的空间比内容需要的多。与 emptySize 为静态 19 的原始答案相比,这里的 emptySize 计算值为 27。
    【解决方案8】:

    在 Dropbox 后面放置一个包含相同内容的列表框。然后使用这样的绑定来强制执行正确的高度:

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

    【讨论】:

      【解决方案9】:

      就我而言,一种更简单的方法似乎可以解决问题, 我只是使用了一个额外的 stackPanel 来包装组合框。

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

      (2008 年在 Visual Studio 工作)

      【讨论】:

        【解决方案10】:

        Alun Harford 的方法,在实践中:

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

        【讨论】:

          【解决方案11】:

          我希望它仅在下拉菜单打开时调整为最大元素,否则适合所选值。这是代码:

          部分基于 Frederik 的回答(实际上对我没有用)

          public static class ComboBoxAutoWidthBehavior {
              public static readonly DependencyProperty ComboBoxAutoWidthProperty =
                      DependencyProperty.RegisterAttached(
                          "ComboBoxAutoWidth",
                          typeof(bool),
                          typeof(ComboBoxAutoWidthBehavior),
                          new UIPropertyMetadata(false, OnComboBoxAutoWidthPropertyChanged)
                      );
          
              public static bool GetComboBoxAutoWidth(DependencyObject obj) {
                  return (bool) obj.GetValue(ComboBoxAutoWidthProperty);
              }
          
              public static void SetComboBoxAutoWidth(DependencyObject obj, bool value) {
                  obj.SetValue(ComboBoxAutoWidthProperty, value);
              }
          
              private static void OnComboBoxAutoWidthPropertyChanged(DependencyObject dpo, DependencyPropertyChangedEventArgs e) {
                  if(dpo is ComboBox comboBox) {
                      if((bool) e.NewValue) {
                          comboBox.Loaded += OnComboBoxLoaded;
                          comboBox.DropDownOpened += OnComboBoxOpened;
                          comboBox.DropDownClosed += OnComboBoxClosed;
                      } else {
                          comboBox.Loaded -= OnComboBoxLoaded;
                          comboBox.DropDownOpened -= OnComboBoxOpened;
                          comboBox.DropDownClosed -= OnComboBoxClosed;
                      }
                  }
              }
          
              private static void OnComboBoxLoaded(object sender, EventArgs eventArgs) {
                  ComboBox comboBox = (ComboBox) sender;
                  comboBox.SetMaxWidthFromItems();
              }
          
              private static void OnComboBoxOpened(object sender, EventArgs eventArgs) {
                  ComboBox comboBox = (ComboBox) sender;
                  comboBox.Width = comboBox.MaxWidth;
              }
          
              private static void OnComboBoxClosed(object sender, EventArgs eventArgs) => ((ComboBox) sender).Width = double.NaN;
          }
          
          public static class ComboBoxExtensionMethods {
              public static void SetMaxWidthFromItems(this ComboBox combo) {
                  double idealWidth = combo.MinWidth;
                  string longestItem = combo.Items.Cast<object>().Select(x => x.ToString()).Max(x => (x?.Length, x)).x;
                  if(longestItem != null && longestItem.Length >= 0) {
                      string tmpTxt = combo.Text;
                      combo.Text = longestItem;
                      Thickness tmpMarg = combo.Margin;
                      combo.Margin = new Thickness(0);
                      combo.UpdateLayout();
          
                      combo.Width = double.NaN;
                      combo.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
          
                      idealWidth = Math.Max(idealWidth, combo.DesiredSize.Width);
          
                      combo.Text = tmpTxt;
                      combo.Margin = tmpMarg;
                  }
          
                  combo.MaxWidth = idealWidth;
              }
          }
          

          然后你像这样启用它:

          <ComboBox behaviours:ComboBoxAutoWidthBehavior.ComboBoxAutoWidth="True" />
          

          您也可以直接设置 Width 而不是 MaxWidth,然后删除 DropDownOpened 和 Closed 部分,如果您希望它的行为与其他 anwsers 一样。

          【讨论】:

            【解决方案12】:

            我自己在寻找答案时,遇到了每个UIElement 都有的UpdateLayout() 方法。

            现在很简单,谢天谢地!

            设置或修改ItemSource后调用ComboBox1.Updatelayout();即可。

            【讨论】:

              【解决方案13】:

              这会将宽度保持为最宽的元素,但仅在打开组合框一次之后。

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

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2015-06-08
                • 1970-01-01
                • 2013-03-22
                • 2017-12-05
                • 1970-01-01
                • 2018-06-08
                • 2022-11-16
                • 2015-07-05
                相关资源
                最近更新 更多