[英]How can I make a WPF combo box have the width of its widest element in XAML?
我知道如何在代码中做到这一点,但这可以在 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;
}
}
}
您不能直接在 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 事件中)
如果没有以下任何一项,就不能在 XAML 中:
这样做的原因是我遇到的默认 ComboBox ControlTemplates(Aero、Luna 等)都将 ItemsPresenter 嵌套在 Popup 中。 这意味着这些项目的布局被推迟,直到它们真正可见。
一个简单的测试方法是修改默认的 ControlTemplate 以将最外层容器(它是 Aero 和 Luna 的网格)的 MinWidth 绑定到 PART_Popup 的 ActualWidth。 当您单击下拉按钮时,您将能够让 ComboBox 自动同步其宽度,但之前不会。
因此,除非您可以在布局系统中强制执行 Measure 操作(您可以通过添加第二个控件来实现),否则我认为它无法完成。
与往常一样,我对一个简短、优雅的解决方案持开放态度——但在这种情况下,代码隐藏或双控制/ControlTemplate hacks 是我见过的唯一解决方案。
是的,这个有点恶心。
我过去所做的是在 ControlTemplate 中添加一个隐藏的列表框(其 itemscontainerpanel 设置为网格)同时显示每个项目,但它们的可见性设置为隐藏。
我很高兴听到任何不依赖可怕的代码隐藏的更好的想法,或者您的视图必须了解它需要使用不同的控件来提供宽度来支持视觉效果(糟糕!)。
根据上面的其他答案,这是我的版本:
<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(恐怕不是 chrome 不可知论者)。
我最终找到了一个“足够好”的解决方案来解决这个问题,即让组合框永远不会缩小到它所容纳的最大尺寸以下,类似于旧的 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 功能。
对 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();
}
}
}
最佳答案的另一种解决方案是测量弹出窗口本身,而不是测量所有项目。 给出稍微简单的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
es。
在 Dropbox 后面放置一个包含相同内容的列表框。 然后用一些这样的绑定来强制执行正确的高度:
<Grid>
<ListBox x:Name="listBox" Height="{Binding ElementName=dropBox, Path=DesiredSize.Height}" />
<ComboBox x:Name="dropBox" />
</Grid>
我希望它只在下拉菜单打开时调整到最大元素,否则适合所选值。 这是代码:
部分基于 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 一样。
在我的情况下,似乎有一种更简单的方法可以解决问题,我只是使用了一个额外的 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工作)
当我遇到每个UIElement
都有的UpdateLayout()
方法时,我自己正在寻找答案。
现在非常简单,谢天谢地!
只需调用ComboBox1.Updatelayout();
在您设置或修改ItemSource
。
阿伦哈福德的做法,在实践中:
<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>
这将宽度保持为最宽的元素,但仅在打开组合框一次之后。
<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>
只需在组合框中添加宽度即可
<ComboBox Name="ComboBox1" HorizontalAlignment="Left" VerticalAlignment="Top" Width="100">
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.