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.
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().
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.
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 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.
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.
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.
ClockSample.kn. The dial, labels, gradients, and hands
are all drawn through Painter.
klyn samples/gui/ClockSample.kn
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.
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.
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.