CanvasControl

March 6, 2026 · View on GitHub

Free-form drawing surface that exposes CharacterBuffer drawing primitives through a local-coordinate API. Supports both retained-mode (async) and immediate-mode (event-driven) painting, or a combination of both.

Overview

CanvasControl gives you a pixel-level (cell-level) drawing canvas inside any window. It owns an internal CharacterBuffer that persists drawn content across render cycles. You can draw to this buffer at any time from any thread using BeginPaint()/EndPaint(), or subscribe to the Paint event to redraw each frame. Both approaches can be combined: the internal buffer is composited first, then the Paint event fires for overlay drawing on top.

The CanvasGraphics context returned by BeginPaint() and passed through the Paint event provides 30+ drawing methods — text, lines, boxes, circles, ellipses, arcs, polygons, gradients, patterns — all operating in canvas-local coordinates (0,0 = top-left of the canvas).

Properties

PropertyTypeDefaultDescription
CanvasWidthint40Logical canvas width in characters. Setting recreates the internal buffer.
CanvasHeightint20Logical canvas height in characters. Setting recreates the internal buffer.
AutoSizeboolfalseWhen true, the internal buffer automatically resizes to match the layout bounds assigned by the parent container. Enable with Stretch/Fill alignment.
AutoClearboolfalseWhen true, the internal buffer is cleared after compositing each frame, so the Paint event redraws from scratch (immediate mode).
BackgroundColorColorContainer or BlackBackground color for the canvas.
ForegroundColorColorContainer or WhiteDefault foreground color.
IsEnabledbooltrueWhether the control accepts input.
HasFocusboolfalseWhether the control has keyboard focus.
MarginMargin0,0,0,0Layout margin around the canvas.
HorizontalAlignmentHorizontalAlignmentLeftHorizontal alignment. Use Stretch with AutoSize for responsive sizing.
VerticalAlignmentVerticalAlignmentTopVertical alignment. Use Fill with AutoSize for responsive sizing.

Events

EventArgsDescription
PaintCanvasPaintEventArgsFires during each render cycle after compositing. Graphics draws to the window buffer.
CanvasMouseClickCanvasMouseEventArgsLeft-click with canvas-local coordinates.
CanvasMouseRightClickCanvasMouseEventArgsRight-click with canvas-local coordinates.
CanvasMouseMoveCanvasMouseEventArgsMouse move with canvas-local coordinates.
CanvasKeyPressedConsoleKeyInfoKey press while the canvas has focus.
GotFocusEventArgsThe canvas received keyboard focus.
LostFocusEventArgsThe canvas lost keyboard focus.

Event Args

CanvasPaintEventArgs:

PropertyTypeDescription
GraphicsCanvasGraphicsDrawing context wrapping the window buffer with offset translation.
CanvasWidthintCurrent canvas width.
CanvasHeightintCurrent canvas height.

CanvasMouseEventArgs:

PropertyTypeDescription
CanvasXintX coordinate in canvas-local space (0-based).
CanvasYintY coordinate in canvas-local space (0-based).
OriginalArgsMouseEventArgsOriginal mouse event for access to flags and absolute position.

Two Painting Modes

Retained Mode (BeginPaint/EndPaint)

Content drawn via BeginPaint()/EndPaint() persists in the internal buffer across render cycles. Draw whenever you want, from any thread.

var g = canvas.BeginPaint();
try
{
    g.Clear(Color.DarkBlue);
    g.DrawBox(0, 0, 60, 20, BoxChars.Single, Color.White, Color.DarkBlue);
    g.WriteStringCentered(10, "Hello Canvas!", Color.White, Color.DarkBlue);
}
finally
{
    canvas.EndPaint(); // releases lock, triggers repaint
}

Immediate Mode (Paint Event)

Subscribe to the Paint event and set AutoClear = true. The canvas clears before each frame, so you redraw from scratch.

var canvas = new CanvasControl { AutoClear = true };
canvas.Paint += (sender, e) =>
{
    var g = e.Graphics;
    g.DrawCircle(30, 10, 8, '*', Color.Yellow, Color.DarkBlue);
};

Combined: Retained Background + Event Overlay

The internal buffer is composited first, then the Paint event fires for drawing on top.

// Static background drawn once
var g = canvas.BeginPaint();
g.GradientFillRect(0, 0, 60, 20, Color.DarkBlue, Color.Black, horizontal: false);
canvas.EndPaint();

