Overview

Beep Controls are built on top of GDI+ with double-buffering enabled by default. However, as your application scales — more rows in a grid, more controls on a form, multiple monitors at different DPI settings — you need to employ the right optimization techniques to keep the UI responsive.

This guide covers the key levers available in the Beep framework: DPI-aware painting, smart invalidation, update batching, virtual data modes, and debounced rendering.

DPI-Aware Rendering

Beep controls automatically scale their rendering based on the current monitor's DPI setting. BaseControl exposes a ScaleFactor property that reflects the current DPI ratio (e.g., 1.0 at 96 dpi, 1.5 at 144 dpi, 2.0 at 192 dpi). Use this factor in custom paint logic instead of hard-coded pixel values.

protected override void OnPaint(PaintEventArgs e)
{
    float sf = ScaleFactor;

    // Scale dimensions with DPI
    int iconSize = (int)(24 * sf);
    int padding = (int)(8 * sf);
    int cornerRadius = (int)(6 * sf);

    // Use scaled values for drawing
    e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
    using var path = RoundedRectangle(ClientRectangle, cornerRadius);
    e.Graphics.FillPath(_backBrush, path);
}

When a form moves between monitors with different DPI settings, BaseControl re-reads the device context and updates ScaleFactor. Override OnDpiChanged to react:

protected override void OnDpiChanged(DpiChangedEventArgs e)
{
    base.OnDpiChanged(e);
    // ScaleFactor is already updated by base
    RebuildImageCache();
    Invalidate();
}

EnableSmartInvalidation

By default, calling Invalidate() marks the entire control rectangle for repaint. With EnableSmartInvalidation = true, BaseControl tracks dirty regions so that only parts of the control that actually changed are repainted. This is especially beneficial for controls that update frequently (e.g., status bars, live data displays).

public class LiveStatusBar : BaseControl
{
    public LiveStatusBar()
    {
        EnableSmartInvalidation = true;
    }

    private string _statusText = "Ready";

    public string StatusText
    {
        get => _statusText;
        set
        {
            _statusText = value;
            // Only invalidates the text area, not the entire bar
            Rectangle dirty = GetTextBounds();
            Invalidate(dirty);
        }
    }
}
When to enable: Controls with frequent partial updates. Do not enable on controls where every paint pass redraws the entire surface (e.g., a fully-rendered chart).

BeginUpdate / EndUpdate Batching

When you need to change multiple properties on a control at once, wrap the changes in BeginUpdate() / EndUpdate(). This suppresses intermediate layout calculations and repaints, performing a single update at the end.

// Without batching: each line triggers Invalidate() and layout
myGrid.Columns.Clear();
myGrid.Columns.Add(new GridColumn("Id"));
myGrid.Columns.Add(new GridColumn("Name"));
myGrid.Columns.Add(new GridColumn("Email"));
myGrid.DataSource = source;
// 4+ repaints

// With batching: single repaint
myGrid.BeginUpdate();
myGrid.Columns.Clear();
myGrid.Columns.Add(new GridColumn("Id"));
myGrid.Columns.Add(new GridColumn("Name"));
myGrid.Columns.Add(new GridColumn("Email"));
myGrid.Columns.Add(new GridColumn("Phone"));
myGrid.Columns.Add(new GridColumn("Status"));
myGrid.DataSource = source;
myGrid.EndUpdate(); // Single repaint and layout pass

Nesting is supported — EndUpdate() only fires when the outermost call completes:

myGrid.BeginUpdate();
    myGrid.Columns.Add(new GridColumn("A"));
    myGrid.BeginUpdate();     // Counter = 2
        myGrid.Columns.Add(new GridColumn("B"));
        myGrid.Columns.Add(new GridColumn("C"));
    myGrid.EndUpdate();       // Counter = 1, no repaint yet
    myGrid.Columns.Add(new GridColumn("D"));
myGrid.EndUpdate();           // Counter = 0, single repaint now

SuspendLayout / ResumeLayout

When adding or removing child controls from a container, use the WinForms SuspendLayout()/ResumeLayout() pair. This prevents the layout engine from recalculating positions after each individual addition.

panel.SuspendLayout();

