Bir WPF birleşik giriş kutusunun XAML'deki en geniş öğesinin genişliğine sahip olmasını nasıl sağlayabilirim?


103

Kodda nasıl yapılacağını biliyorum, ancak bu XAML'de yapılabilir mi?

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/… adresinde benzer satırlardaki başka bir gönderiye bakın. Sorunuzu yanıtladıysa, lütfen sorunuzu "yanıtlandı" olarak işaretleyin.
Sudeep

Bu yaklaşımı kodda da denedim, ancak ölçümün Vista ve XP arasında değişebileceğini gördüm. Vista'da DesiredSize genellikle aşağı açılır ok boyutunu içerir, ancak XP'de genellikle genişlik aşağı açılır oku içermez. Şimdi, sonuçlarımın nedeni, ana pencere görünmeden önce ölçümü yapmaya çalışmam olabilir. Measure'dan önce bir UpdateLayout () eklemek yardımcı olabilir, ancak uygulamada başka yan etkilere neden olabilir. Paylaşmaya istekliysen, bulduğun çözümü görmek isterim.
jschroedl

Sorununuzu nasıl çözdünüz?
Andrew Kalashnikov

Yanıtlar:


31

Bu, aşağıdakilerden biri olmadan XAML'de olamaz:

  • Gizli bir kontrol yaratmak (Alan Hunford'un cevabı)
  • Kontrol Şablonunu büyük ölçüde değiştirme. Bu durumda bile, bir ItemsPresenter'ın gizli bir sürümünün oluşturulması gerekebilir.

Bunun nedeni, karşılaştığım varsayılan ComboBox Kontrol Şablonlarının (Aero, Luna, vb.) Hepsinin ItemsPresenter'ı bir Popup'ta iç içe geçirmesidir. Bu, bu öğelerin düzeninin gerçekte görünür hale gelene kadar ertelendiği anlamına gelir.

Bunu test etmenin kolay bir yolu, varsayılan Kontrol Şablonunu, en dıştaki kabın MinWidth'ini (hem Aero hem de Luna için bir Grid'dir) PART_Popup'ın Gerçek Genişliğine bağlamak için değiştirmektir. Bırak düğmesini tıkladığınızda ComboBox'un genişliğini otomatik olarak senkronize etmesini sağlayabilirsiniz, ancak daha önce değil.

Öyleyse, yerleşim sisteminde ( ikinci bir kontrol ekleyerek yapabilirsiniz) bir Ölçme işlemini zorlayamadığınız sürece , bunun yapılabileceğini sanmıyorum.

Her zaman olduğu gibi, kısa, zarif bir çözüme açığım - ancak bu durumda, gördüğüm tek çözüm arka planda kodlama veya çift kontrol / Kontrol Şablonu korsanları.


57

Bunu doğrudan Xaml'de yapamazsınız, ancak bu Ekli Davranışı kullanabilirsiniz. (Genişlik, Tasarımcı'da görünecektir)

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

Ekli Davranış 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);
    }
}

Yaptığı şey, ComboBox için SetWidthFromItems adında (görünmez bir şekilde) kendini genişleten ve daraltan ve ardından oluşturulan ComboBoxItems'e dayalı olarak Genişliği hesaplayan bir uzantı yöntemini çağırmasıdır. (IExpandCollapseProvider, UIAutomationProvider.dll'ye bir başvuru gerektirir)

Ardından genişletme yöntemi 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();
    }
}

Bu uzantı yöntemi, aynı zamanda

comboBox.SetWidthFromItems();

arkasındaki kodda (örneğin, ComboBox.Loaded olayında)


+1, harika çözüm! Ben de aynı doğrultuda bir şeyler yapmaya çalışıyordum, ama sonunda uygulamanızı kullandım (birkaç değişiklikle)
Thomas Levesque

1
İnanılmaz teşekkürler. Bu, kabul edilen cevap olarak işaretlenmelidir. Görünüşe göre ekli mülkler her zaman her şeye giden yoldur :)
Ignacio Soler Garcia

Bana göre en iyi çözüm. İnternetin dört bir yanından birçok numara denedim ve bulduğum en iyi ve en kolay çözümünüz. +1.
paercebal

7
Aynı pencerede birden fazla birleşik giriş kutunuz varsa ( benim için birleşik giriş kutuları ve bunların arka planda kod içeren içeriklerini oluşturan bir pencereyle oldu ), açılır pencerelerin bir saniye için görünür hale gelebileceğini unutmayın. Sanırım bunun nedeni, herhangi bir "açılır pencereyi kapat" çağrılmadan önce birden fazla "açılır pencere" mesajının gönderilmesidir. Bunun çözümü, SetWidthFromItemsbir eylem / temsilci ve Boşta kalma önceliğine sahip bir BeginInvoke kullanarak tüm yöntemi eşzamansız hale getirmektir (Loaded olayında yapıldığı gibi). Bu şekilde, mesaj pompası boş değilken hiçbir önlem alınmayacak ve bu nedenle hiçbir mesaj serpiştirme oluşmayacaktır
paercebal

