programmatically setting DataGridView column widths

  • Thread starter Thread starter michael sorens
  • Start date Start date
M

michael sorens

I am wondering what is considered a best practice for setting column widths
to fit the data for a DataGridView (.Net 2.0) that was created on-the-fly
from an arbitrary query. What I would probably do is scan the width of each
column for the first couple hundred or so rows and find the largest. (But how
does one convert character counts to field widths in pixels?)

More generally, however, is there a better approach to accomplish this?
 
There are several ways to do it and it depends on how accurate you need to
be as well as whether or not your data will ever be localized to non-latin
character sets.

You can use Graphics.MeasureString() to get the string dimensions. You'll
need to get the font from the styles for the grid... This is fine for latin
and most charactersets but the accuracy is pretty questionable.

Calling into the Windows API's GetTextExtent, GetTextExtentPoint, etc will
provide more accurate dimensions, for any character set. They're also quite
a bit slower than MeasureString.

Unless you have a fixed-width characterset, in which case, the font
information will provide character widths and it's a simple matter of
multiplication.

If it's not fixed width, you'll need to sample the actual strings in the
cells and determine their width individually and determine which strings are
widest based on that.

For example, in a variable-width font, the string on the top will be wider
than the string on the bottom, but they contain the same number of
characters:

WWWWWWWWWW
iiiiiiiiii

You should be able to sample a couple hundred rows of a grid with, say, 20
columns, in less than a second, assuming access to the cell data is pretty
quick, and I believe it is.



The performance hit shouldn't be too bad for just a few hundred rows. .
 
Hi Michael,

I suggest that you set the AutoSizeColumnsMode property of the DataGridView
to DisplayedCellsExceptHeader to get what you want.

When the AutoSizeColumnsMode property of a DataGridView is set to
DisplayedCellsExceptHeader, the column widths adjust to fit the contents of
all cells in the columns that are in rows currently displayed onscreen,
excluding header cells. So you needn't adjust the width of each column in
the DataGridView programmatically.

For more information about content-based automatic sizing, please refer to
the following MSDN document:
http://msdn2.microsoft.com/en-us/library/74b2wakt.aspx

Hope this helps.
If you have any question, please feel free to let me know.

Sincerely,
Linda Liu
Microsoft Online Community Support

==================================================
Get notification to my posts through email? Please refer to
http://msdn.microsoft.com/subscriptions/managednewsgroups/default.aspx#notif
ications.

Note: The MSDN Managed Newsgroup support offering is for non-urgent issues
where an initial response from the community or a Microsoft Support
Engineer within 1 business day is acceptable. Please note that each follow
up response may take approximately 2 business days as the support
professional working with you may need further investigation to reach the
most efficient resolution. The offering is not appropriate for situations
that require urgent, real-time or phone-based interactions or complex
project analysis and dump analysis issues. Issues of this nature are best
handled working with a dedicated Microsoft Support Engineer by contacting
Microsoft Customer Support Services (CSS) at
http://msdn.microsoft.com/subscriptions/support/default.aspx.
==================================================

This posting is provided "AS IS" with no warranties, and confers no rights.
 
When the AutoSizeColumnsMode property of a DataGridView is set to
DisplayedCellsExceptHeader, the column widths adjust to fit the contents
of
all cells in the columns that are in rows currently displayed onscreen,

It is not so.
For unknown reason
DataGridViewAutoSizeColumnsMode.DisplayedCellsExceptHeader mode scans all
rows in grid.
So using this is not reasonable for large grids.
Aby idea how to fix this ?

Andrus.


Steps to reproduce:

1. Run code below
2. Double click in grid header in separation line between columns 1 ja 2

Observed:

CPU usage goes to 100% for a long time.

Andrus.

// sample from Chris Sells book.
using System;
using System.Windows.Forms;
using System.Collections.Generic;

class test {

[STAThreadAttribute()]

public static void Main() {
Application.Run(new VirtualModeForm());
}
}

class VirtualModeForm : Form {

private List<DataObject> m_Data = new List<DataObject>();
private List<bool> m_Visited = new List<bool>();
DataGridView m_Grid = new DataGridView();

public VirtualModeForm() {
Controls.Add(m_Grid);
m_Grid.CellValueNeeded += OnCellValueNeeded;
InitData();
InitGrid();
}

private void InitData() {
for (int i = 0; i < 1000001 + 1; i++) {
m_Visited.Add(false);
DataObject obj = new DataObject();
obj.Id = i;
obj.Val = 2 * i;
m_Data.Add(obj);
}

}

private void InitGrid() {

m_Grid.Dock = DockStyle.Fill;
m_Grid.AutoResizeColumns(DataGridViewAutoSizeColumnsMode.DisplayedCellsExceptHeader);
m_Grid.VirtualMode = true;
m_Grid.ReadOnly = true;
m_Grid.ColumnCount = 3;
m_Grid.Rows.Add();
m_Grid.Rows.AddCopies(0, 1000000);
}

private void OnCellValueNeeded(object sender,

DataGridViewCellValueEventArgs e) {
m_Visited[e.RowIndex] = true;
if (e.ColumnIndex == 0) {
e.Value = m_Data[e.RowIndex].Id;
} else if (e.ColumnIndex == 1) {
e.Value = m_Data[e.RowIndex].Val;
} else if (e.ColumnIndex == 2) {

Random rand = new Random();
e.Value = rand.Next();
}
}
}

