GUI Canvas Custom painting

Canvas and Custom Painting

Canvas is the GUI widget to use when a form widget is not enough: gauges, charts, game surfaces, previews, and visual editors should draw their own content through the typed Painter API.

When to Use Canvas

Prefer ordinary widgets and layouts for forms. Use Canvas when the UI is mostly pixels and geometry. A canvas participates in layouts like any other widget, but its visual content is produced by overriding paint().

Default behavior

Canvas uses an expanding size policy by default and does not take keyboard focus. That makes it a good central drawing area next to compact controls such as sliders, check boxes, and buttons.

Define a Canvas Widget

A custom canvas usually stores a small amount of state, exposes typed setters, calls repaint() when state changes, and renders everything from paint().

import klyn.gui.windows
import klyn.math

class SpeedWidget extends Canvas:

    private _speed as Int = 0
    private _maxSpeed as Int = 130
    private _limiter as Boolean = false

    public SpeedWidget():
        super()
        this.preferredWidth = 260
        this.preferredHeight = 260
        this.minWidth = 180
        this.minHeight = 180

    public setSpeed(speed as Int) as Void:
        this._speed = speed
        if this._limiter and this._speed > this._maxSpeed:
            this._speed = this._maxSpeed
        this.repaint()

    public override paint(painter as Painter) as Void:
        if painter is null:
            return

        painter.save()
        painter.antialiasingEnabled = true
        painter.textAntialiasingEnabled = true
        # Draw the widget here.
        painter.restore()

save() and restore() keep drawing state local to the widget. That matters when several widgets are painted in the same frame.

Painter Basics

Painter exposes explicit state: fill style, stroke style, line width, font, and antialiasing flags. Styles can be colors or gradients.

radius as Double = 0.93 * (Double(minSide) / 2.0)
centerX as Int = this.x + this.width // 2
centerY as Int = this.y + this.height // 2

outerGradient = painter.createLinearGradient(
    0,
    Int(Double(centerY) - radius),
    0,
    Int(Double(centerY) + radius)
)
outerGradient.setColorAt(0.0, Color(224, 224, 224, 1.0))
outerGradient.setColorAt(0.5, Color(110, 119, 116, 1.0))
outerGradient.setColorAt(0.51, Color(10, 14, 10, 1.0))
outerGradient.setColorAt(1.0, Color(10, 8, 9, 1.0))

painter.fillStyle = Color(33, 33, 33, 1.0)
painter.strokeStyle = outerGradient
painter.lineWidth = 8.0
painter.fillEllipse(Int(Double(centerX) - radius), Int(Double(centerY) - radius), Int(radius * 2.0), Int(radius * 2.0))
painter.drawEllipse(Int(Double(centerX) - radius), Int(Double(centerY) - radius), Int(radius * 2.0), Int(radius * 2.0))

Keep geometry derived from this.width and this.height. This makes the drawing responsive when the window is resized.

Speedometer Example

The speedometer sample in samples/gui/SpeedDemoWindow.kn uses a Canvas for the dial and ordinary widgets for interaction. The main layout keeps the canvas expansive and the sliders compact.

topLayout as HBoxLayout = HBoxLayout()
top as Container = Container(topLayout)
top.preferredHeight = 260
top.layoutParams = LayoutParams(1, 1)
topLayout.add(speedWidget)
topLayout.add(speedSlider)

bottomLayout as HBoxLayout = HBoxLayout(6, "center")
bottom as Container = Container(bottomLayout)
bottom.preferredHeight = 36
bottomLayout.add(limiter)
bottomLayout.add(maxSpeedSlider)

speedSlider.valueChanged += lambda(event: ValueChangedEvent): speedWidget.setSpeed(event.value)
maxSpeedSlider.valueChanged += lambda(event: ValueChangedEvent): speedWidget.setMaxSpeed(event.value)
limiter.stateChanged += lambda(event: ValueChangedEvent): speedWidget.setLimiter(event.value != 0)

The widget keeps the requested speed separate from the displayed speed. When the limiter is disabled, the gauge can immediately return to the slider value without losing state.

Klyn speedometer canvas sample at normal size
Normal window size. The canvas owns the central drawing area while the sliders stay compact.
Klyn speedometer canvas sample at compact size
Compact window size. Text and tick geometry are recomputed from the available radius.
Clock and Timer Example

samples/gui/ClockSample.kn adapts the same custom-painting model to an analog clock. The widget derives all geometry from its current bounds, reads the current time during painting, and uses a GUI Timer to request a repaint every second.

class ClockWidget extends Canvas:

    private _timer as Timer

    public ClockWidget():
        super()
        this.preferredWidth = 360
        this.preferredHeight = 360
        this.minWidth = 220
        this.minHeight = 220
        this._timer = Timer(1000)
        this._timer.timeout += lambda(event: ActionEvent): this.repaint()
        this._timer.start()

    public stop() as Void:
        this._timer.stop()

The timer is emitted by the native GUI event loop, so the callback can safely call repaint(). Stop long-lived timers when the owning window closes or after run() returns.

Klyn analog clock canvas sample
Analog clock rendered by ClockSample.kn. The dial, labels, gradients, and hands are all drawn through Painter.
klyn samples/gui/ClockSample.kn
Capture the Result

Window.capture() can render a GUI window to an image file. The screenshots on this page were produced from the sample itself, then converted to PNG for the website.

window = SpeedDemoWindow()
window.capture("speed-demo-window.bmp")

This is useful for documentation and visual regression checks because the image comes from the same painting pipeline used on screen.

Painting Performance

A canvas is repainted often during resize and interactive updates. Keep paint() deterministic, avoid unnecessary allocations in tight loops, and only call repaint() when widget state actually changes.

  • Compute size-dependent geometry from the current widget bounds.
  • Enable antialiasing for shapes and text when the visual quality requires it.
  • Use gradients and alpha colors through painter styles rather than hand-built pixel buffers.
  • Keep application logic outside paint(); painting should only render current state.
Next Step

Run samples/gui/SpeedDemoWindow.kn and samples/gui/ClockSample.kn to inspect full custom painting implementations, or return to Slider Widget for the standard scale control used by the speedometer sample.