夜雨聆风学习资料网

ARTICLE · 1074781

工业级制图软件开发搭建框架(WPF+MVVM)

工业级制图软件开发搭建框架(WPF+MVVM)

前言

这次打算做一个工业级制图软件,以绘图作为核心知识单元,分阶段逐步实现,并将整个过程记录下来形成系列。系列开篇,先把项目框架搭好:多项目分层结构、MVVM 模式、依赖注入、日志系统,一个都不少。

项目介绍

一个基于 WPF 的桌面制图软件,目标是既能画简单的示意图,也能承载复杂的工业制图需求。项目按多项目分层架构组织,核心制图逻辑放在独立的 DiagramDesigner 模块里,界面和业务逻辑通过 MVVM 模式分离。

这一篇先把地基打牢——把项目结构、框架选型、依赖注入和日志系统这些基础设施确定下来。

项目功能

功能模块
当前状态及计划
项目分层架构
已完成,按 App / Common / Component / DiagramDesigner 四个项目拆分
MVVM 框架集成
已完成,使用 CommunityToolkit.Mvvm,支持属性绑定和命令绑定
依赖注入容器
已完成,基于 Microsoft.Extensions.DependencyInjection,配合自定义特性实现自动注册
日志系统
已完成,NLog 封装,支持文件和控制台双输出
自定义控件库
框架已搭建,后续填充具体控件
图元定义和绘制
规划中,后续文章逐步实现
几何算法和交互逻辑
规划中,后续文章逐步实现

项目特点

多项目分层设计:四个项目各司其职,Common 放通用组件,Component 放界面控件,DiagramDesigner 放制图核心,App 做启动和组装,层次清晰

注解驱动的服务注册:在类上标记 [Service] 特性,启动时自动扫描并注册到 DI 容器,不用手动一个个去 AddSingleton

MVVM 轻量框架:CommunityToolkit.Mvvm 用源生成器在编译时生成绑定代码,没有运行时反射开销,写法也简洁

日志埋点早:从项目第一天就把日志系统接进去了,后面调试和排查问题会省很多事

项目技术

技术方向
具体选型
桌面框架
WPF / .NET 8
MVVM 框架
CommunityToolkit.Mvvm 8.4.2
依赖注入
Microsoft.Extensions.DependencyInjection
日志组件
NLog
构建工具
Visual Studio 2022
版本管理
Git

CommunityToolkit.Mvvm 和其他 MVVM 框架最大的区别在于它依赖源生成器。[ObservableProperty] 和 [RelayCommand] 这些注解在编译阶段会生成完整的属性和命令实现代码,所以运行时没有反射开销,性能更好,写起来也更省事。

项目代码

整个解决方案包含四个项目,引用关系很清晰:

├── FlexiDraw.App              ← 主程序入口,View + ViewModel│   ├── ViewModels/            ← ViewModel 层│   ├── Common/                ← DI 相关工具类│   ├── App.xaml / .cs         ← 程序入口,DI 容器初始化│   └── MainWindow.xaml / .cs  ← 主窗体├── FlexiDraw.Common           ← 通用类库│   ├── FileLogHelper.cs       ← NLog 封装│   └── NLog.config            ← NLog 配置├── FlexiDraw.Component        ← 自定义控件库│   ├── Views/                 ← 自定义控件│   └── Themes/                ← 控件样式└── FlexiDraw.DiagramDesigner  ← 制图核心(后续填充)

引用关系:App 引用 Common 和 Component,Component 只依赖 .NET 自身,不引用其他业务项目。这样 Component 里的控件可以独立复用。

FileLogHelper.cs

using System;using NLog;namespaceFlexiDraw.Common{publicclassFileLogHelper    {privatestaticreadonly ILogger _logger = LogManager.GetCurrentClassLogger();publicvoidLogInfo(string message) => _logger.Info(message);publicvoidLogWarning(string message) => _logger.Warn(message);publicvoidLogError(string message, Exception ex = null)        {if (ex != null)                _logger.Error(ex, message);else                _logger.Error(message);        }publicvoidLogDebug(string message) => _logger.Debug(message);    }}

