Làm cách nào để tạo một hộp tổ hợp WPF có chiều rộng của phần tử rộng nhất trong XAML?


103

Tôi biết làm thế nào để làm điều đó trong mã, nhưng điều này có thể được thực hiện trong 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;
        }
    }
}

Kiểm tra một bài đăng khác trên các dòng tương tự tại stackoverflow.com/questions/826985/… Vui lòng đánh dấu câu hỏi của bạn là "đã trả lời" nếu điều này trả lời câu hỏi của bạn.
Sudeep

Tôi cũng đã thử cách tiếp cận này trong mã nhưng thấy rằng phép đo có thể khác nhau giữa Vista và XP. Trên Vista, DesedlySize thường bao gồm kích thước mũi tên thả xuống nhưng trên XP, chiều rộng thường không bao gồm mũi tên thả xuống. Bây giờ, kết quả của tôi có thể là do tôi đang cố thực hiện phép đo trước khi cửa sổ chính hiển thị. Thêm một UpdateLayout () trước Biện pháp có thể hữu ích nhưng có thể gây ra các tác dụng phụ khác trong ứng dụng. Tôi muốn xem giải pháp mà bạn đưa ra nếu bạn sẵn sàng chia sẻ.
jschroedl

Bạn đã giải quyết vấn đề của mình như thế nào?
Andrew Kalashnikov

Câu trả lời:


31

Điều này không thể có trong XAML mà không có:

  • Tạo điều khiển ẩn (câu trả lời của Alan Hunford)
  • Thay đổi đáng kể ControlTemplate. Ngay cả trong trường hợp này, có thể cần tạo một phiên bản ẩn của một ItemsPresenter.

Lý do cho điều này là các ComboBox ControlTemplates mặc định mà tôi đã xem qua (Aero, Luna, v.v.) đều lồng các ItemsPresenter trong một Cửa sổ bật lên. Điều này có nghĩa là bố cục của các mục này được hoãn lại cho đến khi chúng thực sự được hiển thị.

Một cách dễ dàng để kiểm tra điều này là sửa đổi ControlTemplate mặc định để liên kết Chiều rộng tối thiểu của vùng chứa ngoài cùng (đó là Lưới cho cả Aero và Luna) với Chiều rộng thực của PART_Popup. Bạn sẽ có thể có ComboBox tự động đồng bộ hóa chiều rộng của nó khi bạn nhấp vào nút thả, nhưng không phải trước đó.

Vì vậy, trừ khi bạn có thể buộc hoạt động Đo lường trong hệ thống bố trí (mà bạn có thể thực hiện bằng cách thêm điều khiển thứ hai), tôi không nghĩ rằng nó có thể được thực hiện.

Như mọi khi, tôi luôn sẵn sàng cho một giải pháp ngắn gọn, thanh lịch - nhưng trong trường hợp này, hack mã phía sau hoặc kiểm soát kép / ControlTemplate là giải pháp duy nhất tôi thấy.


57

Bạn không thể làm điều đó trực tiếp trong Xaml nhưng bạn có thể sử dụng Hành vi đính kèm này. (Chiều rộng sẽ hiển thị trong Trình thiết kế)

<ComboBox behaviors:ComboBoxWidthFromItemsBehavior.ComboBoxWidthFromItems="True">
    <ComboBoxItem Content="Short"/>
    <ComboBoxItem Content="Medium Long"/>
    <ComboBoxItem Content="Min"/>
</ComboBox>

Hành vi được đính kèm 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);
    }
}

Những gì nó làm là nó gọi một phương thức mở rộng cho ComboBox được gọi là SetWidthFromItems (vô hình) mở rộng và thu gọn chính nó và sau đó tính toán Chiều rộng dựa trên ComboBoxItems đã tạo. (IExpandCollapseProvider yêu cầu tham chiếu đến UIAutomationProvider.dll)

Sau đó, phương thức mở rộng 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();
    }
}

Phương thức mở rộng này cũng cung cấp khả năng gọi

comboBox.SetWidthFromItems();

trong mã phía sau (ví dụ: trong sự kiện ComboBox.Loaded)


+1, giải pháp tuyệt vời! Tôi đã cố gắng làm điều gì đó theo cùng một hướng, nhưng cuối cùng tôi đã sử dụng cách triển khai của bạn (với một vài sửa đổi)
Thomas Levesque

1
Cảm ơn tuyệt vời. Đây phải được đánh dấu là câu trả lời được chấp nhận. Có vẻ như các thuộc tính đính kèm luôn là con đường dẫn đến mọi thứ :)
Ignacio Soler Garcia.

Giải pháp tốt nhất theo như tôi có liên quan. Tôi đã thử nhiều thủ thuật từ khắp nơi trên Internet và giải pháp của bạn là giải pháp tốt nhất và dễ nhất mà tôi tìm thấy. +1.
paercebal

7
Lưu ý rằng nếu bạn có nhiều hộp tổ hợp trong cùng một cửa sổ ( điều đó xảy ra với tôi với một cửa sổ tạo các hộp tổ hợp và nội dung của chúng có mã phía sau ), các cửa sổ bật lên có thể hiển thị trong một giây. Tôi đoán điều này là do nhiều thông báo "cửa sổ bật lên mở" được đăng trước khi bất kỳ "cửa sổ bật lên đóng" nào được gọi. Giải pháp cho điều đó là làm cho toàn bộ phương thức SetWidthFromItemskhông đồng bộ của tôi bằng cách sử dụng một hành động / đại biểu và một BeginInvoke với mức độ ưu tiên Không hoạt động (như được thực hiện trong sự kiện Đã tải). Bằng cách này, sẽ không có biện pháp nào được thực hiện trong khi máy bơm lộn xộn không có sản phẩm nào và do đó, không có sự xen kẽ thông báo nào xảy ra
paercebal

