Bagaimana cara membuat combo box WPF memiliki lebar elemen terluas di XAML?

103

Saya tahu bagaimana melakukannya dalam kode, tetapi bisakah ini dilakukan di 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;
        }
    }
}
Csupor Jenő
sumber
Lihat posting lain pada baris serupa di stackoverflow.com/questions/826985/… Harap tandai pertanyaan Anda sebagai "terjawab" jika ini menjawab pertanyaan Anda.
Sudeep
Saya telah mencoba pendekatan ini dalam kode juga tetapi menemukan bahwa pengukuran dapat bervariasi antara Vista dan XP. Di Vista, DesiredSize biasanya menyertakan ukuran panah drop-down tetapi di XP, seringkali lebarnya tidak menyertakan panah drop-down. Sekarang, hasil saya mungkin karena saya mencoba melakukan pengukuran sebelum jendela induk terlihat. Menambahkan UpdateLayout () sebelum Pengukuran dapat membantu tetapi dapat menyebabkan efek samping lain di aplikasi. Saya akan tertarik untuk melihat solusi yang Anda berikan jika Anda ingin berbagi.
jschroedl
Bagaimana Anda menyelesaikan masalah Anda?
Andrew Kalashnikov

Jawaban:

31

Ini tidak bisa di XAML tanpa:

  • Membuat kontrol tersembunyi (jawaban Alan Hunford)
  • Mengubah ControlTemplate secara drastis. Bahkan dalam kasus ini, versi tersembunyi dari ItemsPresenter mungkin perlu dibuat.

Alasan untuk ini adalah bahwa ComboBox ControlTemplates default yang saya temui (Aero, Luna, dll.) Semua menumpuk ItemsPresenter dalam Popup. Ini berarti bahwa tata letak item-item ini ditangguhkan hingga benar-benar terlihat.

Cara mudah untuk mengujinya adalah dengan memodifikasi ControlTemplate default untuk mengikat MinWidth dari container terluar (ini adalah Grid untuk Aero dan Luna) ke ActualWidth dari PART_Popup. Anda akan dapat membuat ComboBox secara otomatis menyinkronkan lebarnya ketika Anda mengklik tombol drop, tetapi tidak sebelumnya.

Jadi, kecuali Anda dapat memaksa operasi Ukur dalam sistem tata letak (yang dapat Anda lakukan dengan menambahkan kontrol kedua), saya rasa itu tidak bisa dilakukan.

Seperti biasa, saya terbuka untuk solusi singkat dan elegan - tetapi dalam kasus ini hacks code-behind atau dual-control / ControlTemplate adalah satu-satunya solusi yang pernah saya lihat.

micahtan
sumber
57

Anda tidak dapat melakukannya secara langsung di Xaml tetapi Anda dapat menggunakan Perilaku Terlampir ini. (Lebar akan terlihat di Desainer)

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

The Attached Behavior 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);
    }
}

Apa yang dilakukannya adalah memanggil metode ekstensi untuk ComboBox yang disebut SetWidthFromItems yang (tidak terlihat) mengembang dan menciut sendiri dan kemudian menghitung Lebar berdasarkan ComboBoxItems yang dihasilkan. (IExpandCollapseProvider membutuhkan referensi ke UIAutomationProvider.dll)

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

Metode ekstensi ini juga menyediakan kemampuan untuk menelepon

comboBox.SetWidthFromItems();

dalam kode di belakang (misalnya di acara ComboBox.Loaded)

Fredrik Hedblad
sumber
+1, solusi yang bagus! Saya mencoba melakukan sesuatu di sepanjang jalur yang sama, tetapi akhirnya saya menggunakan penerapan Anda (dengan beberapa modifikasi)
Thomas Levesque
1
Terima kasih luar biasa. Ini harus ditandai sebagai jawaban yang diterima. Sepertinya properti terlampir selalu menjadi jalan menuju semuanya :)
Ignacio Soler Garcia
Solusi terbaik sejauh yang saya ketahui. Saya mencoba beberapa trik dari seluruh Internet, dan solusi Anda adalah yang terbaik dan termudah yang saya temukan. +1.
paercebal
7
Perhatikan bahwa jika Anda memiliki beberapa kotak kombo di jendela yang sama ( itu terjadi pada saya dengan jendela yang membuat kotak kombo dan isinya dengan kode di belakang ), munculan dapat terlihat sebentar. Saya rasa ini karena beberapa pesan "sembulan terbuka" diposting sebelum "sembulan tutup" dipanggil. Solusi untuk itu adalah membuat seluruh metode SetWidthFromItemsasynchronous saya menggunakan aksi / delegasi dan BeginInvoke dengan prioritas Idle (seperti yang dilakukan dalam acara Loaded). Dengan cara ini, tidak ada pengukuran yang akan dilakukan saat pompa pesan tidak kosong, dan dengan demikian, tidak akan terjadi interleaving pesan
paercebal
1
Apakah angka ajaib: double comboBoxWidth = 19;dalam kode Anda berhubungan dengan SystemParameters.VerticalScrollBarWidth?
Jf Beaulac
10

