Repository navigation
Forms and Windows
A form is the only kind of window, as in WinForms. It is not an element itself: its
Content is, and everything inside the window hangs from it.
public class OrdersForm : Form
{
public OrdersForm()
{
Title = "Orders";
Size = new Size(1024, 768);
MinimumSize = new Size(640, 480);
WindowStartupLocation = WindowStartupLocation.CenterScreen;
Content = CreateContent();
}
}| Event | When |
|---|---|
Shown |
The window is on the screen for the first time — a good place to start loading data |
Closing |
The window is about to close; e.Cancel = true keeps it open |
Closed |
The window is gone |
| Property | Units | Notes |
|---|---|---|
Size |
logical units | The window's size; with SizeToContent the client area on every platform |
ClientSize |
logical units | The area inside the frame |
Position |
screen pixels | The top-left corner, in the units Displays reports screens in |
MinimumSize, MaximumSize
|
logical units | A NaN axis has no limit |
Position is in screen pixels because with screens of different scales a logical point
of the desktop has no single meaning. On Linux it is the corner of the client area: the
frame belongs to the window manager.
Both Size and Position work both ways while the window is open: setting them moves or
resizes the window, and a move or resize by the user updates them. LocationChanged and
SizeChanged tell about either; ClientSizeChanged follows the area inside the frame.
form.SizeChanged += (_, _) => settings.WindowSize = form.Size;
form.LocationChanged += (_, _) => settings.WindowPosition = form.Position;The limits keep the user's dragging inside them, and an open window outside new limits is brought inside. Setting a minimum above the maximum raises the maximum, and the other way round, as in WinForms.
| Member | Does |
|---|---|
TopMost |
Keeps the window above every window that isn't topmost — a tool palette, a timer |
CenterToScreen() |
Centers the window in the working area of its screen (the taskbar left out) |
WindowState |
Normal, minimized or maximized |
WindowStartupLocation |
Manual (uses Position), CenterScreen, CenterOwner
|
The browser and Android have no windows in this sense: these members are accepted and do nothing there.
SizeToContent makes the window as large as what it shows — a dialog whose text and
buttons decide its size. MaximumSize.Width is then also where long text wraps.
var dialog = new Form
{
Title = "About",
SizeToContent = SizeToContent.WidthAndHeight,
MaximumSize = new Size(480, float.NaN),
Content = aboutPanel,
};Form.Font is the font of every element of the form that has no font of its own and
inherits none. It is never null: until set it is Font.Default, read live, so a theme
switch reaches a form that set no font. Assigning null resets it.
A form can be shown as a modal dialog over an owner. The dialog closes itself with a
result through Accept(value).
DialogResult<string> result = await new RenameForm(name).ShowDialogAsync<string>(this);
if (result.IsAccepted)
Rename(result.Value);ShowDialog<T> blocks the calling code in a nested loop on the desktop; the browser and
Android have none, so use ShowDialogAsync there — and everywhere, if the code is shared.
MessageBoxResult answer = await MessageBox.ShowAsync(
this, "Save the changes?", "Orders", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question);
bool sure = await MessageBox.ConfirmAsync(this, "Delete the order?");
await MessageBox.ErrorAsync(this, "The server did not answer.");
string? name = await InputBox.ShowAsync(this, "What is your name?", initialValue: "Zeppelin");
decimal? amount = await InputBox.ShowNumberAsync(this, "Amount");The boxes are sized to their message and buttons, take the theme's colors and follow the interface language.
string? path = FileDialog.OpenFile(this, new FileDialogOptions
{
Title = "Open an order",
Filters = [new FileFilter("Orders", "csv", "json"), new FileFilter("All files")],
});
string[] images = await FileDialog.OpenFilesAsync(this, new() { Filters = [new("Images", "png", "jpg")] });
string? target = await FileDialog.SaveFileAsync(this, new() { FileName = "report.csv" });
string? folder = await FileDialog.SelectFolderAsync(this);The dialog is the system's where there is one: Explorer's own on Windows (IFileDialog),
the desktop's on Linux through the XDG portal — GTK's on GNOME, KDE's on Plasma, the only
one that sees the user's files from a Flatpak or a Snap. Both are modal to the form, and
the synchronous calls show them too, the forms painting under them. Without a system dialog
— no portal, a bare window manager — or with UseNative = false in the options, the
framework's own is shown, styled by the theme. On Windows the system's dialog wants
[STAThread] on Main, as drag and drop does; without it the framework's own is shown.
A path is a desktop thing: in the browser a picked file has none, and a file to save is
written by the browser. PickedFile is the way to files the same everywhere — a name, the
Path where there is one, and streams:
if (await FileDialog.PickFileAsync(this, options) is { } file)
{
await using Stream input = await file.OpenReadAsync();
Load(input);
}
if (await FileDialog.PickSaveFileAsync(this, new() { FileName = "orders.csv" }) is { } target)
{
await using Stream output = await target.OpenWriteAsync();
await Export(output);
} // disposed: in the browser the file is written nowIn the browser the save goes through the browser's save dialog where it has one — Chromium
on the desktop — and writes the file the user chose; elsewhere it is a download under the
suggested name. PickedFile.FromPath makes one of a path got another way — a list of
recent files, the command line.
| Member | Shows |
|---|---|
ShowFlyout(anchor, content, placement) |
A popup next to an element, closed by a click outside or Escape |
AddOverlay(content) / RemoveOverlay
|
An element over the content that stays until removed |
ShowToast(message) |
A short notice that goes away on its own |
ShowContextMenu(items, position) |
A context menu |
A flyout that doesn't fit on its side opens on the opposite one and stays inside the window.
An element's ContextMenu opens on a right click, on the menu key, and on a finger or a
pen held still on it for half a second; an element without one passes the request to its
parent. An item with Items of its own opens them as a submenu beside it — when the
pointer rests on it, on a click or a tap, on Right or Enter; Left or Esc goes back.
form.Taskbar is the form's button on the taskbar or the dock: the progress of a long work
and a count, seen without opening the window. form.RequestAttention() asks for the user
without taking the focus — the button flashes until the window is activated.
form.Taskbar.ProgressValue = done / (double)total; // 0..1, turns the bar on
form.Taskbar.ProgressState = TaskbarProgressState.Paused; // Indeterminate, Normal, Paused, Error
form.Taskbar.Badge = unread; // null or 0 — none
form.Taskbar.Clear(); // a plain button again
form.RequestAttention(); // a background window only| Windows | Linux (X11, Wayland) | Browser | |
|---|---|---|---|
| Progress | the taskbar button | the launcher entry | — |
| Badge | an overlay in the button's corner | the launcher entry's count | an installed web app's badge |
| Attention | the button flashes | the window is marked | the tab's title blinks |
On Linux the progress and the badge go to the dock as the launcher entry
(com.canonical.Unity.LauncherEntry) of application://{App.Id}.desktop — KDE's task
manager, Dash to Dock and Plank show it, GNOME's own dash doesn't. It is the application's:
every form shows on the one entry, and App.Id has to be the name of its .desktop file.
PlatformCapabilities.TaskbarProgress, TaskbarBadge and WindowAttention tell what the
platform can. Values set before the form is shown come with its window.
Notifications.Show shows the system's notification — outside the windows, in the corner
of the screen and in the notification center. Clicked comes on the UI thread; Activate
brings the form forward from there.
var done = new Notification("Export finished", "orders.csv — 1 248 rows")
{
Kind = NotificationKind.Information, // None, Information, Warning, Error
};
done.Clicked += (_, _) => form.Activate();
Notifications.Show(done);
// or in one line; null — the platform has none
Notifications.Show("Sync failed", "The server didn't answer", NotificationKind.Error);
// from a tray icon: its balloon on Windows, as NotifyIcon.ShowBalloonTip
tray.ShowNotification("Synced", "3 files");| Platform | Shown as |
|---|---|
| Windows | a balloon of a tray icon, which Windows 10 and 11 show as a toast. A visible TrayIcon of the application carries it; without one the application's icon is put into the tray for as long as the notification lasts |
| Linux, X11 and Wayland |
org.freedesktop.Notifications on the session bus — GNOME, KDE, XFCE, mako, dunst. The desktop-entry hint is App.Id, so the application's .desktop file gives it its name and icon; an error stays until dismissed |
| Browser | the Notification API; the first notification asks the user's permission, and a refusal closes it |
Closed comes once: clicked, dismissed, timed out, or replaced by a newer balloon of the
same tray icon. PlatformCapabilities.Notifications tells whether there is a way; Android
has none yet.
form.Activate() brings a window forward and gives it the keyboard — a hidden form is
shown again first. The systems let an application do it after the user's own input, such
as a click on its notification or tray icon; otherwise the button flashes instead.
App.Id, App.DisplayName and App.Version say who the application is. Each is read from
the entry assembly until set — App.Version is its <Version>, without the "+commit" the SDK
appends — so a version kept in one place, the project file or Directory.Build.props, is
there without writing it again:
App.Id = "com.example.Orders"; // before Run: the windows are made with it
new App(new WindowsPlatform()) { MainForm = new MainForm() }.Run();
aboutLabel.Text = $"{App.DisplayName} {App.Version}";The id groups the windows on the taskbar and finds the application's .desktop file: it is the window class (WM_CLASS) on X11, the app_id on Wayland and the application user model id on Windows. Windows gets it only when it is set — a running window with an id no longer groups with a button pinned from a shortcut made without one.
The window belongs to the UI thread. Work from another thread reaches it through
form.Invoke(…), form.BeginInvoke(…) or the form's Dispatcher — see
Threading and timers.
F12 (or Ctrl+Shift+I) opens the inspector over any form: the element under the cursor with its margin and padding, its properties in a property grid, and the accessibility audit.
ZeppelinForms — fully free, for everyone. This wiki is generated from
docs/wiki:
edit it there, not here.
Start
Building the UI
Looks
Behaviour
Tooling