NLog.config

<?xml version="1.0" encoding="utf-8" ?><nlogxmlns="http://www.nlog-project.org/schemas/NLog.xsd"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"autoReload="true"><targets><targetname="logfile"xsi:type="File"fileName="${basedir}/logs/${shortdate}.log"layout="${longdate} [${level:uppercase=true}] ${logger} - ${message} ${exception:format=tostring}"encoding="utf-8"archiveEvery="Day"maxArchiveFiles="30" /><targetname="console"xsi:type="Console"layout="${level:uppercase=true}] ${message}" /></targets><rules><loggername="*"minlevel="Info"writeTo="logfile,console" /></rules></nlog>

ServiceAttribute.cs

using System;using Microsoft.Extensions.DependencyInjection;namespaceFlexiDraw.App.Common{    [AttributeUsage(AttributeTargets.Class, Inherited = false)]publicclassServiceAttribute : Attribute    {public ServiceLifetime Lifetime { get; set; } = ServiceLifetime.Transient;publicServiceAttribute() { }publicServiceAttribute(ServiceLifetime lifetime) => Lifetime = lifetime;    }}

ServiceProviderLocator.cs

using System;using Microsoft.Extensions.DependencyInjection;namespaceFlexiDraw.App.Common{publicstaticclassServiceProviderLocator    {publicstatic IServiceProvider ServiceProvider { get; set; }publicstatic T GetService<T>() where T : class        {if (ServiceProvider == null)thrownew InvalidOperationException("ServiceProvider 尚未初始化!");return ServiceProvider.GetRequiredService<T>();        }    }}

App.xaml

<Applicationx:Class="FlexiDraw.App.App"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:local="clr-namespace:FlexiDraw.App"><Application.Resources><local:ViewModelLocatorx:Key="Locator" /></Application.Resources></Application>

App.xaml.cs

using System;using System.Linq;using System.Reflection;using System.Windows;using FlexiDraw.App.Common;using FlexiDraw.Common;using Microsoft.Extensions.DependencyInjection;namespaceFlexiDraw.App{publicpartialclassApp : Application    {public IServiceProvider ServiceProvider { get; privateset; }protectedoverridevoidOnStartup(StartupEventArgs e)        {base.OnStartup(e);var services = new ServiceCollection();            ConfigureServices(services);            ServiceProvider = services.BuildServiceProvider();            ServiceProviderLocator.ServiceProvider = ServiceProvider;var mainWindow = new MainWindow();            mainWindow.Show();        }privatevoidConfigureServices(ServiceCollection services)        {var assembly = Assembly.GetExecutingAssembly();var serviceTypes = assembly.GetTypes()                .Where(t => t.IsClass && !t.IsAbstract && t.IsDefined(typeof(ServiceAttribute), false));foreach (var type in serviceTypes)            {var attr = type.GetCustomAttribute<ServiceAttribute>();switch (attr.Lifetime)                {case ServiceLifetime.Singleton:                        services.AddSingleton(type);break;case ServiceLifetime.Scoped:                        services.AddScoped(type);break;default:                        services.AddTransient(type);break;                }            }            services.AddSingleton<FileLogHelper>();        }    }}

ViewModelLocator.cs

using FlexiDraw.App.Common;using FlexiDraw.App.ViewModels;namespaceFlexiDraw.App{publicclassViewModelLocator    {public MainWindowVM MainWindowViewModel =>            ServiceProviderLocator.GetService<MainWindowVM>();    }}

MainWindowVM.cs

