Custom controls and owner-drawn rendering
Derive from UserControl or Control, expose properties and events properly, override OnPaint, and take over drawing for list and tree controls.
UserControl or custom Control
| Approach | Best for | Watch out for |
|---|---|---|
UserControl | Composing existing controls into a reusable widget | Child controls swallow KeyDown; set ProcessCmdKey for shortcuts |
Custom Control | A single thing that draws itself | You implement sizing and accessibility yourself |
| Inherited control | Tweaking an existing control's behaviour | Its internals may assume the normal paint path |
| Owner-draw on a built-in | Keeping built-in behaviour, changing the look | You still must handle measurement events |
public class StatusPill : Control
{
private string _text = "";
private Color _accent = Color.SeaGreen;
[Category("Appearance"), DefaultValue("")]
public string Caption
{
get => _text;
set { _text = value ?? ""; Invalidate(); } // never repaint directly from the setter
}
[Category("Appearance")]
public Color Accent
{
get => _accent;
set { _accent = value; Invalidate(); }
}
protected override void OnPaint(PaintEventArgs e)
{
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
using var brush = new SolidBrush(_accent);
e.Graphics.FillEllipse(brush, 0, 0, Height - 1, Height - 1);
TextRenderer.DrawText(e.Graphics, _text, Font,
new Point(Height + 4, (Height - Font.Height) / 2), ForeColor);
}
}Invalidate()marks the control dirty; callingRefresh()from a property setter forces an immediate synchronous repaint and makes layout loops worse.- Decorate public properties with
[Category],[Description]and[DefaultValue]so the designer and the property grid behave sensibly. - To publish an event, use
EventHandler<T>or a customEventArgssubclass - never a bare delegate, or the designer cannot wire it.
Painting well
public partial class GraphView : UserControl
{
public GraphView()
{
InitializeComponent();
SetStyle(ControlStyles.AllPaintingInWmPaint |
ControlStyles.UserPaint |
ControlStyles.OptimizedDoubleBuffer |
ControlStyles.ResizeRedraw, true);
SetStyle(ControlStyles.Selectable, true); // so it can take focus
}
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
Invalidate(); // ResizeRedraw also does this, pick one
}
}DoubleBufferedremoves flicker for the common case; the threeControlStylesabove are the standard combination.- Never allocate a
Pen,BrushorFontinOnPaintwithout disposing it - GDI handles are a finite resource and the symptom is a global crash, not a leak warning. - Cache expensive geometry and rebuild it in
OnResize, not on every paint.
Owner-draw for lists and trees
combo.DrawMode = DrawMode.OwnerDrawFixed;
combo.ItemHeight = 24;
combo.DrawItem += (s, e) =>
{
e.DrawBackground();
if (e.Index < 0) return;
var item = (Contact)combo.Items[e.Index];
var colour = item.Online ? Color.SeaGreen : Color.Gray;
using var dot = new SolidBrush(colour);
e.Graphics.FillEllipse(dot, e.Bounds.Left + 4, e.Bounds.Top + 7, 10, 10);
TextRenderer.DrawText(e.Graphics, item.Name, combo.Font,
new Rectangle(e.Bounds.Left + 20, e.Bounds.Top, e.Bounds.Width - 24, e.Bounds.Height),
e.ForeColor, TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis);
e.DrawFocusRectangle();
};DrawItem for a variable-height list, DrawSubItem for a details-view ListView, DrawNode plus DrawMode = TreeViewDrawMode.OwnerDrawText for a tree. In every case call e.DrawBackground() first and e.DrawFocusRectangle() last, or selection and focus disappear.
⚠️
Owner-drawn text must respect the system DPI and font. Hard-coding pixel offsets that look right at 100 percent scaling produces clipped rows at 150 percent. Measure with
TextRenderer.MeasureText and scale offsets from DeviceDpi.FAQ
UserControl or a templated control?
WinForms has no templating. If the look must change without recompiling, expose properties and repaint; if you need that much flexibility, WPF or WinUI is the better host.
Why does my custom control not show in the toolbox?
Build the project first, and make sure the class is public with a public parameterless constructor. A control defined in the same project only appears when the designer reloads the assembly.
Related
Drawing, images and printing with GDI+ Events and layout
Last refreshed 2026-09-18.