ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Winform可视化表单设计器:Smart.FormDesigner与动态表单实现

Winform可视化表单设计器:Smart.FormDesigner与动态表单实现 简介Smart.FormDesigner 是一款面向计算机、电子信息工程等专业本科生的 WinForm 表单可视化设计工具专为课程设计、期末大作业及毕业设计场景打造解决 GUI 快速原型构建与 .NET C# 实践能力提升的双重需求。资源包共 47 个文件含 12 个核心 C# 源码文件如 MainForm.cs、CustomForm.cs、PropertyWindow.cs 等、6 个 .resx 本地化资源、22 张操作图标 PNG涵盖对齐、复制、预览、撤销等 16×16 工具栏按钮以及 .sln 解决方案、.csproj 项目配置、LICENSE 和 README.md 等完整工程结构文件总大小仅 102KB轻量易读。已有 79 人学习下载。读者可直接运行调试完整可交互表单设计器深入理解控件拖拽逻辑、属性面板动态绑定、设计器文档序列化、事件代码注入等关键机制源码结构清晰分层涵盖 Toolbox、PropertyGrid、Canvas 渲染三大模块是掌握 WinForm 自定义设计器开发范式的优质实践样本。1. 项目整体设计与核心思路拆解1.1 为什么需要自定义表单设计器而不是直接拖控件先聊一个实际场景做毕设或者课设的时候很多同学做的是“信息管理系统”比如学生选课系统、员工考勤系统、设备巡检系统。这类系统有个共同麻烦表单字段老变。今天老师说要加一个“籍贯”字段明天说“家庭住址”也要录进去后天又说“备注”要支持多行。你要是每个页面都手动拖一次控件、改一次布局、调一次属性那真的会把人逼疯。Smart.FormDesigner 解决的就是这个问题。它是一个跑在 Winform 里的可视化表单设计器组件你可以把它理解成“把 Visual Studio 的设计器搬到了你的软件里”。用上它之后管理员或者你自己可以在程序运行期间直接拖拽控件、调整布局、改属性的方式设计表单然后把设计好的表单保存成 XML 或者 JSON。真正的业务界面在启动时根据这个配置动态生成字段加加减减的事情根本不用重新编译程序。这个项目选 Winform 而不是 WPF其实非常聪明。Winform 的设计时机制DesignSurface本来就支持拖拽和属性编辑Smart.FormDesigner 本质上就是把这套机制以一种比较优雅的方式暴露出来了。它底层用了 .NET 的反射、设计器服务DesignerHost和组件模型TypeDescriptor这套东西封装成最终用户也能操作的“可视化设计器”。对毕设来说这天然就是一个很好的技术展示点。1.2 整个项目的模块构成从设计到运行的两层架构如果要看懂 Smart.FormDesigner你得抓住两个核心概念设计时Design-Time和运行时Run-Time。设计时就是用户拖控件、调属性的状态。Smart.FormDesigner 提供FormDesigner控件你把它丢进一个窗体它会自动提供一个带标尺、带网格、带对齐线的工作区。左边放一个Toolbox工具箱里面列出可用的控件类型比如 TextBox、ComboBox、CheckBox、DateTimePicker、自定义的业务控件右边放一个PropertyGrid属性面板用来编辑当前选中控件的属性。运行时就是表单真正展示给最终用户填写的状态。表单设计完成之后Smart.FormDesigner 会把控件信息序列化成 XML 或者自定义格式。运行时读取这份配置通过反射逐条创建控件并设置属性值然后呈现到界面上。这就实现了“同一套代码表单内容完全由配置决定”的效果。在这个架构里有一个很关键的角色叫ControlExtender它负责给控件附加元数据比如字段名、绑定数据源的属性、校验规则等。简单说光有一个 TextBox 不够你还得告诉运行时这个 TextBox 对应数据库的哪个列。这些额外信息就放在扩展属性里跟着 XML 一起保存。如果你拿它做毕设我的建议是讲清楚这条链路配置化数据结构 → 可视化设计 → 运行时反射渲染 → 数据采集与存储。整套逻辑讲下来答辩老师基本挑不出大问题。2. 核心细节解析与实操要点2.1 设计器的实现原理DesignSurface 与 DesignerHostSmart.FormDesigner 能实现拖拽设计核心不是自己造轮子而是用了 .NET Framework 里一套很重要的组件DesignSurface和DesignerHost。这两个类平时你根本碰不到因为它们是 Visual Studio 设计器内部用的基础设施。简单解释一下DesignSurface是一个“设计画布”它管理这个画布上的所有组件DesignerHost是组件的容器负责创建组件、管理组件之间的事务比如撤销/重做、提供设计时服务。Smart.FormDesigner 在FormDesigner控件内部实例化了这样一个设计环境然后把鼠标事件和拖拽行为与设计环境关联起来。这种方案的优点很明确你不需要自己去实现控件移动、属性同步、布局计算这些底层逻辑。你只需要监听设计器的服务接口在合适的时候读取设计结果。缺点也很明显这套设计时体系里很多接口没有完整的文档大部分情况下你得靠反射和尝试才能搞明白某个服务怎么用。如果你遇到“控件拖进去了但属性面板不刷新”这种问题大概率就是设计时服务的引入时机不对。所以我的建议是不要试图深入修改 Smart.FormDesigner 的底层设计器逻辑除非你实在有强烈的自定义需求。绝大多数毕设项目只需要学会调用它提供的公开接口。2.2 控件工具箱注册内置控件与自定义控件工具箱是表单设计器最直观的部分。Smart.FormDesigner 的Toolbox通过列表展示控件类型你拖拽列表项到设计区域即可添加控件。默认支持的基础控件一般包括文本框TextBox下拉框ComboBox复选框CheckBox单选按钮RadioButton日期选择DateTimePicker列表ListBox / CheckedListBox按钮Button表格DataGridView如果你只是使用默认控件那整个过程很顺。但真正让这个项目出彩的是自定义控件注册。比如你做一个学生管理系统你希望表单里可以直接放置一个“专业选择器”或“班级下拉列表”这需要通过反射把控件类型注册到工具箱。核心代码如下// 注册自定义控件到工具箱 var toolbox new Toolbox(); Assembly asm Assembly.LoadFrom(MyControls.dll); toolbox.AddControlsFromAssembly(asm);要注意一点自定义控件必须有一个无参构造函数并且控件类要标上[ToolboxItem(true)]特性否则工具箱列表里不会显示。这是我实际使用中遇到比较多的坑很多同学自定义控件写得挺好但忘了加特性导致在设计器里死活找不到。2.3 属性面板与扩展属性PropertyGrid 的正确使用姿势Smart.FormDesigner 的右侧属性面板直接复用了 Winform 自带的PropertyGrid。这个控件有个特点它默认显示的是控件类里标记[Browsable(true)]的属性。如果你想在设计器里显示额外信息比如“对应数据库字段名”你有两种选择。第一种是直接给属性的定义处加上特性标记[Browsable(true)] [Category(数据绑定)] [Description(设置该控件绑定的数据字段名)] public string FieldName { get; set; }第二种方式是使用TypeDescriptionProvider动态添加这种方案更灵活但代码复杂度略高。在毕设阶段我建议直接用第一种它简单直观而且效果完全够用。还有一个经常被问到的问题“PropertyGrid 只能查看不能修改怎么解决”。这通常发生在两种情况下一是属性本身没有 public set二是控件类没有标记[TypeConverter]。如果属性只是只读只有 getPropertyGrid 会显示该项但禁用编辑。我建议在自定义表单元素的扩展属性上统一加上[Browsable(true)]和[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]前者保证可见后者保证保存布局时能正确序列化。2.4 序列化与运行时渲染的关键逻辑保存表单配置的格式选择上Smart.FormDesigner 支持 XML。早期版本也支持二进制但 XML 在可读性、版本兼容性上有绝对优势。做毕设的时候你说“采用 XML 保存表单结构”本身就比二进制更便于老师审查代码和验证结果。保存时核心是把设计器里的控件树遍历一遍提取每个控件的类型、位置、尺寸、文本、名称、扩展属性然后序列化为 XML。运行时解析 XML 再反射创建控件。这里有三个关键点类型标识XML 里存的类型名必须是程序集限定名或者至少包含命名空间。反射时用Type.GetType(typeName)才能找得到。事件绑定运行时动态创建的控件默认不绑定任何事件。如果你的表单里有“保存”按钮必须在代码里手动把这个按钮的 Click 事件挂到指定方法上。Smart.FormDesigner 对事件的处理不算完善这个你要自己补。嵌套布局如果动态生成的容器如 GroupBox、TabControl内部的控件也想保持相对位置序列化和反序列化时都需要递归处理不能只保存顶层控件。3. 实操过程与核心环节实现3.1 快速接入 Smart.FormDesigner 到你的 Winform 项目不管你是做毕设还是课设第一步都是把 Smart.FormDesigner 的源码或 DLL 引入项目。项目文件解压以后通常会有两个目录一个是源代码工程一个是编译好的程序集。直接引用 DLL 比把源码项目全部编译进来要省事得多因为源码依赖的编译顺序和命名空间容易出问题。操作步骤如下新建一个 .NET Framework 4.6 以上版本的 Winform 项目如果用 .NET 6 Winform 也可以跑但需要确认 DLL 是否重新编译过这点后面会讲。在解决方案里右键“引用”→“添加引用”→“浏览”找到 Smart.FormDesigner.dll。工具栏上右键“选择项”→“浏览”同样选中 Smart.FormDesigner.dll然后你会发现工具箱里多出了FormDesigner和Toolbox两个控件。把一个FormDesigner拖到主窗体左侧一个Toolbox放到左侧停靠面板一个PropertyGrid放到右侧。做完这一步你其实已经有一个“能拖控件”的简易设计器了因为没有做控件之间的事件关联所以拖过来但是属性面板不会联动。下面这步很关键需要在窗体的Load事件里把三者绑定在一起private void MainForm_Load(object sender, EventArgs e) { // 初始化设计器 formDesigner1.DesignerLoaded (s, args) { // 把工具箱和属性面板关联到设计器 formDesigner1.SetToolbox(toolbox1); formDesigner1.SetPropertyGrid(propertyGrid1); }; // 启动设计器会创建一个空白的设计画布 formDesigner1.CreateNewDesigner(); }CreateNewDesigner这个方法会创建一个新的设计宿主实例。注意调用时机一定要在窗体显示之后或者Load里不要在构造函数里调因为在构造函数阶段控件还没有完成句柄创建设计器初始化会失败。3.2 把多个设计器联动Toolbox、PropertyGrid 和 FormDesigner很多新手把控件拖到窗体上以后就直接运行发现工具箱里的控件拖不进去或者属性面板不显示选中项。原因就是没有正确调用SetToolbox和SetPropertyGrid。这两个方法本质上是给设计器注入外部服务设计器内部会把服务连接到当前的设计环境中。如果你有多个表单设计器实例比如实现了多标签页编辑每个 FormDesigner 都要重新调用一次SetToolbox和SetPropertyGrid。这里有个容易踩的坑SetToolbox传入的 Toolbox 如果是同一个实例切换设计器时工具栏内容可能不会自动刷新。在毕设场景里一般一个设计器就够了但你如果做多标签建议每个标签页重新创建 Toolbox 实例。为了确认联动正常你可以在DesignerLoaded事件里手动往设计器上添加一个测试控件formDesigner1.EnsureVisible(); // 使用设计器服务创建控件 var host formDesigner1.GetDesignerHost(); if (host ! null) { var textBox host.CreateComponent(typeof(TextBox), txtTest) as TextBox; textBox.Location new Point(20, 20); textBox.Size new Size(150, 25); textBox.Text 测试文本框; }这段代码一旦运行起来如果右侧属性面板立刻显示了 “txtTest” 的属性那说明联动成功了。3.3 保存表单配置序列化为 XML 的完整流程表单设计的最终产出是一份结构化的配置。Smart.FormDesigner 提供了保存和加载接口常见调用如下// 保存 string xml formDesigner1.SaveLayout(); File.WriteAllText(formConfig.xml, xml); // 加载 formDesigner1.LoadLayout(File.ReadAllText(formConfig.xml));SaveLayout返回的字符串里包含了控件类型、位置、尺寸、扩展属性等信息。我建议拿到这个 XML 以后打开看一眼结构做到心里有数。通常它的根节点类似Root或Control每个子节点对应一个控件。保存完 XML 之后下一步是你自己的业务逻辑把 XML 存到数据库、保存到文件还是放到服务器。在毕设系统里一般做法是建一张FormConfig表字段包括FormId、FormName、FormXml、CreateTime。这样设计表单和管理表单记录就能对应起来。注意如果你在运行时动态修改过控件的属性保存布局时请确保设计器当前没有处于“设计状态修改未提交”的临时状态。建议在保存前调用formDesigner1.FlushDesigner()强制刷新如果有这个方法。没有的话就把焦点先移到非设计区让悬停的控件落定。3.4 运行时根据 XML 动态生成表单这一部分是整个项目最有技术含金量的地方。思路是程序启动时读取某个表单的 XML 配置然后反射创建控件、设置属性、布局到 Panel 里最终呈现给用户。一个精简的运行时渲染代码如下public void RenderForm(Panel targetPanel, string xml) { targetPanel.Controls.Clear(); XDocument doc XDocument.Parse(xml); foreach (var element in doc.Root.Elements(Control)) { string typeName element.Attribute(Type).Value; string name element.Attribute(Name).Value; int x int.Parse(element.Attribute(X).Value); int y int.Parse(element.Attribute(Y).Value); int w int.Parse(element.Attribute(Width).Value); int h int.Parse(element.Attribute(Height).Value); Type controlType Type.GetType(typeName); if (controlType null) continue; Control ctrl Activator.CreateInstance(controlType) as Control; ctrl.Name name; ctrl.Location new Point(x, y); ctrl.Size new Size(w, h); // 设置扩展属性比如绑定字段名 string fieldName element.Attribute(FieldName)?.Value; if (!string.IsNullOrEmpty(fieldName) ctrl is TextBox txt) { txt.Tag fieldName; // 暂时存到 Tag后面取值用 } targetPanel.Controls.Add(ctrl); } }上面这个例子只处理了位置和大小的恢复实际项目你还得恢复字体、颜色、是否可见、数据源等属性。一个更省事的方案是写一个通用的“属性组”循环把所有保存在 XML 里的属性统一反射赋值。这样后续加属性不用改渲染代码。foreach (var attr in element.Elements(Property)) { string propName attr.Attribute(Name).Value; string propValue attr.Attribute(Value).Value; var prop controlType.GetProperty(propName); if (prop ! null prop.CanWrite) { object convertedValue Convert.ChangeType(propValue, prop.PropertyType); prop.SetValue(ctrl, convertedValue); } }这个通用赋值逻辑能极大提升代码简洁度同时给答辩演示时加分你只要改配置文件表单按钮、下拉框、日期选择器全都会自动变化。3.5 运行时采集表单数据从动态控件取值运行时的表单渲染出来只完成了一半更重要是收集用户填写的数据保存到数据库。因为控件是动态创建的编译期你根本不知道会有哪些控件所以取值必须走遍历。我常用的姿势是遍历目标 Panel 下的所有控件根据控件的Tag存字段名或者Name如果没有扩展 Tag作为数据库列名再根据控件类型取不同的属性值。public Dictionarystring, object CollectFormData(Panel targetPanel) { var data new Dictionarystring, object(); foreach (Control ctrl in targetPanel.Controls) { if (ctrl.Tag null) continue; string fieldName ctrl.Tag.ToString(); object value null; switch (ctrl) { case TextBox txt: value txt.Text; break; case ComboBox cbo: value cbo.SelectedItem?.ToString(); break; case CheckBox chk: value chk.Checked; break; case DateTimePicker dtp: value dtp.Value; break; case NumericUpDown num: value num.Value; break; } data[fieldName] value ?? DBNull.Value; } return data; }取出来的字典可以直接拼插入 SQL 的参数化语句也可以交给 ORM如 SqlSugar、EF Core插入数据库。4. 常见问题与排查技巧实录4.1 运行时空白、控件不显示的常见原因排查表我见过不少同学在毕设群里发这类截图设计器里拖得好好的表单运行时 Panel 里什么都没有。这个问题百分之八十出在类型加载上。现象可能原因解决办法运行时 Panel 空白无任何控件Type.GetType只传了类名缺少程序集信息使用完整的程序集限定名或手动扫描已加载程序集设计器拖拽控件无响应设计器未调用CreateNewDesigner在Load事件里调用一次控件创建了但位置全在 0,0没有恢复Location属性检查 XML 是否保存了位置信息属性面板不显示选中项SetPropertyGrid没调用或调用时机过早在DesignerLoaded事件里调用自定义控件在工具箱不可见缺少无参构造或[ToolboxItem(true)]补上特性并确认构造函数运行时控件大小不对忽略了AutoSize属性冲突对 TextBox 等关闭 AutoSize 或统一设置 Size4.2 .NET 版本兼容性Framework 与 .NET Core/6/7/8 的坑很多人会拿现成的 Smart.FormDesigner 源码去跑 .NET 6 或 .NET 8 的 Winform 项目结果发现编译不通过或者运行崩了。因为 Smart.FormDesigner 原本是基于 .NET Framework 的里面的设计器服务接口在某些版本的 .NET Core/5 中行为有差异。尤其是DesignSurface和DesignerHost这套东西在 .NET Core 移植过程中有过不少坑。我的实际建议是如果你是课设优先用 .NET Framework 4.7.2 或 4.8这是最稳妥的路线。如果你是毕设且想用新版本展示比如 .NET 8 Winform那你需要重新编译 Smart.FormDesigner 源码而且要做好调试设计器服务返回异常的心里准备。不要花大量时间在这上面先跑起来最重要。如果编译报错提示类似“DesignSurface 不支持该操作”可以尝试用WindowsFormsDesigner的替代方案或者转向 WPF 的DependencyObject方案但那种工作量大。正常情况下.NET Framework 下运行没问题。4.3 布局错乱与缩放问题的处理设计器画布上拖出来的控件位置是绝对坐标Point。但最终展示表单的容器大小可能和设计画布不一致。如果你的表单要嵌入到不同分辨率的屏幕上直接按绝对坐标放置会出现“控件挤在一起”或“右侧大片空白”的问题。几种方案供你选固定表单设计尺寸设计时就固定一个分辨率比如 1024x768运行时不拉伸缩放跟随窗体。这个最简单适合课设。比例缩放运行时按容器宽度和设计宽度的比例对每个控件的 Location 和 Size 做乘数换算。代码量不大效果尚可。布局容器适配把动态表单放进TableLayoutPanel或FlowLayoutPanel但这对自由拖拽布局支持不好尤其在设计器里你很难预测运行时流向。我个人习惯用第二种因为设计器提供的就是自由布局直接按比例缩放不会破坏原有的设计意图。代码示例float scaleX (float)targetPanel.Width / designerWidth; float scaleY (float)targetPanel.Height / designerHeight; foreach (Control ctrl in targetPanel.Controls) { ctrl.Location new Point( (int)(ctrl.Left * scaleX), (int)(ctrl.Top * scaleY) ); ctrl.Size new Size( (int)(ctrl.Width * scaleX), (int)(ctrl.Height * scaleY) ); }这个方案只适合线性布局如果控件之间有复杂的锚定关系Anchor缩放后可能重叠测试的时候多换几个尺寸看看。4.4 事件绑定与撤销重做功能缺失Smart.FormDesigner 的设计器支持拖拽但“事件绑定”通常不会帮你自动处理。比如你拖了一个按钮进表单希望运行时点击弹出提示那你必须在运行时为这个按钮挂逻辑。比较优雅的方案是在 XML 里为控件配置一个“事件名”运行时用反射查找事件并绑定。例如在序列化时保存一个EventName属性运行时这样绑定Button btn ctrl as Button; if (btn ! null) { string eventName element.Attribute(ClickEvent)?.Value; if (!string.IsNullOrEmpty(eventName)) { MethodInfo handler this.GetType().GetMethod(eventName); if (handler ! null) { var delegateType btn.Click.GetType(); Delegate del Delegate.CreateDelegate(delegateType, this, handler); btn.Click (EventHandler)del; } } }这个思路解决了动态表单控件无法复用固定业务代码的问题。不过要提醒你Delegate.CreateDelegate对方法签名要求严格事件处理器必须是void xxx(object sender, EventArgs e)的形式否则运行时抛异常。关于撤销/重做Smart.FormDesigner 本身支持基本的设计操作撤销但和你业务数据的撤销无关。如果你要做“表单版本管理”建议每次保存时把 XML 存成历史版本并提供对比而不是在控件级做撤销成本会小很多。5. 毕设/课设的包装与扩展建议5.1 你的论文可以从哪些角度切入这个项目对毕设非常友好因为它天然具备了“理论 实践”双保险。你可以这样设计论文的整体思路第一章绪论讲表单数据变化的业务痛点介绍低代码/表单设计器的发展背景。第二章需求分析业务场景是管理员需要动态配置录入表单不需要重新编译程序。第三章系统设计整体架构图、数据表设计表单配置表、表单实例表、数据存储表、用户表。第四章系统实现重点描述可视化设计器的集成、XML序列化、运行时控件反射创建、数据采集入库。第五章系统测试给出不同表单配置的运行效果对比性能测试比如 100 个控件同时创建耗时。第六章总结与展望可以把“支持插件式控件”“支持跨平台 Web 表单设计器”作为未来方向。这个架构非常标准而且每个章节都有实实在在的内容可以填。尤其“运行时反射创建”这一个点就够你写一小节了。5.2 给项目增加亮点的三个扩展方向如果你想让项目在答辩时更有亮点可以考虑往下面三个方向任选其一扩展第一个方向是控件数据源绑定。传统表单设计器只管展示不管数据。你可以给下拉框配置一个“数据源类型”比如从数据库某个表读取运行时自动拉取。这一步做出来你的表单就从“纯静态录入界面”升级成了“数据感知表单”。第二个方向是表单实例与表单配置分离。一张表单配置可以被多个业务单据引用每条单据存一份填写的值。说得直白点你做出了一套简化版的“低代码数据收集引擎”。哪怕只有一个 TextBox 的动态表单只要配合数据库表做序列化存储整体系统就非常完整了。第三个方向是校验规则的配置化。在扩展属性里加入“是否必填”“最大长度”“正则表达式”等规则运行时对动态表单统一做验证。这个扩展点非常出彩因为它在“可视化设计”和“业务数据质量”之间搭了一座桥。用户在设计器里把“邮箱字段”设成必填并且格式校验运行时如果填错直接红框提示。5.3 答辩时容易被追问的问题答辩前我建议你把下面几个问题提前想清楚因为这是老师最喜欢问的为什么选择 XML 而不直接存数据库表结构你可以回答XML 保存的是“界面布局结构”而不是业务数据本身。业务数据仍然存储在标准的数据表里一个字段对应一列或者用 EAV 模型存键值对。XML 的作用是告诉程序界面长什么样两者职责分离。动态创建控件有安全性问题吗如果允许用户传入任意类型名会存在反序列化漏洞的风险。所以在运行时渲染时你需要校验类型白名单比如说只允许System.Windows.Forms命名空间下的控件和自定义业务控件。你可以扩展开发一个基于反射的“类型名验证器”这段代码在答辩里提出来会显得很专业。性能问题怎么解决动态反射创建 100 个控件会有一定的性能开销。你可以用 Stopwatch 写出一个简单的性能测试结果比如“100 个控件耗时 300ms”对比直接硬编码创建的差异并说明这个量级对表单使用场景影响极小。老师看到你有量化数据好感度直接上升。5.4 一些实在的开发建议最后说几个我实际写代码过程中的体会第一先用示例工程跑通再改自己的业务。Smart.FormDesigner 源码里通常带一个 Demo 项目先把 Demo 跑起来搞清楚哪些功能是开箱即用的哪些是残缺的再动手集成到自己的系统里。不要一上来就闷头把代码拖进自己的项目否则你根本分不清问题是出在源码还是你自己的代码。第二统一控件命名规范。动态创建控件的 Name 最好按txt_字段名、cbo_字段名、dtp_字段名这种规则来命名这样采集数据的时候只靠前缀就能判断控件类型不用反射查类名。这个习惯能在后面省很多事。第三千万别忽视 XML 的转义问题。如果某个控件的 Text 属性包含尖括号或者 符号序列化时一定要保证正确转义否则加载时解析 XML 会直接报错。这个看似小问题实际排查起来非常费时间。第四做一个“控件预览”模式。设计器里拖出来的控件可能和运行时效果有差异提前做一个“预览表单”按钮Load 一份 XML 到运行时 Panel 里展示这样你在答辩现场演示时不会出现“设计器和运行界面长得不一样”的尴尬。我把上面这个方法用在自己做的动态表单项目里效果出奇的好。平时给使用者改字段只需要 1 分钟而以前改一次代码加一次发布至少 10 分钟起步。如果你也需要做一个能让用户自己配置录入界面的 Winform 系统Smart.FormDesigner 这套思路真的值得好好研究一遍。本文还有配套的精品资源点击获取
返回列表