ฉันจะทำให้กล่องคำสั่งผสม WPF มีความกว้างขององค์ประกอบที่กว้างที่สุดใน XAML ได้อย่างไร


104

ฉันรู้วิธีทำในโค้ด แต่สามารถทำได้ใน 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/…โปรดทำเครื่องหมายคำถามของคุณว่า "ตอบแล้ว" หากสิ่งนี้ตอบคำถามของคุณ
Sudeep

ฉันได้ลองใช้วิธีนี้ในรหัสเช่นกัน แต่พบว่าการวัดอาจแตกต่างกันระหว่าง Vista และ XP ใน Vista โดยปกติ DesiredSize จะมีขนาดลูกศรแบบเลื่อนลง แต่ใน XP ส่วนใหญ่ความกว้างจะไม่มีลูกศรแบบเลื่อนลง ตอนนี้ผลลัพธ์ของฉันอาจเป็นเพราะฉันกำลังพยายามทำการวัดก่อนที่จะมองเห็นหน้าต่างหลัก การเพิ่ม UpdateLayout () ก่อนการวัดจะช่วยได้ แต่อาจทำให้เกิดผลข้างเคียงอื่น ๆ ในแอป ฉันสนใจที่จะเห็นวิธีแก้ปัญหาที่คุณคิดขึ้นหากคุณยินดีที่จะแบ่งปัน
jschroedl

คุณแก้ไขปัญหาของคุณอย่างไร
Andrew Kalashnikov

คำตอบ:


31

สิ่งนี้ไม่สามารถอยู่ใน XAML หากไม่มี:

  • การสร้างการควบคุมที่ซ่อนอยู่ (คำตอบของ Alan Hunford)
  • การเปลี่ยน ControlTemplate อย่างมาก แม้ว่าในกรณีนี้อาจต้องสร้าง ItemsPresenter เวอร์ชันที่ซ่อนอยู่

เหตุผลนี้ก็คือ ComboBox ControlTemplates เริ่มต้นที่ฉันเจอ (Aero, Luna และอื่น ๆ ) ล้วนซ้อนรายการ ItemsPresenter ในป๊อปอัป ซึ่งหมายความว่าเค้าโครงของรายการเหล่านี้จะถูกเลื่อนออกไปจนกว่าจะปรากฏให้เห็นได้จริง

วิธีง่ายๆในการทดสอบสิ่งนี้คือการแก้ไข ControlTemplate เริ่มต้นเพื่อผูก MinWidth ของคอนเทนเนอร์ด้านนอกสุด (เป็น Grid สำหรับทั้ง Aero และ Luna) กับ ActualWidth ของ PART_Popup คุณจะสามารถให้ ComboBox ซิงโครไนซ์ความกว้างของมันโดยอัตโนมัติเมื่อคุณคลิกปุ่มวาง แต่ไม่ใช่ก่อนหน้านี้

ดังนั้นถ้าคุณไม่สามารถบังคับการดำเนินการวัดในระบบเค้าโครง (ซึ่งคุณสามารถทำได้โดยการเพิ่มตัวควบคุมที่สอง) ฉันไม่คิดว่าจะทำได้

เช่นเคยฉันเปิดรับโซลูชันสั้น ๆ ที่หรูหรา - แต่ในกรณีนี้การแฮ็กรหัสหลังหรือการควบคุมคู่ / ControlTemplate เป็นวิธีแก้ปัญหาเดียวที่ฉันเคยเห็น


57

คุณไม่สามารถทำได้โดยตรงใน 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);
    }
}

สิ่งที่ทำคือเรียกวิธีการขยายสำหรับ ComboBox ที่เรียกว่า SetWidthFromItems ซึ่ง (สุดลูกหูลูกตา) จะขยายและยุบตัวเองจากนั้นคำนวณความกว้างตาม 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 ทางออกที่ยอดเยี่ยม! ฉันพยายามทำอะไรบางอย่างตามแนวเดียวกัน แต่ในที่สุดฉันก็ใช้การใช้งานของคุณ (โดยมีการปรับเปลี่ยนเล็กน้อย)
Thomas Levesque

1
ขอบคุณมาก ควรทำเครื่องหมายว่าเป็นคำตอบที่ยอมรับ ดูเหมือนว่าคุณสมบัติที่แนบมาจะเป็นหนทางไปสู่ทุกสิ่ง :)
Ignacio Soler Garcia

ทางออกที่ดีที่สุดเท่าที่ฉันกังวล ฉันลองใช้กลเม็ดมากมายจากทั่วอินเทอร์เน็ตและวิธีแก้ปัญหาของคุณเป็นวิธีที่ดีที่สุดและง่ายที่สุด +1.
paercebal

