fix(desktop): size grid columns before they are first painted

Grids opened at their designer widths and jumped a moment later, because
GridStyle waited for the first paint with rows before measuring them.
Columns now measure their header and first 50 rows directly: at handle
creation when the rows are already there, otherwise in a call posted when
rows arrive, which runs after the filling code and before the repaint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-26 09:46:42 -04:00
1 parent 86b0370a8f
commit 2e17dd7bc4
2 files changed
+114 -15

No files matched your search

@@ -37,6 +37,61 @@ public class GridStyleTests
Assert.Equal(77, longColumn.Width);
});
[Fact]
public void RowsFilledBeforeTheFormOpensArePaintedAtTheirFinalWidths() => ArchUnitsTests.RunSta(() =>
{
using var form = new Form { Width = 1200, Height = 400, ShowInTaskbar = false };
var grid = GridStyle.Apply(new DataGridView { Dock = DockStyle.Fill });
var column = new DataGridViewTextBoxColumn { HeaderText = "Name", Width = 40 };
grid.Columns.Add(column);
for (var i = 0; i < 20; i++)
grid.Rows.Add(i == 10 ? "A fairly long drawing name" : "A");
form.Controls.Add(grid);
var firstPaintWidth = WidthAtFirstPaint(grid, column);
form.Show();
Application.DoEvents();
Pump(grid);
Assert.True(column.Width > 40);
Assert.Equal(column.Width, firstPaintWidth());
});
[Fact]
public void RowsAddedAfterTheFormOpensArePaintedAtTheirFinalWidths() => ArchUnitsTests.RunSta(() =>
{
using var form = new Form { Width = 1200, Height = 400, ShowInTaskbar = false };
var grid = GridStyle.Apply(new DataGridView { Dock = DockStyle.Fill, AllowUserToAddRows = false });
var column = new DataGridViewTextBoxColumn { HeaderText = "Name", Width = 40 };
grid.Columns.Add(column);
form.Controls.Add(grid);
form.Show();
Pump(grid);
// One row at a time, as most grids fill: the long name is not the first row.
grid.Rows.Add("A");
grid.Rows.Add("A fairly long drawing name");
var firstPaintWidth = WidthAtFirstPaint(grid, column);
Application.DoEvents();
Pump(grid);
Assert.True(column.Width > 40);
Assert.Equal(column.Width, firstPaintWidth());
});
private static Func<int> WidthAtFirstPaint(DataGridView grid, DataGridViewColumn column)
{
var width = -1;
void OnPaint(object sender, PaintEventArgs e)
{
grid.Paint -= OnPaint;
width = column.Width;
}
grid.Paint += OnPaint;
return () => width;
}
private static void Pump(Control control)
{
for (var i = 0; i < 5; i++)
+59 -15
View File
@@ -1,3 +1,4 @@
using System;
using System.Drawing;
using System.Windows.Forms;
@@ -6,13 +7,14 @@ namespace OpenNest.Controls;
/// <summary>
/// The shared look of every <see cref="DataGridView"/> in the desktop app, taken from the
/// database nest browser: flat light headers, faint grid lines, a soft blue selection and
/// fixed row heights. Columns size once to their first content and are then the user's to
/// resize. Other behaviour (editing, selection mode) stays with each grid.
/// fixed row heights. Columns size once to their first content, before it is first painted,
/// and are then the user's to resize. Other behaviour (editing, selection mode) stays with each grid.
/// </summary>
public static class GridStyle
{
public const int RowHeight = 22;
private const int MaxInitialColumnWidth = 400;
private const int MeasuredRowCount = 50;
private static readonly Color HeaderBackColor = Color.FromArgb(245, 245, 245);
private static readonly Color HeaderForeColor = Color.FromArgb(90, 90, 90);
@@ -51,30 +53,72 @@ public static class GridStyle
}
/// <summary>
/// Widens each column to fit its header and the rows first shown, once, then leaves the
/// widths to the user. Columns never stretch to fill the grid.
/// Widens each column to fit its header and its first rows, once, then leaves the widths
/// to the user. Columns never stretch to fill the grid.
/// </summary>
private static void SizeColumnsOnceToContent(DataGridView grid)
{
// The first paint with rows is the first time they are on screen to be measured. The
// Sizing measures the rows rather than waiting for them on screen, so the grid is first
// painted at its final widths. Rows already there when the handle is created are sized
// at once; rows added later are sized in a posted call, which runs after the adding code
// returns (so a loop of Rows.Add is measured whole) and before the grid repaints. The
// empty new-row placeholder does not count: a grid that starts empty keeps its widths
// until it has content.
void OnPaint(object sender, PaintEventArgs e)
var pending = false;
void Detach()
{
var newRowShown = grid.NewRowIndex >= 0 && grid.Rows[grid.NewRowIndex].Displayed;
if (grid.DisplayedRowCount(false) - (newRowShown ? 1 : 0) <= 0)
grid.HandleCreated -= OnHandleCreated;
grid.RowsAdded -= OnRowsAdded;
grid.DataBindingComplete -= OnRowsAdded;
}
void SizeIfFilled()
{
if (grid.IsDisposed || ContentRowCount(grid) == 0)
return;
grid.Paint -= OnPaint;
Detach();
SizeColumnsToRows(grid);
}
void OnHandleCreated(object sender, EventArgs e) => SizeIfFilled();
void OnRowsAdded(object sender, EventArgs e)
{
if (pending || !grid.IsHandleCreated || ContentRowCount(grid) == 0)
return;
pending = true;
grid.BeginInvoke(() =>
{
if (grid.IsDisposed)
return;
grid.AutoResizeColumns(DataGridViewAutoSizeColumnsMode.DisplayedCells);
foreach (DataGridViewColumn column in grid.Columns)
column.Width = System.Math.Min(column.Width, MaxInitialColumnWidth);
pending = false;
SizeIfFilled();
});
}
grid.Paint += OnPaint;
grid.HandleCreated += OnHandleCreated;
grid.RowsAdded += OnRowsAdded;
grid.DataBindingComplete += OnRowsAdded;
if (grid.IsHandleCreated)
SizeIfFilled();
}
private static int ContentRowCount(DataGridView grid) =>
grid.Rows.Count - (grid.NewRowIndex >= 0 ? 1 : 0);
private static void SizeColumnsToRows(DataGridView grid)
{
var rowCount = System.Math.Min(ContentRowCount(grid), MeasuredRowCount);
foreach (DataGridViewColumn column in grid.Columns)
{
if (!column.Visible || column.InheritedAutoSizeMode != DataGridViewAutoSizeColumnMode.None)
continue;
var width = column.HeaderCell.PreferredSize.Width;
for (var row = 0; row < rowCount; row++)
{
if (row != grid.NewRowIndex)
width = System.Math.Max(width, grid.Rows[row].Cells[column.Index].PreferredSize.Width);
}
column.Width = System.Math.Clamp(width, column.MinimumWidth, MaxInitialColumnWidth);
}
}
}