尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C#动态编译与控件拖拽:从零实现可视化IDE核心原理

C#动态编译与控件拖拽:从零实现可视化IDE核心原理 如果你以为开发一个可视化IDE需要掌握多么高深的技术那可能被各种复杂的商业IDE吓到了。实际上用C#和WinForms从零搭建一个基础的可视化开发环境核心不过是动态编译和控件拖拽这两大技术的巧妙组合。这篇文章要解决的问题很直接为什么说从零打造C#可视化IDE在技术层面不过是个玩具这里的玩具不是贬义而是指它剥离了商业IDE的复杂外壳后核心机制出奇地简单。很多开发者对IDE有敬畏感觉得Visual Studio、Rider这样的工具背后肯定有难以企及的技术壁垒。但当你真正拆解一个基础IDE的需求——代码编辑、实时编译、界面设计、控件交互——会发现每个环节在C#生态中都有成熟的解决方案。这篇文章将带你用不到200行核心代码实现一个具备基本功能的C#可视化IDE并重点分析其中真正值得关注的技术要点和设计模式。通过这个玩具项目你能获得什么首先是理解现代IDE的工作原理这对日常开发中的调试、性能优化有直接帮助其次是掌握动态编译这一强大技术它能让你在运行时加载和执行代码为插件系统、脚本引擎等高级功能打下基础最后是WinForms控件编程的实战经验这在工业上位机、数据可视化等场景中非常实用。1. 这篇文章真正要解决的问题为什么我们要花时间打造一个玩具级的IDE这背后有三个层次的考虑技术认知层面商业IDE给人的感觉是庞大而复杂但核心功能模块其实可以拆解。通过亲手实现你能破除对IDE的神秘感理解代码编辑、编译、执行的完整链路。当你在Visual Studio中点击运行时背后发生了什么动态编译技术就是答案之一。实际应用层面动态编译不仅仅是IDE的核心在很多实际场景中都非常有用。比如需要用户自定义脚本的系统、支持插件扩展的应用程序、在线代码评测平台等都需要在运行时编译和执行代码。掌握这项技术意味着你能为应用添加强大的扩展能力。学习价值层面这个项目涵盖了C#开发中的多个重要概念反射、程序集加载、WinForms控件编程、事件处理、设计模式应用等。通过一个完整的项目实践这些抽象概念会变得具体而直观。什么样的开发者最适合阅读本文如果你符合以下任一条件有基础C#语法知识想深入理解.NET运行时机制对IDE工作原理好奇想知其然更知其所以然需要为项目添加脚本支持或插件功能正在学习WinForms或WPF界面开发2. 基础概念与核心原理在开始编码前需要明确几个关键概念2.1 动态编译Dynamic Compilation动态编译指的是在程序运行时将源代码文本转换为可执行代码的过程。与传统的预先编译AOT不同动态编译允许应用程序根据用户输入或配置生成并执行代码。在C#中动态编译主要通过Microsoft.CodeAnalysis.CSharp命名空间Roslyn编译器实现。Roslyn是.NET编译器平台不仅提供编译功能还暴露了完整的编译管道API。// 动态编译的基本流程 string sourceCode public class Demo { }; var compilation CSharpCompilation.Create(DynamicAssembly) .WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)) .AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)) .AddSyntaxTrees(CSharpSyntaxTree.ParseText(sourceCode)); using var ms new MemoryStream(); var result compilation.Emit(ms); if (result.Success) { byte[] assemblyBytes ms.ToArray(); Assembly assembly Assembly.Load(assemblyBytes); // 现在可以动态使用这个程序集了 }2.2 WinForms控件与设计器Windows FormsWinForms是.NET的图形用户界面框架。可视化IDE的核心功能之一就是允许用户通过拖拽方式设计界面。这需要理解控件容器如Panel、Form等可以容纳其他控件的容器控件事件鼠标事件点击、拖拽、键盘事件等序列化将控件属性和布局保存为代码或配置文件2.3 设计模式的应用在一个简单的IDE实现中几种设计模式会自然出现观察者模式处理控件的事件订阅和通知工厂模式创建不同类型的控件实例命令模式实现撤销/重做功能组合模式处理控件容器的层次结构3. 环境准备与前置条件开始实现前确保你的开发环境满足以下要求3.1 开发环境配置操作系统Windows 10/11WinForms对跨平台支持有限.NET版本.NET 6.0 或更高版本本文使用.NET 8.0IDEVisual Studio 2022 或 Visual Studio Code必要NuGet包Microsoft.CodeAnalysis.CSharpMicrosoft.CodeAnalysis.CSharp.Workspaces3.2 项目结构规划创建新的WinForms项目建议采用以下结构CSharpVisualIDE/ ├── Properties/ ├── Controls/ # 自定义控件 ├── Compilers/ # 编译相关类 ├── Designers/ # 设计器功能 ├── Models/ # 数据模型 └── MainForm.cs # 主界面3.3 添加必要的NuGet引用在项目文件中添加以下包引用Project SdkMicrosoft.NET.Sdk PropertyGroup OutputTypeWinExe/OutputType TargetFrameworknet8.0-windows/TargetFramework UseWindowsFormstrue/UseWindowsForms /PropertyGroup ItemGroup PackageReference IncludeMicrosoft.CodeAnalysis.CSharp Version4.8.0 / PackageReference IncludeMicrosoft.CodeAnalysis.CSharp.Workspaces Version4.8.0 / /ItemGroup /Project4. 核心流程拆解实现可视化IDE需要完成以下几个核心步骤4.1 界面布局设计主界面应该包含以下区域工具箱可拖拽的控件列表设计面板控件布局区域属性窗口选中控件的属性编辑代码编辑器源代码输入区域输出窗口编译和执行结果展示4.2 控件拖拽实现拖拽功能的实现分为三个阶段拖拽开始鼠标在工具箱控件上按下时创建控件副本拖拽进行鼠标移动时更新控件位置拖拽结束鼠标释放时将控件添加到设计面板4.3 动态编译流程代码执行的完整流程获取用户输入的C#代码添加必要的程序集引用如System.Windows.Forms使用Roslyn编译代码生成内存程序集通过反射加载和执行程序集捕获并显示执行结果或错误信息4.4 设计器代码生成将设计界面转换为C#代码遍历设计面板中的所有控件生成控件实例化代码生成属性设置代码生成事件绑定代码组合成完整的窗体类定义5. 完整示例与代码实现下面我们逐步实现这个可视化IDE的核心功能。5.1 主界面布局实现首先创建主窗体划分各个功能区域// 文件路径MainForm.cs using System.Windows.Forms; namespace CSharpVisualIDE { public partial class MainForm : Form { private SplitContainer mainSplitContainer; private Panel toolboxPanel; private Panel designPanel; private TextBox codeEditor; private TextBox outputTextBox; private PropertyGrid propertyGrid; public MainForm() { InitializeComponent(); SetupUI(); } private void SetupUI() { this.Text C# 可视化IDE - 玩具版; this.WindowState FormWindowState.Maximized; // 主分割容器 - 左右布局 mainSplitContainer new SplitContainer(); mainSplitContainer.Dock DockStyle.Fill; mainSplitContainer.Orientation Orientation.Horizontal; this.Controls.Add(mainSplitContainer); // 上部分设计和工具箱 var designSplitContainer new SplitContainer(); designSplitContainer.Dock DockStyle.Fill; designSplitContainer.Orientation Orientation.Vertical; designSplitContainer.Panel1MinSize 100; designSplitContainer.Panel2MinSize 300; mainSplitContainer.Panel1.Controls.Add(designSplitContainer); // 工具箱 toolboxPanel new Panel(); toolboxPanel.Dock DockStyle.Fill; toolboxPanel.BackColor Color.LightGray; toolboxPanel.Text 工具箱; designSplitContainer.Panel1.Controls.Add(toolboxPanel); // 设计面板 designPanel new Panel(); designPanel.Dock DockStyle.Fill; designPanel.BackColor Color.White; designPanel.AllowDrop true; designSplitContainer.Panel2.Controls.Add(designPanel); // 下部分代码编辑器和输出 var codeSplitContainer new SplitContainer(); codeSplitContainer.Dock DockStyle.Fill; codeSplitContainer.Orientation Orientation.Vertical; mainSplitContainer.Panel2.Controls.Add(codeSplitContainer); // 代码编辑器 codeEditor new TextBox(); codeEditor.Dock DockStyle.Fill; codeEditor.Multiline true; codeEditor.ScrollBars ScrollBars.Both; codeEditor.Font new Font(Consolas, 10); codeSplitContainer.Panel1.Controls.Add(codeEditor); // 输出窗口 outputTextBox new TextBox(); outputTextBox.Dock DockStyle.Fill; outputTextBox.Multiline true; outputTextBox.ScrollBars ScrollBars.Vertical; outputTextBox.ReadOnly true; codeSplitContainer.Panel2.Controls.Add(outputTextBox); // 属性窗口右侧 propertyGrid new PropertyGrid(); propertyGrid.Dock DockStyle.Fill; propertyGrid.Size new Size(250, 0); propertyGrid.Anchor AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Right; this.Controls.Add(propertyGrid); InitializeToolbox(); } private void InitializeToolbox() { // 添加基本控件到工具箱 var controls new[] { Button, TextBox, Label, Panel }; int yPos 10; foreach (var controlName in controls) { var toolItem new Label(); toolItem.Text controlName; toolItem.BorderStyle BorderStyle.FixedSingle; toolItem.BackColor Color.White; toolItem.Size new Size(80, 25); toolItem.Location new Point(10, yPos); toolItem.MouseDown ToolItem_MouseDown; toolboxPanel.Controls.Add(toolItem); yPos 30; } } private void ToolItem_MouseDown(object sender, MouseEventArgs e) { var toolItem sender as Label; if (toolItem ! null) { // 开始拖拽操作 toolItem.DoDragDrop(toolItem.Text, DragDropEffects.Copy); } } } }5.2 拖拽功能实现为设计面板添加拖拽接收功能// 在MainForm类中添加以下方法 private void SetupDesignPanelEvents() { designPanel.DragEnter DesignPanel_DragEnter; designPanel.DragDrop DesignPanel_DragDrop; } private void DesignPanel_DragEnter(object sender, DragEventArgs e) { if (e.Data.GetDataPresent(DataFormats.Text)) e.Effect DragDropEffects.Copy; else e.Effect DragDropEffects.None; } private void DesignPanel_DragDrop(object sender, DragEventArgs e) { var controlTypeName e.Data.GetData(DataFormats.Text) as string; if (!string.IsNullOrEmpty(controlTypeName)) { Control newControl CreateControlFromType(controlTypeName); if (newControl ! null) { var point designPanel.PointToClient(new Point(e.X, e.Y)); newControl.Location point; newControl.MouseDown DesignControl_MouseDown; designPanel.Controls.Add(newControl); // 选中新控件 propertyGrid.SelectedObject newControl; } } } private Control CreateControlFromType(string typeName) { return typeName switch { Button new Button() { Text Button, Size new Size(75, 23) }, TextBox new TextBox() { Text TextBox, Size new Size(100, 20) }, Label new Label() { Text Label, Size new Size(35, 13), AutoSize true }, Panel new Panel() { BackColor Color.LightBlue, Size new Size(200, 100) }, _ null }; } private void DesignControl_MouseDown(object sender, MouseEventArgs e) { // 选中控件时更新属性窗口 propertyGrid.SelectedObject sender; }5.3 动态编译核心实现创建编译服务类处理代码编译// 文件路径Compilers/DynamicCompiler.cs using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using System.Reflection; using System.Text; namespace CSharpVisualIDE.Compilers { public class DynamicCompiler { public CompilationResult CompileAndExecute(string code, TextBox outputTextBox) { try { // 构建完整的程序代码 string fullCode BuildCompleteProgram(code); // 创建编译选项 var options new CSharpCompilationOptions(OutputKind.ConsoleApplication); // 添加必要的程序集引用 var references GetRequiredReferences(); // 编译代码 var syntaxTree CSharpSyntaxTree.ParseText(fullCode); var compilation CSharpCompilation.Create(DynamicProgram) .WithOptions(options) .AddReferences(references) .AddSyntaxTrees(syntaxTree); using var ms new MemoryStream(); var result compilation.Emit(ms); if (result.Success) { // 加载并执行程序集 var assembly Assembly.Load(ms.ToArray()); return ExecuteAssembly(assembly, outputTextBox); } else { var errors string.Join(\n, result.Diagnostics .Where(d d.Severity DiagnosticSeverity.Error)); return new CompilationResult { Success false, Message errors }; } } catch (Exception ex) { return new CompilationResult { Success false, Message ex.Message }; } } private string BuildCompleteProgram(string userCode) { return $ using System; using System.Windows.Forms; using System.Drawing; namespace DynamicProgram {{ public class Program {{ public static void Main() {{ // 用户代码开始 {userCode} // 用户代码结束 }} }} }}; } private MetadataReference[] GetRequiredReferences() { var assemblies new[] { typeof(object).Assembly, // System.Runtime typeof(Console).Assembly, // System.Console typeof(Form).Assembly, // System.Windows.Forms typeof(Point).Assembly, // System.Drawing Assembly.Load(System.Runtime), // 核心运行时 Assembly.Load(netstandard) // .NET标准 }; return assemblies .Where(asm !string.IsNullOrEmpty(asm?.Location)) .Select(asm MetadataReference.CreateFromFile(asm.Location)) .ToArray(); } private CompilationResult ExecuteAssembly(Assembly assembly, TextBox outputTextBox) { try { // 重定向控制台输出 var originalOutput Console.Out; using var writer new StringWriter(); Console.SetOut(writer); // 执行入口方法 var entryPoint assembly.GetType(DynamicProgram.Program); var mainMethod entryPoint.GetMethod(Main, BindingFlags.Static | BindingFlags.Public); mainMethod.Invoke(null, null); // 恢复输出并获取结果 Console.SetOut(originalOutput); var output writer.ToString(); // 在UI线程中更新输出 outputTextBox.Invoke(new Action(() { outputTextBox.Text output; })); return new CompilationResult { Success true, Message 执行成功 }; } catch (Exception ex) { return new CompilationResult { Success false, Message $执行错误: {ex.Message} }; } } } public class CompilationResult { public bool Success { get; set; } public string Message { get; set; } } }5.4 设计器代码生成实现将设计界面转换为C#代码的功能// 文件路径Designers/CodeGenerator.cs using System.Text; namespace CSharpVisualIDE.Designers { public class CodeGenerator { public string GenerateFormCode(Panel designPanel, string formName GeneratedForm) { var sb new StringBuilder(); // 添加命名空间和类定义 sb.AppendLine(using System.Windows.Forms;); sb.AppendLine(using System.Drawing;); sb.AppendLine(); sb.AppendLine(namespace GeneratedCode); sb.AppendLine({); sb.AppendLine($ public class {formName} : Form); sb.AppendLine( {); // 生成控件字段声明 GenerateControlDeclarations(sb, designPanel); sb.AppendLine(); // 生成构造函数和初始化方法 sb.AppendLine($ public {formName}()); sb.AppendLine( {); sb.AppendLine( InitializeComponent();); sb.AppendLine( }); sb.AppendLine(); sb.AppendLine( private void InitializeComponent()); sb.AppendLine( {); GenerateControlInitialization(sb, designPanel); sb.AppendLine( }); sb.AppendLine( }); sb.AppendLine(}); return sb.ToString(); } private void GenerateControlDeclarations(StringBuilder sb, Control container) { for (int i 0; i container.Controls.Count; i) { var control container.Controls[i]; var controlType control.GetType().Name; var controlName $_{controlType.ToLower()}{i}; sb.AppendLine($ private {controlType} {controlName};); } } private void GenerateControlInitialization(StringBuilder sb, Control container) { for (int i 0; i container.Controls.Count; i) { var control container.Controls[i]; var controlType control.GetType().Name; var controlName $_{controlType.ToLower()}{i}; sb.AppendLine($ {controlName} new {controlType}();); sb.AppendLine($ {controlName}.Name \{controlName}\;); sb.AppendLine($ {controlName}.Text \{control.Text}\;); sb.AppendLine($ {controlName}.Location new Point({control.Left}, {control.Top});); sb.AppendLine($ {controlName}.Size new Size({control.Width}, {control.Height});); if (control.BackColor ! SystemColors.Control) sb.AppendLine($ {controlName}.BackColor Color.FromArgb({control.BackColor.ToArgb()});); sb.AppendLine($ this.Controls.Add({controlName});); sb.AppendLine(); } } } }6. 运行结果与效果验证6.1 整合功能并测试在主窗体中添加运行按钮和代码生成功能// 在MainForm类中添加以下成员和方法 private Button runButton; private Button generateCodeButton; private DynamicCompiler compiler; private CodeGenerator codeGenerator; private void AddControlButtons() { var buttonPanel new Panel(); buttonPanel.Dock DockStyle.Top; buttonPanel.Height 40; this.Controls.Add(buttonPanel); runButton new Button(); runButton.Text 运行代码; runButton.Size new Size(80, 30); runButton.Location new Point(10, 5); runButton.Click RunButton_Click; buttonPanel.Controls.Add(runButton); generateCodeButton new Button(); generateCodeButton.Text 生成代码; generateCodeButton.Size new Size(80, 30); generateCodeButton.Location new Point(100, 5); generateCodeButton.Click GenerateCodeButton_Click; buttonPanel.Controls.Add(generateCodeButton); compiler new DynamicCompiler(); codeGenerator new CodeGenerator(); } private void RunButton_Click(object sender, EventArgs e) { var result compiler.CompileAndExecute(codeEditor.Text, outputTextBox); if (!result.Success) { outputTextBox.Text $编译错误:\n{result.Message}; } } private void GenerateCodeButton_Click(object sender, EventArgs e) { var generatedCode codeGenerator.GenerateFormCode(designPanel, MyGeneratedForm); codeEditor.Text generatedCode; }6.2 测试示例在代码编辑器中输入以下测试代码// 测试动态编译功能 MessageBox.Show(Hello from dynamic code!); Console.WriteLine(这段文字会显示在输出窗口); // 测试窗体创建 Form testForm new Form(); testForm.Text 动态创建的窗体; testForm.Size new Size(300, 200); testForm.ShowDialog();点击运行代码按钮你应该看到弹出消息框显示Hello from dynamic code!输出窗口显示这段文字会显示在输出窗口显示一个新创建的窗体6.3 设计器功能测试从工具箱拖拽Button、TextBox、Label到设计面板选中某个控件在属性窗口中修改其Text、BackColor等属性点击生成代码按钮查看生成的C#代码将生成的代码复制到代码编辑器并运行7. 常见问题与排查思路在实现和使用这个可视化IDE过程中可能会遇到以下问题问题现象可能原因排查方式解决方案拖拽控件无反应设计面板的AllowDrop未设置检查designPanel.AllowDrop属性设置为true并确保DragEnter事件已绑定编译错误缺少引用必要的程序集未添加查看DynamicCompiler中的引用列表添加缺失的程序集引用路径执行时报安全错误.NET安全策略限制检查异常信息中的安全相关提示调整程序集加载权限或使用AppDomain生成的代码无法运行控件初始化不完整检查生成的代码格式完善CodeGenerator中的属性设置逻辑内存泄漏动态程序集未正确释放监控内存使用情况实现程序集卸载机制或使用单独的AppDomain7.1 动态编译的权限问题在部分环境中动态编译可能会受到安全策略的限制。如果遇到权限错误可以尝试以下解决方案// 使用AppDomain隔离动态程序集 public CompilationResult CompileInSeparateDomain(string code) { var domainSetup new AppDomainSetup { ApplicationBase AppDomain.CurrentDomain.SetupInformation.ApplicationBase }; var evidence AppDomain.CurrentDomain.Evidence; var appDomain AppDomain.CreateDomain(DynamicCompilationDomain, evidence, domainSetup); try { // 在独立域中执行编译 var helperType typeof(CompilationHelper); var helper (CompilationHelper)appDomain.CreateInstanceAndUnwrap( helperType.Assembly.FullName, helperType.FullName); return helper.Compile(code); } finally { AppDomain.Unload(appDomain); } } [Serializable] public class CompilationHelper : MarshalByRefObject { public CompilationResult Compile(string code) { // 编译逻辑... return new CompilationResult(); } }7.2 控件事件处理的挑战在动态创建的控件中处理事件需要特别注意// 动态绑定事件示例 private void SetupDynamicEvents(Control control) { if (control is Button button) { button.Click (s, e) { // 动态处理点击事件 MessageBox.Show(按钮被点击了); }; } else if (control is TextBox textBox) { textBox.TextChanged (s, e) { // 实时响应文本变化 Console.WriteLine($文本变为: {textBox.Text}); }; } }8. 最佳实践与工程建议虽然这只是一个玩具级的IDE实现但其中涉及的技术点在实际项目中很有价值。以下是一些工程化建议8.1 代码组织与架构优化分层架构将项目分为表示层UI、业务逻辑层编译服务、数据层代码模型// 建议的项目结构 CSharpVisualIDE/ ├── UI/ # 表示层 │ ├── Controls/ # 自定义控件 │ └── Forms/ # 窗体类 ├── Services/ # 业务逻辑层 │ ├── Compilation/ # 编译服务 │ └── Design/ # 设计器服务 ├── Models/ # 数据层 │ ├── Code/ # 代码模型 │ └── Design/ # 设计模型 └── Utilities/ # 工具类依赖注入使用Microsoft.Extensions.DependencyInjection管理服务依赖// 服务配置 var services new ServiceCollection(); services.AddSingletonDynamicCompiler(); services.AddSingletonCodeGenerator(); services.AddTransientMainForm(); var provider services.BuildServiceProvider(); var mainForm provider.GetRequiredServiceMainForm(); Application.Run(mainForm);8.2 性能优化建议程序集缓存避免重复编译相同的代码public class CompilationCache { private readonly Dictionarystring, Assembly _cache new(); public Assembly GetOrCompile(string code, string cacheKey) { if (_cache.ContainsKey(cacheKey)) return _cache[cacheKey]; var assembly Compile(code); _cache[cacheKey] assembly; return assembly; } }内存管理及时清理不再使用的动态程序集public void Cleanup() { foreach (var assembly in _loadedAssemblies) { // 尝试卸载程序集 if (assembly ! null !assembly.IsDynamic) { // 对于非动态程序集只能通过AppDomain卸载 } } _loadedAssemblies.Clear(); }8.3 安全考虑代码沙箱限制动态代码的访问权限public class CodeSandbox { public void ExecuteInSandbox(Action action) { var permissionSet new PermissionSet(PermissionState.None); permissionSet.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution)); var evidence new Evidence(); evidence.AddHostEvidence(new Zone(SecurityZone.Internet)); // 在受限环境中执行 } }输入验证防止恶意代码注入public class CodeValidator { private static readonly string[] _forbiddenKeywords { System.IO, System.Net, Reflection, Environment }; public bool ValidateCode(string code) { return !_forbiddenKeywords.Any(keyword code.Contains(keyword)); } }8.4 扩展性设计插件系统支持功能扩展public interface IIDEPlugin { string Name { get; } void Initialize(MainForm mainForm); } public class PluginManager { private readonly ListIIDEPlugin _plugins new(); public void LoadPlugin(string assemblyPath) { var assembly Assembly.LoadFrom(assemblyPath); var pluginTypes assembly.GetTypes() .Where(t typeof(IIDEPlugin).IsAssignableFrom(t) !t.IsInterface); foreach (var type in pluginTypes) { var plugin (IIDEPlugin)Activator.CreateInstance(type); _plugins.Add(plugin); } } }9. 总结与后续学习方向通过这个玩具级C#可视化IDE的实现我们验证了一个重要观点看似复杂的技术产品其核心机制往往出奇地简单。这个项目虽然功能有限但涵盖了现代IDE的几个关键技术点技术收获动态编译的原理和实践应用WinForms控件的编程和事件处理设计模式在真实项目中的体现代码生成和程序集加载机制工程思维复杂功能的模块化分解用户交互设计的考虑错误处理和边界情况处理性能和安全性的基本考量这个项目的价值不在于它能否替代Visual Studio而在于它提供了一个理解IDE工作原理的窗口。当你再次使用专业IDE时会对背后的技术机制有更深入的认识。后续深入学习方向语法高亮和智能提示研究Roslyn的语言服务功能实现代码编辑器的增强功能调试器集成了解.NET调试接口实现断点、单步执行等调试功能项目系统学习MSBuild实现多文件项目管理WPF设计器将技术扩展到更现代的WPF界面框架跨平台支持基于Avalonia或MAUI实现跨平台的可视化设计器这个玩具项目的完整代码已经展示了核心技术路径你可以在此基础上继续扩展功能。建议从添加新的控件类型开始逐步实现更复杂的功能如撤销重做、控件对齐、网格吸附等。
返回列表