Ya, yang ini agak buruk.

Apa yang telah saya lakukan di masa lalu adalah menambahkan ke dalam ControlTemplate kotak daftar tersembunyi (dengan itemscontainerpanel diatur ke kisi) yang menampilkan setiap item pada saat yang sama tetapi dengan visibilitas diatur ke tersembunyi.

Saya akan senang mendengar ide yang lebih baik yang tidak bergantung pada kode di belakang yang mengerikan atau pandangan Anda harus memahami bahwa itu perlu menggunakan kontrol yang berbeda untuk memberikan lebar untuk mendukung visual (yuck!).

Alun Harford
sumber
1
Akankah pendekatan ini mengukur kombo cukup lebar sehingga item terluas terlihat sepenuhnya ketika itu adalah item yang dipilih? Di sinilah saya melihat masalah.
jschroedl
8

Berdasarkan jawaban lain di atas, inilah versi saya:

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

HorizontalAlignment = "Left" menghentikan kontrol menggunakan lebar penuh dari kontrol penampung. Height = "0" menyembunyikan kontrol item.
Margin = "15,0" memungkinkan penambahan chrome di sekitar item kotak kombo (sayangnya, bukan chrome agnostik).

Gaspode
sumber
4

Saya akhirnya mendapatkan solusi yang "cukup baik" untuk masalah ini yaitu membuat kotak kombo tidak pernah menyusut di bawah ukuran terbesar yang dimilikinya, mirip dengan WinForms AutoSizeMode = GrowOnly yang lama.

Cara saya melakukan ini adalah dengan konverter nilai khusus:

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

Kemudian saya mengkonfigurasi kotak kombo di XAML seperti ini:

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

Perhatikan bahwa dengan ini Anda memerlukan contoh terpisah dari GrowConverter untuk setiap kotak kombo, kecuali tentu saja Anda ingin satu set dari mereka untuk ukuran bersama, mirip dengan fitur SharedSizeScope Grid.

Cheetah
sumber
1
Bagus, tapi hanya “stabil” setelah memilih entri terpanjang.
primfaktor
1
Benar. Saya telah melakukan sesuatu tentang ini di WinForms, di mana saya akan menggunakan API teks untuk mengukur semua string di kotak kombo, dan mengatur lebar min untuk memperhitungkannya. Melakukan hal yang sama jauh lebih sulit di WPF, terutama bila item Anda bukan string dan / atau berasal dari penjilidan.
Cheetah
3

Tindak lanjut dari jawaban Maleak: Saya sangat menyukai penerapan itu, saya menulis Perilaku yang sebenarnya untuk itu. Jelas Anda membutuhkan Blend SDK sehingga Anda dapat mereferensikan System.Windows.Interactivity.

XAML:

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

Kode:

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();
        }
    }
}
Mike Post
sumber
Ini tidak berfungsi, saat ComboBox tidak diaktifkan. provider.Expand()melempar ElementNotEnabledException. Ketika Kotak Kombo tidak diaktifkan, karena orang tua dinonaktifkan, maka bahkan tidak mungkin untuk mengaktifkan Kotak Kombo untuk sementara sampai pengukuran selesai.
FlyingFoX
1

Letakkan kotak daftar yang berisi konten yang sama di belakang dropbox. Kemudian tegakkan ketinggian yang benar dengan beberapa pengikatan seperti ini:

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

Dalam kasus saya cara yang jauh lebih sederhana tampaknya melakukan trik, saya hanya menggunakan stackPanel ekstra untuk membungkus kotak kombo.

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

(bekerja di studio visual 2008)

Nikos Tsokos
sumber
1

Solusi alternatif untuk jawaban teratas adalah Mengukur Popup itu sendiri daripada mengukur semua item. Memberikan SetWidthFromItems()implementasi yang sedikit lebih sederhana :

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

bekerja pada orang-orang cacat ComboBoxjuga.

keajaiban
sumber
0

Saya sendiri sedang mencari jawabannya, ketika saya menemukan UpdateLayout()metode yang UIElementdimiliki setiap orang .

Ini sangat sederhana sekarang, untungnya!

Panggil saja ComboBox1.Updatelayout();setelah Anda menyetel atau memodifikasi file ItemSource.

Donat
sumber
0

Pendekatan Alun Harford, dalam praktiknya:

<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>
Jan Van Overbeke
sumber
0

Ini membuat lebar menjadi elemen terluas tetapi hanya setelah membuka kotak kombo sekali.

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