mcpField
D
dotnet-winforms-basics

dotnet-winforms-basics

github.com/wshaddix开发86289 次安装35 周前更新
代码仓库

关于 dotnet-winforms-basics

dotnet-winforms-basics 是面向编码 Agent 和开发者的 Skill,介绍 .NET 8+ WinForms 的现代项目模式与实践。内容涵盖 Host builder、依赖注入、高 DPI、实验性深色模式,以及适用场景和现代化建议。

主要功能

  1. 01.NET 8+ 项目与 Host builder 模式
  2. 02Host builder 依赖注入
  3. 03PerMonitorV2 高 DPI 配置
  4. 04深色模式及其限制
  5. 05旧项目现代化与适用场景

适用场景

  1. 01创建使用依赖注入的 WinForms 应用
  2. 02为多显示器配置 DPI 缩放
  3. 03评估 WinForms 是否适合内部工具
  4. 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 模式:

模式值行为
DpiUnaware0不缩放;系统会对窗口进行位图拉伸
SystemAware1启动时按主显示器 DPI 缩放(.NET 8 中的默认值)
PerMonitor2在不同显示器之间移动时进行调整(基础支持)
PerMonitorV23完整的逐显示器缩放,并支持非客户区 (推荐)
DpiUnawareGdiScaled4不感知 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
跨平台 + WebUno 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 注意事项

  1. 不要为面向客户的新应用推荐 WinForms。 WinForms 适用于内部工具、快速原型开发和以数据为中心的实用程序。对于面向客户的应用,请根据需求推荐 WPF、WinUI 3、MAUI 或 Blazor。
  2. 不要使用已弃用的 WinForms API。 Menu(使用 MenuStrip)、MainMenu(使用 MenuStrip)、ContextMenu(使用 ContextMenuStrip)、StatusBar(使用 StatusStrip)、ToolBar(使用 ToolStrip)、DataGrid(使用 DataGridView)。
  3. 不要假定深色模式已可用于生产环境。 通过 Application.SetColorMode 实现的深色模式在 .NET 9 中仍属实验性功能,并计划在 .NET 11 中定型。API 表面和渲染效果可能会发生变化。
  4. 未经测试不要在多显示器场景中使用 HighDpiMode.SystemAware。 对于在 DPI 设置不同的多显示器环境中使用的应用,建议采用 PerMonitorV2。
  5. 不要使用同步调用阻塞 UI 线程。 事件处理程序使用 async void,其他所有异步方法使用 async Task。切勿在 UI 线程上使用 .Result 或 .Wait()。
  6. 如果使用 await 即可,就不要使用 Control.Invoke。 在 .NET 8+ WinForms 中,await 会通过 SynchronizationContext 自动切回 UI 线程。只有从非异步代码(计时器、COM 回调)调用时,才需要手动使用 Invoke/BeginInvoke。
  7. 启用深色模式时不要对颜色进行硬编码。 在自定义绘制和所有者绘制控件中使用 SystemColors 属性(例如 SystemColors.ControlText、SystemColors.Control),以便正确响应主题变化。
  8. 不要忘记在 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+

参考资料

评论

支持 Markdown