关于 dotnet-winforms-basics
dotnet-winforms-basics 是面向编码 Agent 和开发者的 Skill,介绍 .NET 8+ WinForms 的现代项目模式与实践。内容涵盖 Host builder、依赖注入、高 DPI、实验性深色模式,以及适用场景和现代化建议。
主要功能
- 01.NET 8+ 项目与 Host builder 模式
- 02Host builder 依赖注入
- 03PerMonitorV2 高 DPI 配置
- 04深色模式及其限制
- 05旧项目现代化与适用场景
适用场景
- 01创建使用依赖注入的 WinForms 应用
- 02为多显示器配置 DPI 缩放
- 03评估 WinForms 是否适合内部工具
- 04规划 .NET Framework 项目现代化
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill dotnet-winforms-basics安装后在 Claude Code 中输入/dotnet-winforms-basics确认已加载。
详细介绍
dotnet-winforms-basics
.NET 8+ 上的 WinForms:更新后的项目模板(采用 Host 生成器和 DI)、通过 PerMonitorV2 支持高 DPI、使用 Application.SetColorMode 实现深色模式(在 .NET 9 中为实验性功能,计划在 .NET 11 中最终定稿)、何时使用 WinForms、从 .NET Framework 迁移的现代化建议,以及常见的 Agent 陷阱。
**版本假设:**以 .NET 8.0+ 为基准(当前 LTS)。TFM 为 net8.0-windows。明确标记 .NET 9 功能(深色模式为实验性功能)。注明 .NET 11 的最终定稿目标。
**范围边界:**此技能负责现代 .NET 模式下的 WinForms:项目设置、高 DPI、深色模式、DI、适用场景和现代化。从 .NET Framework 迁移到 .NET 8+ 由 [skill:dotnet-wpf-migration] 负责。桌面测试由 [skill:dotnet-ui-testing-core] 负责。
**范围之外:**WinForms .NET Framework 模式(旧版)——此技能仅涵盖 .NET 8+。迁移指南——参见 [skill:dotnet-wpf-migration]。桌面测试——参见 [skill:dotnet-ui-testing-core]。通用 Native AOT 模式——参见 [skill:dotnet-native-aot]。UI 框架选择——参见 [skill:dotnet-ui-chooser]。
交叉引用:[skill:dotnet-ui-testing-core](桌面测试)、[skill:dotnet-wpf-modern](WPF 模式)、[skill:dotnet-winui](WinUI 3 模式)、[skill:dotnet-wpf-migration](迁移指南)、[skill:dotnet-native-aot](通用 AOT)、[skill:dotnet-ui-chooser](框架选择)。
.NET 8+ 的差异
.NET 8+ 上的 WinForms 相较于 .NET Framework WinForms 实现了重大现代化,采用 SDK 风格的项目格式,支持 DI,并更新了 API。
新项目模板
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.*" />
</ItemGroup>
</Project>
与 .NET Framework WinForms 的主要差异:
- SDK 风格的
.csproj(没有packages.config和AssemblyInfo.cs) - 默认启用可空引用类型
- 启用隐式 using
- 使用 NuGet
PackageReference格式 Program.cs使用顶级语句dotnet publish生成单个部署工件- 支持并行安装多个 .NET 版本(不依赖计算机范围的框架)
Host 生成器模式
现代 WinForms 应用使用通用 Host 实现依赖注入:
// Program.cs
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
ApplicationConfiguration.Initialize();
var host = Host.CreateDefaultBuilder()
.ConfigureServices((context, services) =>
{
// Services
services.AddSingleton<IProductService, ProductService>();
services.AddSingleton<ISettingsService, SettingsService>();
// HTTP client
services.AddHttpClient("api", client =>
{
client.BaseAddress = new Uri("https://api.example.com");
});
// Forms
services.AddTransient<MainForm>();
services.AddTransient<ProductDetailForm>();
})
.Build();
var mainForm = host.Services.GetRequiredService<MainForm>();
Application.Run(mainForm);
// 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<ProductDetailForm>();
detailForm.ShowDialog();
}
}
ApplicationConfiguration.Initialize
.NET 8+ WinForms 使用 ApplicationConfiguration.Initialize() 作为入口点,它整合了多个旧版配置调用:
// ApplicationConfiguration.Initialize() is equivalent to:
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.SetHighDpiMode(HighDpiMode.SystemAware); // default; override below for PerMonitorV2
高 DPI
.NET 8+ 上的 WinForms 显著改进了高 DPI 支持。推荐使用 PerMonitorV2 模式,它可以自动处理各显示器的 DPI 变化。
启用 PerMonitorV2
// 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().
也可以通过 runtimeconfig.json 配置:
{
"runtimeOptions": {
"configProperties": {
"System.Windows.Forms.ApplicationHighDpiMode": 3
}
}
}
高 DPI 模式:
| 模式 | 值 | 行为 |
|---|---|---|
DpiUnaware | 0 | 不缩放;系统会对窗口进行位图拉伸 |
SystemAware | 1 | 启动时按主显示器 DPI 缩放(.NET 8 中的默认值) |
PerMonitor | 2 | 在不同显示器之间移动时进行调整(基础支持) |
PerMonitorV2 | 3 | 完整的逐显示器缩放,并支持非客户区 (推荐) |
DpiUnawareGdiScaled | 4 | 不感知 DPI,但 GDI+ 文本会以原生分辨率渲染 |
DPI 无感知设计器模式(.NET 9+)
.NET 9 引入了 DPI 无感知设计器模式,可避免 Visual Studio WinForms 设计器中的布局缩放问题。无论系统 DPI 如何,设计器都会以 96 DPI 渲染,从而避免 .Designer.cs 文件损坏。
<PropertyGroup>
<ForceDesignerDPIUnaware>true</ForceDesignerDPIUnaware>
</PropertyGroup>
缩放注意事项
- 不要为控件使用绝对像素尺寸。 在窗体上使用
AutoScaleMode.Dpi,让布局引擎自动缩放控件。 - Anchor 和 Dock 布局比绝对定位缩放效果更好。 与固定位置控件相比,
TableLayoutPanel和FlowLayoutPanel能更可靠地处理 DPI 变化。 - 自定义绘制(OnPaint)必须使用 DPI 感知坐标。 在
OnPaint重写方法中,按DeviceDpi / 96.0f缩放绘制坐标。 - 图像资源需要提供多个分辨率。 为图标和图像提供 1x、1.5x 和 2x 版本,或使用基于 SVG 的渲染。
// 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);
}
深色模式
WinForms 深色模式在 .NET 9 中处于实验阶段,并且计划在 .NET 11 中完成定稿。它使用 Windows 深色模式 API,为 WinForms 控件提供与系统集成的深色模式。
启用深色模式(.NET 9+ 实验性功能)
// Program.cs -- set before ApplicationConfiguration.Initialize()
Application.SetColorMode(SystemColorMode.Dark);
ApplicationConfiguration.Initialize();
或者跟随系统主题:
// Follow system light/dark preference
Application.SetColorMode(SystemColorMode.System);
SystemColorMode 的值:
| 模式 | 行为 |
|---|---|
Classic | 标准 WinForms 颜色(无深色模式) |
System | 跟随 Windows 系统的浅色/深色主题设置 |
Dark | 强制启用深色模式 |
深色模式注意事项
- 实验状态: API 接口可能会在 .NET 11 定稿前发生变化。请勿在生产环境中依赖特定颜色值或渲染行为。
- 控件覆盖范围: .NET 9 中并非所有控件都支持深色模式。标准控件(Button、TextBox、Label、ListBox、DataGridView)支持深色模式。第三方控件和自定义绘制的控件可能无法正确渲染。
- 所有者绘制控件: 使用
DrawMode.OwnerDrawFixed或自定义OnPaint重写的控件必须手动读取SystemColors,才能响应深色模式。它们不会自动继承深色模式颜色。 - Windows 版本: 深色模式要求 Windows 10 版本 1809(版本号 17763)或更高版本。
- .NET 11 目标: Microsoft 已表示,WinForms 视觉样式(包括深色模式)计划在 .NET 11 中完成定稿。请规划在该版本发布后使用稳定的 API。
// 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);
}
适用场景
WinForms 适用于特定场景。它并不是面向新客户应用的通用 UI 框架。
适用场景
- 快速原型开发: 使用拖放式设计器,快速构建内部工具和概念验证 UI
- 企业内部工具: 业务表单、数据录入,以及使用 DataGridView 的 CRUD 应用
- 简单的 Windows 专用实用工具: 系统托盘应用、配置工具、诊断仪表板
- 维护现有 WinForms 应用: 将现有的 .NET Framework WinForms 应用现代化至 .NET 8+
- 数据密集型表格 UI: DataGridView 的虚拟模式可高效处理数百万行数据
不适用场景
- 新建面向客户的应用: 使用 WPF(丰富的 Windows 桌面体验)、WinUI 3(现代 Windows)、MAUI(跨平台)或 Blazor(Web)
- 复杂的自定义 UI: WinForms 控件的样式定制能力有限;WPF 或 WinUI 提供丰富的模板功能
- 跨平台需求: WinForms 仅支持 Windows;请使用 MAUI 或 Uno Platform
- 优先考虑无障碍体验的应用: WPF 和 WinUI 提供更完善的无障碍 API 和屏幕阅读器支持
- 针对触控优化的界面: WinForms 是为鼠标和键盘设计的;WinUI 或 MAUI 对触控的支持更好
选择指南
| 场景 | 推荐框架 |
|---|---|
| 快速构建内部工具 | WinForms |
| 数据录入表单(Windows) | WinForms 或 WPF |
| 现代 Windows 桌面应用 | WinUI 3 或 WPF(.NET 9+ Fluent) |
| 跨平台移动端 + 桌面端 | MAUI 或 Uno Platform |
| 跨平台 + Web | Uno Platform 或 Blazor |
| 现有 WinForms 应用现代化 | 基于 .NET 8+ 的 WinForms |
完整的框架选择流程请参阅 [skill:dotnet-ui-chooser]。
现代化技巧
将现有 .NET Framework WinForms 应用现代化至 .NET 8+ 的技巧。
添加依赖注入
通过 Host 构建器使用构造函数注入,替代静态引用和单例(请参阅上文“.NET 8+ 差异”部分)。
之前(旧模式):
// 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;
}
}
之后(现代模式):
// 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();
}
}
使用异步模式
将同步阻塞调用替换为 async/await,以保持 UI 响应流畅:
// 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)
{
btnSave.Enabled = false;
try
{
var result = await _httpClient.PostAsync(url, content);
result.EnsureSuccessStatusCode();
MessageBox.Show("Saved!");
}
catch (HttpRequestException ex)
{
MessageBox.Show($"Error: {ex.Message}");
}
finally
{
btnSave.Enabled = true;
}
}
转换为 .NET 8+
使用 .NET Upgrade Assistant 自动迁移:
# Install upgrade assistant
dotnet tool install -g upgrade-assistant
# Analyze the project
upgrade-assistant analyze MyWinFormsApp.csproj
# Upgrade the project
upgrade-assistant upgrade MyWinFormsApp.csproj
常见迁移问题:
App.config设置需要手动迁移到appsettings.json或 Host builder 配置My.Settings(VB.NET)和Settings.settings需要手动迁移- 第三方控件可能没有兼容 .NET 8 的版本
.Designer.cs文件中的设计器生成代码通常可以顺利迁移- COM 互操作(
System.Runtime.InteropServices)的语法可能有所不同
采用现代 C# 功能
// File-scoped namespaces
namespace MyApp.Forms;
// Null-conditional event invocation
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
// Collection expressions
var columns = new[] { "Name", "Price", "Category" };
// Primary constructors for services (C# 12)
public class ProductService(HttpClient httpClient) : IProductService
{
public async Task<List<Product>> GetProductsAsync()
=> await httpClient.GetFromJsonAsync<List<Product>>("/products") ?? [];
}
Agent 注意事项
- 不要为面向客户的新应用推荐 WinForms。 WinForms 适用于内部工具、快速原型开发和以数据为中心的实用程序。对于面向客户的应用,请根据需求推荐 WPF、WinUI 3、MAUI 或 Blazor。
- 不要使用已弃用的 WinForms API。
Menu(使用MenuStrip)、MainMenu(使用MenuStrip)、ContextMenu(使用ContextMenuStrip)、StatusBar(使用StatusStrip)、ToolBar(使用ToolStrip)、DataGrid(使用DataGridView)。 - 不要假定深色模式已可用于生产环境。 通过
Application.SetColorMode实现的深色模式在 .NET 9 中仍属实验性功能,并计划在 .NET 11 中定型。API 表面和渲染效果可能会发生变化。 - 未经测试不要在多显示器场景中使用
HighDpiMode.SystemAware。 对于在 DPI 设置不同的多显示器环境中使用的应用,建议采用PerMonitorV2。 - 不要使用同步调用阻塞 UI 线程。 事件处理程序使用
async void,其他所有异步方法使用async Task。切勿在 UI 线程上使用.Result或.Wait()。 - 如果使用
await即可,就不要使用Control.Invoke。 在 .NET 8+ WinForms 中,await会通过SynchronizationContext自动切回 UI 线程。只有从非异步代码(计时器、COM 回调)调用时,才需要手动使用Invoke/BeginInvoke。 - 启用深色模式时不要对颜色进行硬编码。 在自定义绘制和所有者绘制控件中使用
SystemColors属性(例如SystemColors.ControlText、SystemColors.Control),以便正确响应主题变化。 - 不要忘记在
Application.Run之前调用ApplicationConfiguration.Initialize()。 如果省略此调用,视觉样式和高 DPI 配置将被禁用。
先决条件
- .NET 8.0+,并安装 Windows 桌面工作负载
- TFM:
net8.0-windows(WinForms 无需指定 Windows SDK 版本) - Visual Studio 2022+,并安装 Windows 桌面工作负载(用于设计器支持)
- 深色模式:需要 .NET 9+(实验性),以及 Windows 10 版本 1809+
- DPI 感知不全的设计器:需要 .NET 9+