// Dynamic overlay redrawn each frame
canvas.Paint += (sender, e) =>
{
    e.Graphics.WriteStringCentered(10, $"Time: {DateTime.Now:HH:mm:ss}",
        Color.White, Color.DarkBlue);
};

Creating a Canvas

Fixed Size

var canvas = new CanvasControl(80, 24);
window.AddControl(canvas);

Auto-Sizing (Stretch/Fill)

var canvas = new CanvasControl
{
    HorizontalAlignment = HorizontalAlignment.Stretch,
    VerticalAlignment = VerticalAlignment.Fill,
    AutoSize = true
};
window.AddControl(canvas);

The canvas will resize its internal buffer to match the space assigned by the parent container.

Using WindowBuilder

var canvas = new CanvasControl
{
    HorizontalAlignment = HorizontalAlignment.Stretch,
    VerticalAlignment = VerticalAlignment.Fill,
    AutoSize = true,
    AutoClear = true
};

canvas.Paint += (sender, e) =>
{
    // Draw each frame
};

var window = new WindowBuilder(ws)
    .WithTitle("My Canvas")
    .WithSize(60, 25)
    .Centered()
    .Resizable(true)
    .AddControl(canvas)
    .Build();

ws.AddWindow(window);

CanvasGraphics API

CanvasGraphics wraps a CharacterBuffer and translates all coordinates from canvas-local (0,0) to absolute buffer position. You get a CanvasGraphics from BeginPaint() or from CanvasPaintEventArgs.Graphics.

Core

MethodDescription
SetCell(x, y, ch, fg, bg)Set a single cell.
GetCell(x, y)Read a cell from the buffer.
Clear()Clear with the canvas background color.
Clear(bg)Clear with a specific color.
FillRect(x, y, w, h, ch, fg, bg)Fill a rectangle with a character and colors.
FillRect(x, y, w, h, bg)Fill a rectangle with a background color.

Text

MethodDescription
WriteString(x, y, text, fg, bg)Write text at a position.
WriteStringCentered(y, text, fg, bg)Write text centered horizontally.
WriteStringRight(y, text, fg, bg)Write text right-aligned.
WriteStringInBox(x, y, w, h, text, fg, bg)Write text centered in a box region.
WriteWrappedText(x, y, w, text, fg, bg)Write text with word wrapping.

Lines and Boxes

MethodDescription
DrawLine(x0, y0, x1, y1, ch, fg, bg)Draw a line between two points (Bresenham).
DrawHorizontalLine(x, y, length, ch, fg, bg)Draw a horizontal line.
DrawVerticalLine(x, y, length, ch, fg, bg)Draw a vertical line.
DrawBox(x, y, w, h, boxChars, fg, bg)Draw a box with border characters.

Circles and Ellipses

MethodDescription
DrawCircle(cx, cy, r, ch, fg, bg)Draw a circle outline.
FillCircle(cx, cy, r, ch, fg, bg)Draw a filled circle.
DrawEllipse(cx, cy, rx, ry, ch, fg, bg)Draw an ellipse outline.
FillEllipse(cx, cy, rx, ry, ch, fg, bg)Draw a filled ellipse.
DrawArc(cx, cy, r, startAngle, endAngle, ch, fg, bg)Draw a circular arc.

Polygons

MethodDescription
DrawTriangle(points, ch, fg, bg)Draw a triangle outline from 3 points.
FillTriangle(points, ch, fg, bg)Draw a filled triangle from 3 points.
DrawPolygon(points, ch, fg, bg)Draw a polygon outline from N points.
FillPolygon(points, ch, fg, bg)Draw a filled polygon from N points.

Gradients and Patterns

MethodDescription
GradientFillHorizontal(x, y, w, h, left, right)Horizontal gradient fill.
GradientFillVertical(x, y, w, h, top, bottom)Vertical gradient fill.
GradientFillRect(x, y, w, h, start, end, horizontal)Gradient fill in either direction.
PatternFill(x, y, w, h, pattern, fg, bg)Fill a rectangle with a repeating text pattern.
CheckerFill(x, y, w, h, ch1, ch2, fg1, bg1, fg2, bg2)Fill with alternating checker cells.
StippleFill(x, y, w, h, density, ch, fg, bg)Fill with random stipple pattern at a given density.

Keyboard Support

All key events are forwarded to the CanvasKeyPressed event when the canvas has focus.

KeyBehavior
Any keyFires CanvasKeyPressed with the ConsoleKeyInfo
TabMoves focus to next control (default window behavior)

Mouse Support

