ListView, ColumnHeader.Width when using multiple Fonts

G

Guest

Here is an issue I've been struggling with for a day now, I wanted to share
what happened with the group and find out if I have experienced (and worked
around) a bug, or if there is something that I missed. I am using Visual
Studio .NET 2003 with C#.

The goal: I need to make sure that in a ListView control, the columns are
drawn wide enough to display the text of the ListViewItems. Here's the
catch: ListViewItem.Font may not be the same as the ListView.Font.

Problem #1: ColumnHeader.Width does not take Font into account.
Should be easy... just use (ColumnHeader.Width = -1), right? Sorry, no.
Using the Width = -1 shortcut does not appear to take into account the fact
that one of the ListViewSubItems in that column may be using a font that is
wider than the ListView.Font.

Workaround #1:
I used ListView.CreateGraphics to create a graphics object, and then used
Graphics.MeasureString() like so:

// Assume listView.Font is "Microsoft Sans Serif", 8.25
// Assume listViewItem.Font is "Lucida Console", 8.0
Graphics g = listView.CreateGraphics();
foreach (ColumnHeader ch in listView.Columns)
{
ListViewItem.ListViewSubItem listViewSubItem =
listViewItem.SubItems[columnHeader.Index];
int neededColumnWidth = g.MeasureString(listViewSubItem.Text,
listViewSubItem.Font).ToSize().Width;
if (ch.Width < neededColumnWidth)
{
// Make the column wider
ch.Width = neededColumnWidth;
}
}

Problem #2: In the above example, the listViewSubItem.Font returns the same
value as listView.Font! I would have expected that listViewSubItems would
inherit their font from the parent ListViewItem by default.

Workaround #2: It is fortunate that the Font is identical for each element
of ListViewItem.SubItems, so I just substituted "listViewItem.Font" for
"subListViewItem.Font".

// Assume listView.Font is "Microsoft Sans Serif", 8.25
// Assume listViewItem.Font is "Lucida Console", 8.0
Graphics g = listView.CreateGraphics();
foreach (ColumnHeader ch in listView.Columns)
{
ListViewItem.ListViewSubItem listViewSubItem =
listViewItem.SubItems[columnHeader.Index];
int neededColumnWidth = g.MeasureString(listViewSubItem.Text,
listViewItem.Font).ToSize().Width;
if (ch.Width < neededColumnWidth)
{
// Make the column wider
ch.Width = neededColumnWidth;
}
}

Problem #3:
With the above code, I can see that the columns are indeed resizing to
become wider---but not quite wide enough to display all the text.

Problem #4:
Even when all the ListViewItems are using the same font as the parent
ListView, using (Width = -1) appears to jam the text against the right side
of the column---there is no padding on the right side of the column like
there is on the left.

After doing some research, I believe that this is related to some hardcoded
padding versions of .NET, similar to this bug which says, "Internally, the
Width of a ColumnHeader is padded with some predetermined constant when it is
changed programmatically. This padding is not in effect if the Width of the
ColumnHeader is changed through the user interface."

http://support.microsoft.com/default.aspx?scid=kb;en-us;179988

This article implies that the hardcoded padding values are 29 for get_Width
and 30 for set_Width.

Final Workaround:
Whenever adding or modifying a ListViewItem or one of its SubItems, I also
call the following method instead of simply using (columnHeader.Width = -1;),
which appears to do the trick

/// <summary>
/// Ensures that each column in the list-view is wide enough to the given
text written
/// in the given font. Normally you can use ColumnHeader.Width = -1 (or
-2), but in
/// this application there are several ListView controls that use a
different font for
/// its items and headers. ColumnHeader.Width autosizing appears to
calculate the width
/// for each ListViewSubItem using the ListView.Font instead of the font of
the widest ListView.Item.
/// This method is not thread-safe.
/// </summary>
/// <param name="ch">The column header to be widened. It must have a valid
parent ListView.</param>
/// <param name="subitem">The subitem whose width is to be measured.</param>
private void SetColumnHeaderWidth(ColumnHeader ch, string stringToDisplay,
Font fontOfString)
{
if (null == ch)
{
throw new ArgumentNullException("ch");
}
ListView lv = ch.ListView;
if (null == lv)
{
throw new ArgumentException("The ColumnHeader must have a parent
ListView.", "ch");
}

// This is used for measuring items for the arguments listview
// to make sure the columns are wide enough to see their data.
Graphics g = lv.CreateGraphics();
try
{
int subitemWidth = g.MeasureString(stringToDisplay,
fontOfString).ToSize().Width;
int defaultStringWidth = g.MeasureString(stringToDisplay,
lv.Font).ToSize().Width;
// HACK 10-17-06 SEC: When setting the column header width
programatically,
// it appears to set it just a hair too small for the text you specified.
// I believe that this is related to some hardcoded padding where in
older
// versions of .NET, for example with this bug:
// http://support.microsoft.com/default.aspx?scid=kb;en-us;179988
// "Internally, the Width of a ColumnHeader is padded with some
predetermined
// constant when it is changed programmatically. This padding is not in
effect
// if the Width of the ColumnHeader is changed through the user
interface."
// According to the aforementioned article, these hardcoded values are
// 29 for get and 30 for set.
if (ch.Width < subitemWidth + 29)
{
ch.Width = subitemWidth + 30;
}
}
finally
{
// We *must* manually dispose of the graphics now that we are done with
it,
// per MSDN documentation for Control.CreateGraphics method:
// "The returned Graphics object must be disposed through a call to
// its Dispose method when it is no longer needed."
if (null != g)
{
g.Dispose();
}
}
}

Since I did say that this post is a question, my questions are:
1. Has anyone else encountered these issues and did you find a better way to
resolve them?
2. Are any of these problems known bugs? If so, have they been resolved in
more modern releases of Visual Studio .NET/Visual C# ?
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top