1
Con số kỳ diệu: double comboBoxWidth = 19;trong mã của bạn có liên quan đến SystemParameters.VerticalScrollBarWidth?
Jf Beaulac,

10

Yeah, cái này hơi khó chịu.

Những gì tôi đã làm trước đây là thêm vào ControlTemplate một hộp danh sách ẩn (với bảng điều khiển mục của nó được đặt thành lưới) hiển thị mọi mục cùng một lúc nhưng với khả năng hiển thị của chúng được đặt thành ẩn.

Tôi rất vui khi biết bất kỳ ý tưởng nào tốt hơn mà không dựa vào mã phía sau khủng khiếp hoặc chế độ xem của bạn phải hiểu rằng nó cần sử dụng một điều khiển khác để cung cấp chiều rộng để hỗ trợ hình ảnh (yuck!).


1
Cách tiếp cận này có kích thước kết hợp đủ rộng để mục rộng nhất có thể nhìn thấy đầy đủ khi đó là mục được chọn không? Đây là nơi tôi đã thấy vấn đề.
jschroedl

8

Dựa trên các câu trả lời khác ở trên, đây là phiên bản của tôi:

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

HorizontalAlignment = "Left" dừng các điều khiển bằng cách sử dụng toàn bộ chiều rộng của điều khiển chứa. Chiều cao = "0" ẩn điều khiển các mục.
Margin = "15,0" cho phép thêm chrome xung quanh các mục combo-box (tôi e rằng không phải chrome bất khả tri).


4

Tôi đã kết thúc với một giải pháp "đủ tốt" cho vấn đề này là làm cho hộp tổ hợp không bao giờ thu nhỏ dưới kích thước lớn nhất mà nó có, tương tự như WinForms AutoSizeMode = GrowOnly cũ.

Cách tôi đã làm điều này là với một công cụ chuyển đổi giá trị tùy chỉnh:

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

Sau đó, tôi định cấu hình hộp tổ hợp trong XAML như sau:

 <Whatever>
        <Whatever.Resources>
            <my:GrowConverter x:Key="grow" />
        </Whatever.Resources>
        ...
        <ComboBox MinWidth="{Binding ActualWidth,RelativeSource={RelativeSource Self},Converter={StaticResource grow}}" />
    </Whatever>

Lưu ý rằng với điều này, bạn cần một phiên bản riêng biệt của GrowConverter cho mỗi hộp kết hợp, trừ khi tất nhiên bạn muốn một tập hợp chúng có cùng kích thước, tương tự như tính năng SharedSizeScope của Grid.


1
Đẹp, nhưng chỉ "ổn định" sau khi đã chọn mục dài nhất.
primfaktor

1
Chính xác. Tôi đã làm điều gì đó về điều này trong WinForms, nơi tôi sẽ sử dụng các API văn bản để đo lường tất cả các chuỗi trong hộp tổ hợp và đặt chiều rộng tối thiểu để tính đến điều đó. Làm điều tương tự khó hơn đáng kể trong WPF, đặc biệt khi các mục của bạn không phải là chuỗi và / hoặc đến từ một ràng buộc.
Cheetah

3

Tiếp theo câu trả lời của Maleak: Tôi rất thích cách triển khai đó, tôi đã viết một Hành vi thực tế cho nó. Rõ ràng là bạn sẽ cần Blend SDK để có thể tham khảo System.Windows.Interactivity.

XAML:

    <ComboBox ItemsSource="{Binding ListOfStuff}">
        <i:Interaction.Behaviors>
            <local:ComboBoxWidthBehavior />
        </i:Interaction.Behaviors>
    </ComboBox>

Mã:

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

Điều này không hoạt động khi ComboBox không được bật. provider.Expand()ném một ElementNotEnabledException. Khi ComboBox không được kích hoạt, do phụ huynh bị vô hiệu hóa, thì thậm chí không thể tạm thời kích hoạt ComboBox cho đến khi quá trình đo kết thúc.
FlyingFoX

1

Đặt một hộp danh sách chứa cùng một nội dung phía sau hộp kéo thả. Sau đó, thực thi chiều cao chính xác với một số ràng buộc như sau:

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

1

Trong trường hợp của tôi, có vẻ như một cách đơn giản hơn nhiều để thực hiện thủ thuật, tôi chỉ cần sử dụng thêm một stackPanel để bọc combobox.

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

(làm việc trong studio trực quan 2008)


1

Một giải pháp thay thế cho câu trả lời hàng đầuĐo lường chính Cửa sổ bật lên thay vì đo lường tất cả các mục. SetWidthFromItems()Thực hiện đơn giản hơn một chút :

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

cũng hoạt động trên những người khuyết tật ComboBox.


0

Tôi đang tự mình tìm kiếm câu trả lời, khi tôi bắt gặp UpdateLayout()phương pháp mà mọi người đều UIElementcó.

Nó rất đơn giản bây giờ, rất may!

Chỉ cần gọi ComboBox1.Updatelayout();sau khi bạn đặt hoặc sửa đổi ItemSource.


0

Cách tiếp cận của Alun Harford, trong thực tế:

<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

Điều này giữ cho chiều rộng của phần tử rộng nhất nhưng chỉ sau khi mở hộp tổ hợp một lần.

<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>
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.