WinForms 参数界面封装(二)
WinForms 参数界面封装二上一篇我先讲了背景。也就是为什么参数一多以后手工拖控件会越来越慢最后我开始把参数页面往封装和标准化上整理。这一篇继续往下讲。这次不再只说思路而是结合我现在项目里的代码讲一下这套参数框架到底是怎么拆的。我现在的做法核心不是“做一个万能页面”而是把参数页拆成几层让每层只做自己的事。这样后面新增参数、调整分组、改权限、加新页面时都会顺很多。一、我现在这套参数框架核心就是四层如果用最直白的话说我现在这套参数界面封装主要分成四层参数数据层参数描述层界面生成层配置读写层看起来名字很多但每层做的事情其实很清楚。二、第一层参数数据层只负责“参数有哪些”这一层就是参数类本身。比如我现在项目里就有这些cla_Paracla_Para_correctcla_productmecla_Para_handlecla_Para_knob这一层不负责界面也不负责控件布局它只负责一件事参数到底有哪些字段。像cla_Para里就是一堆真实项目参数比如[XmlRoot(cla_Para)] public class cla_Para { public int is_colseMES 1; public int is_gray1st 1; public int is_gray2st 1; public int is_door 1; public float jintaicurrent_min 0; public float jintaicurrent_max 0; public float workcurrent_min 0; public float workcurrent_max 0; public string boot ; public string software ; public string hardware ; }这一层我现在尽量保持简单。也就是说参数类里只放参数本身。不把页签、分组、显示名、权限这些东西混进来这样参数类会干净很多。三、第二层参数描述层负责“参数怎么显示”这一层是我觉得最关键的地方。因为手工做参数页时最乱的地方往往不是参数值本身而是这些信息总散在窗体里它放哪个页签它放哪个分组它显示什么名字它单位是什么它谁能改它是普通输入还是勾选框所以我把这些东西单独抽成了一层参数描述。核心类就是ParaItemMetaClaParaSchemaParaLayoutOptions1.ParaItemMeta描述一个参数该怎么显示这个类本质上就是“参数显示说明书”。代码里大概长这样public class ParaItemMeta { public string FieldName { get; set; } ; public string TabName { get; set; } 默认页; public string GroupName { get; set; } 默认组; public string DisplayName { get; set; } ; public string Unit { get; set; } ; public int Order { get; set; } 0; public ParaLayoutKind LayoutKind { get; set; } ParaLayoutKind.Pair; public decimal Min { get; set; } -999999M; public decimal Max { get; set; } 9999999M; public int Decimals { get; set; } 2; public decimal Increment { get; set; } 1M; public bool Visible { get; set; } true; public bool ReadOnly { get; set; } false; public int WriteLevel { get; set; } 3; }这一层出来以后一个参数就不只是“字段名”。它还带着页签分组显示名称单位顺序布局方式最小值、最大值、小数位写入权限这样后面界面生成时就不用再靠窗体代码到处写判断了。2.ClaParaSchema把整页参数描述组织起来如果说ParaItemMeta描述的是“一个参数”那ClaParaSchema描述的就是“整个参数页”。我现在是用它来集中配置哪些参数出现每个参数放哪每个参数怎么显示比如代码里已经是这种写法list.Add(Pair(nameof(cla_Para.R_min), TAB_P1, G_STATIC, 电阻下限, order 10, unit: Ω, writeLevel: 1)); list.Add(Pair(nameof(cla_Para.R_max), TAB_P1, G_STATIC, 电阻上限, order 10, unit: Ω, writeLevel: 1)); list.Add(Full(nameof(cla_Para.boot), TAB_P1, G_BANBEN, BOOT号, order 10, editorWidth: 360, writeLevel: 2)); list.Add(Check(nameof(cla_Para.a), TAB_SYS, G_SYS, 测试布尔参数, order 10, writeLevel: 0));我现在比较喜欢这种方式。因为以后如果要改显示名分组页签单位权限基本都集中在 schema 里处理不需要再去 Designer 里一点点翻控件。3.ParaLayoutOptions控制整体布局规则参数页除了“参数怎么显示”还有一个问题页面怎么排版。比如默认一行放几组标签宽度多少编辑框宽度多少单位宽度多少某个分组要不要特殊处理所以我单独做了ParaLayoutOptions。大概是这样public class ParaLayoutOptions { public int DefaultPairCountPerRow { get; set; } 4; public int LabelWidth { get; set; } 180; public int EditorWidth { get; set; } 150; public int UnitWidth { get; set; } 50; public int RowGap { get; set; } 6; public int GroupPadding { get; set; } 10; public ListParaGroupLayout GroupLayouts { get; } new ListParaGroupLayout(); }这一层的价值是布局规则也从窗体里抽出来了。以后改排版不需要直接改一堆控件位置而是改布局配置。四、第三层界面生成层负责“把描述变成界面”前两层做好以后才轮到真正的界面生成。这一层的核心类就是DynamicParaEditorT这个类是整套参数封装里最像“发动机”的部分。它做的事情其实很明确读取参数类字段读取 schema 描述自动生成页签自动生成分组自动生成控件建立字段和控件映射支持对象加载和回写主入口大概是这样public void Build(TabControl tabControl) { if (tabControl null) return; tabControl.TabPages.Clear(); _editorMap.Clear(); ListParaItemMeta visibleItems _schema .Where(x x.Visible) .ToList(); Liststring tabNames visibleItems .Select(x x.TabName) .Where(x !string.IsNullOrWhiteSpace(x)) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); foreach (string tabName in tabNames) { // 创建页签、分组并生成控件 } }也就是说我现在新建一个参数页不再是先手工拖控件而是定义参数类写 schema写布局规则调用Build()自动生成这样做最直接的好处就是把大量重复做参数页的时间省下来了。五、这一层里我最看重的不是“自动生成”而是“统一入口”很多人一看到这种写法第一反应是“哦就是自动生成界面。”但我自己现在更看重的是另一点界面生成终于有统一入口了。以前每个参数页都自己写一套问题很多命名风格不一致分组方式不一致控件读写方式不一致权限判断到处都是后期维护特别累现在统一走DynamicParaEditorT以后这些动作开始变成标准动作了。这其实比“自动生成”本身更值钱。六、第四层配置读写层负责“参数怎么保存和恢复”参数页如果只是显示出来还不够。后面还要解决这些问题参数怎么保存参数怎么读取文件损坏怎么办并发读写怎么办默认配置怎么恢复所以我把配置读写也单独做了一层XmlConfigStoreT这个类不是简单的 XML 序列化而是顺手把实际项目里容易遇到的问题也考虑进去了。类头上我写得很直接/// 1. XML 序列化/反序列化 /// 2. 同文件互斥锁 /// 3. .tmp 临时文件写入 /// 4. .bak 备份文件 /// 5. 主文件/备份失败时自动重建默认文件 public class XmlConfigStoreT where T : new()这一层我觉得很有必要。因为项目里参数文件这类东西最怕的不是“不会写”而是写到一半断了文件坏了多线程同时碰到了配置丢了以后现场没法跑所以我后来会觉得参数界面封装不只是界面问题。参数读写这件事也必须一起做稳。七、最后一层窗体调用层只负责“把这些层接起来”前面这些层拆开以后窗体层反而会简单很多。像我现在窗体加载时流程已经比较清楚private void frmparamtest_Load(object sender, EventArgs e) { string path Path.Combine(Application.StartupPath, Products, cla_Para.xml); _store new XmlConfigStorecla_Para(path, () new cla_Para()); BuildEditor(); LoadPara(); }构建编辑器也很直接private void BuildEditor() { _editor new DynamicParaEditorcla_Para( ClaParaSchema.Create(), ClaParaSchema.CreateLayout(), GetCurrentPermission()); _editor.Build(tabControl1); }保存时也很清楚_editor.SaveToObject(_para); bool ok _store.Write(_para, out err);这一层我现在尽量让它保持轻。也就是说窗体不要再去负责具体布局控件创建字段映射配置读写细节窗体只负责把前面几层接起来。这样后面新做一个参数页时窗体代码就不会越来越重。八、我现在最满意的一点这套结构已经不只服务一个页面了如果一套封装只在一个地方用过一次那它更像是“重写了一版代码”。但我现在这套不是。从前面也能看到我现在已经把同样的思路用在了基础参数修正参数配方参数也就是说这套拆法已经不是只服务一个窗体了。这也是我觉得它值得继续写下去的原因。因为这说明它不是概念而是真正开始复用了。九、这篇先总结一下这一篇我主要想讲清楚一件事我的参数框架不是一个类而是分层拆出来的一套结构。现在这套参数界面封装我是这样拆的参数数据层负责参数有哪些。参数描述层负责参数怎么显示。界面生成层负责把描述变成真正的参数页。配置读写层负责把参数保存和恢复做稳。窗体调用层负责把前面这些层接起来。我现在越来越觉得这种拆法的价值不只是代码看起来更清楚。而是后面会带来很直接的收益新建参数页更快新增参数更快改分组和显示名更方便保存读取更统一权限控制更集中后期维护更轻下一篇我准备继续往下写新增一个参数页面我现在是怎么接进去的。也就是从新建参数类配 schema配布局生成界面接保存和读取把整个流程再走一遍。