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

ApproachBest forWatch out for
UserControlComposing existing controls into a reusable widgetChild controls swallow KeyDown; set ProcessCmdKey for shortcuts
Custom ControlA single thing that draws itselfYou implement sizing and accessibility yourself
Inherited controlTweaking an existing control's behaviourIts internals may assume the normal paint path
Owner-draw on a built-inKeeping built-in behaviour, changing the lookYou 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; calling Refresh() 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 custom EventArgs subclass - 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
    }
}
  • DoubleBuffered removes flicker for the common case; the three ControlStyles above are the standard combination.
  • Never allocate a Pen, Brush or Font in OnPaint without 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.

Drawing, images and printing with GDI+ Events and layout

Last refreshed 2026-09-18.