public class DataObject {
private int m_Id;
private int m_Val;
public int Val {
get { return m_Val; }
set { m_Val = value; }
}

public int Id {
get { return m_Id; }
set { m_Id = value; }
}
}
 
Linda's suggestion solved my issue, in that upon loading a result set the
displayed columns adjusted to the data automatically.

I am curious about Andrus' point also, however. When I ran the sample code
it took about 30 seconds to resize the column after double-clicking the
column header separator, as he indicated. Note that this is a somewhat
different case--it allows the user to resize columns to the data on demand
rather than automatically.
 
Michael,
I am curious about Andrus' point also, however. When I ran the sample code
it took about 30 seconds to resize the column after double-clicking the
column header separator, as he indicated. Note that this is a somewhat
different case--it allows the user to resize columns to the data on demand
rather than automatically.

Nicholas Paldino wrote in other thread, that immediate resize should occur
and requested code to reproduce.
I created this code to reproduce the issue.

This sample demonstates that for Virtual DataGridView containing large
number of rows, automatic column resize based on visible rows is not
possible.

Is this DataGridView bug ?

Andrus.
 
After I implemented this in my code, it raised another question: Could I
somehow limit all columns to a given maximum? With the setting of
AutoSizeColumnsMode property of the DataGridView
to DisplayedCellsExceptHeader, I can get some very wide columns for text
fields; I would like to limit anything to, say, no more than 100 characters
wide.
 
Hi Michael,

Thank you for your reply!

The DataGridViewColumn class has provided a property "MinimumWidth", which
gets or sets the minimum width, in pixels, of the column. Unfortunately,
the DataGridViewColumn class hasn't provided a "MaximumWidth" property.

Alternatively, we can handle the ColumnWidthChanged event of the
DataGridView. If the width of the column is bigger than the expected
maximum value, we set the AutoSizeMode property of the DataGridViewColumn
to None firstly and then set the Width property of the column to the
maximum value.

If the maximum lenght of the cell values under a DataGridViewColumn
decreases to be smaller than the expected maximum value, this column should
shrink. To get this behavior, we can subscribe the CellValueChanged event
of the DataGridView and set the AutoSizeMode property of the
DataGridViewColumn to NotSet in the event handler.

The following is a sample, which assums that the maximum value of the
column width is 200.

public Form1()
{
public Form1()
{
InitializeComponent();
this.dataGridView1.ColumnWidthChanged += new
DataGridViewColumnEventHandler(dataGridView1_ColumnWidthChanged);
this.dataGridView1.CellValueChanged += new
DataGridViewCellEventHandler(dataGridView1_CellValueChanged);
}

void dataGridView1_CellValueChanged(object sender,
DataGridViewCellEventArgs e)
{
if (this.dataGridView1.AutoSizeColumnsMode ==
DataGridViewAutoSizeColumnsMode.DisplayedCellsExceptHeader &&
this.dataGridView1.Columns[e.ColumnIndex].AutoSizeMode ==
DataGridViewAutoSizeColumnMode.None)
{
this.dataGridView1.Columns[e.ColumnIndex].AutoSizeMode =
DataGridViewAutoSizeColumnMode.NotSet;
}
}

void dataGridView1_ColumnWidthChanged(object sender,
DataGridViewColumnEventArgs e)
{
if (e.Column.Width > 200 &&
this.dataGridView1.AutoSizeColumnsMode ==
DataGridViewAutoSizeColumnsMode.DisplayedCellsExceptHeader &&
e.Column.AutoSizeMode == DataGridViewAutoSizeColumnMode.NotSet)
{
e.Column.AutoSizeMode =
DataGridViewAutoSizeColumnMode.None;
this.dataGridView1.ColumnWidthChanged -=new
DataGridViewColumnEventHandler(dataGridView1_ColumnWidthChanged);
e.Column.Width = 200;
this.dataGridView1.ColumnWidthChanged += new
DataGridViewColumnEventHandler(dataGridView1_ColumnWidthChanged);
}
}
}

Hope this helps.
If you have any question, please feel free to let me know.

Sincerely,
Linda Liu
Microsoft Online Community Support
 
(1) I think in my situation this could be simplified because I only need to
size the columns when I load the DataGridView with data, though this could
happen many times during a given run. Am I correct that I could skip the
CellValueChanged handler and just use the ColumnWidthChanged handler you
propose? I hooked it up that way and it appears to work, but I just want to
make sure I am not missing anything.
(2) Could I take this one step farther? I want the columns to autosize up to
a given maximum as in (1), but then I would like the user to be able to
simply grab the column separator to resize columns manually if desired. Is
there a simple way to handle that?
 