EventBehavior
Left clickFires CanvasMouseClick with canvas-local coordinates, focuses the canvas
Right clickFires CanvasMouseRightClick with canvas-local coordinates
Mouse moveFires CanvasMouseMove with canvas-local coordinates

Coordinates are automatically translated from absolute screen position to canvas-local (0,0 = top-left of the drawing area, excluding margins).

Thread Safety

BeginPaint() acquires a monitor lock on the internal buffer. EndPaint() releases it and invalidates the window. This makes it safe to draw from background threads, timers, or async loops.

// Safe from any thread
Task.Run(async () =>
{
    while (!ct.IsCancellationRequested)
    {
        var g = canvas.BeginPaint();
        // draw...
        canvas.EndPaint();
        await Task.Delay(16, ct); // ~60fps
    }
});

The Paint event fires within the render lock, so no additional synchronization is needed there.

Examples

Interactive Drawing

canvas.CanvasMouseClick += (sender, e) =>
{
    var g = canvas.BeginPaint();
    g.FillCircle(e.CanvasX, e.CanvasY, 2, '*', Color.Red, Color.Black);
    canvas.EndPaint();
};

Animated Background with Async Thread

var canvas = new CanvasControl
{
    HorizontalAlignment = HorizontalAlignment.Stretch,
    VerticalAlignment = VerticalAlignment.Fill,
    AutoSize = true
};

var window = new WindowBuilder(ws)
    .WithTitle("Animation")
    .WithSize(60, 25)
    .Centered()
    .WithAsyncWindowThread(async (win, ct) =>
    {
        int frame = 0;
        while (!ct.IsCancellationRequested)
        {
            var g = canvas.BeginPaint();
            g.Clear(Color.Black);
            int cx = canvas.CanvasWidth / 2;
            int cy = canvas.CanvasHeight / 2;
            int r = (int)(Math.Sin(frame * 0.05) * 5 + 7);
            g.FillCircle(cx, cy, r, '*', Color.Cyan, Color.Black);
            canvas.EndPaint();
            frame++;
            await Task.Delay(33, ct);
        }
    })
    .AddControl(canvas)
    .Build();

Geometry Showcase

canvas.Paint += (sender, e) =>
{
    var g = e.Graphics;
    int w = e.CanvasWidth, h = e.CanvasHeight;

    // Gradient background
    g.GradientFillRect(0, 0, w, h, Color.DarkBlue, Color.Black, horizontal: false);

    // Centered circle
    g.DrawCircle(w / 2, h / 2, Math.Min(w, h) / 4, 'o', Color.Cyan, Color.DarkBlue);

    // Box with text
    g.DrawBox(2, 2, 20, 5, BoxChars.Double, Color.Yellow, Color.DarkBlue);
    g.WriteStringInBox(2, 2, 20, 5, "Hello!", Color.White, Color.DarkBlue);

    // Polygon
    var points = new (int X, int Y)[]
    {
        (w - 15, 3), (w - 5, 3), (w - 10, 8)
    };
    g.FillTriangle(points, '#', Color.Green, Color.DarkBlue);
};

Best Practices

  • Use AutoSize = true with Stretch/Fill when the canvas should adapt to the window size. Read CanvasWidth/CanvasHeight each frame for proportional drawing.
  • Always pair BeginPaint() with EndPaint() in a try/finally block to avoid deadlocks.
  • Prefer retained mode for content that changes infrequently (backgrounds, static shapes). Use the Paint event for content that changes every frame (animations, overlays).
  • Read dimensions each frame in the Paint event — e.CanvasWidth and e.CanvasHeight may change if the window is resized.
  • Don't call BeginPaint() inside a Paint handler — the render lock is already held, and you would deadlock.

CanvasControl vs. Compositor Effects

FeatureCanvasControlCompositor Effects
ScopeSingle control within a windowEntire window buffer
CoordinatesCanvas-local (0,0 = top-left of control)Absolute buffer coordinates
Drawing APICanvasGraphics (30+ methods)Raw CharacterBuffer cell access
PersistenceInternal buffer retains contentNo persistence (runs each frame)
Use caseCustom drawing surfaces, games, visualizationsPost-processing effects, transitions, overlays
Thread modelBeginPaint()/EndPaint() from any threadRuns in render lock via event

Use CanvasControl when you need a self-contained drawing area within your window layout. Use Compositor Effects when you need to manipulate the entire rendered window buffer (blur, fade, color grading).

See Also


Back to Controls | Back to Main Documentation