Getting started
This page builds a first PluginUI editor in a VST3 effect written with NPlug, then
shows the same toolkit in a standalone window. It assumes Windows 10 1809 or later (x64), the .NET 10 SDK and Visual
Studio 2026 (or any editor plus the dotnet command line).
The fastest way: a project generated from a design
PluginUIEditor creates a complete, buildable project around a design:
- Build and start the designer:
dotnet run --project tools/PluginUIEditor. - Open
samples/Designs/DemoSynth.pluginui, or start from an empty design and drop a few knobs on it; add the parameters in the Parameters tab and bind the knobs to them (Binding category of the properties). - File ▸ New project from design: choose VST3 effect or VST3 instrument (proposed for a design with a keyboard), a name and a folder.
- Build it:
dotnet build -c Debuggives a debuggable.vst3(CoreCLR proxy),dotnet publish -c Release -r win-x64a NativeAOT one. Copy the.vst3toC:\Program Files\Common Files\VST3and rescan in the DAW.
The project contains the four NPlug classes shown below, the editor split into a generated part (.Designer.cs) and
your part, and — in debug builds — the live design bridge.
By hand: the four classes of an NPlug plug-in
The project references NPlug (the patched copy, see the overview) and
PluginUI.NPlug, which brings PluginUI. A generated project or samples/PluginUI.DemoSynth shows the complete
.csproj (NativeAOT settings, debug proxy, .vst3 bundle).
Model — the parameters, shared by the processor and the controller:
using NPlug;
public sealed class GainModel : AudioProcessorModel
{
public GainModel() : base("Gain")
{
AddByPassParameter(); // mandatory for a VST3 effect
Gain = AddParameter(new AudioRangeParameter("Gain", units: "dB", minValue: -24.0, maxValue: 24.0, defaultPlainValue: 0.0) { Precision = 1 });
}
public AudioRangeParameter Gain { get; }
}
Processor — the audio thread. PluginUIProcessor<TModel> adds what the editor needs (notes played on screen,
shared state for meters):
using NPlug;
using PluginUI.NPlug;
public sealed class GainProcessor : PluginUIProcessor<GainModel>
{
public static readonly Guid ClassId = new("1f3a1c62-3a43-4d55-9b0a-5d3f7d9e2a10");
public GainProcessor() : base(AudioSampleSizeSupport.Float32) { }
public override Guid ControllerClassId => GainController.ClassId;
protected override bool Initialize(AudioHostApplication host)
{
AddAudioInput("Input", SpeakerArrangement.SpeakerStereo);
AddAudioOutput("Output", SpeakerArrangement.SpeakerStereo);
return true;
}
protected override void ProcessMain(in AudioProcessData data)
{
float gain = MathF.Pow(10f, (float)Model.Gain.Value / 20f);
var input = data.Input[0];
var output = data.Output[0];
for (int c = 0; c < Math.Min(input.ChannelCount, output.ChannelCount); c++)
{
var source = input.GetChannelSpanAsFloat32(ProcessSetupData, data, c);
var target = output.GetChannelSpanAsFloat32(ProcessSetupData, data, c);
for (int i = 0; i < data.SampleCount; i++) target[i] = source[i] * gain;
}
}
}
Controller — parameter edits from the editor and the host; PluginUIController<TModel> provides the
begin / perform / end edit bridge, MIDI learn and the shared state:
using System.Runtime.Versioning;
using PluginUI.NPlug;
public sealed class GainController : PluginUIController<GainModel>
{
public static readonly Guid ClassId = new("5c0d3b8e-8f0e-4c7b-a5f1-2e6b9c4d7a31");
[SupportedOSPlatform("windows10.0.17763")]
protected override PluginEditorView? CreateEditor() => new GainEditor(this);
}
Factory — declares both classes to the host when the module loads:
using System.Runtime.CompilerServices;
using NPlug;
public static class GainPlugin
{
[ModuleInitializer] // the host asks for the factory as soon as the module is loaded
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2255", Justification = "Plug-in entry point.")]
internal static void ExportThisPlugin()
{
var factory = new AudioPluginFactory(new AudioPluginFactoryInfo("My Company", "https://example.com", "contact@example.com"));
factory.RegisterPlugin<GainProcessor>(new AudioProcessorClassInfo(GainProcessor.ClassId, "Gain", AudioProcessorCategory.Effect));
factory.RegisterPlugin<GainController>(new AudioControllerClassInfo(GainController.ClassId, "Gain Controller"));
AudioPluginFactoryExporter.Instance = factory;
}
}
The editor
A PluginEditorView builds its element tree once per view, the first time the host asks for its size or attaches it. Controls bind to
parameters through Parameters[...]; everything is positioned in DIPs (1/96 inch) and scaled for the screen DPI:
using System.Runtime.Versioning;
using PluginUI;
using PluginUI.NPlug;
[SupportedOSPlatform("windows10.0.17763")]
public sealed class GainEditor(GainController controller)
: PluginEditorView(controller, new Size(240, 160), SizingMode.ScaleToFit)
{
protected override UIElement Build()
{
var model = controller.Model;
var root = new Canvas();
var gain = new Knob(Parameters[model.Gain]) { Label = "GAIN", ShowValueBubble = true };
gain.SetBounds(24, 24, 72, 96);
var bypass = new ToggleButton("Bypass") { Parameter = Parameters[model.ByPassParameter!] };
bypass.SetBounds(120, 56, 90, 28);
root.Add(gain, bypass, new CornerResizer { Left = 222, Top = 142, Width = 18, Height = 18 });
return root;
}
}
What the view does for you: the VST3 view life cycle and child window, DPI and content scale, size negotiation with the host, mouse wheel and keys forwarded by the host, the gestures that make automation and undo work in the DAW, and the DAW's context menu on a right-click over a bound control.
Next steps:
- browse the element catalog: meters fed from the processor with PeakValue, an on-screen keyboard for instruments, EQ and compressor displays…;
- change the look with a Theme, a vector style, or skins made of film strips, bitmaps or SVG (Extending PluginUI);
- press Ctrl+Shift+E in the plug-in window to move and resize the elements live, and Ctrl+C to copy the
corresponding code; turn the shortcut off in release builds (
host.LiveEditShortcutEnabled = falseinConfigureHost), as generated projects do.
A standalone window
The same elements run in a plain Windows application, without NPlug:
using PluginUI;
StandaloneWindow.EnablePerMonitorDpiAwareness();
var root = new Canvas();
root.Add(new Knob(new LocalParameter("Volume", -48, 6, -12, "dB", decimals: 1)) { Label = "VOLUME", Left = 20, Top = 20, Width = 72, Height = 96 });
var host = new UIHost(root, new Size(320, 200), SizingMode.ScaleToFit);
return new StandaloneWindow(host, "My application").Run();
LocalParameter is a parameter that lives in the application (linear, logarithmic or stepped).
samples/PluginUI.Gallery shows every element this way.