using CommunityToolkit.Mvvm.ComponentModel;using CommunityToolkit.Mvvm.Input;using FlexiDraw.App.Common;using FlexiDraw.Common;namespaceFlexiDraw.App.ViewModels{    [Service(ServiceLifetime.Singleton)]publicpartialclassMainWindowVM : ObservableObject    {privatereadonly FileLogHelper _fileLogHelper;        [ObservableProperty]privatestring userName = "Default User";publicMainWindowVM(FileLogHelper fileLogHelper)        {            _fileLogHelper = fileLogHelper;            _fileLogHelper.LogInfo("MainWindowVM 已创建");        }        [RelayCommand]privatevoidSubmit()        {            UserName = "张三";            _fileLogHelper.LogInfo($"UserName 已更新为: {UserName}");        }    }}

MainWindow.xaml

<Windowx:Class="FlexiDraw.App.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:Views="clr-namespace:FlexiDraw.Component.Views;assembly=FlexiDraw.Component"Title="FlexiDraw"Height="600"Width="900"DataContext="{Binding MainWindowViewModel, Source={StaticResource Locator}}"><Grid><Grid.RowDefinitions><RowDefinitionHeight="2*" /><RowDefinitionHeight="*" /></Grid.RowDefinitions><StackPanelGrid.Row="0"VerticalAlignment="Center"HorizontalAlignment="Center"><TextBlockText="{Binding UserName}"FontSize="32"FontWeight="Bold"HorizontalAlignment="Center" /><ButtonCommand="{Binding SubmitCommand}"Content="更新用户名"FontSize="20"Padding="20,10"Margin="0,20,0,0" /></StackPanel><Views:DrawToolGrid.Row="1"HorizontalAlignment="Stretch"VerticalAlignment="Stretch" /></Grid></Window>

DrawTool.xaml

<UserControlx:Class="FlexiDraw.Component.Views.DrawTool"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"><BorderBackground="#F0F0F0"BorderBrush="#CCCCCC"BorderThickness="1"><TextBlockText="绘图区域(后续填充)"FontSize="18"Foreground="#999999"VerticalAlignment="Center"HorizontalAlignment="Center" /></Border></UserControl>

DrawTool.xaml.cs

using System.Windows.Controls;namespaceFlexiDraw.Component.Views{publicpartialclassDrawTool : UserControl    {publicDrawTool() => InitializeComponent();    }}

总结

写框架代码这事,很多时候都在做"看不见"的工作。这一篇搭好的项目结构、MVVM 框架、依赖注入和日志系统,表面上看没有任何图形界面上的进展,但这些东西决定了后续开发能走多快、走多稳。

回头来看,选 CommunityToolkit.Mvvm 这个组合是合理的——代码量少,侵入性低,源生成器用起来也舒服。DI 容器配合自定义特性的方式,比手动注册要省事很多,后面每新增一个 ViewModel 或者服务,只需要在类上加一行 [Service] 就行。

接下来会进入制图核心模块的开发,从图元的数据结构定义开始,然后是绘制、选择和交互。

推荐阅读:
大多数 C# 性能建议都是噪声:这 5 条才值得进入代码评审
C# 复刻了一个PLC,我才搞懂扫描周期到底是怎么回事
用 C# 构建多租户应用:共享库、独立库与租户上下文边界
MCP 第五版 × OpenClaw.NET:从协议升级到生态编排
使用 .NET + AI 构建智能技术文档生成平台:让代码自动产生文档
一次4秒数据库抖动,引发40分钟事故:.NET中的HttpClient重试风暴治理指南

点击下方卡片关注DotNet NB

一起交流学习

▲点击上方卡片关注DotNet NB,一起交流学习

请在公众号后台

回复【路线图】获取.NET 2026开发者路线
回复【原创内容】获取公众号原创内容
回复【峰会视频】获取.NET Conf大会视频
回复【个人简介】获取作者个人简介
回复【年终总结】获取作者年终回顾
回复【加群】加入DotNet NB 交流学习群
长按识别下方二维码,或点击阅读原文。和我一起,交流学习,分享心得。

相关学习资料