B.K. B.K. - 3 months ago 16
C# Question

Setting ListViewColumn width to the widest item regardless if it's in the initial render?

So, I'm having an interesting problem. I have a

ListView
and two columns:

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


No matter if I set first column's
width
to
auto
or leave it default as it is shown above, the initial width will be set to the widest item within the rendered window. So, if I have my window height set at 400 and the next element is wider than what's being rendered, it won't account for its width. Instead, it'll use the width of the widest rendered item. If I set my height to say... 410, it'll take the width of that next element into consideration. However, with several hundred items, I can't be using height for that purpose. Is there a way to set that column's width as the widest element in it, regardless if it's in the initial render?

Note that I don't want to use
ScrollViewer.CanContentScroll="False"
solution from a related SO question. That would have a huge performance consequence with a very large list.

Answer

This answer is based on the discussion safetyOtter and I had earlier. There was a problem of not having a dynamic accounting for the text size based on its rendering per user's resolution. Another problem with his solution was that the event would fire off every time the size would change. Therefore, I restricted it to the initial loading event, which occurs prior to rendering. Here's what I came up with:

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

There was a slight width error that cut off the last two characters of the string. I measured it to be 12 pixels. A buffer could be added to the column width of somewhere between 12-20 pixels (+ 12.0f) to account for that error. It appears it's pretty common and I'll need to do some more research.

Other methods that I've tried:

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

That one had approximately 14 pixel error on the measurement. The problem with that method and the one that I'll show below is that both of them rely on System.Drawing.SystemFonts.DefaultFont, due to the fact that errors are too great if the font is retrieved from the control. This reliance on the system font is very restrictive if the control is using something different.

Last method that I tried (provides too high of an error on measurement):

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

I'm pretty content with the first method, and I haven't been able to find anything that does a perfect text measurement. So, having just a few characters cut off and fixing it with a buffer zone is not that bad.

EDIT:

Here's another method that I found @ WPF equivalent to TextRenderer It provided a ~14 pixel error. So, the first method is the best performer thus far.

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