ZeppelinForms 0.15.1
dotnet add package ZeppelinForms --version 0.15.1
NuGet\Install-Package ZeppelinForms -Version 0.15.1
<PackageReference Include="ZeppelinForms" Version="0.15.1" />
<PackageVersion Include="ZeppelinForms" Version="0.15.1" />
<PackageReference Include="ZeppelinForms" />
paket add ZeppelinForms --version 0.15.1
#r "nuget: ZeppelinForms, 0.15.1"
#:package ZeppelinForms@0.15.1
#addin nuget:?package=ZeppelinForms&version=0.15.1
#tool nuget:?package=ZeppelinForms&version=0.15.1
ZeppelinForms
ZeppelinForms (ZF) is an experimental project aimed at creating a simple, platform-independent UI framework with hardware acceleration (on Windows) and straightforward code-behind UI development.
⚙️ Current Status
The project is under active development.
| # | Name | Status |
|---|---|---|
| 1 | Headless | ✅ |
| 2 | Windows | ✅ |
| 3 | Linux (X11) | ✅ |
| 4 | WebAssembly | ✅* |
| 5 | Android | ✅ |
| 6 | macOS | 💡 |
* - WebAssembly: dialogs are async-only, and system drag and drop is not supported — see the browser section below.
Android: dialogs are async-only, and there is no file dialog yet.
🐥 TL;DR
| Reason to choose Zeppelin Forms | WinForms | WPF | Avalonia |
|---|---|---|---|
| Cross-platform UI | ✓ | ✓ | |
| Simple code-first UI | ✓ | ✓ | |
| Modern controls out of the box | ✓ | ✓ |
Completely free. No registration required.
🧠 Philosophy
In short: combine the simplicity of WinForms with selected ideas from WPF and Flutter, the cross-platform capabilities of Avalonia, and get rid of tons of legacy baggage along the way.
- No dependency on a specific platform
- No dependency on a specific graphics stack
And if the project is modern, why not take advantage of the full capabilities of .NET 10 and C# 14?
🖌️ Rendering
| # | Name | Status |
|---|---|---|
| 1 | SkiaSharp | ✅ |
| 2 | DirectX | 💡 |
ZeppelinForms itself knows nothing about Skia, because it is implemented in a separate project, ZeppelinForms.Skia. This means that the graphics layer is completely decoupled from the framework logic.
As a result, the framework can be integrated with DirectX or virtually any other graphics stack.
If hardware acceleration is unavailable, the framework falls back to software rendering.
📟 Forms
Forms are the only type of window in ZeppelinForms, just like in WinForms.
| # | Name | Status |
|---|---|---|
| 1 | Debugger | ✅* |
| 2 | Overlays | ✅** |
| 3 | Toast Notifications | ✅*** |
| 4 | ToolTips | ✅ |
| 5 | Dialog Windows | ✅ |
| 6 | Open/Save File Dialog | ✅**** |
| 7 | MessageBox | ✅ |
| 8 | InputBox | ✅ |
| 9 | Clipboard | ✅ |
| 10 | Folder Dialog | ✅**** |
| 11 | Drag&Drop (internal) | ✅ |
* — the inspector currently works only with certain types
** — the API will be extended further
*** — some limitations and unfinished parts remain
**** - managed implementation by default; a platform may supply a system picker through IFilePicker, as the browser backend does
🛣️ Layout
- Supports
MeasureandArrange - All controls support docking
- Supports horizontal and vertical alignment, including content alignment
🧩 Controls
All controls must inherit from UIElement, either directly or indirectly through UnitControl, PanelControl, or WrapControl.
There are no WinForms-style components that are not considered actual controls, such as Timer or BackgroundWorker.
UIElement
The common base type for all controls. A form is not a UIElement, but its Content can be any UIElement.
Unlike WinForms, all controls support:
- Internal padding
- Transparency
- Shadows (
box-shadow) - Scaling
Base class hierarchy
UIElement defines geometry, input and painting entry points. Decoration —
background, corner radius, border — lives one level down. Inherit the closest
base that already does what you need:
Draw is sealed in every Decorated* class: it fills the background, calls
your content, then draws the border. Override these instead:
| Base | Override | Sealed |
|---|---|---|
DecoratedControl |
DrawContent (required), DrawDecoration |
Draw |
DecoratedPanel |
DrawContent, DrawDecoration, MeasureContentOverride, ArrangeContentOverride |
Draw, MeasureOverride, ArrangeOverride |
DecoratedWrapControl |
DrawContent, DrawDecoration |
Draw |
Decorated* is not a fourth category — it is a decoration layer inside each of
the three. DecoratedControl is a UnitControl, so it inherits every default
UnitControl sets, alignment included.
DrawContent runs before children, DrawDecoration after them and outside
their clip — that is where selection outlines, resize handles and drop
indicators go. For state-dependent colors override CurrentBackground and
CurrentBorderColor rather than painting the background yourself.
Shape is the one deliberate exception: shapes have their own Fill and
Stroke, so an inherited Background would only confuse.
Styled Properties
Any property a theme may set must be declared as a styled property. A plain
auto-property gets overwritten by the theme, is invisible to PropertyGrid
and does not trigger a repaint. A source generator expands three lines into
the registration, the backing field and the accessors:
[Styled(Category = "Menu")]
public partial Color HoverColor { get; set; }
private static Color HoverColorDefault => new(255, 232, 240, 254);
Requirements: the property is partial with a getter and a setter, and the type
declaring it is a partial descendant of UIElement. The default comes from a
static property named <Name>Default; omit it when default(T) will do.
Flags: AffectsLayout = true when the value changes measurement — the setter
then invalidates layout instead of only repainting. Inherits = true when the
value cascades down the tree, as TextColor does.
Value precedence, highest first:
- set from user code
- set from user code on any ancestor, for inherited properties
- a binding
- theme or style
- control default —
SetControlDefaultfrom a constructor DefaultValuefrom the registration
A constructor must use SetControlDefault: a plain assignment there would mark
the value as user-set and lock the theme out for good. ZF0006 reports this.
External = true when the value lives in another object, as TextBox.Text does
in its TextDocument. The generator then emits only the registration; the
control writes the accessors itself, routing the setter through
SetValue(Property, value) and exposing a Write<Name> method for the
registration to call directly.
Unit Controls
UIElement → UnitControl
Unit controls are similar to Control in WinForms and can be thought of as regular controls. They cannot contain child controls.
| № | Name | Status | № | Name | Status |
|---|---|---|---|---|---|
| 1 | Label | ✅ | 23 | LineChart | ✅ |
| 2 | Button | ✅ | 24 | PieChart | ✅ |
| 3 | CheckBox | ✅ | 25 | RichLabel | ✅ |
| 4 | PictureBox | ✅ | 26 | LinkLabel | ✅ |
| 5 | RadioButton | ✅ | 27 | LineShape | ✅ |
| 6 | TextBox | ✅* | 28 | RectangleShape | ✅ |
| 7 | ToggleSwitch | ✅ | 29 | EllipseShape | ✅ |
| 8 | DateTimePicker | ✅ | 30 | PolygonShape | ✅ |
| 9 | TimePicker | ✅ | 31 | CheckedComboBox | ✅ |
| 10 | ColorPicker | ✅ | 32 | ComboBox | ✅ |
| 11 | ScrollBar | ✅ | 33 | GridSplitter | ✅ |
| 12 | SvgIcon | ✅ | 34 | MaskedTextBox | ✅ |
| 13 | NumericUpDown | ✅ | 35 | HintLabel | ✅ |
| 14 | ProgressBar | ✅ | 36 | MapControl | ✅ |
| 15 | CircularProgressBar | ✅ | 37 | Loader | ✅ |
| 16 | TrackBar | ✅ | 38 | PageIndicator | ✅ |
| 17 | Calendar | ✅ | 39 | DataGridView | ✅** |
| 18 | MenuBar | ✅ | 40 | Table | ✅ |
| 19 | MenuList | ✅ | 41 | RadarChart | ✅ |
| 20 | SplitButton | ✅ | 42 | CandlestickChart | ✅ |
| 21 | ToggleButton | ✅ | |||
| 22 | BarChart | ✅ |
* — contains bugs and is missing part of its API ** — DataGridView: no cell editing or frozen columns yet
Panels
UIElement → PanelControl
Panels are controls that can contain other controls, including other panels.
| # | Name | Status |
|---|---|---|
| 1 | Panel | ✅ |
| 2 | StackPanel | ✅ |
| 3 | Grid | ✅ |
| 4 | DockPanel | ✅ |
| 5 | TabControl | ✅ |
| 6 | UniformGrid | ✅ |
| 7 | VirtualizingStackPanel | ✅ |
| 8 | SplitContainer | ✅ |
| 9 | PageControl | ✅ |
| 10 | WrapPanel | ✅ |
| 11 | Table | ✅ |
| 12 | AttachButton | ✅ |
| 13 | PropertyGrid | ✅ |
Items Panels
UIElement → PanelControl → ItemsControl
A specialized type of panel capable of working with collections of elements.
| # | Name | Status |
|---|---|---|
| 1 | ListBox | ✅ |
| 2 | CheckedListBox | ✅ |
| 3 | DragList | ✅ |
| 4 | TreeView | ✅ |
⭐ All panels can display a scrollbar when their content overflows, and scroll by finger with kinetic flings and rubber-band overscroll on touch devices.
Wrapper Controls
UIElement → WrapControl
Wrapper controls are controls that can contain a single child control. This concept is unusual in the WinForms world, but familiar from XAML-based frameworks.
| # | Name | Status |
|---|---|---|
| 1 | Border | ✅ |
| 2 | Spoiler | ✅ |
| 3 | ZoomBox | ✅ |
| 4 | GroupBox | ✅ |
| 5 | LayoutBuilder | ✅ |
| 6 | Page | ✅ |
| 7 | GradientBorder | ✅ |
| 8 | GripBox | ✅ |
🎄 Themes
Built-in light and dark themes are included. A theme is a set of appliers matched by control type; they are applied from the base type down, so a specific applier extends the base one instead of replacing it.
A theme never overwrites a value set from user code — see Styled Properties
above for the full precedence. ClearValue gives a property back to the theme.
Also available fluent and high contrast themes!
🎞️ Motion
Transitions belong to the property system, not to individual controls. A rule on a property makes assignment the target while the visible value travels to it:
card.Transitions.Add(Transition.Ease(DecoratedControl.BackColorProperty, 200));
list.ChildrenLayoutTransition = LayoutTransition.Speed();
list.ChildrenEnterTransition = VisibilityTransition.Slide(220, 0f, -12f);
list.ChildrenExitTransition = VisibilityTransition.Fade(160);
Reading a property returns the target — bindings and logic never see a halfway value — while the intermediate value is handed out inside drawing only.
Layout travel works by the FLIP rule: an element the layout moved is already in
its new place but is drawn from the old one. Nothing in the controls knows about
it, because expanding a tree node, reflowing a WrapPanel and inserting into a
DragList are all the same event: a changed position in Arrange.
Motion.Preference follows the system setting where the platform has one, and
reduced motion makes all of the above instant while leaving scroll inertia and
loading indicators alone.
🔗 Bindings
Any styled property can be bound to a property of any object:
nameBox.Bind(TextBox.TextProperty, user, nameof(User.Name), BindingMode.TwoWay);
statusLabel.Bind(Label.TextProperty, user, nameof(User.Status));
saveButton.Bind(UIElement.IsEnabledProperty, user, nameof(User.HasChanges));
OneWay follows the source; TwoWay also writes back. A source implementing
INotifyPropertyChanged pushes its changes to the target — without it a binding
only reads the source once, when it is created.
A binding holds its target weakly, so a model outliving a window does not keep
that window alive. Unbind drops one property, UnbindAll drops them all, and
ClearValue removes the binding together with the value.
Assigning a bound property from code wins over the binding: in TwoWay the
value goes to the source, in OneWay the binding is broken — otherwise the next
source change would silently overwrite what was just written.
🛠️ Code Examples
Creating an application in Windows:
public class Program
{
static void Main()
{
WindowsPlatform windowsPlatform = new();
App myApp = new(windowsPlatform)
{
MainForm = new MainForm()
};
myApp.Run();
}
}
Creating an application in Linux (X11):
public class Program
{
static void Main()
{
X11Platform linuxPlatform = new();
App myApp = new(linuxPlatform)
{
MainForm = new MainForm()
};
myApp.Run();
}
}
Creating an application in the browser (WebAssembly):
public class Program
{
static Task Main() => BrowserApp.RunAsync(
() => new MainForm(),
font: "/fonts/Inter-Regular.ttf",
preload: ["/Assets/Logo.png"]);
}
A factory is passed rather than a form instance: a form measures text in its constructor, and the text measurer only exists once the platform has been created.
The browser has no system fonts and no local file system, so font and preload
name files served from wwwroot. BrowserApp downloads them into the virtual
file system before the first form is built, after which Image.LoadAsset and
Font.WithFile work exactly as they do on the desktop.
Two host files are needed alongside the application. wwwroot/index.html:
<body>
<canvas id="zf-canvas"></canvas>
<script type="module" src="main.js"></script>
</body>
and wwwroot/main.js, which loads the runtime and wires up the JavaScript module:
import { dotnet } from "./_framework/dotnet.js";
import * as zf from "./zf.js";
const runtime = await dotnet.create();
runtime.setModuleImports("zf", { ...zf });
const exports = await runtime.getAssemblyExports("ZeppelinForms.Browser");
globalThis.zfExports = exports.ZeppelinForms.Browser.Interop;
await runtime.runMain();
zf.js ships with ZeppelinForms.Browser and has to be copied into the
application's wwwroot. See examples/ZF Wasm for a complete project.
📺 Previewer
Install previewer extension for VS 2022 and VS 2026.
Press Ctrl+Alt+Shift+P for showing previewer window.
See needed Form or use [Previewer] attribute otherwise.
📝 Markup
Code-first is the main way, and markup is an option for the parts that read better as a
tree. A .zfml file names its class with x:Class; at build time the generator turns it
into C# — a field for every x:Name and an InitializeComponent() of plain new,
assignments, += and Add. No reflection, so trimming and native AOT keep it all.
<StackPanel x:Class="MyApp.SignUpCard"
xmlns="https://zeppelinforms.dev/markup"
xmlns:x="https://zeppelinforms.dev/markup/x"
xmlns:my="using:MyApp.Controls"
Orientation="Vertical" Spacing="10">
<TextBox x:Name="NameBox" Watermark="Your name" Size="260,NaN"/>
<Label Text="{Bind NameBox.Text}"/>
<PrimaryButton Text="Sign up" Classes="wide" Click="OnSubmit"/>
<my:Gauge Level="0.5"/>
</StackPanel>
public partial class SignUpCard
{
private void OnSubmit(object? sender, MouseClickEventArgs e) { /* NameBox.Text … */ }
}
- Values are written as in style sheets:
Padding="14 6"isnew Thickness(14, 6),Size="260,NaN",Background="#0d6efd", enum members, static members by name - Children go to the first property that takes them — one marked
[Content], thenChildren,Items,Nodes,Content,Child;<Border.Child>names one {Bind Name.Property}binds a styled property to a named element,Mode=TwoWayboth ways;{x:Null}is null;<x:String>is a text item of a list- An unknown type, property, event or value is a ZF2xxx error at its line of the markup, and so is a handler of the wrong signature
- A class whose code declares no constructor gets one that calls
InitializeComponent; Hot Reload builds the markup again after an edit
🔥 Hot Reload
Visual Studio's Hot Reload and dotnet watch work with code-first forms. After every
applied edit the open forms restyle, measure and redraw everything, so an edited Draw,
MeasureOverride or theme rule shows at once, and pages made by factories — the entries
of a SplitView, the pages of a PageControl — are made again when their assembly
changed. A form built in its constructor builds itself again in OnHotReload:
public class MainForm : Form
{
public MainForm()
{
Title = "Orders";
Content = CreateContent();
}
private UIElement CreateContent() => new StackPanel { /* ... */ };
protected override void OnHotReload(HotReloadEventArgs e) =>
Content = CreateContent();
}
UIElement.OnHotReload does the same for a control that builds its children itself, and
HotReload.Applied is raised once the forms have caught up. Style sheets loaded from
files follow their saves on their own.
🧪 Snapshot Tests
Reference snapshots are stored in:
tests/ZeppelinForms.UnitTests/Snapshots/Expected/{win,linux}
Text rendering differs between platforms, so separate snapshot sets are maintained for Windows and Linux.
Local snapshot update:
- bash:
ZF_UPDATE_SNAPSHOTS=true dotnet test - PowerShell:
$env:ZF_UPDATE_SNAPSHOTS='true'; dotnet test
📈 Benchmarks
bench/ZeppelinForms.Benchmarks measures layout, rendering, text and the
scrolling of a virtualized list against a stored baseline:
dotnet run --project bench/ZeppelinForms.Benchmarks -c Release
dotnet run --project bench/ZeppelinForms.Benchmarks -c Release -- --update-baseline
Run-to-run spread on a desktop machine reaches tens of percent, so a single run
proves nothing: take the median of three, and record a baseline only from a quiet
machine. Current numbers are in the release entry in CHANGELOG.md.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- No dependencies.
NuGet packages (5)
Showing the top 5 NuGet packages that depend on ZeppelinForms:
| Package | Downloads |
|---|---|
|
ZeppelinForms.Skia
The SkiaSharp renderer of ZeppelinForms, shared by the platform packages. |
|
|
ZeppelinForms.Windows
The Windows platform of ZeppelinForms: Win32 windows and input, clipboard, drag and drop, the system theme, and UI Automation for screen readers. |
|
|
ZeppelinForms.Android
The Android platform of ZeppelinForms: a view hosting the forms, touch input, the on-screen keyboard, the application lifecycle and the system theme. |
|
|
ZeppelinForms.Linux
The Linux platform of ZeppelinForms: X11 windows and input, and the system theme through the XDG desktop portal. |
|
|
ZeppelinForms.Browser
The WebAssembly platform of ZeppelinForms: a canvas hosting the forms, pointer and keyboard input, and an ARIA mirror for screen readers. The page needs the zf.js module, attached to every release. |
GitHub repositories
This package is not used by any popular GitHub repositories.