Hi Michael,

Thank you for your reply!

(1) In the workaround I gave you in my previous reply, once a
DataGridViewColumn's width reaches a given maximum, this column's
AutoSizeMode property is set to None, which means this column will not
autosize with its cell content unless its AutoSizeMode property is set to
NotSet again.
I think in my situation this could be simplified because I only need to size the
columns when I load the DataGridView with data, though this could happen
many times during a given run.

If the data source bound to the DataGridView remains the same and you only
load data into the data source many times, you do need to handle the
CellValueChanged event of the DataGridView in case that the actually
required column widths become shorter than the given maximum value in the
later data loading.

(2) The workaround I provided in my previous reply has already met this
requirement. Once a column autosizes up to a given maximum, its
AutoSizeMode property is set to None so that the user can grab the column
separator to resize the column manually as he/she likes.

Hope this helps.
If you have any question, please feel free to let me know.

Sincerely,
Linda Liu
Microsoft Online Community Support
 
(1) I am re-assigning the data source each time I load, so I understand from
your comments that omitting the CellValueChanged handler will work in this
case.

(2) Your suggested solution does, indeed, allow the "maxed out" columns to
be resized manually--but I would like all other columns to be resizable
manually as well.

I tried a simple approach of just setting the AutoSizeMode property of each
column to None after the table is loaded (i.e. after
dataAdapter.Fill(dataTable);) but then all columns just have the default
width of 100. I think that happened because the
DataGridViewAutoSizeColumnsMode setting of DisplayedCellsExceptHeader never
had a chance to be recognized since the DataGridView had not yet been
rendered.

So any thoughts on where the appropriate place to set AutoSizeMode property
of *all* columns to None?
 
Hi Michael,

Thank you for your reply!
So any thoughts on where the appropriate place to set AutoSizeMode
property of *all* columns to None?

When we set the DataSource property of a DataGridView to a data source, the
columns in the DataGridView are populated automatically and the
ColumnWidthChanged event of the DataGridView will be raised for several
times.

At first, when a DataGridViewColumn is created, its Width property has a
value of 5 and the ColumnWidthChanged event is fired.

Next, if the DataGridView is set to autosize, the column is resized
according to the cell content and the ColumnWidthChanged event is fired
again.

So in the ColumnWidthChanged event handler, we can set the AutoSizeMode
property of the passed column to None only when the column width is not 5.

The following is a sample to get what you want.

void dataGridView1_ColumnWidthChanged(object sender,
DataGridViewColumnEventArgs e)
{
if (e.Column.Width == 5)
return;

if (this.dataGridView1.AutoSizeColumnsMode ==
DataGridViewAutoSizeColumnsMode.DisplayedCellsExceptHeader
&& e.Column.AutoSizeMode ==
DataGridViewAutoSizeColumnMode.NotSet)
{
// write down the value of the column
int width = e.Column.Width;
this.dataGridView1.ColumnWidthChanged -= new
DataGridViewColumnEventHandler(dataGridView1_ColumnWidthChanged);
// when the AutoSizeMode property is set to None, the
column width is set to its default value 100
e.Column.AutoSizeMode = DataGridViewAutoSizeColumnMode.None;

if (width > 200)
{
e.Column.Width = 200;
}
else
{
e.Column.Width = width;
}
this.dataGridView1.ColumnWidthChanged += new
DataGridViewColumnEventHandler(dataGridView1_ColumnWidthChanged);
}
}

Hope this helps.
Please try my suggestion and let me know the result.

Sincerely,
Linda Liu
Microsoft Online Community Support
 
Your suggested approach works, in that it limits wide columns while setting
all columns to be user-adjustable after the load, but the column widths are
determined from looking at just the first row. That led me back to thinking
of my initial concept of scanning the first few hundred rows or some such,
but then I realized that your code provided the clue to the silly error that
I made in my last post: I mentioned that I tried setting the AutoSizeMode
property of each
column to None after the table is loaded and got all columns reset to 100.
All I needed to do, as your code shows, was remember the desired width and
restore that after changing AutoSizeMode. Here, then, are the basics of my
final code:

dataAdapter.Fill(dataTable);
dgv.DataSource = dataTable;
foreach (DataGridViewColumn column in dgv.Columns)
{
int tentativeWidth = column.Width;
column.AutoSizeMode = DataGridViewAutoSizeColumnMode.None;
column.Width = Math.Min(tentativeWidth,
Properties.Settings.Default.MaxColumnWidth);
}
 
Hi Michael,

I'm glad to hear that you have solved the problem and thank you for sharing
the solultion with us!

The solution you provide is very clear and it will definitely benefit all
of us!

If you have any other question in the future, please don't hesitate to
contact us. It's always our pleasure to be of assistance!

Sincerely,
Linda Liu
Microsoft Online Community Support
 
Back
Top