Wizard Painters
Four wizard painters — VerticalStepper, HorizontalStepper, Minimal, Cards — each implementing IWizardPainter for rendering step indicators and navigation chrome
Table of Contents
Overview
Each wizard form type is paired with a corresponding painter class that handles all rendering concerns. Painters are obtained through the WizardPainterFactory, which maps WizardStyle enum values to painter implementations. Painters receive theme tokens, layout bounds, and step state data from their host form and render accordingly.
| Painter | Style | Registration Key | Renders |
|---|---|---|---|
VerticalStepperPainter | VerticalStepper | "VerticalStepper" | Left sidebar, numbered circles with checkmarks, vertical connector lines, active/completed/upcoming step states |
HorizontalStepperPainter | HorizontalStepper | "HorizontalStepper" | Top bar with connected step nodes, horizontal progress line, filled/outlined circle states |
MinimalPainter | Minimal | "Minimal" | No step indicators — only navigation button bar with Back/Next/Cancel |
CardsPainter | Cards | "Cards" | Card surface with drop shadow, step dot indicators, card enter/exit animation rendering |
public static class WizardPainterFactory
{
private static readonly Dictionary<WizardStyle, Func<IWizardPainter>> _factories = new()
{
[WizardStyle.VerticalStepper] = () => new VerticalStepperPainter(),
[WizardStyle.HorizontalStepper] = () => new HorizontalStepperPainter(),
[WizardStyle.Minimal] = () => new MinimalPainter(),
[WizardStyle.Cards] = () => new CardsPainter()
};
public static IWizardPainter Create(WizardStyle style)
{
if (_factories.TryGetValue(style, out var factory))
return factory();
return new VerticalStepperPainter(); // fallback
}
public static void Register(WizardStyle style, Func<IWizardPainter> factory)
{
_factories[style] = factory;
}
}IWizardPainter Contract
All wizard painters implement the IWizardPainter interface. The host form calls these methods in order: Initialize once at form creation, then PaintStepIndicator, PaintConnector, and PaintNavigation on each Paint cycle. The painter receives bounds and state data via properties set by the form before painting.
public interface IWizardPainter
{
void Initialize(WizardForm form, IBeepTheme theme);
void PaintStepIndicator(Graphics g, int stepIndex, WizardStepState state, Rectangle bounds);
void PaintConnector(Graphics g, int fromStepIndex, int toStepIndex, WizardStepState fromState,
Rectangle bounds);
void PaintNavigation(Graphics g, WizardNavigationState navState, Rectangle bounds);
void PaintBackground(Graphics g, Rectangle bounds);
}| Method | When Called | Purpose |
|---|---|---|
| Initialize(WizardForm, IBeepTheme) | Form constructor | Stores references to host form and theme. Prepares any reusable brushes, pens, fonts. Called once per form lifetime. |
| PaintStepIndicator(Graphics, int, WizardStepState, Rectangle) | OnPaint, per step | Renders a single step indicator: circle with number, checkmark if completed, highlighted if active, dimmed if upcoming. stepIndex is 0-based. state is Completed/Active/Upcoming/Error. |
| PaintConnector(Graphics, int, int, WizardStepState, Rectangle) | OnPaint, between steps | Renders the connector line between step indicators fromStepIndex and toStepIndex. The fromState determines if the line is solid (completed) or dashed (upcoming). |
| PaintNavigation(Graphics, WizardNavigationState, Rectangle) | OnPaint, nav bar | Renders navigation button bar with Back/Next/Cancel buttons respecting the current WizardNavigationState flags. |
| PaintBackground(Graphics, Rectangle) | OnPaint, first call | Renders the entire background of the indicator/step area. Called before individual step paints. |
The WizardStepState enum defines the visual state of each step indicator:
public enum WizardStepState
{
Upcoming, // Not yet reached — gray, dimmed
Active, // Current step — theme accent color, bold
Completed, // Completed successfully — green/checkmark
Error // Completed with error — red/warning
}VerticalStepperPainter
Paints the left-side vertical stepper panel. Step indicators are rendered as numbered circles (stepIndex + 1) connected by vertical lines. Completed steps display a checkmark icon instead of the number. The active step uses the theme accent color with a bolder border stroke.
| Aspect | Value | Source |
|---|---|---|
| Circle radius | 18px | VerticalStepperLayoutManager.StepCircleRadius |
| Vertical spacing | 56px between centers | VerticalStepperLayoutManager.StepVerticalSpacing |
| Line width | 3px | Constant |
| Completed circle fill | theme.StepperCompleteColor | IBeepTheme |
| Active circle border | theme.StepperActiveColor, 3px | IBeepTheme |
| Upcoming circle stroke | theme.StepperUpcomingColor, 2px | IBeepTheme |
| Completed line | Solid, theme.StepperCompleteColor | IBeepTheme |
| Upcoming line | Dashed, theme.StepperUpcomingColor | IBeepTheme |
| Font | theme.DefaultFont, 10pt, Bold for active | IBeepTheme |
public class VerticalStepperPainter : IWizardPainter
{
private WizardForm _form;
private IBeepTheme _theme;
public void Initialize(WizardForm form, IBeepTheme theme)
{
_form = form;
_theme = theme;
}
public void PaintStepIndicator(Graphics g, int stepIndex, WizardStepState state, Rectangle bounds)
{
var center = new Point(bounds.X + bounds.Width / 2, bounds.Y + bounds.Height / 2);
int radius = 18;
using (var circleBrush = GetCircleBrush(state))
using (var circlePen = GetCirclePen(state))
using (var textBrush = GetTextBrush(state))
using (var font = GetStepFont(state))
{
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
g.FillEllipse(circleBrush, center.X - radius, center.Y - radius, radius * 2, radius * 2);
g.DrawEllipse(circlePen, center.X - radius, center.Y - radius, radius * 2, radius * 2);
if (state == WizardStepState.Completed)
{
DrawCheckmark(g, center, radius - 4);
}
else
{
string num = (stepIndex + 1).ToString();
var size = g.MeasureString(num, font);
g.DrawString(num, font, textBrush, center.X - size.Width / 2, center.Y - size.Height / 2);
}
}
// Draw step title label below the circle
if (stepIndex < _form.Config.Steps.Count)
{
var label = _form.Config.Steps[stepIndex].Title;
var labelBounds = new Rectangle(bounds.X, bounds.Y + radius * 2 + 8,
bounds.Width, bounds.Height - radius * 2 - 8);
using (var labelFont = state == WizardStepState.Active
? new Font(_theme.DefaultFont, FontStyle.Bold) : new Font(_theme.DefaultFont, FontStyle.Regular))
{
var sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Near };
g.DrawString(label, labelFont, Brushes.Gray, labelBounds, sf);
}
}
}
// ...
}HorizontalStepperPainter
Paints a top-bar horizontal step indicator. Steps are rendered as connected nodes: filled circles for completed, outlined with accent fill for active, and gray outlines for upcoming. A continuous horizontal line runs behind the circles. The line is solid and accent-colored up to the active step, then dashed and gray beyond it.
| Aspect | Value | Description |
|---|---|---|
| Circle radius | 16px | Step node circle size |
| Line position | Center Y of step row | Runs through circle centers |
| Line thickness | 4px | Horizontal progress bar |
| Node spacing | Equal distribution | Calculated as barWidth / (stepCount + 1) |
| Completed fill | theme.StepperCompleteColor | Solid filled circle |
| Active fill | theme.Background (white) | White circle with accent border |
| Active border | theme.StepperActiveColor, 3px | Thick accent stroke |
| Label position | Below each circle | Step title centered under node |
| Completed line | Solid, theme accent, 4px | From left edge to active step center |
| Upcoming line | Dashed, theme.StepperUpcomingColor, 2px | From active step center to right edge |
public class HorizontalStepperPainter : IWizardPainter
{
public void PaintConnector(Graphics g, int fromStepIndex, int toStepIndex,
WizardStepState fromState, Rectangle bounds)
{
// The full horizontal line is painted once in PaintBackground,
// segmented by state: completed=solid/accent, upcoming=dashed/gray
var layout = (_form.LayoutManager as HorizontalStepperLayoutManager);
if (layout == null) return;
var centers = layout.StepNodeCenters;
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
int lineY = centers[0].Y;
using (var completedPen = new Pen(_theme.StepperCompleteColor, 4))
using (var upcomingPen = new Pen(_theme.StepperUpcomingColor, 2)
{ DashStyle = System.Drawing.Drawing2D.DashStyle.Dash })
{
// Draw completed line up to active step
int activeIdx = _form.Config.CurrentStepIndex;
if (activeIdx > 0)
g.DrawLine(completedPen, centers[0].X, lineY, centers[activeIdx].X, lineY);
// Draw upcoming line from active step to end
if (activeIdx < centers.Length - 1)
g.DrawLine(upcomingPen, centers[activeIdx].X, lineY,
centers[centers.Length - 1].X, lineY);
}
}
public void PaintStepIndicator(Graphics g, int stepIndex, WizardStepState state, Rectangle bounds)
{
var center = new Point(bounds.X + bounds.Width / 2, bounds.Y + bounds.Height / 2);
int radius = 16;
using (var fill = GetNodeBrush(state))
using (var border = GetNodePen(state))
{
if (state == WizardStepState.Completed)
g.FillEllipse(fill, center.X - radius, center.Y - radius, radius * 2, radius * 2);
else
g.FillEllipse(Brushes.White, center.X - radius, center.Y - radius, radius * 2, radius * 2);
g.DrawEllipse(border, center.X - radius, center.Y - radius, radius * 2, radius * 2);
if (state == WizardStepState.Completed)
DrawCheckmark(g, center, 8);
else
{
string num = (stepIndex + 1).ToString();
DrawCenteredText(g, num, state == WizardStepState.Active
? _theme.StepperActiveColor : _theme.StepperUpcomingColor, center);
}
}
}
}MinimalPainter
The Minimal painter renders no step indicators — only the navigation button bar. It provides the cleanest possible wizard experience, suitable for simple 2-3 step flows where step indicators add unnecessary visual noise. The nav bar is painted with theme-consistent colors for Back, Next/Cancel buttons.
public class MinimalPainter : IWizardPainter
{
public void PaintStepIndicator(Graphics g, int stepIndex, WizardStepState state, Rectangle bounds)
{
// No step indicators rendered — intentional no-op
}
public void PaintConnector(Graphics g, int fromStepIndex, int toStepIndex,
WizardStepState fromState, Rectangle bounds)
{
// No connectors — intentional no-op
}
public void PaintNavigation(Graphics g, WizardNavigationState navState, Rectangle bounds)
{
// Draw border separating nav bar from content
using (var borderPen = new Pen(_theme.BorderColor))
g.DrawLine(borderPen, bounds.X, bounds.Y, bounds.X + bounds.Width, bounds.Y);
// Navigation buttons are actual WinForms buttons positioned by the layout manager.
// The painter only renders the bar background and separator line.
using (var bgBrush = new SolidBrush(_theme.StatusBarBackColor))
g.FillRectangle(bgBrush, bounds);
}
public void PaintBackground(Graphics g, Rectangle bounds)
{
using (var bgBrush = new SolidBrush(_theme.BackgroundColor))
g.FillRectangle(bgBrush, bounds);
}
}| Painted Element | Color Source |
|---|---|
| Form background | theme.BackgroundColor |
| Nav bar background | theme.StatusBarBackColor |
| Nav bar separator line | theme.BorderColor, 1px solid |
| Back button (WinForms) | theme.ButtonBackColor / theme.ButtonBorderColor |
| Next/Finish button (WinForms) | theme.ButtonBackColor / theme.ButtonBorderColor (PrimaryButton variant) |
| Cancel button (WinForms) | theme.ButtonBackColor (ghost/outline style) |
CardsPainter
Paints card-based wizard transitions. The card surface is a rounded rectangle with a drop shadow. Step position is indicated by dot indicators below the card. The painter works with the animation engine to render enter/exit animations: slide, fade, or flip transitions.
| Aspect | Value | Source |
|---|---|---|
| Card corner radius | theme.BorderRadius (typically 8–12px) | IBeepTheme |
| Card background | theme.CardBackColor (or theme.BackgroundColor) | IBeepTheme |
| Card border | theme.CardBorderColor, 1px | IBeepTheme |
| Drop shadow | 4px blur, 2px offset, 30% opacity black | Hard-coded graphics path |
| Dot size | 10px diameter | Constant |
| Dot spacing | 16px center-to-center | Constant |
| Active dot color | theme.StepperActiveColor | IBeepTheme |
| Completed dot color | theme.StepperCompleteColor | IBeepTheme |
| Upcoming dot color | theme.StepperUpcomingColor | IBeepTheme |
| Transition types | SlideRight, SlideLeft, Fade, FlipHorizontal, FlipVertical | WizardAnimationType enum |
public class CardsPainter : IWizardPainter
{
private WizardForm _form;
private IBeepTheme _theme;
private GraphicsPath _cardPath;
private GraphicsPath _shadowPath;
public void PaintBackground(Graphics g, Rectangle bounds)
{
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
// Draw drop shadow (using shadow path offset by 2px)
using (var shadowBrush = new SolidBrush(Color.FromArgb(77, 0, 0, 0)))
{
var shadowBounds = new Rectangle(bounds.X + 2, bounds.Y + 2,
bounds.Width - 4, bounds.Height - 4);
_shadowPath = GetRoundedRect(shadowBounds, _theme.BorderRadius);
g.FillPath(shadowBrush, _shadowPath);
}
// Draw card surface
var cardBounds = new Rectangle(bounds.X, bounds.Y,
bounds.Width - 4, bounds.Height - 4);
_cardPath = GetRoundedRect(cardBounds, _theme.BorderRadius);
using (var cardBrush = new SolidBrush(_theme.CardBackColor))
using (var cardPen = new Pen(_theme.CardBorderColor))
{
g.FillPath(cardBrush, _cardPath);
g.DrawPath(cardPen, _cardPath);
}
// Draw step dots below card
DrawStepDots(g, cardBounds);
}
private void DrawStepDots(Graphics g, Rectangle cardBounds)
{
int dotY = cardBounds.Bottom + 24;
int totalSteps = _form.Config.Steps.Count;
int totalWidth = totalSteps * 10 + (totalSteps - 1) * 6;
int startX = cardBounds.X + (cardBounds.Width - totalWidth) / 2;
for (int i = 0; i < totalSteps; i++)
{
int x = startX + i * 16;
var state = GetStepState(i);
Color dotColor = state switch
{
WizardStepState.Completed => _theme.StepperCompleteColor,
WizardStepState.Active => _theme.StepperActiveColor,
_ => _theme.StepperUpcomingColor
};
using (var dotBrush = new SolidBrush(dotColor))
g.FillEllipse(dotBrush, x, dotY, 10, 10);
}
}
private GraphicsPath GetRoundedRect(Rectangle rect, int radius)
{
var path = new GraphicsPath();
int d = radius * 2;
path.AddArc(rect.X, rect.Y, d, d, 180, 90);
path.AddArc(rect.Right - d, rect.Y, d, d, 270, 90);
path.AddArc(rect.Right - d, rect.Bottom - d, d, d, 0, 90);
path.AddArc(rect.X, rect.Bottom - d, d, d, 90, 90);
path.CloseFigure();
return path;
}
}Card transition animation types supported by the engine:
| Animation Type | Enum Value | Visual Effect |
|---|---|---|
| Slide to the right | SlideRight | New card slides in from right, old slides out left |
| Slide to the left | SlideLeft | New card slides in from left, old slides out right |
| Cross-fade | Fade | Old card fades out, new card fades in simultaneously (opacity lerp) |
| Flip horizontal | FlipHorizontal | Card rotates 180° on Y axis — back face shows old content, front face shows new |
| Flip vertical | FlipVertical | Card rotates 180° on X axis |
Examples
// Style selection — the factory creates the right painter automatically
var config = new WizardConfig
{
Style = WizardStyle.VerticalStepper,
// ...steps, etc.
};
// Internally: painter = WizardPainterFactory.Create(WizardStyle.VerticalStepper)
// which returns new VerticalStepperPainter()
// Change style programmatically
config.Style = WizardStyle.HorizontalStepper;
// Now uses HorizontalStepperPainter
// Register a custom painter
WizardPainterFactory.Register(WizardStyle.VerticalStepper, () => new CustomStepperPainter());
// All VerticalStepper wizards now use CustomStepperPainter
// Custom painter example — subclass and override rendering
public class CustomStepperPainter : VerticalStepperPainter
{
protected override void DrawCheckmark(Graphics g, Point center, int size)
{
// Custom checkmark with star instead
using (var pen = new Pen(Color.Gold, 3))
{
DrawStar(g, pen, center, size);
}
}
}
// Access painter for direct manipulation
var form = WizardManager.GetWizard("my-wizard").FormHost as VerticalStepperWizardForm;
var painter = form.Painter as VerticalStepperPainter;
// painter now accessible for custom tweaks (rarely needed)