If all you need is flat data in a grid, DataGridView is fine. But the moment someone says “I want a progress bar in this column,” “this row should have a slider,” or “put a dropdown right in this cell,” standard controls start requiring a lot of scaffolding.
ReoGrid answers those requirements with a single concept: CellBody. Subclass it, override what you need, assign it to a cell — and you have full control over both rendering and interaction.
What is CellBody?
ReoGrid doesn’t separate “renderers” from “editors.” One class, CellBody, covers both.
using unvell.ReoGrid.CellTypes;
public class MyCellBody : CellBody
{
// Painting — called every time the cell is redrawn
public override void OnPaint(CellDrawingContext dc) { ... }
// Mouse events
public override bool OnMouseDown(CellMouseEventArgs e) { ... }
public override bool OnMouseMove(CellMouseEventArgs e) { ... }
public override bool OnMouseEnter(CellMouseEventArgs e) { ... }
public override bool OnMouseLeave(CellMouseEventArgs e) { ... }
// Allow or block direct text editing
public override bool OnStartEdit() { return false; }
// Called when the body is assigned to a cell
public override void OnSetup(Cell cell) { base.OnSetup(cell); }
}
Assigning it to a cell is a single indexer assignment:
var sheet = reoGridControl1.CurrentWorksheet;
sheet["B3"] = new MyCellBody();
Built-in CellBody types
Common use cases ship ready to use. All live in the unvell.ReoGrid.CellTypes namespace.
// Button
var btn = new ButtonCell("Run");
btn.Click += (s, e) => MessageBox.Show("Clicked!");
sheet["B2"] = btn;
// Checkbox
var chk = new CheckBoxCell();
chk.CheckChanged += (s, e) => Console.WriteLine(chk.IsChecked);
sheet["B4"] = chk;
// Radio buttons (linked via a group)
var group = new RadioButtonGroup();
sheet["B6"] = new RadioButtonCell { RadioGroup = group };
sheet["B7"] = new RadioButtonCell { RadioGroup = group };
sheet["B8"] = new RadioButtonCell { RadioGroup = group };
// Dropdown list
var dd = new DropdownListCell("Open", "In Progress", "Done", "Rejected");
dd.SelectedItemChanged += (s, e) => Console.WriteLine(dd.SelectedItem);
sheet["B10"] = dd;
// Hyperlink
sheet["B12"] = new HyperlinkCell("https://reogrid.net");
// Image
sheet["B14"] = new ImageCell(Image.FromFile("logo.png"));
No extra wiring required — assign and go.
Example 1: Progress bar cell
Reads the cell value (0.0–1.0) and paints a gradient bar proportional to that value. This is the simplest possible CellBody implementation.
using unvell.ReoGrid.CellTypes;
using unvell.ReoGrid.Graphics;
using unvell.ReoGrid.Rendering;
public class ProgressBarCell : CellBody
{
public SolidColor BarColor { get; set; } = SolidColor.SteelBlue;
public SolidColor BarColorEnd { get; set; } = SolidColor.CornflowerBlue;
public override void OnPaint(CellDrawingContext dc)
{
double value = Cell.Worksheet.GetCellData<double>(Cell.Position);
value = Math.Clamp(value, 0, 1);
var bounds = GetBodyBounds();
int barWidth = (int)(value * bounds.Width);
if (barWidth > 0)
{
var rect = new Rectangle(bounds.Left, bounds.Top + 1, barWidth, bounds.Height - 2);
dc.Graphics.FillRectangleLinear(BarColor, BarColorEnd, 90f, rect);
}
// Let ReoGrid draw the formatted cell text on top
dc.DrawCellText();
}
// Block direct text editing — value is set via API or formula
public override bool OnStartEdit() => false;
}
Usage:
// Make column B a progress-bar column
for (int r = 2; r <= 11; r++)
sheet[r, 1] = new ProgressBarCell();
// Set values (0.0–1.0)
sheet[2, 1] = 0.75;
sheet[3, 1] = 0.42;
// Apply percent format — cells now display "75%", "42%", etc.
sheet.SetRangeDataFormat(2, 1, 10, 1,
DataFormat.CellDataFormatFlag.Percent,
new DataFormat.NumberDataFormatter.NumberFormatArgs { DecimalPlaces = 0 });
You can also drive the bar from a formula so it updates automatically:
// Column C = actual, Column D = target → Column B reflects the ratio live
sheet[2, 1] = "=C2/D2";
Example 2: Slider cell
Lets users drag a thumb inside the cell to change its value. OnMouseDown and OnMouseMove translate cursor position back into a 0–1 float and write it to the worksheet.
public class SliderCell : CellBody
{
private bool isHover = false;
public override void OnSetup(Cell cell)
{
base.OnSetup(cell);
}
public override void OnPaint(CellDrawingContext dc)
{
float value = 0;
float.TryParse(dc.Cell.DisplayText, out value);
value = Math.Clamp(value, 0f, 1f);
var bounds = Cell.GetBounds();
var g = dc.Graphics;
int halfH = (int)(bounds.Height / 2f);
int sliderH = (int)Math.Min(bounds.Height - 4, 20);
// Track background
g.FillRectangle(4, halfH - 3, bounds.Width - 8, 6, SolidColor.Gainsboro);
// Thumb
int thumbX = 2 + (int)(value * (bounds.Width - 12));
var thumbRect = new Rectangle(thumbX, halfH - sliderH / 2, 8, sliderH);
g.FillRectangle(thumbRect, isHover ? SolidColor.MediumSeaGreen : SolidColor.LightGreen);
}
public override bool OnMouseDown(CellMouseEventArgs e)
{
SetValueFromX(e.RelativePosition.X);
return true; // mark as handled — suppress default selection behaviour
}
public override bool OnMouseMove(CellMouseEventArgs e)
{
if (e.Buttons == unvell.ReoGrid.Interaction.MouseButtons.Left)
SetValueFromX(e.RelativePosition.X);
return false;
}
public override bool OnMouseEnter(CellMouseEventArgs e) { isHover = true; return true; }
public override bool OnMouseLeave(CellMouseEventArgs e) { isHover = false; return true; }
public override bool OnStartEdit() => false;
private void SetValueFromX(float x)
{
float value = x / (Cell.GetBounds().Width - 2f);
value = Math.Clamp(value, 0f, 1f);
Cell.Worksheet.SetCellData(Cell.Position, Math.Round(value, 2));
}
}
Linking a slider to a progress bar in another cell is just a formula:
// E5 — slider
sheet["E5"] = new SliderCell();
sheet["E5"] = 0.5;
// D5 — progress bar driven by the slider value
sheet["D5"] = new ProgressBarCell();
sheet["D5"] = "=E5";
// C5 — numeric readout
sheet["C5"] = "=E5";
sheet.SetRangeDataFormat("C5", DataFormat.CellDataFormatFlag.Percent,
new DataFormat.NumberDataFormatter.NumberFormatArgs { DecimalPlaces = 0 });
Example 3: Status badge cell
A common requirement in business apps is a “Status” column that reads like a label but looks like a coloured badge. The body reads Cell.DisplayText and switches background colour accordingly.
public class StatusBadgeCell : CellBody
{
private static readonly Dictionary<string, SolidColor> StatusColors = new()
{
["Open"] = SolidColor.Tomato,
["In Progress"] = SolidColor.SteelBlue,
["Done"] = SolidColor.MediumSeaGreen,
["Rejected"] = SolidColor.Gray,
};
public override void OnPaint(CellDrawingContext dc)
{
string text = Cell.DisplayText;
var bounds = GetBodyBounds();
var bgColor = StatusColors.TryGetValue(text, out var c) ? c
: new SolidColor(200, SolidColor.LightGray);
// Rounded rectangle background
var rect = new Rectangle(
bounds.Left + 2, bounds.Top + 2,
bounds.Width - 4, bounds.Height - 4);
dc.Graphics.FillRoundedRectangle(rect, 4, bgColor);
// White label text, centred
dc.Graphics.DrawText(
text,
dc.Cell.CalcedStyle?.FontName ?? "Segoe UI",
dc.Cell.CalcedStyle?.FontSize ?? 9f,
SolidColor.White,
bounds,
ReoGridHorAlign.Center,
ReoGridVerAlign.Middle);
}
// Allow typing to change the status directly
public override bool OnStartEdit() => true;
}
To pair it with a dropdown, inherit from DropdownListCell instead and override OnPaint there.
CellBody in WPF
The WPF package (unvell.ReoGridWPF.dll) uses the same CellBody inheritance model. The only difference is that CellDrawingContext.Graphics dispatches to a WPF DrawingContext wrapper instead of System.Drawing.Graphics. The calling code is identical:
// WinForms — delegates to System.Drawing.Graphics
dc.Graphics.FillRectangleLinear(color1, color2, 90f, rect);
// WPF — same call, different backend
dc.Graphics.FillRectangleLinear(color1, color2, 90f, rect);
If you share a class library between WinForms and WPF projects, one CellBody subclass compiles and works in both.
Applying a CellBody to a range
Override Clone() so each cell gets its own independent instance:
public class ProgressBarCell : CellBody
{
// ... painting code
public override ICellBody Clone()
{
return new ProgressBarCell
{
BarColor = this.BarColor,
BarColorEnd = this.BarColorEnd,
};
}
}
// Apply to a column in one loop
for (int r = 1; r <= 20; r++)
sheet[r, 2] = new ProgressBarCell { BarColor = SolidColor.Coral };
Without Clone(), bulk APIs like SetRangeData may share a single instance across cells, which can cause surprising state bugs.
Quick reference
| Goal | Approach |
|---|---|
| Use a ready-made dropdown / checkbox | DropdownListCell / CheckBoxCell as-is |
| Visualise a value graphically | Override CellBody.OnPaint |
| Let mouse interaction change a value | Override OnMouseDown / OnMouseMove |
| Block direct text entry | Return false from OnStartEdit |
| Share one class across WinForms and WPF | Put the subclass in a shared class library |
| Apply to a range safely | Override Clone() |
CellBody is not just “change the look” — it replaces the entire cell with a custom UI component. The same pattern scales from simple progress bars to calendar pickers, star-rating inputs, or anything else that belongs in a cell.