name: dotnet-winforms-基础 description: “在.NET 8+上构建WinForms。高DPI、暗黑模式(实验性)、DI模式、现代化。” user-invocable: false
dotnet-winforms-基础
在.NET 8+上的WinForms:更新的项目模板包含Host构建器和DI,通过PerMonitorV2支持高DPI,通过Application.SetColorMode支持暗黑模式(在.NET 9中实验性,目标在.NET 11最终化),何时使用WinForms,从.NET Framework迁移的现代化技巧,以及常见代理陷阱。
版本假设: .NET 8.0+ 基础(当前LTS)。TFM net8.0-windows。.NET 9功能(暗黑模式实验性)明确标记。.NET 11最终化目标已注明。
范围
- WinForms .NET 8+ 项目设置(SDK风格)
- 通过PerMonitorV2支持高DPI
- 通过Application.SetColorMode支持暗黑模式(实验性)
- Host构建器和DI模式
- 从.NET Framework现代化技巧
范围外
- WinForms .NET Framework模式(旧版)
- 迁移指南 — 见[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。
新项目模板
<!-- MyWinFormsApp.csproj (SDK风格) -->
<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) - 默认启用可为空引用类型
- 启用隐式usings
- NuGet
PackageReference格式 Program.cs使用顶级语句dotnet publish生成单个部署工件- 并排.NET安装(无机器范围框架依赖)
Host构建器模式
现代WinForms应用使用泛型主机进行依赖注入:
// Program.cs
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
ApplicationConfiguration.Initialize();
var host = Host.CreateDefaultBuilder()
.ConfigureServices((context, services) =>
{
// 服务
services.AddSingleton<IProductService, ProductService>();
services.AddSingleton<ISettingsService, SettingsService>();
// HTTP客户端
services.AddHttpClient("api", client =>
{
client.BaseAddress = new Uri("https://api.example.com");
});
// 窗体
services.AddTransient<MainForm>();
services.AddTransient<ProductDetailForm>();
})
.Build();
var mainForm = host.Services.GetRequiredService<MainForm>();
Application.Run(mainForm);
// MainForm.cs — 构造函数注入
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()等价于:
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.SetHighDpiMode(HighDpiMode.SystemAware); // 默认;下方覆盖为PerMonitorV2
高DPI
在.NET 8+上的WinForms有显著改进的高DPI支持。推荐模式是PerMonitorV2,自动处理每显示器DPI更改。
启用PerMonitorV2
// Program.cs — 在ApplicationConfiguration.Initialize()之前设置
Application.SetHighDpiMode(HighDpiMode.PerMonitorV2);
ApplicationConfiguration.Initialize();
// 注意:在Initialize()之前调用SetHighDpiMode()优先于Initialize()设置的默认SystemAware模式。
或通过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设计器中的布局缩放问题。设计器在96 DPI渲染,无论系统DPI,防止损坏.Designer.cs文件。
<!-- .csproj: 选择加入DPI不感知设计器(.NET 9+) -->
<PropertyGroup>
<ForceDesignerDPIUnaware>true</ForceDesignerDPIUnaware>
</PropertyGroup>
缩放陷阱
- 不要使用绝对像素大小控件。 在窗体上使用
AutoScaleMode.Dpi,让布局引擎自动缩放控件。 - 锚定和停靠布局比绝对定位缩放更好。
TableLayoutPanel和FlowLayoutPanel处理DPI更改更可靠。 - 自定义绘制(OnPaint)必须使用DPI感知坐标。 在
OnPaint重写中,通过DeviceDpi / 96.0f缩放绘制坐标。 - 图像资源需要多分辨率。 提供1x、1.5x和2x版本的图标和图像,或使用SVG渲染。
// DPI感知自定义绘制
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("缩放文本", font, Brushes.Black, 10 * scale, 10 * scale);
}
暗黑模式
WinForms暗黑模式在**.NET 9中为实验性**,目标在.NET 11最终化。它使用Windows暗黑模式API为WinForms控件提供系统集成的暗黑模式。
启用暗黑模式(.NET 9+ 实验性)
// Program.cs — 在ApplicationConfiguration.Initialize()之前设置
Application.SetColorMode(SystemColorMode.Dark);
ApplicationConfiguration.Initialize();
或跟随系统主题:
// 跟随系统亮/暗偏好
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稳定性。
// 所有者绘制控件必须使用SystemColors进行暗黑模式兼容性
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
// 使用SystemColors代替硬编码颜色
using var textBrush = new SolidBrush(SystemColors.ControlText);
using var bgBrush = new SolidBrush(SystemColors.Control);
e.Graphics.FillRectangle(bgBrush, ClientRectangle);
e.Graphics.DrawString("文本", 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现代化 | WinForms on .NET 8+ |
完整框架决策树,见[skill:dotnet-ui-chooser]。
现代化技巧
将现有.NET Framework WinForms应用现代化到.NET 8+的技巧。
添加依赖注入
通过Host构建器(见.NET 8+差异部分)用构造函数注入替换静态引用和单例。
之前(旧版模式):
// 反模式:静态服务引用
public partial class MainForm : Form
{
private void btnLoad_Click(object sender, EventArgs e)
{
var products = ProductService.Instance.GetProducts();
dataGridProducts.DataSource = products;
}
}
之后(现代模式):
// 现代:构造函数注入
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响应性:
// 之前:阻塞UI线程
private void btnSave_Click(object sender, EventArgs e)
{
var client = new HttpClient();
var result = client.PostAsync(url, content).Result; // 阻塞UI
MessageBox.Show("已保存!");
}
// 之后:异步保持UI响应性
private async void btnSave_Click(object sender, EventArgs e)
{
btnSave.Enabled = false;
try
{
var result = await _httpClient.PostAsync(url, content);
result.EnsureSuccessStatusCode();
MessageBox.Show("已保存!");
}
catch (HttpRequestException ex)
{
MessageBox.Show($"错误:{ex.Message}");
}
finally
{
btnSave.Enabled = true;
}
}
转换为.NET 8+
使用.NET Upgrade Assistant进行自动迁移:
# 安装升级助手
dotnet tool install -g upgrade-assistant
# 分析项目
upgrade-assistant analyze MyWinFormsApp.csproj
# 升级项目
upgrade-assistant upgrade MyWinFormsApp.csproj
常见迁移问题:
App.config设置需要手动迁移到appsettings.json或Host构建器配置My.Settings(VB.NET)和Settings.settings需要手动迁移- 第三方控件可能没有.NET 8兼容版本
- 设计器生成代码在
.Designer.cs文件中通常迁移干净 - COM互操作(
System.Runtime.InteropServices)语法可能不同
采用现代C#功能
// 文件范围命名空间
namespace MyApp.Forms;
// 空条件事件调用
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
// 集合表达式
var columns = new[] { "名称", "价格", "类别" };
// 服务的主要构造函数(C# 12)
public class ProductService(HttpClient httpClient) : IProductService
{
public async Task<List<Product>> GetProductsAsync()
=> await httpClient.GetFromJsonAsync<List<Product>>("/products") ?? [];
}
代理陷阱
- 不要为新面向客户应用推荐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。 对于在多显示器设置上使用的应用,推荐PerMonitorV2。 - 不要用同步调用阻塞UI线程。 对事件处理程序使用
async void,对所有其他异步方法使用async Task。在UI线程上永远不要使用.Result或.Wait()。 - 当
await足够时,不要使用Control.Invoke。 在.NET 8+ WinForms中,await通过SynchronizationContext自动编组回UI线程。手动Invoke/BeginInvoke仅在从非异步代码(计时器、COM回调)调用时需要。 - 当启用暗黑模式时,不要硬编码颜色。 在自定义绘制和所有者绘制控件中使用
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+