--- name: dotnet-winforms-basics description: >- Builds WinForms on .NET 8+. High-DPI, dark mode (experimental), DI patterns, modernization. metadata: short-description: .NET skill guidance for ui tasks --- # dotnet-winforms-basics WinForms on .NET 8+: updated project templates with Host builder and DI, high-DPI support with `PerMonitorV2`, dark mode via `Application.SetColorMode` (experimental in .NET 9, targeting finalization in .NET 11), when to use WinForms, modernization tips for migrating from .NET Framework, and common agent pitfalls. **Version assumptions:** .NET 8.0+ baseline (current LTS). TFM `net8.0-windows`. .NET 9 features (dark mode experimental) explicitly marked. .NET 11 finalization targets noted. ## Scope - WinForms .NET 8+ project setup (SDK-style) - High-DPI support with PerMonitorV2 - Dark mode via Application.SetColorMode (experimental) - Host builder and DI patterns - Modernization tips from .NET Framework ## Out of scope - WinForms .NET Framework patterns (legacy) - Migration guidance -- see [skill:dotnet-wpf-migration] - Desktop testing -- see [skill:dotnet-ui-testing-core] - General Native AOT patterns -- see [skill:dotnet-native-aot] - UI framework selection -- see [skill:dotnet-ui-chooser] Cross-references: [skill:dotnet-ui-testing-core] for desktop testing, [skill:dotnet-wpf-modern] for WPF patterns, [skill:dotnet-winui] for WinUI 3 patterns, [skill:dotnet-wpf-migration] for migration guidance, [skill:dotnet-native-aot] for general AOT, [skill:dotnet-ui-chooser] for framework selection. --- ## .NET 8+ Differences WinForms on .NET 8+ is a significant modernization from .NET Framework WinForms, with an SDK-style project format, DI support, and updated APIs. ### New Project Template ````xml WinExe net8.0-windows true enable enable ```text **Key differences from .NET Framework WinForms:** - SDK-style `.csproj` (no `packages.config`, no `AssemblyInfo.cs`) - Nullable reference types enabled by default - Implicit usings enabled - NuGet `PackageReference` format - `Program.cs` uses top-level statements - `dotnet publish` produces a single deployment artifact - Side-by-side .NET installation (no machine-wide framework dependency) ### Host Builder Pattern Modern WinForms apps use the generic host for dependency injection: ```csharp // Program.cs using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; ApplicationConfiguration.Initialize(); var host = Host.CreateDefaultBuilder() .ConfigureServices((context, services) => { // Services services.AddSingleton(); services.AddSingleton(); // HTTP client services.AddHttpClient("api", client => { client.BaseAddress = new Uri("https://api.example.com"); }); // Forms services.AddTransient(); services.AddTransient(); }) .Build(); var mainForm = host.Services.GetRequiredService(); Application.Run(mainForm); ```text ```csharp // MainForm.cs -- constructor injection public partial class MainForm : Form { private readonly IProductService _productService; private readonly IServiceProvider _serviceProvider; public MainForm(IProductService productService, IServiceProvider serviceProvider) { _productService = productService; _serviceProvider = serviceProvider; InitializeComponent(); } private async void btnLoad_Click(object sender, EventArgs e) { var products = await _productService.GetProductsAsync(); dataGridProducts.DataSource = products.ToList(); } private void btnDetails_Click(object sender, EventArgs e) { var detailForm = _serviceProvider.GetRequiredService(); detailForm.ShowDialog(); } } ```text ### ApplicationConfiguration.Initialize .NET 8+ WinForms uses `ApplicationConfiguration.Initialize()` as the entry point, which consolidates multiple legacy configuration calls: ```csharp // ApplicationConfiguration.Initialize() is equivalent to: Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.SetHighDpiMode(HighDpiMode.SystemAware); // default; override below for PerMonitorV2 ```text --- ## High-DPI WinForms on .NET 8+ has significantly improved high-DPI support. The recommended mode is `PerMonitorV2`, which handles per-monitor DPI changes automatically. ### Enabling PerMonitorV2 ```csharp // Program.cs -- set before ApplicationConfiguration.Initialize() Application.SetHighDpiMode(HighDpiMode.PerMonitorV2); ApplicationConfiguration.Initialize(); // Note: SetHighDpiMode() called before Initialize() takes precedence // over the default SystemAware mode set by Initialize(). ```text Or configure via `runtimeconfig.json`: ```json { "runtimeOptions": { "configProperties": { "System.Windows.Forms.ApplicationHighDpiMode": 3 } } } ```text **High-DPI modes:** | Mode | Value | Behavior | |------|-------|----------| | `DpiUnaware` | 0 | No scaling; system bitmap-stretches the window | | `SystemAware` | 1 | Scales to primary monitor DPI at startup (default in .NET 8) | | `PerMonitor` | 2 | Adjusts when moved between monitors (basic) | | `PerMonitorV2` | 3 | Full per-monitor scaling with non-client area support **(recommended)** | | `DpiUnawareGdiScaled` | 4 | DPI-unaware but GDI+ text renders at native resolution | ### DPI-Unaware Designer Mode (.NET 9+) .NET 9 introduces a DPI-unaware designer mode that prevents layout scaling issues in the Visual Studio WinForms designer. The designer renders at 96 DPI regardless of system DPI, preventing corrupted `.Designer.cs` files. ```xml true ```csharp ### Scaling Gotchas - **Do not use absolute pixel sizes for controls.** Use `AutoScaleMode.Dpi` on forms and let the layout engine scale controls automatically. - **Anchor and Dock layouts scale better than absolute positioning.** `TableLayoutPanel` and `FlowLayoutPanel` handle DPI changes more reliably than fixed-position controls. - **Custom drawing (OnPaint) must use DPI-aware coordinates.** Scale drawing coordinates by `DeviceDpi / 96.0f` in `OnPaint` overrides. - **Image resources need multiple resolutions.** Provide 1x, 1.5x, and 2x versions of icons and images, or use SVG-based rendering. ```csharp // DPI-aware custom drawing protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); float scale = DeviceDpi / 96.0f; float fontSize = 12.0f * scale; using var font = new Font("Segoe UI", fontSize); e.Graphics.DrawString("Scaled text", font, Brushes.Black, 10 * scale, 10 * scale); } ```text --- ## Dark Mode WinForms dark mode is **experimental in .NET 9** and is **targeting finalization in .NET 11**. It provides system-integrated dark mode for WinForms controls using the Windows dark mode APIs. ### Enabling Dark Mode (.NET 9+ Experimental) ```csharp // Program.cs -- set before ApplicationConfiguration.Initialize() Application.SetColorMode(SystemColorMode.Dark); ApplicationConfiguration.Initialize(); ```csharp Or follow system theme: ```csharp // Follow system light/dark preference Application.SetColorMode(SystemColorMode.System); ```csharp **SystemColorMode values:** | Mode | Behavior | |------|----------| | `Classic` | Standard WinForms colors (no dark mode) | | `System` | Follow Windows system light/dark theme setting | | `Dark` | Force dark mode | ### Dark Mode Caveats - **Experimental status:** The API surface may change before .NET 11 finalization. Do not depend on specific color values or rendering behavior in production. - **Control coverage:** Not all controls support dark mode in .NET 9. Standard controls (Button, TextBox, Label, ListBox, DataGridView) have dark mode support. Third-party and custom-drawn controls may not render correctly. - **Owner-drawn controls:** Controls using `DrawMode.OwnerDrawFixed` or custom `OnPaint` overrides must manually read `SystemColors` to respond to dark mode. They do not automatically inherit dark mode colors. - **Windows version:** Dark mode requires Windows 10 version 1809 (build 17763) or later. - **.NET 11 target:** Microsoft has indicated that WinForms visual styles (including dark mode) are targeting finalization in .NET 11. Plan for API stability after that release. ```csharp // Owner-drawn controls must use SystemColors for dark mode compatibility protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); // Use SystemColors instead of hardcoded colors using var textBrush = new SolidBrush(SystemColors.ControlText); using var bgBrush = new SolidBrush(SystemColors.Control); e.Graphics.FillRectangle(bgBrush, ClientRectangle); e.Graphics.DrawString("Text", Font, textBrush, 10, 10); } ```text --- ## When to Use WinForms is the right choice for specific scenarios. It is not a general-purpose UI framework for new customer-facing applications. ### Good Fit - **Rapid prototyping:** Drag-and-drop designer for quick internal tools and proof-of-concept UIs - **Internal enterprise tools:** Line-of-business forms, data entry, CRUD applications with DataGridView - **Simple Windows-only utilities:** System tray apps, configuration tools, diagnostics dashboards - **Existing WinForms maintenance:** Modernizing existing .NET Framework WinForms apps to .NET 8+ - **Data-heavy tabular UIs:** DataGridView with virtual mode handles millions of rows efficiently ### Not a Good Fit - **New customer-facing applications:** Use WPF (rich Windows desktop), WinUI 3 (modern Windows), MAUI (cross-platform), or Blazor (web) - **Complex custom UI:** WinForms controls are limited in styling; WPF or WinUI provide rich templating - **Cross-platform requirements:** WinForms is Windows-only; use MAUI or Uno Platform - **Accessibility-first applications:** WPF and WinUI have better accessibility APIs and screen reader support - **Touch-optimized interfaces:** WinForms was designed for mouse/keyboard; WinUI or MAUI handle touch better ### Decision Guidance | Scenario | Recommended Framework | |----------|----------------------| | Quick internal tool | WinForms | | Data entry form (Windows) | WinForms or WPF | | Modern Windows desktop app | WinUI 3 or WPF (.NET 9+ Fluent) | | Cross-platform mobile + desktop | MAUI or Uno Platform | | Cross-platform + web | Uno Platform or Blazor | | Existing WinForms modernization | WinForms on .NET 8+ | For the full framework decision tree, see [skill:dotnet-ui-chooser]. --- ## Modernization Tips Tips for modernizing existing .NET Framework WinForms applications to .NET 8+. ### Add Dependency Injection Replace static references and singletons with constructor injection via Host builder (see .NET 8+ Differences section above). **Before (legacy pattern):** ```csharp // Anti-pattern: static service references public partial class MainForm : Form { private void btnLoad_Click(object sender, EventArgs e) { var products = ProductService.Instance.GetProducts(); dataGridProducts.DataSource = products; } } ```text **After (modern pattern):** ```csharp // Modern: constructor injection public partial class MainForm : Form { private readonly IProductService _productService; public MainForm(IProductService productService) { _productService = productService; InitializeComponent(); } private async void btnLoad_Click(object sender, EventArgs e) { var products = await _productService.GetProductsAsync(); dataGridProducts.DataSource = products.ToList(); } } ```text ### Use Async Patterns Replace synchronous blocking calls with async/await to keep the UI responsive: ```csharp // Before: blocks UI thread private void btnSave_Click(object sender, EventArgs e) { var client = new HttpClient(); var result = client.PostAsync(url, content).Result; // BLOCKS UI MessageBox.Show("Saved!"); } // After: async keeps UI responsive private async void btnSave_Click(object sender, EventArgs e) ## Detailed Examples See [references/detailed-examples.md](references/detailed-examples.md) for complete code samples and advanced patterns.