簡體   English   中英

如何使 WPF 組合框具有 XAML 中其最寬元素的寬度?

[英]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 中:

  • 創建隱藏控件(Alan Hunford 的回答)
  • 徹底改變 ControlTemplate。 即使在這種情況下,也可能需要創建 ItemsPresenter 的隱藏版本。

這樣做的原因是我遇到的默認 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>

Horizo​​ntalAlignment="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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM