【问题标题】:Setting ListViewColumn width to the widest item regardless if it's in the initial render?无论它是否在初始渲染中,都将 ListViewColumn 宽度设置为最宽的项目?
【发布时间】:2014-04-20 14:18:51
【问题描述】:

所以,我遇到了一个有趣的问题。我有一个ListView 和两列:

<ListView x:Name="dataView">
    <ListView.View>
        <GridView>
            <GridViewColumn Header="R1" DisplayMemberBinding="{Binding Path=R1}"/>
            <GridViewColumn Header="R1 Icon">
                <GridViewColumn.CellTemplate>
                    <DataTemplate>
                        <Image Source="{Binding Path=R1Icon}"/>
                    </DataTemplate>
                </GridViewColumn.CellTemplate>
            </GridViewColumn>
        </GridView>
    </ListView.View>
</ListView>

无论我将第一列的width 设置为auto 还是将其保留为如上所示的默认值,初始宽度都将设置为呈现窗口中最宽的项目。因此,如果我将窗口高度设置为 400,并且下一个元素比正在渲染的元素宽,则它不会考虑其宽度。相反,它将使用最宽的渲染项的宽度。如果我将高度设置为... 410,它将考虑下一个元素的宽度。但是,有数百个项目,我不能为此目的使用高度。有没有办法将该列的宽度设置为其中最宽的元素,无论它是否在初始渲染中?

请注意,我不想使用相关 SO 问题中的 ScrollViewer.CanContentScroll="False" 解决方案。如果列表非常大,这将对性能产生巨大影响。

【问题讨论】:

  • 你试过像this answer那样的SharedSizeScope吗?
  • @user3411327 不幸的是,这不适用于未渲染的元素。甚至这个问题也只针对可见对象。

标签: c# .net wpf listview


【解决方案1】:

这个答案是基于我之前的讨论 safeOtter。存在一个问题,即没有根据每个用户的分辨率呈现的文本大小动态核算。他的解决方案的另一个问题是,每次大小发生变化时都会触发事件。因此,我将其限制为在渲染之前发生的初始加载事件。这是我想出的:

private void View_Loaded(object sender, RoutedEventArgs e)
{
    var listView = (sender as ListView);
    var gridView = (listView.View as GridView);

    // Standard safety check.
    if (listView == null || gridView == null)
    {
        return;
    }

    // Initialize a new typeface based on the currently used font.
    var typeFace = new Typeface(listView.FontFamily, listView.FontStyle, 
                                listView.FontWeight, listView.FontStretch);

    // This variable will hold the longest string from the source list.
    var longestString = dataList.OrderByDescending(s => s.Length).First();

    // Initialize a new FormattedText instance based on our longest string.
    var text = new System.Windows.Media.FormattedText(longestString, 
                       System.Globalization.CultureInfo.CurrentCulture,
                       System.Windows.FlowDirection.LeftToRight, typeFace,  
                       listView.FontSize, listView.Foreground);

    // Assign the width of the FormattedText to the column width.
    gridView.Columns[0].Width = text.Width;
}

有一个轻微的宽度错误,切断了字符串的最后两个字符。我测量它是12像素。可以将缓冲区添加到 12-20 像素 (+ 12.0f) 之间的列宽以解决该错误。看来这很常见,我需要做更多的研究。

我尝试过的其他方法:

using (Graphics g = Graphics.FromHwnd(IntPtr.Zero))
{
    SizeF size = g.MeasureString(longestString, 
                     System.Drawing.SystemFonts.DefaultFont);
    gridView.Columns[0].Width = size.Width;
}

那个有大约 14 像素的测量误差。该方法和我将在下面展示的方法的问题是,它们都依赖于System.Drawing.SystemFonts.DefaultFont,因为如果从控件中检索字体时错误太大了。如果控件使用不同的东西,这种对系统字体的依赖是非常严格的。

我尝试的最后一种方法(测量误差太大):

gridView.Columns[0].Width = System.Windows.Forms.TextRenderer.MeasureText(
                                longestString, 
                                System.Drawing.SystemFonts.DefaultFont).Width;

我对第一种方法很满意,但我还没有找到任何可以完美测量文本的方法。所以,只剪掉几个字符并用缓冲区修复它并不是那么糟糕。

编辑:

这是我发现的另一种方法 @WPF equivalent to TextRenderer 它提供了大约 14 像素的错误。所以,第一种方法是迄今为止表现最好的。

    private void View_Loaded(object sender, RoutedEventArgs e)
    {
        var listView = (sender as ListView);
        var gridView = (listView.View as GridView);

        if (listView == null || gridView == null)
        {
            return;
        }

        gridView.Columns[0].Width = MeasureText(dataList.OrderByDescending(
                                        s => s.Length).First(),
                                        listView.FontFamily, 
                                        listView.FontStyle, 
                                        listView.FontWeight, 
                                        listView.FontStretch, 
                                        listView.FontSize).Width;
    }

    public static System.Windows.Size MeasureTextSize(string text, 
                                          System.Windows.Media.FontFamily fontFamily, 
                                          System.Windows.FontStyle fontStyle, 
                                          FontWeight fontWeight, 
                                          FontStretch fontStretch, double fontSize)
    {
        FormattedText ft = new FormattedText(text,
                                             CultureInfo.CurrentCulture,
                                             FlowDirection.LeftToRight,
                                             new Typeface(fontFamily, fontStyle, 
                                                 fontWeight, fontStretch),
                                                 fontSize,
                                                 System.Windows.Media.Brushes.Black);
        return new System.Windows.Size(ft.Width, ft.Height);
    }

    public static System.Windows.Size MeasureText(string text, 
                                          System.Windows.Media.FontFamily fontFamily, 
                                          System.Windows.FontStyle fontStyle, 
                                          FontWeight fontWeight, 
                                          FontStretch fontStretch, double fontSize)
    {
        Typeface typeface = new Typeface(fontFamily, fontStyle, fontWeight,
                                         fontStretch);
        GlyphTypeface glyphTypeface;

        if (!typeface.TryGetGlyphTypeface(out glyphTypeface))
        {
            return MeasureTextSize(text, fontFamily, fontStyle, fontWeight, 
                                   fontStretch, fontSize);
        }

        double totalWidth = 0;
        double height = 0;

        for (int n = 0; n < text.Length; n++)
        {
            ushort glyphIndex = glyphTypeface.CharacterToGlyphMap[text[n]];

            double width = glyphTypeface.AdvanceWidths[glyphIndex] * fontSize;

            double glyphHeight = glyphTypeface.AdvanceHeights[glyphIndex] * fontSize;

            if (glyphHeight > height)
            {
                height = glyphHeight;
            }

            totalWidth += width;
        }

        return new System.Windows.Size(totalWidth, height);
    }

【讨论】:

  • 很好,我在偷这个!
  • @safetyOtter 嘿,很高兴它可以使我以外的人受益。 :)
  • 呵呵我有一个列表视图,每次看到它都会让我烦恼,这将使我的屏幕爬行更少。谢谢!
  • @safetyOtter 没问题,感谢您引导我朝着正确的方向前进。
  • 我发现这种方法的两个问题可能是您描述的大小错误的原因: 1. 此实现采用等宽字体。对于可变宽度字体,“最长字符串 = 最宽标签”的假设不成立。 2. FormattedText 类有一个 WidthIncludingTrailingWhitespace 属性,您可能想用它来计算网格宽度。
【解决方案2】:

将处理程序附加到列表视图的 sizechanged 事件并在其中设置宽度。我过去做过类似的事情,你可以修改它以满足你的需要

    private void dataView_SizeChanged(object sender, SizeChangedEventArgs e)
    {
        ListView listView = sender as ListView;
        if (listView == null) return;
        GridView gView = listView.View as GridView;
        if (gView == null) return;
        gView.Columns[0].Width = 
               YourObjectHere.OrderByDescending(s => s.Length).First().Length * 11;
        gView.Columns[1].Width = 100; // width of your icon
    }

【讨论】:

  • 11 代表什么?
  • 我写的时候使用的字体中 W 的宽度(以像素为单位)。这是一种基于字符串长度来估计字符串宽度(以像素为单位)的 hacky 方法,它总是有点太宽,但永远不会太短。可能有更好的方法,但我不知道。
  • 嗯...静态宽度。顺便说一句,我很惊讶它完全是为你编译的。 Length 是一个属性,而不是一个方法。
  • 哎呀,从记忆中重写,将编辑:P 希望您能找到更好的解决方案并发布!
  • 好吧,我正在测试你的代码,它确实会影响宽度;但是,数字有点偏。 6 的修饰符给了我一个“足够接近”的宽度,但我不喜欢静态宽度修饰符,因为它施加了限制。我们可能以不同的分辨率运行,因此每个用户的分辨率会有所不同......我需要一些更动态的东西。
【解决方案3】:

@B.K. 给出的答案。让我完成了 90% 的工作(我不得不调整它以使用 ListBox 控件)。然而,正如我对他的解决方案所评论的那样,它没有考虑可变宽度字体。

下面是一个方法,给定一个字符串集合,当给定的字体以给定的点大小呈现时,它将找出哪个最宽。

private FormattedText getLongestFormattedString(IEnumerable<string> list, Typeface typeface, double size)
{
    FormattedText longest = null;

    foreach(string item in list)
    {
        var renderedText = new FormattedText(filter.Filter.Name,
                               System.Globalization.CultureInfo.CurrentCulture,
                               FlowDirection.LeftToRight, typeface,
                               size, Brushes.Black);
        longest = (longest == null || renderedText.WidthIncludingTrailingWhitespace > longest.WidthIncludingTrailingWhitespace) ? renderedText : longest;
    }

    return longest;
}

Loaded 处理程序中,您可以这样使用它:

void listView_Loaded(object sender, RoutedEventArgs e)
{
    var listView = sender as System.Windows.Controls.ListView;
    if(listView == null) return;

    var gridView = listView.View as GridView;
    if(gridView == null) return;

    // this assumes the items are coming from data binding.
    // generating an iteration of strings from your actual data source
    // is left as an exercise to the reader
    var dataList = listView.Items.SourceCollection as List<string>;
    if(dataList == null) return;

    var typeFace = new Typeface(listView.FontFamily, listView.FontStyle,
                                    listView.FontWeight, listView.FontStretch);
    var text = getLongestFormattedString(dataList, typeFace, listView.FontSize);

    gridView.Columns[0].Width = text.WidthIncludingTrailingWhitespace;
}

【讨论】:

  • 干得好,内森。
【解决方案4】:

我知道这是一篇旧帖子,但对于任何遇到它的人来说,如果他们从他们的 ViewModel 绑定到 ListView ItemsSource 属性,也许 MultiValue 转换器可以提供帮助。我知道还有其他解决问题的方法,但我喜欢使用这种方法可以保持的控制水平,它同样适用于ListViewGridViewColumn

我还喜欢这样一个事实,即使用转换器,我的 ICollection 对象可以包含字符串/原始类型或可以通过反射访问其属性的类对象。

简而言之:传入转换器的值数组是创建字体和实例化FormattedText 对象、UIElement 的最小和最大宽度以及绑定的ICollection 对象本身所需的值。参数参数实际上是一个数组,其中包含一个类对象的属性名称(如果不需要,则为 null)和一个 Thickness 对象,用于为计算的宽度添加填充。 Thickness 对象允许我满足构成我的ListView 一部分的任何填充/边距设计。

注意:ListView ItemsPane 上的默认填充是 {12,0,12,0} 和 GridViewColumn {6,0,6,0}。这可能是 BK 提到的 12-20 像素错误的原因。

转换器本身如下所示:

/// <summary>
/// Iterates a collection of items to calculate the maximum text width of those items.
/// Items can either be primitive types and strings or objects with a property that is
/// a primitive type or string.
/// </summary>
public sealed class ItemsToWidthConverter : IMultiValueConverter
{
    //Constants for array indexes.
    private const int FONTFAMILY_ID = 0;
    private const int FONTSTYLE_ID = 1;
    private const int FONTWEIGHT_ID = 2;
    private const int FONTSTRETCH_ID = 3;
    private const int FONTSIZE_ID = 4;
    private const int FOREGROUND_ID = 5;
    private const int MINWIDTH_ID = 6;
    private const int MAXWIDTH_ID = 7;
    private const int ICOLLECTION_ID = 8;
    private const int PARAMETERPROPERTY_ID = 0;
    private const int PARAMETERGAP_ID = 1;

    /// <summary>
    /// Converts collection items to a width.
    /// Parameter[0] is the property name of an object. If no property name is needed, pass in null.
    /// Parameter[1] is the padding to be added to the calculated width. If no padding is needed, pass in a Thickness of 0.
    /// Note: a ListViewItem has default padding of {12,0,12,0}. A GridViewColumn has default padding of {6,0,6,0}.
    /// </summary>
    /// <param name="values">Array of 9 objects {FontFamily, FontStyle, FontWeight, FontStretch, double [FontSize], Brush, double [MinWidth], double [MaxWidth], ICollection}</param>
    /// <param name="targetType">Double</param>
    /// <param name="parameter">Array of 2 objects {string [Property Name], Thickness}</param>
    /// <param name="culture">Desired CultureInfo</param>
    /// <returns>Width of widest item including padding or Nan if none is calculated.</returns>
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        // Throw error if passed parameters are incorrect.
        if (values.Length != 9) throw new Exception("Incorrect number of items passed in 'values'.");
        if (!(parameter.GetType().IsArray)) throw new Exception("'Parameter' must be an array.");
        var prm = (object[])parameter;
        if (prm.Length !=2) throw new Exception("Incorrect number of items passed in 'parameter'.");
        if (prm[PARAMETERPROPERTY_ID] != null && !(prm[PARAMETERPROPERTY_ID] is string property)) throw new Exception("'Parameter['" + PARAMETERPROPERTY_ID + "]' is neither null nor of type 'string'.");
        if (!(prm[PARAMETERGAP_ID] is Thickness margin)) throw new Exception("'Parameter['" + PARAMETERGAP_ID + "]' is not of type 'Thickness'.");
        if (values[ICOLLECTION_ID] == null) return double.NaN;
        if (!(values[FONTFAMILY_ID] is FontFamily family)) throw new Exception("'Value['" + FONTFAMILY_ID + "]' is not of type 'FontFamily'.");
        if (!(values[FONTSTYLE_ID] is FontStyle style)) throw new Exception("'Value['" + FONTSTYLE_ID + "]' is not of type 'FontStyle'.");
        if (!(values[FONTWEIGHT_ID] is FontWeight weight)) throw new Exception("'Value['" + FONTWEIGHT_ID + "]' is not of type 'FontWeight'.");
        if (!(values[FONTSTRETCH_ID] is FontStretch stretch)) throw new Exception("'Value['" + FONTSTRETCH_ID + "]' is not of type 'FontStretch'.");
        if (!(values[FONTSIZE_ID] is double size)) throw new Exception("'Value['" + FONTSIZE_ID + "]' is not of type 'double'.");
        if (!(values[FOREGROUND_ID] is Brush foreground)) throw new Exception("'Value['" + FOREGROUND_ID + "]' is not of type 'Brush'.");
        if (!(values[MINWIDTH_ID] is double minWidth)) throw new Exception("'Value['" + MINWIDTH_ID + "]' is not of type 'double'.");
        if (!(values[MAXWIDTH_ID] is double maxWidth)) throw new Exception("'Value['" + MAXWIDTH_ID + "]' is not of type 'double'.");
        if (!(values[ICOLLECTION_ID] is ICollection col)) throw new Exception("'Value['" + ICOLLECTION_ID + "]' is not of type 'ICollection'.");

        // Conver font properties to a typeface.
        var typeFace = new Typeface(family, style, weight, stretch);