7
โปรดทราบว่าหากคุณมีกล่องคอมโบหลายอันในหน้าต่างเดียวกัน ( มันเกิดขึ้นกับฉันโดยมีหน้าต่างที่สร้างกล่องคอมโบและเนื้อหาที่มีโค้ดอยู่ข้างหลัง ) ป๊อปอัปจะปรากฏขึ้นเป็นเวลาหนึ่งวินาที ฉันเดาว่านี่เป็นเพราะข้อความ "เปิดป๊อปอัป" หลายรายการถูกโพสต์ก่อนที่จะเรียก "ป๊อปอัปปิด" ใด ๆ วิธีแก้ปัญหาคือทำให้เมธอดทั้งหมดเป็นSetWidthFromItemsแบบอะซิงโครนัสโดยใช้การดำเนินการ / ผู้รับมอบสิทธิ์และ BeginInvoke ที่มีลำดับความสำคัญไม่ได้ใช้งาน (ดังที่ทำในเหตุการณ์ Loaded) วิธีนี้จะไม่มีการวัดผลใด ๆ ในขณะที่ปั๊มระเบียบไม่ว่างเปล่าดังนั้นจะไม่มีการแทรกข้อความ
paercebal

1
เลขวิเศษ: double comboBoxWidth = 19;ในรหัสของคุณเกี่ยวข้องกับSystemParameters.VerticalScrollBarWidthอะไร?
Jf Beaulac

10

ใช่อันนี้น่ารังเกียจนิดหน่อย

สิ่งที่ฉันเคยทำในอดีตคือการเพิ่มลงใน ControlTemplate ซึ่งเป็นลิสต์บ็อกซ์ที่ซ่อนอยู่ (โดยมีการตั้งค่า itemscontainerpanel เป็นตาราง) แสดงทุกรายการพร้อมกัน แต่ตั้งค่าการเปิดเผยเป็นซ่อน

ฉันยินดีที่จะรับทราบแนวคิดที่ดีกว่าที่ไม่ต้องพึ่งพาโค้ดที่น่ากลัวหรือมุมมองของคุณที่ต้องเข้าใจว่าจำเป็นต้องใช้การควบคุมที่แตกต่างกันเพื่อให้ความกว้างในการรองรับภาพ (ห่วย!)


1
วิธีนี้จะปรับขนาดคำสั่งผสมให้กว้างพอที่จะมองเห็นรายการที่กว้างที่สุดได้อย่างเต็มที่เมื่อเป็นรายการที่เลือกหรือไม่? นี่คือที่ที่ฉันเห็นปัญหา
jschroedl

8

จากคำตอบอื่น ๆ ข้างต้นนี่คือเวอร์ชันของฉัน:

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

HorizontalAlignment = "ซ้าย" หยุดการควบคุมโดยใช้ความกว้างเต็มของตัวควบคุมที่มี Height = "0" ซ่อนส่วนควบคุมรายการ
Margin = "15,0" อนุญาตให้มีโครเมี่ยมเพิ่มเติมรอบ ๆ รายการคอมโบบ็อกซ์ (ไม่ใช่โครเมี่ยมไม่เชื่อเรื่องพระเจ้าฉันกลัว)


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 ของ Grid


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

1

วางกล่องรายการที่มีเนื้อหาเดียวกันไว้ด้านหลังดรอปบ็อกซ์ จากนั้นบังคับใช้ความสูงที่ถูกต้องด้วยการผูกแบบนี้:

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

1

ในกรณีของฉันดูเหมือนวิธีที่ง่ายกว่ามากในการทำเคล็ดลับฉันเพิ่งใช้ 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

อีกทางเลือกหนึ่งสำหรับคำตอบอันดับต้น ๆคือการวัดป๊อปอัพเองแทนที่จะวัดรายการทั้งหมด ให้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เช่นกัน


0

ฉันกำลังมองหาคำตอบด้วยตัวเองเมื่อฉันเจอUpdateLayout()วิธีการที่ทุกคนUIElementมี

ตอนนี้มันง่ายมากขอบคุณ!

เพียงโทรComboBox1.Updatelayout();หลังจากที่คุณตั้งค่าหรือแก้ไขไฟล์ItemSource.


0

แนวทางของ 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>

0

สิ่งนี้จะรักษาความกว้างให้เป็นองค์ประกอบที่กว้างที่สุด แต่หลังจากเปิดกล่องคำสั่งผสมเพียงครั้งเดียว

<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>
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.