for (int i = 0; i < 200; i++)
{
    var label = new BeepLabel
    {
        Text = $"Row {i}",
        Location = new Point(10, i * 28),
        Size = new Size(300, 26)
    };
    panel.Controls.Add(label);
}

panel.ResumeLayout(true);

Combine SuspendLayout() with BeginUpdate() for maximum benefit when both adding controls and changing properties:

form.SuspendLayout();
grid.BeginUpdate();

// Bulk operations here...

grid.EndUpdate();
form.ResumeLayout(true);

Virtual Mode for BeepGridPro

When displaying datasets with thousands of rows, set VirtualMode = true. Instead of creating row objects for every data item, the grid requests cell values on-demand via the CellValueNeeded event, dramatically reducing memory and startup time.

public class LargeDataForm : BeepiFormPro
{
    private List _allCustomers;

    public LargeDataForm()
    {
        _allCustomers = LoadAllCustomers(); // 100,000+ rows

        var grid = new BeepGridPro();
        grid.VirtualMode = true;
        grid.RowCount = _allCustomers.Count;
        grid.Columns.Add(new GridColumn("Id"));
        grid.Columns.Add(new GridColumn("Name"));
        grid.Columns.Add(new GridColumn("Email"));

        grid.CellValueNeeded += (s, e) =>
        {
            var customer = _allCustomers[e.RowIndex];
            e.Value = e.ColumnIndex switch
            {
                0 => customer.Id,
                1 => customer.Name,
                2 => customer.Email,
                _ => null
            };
        };

        Controls.Add(grid);
    }
}
When to use: Datasets with more than 1,000 rows. Below that threshold the overhead of virtual mode outweighs the benefit of pre-loaded rows.

Debounced Painting

When responding to rapid-fire events (mouse move, resize, scroll), painting on every event can cause stutter. Use debouncing to coalesce multiple paint requests into one. The DebounceInvalidate helper skips repaints that are queued within a configurable time window.

public class SmoothChart : BaseControl
{
    private readonly Timer _debounceTimer = new Timer { Interval = 16 };

    public SmoothChart()
    {
        _debounceTimer.Tick += (s, e) =>
        {
            _debounceTimer.Stop();
            Invalidate();
        };
    }

    protected override void OnResize(EventArgs e)
    {
        base.OnResize(e);
        DebounceInvalidate(); // Fires at most once per ~16ms (60 fps)
    }

    private void DebounceInvalidate()
    {
        _debounceTimer.Stop();
        _debounceTimer.Start();
    }
}

Caching GDI+ Resources

Creating Brush, Pen, and Font objects in OnPaint is a common performance pitfall. Cache them as fields and only recreate when the theme changes.

public class BadgeControl : BaseControl
{
    private SolidBrush _bgBrush;
    private SolidBrush _textBrush;
    private Pen _borderPen;

    protected override void ApplyControlStyle()
    {
        base.ApplyControlStyle();
        var theme = BeepThemesManager.ActiveTheme;

        _bgBrush?.Dispose();
        _textBrush?.Dispose();
        _borderPen?.Dispose();

        _bgBrush = new SolidBrush(theme.AccentColor);
        _textBrush = new SolidBrush(theme.ForeColor);
        _borderPen = new Pen(theme.BorderColor, 1f);
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        e.Graphics.FillRectangle(_bgBrush, ClientRectangle);
        e.Graphics.DrawRectangle(_borderPen, 0, 0, Width - 1, Height - 1);
        // Draw text using _textBrush
    }
}

Layout and Control Count Tips

  • Use BeepSimpleGrid for tabular layouts — instead of creating 50+ individual labels and textboxes.
  • Use BeepTree for tree structures — avoid nesting dozens of expandable panels.
  • Use BeepListBox for selectable lists — don't create individual checkboxes for each item.
  • Limit form control count — forms with 300+ controls exhibit measurable GDI+ overhead. Consider tab-based layouts.
  • Lazy-load tabs — populate tab pages only when the user navigates to them, not at form startup.
  • Use Pagination — combine BeepDataNavigator with server-side paging instead of loading all rows into the grid.
  • Profile with a debug build — measure paint times with Stopwatch to identify bottlenecks.