        // Initialise the max_width variable at 0.
        var widest = 0.0;
        foreach (var item in col)
        {
            // If property parameter is null, assume the ICollection contains primitives or strings.
            if (prm[PARAMETERPROPERTY_ID] == null)
            {
                if (item.GetType().IsPrimitive || item is string)
                {
                    var text = new FormattedText(item.ToString(),
                                                 culture,
                                                 FlowDirection.LeftToRight,
                                                 typeFace,
                                                 size,
                                                 foreground,
                                                 null,
                                                 TextFormattingMode.Ideal);

                    if (text.WidthIncludingTrailingWhitespace > widest)
                        widest = text.WidthIncludingTrailingWhitespace;
                }
            }
            else
            // Property parameter contains a string, so assume ICollection is an object
            // and use reflection to get property value.
            {
                if (item.GetType().GetProperty(prm[PARAMETERPROPERTY_ID].ToString()) != null)
                {
                    var propertyValue = item.GetType().GetProperty(prm[PARAMETERPROPERTY_ID].ToString()).GetValue(item);
                    if (propertyValue.GetType().IsPrimitive || propertyValue is string)
                    {
                        var text = new FormattedText(propertyValue.ToString(),
                                                     culture,
                                                     FlowDirection.LeftToRight,
                                                     typeFace,
                                                     size,
                                                     foreground,
                                                     null,
                                                     TextFormattingMode.Display);

                        if (text.WidthIncludingTrailingWhitespace > widest)
                            widest = text.WidthIncludingTrailingWhitespace;
                    }
                }
            }
        }

        // If no width could be calculated, return Nan which sets the width to 'Automatic'
        if (widest == 0) return double.NaN;

        // Add the left and right thickness values to the calculated width and
        // check result is within min and max values.
        {
            widest += ((Thickness)prm[PARAMETERGAP_ID]).Left + ((Thickness)prm[PARAMETERGAP_ID]).Right;
            if (widest < minWidth || widest > maxWidth) return double.NaN;
            return widest;
        }
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

它可以在 XAML 中实现,如下所示。

例如 1 - 模板中的简单 ListView:

<ListView ItemsSource="{Binding MyStrings}">
    <ListView.Width>
        <MultiBinding Converter="{StaticResource ItemsToWidthConverter}">
            <MultiBinding.ConverterParameter>
                <x:Array Type="sys:Object">
                    <x:Null />
                    <Thickness>12,0,12,0</Thickness>
                </x:Array>
            </MultiBinding.ConverterParameter>
            <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="FontFamily" />
            <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="FontStyle" />
            <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="FontWeight" />
            <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="FontStretch" />
            <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="FontSize" />
            <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Foreground" />
            <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="MinWidth" />
            <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="MaxWidth" />
            <Binding Path="MyStrings" />
        </MultiBinding>
    </ListView.Width>
</ListView>

例如 2 - ListView 和 GridView

<ListView ItemsSource="{Binding Employees}">
    <ListView.View>
        <GridView>
            <GridViewColumn DisplayMemberBinding="{Binding EmployeeName}">
                <GridViewColumn.Width>
                    <MultiBinding Converter="{StaticResource ItemsToWidthConverter}">
                        <MultiBinding.ConverterParameter>
                            <x:Array Type="sys:Object">
                                <sys:String>EmployeeName</sys:String>
                                <Thickness>6,0,6,0</Thickness>
                            </x:Array>
                        </MultiBinding.ConverterParameter>
                        <Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=ListView}" Path="FontFamily" />
                        <Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=ListView}" Path="FontStyle" />
                        <Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=ListView}" Path="FontWeight" />
                        <Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=ListView}" Path="FontStretch" />
                        <Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=ListView}" Path="FontSize" />
                        <Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=ListView}" Path="Foreground" />
                        <Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=ListView}" Path="MinWidth" />
                        <Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=ListView}" Path="MaxWidth" />
                        <Binding Path="Employees" />
                    </MultiBinding>
                </GridViewColumn.Width>
            </GridViewColumn>
        </GridView>
    </ListView.View>
</ListView>

当然,还有很多其他的绑定变体/组合。

这种方法的唯一问题是,如果 ObservableCollection 发生更改(例如通过添加项目),则不会通知 MultiBinding,因此可能需要额外的通知代码。我没有遇到过这个问题,因为我主要将此技术用于替换整个集合的条件列表(因此会触发 OnPropertyChanged,它由 MultiBinding 使用),但 SO 给出了如何编写此类通知的示例。

【讨论】:

    猜你喜欢
    • 2018-08-10
    • 2020-12-25
    • 1970-01-01
    • 2018-05-22
    • 2017-02-21
    • 1970-01-01
    • 1970-01-01
    • 2018-10-19
    • 2017-11-14
    相关资源
    最近更新 更多