ARTICLE · 1074781
工业级制图软件开发搭建框架(WPF+MVVM)
前言
这次打算做一个工业级制图软件,以绘图作为核心知识单元,分阶段逐步实现,并将整个过程记录下来形成系列。系列开篇,先把项目框架搭好:多项目分层结构、MVVM 模式、依赖注入、日志系统,一个都不少。
项目介绍
一个基于 WPF 的桌面制图软件,目标是既能画简单的示意图,也能承载复杂的工业制图需求。项目按多项目分层架构组织,核心制图逻辑放在独立的 DiagramDesigner 模块里,界面和业务逻辑通过 MVVM 模式分离。
这一篇先把地基打牢——把项目结构、框架选型、依赖注入和日志系统这些基础设施确定下来。
项目功能
项目特点
多项目分层设计:四个项目各司其职,Common 放通用组件,Component 放界面控件,DiagramDesigner 放制图核心,App 做启动和组装,层次清晰
注解驱动的服务注册:在类上标记 [Service] 特性,启动时自动扫描并注册到 DI 容器,不用手动一个个去 AddSingleton
MVVM 轻量框架:CommunityToolkit.Mvvm 用源生成器在编译时生成绑定代码,没有运行时反射开销,写法也简洁
日志埋点早:从项目第一天就把日志系统接进去了,后面调试和排查问题会省很多事
项目技术
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] 就行。
接下来会进入制图核心模块的开发,从图元的数据结构定义开始,然后是绘制、选择和交互。
点击下方卡片关注DotNet NB
一起交流学习
▲点击上方卡片关注DotNet NB,一起交流学习
请在公众号后台
回复【路线图】获取.NET 2026开发者路线 回复【原创内容】获取公众号原创内容 回复【峰会视频】获取.NET Conf大会视频 回复【个人简介】获取作者个人简介 回复【年终总结】获取作者年终回顾 回复【加群】加入DotNet NB 交流学习群 长按识别下方二维码,或点击阅读原文。和我一起,交流学习,分享心得。