1
double comboBoxWidth = 19;Kodunuzdaki sihirli sayı: ile ilgili SystemParameters.VerticalScrollBarWidthmi?
Jf Beaulac

10

Evet, bu biraz iğrenç.

Geçmişte yaptığım şey, ControlTemplate'e gizli bir liste kutusu (itemscontainerpanel'i bir ızgaraya ayarlanmış) eklemek ve her öğeyi aynı anda gösteren, ancak görünürlüğü gizli olarak ayarlanmış.

Korkunç arka plan kodlamaya dayanmayan daha iyi fikirleri veya görselleri desteklemek için genişliği sağlamak için farklı bir kontrol kullanması gerektiğini anlamak zorunda olduğunuzu duymaktan memnun olurum (iğrenç!).


1
Bu yaklaşım, en geniş öğe seçilen öğe olduğunda tamamen görünür olacak şekilde komboyu yeterince geniş boyutlandıracak mı? Sorunları burada gördüm.
jschroedl

8

Yukarıdaki diğer cevaplara göre, benim versiyonum:

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

HorizontalAlignment = "Left", içeren kontrolün tam genişliğini kullanarak kontrolleri durdurur. Yükseklik = "0" öğe kontrolünü gizler.
Margin = "15,0", birleşik kutu öğeleri etrafında ek kroma izin verir (korkarım krom agnostik değil).


4

Bu soruna, eski WinForms AutoSizeMode = GrowOnly gibi, açılan kutuyu hiçbir zaman sahip olduğu en büyük boyutun altına küçültmek olan "yeterince iyi" bir çözüm buldum.

Bunu özel bir değer dönüştürücü ile yapma şeklim:

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

Ardından XAML'deki birleşik giriş kutusunu şu şekilde yapılandırıyorum:

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

Bununla birlikte, Grid'in SharedSizeScope özelliğine benzer şekilde bir dizi birlikte boyutlandırmak istemediğiniz sürece, her bir açılan kutu için ayrı bir GrowConverter örneğine ihtiyacınız olduğunu unutmayın.


1
Güzel, ancak yalnızca en uzun girişi seçtikten sonra "kararlı".
primfaktor

1
Doğru. WinForms'ta bununla ilgili bir şey yapmıştım, burada açılan kutudaki tüm dizeleri ölçmek için metin API'lerini kullanacak ve bunu hesaba katmak için minimum genişliği ayarlayacaktım. Aynısını yapmak, özellikle öğeleriniz dizge olmadığında ve / veya bir bağlamadan geliyorsa, WPF'de çok daha zordur.
Cheetah

3

Maleak'ın cevabının devamı: Bu uygulamayı çok beğendim, bunun için gerçek bir Davranış yazdım. Açıkçası, System.Windows.Interactivity'ye başvurabilmeniz için Blend SDK'ya ihtiyacınız olacak.

XAML:

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

Kod:

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 etkinleştirilmediğinde bu çalışmaz. provider.Expand()bir atar ElementNotEnabledException. Bir ebeveynin devre dışı bırakılması nedeniyle ComboBox etkinleştirilmediğinde, ölçüm bitene kadar ComboBox'ı geçici olarak etkinleştirmek bile mümkün değildir.
flyingfox

1

Dropbox'ın arkasına aynı içeriği içeren bir liste kutusu yerleştirin. Ardından, aşağıdaki gibi bir bağlama ile doğru yüksekliği zorlayın:

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

1

Benim durumumda çok daha basit bir yol hile yapmak gibi görünüyordu, sadece birleşik giriş kutusunu sarmak için fazladan bir stackPanel kullandım.

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

(visual studio 2008'de çalıştı)


1

Alternatif bir çözüm üst cevap etmektir Popup ölçün yerine tüm öğeleri ölçme daha kendisi. Biraz daha basit SetWidthFromItems()uygulama sağlar:

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

engelli ComboBoxeslerde de çalışır .


0

Herkesin sahip olduğu UpdateLayout()yöntemle karşılaştığımda cevabı kendim arıyordum UIElement.

Şükürler olsun, şimdi çok basit!

ComboBox1.Updatelayout();Ayarladıktan veya değiştirdikten sonra aramanız yeterlidir ItemSource.


0

Alun Harford'un pratikte yaklaşımı:

<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

Bu, genişliği en geniş öğeye kadar tutar, ancak yalnızca açılan kutuyu bir kez açtıktan sonra.

<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>
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.